Merge lp:~andreserl/maas/sm15k-power-query-lp1384424-1.7 into lp:maas/1.7
- sm15k-power-query-lp1384424-1.7
- Merge into 1.7
Proposed by
Andres Rodriguez
Status: | Merged |
---|---|
Approved by: | Andres Rodriguez |
Approved revision: | no longer in the source branch. |
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
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
Description of the change
To post a comment you must log in.
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 | 3 | # Control a system via ipmipower, sending the seamicro specific hex codes | 3 | # Control a system via ipmipower, sending the seamicro specific hex codes |
6 | 4 | # | 4 | # |
7 | 5 | 5 | ||
20 | 6 | # Parameters | 6 | # Exit with failure message. |
21 | 7 | power_change={{power_change}} | 7 | # Parameters: exit code, and error message. |
22 | 8 | power_address={{power_address}} | 8 | fail() { |
23 | 9 | power_user={{power_user}} | 9 | echo "$2" >&2 |
24 | 10 | power_pass={{power_pass}} | 10 | exit $1 |
25 | 11 | power_control={{power_control}} | 11 | } |
14 | 12 | system_id={{system_id}} | ||
15 | 13 | ipmitool={{ipmitool}} | ||
16 | 14 | |||
17 | 15 | # IPMI power mode | ||
18 | 16 | {{py: power_mode = 1 if power_change == 'on' else 6 }} | ||
19 | 17 | power_mode={{power_mode}} | ||
26 | 18 | 12 | ||
27 | 19 | # Control power using IPMI | 13 | # Control power using IPMI |
28 | 20 | issue_ipmi_command() { | 14 | issue_ipmi_command() { |
33 | 21 | ${ipmitool} -I lanplus \ | 15 | {{py: power_mode = 1 if power_change == 'on' else 6 }} |
34 | 22 | -H ${power_address} -U ${power_user}\ | 16 | {{ipmitool}} -I lanplus \ |
35 | 23 | -P ${power_pass} raw 0x2E 1 0x00 0x7d 0xab \ | 17 | -H {{power_address}} -U {{power_user}}\ |
36 | 24 | ${power_mode} 0 ${system_id} | 18 | -P {{power_pass}} raw 0x2E 1 0x00 0x7d 0xab \ |
37 | 19 | {{power_mode}} 0 {{system_id}} | ||
38 | 25 | } | 20 | } |
39 | 26 | 21 | ||
40 | 27 | # Control power using REST v0.9 | 22 | # Control power using REST v0.9 |
41 | 28 | issue_rest_v09_command() { | 23 | issue_rest_v09_command() { |
42 | 29 | python - << END | 24 | python - << END |
43 | 25 | import sys | ||
44 | 30 | from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v09 | 26 | from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v09 |
46 | 31 | power_control_seamicro15k_v09("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}") | 27 | try: |
47 | 28 | power_control_seamicro15k_v09( | ||
48 | 29 | {{escape_py_literal(power_address) | safe}}, | ||
49 | 30 | {{escape_py_literal(power_user) | safe}}, | ||
50 | 31 | {{escape_py_literal(power_pass) | safe}}, | ||
51 | 32 | {{escape_py_literal(system_id) | safe}}, | ||
52 | 33 | {{escape_py_literal(power_change) | safe}}, | ||
53 | 34 | ) | ||
54 | 35 | except Exception as e: | ||
55 | 36 | # This gets in the node event log: print the exception's message | ||
56 | 37 | # and not the stacktrace. | ||
57 | 38 | print(unicode(e)) | ||
58 | 39 | sys.exit(1) | ||
59 | 32 | END | 40 | END |
60 | 33 | } | 41 | } |
61 | 34 | 42 | ||
62 | 35 | # Control power using REST v2 | 43 | # Control power using REST v2 |
63 | 36 | issue_rest_v2_command() { | 44 | issue_rest_v2_command() { |
64 | 37 | python - << END | 45 | python - << END |
65 | 46 | import sys | ||
66 | 38 | from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v2 | 47 | from provisioningserver.drivers.hardware.seamicro import power_control_seamicro15k_v2 |
81 | 39 | power_control_seamicro15k_v2("${power_address}", "${power_user}", "${power_pass}", "${system_id}", "${power_change}") | 48 | try: |
82 | 40 | END | 49 | power_control_seamicro15k_v2( |
83 | 41 | } | 50 | {{escape_py_literal(power_address) | safe}}, |
84 | 42 | 51 | {{escape_py_literal(power_user) | safe}}, | |
85 | 43 | if [ "${power_control}" = "ipmi" ] | 52 | {{escape_py_literal(power_pass) | safe}}, |
86 | 44 | then | 53 | {{escape_py_literal(system_id) | safe}}, |
87 | 45 | issue_ipmi_command | 54 | {{escape_py_literal(power_change) | safe}}, |
88 | 46 | elif [ "${power_control}" = "restapi" ] | 55 | ) |
89 | 47 | then | 56 | except Exception as e: |
90 | 48 | issue_rest_v09_command | 57 | # This gets in the node event log: print the exception's message |
91 | 49 | elif [ "${power_control}" = "restapi2" ] | 58 | # and not the stacktrace. |
92 | 50 | then | 59 | print(unicode(e)) |
93 | 51 | issue_rest_v2_command | 60 | sys.exit(1) |
94 | 52 | fi | 61 | END |
95 | 62 | } | ||
96 | 63 | |||
97 | 64 | # Query power state using REST v2 | ||
98 | 65 | query_state_rest_v2() { | ||
99 | 66 | python - << END | ||
100 | 67 | import sys | ||
101 | 68 | from provisioningserver.drivers.hardware.seamicro import power_query_seamicro15k_v2 | ||
102 | 69 | try: | ||
103 | 70 | print(power_query_seamicro15k_v2( | ||
104 | 71 | {{escape_py_literal(power_address) | safe}}, | ||
105 | 72 | {{escape_py_literal(power_user) | safe}}, | ||
106 | 73 | {{escape_py_literal(power_pass) | safe}}, | ||
107 | 74 | {{escape_py_literal(system_id) | safe}}, | ||
108 | 75 | )) | ||
109 | 76 | except Exception as e: | ||
110 | 77 | # This gets in the node event log: print the exception's message | ||
111 | 78 | # and not the stacktrace. | ||
112 | 79 | print(unicode(e)) | ||
113 | 80 | sys.exit(1) | ||
114 | 81 | END | ||
115 | 82 | } | ||
116 | 83 | |||
117 | 84 | # Perform power control | ||
118 | 85 | power_control() { | ||
119 | 86 | if [ "{{power_control}}" = "ipmi" ] | ||
120 | 87 | then | ||
121 | 88 | issue_ipmi_command | ||
122 | 89 | elif [ "{{power_control}}" = "restapi" ] | ||
123 | 90 | then | ||
124 | 91 | issue_rest_v09_command | ||
125 | 92 | elif [ "{{power_control}}" = "restapi2" ] | ||
126 | 93 | then | ||
127 | 94 | issue_rest_v2_command | ||
128 | 95 | fi | ||
129 | 96 | } | ||
130 | 97 | |||
131 | 98 | # Query the state. | ||
132 | 99 | # Only supported by REST v2. | ||
133 | 100 | query_state() { | ||
134 | 101 | if [ "{{power_control}}" = "ipmi" ] | ||
135 | 102 | then | ||
136 | 103 | echo "unknown" | ||
137 | 104 | elif [ "{{power_control}}" = "restapi" ] | ||
138 | 105 | then | ||
139 | 106 | echo "unknown" | ||
140 | 107 | elif [ "{{power_control}}" = "restapi2" ] | ||
141 | 108 | then | ||
142 | 109 | query_state_rest_v2 | ||
143 | 110 | fi | ||
144 | 111 | } | ||
145 | 112 | |||
146 | 113 | main() { | ||
147 | 114 | case $1 in | ||
148 | 115 | 'on'|'off') | ||
149 | 116 | power_control | ||
150 | 117 | ;; | ||
151 | 118 | 'query') | ||
152 | 119 | query_state | ||
153 | 120 | ;; | ||
154 | 121 | *) | ||
155 | 122 | fail 2 "Unknown power command: '$1'" | ||
156 | 123 | esac | ||
157 | 124 | } | ||
158 | 125 | |||
159 | 126 | main "{{power_change}}" | ||
160 | 53 | 127 | ||
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 | 321 | power_change): | 321 | power_change): |
166 | 322 | server_id = '%s/0' % server_id | 322 | server_id = '%s/0' % server_id |
167 | 323 | api = get_seamicro15k_api('v2.0', ip, username, password) | 323 | api = get_seamicro15k_api('v2.0', ip, username, password) |
174 | 324 | if api: | 324 | if api is None: |
175 | 325 | server = api.servers.get(server_id) | 325 | raise SeaMicroError('Unable to contact BMC controller.') |
176 | 326 | if power_change == "on": | 326 | server = api.servers.get(server_id) |
177 | 327 | server.power_on(using_pxe=True) | 327 | if power_change == "on": |
178 | 328 | elif power_change == "off": | 328 | server.power_on(using_pxe=True) |
179 | 329 | server.power_off(force=True) | 329 | elif power_change == "off": |
180 | 330 | server.power_off(force=True) | ||
181 | 331 | |||
182 | 332 | |||
183 | 333 | def power_query_seamicro15k_v2(ip, username, password, server_id): | ||
184 | 334 | server_id = '%s/0' % server_id | ||
185 | 335 | api = get_seamicro15k_api('v2.0', ip, username, password) | ||
186 | 336 | if api is None: | ||
187 | 337 | raise SeaMicroError('Unable to contact BMC controller.') | ||
188 | 338 | server = api.servers.get(server_id) | ||
189 | 339 | if server.active: | ||
190 | 340 | return "on" | ||
191 | 341 | return "off" | ||
192 | 330 | 342 | ||
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 | 34 | find_seamicro15k_servers, | 34 | find_seamicro15k_servers, |
198 | 35 | power_control_seamicro15k_v09, | 35 | power_control_seamicro15k_v09, |
199 | 36 | power_control_seamicro15k_v2, | 36 | power_control_seamicro15k_v2, |
200 | 37 | power_query_seamicro15k_v2, | ||
201 | 37 | POWER_STATUS, | 38 | POWER_STATUS, |
202 | 38 | probe_seamicro15k_and_enlist, | 39 | probe_seamicro15k_and_enlist, |
203 | 39 | SeaMicroAPIV09, | 40 | SeaMicroAPIV09, |
204 | @@ -461,3 +462,71 @@ | |||
205 | 461 | 462 | ||
206 | 462 | power_control_seamicro15k_v2(ip, username, password, '0', 'on') | 463 | power_control_seamicro15k_v2(ip, username, password, '0', 'on') |
207 | 463 | mock_power_on.assert_called() | 464 | mock_power_on.assert_called() |
208 | 465 | |||
209 | 466 | def test_power_control_seamicro15k_v2_raises_error_when_api_None(self): | ||
210 | 467 | ip = factory.make_ipv4_address() | ||
211 | 468 | username = factory.make_string() | ||
212 | 469 | password = factory.make_string() | ||
213 | 470 | |||
214 | 471 | mock_get_api = self.patch( | ||
215 | 472 | seamicro, | ||
216 | 473 | 'get_seamicro15k_api') | ||
217 | 474 | mock_get_api.return_value = None | ||
218 | 475 | |||
219 | 476 | self.assertRaises( | ||
220 | 477 | SeaMicroError, | ||
221 | 478 | power_control_seamicro15k_v2, ip, username, password, '0', 'on') | ||
222 | 479 | |||
223 | 480 | def test_power_query_seamicro15k_v2_power_on(self): | ||
224 | 481 | ip = factory.make_ipv4_address() | ||
225 | 482 | username = factory.make_string() | ||
226 | 483 | password = factory.make_string() | ||
227 | 484 | |||
228 | 485 | fake_server = FakeServer('0/0') | ||
229 | 486 | self.patch(fake_server, 'active', True) | ||
230 | 487 | fake_client = FakeSeaMicroClient() | ||
231 | 488 | fake_client.servers = FakeSeaMicroServerManager() | ||
232 | 489 | fake_client.servers.servers.append(fake_server) | ||
233 | 490 | |||
234 | 491 | mock_get_api = self.patch( | ||
235 | 492 | seamicro, | ||
236 | 493 | 'get_seamicro15k_api') | ||
237 | 494 | mock_get_api.return_value = fake_client | ||
238 | 495 | |||
239 | 496 | self.assertEqual( | ||
240 | 497 | "on", | ||
241 | 498 | power_query_seamicro15k_v2(ip, username, password, '0')) | ||
242 | 499 | |||
243 | 500 | def test_power_query_seamicro15k_v2_power_off(self): | ||
244 | 501 | ip = factory.make_ipv4_address() | ||
245 | 502 | username = factory.make_string() | ||
246 | 503 | password = factory.make_string() | ||
247 | 504 | |||
248 | 505 | fake_server = FakeServer('0/0') | ||
249 | 506 | self.patch(fake_server, 'active', False) | ||
250 | 507 | fake_client = FakeSeaMicroClient() | ||
251 | 508 | fake_client.servers = FakeSeaMicroServerManager() | ||
252 | 509 | fake_client.servers.servers.append(fake_server) | ||
253 | 510 | |||
254 | 511 | mock_get_api = self.patch( | ||
255 | 512 | seamicro, | ||
256 | 513 | 'get_seamicro15k_api') | ||
257 | 514 | mock_get_api.return_value = fake_client | ||
258 | 515 | |||
259 | 516 | self.assertEqual( | ||
260 | 517 | "off", | ||
261 | 518 | power_query_seamicro15k_v2(ip, username, password, '0')) | ||
262 | 519 | |||
263 | 520 | def test_power_query_seamicro15k_v2_raises_error_when_api_None(self): | ||
264 | 521 | ip = factory.make_ipv4_address() | ||
265 | 522 | username = factory.make_string() | ||
266 | 523 | password = factory.make_string() | ||
267 | 524 | |||
268 | 525 | mock_get_api = self.patch( | ||
269 | 526 | seamicro, | ||
270 | 527 | 'get_seamicro15k_api') | ||
271 | 528 | mock_get_api.return_value = None | ||
272 | 529 | |||
273 | 530 | self.assertRaises( | ||
274 | 531 | SeaMicroError, | ||
275 | 532 | power_query_seamicro15k_v2, ip, username, password, '0') | ||
276 | 464 | 533 | ||
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 | 61 | # state for these power types. | 61 | # state for these power types. |
282 | 62 | # This is meant to be temporary until all the power types support | 62 | # This is meant to be temporary until all the power types support |
283 | 63 | # querying the power state of a node. | 63 | # querying the power state of a node. |
285 | 64 | QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'ucsm', 'virsh'] | 64 | QUERY_POWER_TYPES = ['amt', 'ipmi', 'mscm', 'sm15k', 'ucsm', 'virsh'] |
286 | 65 | 65 | ||
287 | 66 | 66 | ||
288 | 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, |
289 | @@ -232,7 +232,7 @@ | |||
290 | 232 | new_power_state = yield deferToThread( | 232 | new_power_state = yield deferToThread( |
291 | 233 | perform_power_change, system_id, hostname, power_type, | 233 | perform_power_change, system_id, hostname, power_type, |
292 | 234 | 'query', context) | 234 | 'query', context) |
294 | 235 | if new_power_state == power_change: | 235 | if new_power_state == "unknown" or new_power_state == power_change: |
295 | 236 | yield power_change_success(system_id, hostname, power_change) | 236 | yield power_change_success(system_id, hostname, power_change) |
296 | 237 | return | 237 | return |
297 | 238 | 238 | ||
298 | @@ -298,7 +298,7 @@ | |||
299 | 298 | try: | 298 | try: |
300 | 299 | power_state = yield deferToThread( | 299 | power_state = yield deferToThread( |
301 | 300 | perform_power_query, system_id, hostname, power_type, context) | 300 | perform_power_query, system_id, hostname, power_type, context) |
303 | 301 | if power_state not in ("on", "off"): | 301 | if power_state not in ("on", "off", "unknown"): |
304 | 302 | # This is considered an error. | 302 | # This is considered an error. |
305 | 303 | raise PowerActionFail(power_state) | 303 | raise PowerActionFail(power_state) |
306 | 304 | except PowerActionFail as e: | 304 | except PowerActionFail as e: |
307 | 305 | 305 | ||
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 | 321 | self.assertThat(markNodeBroken, MockNotCalled()) | 321 | self.assertThat(markNodeBroken, MockNotCalled()) |
313 | 322 | 322 | ||
314 | 323 | @inlineCallbacks | 323 | @inlineCallbacks |
315 | 324 | def test_change_power_state_doesnt_retry_if_query_returns_unknown(self): | ||
316 | 325 | system_id = factory.make_name('system_id') | ||
317 | 326 | hostname = factory.make_name('hostname') | ||
318 | 327 | power_type = random.choice(power.QUERY_POWER_TYPES) | ||
319 | 328 | power_change = random.choice(['on', 'off']) | ||
320 | 329 | context = { | ||
321 | 330 | factory.make_name('context-key'): factory.make_name('context-val') | ||
322 | 331 | } | ||
323 | 332 | self.patch(power, 'pause') | ||
324 | 333 | power.power_action_registry[system_id] = power_change | ||
325 | 334 | # Patch the power action utility so that it says the node is | ||
326 | 335 | # in the required power state. | ||
327 | 336 | power_action, execute = patch_power_action( | ||
328 | 337 | self, return_value="unknown") | ||
329 | 338 | markNodeBroken = yield self.patch_rpc_methods() | ||
330 | 339 | |||
331 | 340 | yield power.change_power_state( | ||
332 | 341 | system_id, hostname, power_type, power_change, context) | ||
333 | 342 | self.assertThat( | ||
334 | 343 | execute, | ||
335 | 344 | MockCallsMatch( | ||
336 | 345 | # One call to change the power state. | ||
337 | 346 | call(power_change=power_change, **context), | ||
338 | 347 | # One call to query the power state. | ||
339 | 348 | call(power_change='query', **context), | ||
340 | 349 | ), | ||
341 | 350 | ) | ||
342 | 351 | # The node hasn't been marked broken. | ||
343 | 352 | self.assertThat(markNodeBroken, MockNotCalled()) | ||
344 | 353 | |||
345 | 354 | @inlineCallbacks | ||
346 | 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): |
347 | 325 | system_id = factory.make_name('system_id') | 356 | system_id = factory.make_name('system_id') |
348 | 326 | hostname = factory.make_name('hostname') | 357 | hostname = factory.make_name('hostname') |
349 | @@ -546,6 +577,29 @@ | |||
350 | 546 | self.assertThat( | 577 | self.assertThat( |
351 | 547 | power_state_update, MockCalledOnceWith(system_id, power_state)) | 578 | power_state_update, MockCalledOnceWith(system_id, power_state)) |
352 | 548 | 579 | ||
353 | 580 | def test_get_power_state_changes_power_state_if_unknown(self): | ||
354 | 581 | system_id = factory.make_name('system_id') | ||
355 | 582 | hostname = factory.make_name('hostname') | ||
356 | 583 | power_state = "unknown" | ||
357 | 584 | power_type = random.choice(power.QUERY_POWER_TYPES) | ||
358 | 585 | context = { | ||
359 | 586 | factory.make_name('context-key'): factory.make_name('context-val') | ||
360 | 587 | } | ||
361 | 588 | self.patch(power, 'pause') | ||
362 | 589 | power_state_update = self.patch_autospec(power, 'power_state_update') | ||
363 | 590 | |||
364 | 591 | # Simulate success. | ||
365 | 592 | power_action, execute = patch_power_action( | ||
366 | 593 | self, return_value=power_state) | ||
367 | 594 | _, _, io = self.patch_rpc_methods() | ||
368 | 595 | |||
369 | 596 | d = power.get_power_state( | ||
370 | 597 | system_id, hostname, power_type, context) | ||
371 | 598 | io.flush() | ||
372 | 599 | self.assertEqual(power_state, extract_result(d)) | ||
373 | 600 | self.assertThat( | ||
374 | 601 | power_state_update, MockCalledOnceWith(system_id, power_state)) | ||
375 | 602 | |||
376 | 549 | def test_get_power_state_pauses_inbetween_retries(self): | 603 | def test_get_power_state_pauses_inbetween_retries(self): |
377 | 550 | system_id = factory.make_name('system_id') | 604 | system_id = factory.make_name('system_id') |
378 | 551 | hostname = factory.make_name('hostname') | 605 | hostname = factory.make_name('hostname') |
self approve!