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
1=== modified file 'etc/maas/templates/power/sm15k.template'
2--- etc/maas/templates/power/sm15k.template 2014-06-10 14:45:14 +0000
3+++ etc/maas/templates/power/sm15k.template 2014-11-05 21:52:32 +0000
4@@ -3,50 +3,124 @@
5 # Control a system via ipmipower, sending the seamicro specific hex codes
6 #
7
8-# Parameters
9-power_change={{power_change}}
10-power_address={{power_address}}
11-power_user={{power_user}}
12-power_pass={{power_pass}}
13-power_control={{power_control}}
14-system_id={{system_id}}
15-ipmitool={{ipmitool}}
16-
17-# IPMI power mode
18-{{py: power_mode = 1 if power_change == 'on' else 6 }}
19-power_mode={{power_mode}}
20+# Exit with failure message.
21+# Parameters: exit code, and error message.
22+fail() {
23+ echo "$2" >&2
24+ exit $1
25+}
26
27 # Control power using IPMI
28 issue_ipmi_command() {
29- ${ipmitool} -I lanplus \
30- -H ${power_address} -U ${power_user}\
31- -P ${power_pass} raw 0x2E 1 0x00 0x7d 0xab \
32- ${power_mode} 0 ${system_id}
33+ {{py: power_mode = 1 if power_change == 'on' else 6 }}
34+ {{ipmitool}} -I lanplus \
35+ -H {{power_address}} -U {{power_user}}\
36+ -P {{power_pass}} raw 0x2E 1 0x00 0x7d 0xab \
37+ {{power_mode}} 0 {{system_id}}
38 }
39
40 # Control power using REST v0.9
41 issue_rest_v09_command() {
42 python - << END
43+import sys
44 from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v09
45-power_control_seamicro15k_v09("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}")
46+try:
47+ power_control_seamicro15k_v09(
48+ {{escape_py_literal(power_address) | safe}},
49+ {{escape_py_literal(power_user) | safe}},
50+ {{escape_py_literal(power_pass) | safe}},
51+ {{escape_py_literal(system_id) | safe}},
52+ {{escape_py_literal(power_change) | 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 # Control power using REST v2
63 issue_rest_v2_command() {
64 python - << END
65+import sys
66 from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v2
67-power_control_seamicro15k_v2("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}")
68-END
69-}
70-
71-if [ "${power_control}" = "ipmi" ]
72-then
73- issue_ipmi_command
74-elif [ "${power_control}" = "restapi" ]
75-then
76- issue_rest_v09_command
77-elif [ "${power_control}" = "restapi2" ]
78-then
79- issue_rest_v2_command
80-fi
81+try:
82+ power_control_seamicro15k_v2(
83+ {{escape_py_literal(power_address) | safe}},
84+ {{escape_py_literal(power_user) | safe}},
85+ {{escape_py_literal(power_pass) | safe}},
86+ {{escape_py_literal(system_id) | safe}},
87+ {{escape_py_literal(power_change) | safe}},
88+ )
89+except Exception as e:
90+ # This gets in the node event log: print the exception's message
91+ # and not the stacktrace.
92+ print(unicode(e))
93+ sys.exit(1)
94+END
95+}
96+
97+# Query power state using REST v2
98+query_state_rest_v2() {
99+python - << END
100+import sys
101+from provisioningserver.drivers.hardware.mscm seamicro power_query_seamicro15k_v2
102+try:
103+ print(power_query_seamicro15k_v2(
104+ {{escape_py_literal(power_address) | safe}},
105+ {{escape_py_literal(power_user) | safe}},
106+ {{escape_py_literal(power_pass) | safe}},
107+ {{escape_py_literal(system_id) | safe}},
108+ ))
109+except Exception as e:
110+ # This gets in the node event log: print the exception's message
111+ # and not the stacktrace.
112+ print(unicode(e))
113+ sys.exit(1)
114+END
115+}
116+
117+# Perform power control
118+power_control() {
119+ if [ "{{power_control}}" = "ipmi" ]
120+ then
121+ issue_ipmi_command
122+ elif [ "{{power_control}}" = "restapi" ]
123+ then
124+ issue_rest_v09_command
125+ elif [ "{{power_control}}" = "restapi2" ]
126+ then
127+ issue_rest_v2_command
128+ fi
129+}
130+
131+# Query the state.
132+# Only supported by REST v2.
133+query_state() {
134+ if [ "{{power_control}}" = "ipmi" ]
135+ then
136+ echo "unknown"
137+ elif [ "{{power_control}}" = "restapi" ]
138+ then
139+ echo "unknown"
140+ elif [ "{{power_control}}" = "restapi2" ]
141+ then
142+ query_state_rest_v2
143+ fi
144+}
145+
146+main() {
147+ case $1 in
148+ 'on'|'off')
149+ power_control
150+ ;;
151+ 'query')
152+ query_state
153+ ;;
154+ *)
155+ fail 2 "Unknown power command: '$1'"
156+ esac
157+}
158+
159+main "{{power_change}}"
160
161=== modified file 'src/provisioningserver/drivers/hardware/seamicro.py'
162--- src/provisioningserver/drivers/hardware/seamicro.py 2014-09-10 16:20:31 +0000
163+++ src/provisioningserver/drivers/hardware/seamicro.py 2014-11-05 21:52:32 +0000
164@@ -320,9 +320,22 @@
165 power_change):
166 server_id = '%s/0' % server_id
167 api = get_seamicro15k_api('v2.0', ip, username, password)
168- if api:
169- server = api.servers.get(server_id)
170- if power_change == "on":
171- server.power_on(using_pxe=True)
172- elif power_change == "off":
173- server.power_off(force=True)
174+ if api is None:
175+ raise SeaMicroError('Unable to contact BMC controller.')
176+ server = api.servers.get(server_id)
177+ if power_change == "on":
178+ server.power_on(using_pxe=True)
179+ elif power_change == "off":
180+ server.power_off(force=True)
181+
182+
183+def power_query_seamicro15k_v2(ip, username, password, server_id,
184+ power_change):
185+ server_id = '%s/0' % server_id
186+ api = get_seamicro15k_api('v2.0', ip, username, password)
187+ if api is None:
188+ raise SeaMicroError('Unable to contact BMC controller.')
189+ server = api.servers.get(server_id)
190+ if server.active:
191+ return "on"
192+ return "off"
193
194=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
195--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-09-18 12:44:38 +0000
196+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-11-05 21:52:32 +0000
197@@ -34,6 +34,7 @@
198 find_seamicro15k_servers,
199 power_control_seamicro15k_v09,
200 power_control_seamicro15k_v2,
201+ power_query_seamicro15k_v2,
202 POWER_STATUS,
203 probe_seamicro15k_and_enlist,
204 SeaMicroAPIV09,
205@@ -466,3 +467,71 @@
206
207 power_control_seamicro15k_v2(ip, username, password, '0', 'on')
208 mock_power_on.assert_called()
209+
210+ def test_power_control_seamicro15k_v2_raises_error_when_api_None(self):
211+ ip = factory.make_ipv4_address()
212+ username = factory.make_string()
213+ password = factory.make_string()
214+
215+ mock_get_api = self.patch(
216+ seamicro,
217+ 'get_seamicro15k_api')
218+ mock_get_api.return_value = None
219+
220+ self.assertRaises(
221+ SeaMicroError,
222+ power_control_seamicro15k_v2, ip, username, password, '0', 'on')
223+
224+ def test_power_query_seamicro15k_v2_power_on(self):
225+ ip = factory.make_ipv4_address()
226+ username = factory.make_string()
227+ password = factory.make_string()
228+
229+ fake_server = FakeServer('0/0')
230+ self.patch(fake_server, 'active', True)
231+ fake_client = FakeSeaMicroClient()
232+ fake_client.servers = FakeSeaMicroServerManager()
233+ fake_client.servers.servers.append(fake_server)
234+
235+ mock_get_api = self.patch(
236+ seamicro,
237+ 'get_seamicro15k_api')
238+ mock_get_api.return_value = fake_client
239+
240+ self.assertEqual(
241+ "on",
242+ power_query_seamicro15k_v2(ip, username, password, '0', 'on'))
243+
244+ def test_power_query_seamicro15k_v2_power_off(self):
245+ ip = factory.make_ipv4_address()
246+ username = factory.make_string()
247+ password = factory.make_string()
248+
249+ fake_server = FakeServer('0/0')
250+ self.patch(fake_server, 'active', False)
251+ fake_client = FakeSeaMicroClient()
252+ fake_client.servers = FakeSeaMicroServerManager()
253+ fake_client.servers.servers.append(fake_server)
254+
255+ mock_get_api = self.patch(
256+ seamicro,
257+ 'get_seamicro15k_api')
258+ mock_get_api.return_value = fake_client
259+
260+ self.assertEqual(
261+ "off",
262+ power_query_seamicro15k_v2(ip, username, password, '0', 'on'))
263+
264+ def test_power_query_seamicro15k_v2_raises_error_when_api_None(self):
265+ ip = factory.make_ipv4_address()
266+ username = factory.make_string()
267+ password = factory.make_string()
268+
269+ mock_get_api = self.patch(
270+ seamicro,
271+ 'get_seamicro15k_api')
272+ mock_get_api.return_value = None
273+
274+ self.assertRaises(
275+ SeaMicroError,
276+ power_query_seamicro15k_v2, ip, username, password, '0', 'on')
277
278=== modified file 'src/provisioningserver/rpc/power.py'
279--- src/provisioningserver/rpc/power.py 2014-10-28 21:48:26 +0000
280+++ src/provisioningserver/rpc/power.py 2014-11-05 21:52:32 +0000
281@@ -61,7 +61,7 @@
282 # state for these power types.
283 # This is meant to be temporary until all the power types support
284 # querying the power state of a node.
285-QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'virsh']
286+QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'sm15k', 'virsh']
287
288
289 # Timeout for change_power_state(). We set it to 2 minutes by default,
290@@ -232,7 +232,7 @@
291 new_power_state = yield deferToThread(
292 perform_power_change, system_id, hostname, power_type,
293 'query', context)
294- if new_power_state == power_change:
295+ if new_power_state == "unknown" or new_power_state == power_change:
296 yield power_change_success(system_id, hostname, power_change)
297 return
298
299@@ -298,7 +298,7 @@
300 try:
301 power_state = yield deferToThread(
302 perform_power_query, system_id, hostname, power_type, context)
303- if power_state not in ("on", "off"):
304+ if power_state not in ("on", "off", "unknown"):
305 # This is considered an error.
306 raise PowerActionFail(power_state)
307 except PowerActionFail as e:
308
309=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
310--- src/provisioningserver/rpc/tests/test_power.py 2014-10-27 11:56:50 +0000
311+++ src/provisioningserver/rpc/tests/test_power.py 2014-11-05 21:52:32 +0000
312@@ -321,6 +321,37 @@
313 self.assertThat(markNodeBroken, MockNotCalled())
314
315 @inlineCallbacks
316+ def test_change_power_state_doesnt_retry_if_query_returns_unknown(self):
317+ system_id = factory.make_name('system_id')
318+ hostname = factory.make_name('hostname')
319+ power_type = random.choice(power.QUERY_POWER_TYPES)
320+ power_change = random.choice(['on', 'off'])
321+ context = {
322+ factory.make_name('context-key'): factory.make_name('context-val')
323+ }
324+ self.patch(power, 'pause')
325+ power.power_action_registry[system_id] = power_change
326+ # Patch the power action utility so that it says the node is
327+ # in the required power state.
328+ power_action, execute = patch_power_action(
329+ self, return_value="unknown")
330+ markNodeBroken = yield self.patch_rpc_methods()
331+
332+ yield power.change_power_state(
333+ system_id, hostname, power_type, power_change, context)
334+ self.assertThat(
335+ execute,
336+ MockCallsMatch(
337+ # One call to change the power state.
338+ call(power_change=power_change, **context),
339+ # One call to query the power state.
340+ call(power_change='query', **context),
341+ ),
342+ )
343+ # The node hasn't been marked broken.
344+ self.assertThat(markNodeBroken, MockNotCalled())
345+
346+ @inlineCallbacks
347 def test_change_power_state_marks_the_node_broken_if_failure(self):
348 system_id = factory.make_name('system_id')
349 hostname = factory.make_name('hostname')
350@@ -546,6 +577,29 @@
351 self.assertThat(
352 power_state_update, MockCalledOnceWith(system_id, power_state))
353
354+ def test_get_power_state_changes_power_state_if_unknown(self):
355+ system_id = factory.make_name('system_id')
356+ hostname = factory.make_name('hostname')
357+ power_state = "unknown"
358+ power_type = random.choice(power.QUERY_POWER_TYPES)
359+ context = {
360+ factory.make_name('context-key'): factory.make_name('context-val')
361+ }
362+ self.patch(power, 'pause')
363+ power_state_update = self.patch_autospec(power, 'power_state_update')
364+
365+ # Simulate success.
366+ power_action, execute = patch_power_action(
367+ self, return_value=power_state)
368+ _, _, io = self.patch_rpc_methods()
369+
370+ d = power.get_power_state(
371+ system_id, hostname, power_type, context)
372+ io.flush()
373+ self.assertEqual(power_state, extract_result(d))
374+ self.assertThat(
375+ power_state_update, MockCalledOnceWith(system_id, power_state))
376+
377 def test_get_power_state_pauses_inbetween_retries(self):
378 system_id = factory.make_name('system_id')
379 hostname = factory.make_name('hostname')