Merge lp:~andreserl/maas/sm15k-power-query-lp1384424 into lp:~maas-committers/maas/trunk

Proposed by Andres Rodriguez
Status: Merged
Approved by: Andres Rodriguez
Approved revision: no longer in the source branch.
Merged at revision: 3403
Proposed branch: lp:~andreserl/maas/sm15k-power-query-lp1384424
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 378 lines (+249/-40)
5 files modified
etc/maas/templates/power/sm15k.template (+105/-31)
src/provisioningserver/drivers/hardware/seamicro.py (+18/-6)
src/provisioningserver/drivers/hardware/tests/test_seamicro.py (+69/-0)
src/provisioningserver/rpc/power.py (+3/-3)
src/provisioningserver/rpc/tests/test_power.py (+54/-0)
To merge this branch: bzr merge lp:~andreserl/maas/sm15k-power-query-lp1384424
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+243897@code.launchpad.net

Commit message

Support the ability to query the power status of sm15k using RESTAPI v2.0

Description of the change

Support the ability to query the power status of sm15k using RESTAPI v2.0. This merges Blake branches + fixes to the code and tests.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Self approving. this was tested manually too.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'etc/maas/templates/power/sm15k.template'
--- etc/maas/templates/power/sm15k.template 2014-06-10 14:45:14 +0000
+++ etc/maas/templates/power/sm15k.template 2014-12-06 16:48:48 +0000
@@ -3,50 +3,124 @@
3# Control a system via ipmipower, sending the seamicro specific hex codes3# Control a system via ipmipower, sending the seamicro specific hex codes
4#4#
55
6# Parameters6# Exit with failure message.
7power_change={{power_change}}7# Parameters: exit code, and error message.
8power_address={{power_address}}8fail() {
9power_user={{power_user}}9 echo "$2" >&2
10power_pass={{power_pass}}10 exit $1
11power_control={{power_control}}11}
12system_id={{system_id}}
13ipmitool={{ipmitool}}
14
15# IPMI power mode
16{{py: power_mode = 1 if power_change == 'on' else 6 }}
17power_mode={{power_mode}}
1812
19# Control power using IPMI13# Control power using IPMI
20issue_ipmi_command() {14issue_ipmi_command() {
21 ${ipmitool} -I lanplus \15 {{py: power_mode = 1 if power_change == 'on' else 6 }}
22 -H ${power_address} -U ${power_user}\16 {{ipmitool}} -I lanplus \
23 -P ${power_pass} raw 0x2E 1 0x00 0x7d 0xab \17 -H {{power_address}} -U {{power_user}}\
24 ${power_mode} 0 ${system_id}18 -P {{power_pass}} raw 0x2E 1 0x00 0x7d 0xab \
19 {{power_mode}} 0 {{system_id}}
25}20}
2621
27# Control power using REST v0.922# Control power using REST v0.9
28issue_rest_v09_command() {23issue_rest_v09_command() {
29python - << END24python - << END
25import sys
30from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v0926from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v09
31power_control_seamicro15k_v09("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}")27try:
28 power_control_seamicro15k_v09(
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(system_id) | safe}},
33 {{escape_py_literal(power_change) | safe}},
34 )
35except 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)
32END40END
33}41}
3442
35# Control power using REST v243# Control power using REST v2
36issue_rest_v2_command() {44issue_rest_v2_command() {
37python - << END45python - << END
46import sys
38from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v247from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v2
39power_control_seamicro15k_v2("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}")48try:
40END49 power_control_seamicro15k_v2(
41}50 {{escape_py_literal(power_address) | safe}},
4251 {{escape_py_literal(power_user) | safe}},
43if [ "${power_control}" = "ipmi" ]52 {{escape_py_literal(power_pass) | safe}},
44then53 {{escape_py_literal(system_id) | safe}},
45 issue_ipmi_command54 {{escape_py_literal(power_change) | safe}},
46elif [ "${power_control}" = "restapi" ]55 )
47then56except Exception as e:
48 issue_rest_v09_command57 # This gets in the node event log: print the exception's message
49elif [ "${power_control}" = "restapi2" ]58 # and not the stacktrace.
50then59 print(unicode(e))
51 issue_rest_v2_command60 sys.exit(1)
52fi61END
62}
63
64# Query power state using REST v2
65query_state_rest_v2() {
66python - << END
67import sys
68from provisioningserver.drivers.hardware.seamicro import power_query_seamicro15k_v2
69try:
70 print(power_query_seamicro15k_v2(
71 {{escape_py_literal(power_address) | safe}},
72 {{escape_py_literal(power_user) | safe}},
73 {{escape_py_literal(power_pass) | safe}},
74 {{escape_py_literal(system_id) | safe}},
75 ))
76except Exception as e:
77 # This gets in the node event log: print the exception's message
78 # and not the stacktrace.
79 print(unicode(e))
80 sys.exit(1)
81END
82}
83
84# Perform power control
85power_control() {
86 if [ "{{power_control}}" = "ipmi" ]
87 then
88 issue_ipmi_command
89 elif [ "{{power_control}}" = "restapi" ]
90 then
91 issue_rest_v09_command
92 elif [ "{{power_control}}" = "restapi2" ]
93 then
94 issue_rest_v2_command
95 fi
96}
97
98# Query the state.
99# Only supported by REST v2.
100query_state() {
101 if [ "{{power_control}}" = "ipmi" ]
102 then
103 echo "unknown"
104 elif [ "{{power_control}}" = "restapi" ]
105 then
106 echo "unknown"
107 elif [ "{{power_control}}" = "restapi2" ]
108 then
109 query_state_rest_v2
110 fi
111}
112
113main() {
114 case $1 in
115 'on'|'off')
116 power_control
117 ;;
118 'query')
119 query_state
120 ;;
121 *)
122 fail 2 "Unknown power command: '$1'"
123 esac
124}
125
126main "{{power_change}}"
53127
=== modified file 'src/provisioningserver/drivers/hardware/seamicro.py'
--- src/provisioningserver/drivers/hardware/seamicro.py 2014-11-17 11:56:49 +0000
+++ src/provisioningserver/drivers/hardware/seamicro.py 2014-12-06 16:48:48 +0000
@@ -321,9 +321,21 @@
321 power_change):321 power_change):
322 server_id = '%s/0' % server_id322 server_id = '%s/0' % server_id
323 api = get_seamicro15k_api('v2.0', ip, username, password)323 api = get_seamicro15k_api('v2.0', ip, username, password)
324 if api:324 if api is None:
325 server = api.servers.get(server_id)325 raise SeaMicroError('Unable to contact BMC controller.')
326 if power_change == "on":326 server = api.servers.get(server_id)
327 server.power_on(using_pxe=True)327 if power_change == "on":
328 elif power_change == "off":328 server.power_on(using_pxe=True)
329 server.power_off(force=True)329 elif power_change == "off":
330 server.power_off(force=True)
331
332
333def power_query_seamicro15k_v2(ip, username, password, server_id):
334 server_id = '%s/0' % server_id
335 api = get_seamicro15k_api('v2.0', ip, username, password)
336 if api is None:
337 raise SeaMicroError('Unable to contact BMC controller.')
338 server = api.servers.get(server_id)
339 if server.active:
340 return "on"
341 return "off"
330342
=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-11-07 13:16:58 +0000
+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-12-06 16:48:48 +0000
@@ -34,6 +34,7 @@
34 find_seamicro15k_servers,34 find_seamicro15k_servers,
35 power_control_seamicro15k_v09,35 power_control_seamicro15k_v09,
36 power_control_seamicro15k_v2,36 power_control_seamicro15k_v2,
37 power_query_seamicro15k_v2,
37 POWER_STATUS,38 POWER_STATUS,
38 probe_seamicro15k_and_enlist,39 probe_seamicro15k_and_enlist,
39 SeaMicroAPIV09,40 SeaMicroAPIV09,
@@ -461,3 +462,71 @@
461462
462 power_control_seamicro15k_v2(ip, username, password, '0', 'on')463 power_control_seamicro15k_v2(ip, username, password, '0', 'on')
463 mock_power_on.assert_called()464 mock_power_on.assert_called()
465
466 def test_power_control_seamicro15k_v2_raises_error_when_api_None(self):
467 ip = factory.make_ipv4_address()
468 username = factory.make_string()
469 password = factory.make_string()
470
471 mock_get_api = self.patch(
472 seamicro,
473 'get_seamicro15k_api')
474 mock_get_api.return_value = None
475
476 self.assertRaises(
477 SeaMicroError,
478 power_control_seamicro15k_v2, ip, username, password, '0', 'on')
479
480 def test_power_query_seamicro15k_v2_power_on(self):
481 ip = factory.make_ipv4_address()
482 username = factory.make_string()
483 password = factory.make_string()
484
485 fake_server = FakeServer('0/0')
486 self.patch(fake_server, 'active', True)
487 fake_client = FakeSeaMicroClient()
488 fake_client.servers = FakeSeaMicroServerManager()
489 fake_client.servers.servers.append(fake_server)
490
491 mock_get_api = self.patch(
492 seamicro,
493 'get_seamicro15k_api')
494 mock_get_api.return_value = fake_client
495
496 self.assertEqual(
497 "on",
498 power_query_seamicro15k_v2(ip, username, password, '0'))
499
500 def test_power_query_seamicro15k_v2_power_off(self):
501 ip = factory.make_ipv4_address()
502 username = factory.make_string()
503 password = factory.make_string()
504
505 fake_server = FakeServer('0/0')
506 self.patch(fake_server, 'active', False)
507 fake_client = FakeSeaMicroClient()
508 fake_client.servers = FakeSeaMicroServerManager()
509 fake_client.servers.servers.append(fake_server)
510
511 mock_get_api = self.patch(
512 seamicro,
513 'get_seamicro15k_api')
514 mock_get_api.return_value = fake_client
515
516 self.assertEqual(
517 "off",
518 power_query_seamicro15k_v2(ip, username, password, '0'))
519
520 def test_power_query_seamicro15k_v2_raises_error_when_api_None(self):
521 ip = factory.make_ipv4_address()
522 username = factory.make_string()
523 password = factory.make_string()
524
525 mock_get_api = self.patch(
526 seamicro,
527 'get_seamicro15k_api')
528 mock_get_api.return_value = None
529
530 self.assertRaises(
531 SeaMicroError,
532 power_query_seamicro15k_v2, ip, username, password, '0')
464533
=== modified file 'src/provisioningserver/rpc/power.py'
--- src/provisioningserver/rpc/power.py 2014-12-03 19:26:01 +0000
+++ src/provisioningserver/rpc/power.py 2014-12-06 16:48:48 +0000
@@ -61,7 +61,7 @@
61# state for these power types.61# state for these power types.
62# This is meant to be temporary until all the power types support62# This is meant to be temporary until all the power types support
63# querying the power state of a node.63# querying the power state of a node.
64QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'ucsm', 'virsh']64QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'sm15k', 'ucsm', 'virsh']
6565
6666
67# Timeout for change_power_state(). We set it to 2 minutes by default,67# Timeout for change_power_state(). We set it to 2 minutes by default,
@@ -232,7 +232,7 @@
232 new_power_state = yield deferToThread(232 new_power_state = yield deferToThread(
233 perform_power_change, system_id, hostname, power_type,233 perform_power_change, system_id, hostname, power_type,
234 'query', context)234 'query', context)
235 if new_power_state == power_change:235 if new_power_state == "unknown" or new_power_state == power_change:
236 yield power_change_success(system_id, hostname, power_change)236 yield power_change_success(system_id, hostname, power_change)
237 return237 return
238238
@@ -301,7 +301,7 @@
301 try:301 try:
302 power_state = yield deferToThread(302 power_state = yield deferToThread(
303 perform_power_query, system_id, hostname, power_type, context)303 perform_power_query, system_id, hostname, power_type, context)
304 if power_state not in ("on", "off"):304 if power_state not in ("on", "off", "unknown"):
305 # This is considered an error.305 # This is considered an error.
306 raise PowerActionFail(power_state)306 raise PowerActionFail(power_state)
307 except PowerActionFail as e:307 except PowerActionFail as e:
308308
=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
--- src/provisioningserver/rpc/tests/test_power.py 2014-11-24 16:33:04 +0000
+++ src/provisioningserver/rpc/tests/test_power.py 2014-12-06 16:48:48 +0000
@@ -323,6 +323,37 @@
323 self.assertThat(markNodeBroken, MockNotCalled())323 self.assertThat(markNodeBroken, MockNotCalled())
324324
325 @inlineCallbacks325 @inlineCallbacks
326 def test_change_power_state_doesnt_retry_if_query_returns_unknown(self):
327 system_id = factory.make_name('system_id')
328 hostname = factory.make_name('hostname')
329 power_type = random.choice(power.QUERY_POWER_TYPES)
330 power_change = random.choice(['on', 'off'])
331 context = {
332 factory.make_name('context-key'): factory.make_name('context-val')
333 }
334 self.patch(power, 'pause')
335 power.power_action_registry[system_id] = power_change
336 # Patch the power action utility so that it says the node is
337 # in the required power state.
338 power_action, execute = patch_power_action(
339 self, return_value="unknown")
340 markNodeBroken = yield self.patch_rpc_methods()
341
342 yield power.change_power_state(
343 system_id, hostname, power_type, power_change, context)
344 self.assertThat(
345 execute,
346 MockCallsMatch(
347 # One call to change the power state.
348 call(power_change=power_change, **context),
349 # One call to query the power state.
350 call(power_change='query', **context),
351 ),
352 )
353 # The node hasn't been marked broken.
354 self.assertThat(markNodeBroken, MockNotCalled())
355
356 @inlineCallbacks
326 def test_change_power_state_marks_the_node_broken_if_failure(self):357 def test_change_power_state_marks_the_node_broken_if_failure(self):
327 system_id = factory.make_name('system_id')358 system_id = factory.make_name('system_id')
328 hostname = factory.make_name('hostname')359 hostname = factory.make_name('hostname')
@@ -548,6 +579,29 @@
548 self.assertThat(579 self.assertThat(
549 power_state_update, MockCalledOnceWith(system_id, power_state))580 power_state_update, MockCalledOnceWith(system_id, power_state))
550581
582 def test_get_power_state_changes_power_state_if_unknown(self):
583 system_id = factory.make_name('system_id')
584 hostname = factory.make_name('hostname')
585 power_state = "unknown"
586 power_type = random.choice(power.QUERY_POWER_TYPES)
587 context = {
588 factory.make_name('context-key'): factory.make_name('context-val')
589 }
590 self.patch(power, 'pause')
591 power_state_update = self.patch_autospec(power, 'power_state_update')
592
593 # Simulate success.
594 power_action, execute = patch_power_action(
595 self, return_value=power_state)
596 _, _, io = self.patch_rpc_methods()
597
598 d = power.get_power_state(
599 system_id, hostname, power_type, context)
600 io.flush()
601 self.assertEqual(power_state, extract_result(d))
602 self.assertThat(
603 power_state_update, MockCalledOnceWith(system_id, power_state))
604
551 def test_get_power_state_pauses_inbetween_retries(self):605 def test_get_power_state_pauses_inbetween_retries(self):
552 system_id = factory.make_name('system_id')606 system_id = factory.make_name('system_id')
553 hostname = factory.make_name('hostname')607 hostname = factory.make_name('hostname')