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

Proposed by Andres Rodriguez on 2014-12-06
Status: Merged
Approved by: Andres Rodriguez on 2014-12-06
Approved revision: 3405
Merged at revision: 3403
Proposed branch: lp:~andreserl/maas/sm15k-power-query-lp1384424
Merge into: lp: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 Approve on 2014-12-06
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.
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
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-12-06 16:48:48 +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.seamicro import 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-11-17 11:56:49 +0000
163+++ src/provisioningserver/drivers/hardware/seamicro.py 2014-12-06 16:48:48 +0000
164@@ -321,9 +321,21 @@
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+ server_id = '%s/0' % server_id
185+ api = get_seamicro15k_api('v2.0', ip, username, password)
186+ if api is None:
187+ raise SeaMicroError('Unable to contact BMC controller.')
188+ server = api.servers.get(server_id)
189+ if server.active:
190+ return "on"
191+ return "off"
192
193=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
194--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-11-07 13:16:58 +0000
195+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-12-06 16:48:48 +0000
196@@ -34,6 +34,7 @@
197 find_seamicro15k_servers,
198 power_control_seamicro15k_v09,
199 power_control_seamicro15k_v2,
200+ power_query_seamicro15k_v2,
201 POWER_STATUS,
202 probe_seamicro15k_and_enlist,
203 SeaMicroAPIV09,
204@@ -461,3 +462,71 @@
205
206 power_control_seamicro15k_v2(ip, username, password, '0', 'on')
207 mock_power_on.assert_called()
208+
209+ def test_power_control_seamicro15k_v2_raises_error_when_api_None(self):
210+ ip = factory.make_ipv4_address()
211+ username = factory.make_string()
212+ password = factory.make_string()
213+
214+ mock_get_api = self.patch(
215+ seamicro,
216+ 'get_seamicro15k_api')
217+ mock_get_api.return_value = None
218+
219+ self.assertRaises(
220+ SeaMicroError,
221+ power_control_seamicro15k_v2, ip, username, password, '0', 'on')
222+
223+ def test_power_query_seamicro15k_v2_power_on(self):
224+ ip = factory.make_ipv4_address()
225+ username = factory.make_string()
226+ password = factory.make_string()
227+
228+ fake_server = FakeServer('0/0')
229+ self.patch(fake_server, 'active', True)
230+ fake_client = FakeSeaMicroClient()
231+ fake_client.servers = FakeSeaMicroServerManager()
232+ fake_client.servers.servers.append(fake_server)
233+
234+ mock_get_api = self.patch(
235+ seamicro,
236+ 'get_seamicro15k_api')
237+ mock_get_api.return_value = fake_client
238+
239+ self.assertEqual(
240+ "on",
241+ power_query_seamicro15k_v2(ip, username, password, '0'))
242+
243+ def test_power_query_seamicro15k_v2_power_off(self):
244+ ip = factory.make_ipv4_address()
245+ username = factory.make_string()
246+ password = factory.make_string()
247+
248+ fake_server = FakeServer('0/0')
249+ self.patch(fake_server, 'active', False)
250+ fake_client = FakeSeaMicroClient()
251+ fake_client.servers = FakeSeaMicroServerManager()
252+ fake_client.servers.servers.append(fake_server)
253+
254+ mock_get_api = self.patch(
255+ seamicro,
256+ 'get_seamicro15k_api')
257+ mock_get_api.return_value = fake_client
258+
259+ self.assertEqual(
260+ "off",
261+ power_query_seamicro15k_v2(ip, username, password, '0'))
262+
263+ def test_power_query_seamicro15k_v2_raises_error_when_api_None(self):
264+ ip = factory.make_ipv4_address()
265+ username = factory.make_string()
266+ password = factory.make_string()
267+
268+ mock_get_api = self.patch(
269+ seamicro,
270+ 'get_seamicro15k_api')
271+ mock_get_api.return_value = None
272+
273+ self.assertRaises(
274+ SeaMicroError,
275+ power_query_seamicro15k_v2, ip, username, password, '0')
276
277=== modified file 'src/provisioningserver/rpc/power.py'
278--- src/provisioningserver/rpc/power.py 2014-12-03 19:26:01 +0000
279+++ src/provisioningserver/rpc/power.py 2014-12-06 16:48:48 +0000
280@@ -61,7 +61,7 @@
281 # state for these power types.
282 # This is meant to be temporary until all the power types support
283 # querying the power state of a node.
284-QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'ucsm', 'virsh']
285+QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'sm15k', 'ucsm', 'virsh']
286
287
288 # Timeout for change_power_state(). We set it to 2 minutes by default,
289@@ -232,7 +232,7 @@
290 new_power_state = yield deferToThread(
291 perform_power_change, system_id, hostname, power_type,
292 'query', context)
293- if new_power_state == power_change:
294+ if new_power_state == "unknown" or new_power_state == power_change:
295 yield power_change_success(system_id, hostname, power_change)
296 return
297
298@@ -301,7 +301,7 @@
299 try:
300 power_state = yield deferToThread(
301 perform_power_query, system_id, hostname, power_type, context)
302- if power_state not in ("on", "off"):
303+ if power_state not in ("on", "off", "unknown"):
304 # This is considered an error.
305 raise PowerActionFail(power_state)
306 except PowerActionFail as e:
307
308=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
309--- src/provisioningserver/rpc/tests/test_power.py 2014-11-24 16:33:04 +0000
310+++ src/provisioningserver/rpc/tests/test_power.py 2014-12-06 16:48:48 +0000
311@@ -323,6 +323,37 @@
312 self.assertThat(markNodeBroken, MockNotCalled())
313
314 @inlineCallbacks
315+ def test_change_power_state_doesnt_retry_if_query_returns_unknown(self):
316+ system_id = factory.make_name('system_id')
317+ hostname = factory.make_name('hostname')
318+ power_type = random.choice(power.QUERY_POWER_TYPES)
319+ power_change = random.choice(['on', 'off'])
320+ context = {
321+ factory.make_name('context-key'): factory.make_name('context-val')
322+ }
323+ self.patch(power, 'pause')
324+ power.power_action_registry[system_id] = power_change
325+ # Patch the power action utility so that it says the node is
326+ # in the required power state.
327+ power_action, execute = patch_power_action(
328+ self, return_value="unknown")
329+ markNodeBroken = yield self.patch_rpc_methods()
330+
331+ yield power.change_power_state(
332+ system_id, hostname, power_type, power_change, context)
333+ self.assertThat(
334+ execute,
335+ MockCallsMatch(
336+ # One call to change the power state.
337+ call(power_change=power_change, **context),
338+ # One call to query the power state.
339+ call(power_change='query', **context),
340+ ),
341+ )
342+ # The node hasn't been marked broken.
343+ self.assertThat(markNodeBroken, MockNotCalled())
344+
345+ @inlineCallbacks
346 def test_change_power_state_marks_the_node_broken_if_failure(self):
347 system_id = factory.make_name('system_id')
348 hostname = factory.make_name('hostname')
349@@ -548,6 +579,29 @@
350 self.assertThat(
351 power_state_update, MockCalledOnceWith(system_id, power_state))
352
353+ def test_get_power_state_changes_power_state_if_unknown(self):
354+ system_id = factory.make_name('system_id')
355+ hostname = factory.make_name('hostname')
356+ power_state = "unknown"
357+ power_type = random.choice(power.QUERY_POWER_TYPES)
358+ context = {
359+ factory.make_name('context-key'): factory.make_name('context-val')
360+ }
361+ self.patch(power, 'pause')
362+ power_state_update = self.patch_autospec(power, 'power_state_update')
363+
364+ # Simulate success.
365+ power_action, execute = patch_power_action(
366+ self, return_value=power_state)
367+ _, _, io = self.patch_rpc_methods()
368+
369+ d = power.get_power_state(
370+ system_id, hostname, power_type, context)
371+ io.flush()
372+ self.assertEqual(power_state, extract_result(d))
373+ self.assertThat(
374+ power_state_update, MockCalledOnceWith(system_id, power_state))
375+
376 def test_get_power_state_pauses_inbetween_retries(self):
377 system_id = factory.make_name('system_id')
378 hostname = factory.make_name('hostname')