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

Proposed by Andres Rodriguez on 2014-12-06
Status: Merged
Approved by: Andres Rodriguez on 2014-12-06
Approved revision: 3303
Merged at revision: 3303
Proposed branch: lp:~andreserl/maas/sm15k-power-query-lp1384424-1.7
Merge into: lp:maas/1.7
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-1.7
Reviewer Review Type Date Requested Status
Andres Rodriguez Approve on 2014-12-06
Review via email: mp+243899@code.launchpad.net

Commit message

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

To post a comment you must log in.
Andres Rodriguez (andreserl) wrote :

self approve!

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 17:36:44 +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 17:36:44 +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 17:36:44 +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-06 03:04:15 +0000
279+++ src/provisioningserver/rpc/power.py 2014-12-06 17:36:44 +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@@ -298,7 +298,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-12-02 15:55:34 +0000
310+++ src/provisioningserver/rpc/tests/test_power.py 2014-12-06 17:36:44 +0000
311@@ -321,6 +321,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@@ -546,6 +577,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')

Subscribers

People subscribed via source and target branches

to all changes: