Merge lp:~jtv/maas-test/ssh-key into lp:maas-test
- ssh-key
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | 23 |
Merged at revision: | 16 |
Proposed branch: | lp:~jtv/maas-test/ssh-key |
Merge into: | lp:maas-test |
Diff against target: |
583 lines (+277/-68) 4 files modified
maastest/kvmfixture.py (+50/-14) maastest/tests/test_kvmfixture.py (+206/-54) maastest/tests/test_utils.py (+14/-0) maastest/utils.py (+7/-0) |
To merge this branch: | bzr merge lp:~jtv/maas-test/ssh-key |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | Approve | ||
Review via email: mp+194488@code.launchpad.net |
Commit message
Set up temporary, passphrase-less ssh key pair for communicating with the virtual machine.
Description of the change
The keypair gets created during KVMFixture.setUp(). After I was done with the tests, I thought implementation would be easy. But I couldn't get my tests to pass! Turns out the tests' setUp() was patching out run_command, and so ssh-keygen was never actually being run. I had to make that patch explicit, in only the tests for which it was appropriate.
Some other tests were a bit brittle, expecting too much detail about command lines being run, so that small changes required updates to seemingly tests. I made the tests look for more specific changes, leaving out the irrelevant. It's not as easy, and sometimes I think test output will suffer, but hopefully that's still better than a future where we mindlessly update tests to match whatever actually happens.
Along the way, I also remove some TODOs that no longer match the evolution of the design. And a few tests were unnecessarily nested in "with:" blocks. I don't like unnecessary nesting. Instead, I keep just the necessary data-gathering inside the nested block, and do the testing at the base indentation level.
Jeroen
- 18. By Jeroen T. Vermeulen
-
Merge trunk, resolve conflicts.
Raphaël Badin (rvb) wrote : | # |
Raphaël Badin (rvb) wrote : | # |
One more thing: I think it would be better to put the keys in a fixed place like ~/.maas-
Jeroen T. Vermeulen (jtv) wrote : | # |
> - I'm really not fond of find_matching_
> with. I think you're relaxing the assertions done in the tests too much with
> this. In my opinion, it's better to keep checking the complete commands that
> are run. It's a sort of double-entry bookkeeping system that is there to
> guarantee that any change to the code is voluntary and I don't think the tests
> are brittle at all.
I see that there's some missing test coverage on setUp as a whole, and I'll fix that. But there is no refactoring here that went with find_matching_call — it's entirely the other way around. I first ran into the problem of test brittleness. Several tests really were going to break because of unrelated changes to command lines. The choice was one of increasing it or decreasing it, and I first specified find_matching_call to avoid increasing it. I only wrote the actual function once I saw that it allowed me to solve that problem. Adding another option to the ssh command line will now break one test, but no more.
Tests are like double-entry bookkeeping, but double-entry bookkeeping is not writing down the same transaction in multiple places like these tests were doing. Rather, it's a system of recording complementary aspects of a transaction. Failure of the two records to match up is a crucial signal that something went wrong. False positives, and the habit of updating the tests to match the code, degrade the value of that signal. That's how double-entry bookkeeping devolves towards forms-in-
Jeroen T. Vermeulen (jtv) wrote : | # |
> One more thing: I think it would be better to put the keys in a fixed place
> like ~/.maas-
> useful to be able to manually log into the VM while maas-test is running (and
> actually, I've used that a lot already when working on the networking stuff)
> and that will be more easy to do if the private ssh key is in a fixed place
> rather than somewhere in /tmp/.
These are fleeting, single-use keys, so keeping them in a permanent location does not make sense to me from a lifetime point of view. What you need for debugging is *access* to the keys, not a fixed location. There are other ways to skin that cat, such as logging the key we use.
Julian Edwards (julian-edwards) wrote : | # |
First comment - I don't think this key should be fleeting, I agree with Raphaël and his reasoning here. Generate it on demand if it doesn't exist and stick it in ~/.maas-test.
Julian Edwards (julian-edwards) wrote : | # |
43 + def get_ssh_
This is returning a path to a key, not a key per se. It's worth renaming to avoid confusion.
128 + self.patch(
You don't need the third param here, it defaults to that.
233 + self.assertIsNo
234 + self.find_
Get rid of this and find_matching_
236 + self.assertEqual(
237 + [
238 + mock.call(
239 + ],
240 + mock_make_
And here! And a few other places in the code.
- 19. By Jeroen T. Vermeulen
-
Merge trunk, resolve conflicts. Not trivial.
Jeroen T. Vermeulen (jtv) wrote : | # |
> 43 + def get_ssh_
>
> This is returning a path to a key, not a key per se. It's worth renaming to
> avoid confusion.
OK.
> 128 + self.patch(
>
> You don't need the third param here, it defaults to that.
In MAAS it does. Here, it doesn't and so I do.
> 233 + self.assertIsNo
> 234 + self.find_
>
> Get rid of this and find_matching_
> assert_any_call().
Doesn't work — mock.ANY can be useful for pattern-matching on the call itself, but not within the list that is being passed here. :(
- 20. By Jeroen T. Vermeulen
-
Review change: rename ssh_{public,
private} _key to say they're filenames. - 21. By Jeroen T. Vermeulen
-
Tests for review change: generate ssh keys in home directory, and keep them persistent. Tests: 2 failures.
- 22. By Jeroen T. Vermeulen
-
Move ssh keys into home directory. Satisfy tests.
- 23. By Jeroen T. Vermeulen
-
Test read_file.
Jeroen T. Vermeulen (jtv) wrote : | # |
The branch has grown a bit. I had to remove SSH_COMMAND along the way. It still broke with the changes to the ssh command. Instead, I added a method that generates the ssh command line.
The ssh keys are in a fixed location now, so it would be possible to go back to using SSH_COMMAND with some trickery, but even then we'd have the ugly situation that it couldn't contain the full command line — so tests still had to extend that list for themselves. What we have now neatly eliminates that intimacy between the tests and the main code.
Graham Binns (gmb) wrote : | # |
After all the conversation heretofore, I rather feel like my job is an easy one :). This is a good branch; nice work Jeroen.
Preview Diff
1 | === modified file 'maastest/kvmfixture.py' |
2 | --- maastest/kvmfixture.py 2013-11-08 15:33:37 +0000 |
3 | +++ maastest/kvmfixture.py 2013-11-11 10:35:06 +0000 |
4 | @@ -17,6 +17,7 @@ |
5 | |
6 | import itertools |
7 | import logging |
8 | +import os.path |
9 | from pipes import quote |
10 | import textwrap |
11 | from time import sleep |
12 | @@ -28,12 +29,6 @@ |
13 | from testtools.content import text_content |
14 | |
15 | |
16 | -SSH_COMMAND = [ |
17 | - "ssh", "-o", "LogLevel=quiet", "-o", |
18 | - "UserKnownHostsFile=/dev/null", "-o", |
19 | - "StrictHostKeyChecking=no"] |
20 | - |
21 | - |
22 | class KVMFixture(Fixture): |
23 | """A fixture for a Kernel-based Virtual Machine. |
24 | |
25 | @@ -90,16 +85,16 @@ |
26 | assert direct_ip is not None, ( |
27 | "'direct_ip' cannot be empty if 'direct_interface' is not " |
28 | "provided") |
29 | - # TODO: add an option to prepare (and later reuse) an existing |
30 | - # KVM instance. |
31 | if name is None: |
32 | name = unicode(uuid1()) |
33 | self.name = name |
34 | + self.ssh_private_key_file = os.path.join( |
35 | + os.path.expanduser('~'), '.maas-test', 'vm_ssh_id_rsa') |
36 | |
37 | def setUp(self): |
38 | super(KVMFixture, self).setUp() |
39 | - # TODO: check that there is a ssh public/private key pair set up. |
40 | self.import_image() |
41 | + self.generate_ssh_key() |
42 | self.start() |
43 | self.configure() |
44 | self.addCleanup(self.destroy) |
45 | @@ -115,6 +110,10 @@ |
46 | ["uvt-kvm", "ip", self.name], check_call=True) |
47 | return ip_address.strip() |
48 | |
49 | + def get_ssh_public_key_file(self): |
50 | + """Return path to public SSH key file, for VM sshd configuration.""" |
51 | + return self.ssh_private_key_file + '.pub' |
52 | + |
53 | def import_image(self): |
54 | """Download the image required to create the virtual machine.""" |
55 | logging.info( |
56 | @@ -128,18 +127,36 @@ |
57 | "Done downloading KVM image for series=%s, arch=%s.", |
58 | self.series, self.architecture) |
59 | |
60 | + def generate_ssh_key(self): |
61 | + """Set up an ssh key pair for accessing the virtual machine. |
62 | + |
63 | + The key pair will be in a directory `~/.maas-test/`. If this did not |
64 | + exist yet, it will be created. |
65 | + """ |
66 | + key_dir = os.path.dirname(self.ssh_private_key_file) |
67 | + if not os.path.exists(key_dir): |
68 | + os.makedirs(key_dir, mode=0700) |
69 | + if not os.path.exists(self.ssh_private_key_file): |
70 | + run_command( |
71 | + [ |
72 | + 'ssh-keygen', |
73 | + '-t', 'rsa', |
74 | + '-f', self.ssh_private_key_file, |
75 | + '-N', '', |
76 | + ], |
77 | + check_call=True) |
78 | + |
79 | def start(self): |
80 | """Create and start the virtual machine. |
81 | |
82 | When this method returns, the VM is ready to be used.""" |
83 | - # TODO: check if already running (in case this is reusing an |
84 | - # existing instance). |
85 | logging.info( |
86 | "Creating virtual machine %s, arch=%s.", self.name, |
87 | self.architecture) |
88 | template = make_kvm_template(self.direct_interface) |
89 | run_command([ |
90 | "sudo", "uvt-kvm", "create", self.name, |
91 | + "--ssh-public-key-file=%s" % self.get_ssh_public_key_file(), |
92 | "arch=%s" % self.architecture, |
93 | "release=%s" % self.series, "--template", "-"], |
94 | input=template, check_call=True) |
95 | @@ -178,12 +195,31 @@ |
96 | ["sudo", "uvt-kvm", "destroy", self.name], check_call=True) |
97 | logging.info("Done destroying virtual machine %s.", self.name) |
98 | |
99 | + def _make_ssh_command(self, command_line): |
100 | + """Compose an `ssh` command line to execute `command_line` in the VM. |
101 | + |
102 | + :param command_line: Shell command to be executed on the virtual |
103 | + machine, in the form of a sequence of arguments (starting with the |
104 | + executable). |
105 | + :return: An `ssh` command line to execute `command_line` on the virtual |
106 | + machine, in the form of a list. |
107 | + """ |
108 | + remote_command_line = ' '.join(map(quote, command_line)) |
109 | + return [ |
110 | + "ssh", |
111 | + "-i", self.ssh_private_key_file, |
112 | + "-o", "LogLevel=quiet", |
113 | + "-o", "UserKnownHostsFile=/dev/null", |
114 | + "-o", "StrictHostKeyChecking=no", |
115 | + self.identity(), |
116 | + remote_command_line, |
117 | + ] |
118 | + |
119 | def run_command(self, args, input=None, check_call=False): |
120 | """Run the given command in the VM.""" |
121 | - cmd = " ".join(map(quote, args)) |
122 | - args = SSH_COMMAND + [self.identity(), cmd] |
123 | + args = self._make_ssh_command(args) |
124 | retcode, stdout, stderr = run_command( |
125 | - args, input=input, check_call=check_call) |
126 | + args, check_call=check_call, input=input) |
127 | count = next(self.command_count) |
128 | self.addDetail('cmd #%d' % count, text_content(' '.join(args))) |
129 | self.addDetail('cmd #%d retcode' % count, text_content(str(retcode))) |
130 | |
131 | === modified file 'maastest/tests/test_kvmfixture.py' |
132 | --- maastest/tests/test_kvmfixture.py 2013-11-08 14:26:53 +0000 |
133 | +++ maastest/tests/test_kvmfixture.py 2013-11-11 10:35:06 +0000 |
134 | @@ -12,25 +12,50 @@ |
135 | __metaclass__ = type |
136 | __all__ = [] |
137 | |
138 | +import os.path |
139 | +from random import randint |
140 | import textwrap |
141 | |
142 | import fixtures |
143 | from lxml import etree |
144 | from maastest import kvmfixture |
145 | +from maastest.utils import read_file |
146 | import mock |
147 | import netaddr |
148 | import testtools |
149 | -from testtools.content import text_content |
150 | +from testtools.matchers import ( |
151 | + FileContains, |
152 | + FileExists, |
153 | + HasPermissions, |
154 | + MatchesRegex, |
155 | + Not, |
156 | + StartsWith, |
157 | + ) |
158 | |
159 | |
160 | class TestKVMFixture(testtools.TestCase): |
161 | |
162 | def setUp(self): |
163 | super(TestKVMFixture, self).setUp() |
164 | + self.logger = self.useFixture(fixtures.FakeLogger()) |
165 | + self.patch(kvmfixture, 'sleep', mock.MagicMock()) |
166 | + # KVMFixture.generate_ssh_key() generates an ssh key in ~/.maas-test, |
167 | + # creating that directory if necessary. Set up a fake home directory |
168 | + # so it can do what it needs to. |
169 | + home_dir = self.useFixture(fixtures.TempDir()).path |
170 | + self.patch( |
171 | + os.path, 'expanduser', mock.MagicMock(return_value=home_dir)) |
172 | + |
173 | + def patch_run_command(self): |
174 | + """Patch out `kvmfixture` calls to `run_command`. |
175 | + |
176 | + Call this so your test doesn't accidentally make `KVMFixture` download |
177 | + machine images, ssh into it, etc. |
178 | + |
179 | + You can access the fake version as `self.mock_run_command`. |
180 | + """ |
181 | self.mock_run_command = mock.MagicMock() |
182 | - self.logger = self.useFixture(fixtures.FakeLogger()) |
183 | self.patch(kvmfixture, 'run_command', self.mock_run_command) |
184 | - self.patch(kvmfixture, 'sleep', mock.MagicMock()) |
185 | |
186 | def make_KVMFixture(self, series=None, architecture=None, name=None, |
187 | direct_interface=None, direct_network=None, |
188 | @@ -50,15 +75,39 @@ |
189 | fixture._details = {} |
190 | return fixture |
191 | |
192 | + def find_matching_call(self, command_prefix): |
193 | + """Find the first `run_command` call with a matching command line. |
194 | + |
195 | + This assumes that you have called `patch_run_command` first. |
196 | + |
197 | + :param command_prefix: A sequence of items on the command line. A |
198 | + call to `run_commanad` will match this when this forms a prefix of |
199 | + the executed command line. For instance, a `command_prefix` of |
200 | + `['ls']` will match a command line `['ls', '/tmp']` but not |
201 | + a command line `['sudo', 'ls', '/tmp']`. On the other hand, a |
202 | + prefix `['sudo', 'ls']` will match `['sudo', 'ls', '/tmp']` but |
203 | + not `['ls', '/tmp']`. |
204 | + :rtype: `mock.call` |
205 | + """ |
206 | + command_prefix = list(command_prefix) |
207 | + prefix_length = len(command_prefix) |
208 | + for invocation in self.mock_run_command.mock_calls: |
209 | + _, args, _ = invocation |
210 | + command_line = args[0] |
211 | + if command_line[:prefix_length] == command_prefix: |
212 | + return invocation |
213 | + return None |
214 | + |
215 | def test_init_sets_properties(self): |
216 | series = "test-series" |
217 | architecture = "test-arch" |
218 | - with self.make_KVMFixture(series, architecture) as fixture: |
219 | - self.assertEqual( |
220 | - (series, architecture, 36), |
221 | - (fixture.series, fixture.architecture, len(fixture.name))) |
222 | + fixture = kvmfixture.KVMFixture(series, architecture) |
223 | + self.assertEqual( |
224 | + (series, architecture, 36), |
225 | + (fixture.series, fixture.architecture, len(fixture.name))) |
226 | |
227 | def test_ip_address(self): |
228 | + self.patch_run_command() |
229 | series = "test-series" |
230 | architecture = "test-arch" |
231 | ip_address = '192.168.2.1' |
232 | @@ -67,6 +116,7 @@ |
233 | self.assertEqual(ip_address, fixture.ip_address()) |
234 | |
235 | def test_identity(self): |
236 | + self.patch_run_command() |
237 | series = "test-series" |
238 | architecture = "test-arch" |
239 | ip_address = '192.168.2.1' |
240 | @@ -75,6 +125,7 @@ |
241 | self.assertEqual("ubuntu@%s" % ip_address, fixture.identity()) |
242 | |
243 | def test_setUp_calls_machine_creation_methods(self): |
244 | + self.patch_run_command() |
245 | mock_import_image = mock.MagicMock() |
246 | self.patch(kvmfixture.KVMFixture, 'import_image', mock_import_image) |
247 | mock_start = mock.MagicMock() |
248 | @@ -99,32 +150,39 @@ |
249 | def test_start_starts_vm(self): |
250 | series = "test-series" |
251 | architecture = "test-arch" |
252 | + self.patch_run_command() |
253 | + self.patch(kvmfixture.KVMFixture, 'generate_ssh_key', mock.MagicMock()) |
254 | mock_make_kvm_template = mock.MagicMock() |
255 | kvm_template = self.getUniqueString() |
256 | mock_make_kvm_template.return_value = kvm_template |
257 | self.patch(kvmfixture, 'make_kvm_template', mock_make_kvm_template) |
258 | + |
259 | with self.make_KVMFixture(series, architecture) as fixture: |
260 | - self.assertEqual( |
261 | - [ |
262 | - mock.call([ |
263 | - 'sudo', 'uvt-simplestreams-libvirt', 'sync', |
264 | - 'arch=%s' % architecture, 'release=%s' % series], |
265 | - check_call=True), |
266 | - mock.call([ |
267 | - 'sudo', 'uvt-kvm', 'create', fixture.name, |
268 | - 'arch=%s' % architecture, 'release=%s' % series, |
269 | - '--template', '-'], |
270 | - input=kvm_template, |
271 | - check_call=True), |
272 | - ], |
273 | - self.mock_run_command.mock_calls) |
274 | - self.assertEqual( |
275 | - [ |
276 | - mock.call(None), |
277 | - ], |
278 | - mock_make_kvm_template.mock_calls) |
279 | + name = fixture.name |
280 | + public_key = fixture.get_ssh_public_key_file() |
281 | + |
282 | + self.mock_run_command.assert_has_calls( |
283 | + [ |
284 | + mock.call([ |
285 | + 'sudo', 'uvt-simplestreams-libvirt', 'sync', |
286 | + 'arch=%s' % architecture, 'release=%s' % series], |
287 | + check_call=True), |
288 | + mock.call([ |
289 | + 'sudo', 'uvt-kvm', 'create', name, |
290 | + '--ssh-public-key-file=%s' % public_key, |
291 | + 'arch=%s' % architecture, 'release=%s' % series, |
292 | + '--template', '-'], |
293 | + input=kvm_template, |
294 | + check_call=True), |
295 | + ]) |
296 | + self.assertEqual( |
297 | + [ |
298 | + mock.call(None), |
299 | + ], |
300 | + mock_make_kvm_template.mock_calls) |
301 | |
302 | def test_import_image_logs_to_root_logger(self): |
303 | + self.patch_run_command() |
304 | fixture = self.make_KVMFixture() |
305 | fixture.import_image() |
306 | self.assertEqual( |
307 | @@ -136,8 +194,87 @@ |
308 | ], |
309 | self.logger.output.strip().split("\n")) |
310 | |
311 | + def test_generate_ssh_key_sets_up_temporary_key_pair(self): |
312 | + home_dir = os.path.expanduser('~') |
313 | + fixture = self.make_KVMFixture() |
314 | + fixtures.Fixture.setUp(fixture) |
315 | + |
316 | + fixture.generate_ssh_key() |
317 | + |
318 | + self.assertThat(fixture.ssh_private_key_file, FileExists()) |
319 | + self.assertThat(fixture.get_ssh_public_key_file(), FileExists()) |
320 | + self.assertThat( |
321 | + fixture.ssh_private_key_file, StartsWith(home_dir + '/')) |
322 | + |
323 | + self.assertThat( |
324 | + fixture.ssh_private_key_file, |
325 | + FileContains( |
326 | + matcher=StartsWith('-----BEGIN RSA PRIVATE KEY-----'))) |
327 | + self.assertThat( |
328 | + fixture.get_ssh_public_key_file(), |
329 | + FileContains(matcher=StartsWith('ssh-rsa'))) |
330 | + |
331 | + # Other users are not allowed to read the private key. |
332 | + self.assertThat(fixture.ssh_private_key_file, HasPermissions('0600')) |
333 | + |
334 | + # The key pair is not protected by a passphrase, as indicated by |
335 | + # the absence of this line. |
336 | + self.assertThat( |
337 | + fixture.ssh_private_key_file, |
338 | + Not(FileContains(matcher=MatchesRegex("^Proc-Type: .*ENCRYPTED")))) |
339 | + |
340 | + def test_generate_ssh_key_reuses_existing_key_pair(self): |
341 | + earlier_fixture = self.make_KVMFixture() |
342 | + fixtures.Fixture.setUp(earlier_fixture) |
343 | + earlier_fixture.generate_ssh_key() |
344 | + earlier_private_key_contents = read_file( |
345 | + earlier_fixture.ssh_private_key_file) |
346 | + earlier_public_key_contents = read_file( |
347 | + earlier_fixture.get_ssh_public_key_file()) |
348 | + |
349 | + later_fixture = self.make_KVMFixture() |
350 | + fixtures.Fixture.setUp(later_fixture) |
351 | + later_fixture.generate_ssh_key() |
352 | + |
353 | + self.assertEqual( |
354 | + earlier_fixture.ssh_private_key_file, |
355 | + later_fixture.ssh_private_key_file) |
356 | + self.assertEqual( |
357 | + earlier_fixture.get_ssh_public_key_file(), |
358 | + later_fixture.get_ssh_public_key_file()) |
359 | + self.assertThat( |
360 | + later_fixture.ssh_private_key_file, |
361 | + FileContains(earlier_private_key_contents)) |
362 | + self.assertThat( |
363 | + later_fixture.get_ssh_public_key_file(), |
364 | + FileContains(earlier_public_key_contents)) |
365 | + |
366 | + def test_start_passes_public_ssh_key(self): |
367 | + self.patch_run_command() |
368 | + |
369 | + with self.make_KVMFixture() as fixture: |
370 | + fixture.start() |
371 | + |
372 | + uvt_call = self.find_matching_call(['sudo', 'uvt-kvm']) |
373 | + self.assertIsNotNone(uvt_call) |
374 | + _, args, _ = uvt_call |
375 | + command_line = args[0] |
376 | + self.assertIn( |
377 | + '--ssh-public-key-file=%s' % fixture.get_ssh_public_key_file(), |
378 | + command_line) |
379 | + |
380 | + def test_get_ssh_public_key_file_returns_public_key(self): |
381 | + self.patch_run_command() |
382 | + |
383 | + with self.make_KVMFixture() as fixture: |
384 | + public_key = fixture.get_ssh_public_key_file() |
385 | + |
386 | + self.assertEqual(fixture.ssh_private_key_file + '.pub', public_key) |
387 | + |
388 | def test_start_logs_to_root_logger(self): |
389 | + self.patch_run_command() |
390 | fixture = self.make_KVMFixture() |
391 | + |
392 | fixture.start() |
393 | self.assertEqual( |
394 | [ |
395 | @@ -149,6 +286,7 @@ |
396 | self.logger.output.strip().split("\n")) |
397 | |
398 | def test_destroy_logs_to_root_logger(self): |
399 | + self.patch_run_command() |
400 | fixture = self.make_KVMFixture() |
401 | fixture.destroy() |
402 | self.assertEqual( |
403 | @@ -159,37 +297,36 @@ |
404 | self.logger.output.strip().split("\n")) |
405 | |
406 | def test_configure_configures_network(self): |
407 | + self.patch_run_command() |
408 | series = "test-series" |
409 | architecture = "test-arch" |
410 | mock_identity = mock.MagicMock() |
411 | mock_identity.return_value = "ubuntu@test" |
412 | self.patch(kvmfixture.KVMFixture, 'identity', mock_identity) |
413 | network = netaddr.IPNetwork('192.168.12.3/24') |
414 | - self.mock_run_command.return_value = (0, '', '') |
415 | ip = "192.168.12.22" |
416 | interface = self.getUniqueString() |
417 | fixture = self.make_KVMFixture( |
418 | series, architecture, direct_interface=interface, |
419 | direct_network=network, direct_ip=ip) |
420 | + kvm_run_command = mock.MagicMock(return_value=(0, '', '')) |
421 | + self.patch(kvmfixture.KVMFixture, 'run_command', kvm_run_command) |
422 | |
423 | fixture.configure() |
424 | |
425 | - netconfig_command = ( |
426 | - kvmfixture.SSH_COMMAND + |
427 | - [fixture.identity(), "sudo tee -a /etc/network/interfaces"]) |
428 | + netconfig_command = ["sudo", "tee", "-a", "/etc/network/interfaces"] |
429 | input = kvmfixture.make_network_interface_config( |
430 | 'eth1', ip, network) |
431 | - netup_command = ( |
432 | - kvmfixture.SSH_COMMAND + |
433 | - [fixture.identity(), "sudo ifup eth1"]) |
434 | + netup_command = ["sudo", "ifup", "eth1"] |
435 | self.assertEqual( |
436 | [ |
437 | mock.call(netconfig_command, input=input, check_call=True), |
438 | - mock.call(netup_command, input=None, check_call=True), |
439 | + mock.call(netup_command, check_call=True), |
440 | ], |
441 | - self.mock_run_command.mock_calls) |
442 | + kvm_run_command.mock_calls) |
443 | |
444 | def test_machine_is_destroyed(self): |
445 | + self.patch_run_command() |
446 | series = "test-series" |
447 | architecture = "test-arch" |
448 | with kvmfixture.KVMFixture(series, architecture) as fixture: |
449 | @@ -201,41 +338,56 @@ |
450 | self.mock_run_command.mock_calls[-1]) |
451 | |
452 | def test_run_command_runs_command_remotely(self): |
453 | + self.patch_run_command() |
454 | series = "test-series" |
455 | architecture = "test-arch" |
456 | input = self.getUniqueString() |
457 | self.mock_run_command.return_value = (0, '', '') |
458 | + command = ["echo", "Hello"] |
459 | with self.make_KVMFixture(series, architecture) as fixture: |
460 | - fixture.run_command( |
461 | - ["echo", "Hello"], input=input, check_call=False) |
462 | - command = ( |
463 | - kvmfixture.SSH_COMMAND + [fixture.identity(), "echo Hello"]) |
464 | - self.assertEqual( |
465 | - mock.call(command, input=input, check_call=False), |
466 | - self.mock_run_command.mock_calls[-2]) |
467 | + expected_command = fixture._make_ssh_command(command) |
468 | + fixture.run_command(command, input=input, check_call=False) |
469 | + ssh_command = self.mock_run_command.mock_calls[-1] |
470 | + |
471 | + self.assertEqual( |
472 | + mock.call(expected_command, input=input, check_call=False), |
473 | + ssh_command) |
474 | |
475 | def test_run_command_adds_details(self): |
476 | + self.patch_run_command() |
477 | series = "test-series" |
478 | architecture = "test-arch" |
479 | + return_value = randint(0, 3) |
480 | input = self.getUniqueString() |
481 | stdout_content = self.getUniqueString() |
482 | stderr_content = self.getUniqueString() |
483 | self.mock_run_command.return_value = ( |
484 | - 0, stdout_content, stderr_content) |
485 | + return_value, |
486 | + stdout_content, |
487 | + stderr_content, |
488 | + ) |
489 | + |
490 | with self.make_KVMFixture(series, architecture) as fixture: |
491 | - expected_args = ( |
492 | - kvmfixture.SSH_COMMAND + [fixture.identity(), "echo Hello"]) |
493 | fixture.run_command( |
494 | ["echo", "Hello"], input=input, check_call=False) |
495 | - self.assertEqual( |
496 | - { |
497 | - 'cmd #1': text_content(' '.join(expected_args)), |
498 | - 'cmd #1 retcode': text_content('0'), |
499 | - 'cmd #1 input': text_content(input), |
500 | - 'cmd #1 stdout': text_content(stdout_content), |
501 | - 'cmd #1 stderr': text_content(stderr_content), |
502 | - }, |
503 | - fixture.getDetails()) |
504 | + details = fixture.getDetails() |
505 | + |
506 | + self.assertThat( |
507 | + details['cmd #1'].as_text(), |
508 | + MatchesRegex('ssh .* echo Hello$')) |
509 | + self.assertEqual( |
510 | + ( |
511 | + unicode(return_value), |
512 | + input, |
513 | + stdout_content, |
514 | + stderr_content, |
515 | + ), |
516 | + ( |
517 | + details['cmd #1 retcode'].as_text(), |
518 | + details['cmd #1 input'].as_text(), |
519 | + details['cmd #1 stdout'].as_text(), |
520 | + details['cmd #1 stderr'].as_text(), |
521 | + )) |
522 | |
523 | |
524 | def xml_normalize(snippet): |
525 | |
526 | === modified file 'maastest/tests/test_utils.py' |
527 | --- maastest/tests/test_utils.py 2013-11-07 14:38:49 +0000 |
528 | +++ maastest/tests/test_utils.py 2013-11-11 10:35:06 +0000 |
529 | @@ -12,8 +12,10 @@ |
530 | __metaclass__ = type |
531 | __all__ = [] |
532 | |
533 | +import os.path |
534 | from subprocess import PIPE |
535 | |
536 | +from fixtures import TempDir |
537 | from maastest import utils |
538 | import mock |
539 | import testtools |
540 | @@ -21,6 +23,18 @@ |
541 | |
542 | class TestRunCommand(testtools.TestCase): |
543 | |
544 | + def test_read_file_returns_file_contents_as_bytes(self): |
545 | + temp_dir = self.useFixture(TempDir()) |
546 | + sample_file = os.path.join(temp_dir.path, self.getUniqueString()) |
547 | + contents = self.getUniqueString().encode('ascii') |
548 | + with open(sample_file, 'wb') as f: |
549 | + f.write(contents) |
550 | + |
551 | + read_contents = utils.read_file(sample_file) |
552 | + |
553 | + self.assertEqual(contents, read_contents) |
554 | + self.assertIsInstance(contents, bytes) |
555 | + |
556 | def test_run_command_calls_Popen(self): |
557 | mock_Popen = mock.MagicMock() |
558 | expected_retcode = self.getUniqueInteger() |
559 | |
560 | === modified file 'maastest/utils.py' |
561 | --- maastest/utils.py 2013-11-07 16:05:06 +0000 |
562 | +++ maastest/utils.py 2013-11-11 10:35:06 +0000 |
563 | @@ -11,6 +11,7 @@ |
564 | |
565 | __metaclass__ = type |
566 | __all__ = [ |
567 | + "read_file", |
568 | "run_command", |
569 | ] |
570 | |
571 | @@ -21,6 +22,12 @@ |
572 | ) |
573 | |
574 | |
575 | +def read_file(path): |
576 | + """Return a given file's contents, as `bytes`.""" |
577 | + with open(path) as f: |
578 | + return f.read() |
579 | + |
580 | + |
581 | def run_command(args, input=None, check_call=False): |
582 | """A wrapper to Popen to run commands in the command-line.""" |
583 | process = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=False) |
Not a proper review but a couple of remarks: e.py). call() and the refactoring that goes with. I think you're relaxing the assertions done in the tests too much with this. In my opinion, it's better to keep checking the complete commands that are run. It's a sort of double-entry bookkeeping system that is there to guarantee that any change to the code is voluntary and I don't think the tests are brittle at all.
- The main functionality you're adding here seems fine to me.
- You probably need to merge trunk as I've made changes in the same area recently (test_kvmfixtur
- I'm really not fond of find_matching_