Merge lp:~newell-jensen/maas/arm64-backport-1.5 into lp:maas/1.5

Proposed by Newell Jensen on 2014-07-24
Status: Merged
Approved by: Newell Jensen on 2014-07-24
Approved revision: 2295
Merged at revision: 2292
Proposed branch: lp:~newell-jensen/maas/arm64-backport-1.5
Merge into: lp:maas/1.5
Diff against target: 725 lines (+582/-1)
12 files modified
etc/maas/templates/power/mscm.template (+15/-0)
src/maasserver/api.py (+24/-0)
src/maasserver/models/nodegroup.py (+11/-0)
src/maasserver/tests/test_api_nodegroup.py (+26/-0)
src/maastesting/factory.py (+9/-0)
src/provisioningserver/driver/__init__.py (+6/-0)
src/provisioningserver/drivers/hardware/mscm.py (+187/-0)
src/provisioningserver/drivers/hardware/tests/test_mscm.py (+259/-0)
src/provisioningserver/power/tests/test_poweraction.py (+11/-0)
src/provisioningserver/power_schema.py (+13/-0)
src/provisioningserver/tasks.py (+9/-1)
src/provisioningserver/tests/test_tasks.py (+12/-0)
To merge this branch: bzr merge lp:~newell-jensen/maas/arm64-backport-1.5
Reviewer Review Type Date Requested Status
Newell Jensen Approve on 2014-07-24
Review via email: mp+228041@code.launchpad.net

Commit message

arm64 enablement backported to MaaS 1.5

To post a comment you must log in.
Newell Jensen (newell-jensen) wrote :

