Merge lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428 into lp:~maas-committers/maas/trunk

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: no longer in the source branch.
Merged at revision: 3325
Proposed branch: lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 545 lines (+259/-128)
4 files modified
etc/maas/templates/power/mscm.template (+58/-8)
src/provisioningserver/drivers/hardware/mscm.py (+44/-15)
src/provisioningserver/drivers/hardware/tests/test_mscm.py (+156/-104)
src/provisioningserver/rpc/power.py (+1/-1)
To merge this branch: bzr merge lp:~newell-jensen/maas/add-mscm-power-query-bug-1384428
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Blake Rouse (community) Approve
Review via email: mp+239917@code.launchpad.net

Commit message

This branch adds power querying capabilities to MSCM. Many of the tests for MSCM have also been cleaned up and reorganized.

Description of the change

This was tested with packages on an MSCM system.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

This passed CI testing as well.

Revision history for this message
Christian Reis (kiko) wrote :

Could you ask Sean F. or Narinder to test this?

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Kiko,

Sean tested it and says it works good. I also tested it.

Revision history for this message
Sean Feole (sfeole) wrote :

Tested and verified on maas 1.7

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks great.

Only one comment inline.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good, couple of remarks but nothing major.

review: Approve
Revision history for this message
Newell Jensen (newell-jensen) wrote :

