Merge lp:~blake-rouse/maas/sm15k-query into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Merged at revision: 3403
Proposed branch: lp:~blake-rouse/maas/sm15k-query
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 379 lines (+250/-40)
5 files modified
etc/maas/templates/power/sm15k.template (+105/-31)
src/provisioningserver/drivers/hardware/seamicro.py (+19/-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:~blake-rouse/maas/sm15k-query
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Needs Fixing
Review via email: mp+240785@code.launchpad.net

Commit message

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

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

See comments inline!

review: Needs Fixing

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-11-05 21:52:32 +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.mscm seamicro 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-09-10 16:20:31 +0000
+++ src/provisioningserver/drivers/hardware/seamicro.py 2014-11-05 21:52:32 +0000
@@ -320,9 +320,22 @@
320 power_change):320 power_change):
321 server_id = '%s/0' % server_id321 server_id = '%s/0' % server_id
322 api = get_seamicro15k_api('v2.0', ip, username, password)322 api = get_seamicro15k_api('v2.0', ip, username, password)
323 if api:323 if api is None:
324 server = api.servers.get(server_id)324 raise SeaMicroError('Unable to contact BMC controller.')
325 if power_change == "on":325 server = api.servers.get(server_id)
326 server.power_on(using_pxe=True)326 if power_change == "on":
327 elif power_change == "off":327 server.power_on(using_pxe=True)
328 server.power_off(force=True)328 elif power_change == "off":
329 server.power_off(force=True)
330
331
332def power_query_seamicro15k_v2(ip, username, password, server_id,
333 power_change):
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"
329342
=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-09-18 12:44:38 +0000
+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-11-05 21:52:32 +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,
@@ -466,3 +467,71 @@
466467
467 power_control_seamicro15k_v2(ip, username, password, '0', 'on')468 power_control_seamicro15k_v2(ip, username, password, '0', 'on')
468 mock_power_on.assert_called()469 mock_power_on.assert_called()
470
471 def test_power_control_seamicro15k_v2_raises_error_when_api_None(self):
472 ip = factory.make_ipv4_address()
473 username = factory.make_string()
474 password = factory.make_string()
475
476 mock_get_api = self.patch(
477 seamicro,
478 'get_seamicro15k_api')
479 mock_get_api.return_value = None
480
481 self.assertRaises(
482 SeaMicroError,
483 power_control_seamicro15k_v2, ip, username, password, '0', 'on')
484
485 def test_power_query_seamicro15k_v2_power_on(self):
486 ip = factory.make_ipv4_address()
487 username = factory.make_string()
488 password = factory.make_string()
489
490 fake_server = FakeServer('0/0')
491 self.patch(fake_server, 'active', True)
492 fake_client = FakeSeaMicroClient()
493 fake_client.servers = FakeSeaMicroServerManager()
494 fake_client.servers.servers.append(fake_server)
495
496 mock_get_api = self.patch(
497 seamicro,
498 'get_seamicro15k_api')
499 mock_get_api.return_value = fake_client
500
501 self.assertEqual(
502 "on",
503 power_query_seamicro15k_v2(ip, username, password, '0', 'on'))
504
505 def test_power_query_seamicro15k_v2_power_off(self):
506 ip = factory.make_ipv4_address()
507 username = factory.make_string()
508 password = factory.make_string()
509
510 fake_server = FakeServer('0/0')
511 self.patch(fake_server, 'active', False)
512 fake_client = FakeSeaMicroClient()
513 fake_client.servers = FakeSeaMicroServerManager()
514 fake_client.servers.servers.append(fake_server)
515
516 mock_get_api = self.patch(
517 seamicro,
518 'get_seamicro15k_api')
519 mock_get_api.return_value = fake_client
520
521 self.assertEqual(
522 "off",
523 power_query_seamicro15k_v2(ip, username, password, '0', 'on'))
524
525 def test_power_query_seamicro15k_v2_raises_error_when_api_None(self):
526 ip = factory.make_ipv4_address()
527 username = factory.make_string()
528 password = factory.make_string()
529
530 mock_get_api = self.patch(
531 seamicro,
532 'get_seamicro15k_api')
533 mock_get_api.return_value = None
534
535 self.assertRaises(
536 SeaMicroError,
537 power_query_seamicro15k_v2, ip, username, password, '0', 'on')
469538
=== modified file 'src/provisioningserver/rpc/power.py'
--- src/provisioningserver/rpc/power.py 2014-10-28 21:48:26 +0000
+++ src/provisioningserver/rpc/power.py 2014-11-05 21:52:32 +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', 'virsh']64QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'sm15k', '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
@@ -298,7 +298,7 @@
298 try:298 try:
299 power_state = yield deferToThread(299 power_state = yield deferToThread(
300 perform_power_query, system_id, hostname, power_type, context)300 perform_power_query, system_id, hostname, power_type, context)
301 if power_state not in ("on", "off"):301 if power_state not in ("on", "off", "unknown"):
302 # This is considered an error.302 # This is considered an error.
303 raise PowerActionFail(power_state)303 raise PowerActionFail(power_state)
304 except PowerActionFail as e:304 except PowerActionFail as e:
305305
=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
--- src/provisioningserver/rpc/tests/test_power.py 2014-10-27 11:56:50 +0000
+++ src/provisioningserver/rpc/tests/test_power.py 2014-11-05 21:52:32 +0000
@@ -321,6 +321,37 @@
321 self.assertThat(markNodeBroken, MockNotCalled())321 self.assertThat(markNodeBroken, MockNotCalled())
322322
323 @inlineCallbacks323 @inlineCallbacks
324 def test_change_power_state_doesnt_retry_if_query_returns_unknown(self):
325 system_id = factory.make_name('system_id')
326 hostname = factory.make_name('hostname')
327 power_type = random.choice(power.QUERY_POWER_TYPES)
328 power_change = random.choice(['on', 'off'])
329 context = {
330 factory.make_name('context-key'): factory.make_name('context-val')
331 }
332 self.patch(power, 'pause')
333 power.power_action_registry[system_id] = power_change
334 # Patch the power action utility so that it says the node is
335 # in the required power state.
336 power_action, execute = patch_power_action(
337 self, return_value="unknown")
338 markNodeBroken = yield self.patch_rpc_methods()
339
340 yield power.change_power_state(
341 system_id, hostname, power_type, power_change, context)
342 self.assertThat(
343 execute,
344 MockCallsMatch(
345 # One call to change the power state.
346 call(power_change=power_change, **context),
347 # One call to query the power state.
348 call(power_change='query', **context),
349 ),
350 )
351 # The node hasn't been marked broken.
352 self.assertThat(markNodeBroken, MockNotCalled())
353
354 @inlineCallbacks
324 def test_change_power_state_marks_the_node_broken_if_failure(self):355 def test_change_power_state_marks_the_node_broken_if_failure(self):
325 system_id = factory.make_name('system_id')356 system_id = factory.make_name('system_id')
326 hostname = factory.make_name('hostname')357 hostname = factory.make_name('hostname')
@@ -546,6 +577,29 @@
546 self.assertThat(577 self.assertThat(
547 power_state_update, MockCalledOnceWith(system_id, power_state))578 power_state_update, MockCalledOnceWith(system_id, power_state))
548579
580 def test_get_power_state_changes_power_state_if_unknown(self):
581 system_id = factory.make_name('system_id')
582 hostname = factory.make_name('hostname')
583 power_state = "unknown"
584 power_type = random.choice(power.QUERY_POWER_TYPES)
585 context = {
586 factory.make_name('context-key'): factory.make_name('context-val')
587 }
588 self.patch(power, 'pause')
589 power_state_update = self.patch_autospec(power, 'power_state_update')
590
591 # Simulate success.
592 power_action, execute = patch_power_action(
593 self, return_value=power_state)
594 _, _, io = self.patch_rpc_methods()
595
596 d = power.get_power_state(
597 system_id, hostname, power_type, context)
598 io.flush()
599 self.assertEqual(power_state, extract_result(d))
600 self.assertThat(
601 power_state_update, MockCalledOnceWith(system_id, power_state))
602
549 def test_get_power_state_pauses_inbetween_retries(self):603 def test_get_power_state_pauses_inbetween_retries(self):
550 system_id = factory.make_name('system_id')604 system_id = factory.make_name('system_id')
551 hostname = factory.make_name('hostname')605 hostname = factory.make_name('hostname')