Merge lp:~blake-rouse/maas/virsh-probe-and-enlist-1.5 into lp:maas/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
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+220509@code.launchpad.net

Commit message

Use probe-and-enlist-hardware to enlist all virtual machine inside a libvirt machine, allow password qemu+ssh connections.

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)

Subscribers

People subscribed via source and target branches

to all changes: