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
=== modified file 'maastest/kvmfixture.py'
--- maastest/kvmfixture.py 2013-11-08 15:33:37 +0000
+++ maastest/kvmfixture.py 2013-11-11 10:35:06 +0000
@@ -17,6 +17,7 @@
1717
18import itertools18import itertools
19import logging19import logging
20import os.path
20from pipes import quote21from pipes import quote
21import textwrap22import textwrap
22from time import sleep23from time import sleep
@@ -28,12 +29,6 @@
28from testtools.content import text_content29from testtools.content import text_content
2930
3031
31SSH_COMMAND = [
32 "ssh", "-o", "LogLevel=quiet", "-o",
33 "UserKnownHostsFile=/dev/null", "-o",
34 "StrictHostKeyChecking=no"]
35
36
37class KVMFixture(Fixture):32class KVMFixture(Fixture):
38 """A fixture for a Kernel-based Virtual Machine.33 """A fixture for a Kernel-based Virtual Machine.
3934
@@ -90,16 +85,16 @@
90 assert direct_ip is not None, (85 assert direct_ip is not None, (
91 "'direct_ip' cannot be empty if 'direct_interface' is not "86 "'direct_ip' cannot be empty if 'direct_interface' is not "
92 "provided")87 "provided")
93 # TODO: add an option to prepare (and later reuse) an existing
94 # KVM instance.
95 if name is None:88 if name is None:
96 name = unicode(uuid1())89 name = unicode(uuid1())
97 self.name = name90 self.name = name
91 self.ssh_private_key_file = os.path.join(
92 os.path.expanduser('~'), '.maas-test', 'vm_ssh_id_rsa')
9893
99 def setUp(self):94 def setUp(self):
100 super(KVMFixture, self).setUp()95 super(KVMFixture, self).setUp()
101 # TODO: check that there is a ssh public/private key pair set up.
102 self.import_image()96 self.import_image()
97 self.generate_ssh_key()
103 self.start()98 self.start()
104 self.configure()99 self.configure()
105 self.addCleanup(self.destroy)100 self.addCleanup(self.destroy)
@@ -115,6 +110,10 @@
115 ["uvt-kvm", "ip", self.name], check_call=True)110 ["uvt-kvm", "ip", self.name], check_call=True)
116 return ip_address.strip()111 return ip_address.strip()
117112
113 def get_ssh_public_key_file(self):
114 """Return path to public SSH key file, for VM sshd configuration."""
115 return self.ssh_private_key_file + '.pub'
116
118 def import_image(self):117 def import_image(self):
119 """Download the image required to create the virtual machine."""118 """Download the image required to create the virtual machine."""
120 logging.info(119 logging.info(
@@ -128,18 +127,36 @@
128 "Done downloading KVM image for series=%s, arch=%s.",127 "Done downloading KVM image for series=%s, arch=%s.",
129 self.series, self.architecture)128 self.series, self.architecture)
130129
130 def generate_ssh_key(self):
131 """Set up an ssh key pair for accessing the virtual machine.
132
133 The key pair will be in a directory `~/.maas-test/`. If this did not
134 exist yet, it will be created.
135 """
136 key_dir = os.path.dirname(self.ssh_private_key_file)
137 if not os.path.exists(key_dir):
138 os.makedirs(key_dir, mode=0700)
139 if not os.path.exists(self.ssh_private_key_file):
140 run_command(
141 [
142 'ssh-keygen',
143 '-t', 'rsa',
144 '-f', self.ssh_private_key_file,
145 '-N', '',
146 ],
147 check_call=True)
148
131 def start(self):149 def start(self):
132 """Create and start the virtual machine.150 """Create and start the virtual machine.
133151
134 When this method returns, the VM is ready to be used."""152 When this method returns, the VM is ready to be used."""
135 # TODO: check if already running (in case this is reusing an
136 # existing instance).
137 logging.info(153 logging.info(
138 "Creating virtual machine %s, arch=%s.", self.name,154 "Creating virtual machine %s, arch=%s.", self.name,
139 self.architecture)155 self.architecture)
140 template = make_kvm_template(self.direct_interface)156 template = make_kvm_template(self.direct_interface)
141 run_command([157 run_command([
142 "sudo", "uvt-kvm", "create", self.name,158 "sudo", "uvt-kvm", "create", self.name,
159 "--ssh-public-key-file=%s" % self.get_ssh_public_key_file(),
143 "arch=%s" % self.architecture,160 "arch=%s" % self.architecture,
144 "release=%s" % self.series, "--template", "-"],161 "release=%s" % self.series, "--template", "-"],
145 input=template, check_call=True)162 input=template, check_call=True)
@@ -178,12 +195,31 @@
178 ["sudo", "uvt-kvm", "destroy", self.name], check_call=True)195 ["sudo", "uvt-kvm", "destroy", self.name], check_call=True)
179 logging.info("Done destroying virtual machine %s.", self.name)196 logging.info("Done destroying virtual machine %s.", self.name)
180197
198 def _make_ssh_command(self, command_line):
199 """Compose an `ssh` command line to execute `command_line` in the VM.
200
201 :param command_line: Shell command to be executed on the virtual
202 machine, in the form of a sequence of arguments (starting with the
203 executable).
204 :return: An `ssh` command line to execute `command_line` on the virtual
205 machine, in the form of a list.
206 """
207 remote_command_line = ' '.join(map(quote, command_line))
208 return [
209 "ssh",
210 "-i", self.ssh_private_key_file,
211 "-o", "LogLevel=quiet",
212 "-o", "UserKnownHostsFile=/dev/null",
213 "-o", "StrictHostKeyChecking=no",
214 self.identity(),
215 remote_command_line,
216 ]
217
181 def run_command(self, args, input=None, check_call=False):218 def run_command(self, args, input=None, check_call=False):
182 """Run the given command in the VM."""219 """Run the given command in the VM."""
183 cmd = " ".join(map(quote, args))220 args = self._make_ssh_command(args)
184 args = SSH_COMMAND + [self.identity(), cmd]
185 retcode, stdout, stderr = run_command(221 retcode, stdout, stderr = run_command(
186 args, input=input, check_call=check_call)222 args, check_call=check_call, input=input)
187 count = next(self.command_count)223 count = next(self.command_count)
188 self.addDetail('cmd #%d' % count, text_content(' '.join(args)))224 self.addDetail('cmd #%d' % count, text_content(' '.join(args)))
189 self.addDetail('cmd #%d retcode' % count, text_content(str(retcode)))225 self.addDetail('cmd #%d retcode' % count, text_content(str(retcode)))
190226
=== modified file 'maastest/tests/test_kvmfixture.py'
--- maastest/tests/test_kvmfixture.py 2013-11-08 14:26:53 +0000
+++ maastest/tests/test_kvmfixture.py 2013-11-11 10:35:06 +0000
@@ -12,25 +12,50 @@
12__metaclass__ = type12__metaclass__ = type
13__all__ = []13__all__ = []
1414
15import os.path
16from random import randint
15import textwrap17import textwrap
1618
17import fixtures19import fixtures
18from lxml import etree20from lxml import etree
19from maastest import kvmfixture21from maastest import kvmfixture
22from maastest.utils import read_file
20import mock23import mock
21import netaddr24import netaddr
22import testtools25import testtools
23from testtools.content import text_content26from testtools.matchers import (
27 FileContains,
28 FileExists,
29 HasPermissions,
30 MatchesRegex,
31 Not,
32 StartsWith,
33 )
2434
2535
26class TestKVMFixture(testtools.TestCase):36class TestKVMFixture(testtools.TestCase):
2737
28 def setUp(self):38 def setUp(self):
29 super(TestKVMFixture, self).setUp()39 super(TestKVMFixture, self).setUp()
40 self.logger = self.useFixture(fixtures.FakeLogger())
41 self.patch(kvmfixture, 'sleep', mock.MagicMock())
42 # KVMFixture.generate_ssh_key() generates an ssh key in ~/.maas-test,
43 # creating that directory if necessary. Set up a fake home directory
44 # so it can do what it needs to.
45 home_dir = self.useFixture(fixtures.TempDir()).path
46 self.patch(
47 os.path, 'expanduser', mock.MagicMock(return_value=home_dir))
48
49 def patch_run_command(self):
50 """Patch out `kvmfixture` calls to `run_command`.
51
52 Call this so your test doesn't accidentally make `KVMFixture` download
53 machine images, ssh into it, etc.
54
55 You can access the fake version as `self.mock_run_command`.
56 """
30 self.mock_run_command = mock.MagicMock()57 self.mock_run_command = mock.MagicMock()
31 self.logger = self.useFixture(fixtures.FakeLogger())
32 self.patch(kvmfixture, 'run_command', self.mock_run_command)58 self.patch(kvmfixture, 'run_command', self.mock_run_command)
33 self.patch(kvmfixture, 'sleep', mock.MagicMock())
3459
35 def make_KVMFixture(self, series=None, architecture=None, name=None,60 def make_KVMFixture(self, series=None, architecture=None, name=None,
36 direct_interface=None, direct_network=None,61 direct_interface=None, direct_network=None,
@@ -50,15 +75,39 @@
50 fixture._details = {}75 fixture._details = {}
51 return fixture76 return fixture
5277
78 def find_matching_call(self, command_prefix):
79 """Find the first `run_command` call with a matching command line.
80
81 This assumes that you have called `patch_run_command` first.
82
83 :param command_prefix: A sequence of items on the command line. A
84 call to `run_commanad` will match this when this forms a prefix of
85 the executed command line. For instance, a `command_prefix` of
86 `['ls']` will match a command line `['ls', '/tmp']` but not
87 a command line `['sudo', 'ls', '/tmp']`. On the other hand, a
88 prefix `['sudo', 'ls']` will match `['sudo', 'ls', '/tmp']` but
89 not `['ls', '/tmp']`.
90 :rtype: `mock.call`
91 """
92 command_prefix = list(command_prefix)
93 prefix_length = len(command_prefix)
94 for invocation in self.mock_run_command.mock_calls:
95 _, args, _ = invocation
96 command_line = args[0]
97 if command_line[:prefix_length] == command_prefix:
98 return invocation
99 return None
100
53 def test_init_sets_properties(self):101 def test_init_sets_properties(self):
54 series = "test-series"102 series = "test-series"
55 architecture = "test-arch"103 architecture = "test-arch"
56 with self.make_KVMFixture(series, architecture) as fixture:104 fixture = kvmfixture.KVMFixture(series, architecture)
57 self.assertEqual(105 self.assertEqual(
58 (series, architecture, 36),106 (series, architecture, 36),
59 (fixture.series, fixture.architecture, len(fixture.name)))107 (fixture.series, fixture.architecture, len(fixture.name)))
60108
61 def test_ip_address(self):109 def test_ip_address(self):
110 self.patch_run_command()
62 series = "test-series"111 series = "test-series"
63 architecture = "test-arch"112 architecture = "test-arch"
64 ip_address = '192.168.2.1'113 ip_address = '192.168.2.1'
@@ -67,6 +116,7 @@
67 self.assertEqual(ip_address, fixture.ip_address())116 self.assertEqual(ip_address, fixture.ip_address())
68117
69 def test_identity(self):118 def test_identity(self):
119 self.patch_run_command()
70 series = "test-series"120 series = "test-series"
71 architecture = "test-arch"121 architecture = "test-arch"
72 ip_address = '192.168.2.1'122 ip_address = '192.168.2.1'
@@ -75,6 +125,7 @@
75 self.assertEqual("ubuntu@%s" % ip_address, fixture.identity())125 self.assertEqual("ubuntu@%s" % ip_address, fixture.identity())
76126
77 def test_setUp_calls_machine_creation_methods(self):127 def test_setUp_calls_machine_creation_methods(self):
128 self.patch_run_command()
78 mock_import_image = mock.MagicMock()129 mock_import_image = mock.MagicMock()
79 self.patch(kvmfixture.KVMFixture, 'import_image', mock_import_image)130 self.patch(kvmfixture.KVMFixture, 'import_image', mock_import_image)
80 mock_start = mock.MagicMock()131 mock_start = mock.MagicMock()
@@ -99,32 +150,39 @@
99 def test_start_starts_vm(self):150 def test_start_starts_vm(self):
100 series = "test-series"151 series = "test-series"
101 architecture = "test-arch"152 architecture = "test-arch"
153 self.patch_run_command()
154 self.patch(kvmfixture.KVMFixture, 'generate_ssh_key', mock.MagicMock())
102 mock_make_kvm_template = mock.MagicMock()155 mock_make_kvm_template = mock.MagicMock()
103 kvm_template = self.getUniqueString()156 kvm_template = self.getUniqueString()
104 mock_make_kvm_template.return_value = kvm_template157 mock_make_kvm_template.return_value = kvm_template
105 self.patch(kvmfixture, 'make_kvm_template', mock_make_kvm_template)158 self.patch(kvmfixture, 'make_kvm_template', mock_make_kvm_template)
159
106 with self.make_KVMFixture(series, architecture) as fixture:160 with self.make_KVMFixture(series, architecture) as fixture:
107 self.assertEqual(161 name = fixture.name
108 [162 public_key = fixture.get_ssh_public_key_file()
109 mock.call([163
110 'sudo', 'uvt-simplestreams-libvirt', 'sync',164 self.mock_run_command.assert_has_calls(
111 'arch=%s' % architecture, 'release=%s' % series],165 [
112 check_call=True),166 mock.call([
113 mock.call([167 'sudo', 'uvt-simplestreams-libvirt', 'sync',
114 'sudo', 'uvt-kvm', 'create', fixture.name,168 'arch=%s' % architecture, 'release=%s' % series],
115 'arch=%s' % architecture, 'release=%s' % series,169 check_call=True),
116 '--template', '-'],170 mock.call([
117 input=kvm_template,171 'sudo', 'uvt-kvm', 'create', name,
118 check_call=True),172 '--ssh-public-key-file=%s' % public_key,
119 ],173 'arch=%s' % architecture, 'release=%s' % series,
120 self.mock_run_command.mock_calls)174 '--template', '-'],
121 self.assertEqual(175 input=kvm_template,
122 [176 check_call=True),
123 mock.call(None),177 ])
124 ],178 self.assertEqual(
125 mock_make_kvm_template.mock_calls)179 [
180 mock.call(None),
181 ],
182 mock_make_kvm_template.mock_calls)
126183
127 def test_import_image_logs_to_root_logger(self):184 def test_import_image_logs_to_root_logger(self):
185 self.patch_run_command()
128 fixture = self.make_KVMFixture()186 fixture = self.make_KVMFixture()
129 fixture.import_image()187 fixture.import_image()
130 self.assertEqual(188 self.assertEqual(
@@ -136,8 +194,87 @@
136 ],194 ],
137 self.logger.output.strip().split("\n"))195 self.logger.output.strip().split("\n"))
138196
197 def test_generate_ssh_key_sets_up_temporary_key_pair(self):
198 home_dir = os.path.expanduser('~')
199 fixture = self.make_KVMFixture()
200 fixtures.Fixture.setUp(fixture)
201
202 fixture.generate_ssh_key()
203
204 self.assertThat(fixture.ssh_private_key_file, FileExists())
205 self.assertThat(fixture.get_ssh_public_key_file(), FileExists())
206 self.assertThat(
207 fixture.ssh_private_key_file, StartsWith(home_dir + '/'))
208
209 self.assertThat(
210 fixture.ssh_private_key_file,
211 FileContains(
212 matcher=StartsWith('-----BEGIN RSA PRIVATE KEY-----')))
213 self.assertThat(
214 fixture.get_ssh_public_key_file(),
215 FileContains(matcher=StartsWith('ssh-rsa')))
216
217 # Other users are not allowed to read the private key.
218 self.assertThat(fixture.ssh_private_key_file, HasPermissions('0600'))
219
220 # The key pair is not protected by a passphrase, as indicated by
221 # the absence of this line.
222 self.assertThat(
223 fixture.ssh_private_key_file,
224 Not(FileContains(matcher=MatchesRegex("^Proc-Type: .*ENCRYPTED"))))
225
226 def test_generate_ssh_key_reuses_existing_key_pair(self):
227 earlier_fixture = self.make_KVMFixture()
228 fixtures.Fixture.setUp(earlier_fixture)
229 earlier_fixture.generate_ssh_key()
230 earlier_private_key_contents = read_file(
231 earlier_fixture.ssh_private_key_file)
232 earlier_public_key_contents = read_file(
233 earlier_fixture.get_ssh_public_key_file())
234
235 later_fixture = self.make_KVMFixture()
236 fixtures.Fixture.setUp(later_fixture)
237 later_fixture.generate_ssh_key()
238
239 self.assertEqual(
240 earlier_fixture.ssh_private_key_file,
241 later_fixture.ssh_private_key_file)
242 self.assertEqual(
243 earlier_fixture.get_ssh_public_key_file(),
244 later_fixture.get_ssh_public_key_file())
245 self.assertThat(
246 later_fixture.ssh_private_key_file,
247 FileContains(earlier_private_key_contents))
248 self.assertThat(
249 later_fixture.get_ssh_public_key_file(),
250 FileContains(earlier_public_key_contents))
251
252 def test_start_passes_public_ssh_key(self):
253 self.patch_run_command()
254
255 with self.make_KVMFixture() as fixture:
256 fixture.start()
257
258 uvt_call = self.find_matching_call(['sudo', 'uvt-kvm'])
259 self.assertIsNotNone(uvt_call)
260 _, args, _ = uvt_call
261 command_line = args[0]
262 self.assertIn(
263 '--ssh-public-key-file=%s' % fixture.get_ssh_public_key_file(),
264 command_line)
265
266 def test_get_ssh_public_key_file_returns_public_key(self):
267 self.patch_run_command()
268
269 with self.make_KVMFixture() as fixture:
270 public_key = fixture.get_ssh_public_key_file()
271
272 self.assertEqual(fixture.ssh_private_key_file + '.pub', public_key)
273
139 def test_start_logs_to_root_logger(self):274 def test_start_logs_to_root_logger(self):
275 self.patch_run_command()
140 fixture = self.make_KVMFixture()276 fixture = self.make_KVMFixture()
277
141 fixture.start()278 fixture.start()
142 self.assertEqual(279 self.assertEqual(
143 [280 [
@@ -149,6 +286,7 @@
149 self.logger.output.strip().split("\n"))286 self.logger.output.strip().split("\n"))
150287
151 def test_destroy_logs_to_root_logger(self):288 def test_destroy_logs_to_root_logger(self):
289 self.patch_run_command()
152 fixture = self.make_KVMFixture()290 fixture = self.make_KVMFixture()
153 fixture.destroy()291 fixture.destroy()
154 self.assertEqual(292 self.assertEqual(
@@ -159,37 +297,36 @@
159 self.logger.output.strip().split("\n"))297 self.logger.output.strip().split("\n"))
160298
161 def test_configure_configures_network(self):299 def test_configure_configures_network(self):
300 self.patch_run_command()
162 series = "test-series"301 series = "test-series"
163 architecture = "test-arch"302 architecture = "test-arch"
164 mock_identity = mock.MagicMock()303 mock_identity = mock.MagicMock()
165 mock_identity.return_value = "ubuntu@test"304 mock_identity.return_value = "ubuntu@test"
166 self.patch(kvmfixture.KVMFixture, 'identity', mock_identity)305 self.patch(kvmfixture.KVMFixture, 'identity', mock_identity)
167 network = netaddr.IPNetwork('192.168.12.3/24')306 network = netaddr.IPNetwork('192.168.12.3/24')
168 self.mock_run_command.return_value = (0, '', '')
169 ip = "192.168.12.22"307 ip = "192.168.12.22"
170 interface = self.getUniqueString()308 interface = self.getUniqueString()
171 fixture = self.make_KVMFixture(309 fixture = self.make_KVMFixture(
172 series, architecture, direct_interface=interface,310 series, architecture, direct_interface=interface,
173 direct_network=network, direct_ip=ip)311 direct_network=network, direct_ip=ip)
312 kvm_run_command = mock.MagicMock(return_value=(0, '', ''))
313 self.patch(kvmfixture.KVMFixture, 'run_command', kvm_run_command)
174314
175 fixture.configure()315 fixture.configure()
176316
177 netconfig_command = (317 netconfig_command = ["sudo", "tee", "-a", "/etc/network/interfaces"]
178 kvmfixture.SSH_COMMAND +
179 [fixture.identity(), "sudo tee -a /etc/network/interfaces"])
180 input = kvmfixture.make_network_interface_config(318 input = kvmfixture.make_network_interface_config(
181 'eth1', ip, network)319 'eth1', ip, network)
182 netup_command = (320 netup_command = ["sudo", "ifup", "eth1"]
183 kvmfixture.SSH_COMMAND +
184 [fixture.identity(), "sudo ifup eth1"])
185 self.assertEqual(321 self.assertEqual(
186 [322 [
187 mock.call(netconfig_command, input=input, check_call=True),323 mock.call(netconfig_command, input=input, check_call=True),
188 mock.call(netup_command, input=None, check_call=True),324 mock.call(netup_command, check_call=True),
189 ],325 ],
190 self.mock_run_command.mock_calls)326 kvm_run_command.mock_calls)
191327
192 def test_machine_is_destroyed(self):328 def test_machine_is_destroyed(self):
329 self.patch_run_command()
193 series = "test-series"330 series = "test-series"
194 architecture = "test-arch"331 architecture = "test-arch"
195 with kvmfixture.KVMFixture(series, architecture) as fixture:332 with kvmfixture.KVMFixture(series, architecture) as fixture:
@@ -201,41 +338,56 @@
201 self.mock_run_command.mock_calls[-1])338 self.mock_run_command.mock_calls[-1])
202339
203 def test_run_command_runs_command_remotely(self):340 def test_run_command_runs_command_remotely(self):
341 self.patch_run_command()
204 series = "test-series"342 series = "test-series"
205 architecture = "test-arch"343 architecture = "test-arch"
206 input = self.getUniqueString()344 input = self.getUniqueString()
207 self.mock_run_command.return_value = (0, '', '')345 self.mock_run_command.return_value = (0, '', '')
346 command = ["echo", "Hello"]
208 with self.make_KVMFixture(series, architecture) as fixture:347 with self.make_KVMFixture(series, architecture) as fixture:
209 fixture.run_command(348 expected_command = fixture._make_ssh_command(command)
210 ["echo", "Hello"], input=input, check_call=False)349 fixture.run_command(command, input=input, check_call=False)
211 command = (350 ssh_command = self.mock_run_command.mock_calls[-1]
212 kvmfixture.SSH_COMMAND + [fixture.identity(), "echo Hello"])351
213 self.assertEqual(352 self.assertEqual(
214 mock.call(command, input=input, check_call=False),353 mock.call(expected_command, input=input, check_call=False),
215 self.mock_run_command.mock_calls[-2])354 ssh_command)
216355
217 def test_run_command_adds_details(self):356 def test_run_command_adds_details(self):
357 self.patch_run_command()
218 series = "test-series"358 series = "test-series"
219 architecture = "test-arch"359 architecture = "test-arch"
360 return_value = randint(0, 3)
220 input = self.getUniqueString()361 input = self.getUniqueString()
221 stdout_content = self.getUniqueString()362 stdout_content = self.getUniqueString()
222 stderr_content = self.getUniqueString()363 stderr_content = self.getUniqueString()
223 self.mock_run_command.return_value = (364 self.mock_run_command.return_value = (
224 0, stdout_content, stderr_content)365 return_value,
366 stdout_content,
367 stderr_content,
368 )
369
225 with self.make_KVMFixture(series, architecture) as fixture:370 with self.make_KVMFixture(series, architecture) as fixture:
226 expected_args = (
227 kvmfixture.SSH_COMMAND + [fixture.identity(), "echo Hello"])
228 fixture.run_command(371 fixture.run_command(
229 ["echo", "Hello"], input=input, check_call=False)372 ["echo", "Hello"], input=input, check_call=False)
230 self.assertEqual(373 details = fixture.getDetails()
231 {374
232 'cmd #1': text_content(' '.join(expected_args)),375 self.assertThat(
233 'cmd #1 retcode': text_content('0'),376 details['cmd #1'].as_text(),
234 'cmd #1 input': text_content(input),377 MatchesRegex('ssh .* echo Hello$'))
235 'cmd #1 stdout': text_content(stdout_content),378 self.assertEqual(
236 'cmd #1 stderr': text_content(stderr_content),379 (
237 },380 unicode(return_value),
238 fixture.getDetails())381 input,
382 stdout_content,
383 stderr_content,
384 ),
385 (
386 details['cmd #1 retcode'].as_text(),
387 details['cmd #1 input'].as_text(),
388 details['cmd #1 stdout'].as_text(),
389 details['cmd #1 stderr'].as_text(),
390 ))
239391
240392
241def xml_normalize(snippet):393def xml_normalize(snippet):
242394
=== modified file 'maastest/tests/test_utils.py'
--- maastest/tests/test_utils.py 2013-11-07 14:38:49 +0000
+++ maastest/tests/test_utils.py 2013-11-11 10:35:06 +0000
@@ -12,8 +12,10 @@
12__metaclass__ = type12__metaclass__ = type
13__all__ = []13__all__ = []
1414
15import os.path
15from subprocess import PIPE16from subprocess import PIPE
1617
18from fixtures import TempDir
17from maastest import utils19from maastest import utils
18import mock20import mock
19import testtools21import testtools
@@ -21,6 +23,18 @@
2123
22class TestRunCommand(testtools.TestCase):24class TestRunCommand(testtools.TestCase):
2325
26 def test_read_file_returns_file_contents_as_bytes(self):
27 temp_dir = self.useFixture(TempDir())
28 sample_file = os.path.join(temp_dir.path, self.getUniqueString())
29 contents = self.getUniqueString().encode('ascii')
30 with open(sample_file, 'wb') as f:
31 f.write(contents)
32
33 read_contents = utils.read_file(sample_file)
34
35 self.assertEqual(contents, read_contents)
36 self.assertIsInstance(contents, bytes)
37
24 def test_run_command_calls_Popen(self):38 def test_run_command_calls_Popen(self):
25 mock_Popen = mock.MagicMock()39 mock_Popen = mock.MagicMock()
26 expected_retcode = self.getUniqueInteger()40 expected_retcode = self.getUniqueInteger()
2741
=== modified file 'maastest/utils.py'
--- maastest/utils.py 2013-11-07 16:05:06 +0000
+++ maastest/utils.py 2013-11-11 10:35:06 +0000
@@ -11,6 +11,7 @@
1111
12__metaclass__ = type12__metaclass__ = type
13__all__ = [13__all__ = [
14 "read_file",
14 "run_command",15 "run_command",
15 ]16 ]
1617
@@ -21,6 +22,12 @@
21 )22 )
2223
2324
25def read_file(path):
26 """Return a given file's contents, as `bytes`."""
27 with open(path) as f:
28 return f.read()
29
30
24def run_command(args, input=None, check_call=False):31def run_command(args, input=None, check_call=False):
25 """A wrapper to Popen to run commands in the command-line."""32 """A wrapper to Popen to run commands in the command-line."""
26 process = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=False)33 process = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=False)

Subscribers

People subscribed via source and target branches