Self review, backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'etc/maas/templates/power/mscm.template'
2--- etc/maas/templates/power/mscm.template 1970-01-01 00:00:00 +0000
3+++ etc/maas/templates/power/mscm.template 2014-07-24 05:13:03 +0000
4@@ -0,0 +1,15 @@
5+# -*- mode: shell-script -*-
6+#
7+# Control a system via Moonshot HP iLO Chassis Manager (MSCM).
8+
9+{{py: from provisioningserver.utils import escape_py_literal}}
10+python - << END
11+from provisioningserver.drivers.hardware.mscm import power_control_mscm
12+power_control_mscm(
13+ {{escape_py_literal(power_address) | safe}},
14+ {{escape_py_literal(power_user) | safe}},
15+ {{escape_py_literal(power_pass) | safe}},
16+ {{escape_py_literal(node_id) | safe}},
17+ {{escape_py_literal(power_change) | safe}},
18+)
19+END
20
21=== added symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template'
22=== target is u'config.commissioning.armhf.template'
23=== added symlink 'etc/maas/templates/pxe/config.install.arm64.template'
24=== target is u'config.install.armhf.template'
25=== added symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template'
26=== target is u'config.xinstall.armhf.template'
27=== modified file 'src/maasserver/api.py'
28--- src/maasserver/api.py 2014-06-19 11:25:08 +0000
29+++ src/maasserver/api.py 2014-07-24 05:13:03 +0000
30@@ -1763,6 +1763,30 @@
31
32 return HttpResponse(status=httplib.OK)
33
34+ @admin_method
35+ @operation(idempotent=False)
36+ def probe_and_enlist_mscm(self, request, uuid):
37+ """Add the nodes from a Moonshot HP iLO Chassis Manager (MSCM).
38+
39+ :param host: IP Address for the MSCM.
40+ :type host: unicode
41+ :param username: The username for the MSCM.
42+ :type username: unicode
43+ :param password: The password for the MSCM.
44+ :type password: unicode
45+
46+ """
47+ nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
48+
49+ host = get_mandatory_param(request.data, 'host')
50+ username = get_mandatory_param(request.data, 'username')
51+ password = get_mandatory_param(request.data, 'password')
52+
53+ nodegroup.enlist_nodes_from_mscm(host, username, password)
54+
55+ return HttpResponse(status=httplib.OK)
56+
57+
58 DISPLAYED_NODEGROUPINTERFACE_FIELDS = (
59 'ip', 'management', 'interface', 'subnet_mask',
60 'broadcast_ip', 'ip_range_low', 'ip_range_high')
61
62=== modified file 'src/maasserver/models/nodegroup.py'
63--- src/maasserver/models/nodegroup.py 2014-05-21 17:30:26 +0000
64+++ src/maasserver/models/nodegroup.py 2014-07-24 05:13:03 +0000
65@@ -42,6 +42,7 @@
66 add_new_dhcp_host_map,
67 add_seamicro15k,
68 add_virsh,
69+ enlist_nodes_from_mscm,
70 enlist_nodes_from_ucsm,
71 import_boot_images,
72 report_boot_images,
73@@ -307,6 +308,16 @@
74 args = (url, username, password)
75 enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args)
76
77+ def enlist_nodes_from_mscm(self, host, username, password):
78+ """ Add the servers from a Moonshot HP iLO Chassis Manager.
79+
80+ :param host: IP address for the MSCM.
81+ :param username: username for MSCM.
82+ :param password: password for MSCM.
83+ """
84+ args = (host, username, password)
85+ enlist_nodes_from_mscm.apply_async(queue=self.uuid, args=args)
86+
87 def add_dhcp_host_maps(self, new_leases):
88 if len(new_leases) > 0 and len(self.get_managed_interfaces()) > 0:
89 # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP
90
91=== modified file 'src/maasserver/tests/test_api_nodegroup.py'
92--- src/maasserver/tests/test_api_nodegroup.py 2014-05-06 21:18:17 +0000
93+++ src/maasserver/tests/test_api_nodegroup.py 2014-07-24 05:13:03 +0000
94@@ -460,6 +460,32 @@
95 matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args)
96 self.assertThat(mock.apply_async, matcher)
97
98+ def test_probe_and_enlist_mscm_adds_mscm(self):
99+ nodegroup = factory.make_node_group()
100+ host = 'http://host'
101+ username = factory.make_name('user')
102+ password = factory.make_name('password')
103+ self.become_admin()
104+
105+ mock = self.patch(nodegroup_module, 'enlist_nodes_from_mscm')
106+
107+ response = self.client.post(
108+ reverse('nodegroup_handler', args=[nodegroup.uuid]),
109+ {
110+ 'op': 'probe_and_enlist_mscm',
111+ 'host': host,
112+ 'username': username,
113+ 'password': password,
114+ })
115+
116+ self.assertEqual(
117+ httplib.OK, response.status_code,
118+ explain_unexpected_response(httplib.OK, response))
119+
120+ args = (host, username, password)
121+ matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args)
122+ self.assertThat(mock.apply_async, matcher)
123+
124
125 class TestNodeGroupAPIAuth(MAASServerTestCase):
126 """Authorization tests for nodegroup API."""
127
128=== modified file 'src/maastesting/factory.py'
129--- src/maastesting/factory.py 2014-03-13 02:46:10 +0000
130+++ src/maastesting/factory.py 2014-07-24 05:13:03 +0000
131@@ -35,6 +35,7 @@
132 from uuid import uuid1
133
134 from maastesting.fixtures import TempDirectory
135+import mock
136 from netaddr import (
137 IPAddress,
138 IPNetwork,
139@@ -264,6 +265,14 @@
140
141 return tarball
142
143+ def make_streams(self, stdin=None, stdout=None, stderr=None):
144+ """Make a fake return value for a SSHClient.exec_command."""
145+ # stdout.read() is called so stdout can't be None.
146+ if stdout is None:
147+ stdout = mock.Mock()
148+
149+ return (stdin, stdout, stderr)
150+
151
152 # Create factory singleton.
153 factory = Factory()
154
155=== modified file 'src/provisioningserver/driver/__init__.py'
156--- src/provisioningserver/driver/__init__.py 2014-06-02 20:14:18 +0000
157+++ src/provisioningserver/driver/__init__.py 2014-07-24 05:13:03 +0000
158@@ -134,6 +134,12 @@
159 Architecture(name="i386/generic", description="i386"),
160 Architecture(name="amd64/generic", description="amd64"),
161 Architecture(
162+ name="arm64/generic", description="arm64/generic",
163+ pxealiases=["arm"]),
164+ Architecture(
165+ name="arm64/xgene-uboot", description="arm64/xgene-uboot",
166+ pxealiases=["arm"]),
167+ Architecture(
168 name="armhf/highbank", description="armhf/highbank",
169 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),
170 Architecture(
171
172=== added directory 'src/provisioningserver/drivers'
173=== added file 'src/provisioningserver/drivers/__init__.py'
174=== added directory 'src/provisioningserver/drivers/hardware'
175=== added file 'src/provisioningserver/drivers/hardware/__init__.py'
176=== added file 'src/provisioningserver/drivers/hardware/mscm.py'
177--- src/provisioningserver/drivers/hardware/mscm.py 1970-01-01 00:00:00 +0000
178+++ src/provisioningserver/drivers/hardware/mscm.py 2014-07-24 05:13:03 +0000
179@@ -0,0 +1,187 @@
180+# Copyright 2014 Canonical Ltd. This software is licensed under the
181+# GNU Affero General Public License version 3 (see the file LICENSE).
182+
183+"""Support for managing nodes via the Moonshot HP iLO Chassis Manager CLI.
184+
185+This module provides support for interacting with HP Moonshot iLO Chassis
186+Management (MSCM) CLI via SSH, and for using that support to allow MAAS to
187+manage systems via iLO.
188+"""
189+
190+from __future__ import (
191+ absolute_import,
192+ print_function,
193+ unicode_literals,
194+ )
195+str = None
196+
197+__metaclass__ = type
198+__all__ = [
199+ 'power_control_mscm',
200+ 'probe_and_enlist_mscm',
201+]
202+
203+import re
204+
205+from paramiko import (
206+ AutoAddPolicy,
207+ SSHClient,
208+ )
209+import provisioningserver.custom_hardware.utils as utils
210+
211+
212+cartridge_mapping = {
213+ 'ProLiant Moonshot Cartridge': 'amd64/generic',
214+ 'ProLiant m300 Server Cartridge': 'amd64/generic',
215+ 'ProLiant m350 Server Cartridge': 'amd64/generic',
216+ 'ProLiant m400 Server Cartridge': 'arm64/xgene-uboot',
217+ 'ProLiant m500 Server Cartridge': 'amd64/generic',
218+ 'ProLiant m710 Server Cartridge': 'amd64/generic',
219+ 'ProLiant m800 Server Cartridge': 'armhf/keystone',
220+ 'Default': 'arm64/generic',
221+}
222+
223+
224+class MSCM_CLI_API(object):
225+ """An API for interacting with the Moonshot iLO CM CLI."""
226+
227+ def __init__(self, host, username, password):
228+ """MSCM_CLI_API Constructor."""
229+ self.host = host
230+ self.username = username
231+ self.password = password
232+ self._ssh = SSHClient()
233+ self._ssh.set_missing_host_key_policy(AutoAddPolicy())
234+
235+ def _run_cli_command(self, command):
236+ """Run a single command and return unparsed text from stdout."""
237+ self._ssh.connect(
238+ self.host, username=self.username, password=self.password)
239+ try:
240+ _, stdout, _ = self._ssh.exec_command(command)
241+ output = stdout.read()
242+ finally:
243+ self._ssh.close()
244+
245+ return output
246+
247+ def discover_nodes(self):
248+ """Discover all available nodes.
249+
250+ Example of stdout from running "show node list":
251+
252+ 'show node list\r\r\nSlot ID Proc Manufacturer
253+ Architecture Memory Power Health\r\n----
254+ ----- ---------------------- --------------------
255+ ------ ----- ------\r\n 01 c1n1 Intel Corporation
256+ x86 Architecture 32 GB On OK \r\n 02 c2n1
257+ N/A No Asset Information \r\n\r\n'
258+
259+ The regex 'c\d+n\d' is finding the node_id's c1-45n1-8
260+ """
261+ node_list = self._run_cli_command("show node list")
262+ return re.findall(r'c\d+n\d', node_list)
263+
264+ def get_node_macaddr(self, node_id):
265+ """Get node MAC address(es).
266+
267+ Example of stdout from running "show node macaddr <node_id>":
268+
269+ 'show node macaddr c1n1\r\r\nSlot ID NIC 1 (Switch A)
270+ NIC 2 (Switch B) NIC 3 (Switch A) NIC 4 (Switch B)\r\n
271+ ---- ----- ----------------- ----------------- -----------------
272+ -----------------\r\n 1 c1n1 a0:1d:48:b5:04:34 a0:1d:48:b5:04:35
273+ a0:1d:48:b5:04:36 a0:1d:48:b5:04:37\r\n\r\n\r\n'
274+
275+ The regex '[\:]'.join(['[0-9A-F]{1,2}'] * 6) is finding
276+ the MAC Addresses for the given node_id.
277+ """
278+ macs = self._run_cli_command("show node macaddr %s" % node_id)
279+ return re.findall(r':'.join(['[0-9a-f]{2}'] * 6), macs)
280+
281+ def get_node_arch(self, node_id):
282+ """Get node architecture.
283+
284+ Example of stdout from running "show node info <node_id>":
285+
286+ 'show node info c1n1\r\r\n\r\nCartridge #1 \r\n Type: Compute\r\n
287+ Manufacturer: HP\r\n Product Name: ProLiant m500 Server Cartridge\r\n'
288+
289+ Parsing this retrieves 'ProLiant m500 Server Cartridge'
290+ """
291+ node_detail = self._run_cli_command("show node info %s" % node_id)
292+ cartridge = node_detail.split('Product Name: ')[1].splitlines()[0]
293+ if cartridge in cartridge_mapping:
294+ return cartridge_mapping[cartridge]
295+ else:
296+ return cartridge_mapping['Default']
297+
298+ def get_node_power_status(self, node_id):
299+ """Get power state of node (on/off).
300+
301+ Example of stdout from running "show node power <node_id>":
302+
303+ 'show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n
304+ Power State: On\r\n'
305+
306+ Parsing this retrieves 'On'
307+ """
308+ power_state = self._run_cli_command("show node power %s" % node_id)
309+ return power_state.split('Power State: ')[1].splitlines()[0]
310+
311+ def power_node_on(self, node_id):
312+ """Power node on."""
313+ return self._run_cli_command("set node power on %s" % node_id)
314+
315+ def power_node_off(self, node_id):
316+ """Power node off."""
317+ return self._run_cli_command("set node power off force %s" % node_id)
318+
319+ def configure_node_boot_m2(self, node_id):
320+ """Configure HDD boot for node."""
321+ return self._run_cli_command("set node boot M.2 %s" % node_id)
322+
323+ def configure_node_bootonce_pxe(self, node_id):
324+ """Configure PXE boot for node once."""
325+ return self._run_cli_command("set node bootonce pxe %s" % node_id)
326+
327+
328+def power_control_mscm(host, username, password, node_id, power_change):
329+ """Handle calls from the power template for nodes with a power type
330+ of 'mscm'.
331+ """
332+ mscm = MSCM_CLI_API(host, username, password)
333+ power_status = mscm.get_node_power_status(node_id)
334+
335+ if power_change == 'off':
336+ mscm.power_node_off(node_id)
337+ return
338+
339+ if power_change != 'on':
340+ raise AssertionError('Unexpected maas power mode.')
341+
342+ if power_status == 'On':
343+ mscm.power_node_off(node_id)
344+
345+ mscm.configure_node_bootonce_pxe(node_id)
346+ mscm.power_node_on(node_id)
347+
348+
349+def probe_and_enlist_mscm(host, username, password):
350+ """ Extracts all of nodes from mscm, sets all of them to boot via HDD by,
351+ default, sets them to bootonce via PXE, and then enlists them into MAAS.
352+ """
353+ mscm = MSCM_CLI_API(host, username, password)
354+ nodes = mscm.discover_nodes()
355+ for node_id in nodes:
356+ # Set default boot to HDD
357+ mscm.configure_node_boot_m2(node_id)
358+ params = {
359+ 'power_address': host,
360+ 'power_user': username,
361+ 'power_pass': password,
362+ 'node_id': node_id,
363+ }
364+ arch = mscm.get_node_arch(node_id)
365+ macs = mscm.get_node_macaddr(node_id)
366+ utils.create_node(macs, arch, 'mscm', params)
367
368=== added directory 'src/provisioningserver/drivers/hardware/tests'
369=== added file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py'
370--- src/provisioningserver/drivers/hardware/tests/test_mscm.py 1970-01-01 00:00:00 +0000
371+++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-07-24 05:13:03 +0000
372@@ -0,0 +1,259 @@
373+# Copyright 2014 Canonical Ltd. This software is licensed under the
374+# GNU Affero General Public License version 3 (see the file LICENSE).
375+
376+"""Tests for ``provisioningserver.drivers.hardware.mscm``."""
377+
378+from __future__ import (
379+ absolute_import,
380+ print_function,
381+ unicode_literals,
382+ )
383+
384+str = None
385+
386+__metaclass__ = type
387+__all__ = []
388+
389+from random import randint
390+import re
391+from StringIO import StringIO
392+
393+from maastesting.factory import factory
394+from maastesting.matchers import MockCalledOnceWith
395+from maastesting.testcase import MAASTestCase
396+from mock import Mock
397+from provisioningserver.drivers.hardware.mscm import (
398+ cartridge_mapping,
399+ MSCM_CLI_API,
400+ power_control_mscm,
401+ probe_and_enlist_mscm,
402+ )
403+import provisioningserver.custom_hardware.utils as utils
404+
405+
406+def make_mscm_api():
407+ """Make a MSCM_CLI_API object with randomized parameters."""
408+ host = factory.make_hostname('mscm')
409+ username = factory.make_name('user')
410+ password = factory.make_name('password')
411+ return MSCM_CLI_API(host, username, password)
412+
413+
414+def make_node_id():
415+ """Make a node_id."""
416+ return 'c%sn%s' % (randint(1, 45), randint(1, 8))
417+
418+
419+def make_show_node_list(length=10):
420+ """Make a fake return value for discover_nodes."""
421+ return re.findall(r'c\d+n\d', ''.join(make_node_id()
422+ for _ in xrange(length)))
423+
424+
425+def make_show_node_macaddr(length=10):
426+ """Make a fake return value for get_node_macaddr."""
427+ return ''.join((factory.getRandomMACAddress() + ' ')
428+ for _ in xrange(length))
429+
430+
431+class TestRunCliCommand(MAASTestCase):
432+ """Tests for ``MSCM_CLI_API.run_cli_command``."""
433+
434+ def test_returns_output(self):
435+ api = make_mscm_api()
436+ ssh_mock = self.patch(api, '_ssh')
437+ expected = factory.make_name('output')
438+ stdout = StringIO(expected)
439+ streams = factory.make_streams(stdout=stdout)
440+ ssh_mock.exec_command = Mock(return_value=streams)
441+ output = api._run_cli_command(factory.make_name('command'))
442+ self.assertEqual(expected, output)
443+
444+ def test_connects_and_closes_ssh_client(self):
445+ api = make_mscm_api()
446+ ssh_mock = self.patch(api, '_ssh')
447+ ssh_mock.exec_command = Mock(return_value=factory.make_streams())
448+ api._run_cli_command(factory.make_name('command'))
449+ self.assertThat(
450+ ssh_mock.connect,
451+ MockCalledOnceWith(
452+ api.host, username=api.username, password=api.password))
453+ self.assertThat(ssh_mock.close, MockCalledOnceWith())
454+
455+ def test_closes_when_exception_raised(self):
456+ api = make_mscm_api()
457+ ssh_mock = self.patch(api, '_ssh')
458+
459+ def fail():
460+ raise Exception('fail')
461+
462+ ssh_mock.exec_command = Mock(side_effect=fail)
463+ command = factory.make_name('command')
464+ self.assertRaises(Exception, api._run_cli_command, command)
465+ self.assertThat(ssh_mock.close, MockCalledOnceWith())
466+
467+
468+class TestDiscoverNodes(MAASTestCase):
469+ """Tests for ``MSCM_CLI_API.discover_nodes``."""
470+
471+ def test_discover_nodes(self):
472+ api = make_mscm_api()
473+ ssh_mock = self.patch(api, '_ssh')
474+ expected = make_show_node_list()
475+ stdout = StringIO(expected)
476+ streams = factory.make_streams(stdout=stdout)
477+ ssh_mock.exec_command = Mock(return_value=streams)
478+ output = api.discover_nodes()
479+ self.assertEqual(expected, output)
480+
481+
482+class TestNodeMACAddress(MAASTestCase):
483+ """Tests for ``MSCM_CLI_API.get_node_macaddr``."""
484+
485+ def test_get_node_macaddr(self):
486+ api = make_mscm_api()
487+ expected = make_show_node_macaddr()
488+ cli_mock = self.patch(api, '_run_cli_command')
489+ cli_mock.return_value = expected
490+ node_id = make_node_id()
491+ output = api.get_node_macaddr(node_id)
492+ self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6),
493+ expected), output)
494+
495+
496+class TestNodeArch(MAASTestCase):
497+ """Tests for ``MSCM_CLI_API.get_node_arch``."""
498+
499+ def test_get_node_arch(self):
500+ api = make_mscm_api()
501+ expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n'
502+ cli_mock = self.patch(api, '_run_cli_command')
503+ cli_mock.return_value = expected
504+ node_id = make_node_id()
505+ output = api.get_node_arch(node_id)
506+ key = expected.split('Product Name: ')[1].splitlines()[0]
507+ self.assertEqual(cartridge_mapping[key], output)
508+
509+
510+class TestGetNodePowerStatus(MAASTestCase):
511+ """Tests for ``MSCM_CLI_API.get_node_power_status``."""
512+
513+ def test_get_node_power_status(self):
514+ api = make_mscm_api()
515+ expected = '\r\n Node #1\r\n Power State: On\r\n'
516+ cli_mock = self.patch(api, '_run_cli_command')
517+ cli_mock.return_value = expected
518+ node_id = make_node_id()
519+ output = api.get_node_power_status(node_id)
520+ self.assertEqual(expected.split('Power State: ')[1].splitlines()[0],
521+ output)
522+
523+
524+class TestPowerAndConfigureNode(MAASTestCase):
525+ """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe,
526+ MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``.
527+ """
528+
529+ scenarios = [
530+ ('power_node_on()',
531+ dict(method='power_node_on')),
532+ ('power_node_off()',
533+ dict(method='power_node_off')),
534+ ('configure_node_bootonce_pxe()',
535+ dict(method='configure_node_bootonce_pxe')),
536+ ]
537+
538+ def test_returns_expected_outout(self):
539+ api = make_mscm_api()
540+ ssh_mock = self.patch(api, '_ssh')
541+ expected = factory.make_name('output')
542+ stdout = StringIO(expected)
543+ streams = factory.make_streams(stdout=stdout)
544+ ssh_mock.exec_command = Mock(return_value=streams)
545+ output = getattr(api, self.method)(make_node_id())
546+ self.assertEqual(expected, output)
547+
548+
549+class TestPowerControlMSCM(MAASTestCase):
550+ """Tests for ``power_control_ucsm``."""
551+
552+ def test_power_control_mscm_on_on(self):
553+ # power_change and power_status are both 'on'
554+ host = factory.make_hostname('mscm')
555+ username = factory.make_name('user')
556+ password = factory.make_name('password')
557+ node_id = make_node_id()
558+ bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
559+ power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
560+ power_status_mock.return_value = 'On'
561+ power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
562+ power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
563+
564+ power_control_mscm(host, username, password, node_id,
565+ power_change='on')
566+ self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
567+ self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
568+ self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
569+
570+ def test_power_control_mscm_on_off(self):
571+ # power_change is 'on' and power_status is 'off'
572+ host = factory.make_hostname('mscm')
573+ username = factory.make_name('user')
574+ password = factory.make_name('password')
575+ node_id = make_node_id()
576+ bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
577+ power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
578+ power_status_mock.return_value = 'Off'
579+ power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
580+
581+ power_control_mscm(host, username, password, node_id,
582+ power_change='on')
583+ self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
584+ self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
585+
586+ def test_power_control_mscm_off_on(self):
587+ # power_change is 'off' and power_status is 'on'
588+ host = factory.make_hostname('mscm')
589+ username = factory.make_name('user')
590+ password = factory.make_name('password')
591+ node_id = make_node_id()
592+ power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
593+ power_status_mock.return_value = 'On'
594+ power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
595+
596+ power_control_mscm(host, username, password, node_id,
597+ power_change='off')
598+ self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
599+
600+
601+class TestProbeAndEnlistMSCM(MAASTestCase):
602+ """Tests for ``probe_and_enlist_mscm``."""
603+
604+ def test_probe_and_enlist(self):
605+ host = factory.make_hostname('mscm')
606+ username = factory.make_name('user')
607+ password = factory.make_name('password')
608+ node_id = make_node_id()
609+ macs = make_show_node_macaddr(4)
610+ arch = 'arm64/xgene-uboot'
611+ discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes')
612+ discover_nodes_mock.return_value = [node_id]
613+ boot_m2_mock = self.patch(MSCM_CLI_API, 'configure_node_boot_m2')
614+ node_arch_mock = self.patch(MSCM_CLI_API, 'get_node_arch')
615+ node_arch_mock.return_value = arch
616+ node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr')
617+ node_macs_mock.return_value = macs
618+ create_node_mock = self.patch(utils, 'create_node')
619+ probe_and_enlist_mscm(host, username, password)
620+ self.assertThat(discover_nodes_mock, MockCalledOnceWith())
621+ self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id))
622+ self.assertThat(node_arch_mock, MockCalledOnceWith(node_id))
623+ self.assertThat(node_macs_mock, MockCalledOnceWith(node_id))
624+ params = {
625+ 'power_address': host,
626+ 'power_user': username,
627+ 'power_pass': password,
628+ 'node_id': node_id,
629+ }
630+ self.assertThat(create_node_mock,
631+ MockCalledOnceWith(macs, arch, 'mscm', params))
632
633=== modified file 'src/provisioningserver/power/tests/test_poweraction.py'
634--- src/provisioningserver/power/tests/test_poweraction.py 2014-05-21 17:30:26 +0000
635+++ src/provisioningserver/power/tests/test_poweraction.py 2014-07-24 05:13:03 +0000
636@@ -220,3 +220,14 @@
637 power_user='bar', power_pass='baz',
638 uuid=factory.getRandomUUID(), power_change='on')
639 self.assertIn('power_control_ucsm', script)
640+
641+ def test_mscm_renders_template(self):
642+ # I'd like to assert that escape_py_literal is being used here,
643+ # but it's not obvious how to mock things in the template
644+ # rendering namespace so I passed on that.
645+ action = PowerAction('mscm')
646+ script = action.render_template(
647+ action.get_template(), power_address='foo',
648+ power_user='bar', power_pass='baz',
649+ node_id='c1n1', power_change='on')
650+ self.assertIn('power_control_mscm', script)
651
652=== modified file 'src/provisioningserver/power_schema.py'
653--- src/provisioningserver/power_schema.py 2014-05-21 17:30:26 +0000
654+++ src/provisioningserver/power_schema.py 2014-07-24 05:13:03 +0000
655@@ -255,4 +255,17 @@
656 make_json_field('power_pass', "API password"),
657 ],
658 },
659+ {
660+ 'name': 'mscm',
661+ 'description': "Moonshot HP iLO Chassis Manager",
662+ 'fields': [
663+ make_json_field('power_address', "IP for MSCM CLI API"),
664+ make_json_field('power_user', "MSCM CLI API user"),
665+ make_json_field('power_pass', "MSCM CLI API password"),
666+ make_json_field(
667+ 'node_id',
668+ "Node ID - Must adhere to cXnY format "
669+ "(X=cartridge number, Y=node number)."),
670+ ],
671+ },
672 ]
673
674=== modified file 'src/provisioningserver/tasks.py'
675--- src/provisioningserver/tasks.py 2014-06-02 07:48:17 +0000
676+++ src/provisioningserver/tasks.py 2014-07-24 05:13:03 +0000
677@@ -58,6 +58,7 @@
678 set_up_options_conf,
679 setup_rndc,
680 )
681+from provisioningserver.drivers.hardware.mscm import probe_and_enlist_mscm
682 from provisioningserver.omshell import Omshell
683 from provisioningserver.power.poweraction import (
684 PowerAction,
685@@ -493,5 +494,12 @@
686 @task
687 @log_exception_text
688 def enlist_nodes_from_ucsm(url, username, password):
689- """ See `maasserver.api.NodeGroupsHandler.enlist_nodes_from_ucsm`. """
690+ """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_ucsm`. """
691 probe_and_enlist_ucsm(url, username, password)
692+
693+
694+@task
695+@log_exception_text
696+def enlist_nodes_from_mscm(host, username, password):
697+ """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_mscm`. """
698+ probe_and_enlist_mscm(host, username, password)
699
700=== modified file 'src/provisioningserver/tests/test_tasks.py'
701--- src/provisioningserver/tests/test_tasks.py 2014-06-02 07:48:17 +0000
702+++ src/provisioningserver/tests/test_tasks.py 2014-07-24 05:13:03 +0000
703@@ -73,6 +73,7 @@
704 from provisioningserver.tags import MissingCredentials
705 from provisioningserver.tasks import (
706 add_new_dhcp_host_map,
707+ enlist_nodes_from_mscm,
708 enlist_nodes_from_ucsm,
709 import_boot_images,
710 Omshell,
711@@ -661,3 +662,14 @@
712 mock = self.patch(tasks, 'probe_and_enlist_ucsm')
713 enlist_nodes_from_ucsm(url, username, password)
714 self.assertThat(mock, MockCalledOnceWith(url, username, password))
715+
716+
717+class TestAddMSCM(PservTestCase):
718+
719+ def test_enlist_nodes_from_mscm(self):
720+ host = 'host'
721+ username = 'username'
722+ password = 'password'
723+ mock = self.patch(tasks, 'probe_and_enlist_mscm')
724+ enlist_nodes_from_mscm(host, username, password)
725+ self.assertThat(mock, MockCalledOnceWith(host, username, password))

Subscribers

People subscribed via source and target branches

to all changes: