Merge lp:~jtv/maas-test/ssh-key into lp:maas-test

Proposed by Jeroen T. Vermeulen
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
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

To post a comment you must log in.
lp:~jtv/maas-test/ssh-key updated
18. By Jeroen T. Vermeulen

Merge trunk, resolve conflicts.

Revision history for this message
Raphaël Badin (rvb) wrote :

Not a proper review but a couple of remarks:
- 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_kvmfixture.py).
- I'm really not fond of find_matching_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.

Revision history for this message
Raphaël Badin (rvb) wrote :

One more thing: I think it would be better to put the keys in a fixed place like ~/.maas-test/key[,.pub] to help debugging. I suspect it will be very 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/.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> - I'm really not fond of find_matching_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.

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-triplicate: a bureaucratic exercise that slows things down and confuses, and doesn't improve quality much. It's also self-reinforcing, which is why it's so important to fight the tendency.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> One more thing: I think it would be better to put the keys in a fixed place
> like ~/.maas-test/key[,.pub] to help debugging. I suspect it will be very
> 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.

Revision history for this message
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.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

43 + def get_ssh_public_key(self):

This is returning a path to a key, not a key per se. It's worth renaming to avoid confusion.

128 + self.patch(kvmfixture, 'sleep', mock.MagicMock())

You don't need the third param here, it defaults to that.

233 + self.assertIsNotNone(
234 + self.find_matching_call(['sudo', 'uvt-kvm', 'create']))

Get rid of this and find_matching_call() and use Mock's assert_has_calls() or assert_any_call().

236 + self.assertEqual(
237 + [
238 + mock.call(interface),
239 + ],
240 + mock_make_kvm_template.mock_calls)

And here! And a few other places in the code.

lp:~jtv/maas-test/ssh-key updated
19. By Jeroen T. Vermeulen

Merge trunk, resolve conflicts. Not trivial.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> 43 + def get_ssh_public_key(self):
>
> This is returning a path to a key, not a key per se. It's worth renaming to
> avoid confusion.

OK.

> 128 + self.patch(kvmfixture, 'sleep', mock.MagicMock())
>
> 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.assertIsNotNone(
> 234 + self.find_matching_call(['sudo', 'uvt-kvm', 'create']))
>
> Get rid of this and find_matching_call() and use Mock's assert_has_calls() or
> 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. :(

lp:~jtv/maas-test/ssh-key updated
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.

Revision history for this message
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.

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches