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