Merge lp:~blake-rouse/maas/virsh-probe-and-enlist-1.5 into lp:maas/1.5
- virsh-probe-and-enlist-1.5
- Merge into 1.5
Proposed by
Blake Rouse
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Blake Rouse | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | 2276 | ||||||||
Proposed branch: | lp:~blake-rouse/maas/virsh-probe-and-enlist-1.5 | ||||||||
Merge into: | lp:maas/1.5 | ||||||||
Diff against target: |
727 lines (+518/-64) 11 files modified
etc/maas/templates/power/virsh.template (+12/-46) required-packages/base (+1/-0) src/maasserver/api.py (+19/-2) src/maasserver/models/node.py (+1/-1) src/maasserver/models/nodegroup.py (+10/-0) src/provisioningserver/custom_hardware/tests/test_virsh.py (+240/-0) src/provisioningserver/custom_hardware/utils.py (+4/-0) src/provisioningserver/custom_hardware/virsh.py (+219/-0) src/provisioningserver/power/tests/test_poweraction.py (+0/-14) src/provisioningserver/power_schema.py (+3/-0) src/provisioningserver/tasks.py (+9/-1) |
||||||||
To merge this branch: | bzr merge lp:~blake-rouse/maas/virsh-probe-and-enlist-1.5 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+220509@code.launchpad.net |
Commit message
Use probe-and-
Description of the change
To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'etc/maas/templates/power/virsh.template' |
2 | --- etc/maas/templates/power/virsh.template 2013-11-08 02:28:38 +0000 |
3 | +++ etc/maas/templates/power/virsh.template 2014-05-21 17:37:04 +0000 |
4 | @@ -3,50 +3,16 @@ |
5 | # Control virtual system's "power" through virsh. |
6 | # |
7 | |
8 | -# Parameters. |
9 | -power_change={{power_change}} |
10 | -power_address={{power_address}} |
11 | -power_id={{power_id}} |
12 | -virsh={{virsh}} |
13 | - |
14 | - |
15 | -# Choose command for virsh to make the requested power change happen. |
16 | -formulate_power_command() { |
17 | - if [ ${power_change} = 'on' ] |
18 | - then |
19 | - echo 'start' |
20 | - else |
21 | - echo 'destroy' |
22 | - fi |
23 | -} |
24 | - |
25 | - |
26 | -# Express system's current state as expressed by virsh as "on" or "off". |
27 | -formulate_power_state() { |
28 | - case $1 in |
29 | - 'running') echo 'on' ;; |
30 | - 'shut off') echo 'off' ;; |
31 | - *) |
32 | - echo "Got unknown power state from virsh: '$1'" >&2 |
33 | - exit 1 |
34 | - esac |
35 | -} |
36 | - |
37 | - |
38 | -# Issue command to virsh, for the given system. |
39 | issue_virsh_command() { |
40 | - ${virsh} --connect ${power_address} $1 ${power_id} |
41 | -} |
42 | - |
43 | - |
44 | -# Get the given system's power state: 'on' or 'off'. |
45 | -get_power_state() { |
46 | - virsh_state=$(issue_virsh_command domstate) |
47 | - formulate_power_state ${virsh_state} |
48 | -} |
49 | - |
50 | - |
51 | -if [ "$(get_power_state)" != "${power_change}" ] |
52 | -then |
53 | - issue_virsh_command $(formulate_power_command) |
54 | -fi |
55 | +python - << END |
56 | +from provisioningserver.custom_hardware.virsh import power_control_virsh |
57 | +power_control_virsh( |
58 | + {{repr(power_address).decode("ascii") | safe}}, |
59 | + {{repr(power_id).decode("ascii") | safe}}, |
60 | + {{repr(power_change).decode("ascii") | safe}}, |
61 | + {{repr(power_pass).decode("ascii") | safe}}, |
62 | +) |
63 | +END |
64 | +} |
65 | + |
66 | +issue_virsh_command |
67 | |
68 | === modified file 'required-packages/base' |
69 | --- required-packages/base 2014-03-25 13:13:36 +0000 |
70 | +++ required-packages/base 2014-05-21 17:37:04 +0000 |
71 | @@ -35,6 +35,7 @@ |
72 | python-oops-datedir-repo |
73 | python-oops-twisted |
74 | python-oops-wsgi |
75 | +python-pexpect |
76 | python-psycopg2 |
77 | python-pyinotify |
78 | python-seamicroclient |
79 | |
80 | === modified file 'src/maasserver/api.py' |
81 | --- src/maasserver/api.py 2014-05-07 02:53:23 +0000 |
82 | +++ src/maasserver/api.py 2014-05-21 17:37:04 +0000 |
83 | @@ -1684,8 +1684,8 @@ |
84 | def probe_and_enlist_hardware(self, request, uuid): |
85 | """Add special hardware types. |
86 | |
87 | - :param model: The type of special hardware, currently only |
88 | - 'seamicro15k' is supported. |
89 | + :param model: The type of special hardware, 'seamicro15k' and |
90 | + 'virsh' is supported. |
91 | :type model: unicode |
92 | |
93 | The following are only required if you are probing a seamicro15k: |
94 | @@ -1702,6 +1702,17 @@ |
95 | :param power_control: The power_control to use, either ipmi (default) |
96 | or restapi. |
97 | :type power_control: unicode |
98 | + |
99 | + The following are only required if you are probing a virsh: |
100 | + |
101 | + :param power_address: The connection string to virsh. |
102 | + :type power_address: unicode |
103 | + |
104 | + The following are optional if you are probing a virsh: |
105 | + |
106 | + :param power_pass: The password to use, when qemu+ssh is given as a |
107 | + connection string and ssh key authentication is not being used. |
108 | + :type power_pass: unicode |
109 | """ |
110 | nodegroup = get_object_or_404(NodeGroup, uuid=uuid) |
111 | |
112 | @@ -1716,6 +1727,12 @@ |
113 | |
114 | nodegroup.add_seamicro15k( |
115 | mac, username, password, power_control=power_control) |
116 | + elif model == 'powerkvm' or model == 'virsh': |
117 | + poweraddr = get_mandatory_param(request.data, 'power_address') |
118 | + password = get_optional_param( |
119 | + request.data, 'power_pass', default=None) |
120 | + |
121 | + nodegroup.add_virsh(poweraddr, password=password) |
122 | else: |
123 | return HttpResponse(status=httplib.BAD_REQUEST) |
124 | |
125 | |
126 | === modified file 'src/maasserver/models/node.py' |
127 | --- src/maasserver/models/node.py 2014-05-07 02:53:23 +0000 |
128 | +++ src/maasserver/models/node.py 2014-05-21 17:37:04 +0000 |
129 | @@ -758,7 +758,6 @@ |
130 | power_params = {} |
131 | |
132 | power_params.setdefault('system_id', self.system_id) |
133 | - power_params.setdefault('virsh', '/usr/bin/virsh') |
134 | power_params.setdefault('fence_cdu', '/usr/sbin/fence_cdu') |
135 | power_params.setdefault('ipmipower', '/usr/sbin/ipmipower') |
136 | power_params.setdefault('ipmitool', '/usr/bin/ipmitool') |
137 | @@ -769,6 +768,7 @@ |
138 | power_params.setdefault('username', '') |
139 | power_params.setdefault('power_id', self.system_id) |
140 | power_params.setdefault('power_driver', '') |
141 | + power_params.setdefault('power_pass', '') |
142 | |
143 | # The "mac" parameter defaults to the node's primary MAC |
144 | # address, but only if not already set. |
145 | |
146 | === modified file 'src/maasserver/models/nodegroup.py' |
147 | --- src/maasserver/models/nodegroup.py 2014-05-06 21:18:17 +0000 |
148 | +++ src/maasserver/models/nodegroup.py 2014-05-21 17:37:04 +0000 |
149 | @@ -41,6 +41,7 @@ |
150 | from provisioningserver.tasks import ( |
151 | add_new_dhcp_host_map, |
152 | add_seamicro15k, |
153 | + add_virsh, |
154 | enlist_nodes_from_ucsm, |
155 | import_boot_images, |
156 | report_boot_images, |
157 | @@ -287,6 +288,15 @@ |
158 | args = (mac, username, password, power_control) |
159 | add_seamicro15k.apply_async(queue=self.uuid, args=args) |
160 | |
161 | + def add_virsh(self, poweraddr, password=None): |
162 | + """ Add all of the virtual machines inside a virsh controller. |
163 | + |
164 | + :param poweraddr: virsh connection string |
165 | + :param password: ssh password |
166 | + """ |
167 | + args = (poweraddr, password) |
168 | + add_virsh.apply_async(queue=self.uuid, args=args) |
169 | + |
170 | def enlist_nodes_from_ucsm(self, url, username, password): |
171 | """ Add the servers from a Cicso UCS Manager. |
172 | |
173 | |
174 | === added file 'src/provisioningserver/custom_hardware/tests/test_virsh.py' |
175 | --- src/provisioningserver/custom_hardware/tests/test_virsh.py 1970-01-01 00:00:00 +0000 |
176 | +++ src/provisioningserver/custom_hardware/tests/test_virsh.py 2014-05-21 17:37:04 +0000 |
177 | @@ -0,0 +1,240 @@ |
178 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
179 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
180 | + |
181 | +"""Tests for `provisioningserver.custom_hardware.virsh`. |
182 | +""" |
183 | + |
184 | +from __future__ import ( |
185 | + absolute_import, |
186 | + print_function, |
187 | + unicode_literals, |
188 | + ) |
189 | + |
190 | +str = None |
191 | + |
192 | +__metaclass__ = type |
193 | +__all__ = [] |
194 | + |
195 | +import random |
196 | +from textwrap import dedent |
197 | + |
198 | +from maastesting.factory import factory |
199 | +from maastesting.matchers import ( |
200 | + MockCalledOnceWith, |
201 | + MockCallsMatch, |
202 | + ) |
203 | +from maastesting.testcase import MAASTestCase |
204 | +from mock import call |
205 | +from provisioningserver.custom_hardware import ( |
206 | + utils, |
207 | + virsh, |
208 | + ) |
209 | + |
210 | + |
211 | +SAMPLE_IFLIST = dedent(""" |
212 | + Interface Type Source Model MAC |
213 | + ------------------------------------------------------- |
214 | + - bridge br0 e1000 %s |
215 | + - bridge br1 e1000 %s |
216 | + """) |
217 | + |
218 | +SAMPLE_DUMPXML = dedent(""" |
219 | + <domain type='kvm'> |
220 | + <name>test</name> |
221 | + <memory unit='KiB'>4096576</memory> |
222 | + <currentMemory unit='KiB'>4096576</currentMemory> |
223 | + <vcpu placement='static'>1</vcpu> |
224 | + <os> |
225 | + <type arch='%s'>hvm</type> |
226 | + <boot dev='network'/> |
227 | + </os> |
228 | + </domain> |
229 | + """) |
230 | + |
231 | + |
232 | +class TestVirshSSH(MAASTestCase): |
233 | + """Tests for `VirshSSH`.""" |
234 | + |
235 | + def configure_virshssh(self, output): |
236 | + self.patch(virsh.VirshSSH, 'run').return_value = output |
237 | + return virsh.VirshSSH() |
238 | + |
239 | + def test_virssh_mac_addresses_returns_list(self): |
240 | + macs = [factory.getRandomMACAddress() for _ in range(2)] |
241 | + output = SAMPLE_IFLIST % (macs[0], macs[1]) |
242 | + conn = self.configure_virshssh(output) |
243 | + expected = conn.get_mac_addresses('') |
244 | + for i in range(2): |
245 | + self.assertEqual(macs[i], expected[i]) |
246 | + |
247 | + def test_virssh_get_arch_returns_valid(self): |
248 | + arch = factory.make_name('arch') |
249 | + output = SAMPLE_DUMPXML % arch |
250 | + conn = self.configure_virshssh(output) |
251 | + expected = conn.get_arch('') |
252 | + self.assertEqual(arch, expected) |
253 | + |
254 | + def test_virssh_get_arch_returns_valid_fixed(self): |
255 | + arch = random.choice(virsh.ARCH_FIX.keys()) |
256 | + fixed_arch = virsh.ARCH_FIX[arch] |
257 | + output = SAMPLE_DUMPXML % arch |
258 | + conn = self.configure_virshssh(output) |
259 | + expected = conn.get_arch('') |
260 | + self.assertEqual(fixed_arch, expected) |
261 | + |
262 | + |
263 | +class TestVirsh(MAASTestCase): |
264 | + """Tests for `probe_virsh_and_enlist`.""" |
265 | + |
266 | + def test_probe_and_enlist(self): |
267 | + # Patch VirshSSH list so that some machines are returned |
268 | + # with some fake architectures. |
269 | + machines = [factory.make_name('machine') for _ in range(3)] |
270 | + self.patch(virsh.VirshSSH, 'list').return_value = machines |
271 | + fake_arch = factory.make_name('arch') |
272 | + mock_arch = self.patch(virsh.VirshSSH, 'get_arch') |
273 | + mock_arch.return_value = fake_arch |
274 | + |
275 | + # Patch get_state so that one of the machines is on, so we |
276 | + # can check that it will be forced off. |
277 | + fake_states = [ |
278 | + virsh.VirshVMState.ON, |
279 | + virsh.VirshVMState.OFF, |
280 | + virsh.VirshVMState.OFF |
281 | + ] |
282 | + mock_state = self.patch(virsh.VirshSSH, 'get_state') |
283 | + mock_state.side_effect = fake_states |
284 | + |
285 | + # Setup the power parameters that we should expect to be |
286 | + # the output of the probe_and_enlist |
287 | + fake_password = factory.getRandomString() |
288 | + poweraddr = factory.make_name('poweraddr') |
289 | + called_params = [] |
290 | + fake_macs = [] |
291 | + for machine in machines: |
292 | + macs = [factory.getRandomMACAddress() for _ in range(3)] |
293 | + fake_macs.append(macs) |
294 | + called_params.append({ |
295 | + 'power_address': poweraddr, |
296 | + 'power_id': machine, |
297 | + 'power_pass': fake_password, |
298 | + }) |
299 | + |
300 | + # Patch the get_mac_addresses so we get a known list of |
301 | + # mac addresses for each machine. |
302 | + mock_macs = self.patch(virsh.VirshSSH, 'get_mac_addresses') |
303 | + mock_macs.side_effect = fake_macs |
304 | + |
305 | + # Patch the poweroff and create as we really don't want these |
306 | + # actions to occur, but want to also check that they are called. |
307 | + mock_poweroff = self.patch(virsh.VirshSSH, 'poweroff') |
308 | + mock_create = self.patch(utils, 'create_node') |
309 | + |
310 | + # Patch login and logout so that we don't really contact |
311 | + # a server at the fake poweraddr |
312 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
313 | + mock_login.return_value = True |
314 | + mock_logout = self.patch(virsh.VirshSSH, 'logout') |
315 | + |
316 | + # Perform the probe and enlist |
317 | + virsh.probe_virsh_and_enlist(poweraddr, password=fake_password) |
318 | + |
319 | + # Check that login was called with the provided poweraddr and |
320 | + # password. |
321 | + self.assertThat( |
322 | + mock_login, MockCalledOnceWith(poweraddr, fake_password)) |
323 | + |
324 | + # The first machine should have poweroff called on it, as it |
325 | + # was initial in the on state. |
326 | + self.assertThat( |
327 | + mock_poweroff, MockCalledOnceWith(machines[0])) |
328 | + |
329 | + # Check that the create command had the correct parameters for |
330 | + # each machine. |
331 | + self.assertThat( |
332 | + mock_create, MockCallsMatch( |
333 | + call(fake_macs[0], fake_arch, 'virsh', called_params[0]), |
334 | + call(fake_macs[1], fake_arch, 'virsh', called_params[1]), |
335 | + call(fake_macs[2], fake_arch, 'virsh', called_params[2]))) |
336 | + mock_logout.assert_called() |
337 | + |
338 | + def test_probe_and_enlist_login_failure(self): |
339 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
340 | + mock_login.return_value = False |
341 | + self.assertRaises( |
342 | + virsh.VirshError, virsh.probe_virsh_and_enlist, |
343 | + factory.make_name('poweraddr'), password=factory.getRandomString()) |
344 | + |
345 | + |
346 | +class TestVirshPowerControl(MAASTestCase): |
347 | + """Tests for `power_control_virsh`.""" |
348 | + |
349 | + def test_power_control_login_failure(self): |
350 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
351 | + mock_login.return_value = False |
352 | + self.assertRaises( |
353 | + virsh.VirshError, virsh.power_control_virsh, |
354 | + factory.make_name('poweraddr'), factory.make_name('machine'), |
355 | + 'on', password=factory.getRandomString()) |
356 | + |
357 | + def test_power_control_on(self): |
358 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
359 | + mock_login.return_value = True |
360 | + mock_state = self.patch(virsh.VirshSSH, 'get_state') |
361 | + mock_state.return_value = virsh.VirshVMState.OFF |
362 | + mock_poweron = self.patch(virsh.VirshSSH, 'poweron') |
363 | + |
364 | + poweraddr = factory.make_name('poweraddr') |
365 | + machine = factory.make_name('machine') |
366 | + virsh.power_control_virsh(poweraddr, machine, 'on') |
367 | + |
368 | + self.assertThat( |
369 | + mock_login, MockCalledOnceWith(poweraddr, None)) |
370 | + self.assertThat( |
371 | + mock_state, MockCalledOnceWith(machine)) |
372 | + self.assertThat( |
373 | + mock_poweron, MockCalledOnceWith(machine)) |
374 | + |
375 | + def test_power_control_off(self): |
376 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
377 | + mock_login.return_value = True |
378 | + mock_state = self.patch(virsh.VirshSSH, 'get_state') |
379 | + mock_state.return_value = virsh.VirshVMState.ON |
380 | + mock_poweroff = self.patch(virsh.VirshSSH, 'poweroff') |
381 | + |
382 | + poweraddr = factory.make_name('poweraddr') |
383 | + machine = factory.make_name('machine') |
384 | + virsh.power_control_virsh(poweraddr, machine, 'off') |
385 | + |
386 | + self.assertThat( |
387 | + mock_login, MockCalledOnceWith(poweraddr, None)) |
388 | + self.assertThat( |
389 | + mock_state, MockCalledOnceWith(machine)) |
390 | + self.assertThat( |
391 | + mock_poweroff, MockCalledOnceWith(machine)) |
392 | + |
393 | + def test_power_control_bad_domain(self): |
394 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
395 | + mock_login.return_value = True |
396 | + mock_state = self.patch(virsh.VirshSSH, 'get_state') |
397 | + mock_state.return_value = None |
398 | + |
399 | + poweraddr = factory.make_name('poweraddr') |
400 | + machine = factory.make_name('machine') |
401 | + self.assertRaises( |
402 | + virsh.VirshError, virsh.power_control_virsh, |
403 | + poweraddr, machine, 'on') |
404 | + |
405 | + def test_power_control_power_failure(self): |
406 | + mock_login = self.patch(virsh.VirshSSH, 'login') |
407 | + mock_login.return_value = True |
408 | + mock_state = self.patch(virsh.VirshSSH, 'get_state') |
409 | + mock_state.return_value = virsh.VirshVMState.ON |
410 | + mock_poweroff = self.patch(virsh.VirshSSH, 'poweroff') |
411 | + mock_poweroff.return_value = False |
412 | + |
413 | + poweraddr = factory.make_name('poweraddr') |
414 | + machine = factory.make_name('machine') |
415 | + self.assertRaises( |
416 | + virsh.VirshError, virsh.power_control_virsh, |
417 | + poweraddr, machine, 'off') |
418 | |
419 | === modified file 'src/provisioningserver/custom_hardware/utils.py' |
420 | --- src/provisioningserver/custom_hardware/utils.py 2014-03-27 04:15:45 +0000 |
421 | +++ src/provisioningserver/custom_hardware/utils.py 2014-05-21 17:37:04 +0000 |
422 | @@ -42,3 +42,7 @@ |
423 | 'autodetect_nodegroup': 'true' |
424 | } |
425 | return client.post('/api/1.0/nodes/', 'new', **data) |
426 | + |
427 | + |
428 | +def escape_string(data): |
429 | + return repr(data).decode("ascii") |
430 | |
431 | === added file 'src/provisioningserver/custom_hardware/virsh.py' |
432 | --- src/provisioningserver/custom_hardware/virsh.py 1970-01-01 00:00:00 +0000 |
433 | +++ src/provisioningserver/custom_hardware/virsh.py 2014-05-21 17:37:04 +0000 |
434 | @@ -0,0 +1,219 @@ |
435 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
436 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
437 | + |
438 | +from __future__ import ( |
439 | + absolute_import, |
440 | + print_function, |
441 | + unicode_literals, |
442 | + ) |
443 | + |
444 | +str = None |
445 | + |
446 | +__metaclass__ = type |
447 | +__all__ = [ |
448 | + 'probe_virsh_and_enlist', |
449 | + ] |
450 | + |
451 | +from lxml import etree |
452 | +import pexpect |
453 | +import provisioningserver.custom_hardware.utils as utils |
454 | + |
455 | + |
456 | +XPATH_ARCH = "/domain/os/type/@arch" |
457 | + |
458 | +# Virsh stores the architecture with a different |
459 | +# label then MAAS. This maps virsh architecture to |
460 | +# MAAS architecture. |
461 | +ARCH_FIX = { |
462 | + 'x86_64': 'amd64', |
463 | + 'ppc64': 'ppc64el', |
464 | + } |
465 | + |
466 | + |
467 | +class VirshVMState: |
468 | + OFF = "shut off" |
469 | + ON = "running" |
470 | + |
471 | + |
472 | +class VirshError(Exception): |
473 | + """Failure communicating to virsh. """ |
474 | + |
475 | + |
476 | +class VirshSSH(pexpect.spawn): |
477 | + |
478 | + PROMPT = r"virsh \#" |
479 | + PROMPT_SSHKEY = "(?i)are you sure you want to continue connecting" |
480 | + PROMPT_PASSWORD = "(?i)(?:password)|(?:passphrase for key)" |
481 | + PROMPT_DENIED = "(?i)permission denied" |
482 | + PROMPT_CLOSED = "(?i)connection closed by remote host" |
483 | + |
484 | + PROMPTS = [ |
485 | + PROMPT_SSHKEY, |
486 | + PROMPT_PASSWORD, |
487 | + PROMPT, |
488 | + PROMPT_DENIED, |
489 | + pexpect.TIMEOUT, |
490 | + PROMPT_CLOSED |
491 | + ] |
492 | + |
493 | + I_PROMPT = PROMPTS.index(PROMPT) |
494 | + I_PROMPT_SSHKEY = PROMPTS.index(PROMPT_SSHKEY) |
495 | + I_PROMPT_PASSWORD = PROMPTS.index(PROMPT_PASSWORD) |
496 | + |
497 | + def __init__(self, timeout=30, maxread=2000): |
498 | + super(VirshSSH, self).__init__( |
499 | + None, timeout=timeout, maxread=maxread) |
500 | + self.name = '<virssh>' |
501 | + |
502 | + def login(self, poweraddr, password=None): |
503 | + """Starts connection to virsh.""" |
504 | + cmd = 'virsh --connect %s' % poweraddr |
505 | + super(VirshSSH, self)._spawn(cmd) |
506 | + |
507 | + i = self.expect(self.PROMPTS, timeout=10) |
508 | + if i == self.I_PROMPT_SSHKEY: |
509 | + # New certificate, lets always accept but if |
510 | + # it changes it will fail to login. |
511 | + self.sendline("yes") |
512 | + i = self.expect(self.EXPECT_PROMPTS) |
513 | + elif i == self.I_PROMPT_PASSWORD: |
514 | + # Requesting password, give it if available. |
515 | + if password is None: |
516 | + self.close() |
517 | + return False |
518 | + self.sendline(password) |
519 | + i = self.expect(self.EXPECT_PROMPTS) |
520 | + |
521 | + if i != self.I_PROMPT: |
522 | + # Something bad happened, either disconnect, |
523 | + # timeout, wrong password. |
524 | + self.close() |
525 | + return False |
526 | + return True |
527 | + |
528 | + def logout(self): |
529 | + """Quits the virsh session.""" |
530 | + self.sendline("quit") |
531 | + self.close() |
532 | + |
533 | + def prompt(self, timeout=None): |
534 | + """Waits for virsh prompt.""" |
535 | + if timeout is None: |
536 | + timeout = self.timeout |
537 | + i = self.expect([self.VIRSH_PROMPT, pexpect.TIMEOUT], timeout=timeout) |
538 | + if i == 1: |
539 | + return False |
540 | + return True |
541 | + |
542 | + def run(self, args): |
543 | + cmd = ' '.join(args) |
544 | + self.sendline(cmd) |
545 | + self.prompt() |
546 | + result = self.before.splitlines() |
547 | + return '\n'.join(result[1:]) |
548 | + |
549 | + def list(self): |
550 | + """Lists all virtual machines by name.""" |
551 | + machines = self.run(['list', '--all', '--name']) |
552 | + return machines.strip().splitlines() |
553 | + |
554 | + def get_state(self, machine): |
555 | + """Gets the virtual machine state.""" |
556 | + state = self.run(['domstate', machine]) |
557 | + state = state.strip() |
558 | + if 'error' in state: |
559 | + return None |
560 | + return state |
561 | + |
562 | + def get_mac_addresses(self, machine): |
563 | + """Gets list of mac addressess assigned to the virtual machine.""" |
564 | + output = self.run(['domiflist', machine]).strip() |
565 | + if 'error' in output: |
566 | + return None |
567 | + output = output.splitlines()[2:] |
568 | + return [line.split()[4] for line in output] |
569 | + |
570 | + def get_arch(self, machine): |
571 | + """Gets the virtual machine architecture.""" |
572 | + output = self.run(['dumpxml', machine]).strip() |
573 | + if 'error' in output: |
574 | + return None |
575 | + |
576 | + doc = etree.XML(output) |
577 | + evaluator = etree.XPathEvaluator(doc) |
578 | + arch = evaluator(XPATH_ARCH)[0] |
579 | + |
580 | + # Fix architectures that need to be referenced by a different |
581 | + # name, that MAAS understands. |
582 | + return ARCH_FIX.get(arch, arch) |
583 | + |
584 | + def poweron(self, machine): |
585 | + """Poweron a virtual machine.""" |
586 | + output = self.run(['start', machine]).strip() |
587 | + if 'error' in output: |
588 | + return False |
589 | + return True |
590 | + |
591 | + def poweroff(self, machine): |
592 | + """Poweroff a virtual machine.""" |
593 | + output = self.run(['destroy', machine]).strip() |
594 | + if 'error' in output: |
595 | + return False |
596 | + return True |
597 | + |
598 | + |
599 | +def probe_virsh_and_enlist(poweraddr, password=None): |
600 | + """Extracts all of the virtual machines from virsh and enlists them |
601 | + into MAAS. |
602 | + |
603 | + :param poweraddr: virsh connection string |
604 | + """ |
605 | + conn = VirshSSH() |
606 | + if not conn.login(poweraddr, password): |
607 | + raise VirshError('Failed to login to virsh console.') |
608 | + |
609 | + for machine in conn.list(): |
610 | + arch = conn.get_arch(machine) |
611 | + state = conn.get_state(machine) |
612 | + macs = conn.get_mac_addresses(machine) |
613 | + |
614 | + # Force the machine off, as MAAS will control the machine |
615 | + # and it needs to be in a known state of off. |
616 | + if state == VirshVMState.ON: |
617 | + conn.poweroff(machine) |
618 | + |
619 | + params = { |
620 | + 'power_address': poweraddr, |
621 | + 'power_id': machine, |
622 | + } |
623 | + if password is not None: |
624 | + params['power_pass'] = password |
625 | + utils.create_node(macs, arch, 'virsh', params) |
626 | + |
627 | + conn.logout() |
628 | + |
629 | + |
630 | +def power_control_virsh(poweraddr, machine, power_change, password=None): |
631 | + """Powers controls a virtual machine using virsh.""" |
632 | + |
633 | + # Force password to None if blank, as the power control |
634 | + # script will send a blank password if one is not set. |
635 | + if password == '': |
636 | + password = None |
637 | + |
638 | + conn = VirshSSH() |
639 | + if not conn.login(poweraddr, password): |
640 | + raise VirshError('Failed to login to virsh console.') |
641 | + |
642 | + state = conn.get_state(machine) |
643 | + if state is None: |
644 | + raise VirshError('Failed to get domain: %s' % machine) |
645 | + |
646 | + if state == VirshVMState.OFF: |
647 | + if power_change == 'on': |
648 | + if conn.poweron(machine) is False: |
649 | + raise VirshError('Failed to power on domain: %s' % machine) |
650 | + elif state == VirshVMState.ON: |
651 | + if power_change == 'off': |
652 | + if conn.poweroff(machine) is False: |
653 | + raise VirshError('Failed to power off domain: %s' % machine) |
654 | |
655 | === modified file 'src/provisioningserver/power/tests/test_poweraction.py' |
656 | --- src/provisioningserver/power/tests/test_poweraction.py 2014-05-06 21:18:17 +0000 |
657 | +++ src/provisioningserver/power/tests/test_poweraction.py 2014-05-21 17:37:04 +0000 |
658 | @@ -158,20 +158,6 @@ |
659 | PowerActionFail, |
660 | pa.execute, power_change='off', mac=factory.getRandomMACAddress()) |
661 | |
662 | - def test_virsh_checks_vm_state(self): |
663 | - # We can't test the virsh template in detail (and it may be |
664 | - # customized), but by making it use "echo" instead of a real |
665 | - # virsh we can make it get a bogus answer from its status check. |
666 | - # The bogus answer is actually the rest of the virsh command |
667 | - # line. It will complain about this and fail. |
668 | - action = PowerAction('virsh') |
669 | - script = action.render_template( |
670 | - action.get_template(), power_change='on', |
671 | - power_address='qemu://example.com/', |
672 | - power_id='mysystem', virsh='echo') |
673 | - output = action.run_shell(script) |
674 | - self.assertIn("Got unknown power state from virsh", output) |
675 | - |
676 | def test_fence_cdu_checks_state(self): |
677 | # We can't test the fence_cdu template in detail (and it may be |
678 | # customized), but by making it use "echo" instead of a real |
679 | |
680 | === modified file 'src/provisioningserver/power_schema.py' |
681 | --- src/provisioningserver/power_schema.py 2014-05-06 21:18:17 +0000 |
682 | +++ src/provisioningserver/power_schema.py 2014-05-21 17:37:04 +0000 |
683 | @@ -168,6 +168,9 @@ |
684 | 'fields': [ |
685 | make_json_field('power_address', "Power address"), |
686 | make_json_field('power_id', "Power ID"), |
687 | + make_json_field( |
688 | + 'power_pass', "Power password (optional)", |
689 | + required=False), |
690 | ], |
691 | }, |
692 | { |
693 | |
694 | === modified file 'src/provisioningserver/tasks.py' |
695 | --- src/provisioningserver/tasks.py 2014-05-06 21:18:17 +0000 |
696 | +++ src/provisioningserver/tasks.py 2014-05-21 17:37:04 +0000 |
697 | @@ -46,6 +46,7 @@ |
698 | probe_seamicro15k_and_enlist, |
699 | ) |
700 | from provisioningserver.custom_hardware.ucsm import probe_and_enlist_ucsm |
701 | +from provisioningserver.custom_hardware.virsh import probe_virsh_and_enlist |
702 | from provisioningserver.dhcp import ( |
703 | config, |
704 | detect, |
705 | @@ -464,7 +465,7 @@ |
706 | @task |
707 | @log_exception_text |
708 | def add_seamicro15k(mac, username, password, power_control=None): |
709 | - """ See `maasserver.api.NodeGroupsHandler.add_seamicro15k`. """ |
710 | + """ See `maasserver.api.NodeGroup.add_seamicro15k`. """ |
711 | ip = find_ip_via_arp(mac) |
712 | if ip is not None: |
713 | probe_seamicro15k_and_enlist( |
714 | @@ -476,6 +477,13 @@ |
715 | |
716 | @task |
717 | @log_exception_text |
718 | +def add_virsh(poweraddr, password=None): |
719 | + """ See `maasserver.api.NodeGroup.add_virsh`. """ |
720 | + probe_virsh_and_enlist(poweraddr, password=password) |
721 | + |
722 | + |
723 | +@task |
724 | +@log_exception_text |
725 | def enlist_nodes_from_ucsm(url, username, password): |
726 | """ See `maasserver.api.NodeGroupsHandler.enlist_nodes_from_ucsm`. """ |
727 | probe_and_enlist_ucsm(url, username, password) |