In commit I said "fixed error" but should have said "fixed exception message".

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/maas/templates/power/mscm.template'
2--- etc/maas/templates/power/mscm.template 2014-07-18 17:05:57 +0000
3+++ etc/maas/templates/power/mscm.template 2014-10-30 20:06:36 +0000
4@@ -2,13 +2,63 @@
5 #
6 # Control a system via Moonshot HP iLO Chassis Manager (MSCM).
7
8+# Exit with failure message.
9+# Parameters: exit code, and error message.
10+fail() {
11+ echo "$2" >&2
12+ exit $1
13+}
14+
15+issue_mscm_command() {
16 python - << END
17+import sys
18 from provisioningserver.drivers.hardware.mscm import power_control_mscm
19-power_control_mscm(
20- {{escape_py_literal(power_address) | safe}},
21- {{escape_py_literal(power_user) | safe}},
22- {{escape_py_literal(power_pass) | safe}},
23- {{escape_py_literal(node_id) | safe}},
24- {{escape_py_literal(power_change) | safe}},
25-)
26-END
27+try:
28+ power_control_mscm(
29+ {{escape_py_literal(power_address) | safe}},
30+ {{escape_py_literal(power_user) | safe}},
31+ {{escape_py_literal(power_pass) | safe}},
32+ {{escape_py_literal(node_id) | safe}},
33+ {{escape_py_literal(power_change) | safe}},
34+ )
35+except Exception as e:
36+ # This gets in the node event log: print the exception's message
37+ # and not the stacktrace.
38+ print(unicode(e))
39+ sys.exit(1)
40+END
41+}
42+
43+query_state() {
44+python - << END
45+import sys
46+from provisioningserver.drivers.hardware.mscm import power_state_mscm
47+try:
48+ print(power_state_mscm(
49+ {{escape_py_literal(power_address) | safe}},
50+ {{escape_py_literal(power_user) | safe}},
51+ {{escape_py_literal(power_pass) | safe}},
52+ {{escape_py_literal(node_id) | safe}},
53+ ))
54+except Exception as e:
55+ # This gets in the node event log: print the exception's message
56+ # and not the stacktrace.
57+ print(unicode(e))
58+ sys.exit(1)
59+END
60+}
61+
62+main() {
63+ case $1 in
64+ 'on'|'off')
65+ issue_mscm_command
66+ ;;
67+ 'query')
68+ query_state
69+ ;;
70+ *)
71+ fail 2 "Unknown power command: '$1'"
72+ esac
73+}
74+
75+main "{{power_change}}"
76
77=== modified file 'src/provisioningserver/drivers/hardware/mscm.py'
78--- src/provisioningserver/drivers/hardware/mscm.py 2014-09-24 22:20:11 +0000
79+++ src/provisioningserver/drivers/hardware/mscm.py 2014-10-30 20:06:36 +0000
80@@ -18,6 +18,7 @@
81 __metaclass__ = type
82 __all__ = [
83 'power_control_mscm',
84+ 'power_state_mscm',
85 'probe_and_enlist_mscm',
86 ]
87
88@@ -43,6 +44,15 @@
89 }
90
91
92+class MSCMState:
93+ OFF = "Off"
94+ ON = "On"
95+
96+
97+class MSCMError(Exception):
98+ """Failure communicating to MSCM. """
99+
100+
101 class MSCM_CLI_API:
102 """An API for interacting with the Moonshot iLO CM CLI."""
103
104@@ -117,7 +127,7 @@
105 else:
106 return cartridge_mapping['Default']
107
108- def get_node_power_status(self, node_id):
109+ def get_node_power_state(self, node_id):
110 """Get power state of node (on/off).
111
112 Example of stdout from running "show node power <node_id>":
113@@ -152,30 +162,49 @@
114 of 'mscm'.
115 """
116 mscm = MSCM_CLI_API(host, username, password)
117- power_status = mscm.get_node_power_status(node_id)
118
119 if power_change == 'off':
120 mscm.power_node_off(node_id)
121- return
122-
123- if power_change != 'on':
124- raise AssertionError('Unexpected maas power mode.')
125-
126- if power_status == 'On':
127- mscm.power_node_off(node_id)
128-
129- mscm.configure_node_bootonce_pxe(node_id)
130- mscm.power_node_on(node_id)
131+ elif power_change == 'on':
132+ if mscm.get_node_power_state(node_id) == MSCMState.ON:
133+ mscm.power_node_off(node_id)
134+ mscm.configure_node_bootonce_pxe(node_id)
135+ mscm.power_node_on(node_id)
136+ else:
137+ raise MSCMError("Unexpected maas power mode.")
138+
139+
140+def power_state_mscm(host, username, password, node_id):
141+ """Return the power state for the mscm machine."""
142+ mscm = MSCM_CLI_API(host, username, password)
143+ try:
144+ power_state = mscm.get_node_power_state(node_id)
145+ except:
146+ raise MSCMError("Failed to retrieve power state.")
147+
148+ if power_state == MSCMState.OFF:
149+ return 'off'
150+ elif power_state == MSCMState.ON:
151+ return 'on'
152+ raise MSCMError('Unknown power state: %s' % power_state)
153
154
155 def probe_and_enlist_mscm(host, username, password):
156- """ Extracts all of nodes from mscm, sets all of them to boot via HDD by,
157+ """ Extracts all of nodes from mscm, sets all of them to boot via M.2 by,
158 default, sets them to bootonce via PXE, and then enlists them into MAAS.
159 """
160 mscm = MSCM_CLI_API(host, username, password)
161- nodes = mscm.discover_nodes()
162+ try:
163+ # if discover_nodes works, we have access to the system
164+ nodes = mscm.discover_nodes()
165+ except:
166+ raise MSCMError(
167+ "Failed to probe nodes for mscm with host=%s, "
168+ "username=%s, password=%s"
169+ % (host, username, password))
170+
171 for node_id in nodes:
172- # Set default boot to HDD
173+ # Set default boot to M.2
174 mscm.configure_node_boot_m2(node_id)
175 params = {
176 'power_address': host,
177
178=== modified file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py'
179--- src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-09-18 12:44:38 +0000
180+++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-10-30 20:06:36 +0000
181@@ -19,16 +19,24 @@
182 from StringIO import StringIO
183
184 from maastesting.factory import factory
185-from maastesting.matchers import MockCalledOnceWith
186+from maastesting.matchers import (
187+ MockAnyCall,
188+ MockCalledOnceWith,
189+ MockCalledWith,
190+ )
191 from maastesting.testcase import MAASTestCase
192 from mock import Mock
193 from provisioningserver.drivers.hardware.mscm import (
194 cartridge_mapping,
195 MSCM_CLI_API,
196+ MSCMError,
197+ MSCMState,
198 power_control_mscm,
199+ power_state_mscm,
200 probe_and_enlist_mscm,
201 )
202 import provisioningserver.utils as utils
203+from testtools.matchers import Equals
204
205
206 def make_mscm_api():
207@@ -56,10 +64,19 @@
208 for _ in xrange(length))
209
210
211-class TestRunCliCommand(MAASTestCase):
212- """Tests for ``MSCM_CLI_API.run_cli_command``."""
213-
214- def test_returns_output(self):
215+class TestMSCMCliApi(MAASTestCase):
216+ """Tests for `MSCM_CLI_API`."""
217+
218+ scenarios = [
219+ ('power_node_on',
220+ dict(method='power_node_on')),
221+ ('power_node_off',
222+ dict(method='power_node_off')),
223+ ('configure_node_bootonce_pxe',
224+ dict(method='configure_node_bootonce_pxe')),
225+ ]
226+
227+ def test_run_cli_command_returns_output(self):
228 api = make_mscm_api()
229 ssh_mock = self.patch(api, '_ssh')
230 expected = factory.make_name('output')
231@@ -69,18 +86,18 @@
232 output = api._run_cli_command(factory.make_name('command'))
233 self.assertEqual(expected, output)
234
235- def test_connects_and_closes_ssh_client(self):
236+ def test_run_cli_command_connects_and_closes_ssh_client(self):
237 api = make_mscm_api()
238 ssh_mock = self.patch(api, '_ssh')
239 ssh_mock.exec_command = Mock(return_value=factory.make_streams())
240 api._run_cli_command(factory.make_name('command'))
241- self.assertThat(
242+ self.expectThat(
243 ssh_mock.connect,
244 MockCalledOnceWith(
245 api.host, username=api.username, password=api.password))
246- self.assertThat(ssh_mock.close, MockCalledOnceWith())
247+ self.expectThat(ssh_mock.close, MockCalledOnceWith())
248
249- def test_closes_when_exception_raised(self):
250+ def test_run_cli_command_closes_when_exception_raised(self):
251 api = make_mscm_api()
252 ssh_mock = self.patch(api, '_ssh')
253
254@@ -90,11 +107,7 @@
255 ssh_mock.exec_command = Mock(side_effect=fail)
256 command = factory.make_name('command')
257 self.assertRaises(Exception, api._run_cli_command, command)
258- self.assertThat(ssh_mock.close, MockCalledOnceWith())
259-
260-
261-class TestDiscoverNodes(MAASTestCase):
262- """Tests for ``MSCM_CLI_API.discover_nodes``."""
263+ self.expectThat(ssh_mock.close, MockCalledOnceWith())
264
265 def test_discover_nodes(self):
266 api = make_mscm_api()
267@@ -106,10 +119,6 @@
268 output = api.discover_nodes()
269 self.assertEqual(expected, output)
270
271-
272-class TestNodeMACAddress(MAASTestCase):
273- """Tests for ``MSCM_CLI_API.get_node_macaddr``."""
274-
275 def test_get_node_macaddr(self):
276 api = make_mscm_api()
277 expected = make_show_node_macaddr()
278@@ -120,10 +129,6 @@
279 self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6),
280 expected), output)
281
282-
283-class TestNodeArch(MAASTestCase):
284- """Tests for ``MSCM_CLI_API.get_node_arch``."""
285-
286 def test_get_node_arch(self):
287 api = make_mscm_api()
288 expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n'
289@@ -134,36 +139,17 @@
290 key = expected.split('Product Name: ')[1].splitlines()[0]
291 self.assertEqual(cartridge_mapping[key], output)
292
293-
294-class TestGetNodePowerStatus(MAASTestCase):
295- """Tests for ``MSCM_CLI_API.get_node_power_status``."""
296-
297- def test_get_node_power_status(self):
298+ def test_get_node_power_state(self):
299 api = make_mscm_api()
300 expected = '\r\n Node #1\r\n Power State: On\r\n'
301 cli_mock = self.patch(api, '_run_cli_command')
302 cli_mock.return_value = expected
303 node_id = make_node_id()
304- output = api.get_node_power_status(node_id)
305+ output = api.get_node_power_state(node_id)
306 self.assertEqual(expected.split('Power State: ')[1].splitlines()[0],
307 output)
308
309-
310-class TestPowerAndConfigureNode(MAASTestCase):
311- """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe,
312- MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``.
313- """
314-
315- scenarios = [
316- ('power_node_on()',
317- dict(method='power_node_on')),
318- ('power_node_off()',
319- dict(method='power_node_off')),
320- ('configure_node_bootonce_pxe()',
321- dict(method='configure_node_bootonce_pxe')),
322- ]
323-
324- def test_returns_expected_outout(self):
325+ def test_power_and_configure_node_returns_expected_outout(self):
326 api = make_mscm_api()
327 ssh_mock = self.patch(api, '_ssh')
328 expected = factory.make_name('output')
329@@ -174,60 +160,8 @@
330 self.assertEqual(expected, output)
331
332
333-class TestPowerControlMSCM(MAASTestCase):
334- """Tests for ``power_control_ucsm``."""
335-
336- def test_power_control_mscm_on_on(self):
337- # power_change and power_status are both 'on'
338- host = factory.make_hostname('mscm')
339- username = factory.make_name('user')
340- password = factory.make_name('password')
341- node_id = make_node_id()
342- bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
343- power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
344- power_status_mock.return_value = 'On'
345- power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
346- power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
347-
348- power_control_mscm(host, username, password, node_id,
349- power_change='on')
350- self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
351- self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
352- self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
353-
354- def test_power_control_mscm_on_off(self):
355- # power_change is 'on' and power_status is 'off'
356- host = factory.make_hostname('mscm')
357- username = factory.make_name('user')
358- password = factory.make_name('password')
359- node_id = make_node_id()
360- bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
361- power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
362- power_status_mock.return_value = 'Off'
363- power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
364-
365- power_control_mscm(host, username, password, node_id,
366- power_change='on')
367- self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
368- self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
369-
370- def test_power_control_mscm_off_on(self):
371- # power_change is 'off' and power_status is 'on'
372- host = factory.make_hostname('mscm')
373- username = factory.make_name('user')
374- password = factory.make_name('password')
375- node_id = make_node_id()
376- power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
377- power_status_mock.return_value = 'On'
378- power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
379-
380- power_control_mscm(host, username, password, node_id,
381- power_change='off')
382- self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
383-
384-
385-class TestProbeAndEnlistMSCM(MAASTestCase):
386- """Tests for ``probe_and_enlist_mscm``."""
387+class TestMSCMProbeAndEnlist(MAASTestCase):
388+ """Tests for `probe_and_enlist_mscm`."""
389
390 def test_probe_and_enlist(self):
391 host = factory.make_hostname('mscm')
392@@ -244,16 +178,134 @@
393 node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr')
394 node_macs_mock.return_value = macs
395 create_node_mock = self.patch(utils, 'create_node')
396- probe_and_enlist_mscm(host, username, password)
397- self.assertThat(discover_nodes_mock, MockCalledOnceWith())
398- self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id))
399- self.assertThat(node_arch_mock, MockCalledOnceWith(node_id))
400- self.assertThat(node_macs_mock, MockCalledOnceWith(node_id))
401 params = {
402 'power_address': host,
403 'power_user': username,
404 'power_pass': password,
405 'node_id': node_id,
406 }
407- self.assertThat(create_node_mock,
408+
409+ probe_and_enlist_mscm(host, username, password)
410+ self.expectThat(discover_nodes_mock, MockAnyCall())
411+ self.expectThat(boot_m2_mock, MockCalledWith(node_id))
412+ self.expectThat(node_arch_mock, MockCalledOnceWith(node_id))
413+ self.expectThat(node_macs_mock, MockCalledOnceWith(node_id))
414+ self.expectThat(create_node_mock,
415 MockCalledOnceWith(macs, arch, 'mscm', params))
416+
417+ def test_probe_and_enlist_discover_nodes_failure(self):
418+ host = factory.make_hostname('mscm')
419+ username = factory.make_name('user')
420+ password = factory.make_name('password')
421+ discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes')
422+ discover_nodes_mock.side_effect = MSCMError('error')
423+ self.assertRaises(
424+ MSCMError, probe_and_enlist_mscm, host, username, password)
425+
426+
427+class TestMSCMPowerControl(MAASTestCase):
428+ """Tests for `power_control_mscm`."""
429+
430+ def test_power_control_error_on_unknown_power_change(self):
431+ host = factory.make_hostname('mscm')
432+ username = factory.make_name('user')
433+ password = factory.make_name('password')
434+ node_id = make_node_id()
435+ power_change = factory.make_name('error')
436+ self.assertRaises(
437+ MSCMError, power_control_mscm, host,
438+ username, password, node_id, power_change)
439+
440+ def test_power_control_power_change_on_power_state_on(self):
441+ # power_change and current power_state are both 'on'
442+ host = factory.make_hostname('mscm')
443+ username = factory.make_name('user')
444+ password = factory.make_name('password')
445+ node_id = make_node_id()
446+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
447+ power_state_mock.return_value = MSCMState.ON
448+ power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
449+ bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
450+ power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
451+
452+ power_control_mscm(host, username, password, node_id,
453+ power_change='on')
454+ self.expectThat(power_state_mock, MockCalledOnceWith(node_id))
455+ self.expectThat(power_node_off_mock, MockCalledOnceWith(node_id))
456+ self.expectThat(bootonce_mock, MockCalledOnceWith(node_id))
457+ self.expectThat(power_node_on_mock, MockCalledOnceWith(node_id))
458+
459+ def test_power_control_power_change_on_power_state_off(self):
460+ # power_change is 'on' and current power_state is 'off'
461+ host = factory.make_hostname('mscm')
462+ username = factory.make_name('user')
463+ password = factory.make_name('password')
464+ node_id = make_node_id()
465+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
466+ power_state_mock.return_value = MSCMState.OFF
467+ bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
468+ power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
469+
470+ power_control_mscm(host, username, password, node_id,
471+ power_change='on')
472+ self.expectThat(power_state_mock, MockCalledOnceWith(node_id))
473+ self.expectThat(bootonce_mock, MockCalledOnceWith(node_id))
474+ self.expectThat(power_node_on_mock, MockCalledOnceWith(node_id))
475+
476+ def test_power_control_power_change_off_power_state_on(self):
477+ # power_change is 'off' and current power_state is 'on'
478+ host = factory.make_hostname('mscm')
479+ username = factory.make_name('user')
480+ password = factory.make_name('password')
481+ node_id = make_node_id()
482+ power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
483+
484+ power_control_mscm(host, username, password, node_id,
485+ power_change='off')
486+ self.expectThat(power_node_off_mock, MockCalledOnceWith(node_id))
487+
488+
489+class TestMSCMPowerState(MAASTestCase):
490+ """Tests for `power_state_mscm`."""
491+
492+ def test_power_state_failed_to_get_state(self):
493+ host = factory.make_hostname('mscm')
494+ username = factory.make_name('user')
495+ password = factory.make_name('password')
496+ node_id = make_node_id()
497+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
498+ power_state_mock.side_effect = MSCMError('error')
499+ self.assertRaises(
500+ MSCMError, power_state_mscm, host, username, password, node_id)
501+
502+ def test_power_state_get_off(self):
503+ host = factory.make_hostname('mscm')
504+ username = factory.make_name('user')
505+ password = factory.make_name('password')
506+ node_id = make_node_id()
507+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
508+ power_state_mock.return_value = MSCMState.OFF
509+ self.assertThat(
510+ power_state_mscm(host, username, password, node_id),
511+ Equals('off'))
512+
513+ def test_power_state_get_on(self):
514+ host = factory.make_hostname('mscm')
515+ username = factory.make_name('user')
516+ password = factory.make_name('password')
517+ node_id = make_node_id()
518+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
519+ power_state_mock.return_value = MSCMState.ON
520+ self.assertThat(
521+ power_state_mscm(host, username, password, node_id),
522+ Equals('on'))
523+
524+ def test_power_state_error_on_unknown_state(self):
525+ host = factory.make_hostname('mscm')
526+ username = factory.make_name('user')
527+ password = factory.make_name('password')
528+ node_id = make_node_id()
529+ power_state_mock = self.patch(MSCM_CLI_API, 'get_node_power_state')
530+ power_state_mock.return_value = factory.make_name('error')
531+ self.assertRaises(
532+ MSCMError, power_state_mscm, host, username, password, node_id)
533
534=== modified file 'src/provisioningserver/rpc/power.py'
535--- src/provisioningserver/rpc/power.py 2014-10-27 11:56:50 +0000
536+++ src/provisioningserver/rpc/power.py 2014-10-30 20:06:36 +0000
537@@ -61,7 +61,7 @@
538 # state for these power types.
539 # This is meant to be temporary until all the power types support
540 # querying the power state of a node.
541-QUERY_POWER_TYPES = ['amt', 'ipmi', 'virsh']
542+QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'virsh']
543
544
545 # Timeout for change_power_state(). We set it to 2 minutes by default,