Merge ~ltrager/maas:proxmox into maas:master
- Git
- lp:~ltrager/maas
- proxmox
- Merge into master
Proposed by
Lee Trager
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Lee Trager | ||||
Approved revision: | 591d5be43431444706e00d826ef4c5d8cc0d01a0 | ||||
Merge reported by: | MAAS Lander | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~ltrager/maas:proxmox | ||||
Merge into: | maas:master | ||||
Prerequisite: | ~ltrager/maas:webhook | ||||
Diff against target: |
651 lines (+616/-1) 3 files modified
src/provisioningserver/drivers/power/proxmox.py (+194/-0) src/provisioningserver/drivers/power/registry.py (+3/-1) src/provisioningserver/drivers/power/tests/test_proxmox.py (+419/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Adam Collard (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+396217@code.launchpad.net |
Commit message
LP: #1805799 - Add Proxmox power driver
Description of the change
To post a comment you must log in.
Revision history for this message
Adam Collard (adam-collard) wrote : | # |
Not been able to test this with a real Proxmox install, but code seems good. +1
review:
Approve
Revision history for this message
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b proxmox lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
~ltrager/maas:proxmox
updated
- 591d5be... by Lee Trager
-
Merge branch 'master' into proxmox
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/src/provisioningserver/drivers/power/proxmox.py b/src/provisioningserver/drivers/power/proxmox.py |
2 | new file mode 100644 |
3 | index 0000000..c0e6ca1 |
4 | --- /dev/null |
5 | +++ b/src/provisioningserver/drivers/power/proxmox.py |
6 | @@ -0,0 +1,194 @@ |
7 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
8 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
9 | + |
10 | +"""Generic Proxmox Power Driver.""" |
11 | + |
12 | +from io import BytesIO |
13 | +import json |
14 | + |
15 | +from twisted.internet.defer import inlineCallbacks, succeed |
16 | +from twisted.web.client import FileBodyProducer |
17 | + |
18 | +from provisioningserver.drivers import ( |
19 | + make_ip_extractor, |
20 | + make_setting_field, |
21 | + SETTING_SCOPE, |
22 | +) |
23 | +from provisioningserver.drivers.power import PowerActionError |
24 | +from provisioningserver.drivers.power.webhook import ( |
25 | + SSL_INSECURE_CHOICES, |
26 | + SSL_INSECURE_NO, |
27 | + WebhookPowerDriver, |
28 | +) |
29 | +from provisioningserver.utils.twisted import asynchronous |
30 | + |
31 | + |
32 | +class ProxmoxPowerDriver(WebhookPowerDriver): |
33 | + |
34 | + name = "proxmox" |
35 | + chassis = False |
36 | + # XXX ltrager - 2021-01-11 - Support for probing and Pods could be added. |
37 | + can_probe = False |
38 | + description = "Proxmox" |
39 | + settings = [ |
40 | + make_setting_field( |
41 | + "power_address", "Proxmox host name or IP", required=True |
42 | + ), |
43 | + make_setting_field( |
44 | + "power_user", "Proxmox username, including realm", required=True |
45 | + ), |
46 | + make_setting_field( |
47 | + "power_pass", |
48 | + "Proxmox password, required if a token name and secret aren't " |
49 | + "given", |
50 | + field_type="password", |
51 | + ), |
52 | + make_setting_field("power_token_name", "Proxmox API token name"), |
53 | + make_setting_field( |
54 | + "power_token_secret", |
55 | + "Proxmox API token secret", |
56 | + field_type="password", |
57 | + ), |
58 | + make_setting_field( |
59 | + "power_vm_name", "Node ID", scope=SETTING_SCOPE.NODE |
60 | + ), |
61 | + make_setting_field( |
62 | + "power_verify_ssl", |
63 | + "Verify SSL connections with system CA certificates", |
64 | + field_type="choice", |
65 | + required=True, |
66 | + choices=SSL_INSECURE_CHOICES, |
67 | + default=SSL_INSECURE_NO, |
68 | + ), |
69 | + ] |
70 | + |
71 | + ip_extractor = make_ip_extractor("power_address") |
72 | + |
73 | + def _get_url(self, context, endpoint, params=None): |
74 | + uri = f"https://{context['power_address']}:8006/api2/json/{endpoint}" |
75 | + if params: |
76 | + uri = "%s?%s" % ( |
77 | + uri, |
78 | + "&".join([f"{key}={value}" for key, value in params.items()]), |
79 | + ) |
80 | + return uri.encode() |
81 | + |
82 | + def _login(self, system_id, context): |
83 | + power_token_name = context.get("power_token_name") |
84 | + if power_token_name: |
85 | + if "!" not in power_token_name: |
86 | + # The username must be included with the token name. Proxmox |
87 | + # docs doesn't make this obvious and the UI includes it. |
88 | + power_token_name = ( |
89 | + f"{context['power_user']}!{power_token_name}" |
90 | + ) |
91 | + return succeed( |
92 | + { |
93 | + b"Authorization": [ |
94 | + f"PVEAPIToken={power_token_name}=" |
95 | + f"{context['power_token_secret']}".encode() |
96 | + ] |
97 | + } |
98 | + ) |
99 | + |
100 | + d = self._webhook_request( |
101 | + b"POST", |
102 | + self._get_url(context, "access/ticket"), |
103 | + # Proxmox doesn't support basic HTTP authentication. Don't pass |
104 | + # the context so an authorization header isn't created. |
105 | + self._make_auth_headers( |
106 | + system_id, |
107 | + {}, |
108 | + {b"Content-Type": [b"application/json; charset=utf-8"]}, |
109 | + ), |
110 | + context.get("power_verify_ssl") is True, |
111 | + FileBodyProducer( |
112 | + BytesIO( |
113 | + json.dumps( |
114 | + { |
115 | + "username": context["power_user"], |
116 | + "password": context["power_pass"], |
117 | + } |
118 | + ).encode() |
119 | + ) |
120 | + ), |
121 | + ) |
122 | + |
123 | + def cb(response_data): |
124 | + parsed_data = json.loads(response_data) |
125 | + return { |
126 | + b"Cookie": [ |
127 | + f"PVEAuthCookie={parsed_data['data']['ticket']}".encode() |
128 | + ], |
129 | + b"CSRFPreventionToken": [ |
130 | + parsed_data["data"]["CSRFPreventionToken"].encode() |
131 | + ], |
132 | + } |
133 | + |
134 | + d.addCallback(cb) |
135 | + return d |
136 | + |
137 | + def _find_vm(self, system_id, context, extra_headers): |
138 | + power_vm_name = context["power_vm_name"] |
139 | + d = self._webhook_request( |
140 | + b"GET", |
141 | + self._get_url(context, "cluster/resources", {"type": "vm"}), |
142 | + self._make_auth_headers(system_id, {}, extra_headers), |
143 | + context.get("power_verify_ssl") is True, |
144 | + ) |
145 | + |
146 | + def cb(response_data): |
147 | + parsed_data = json.loads(response_data) |
148 | + for vm in parsed_data["data"]: |
149 | + if power_vm_name in (str(vm.get("vmid")), vm.get("name")): |
150 | + return vm |
151 | + raise PowerActionError("Unable to find virtual machine") |
152 | + |
153 | + d.addCallback(cb) |
154 | + return d |
155 | + |
156 | + @asynchronous |
157 | + @inlineCallbacks |
158 | + def power_on(self, system_id, context): |
159 | + extra_headers = yield self._login(system_id, context) |
160 | + vm = yield self._find_vm(system_id, context, extra_headers) |
161 | + if vm["status"] != "running": |
162 | + yield self._webhook_request( |
163 | + b"POST", |
164 | + self._get_url( |
165 | + context, |
166 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" |
167 | + "status/start", |
168 | + ), |
169 | + self._make_auth_headers(system_id, {}, extra_headers), |
170 | + context.get("power_verify_ssl") is True, |
171 | + ) |
172 | + |
173 | + @asynchronous |
174 | + @inlineCallbacks |
175 | + def power_off(self, system_id, context): |
176 | + extra_headers = yield self._login(system_id, context) |
177 | + vm = yield self._find_vm(system_id, context, extra_headers) |
178 | + if vm["status"] != "stopped": |
179 | + yield self._webhook_request( |
180 | + b"POST", |
181 | + self._get_url( |
182 | + context, |
183 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" |
184 | + "status/stop", |
185 | + ), |
186 | + self._make_auth_headers(system_id, {}, extra_headers), |
187 | + context.get("power_verify_ssl") is True, |
188 | + ) |
189 | + |
190 | + @asynchronous |
191 | + @inlineCallbacks |
192 | + def power_query(self, system_id, context): |
193 | + extra_headers = yield self._login(system_id, context) |
194 | + vm = yield self._find_vm(system_id, context, extra_headers) |
195 | + if vm["status"] == "running": |
196 | + return "on" |
197 | + elif vm["status"] == "stopped": |
198 | + return "off" |
199 | + else: |
200 | + return "unknown" |
201 | diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py |
202 | index 91baa68..5eb0b66 100644 |
203 | --- a/src/provisioningserver/drivers/power/registry.py |
204 | +++ b/src/provisioningserver/drivers/power/registry.py |
205 | @@ -1,4 +1,4 @@ |
206 | -# Copyright 2017-2020 Canonical Ltd. This software is licensed under the |
207 | +# Copyright 2017-2021 Canonical Ltd. This software is licensed under the |
208 | # GNU Affero General Public License version 3 (see the file LICENSE). |
209 | |
210 | """Load all power drivers.""" |
211 | @@ -20,6 +20,7 @@ from provisioningserver.drivers.power.mscm import MSCMPowerDriver |
212 | from provisioningserver.drivers.power.msftocs import MicrosoftOCSPowerDriver |
213 | from provisioningserver.drivers.power.nova import NovaPowerDriver |
214 | from provisioningserver.drivers.power.openbmc import OpenBMCPowerDriver |
215 | +from provisioningserver.drivers.power.proxmox import ProxmoxPowerDriver |
216 | from provisioningserver.drivers.power.recs import RECSPowerDriver |
217 | from provisioningserver.drivers.power.redfish import RedfishPowerDriver |
218 | from provisioningserver.drivers.power.seamicro import SeaMicroPowerDriver |
219 | @@ -61,6 +62,7 @@ power_drivers = [ |
220 | MicrosoftOCSPowerDriver(), |
221 | NovaPowerDriver(), |
222 | OpenBMCPowerDriver(), |
223 | + ProxmoxPowerDriver(), |
224 | RECSPowerDriver(), |
225 | RedfishPowerDriver(), |
226 | SeaMicroPowerDriver(), |
227 | diff --git a/src/provisioningserver/drivers/power/tests/test_proxmox.py b/src/provisioningserver/drivers/power/tests/test_proxmox.py |
228 | new file mode 100644 |
229 | index 0000000..7c32b78 |
230 | --- /dev/null |
231 | +++ b/src/provisioningserver/drivers/power/tests/test_proxmox.py |
232 | @@ -0,0 +1,419 @@ |
233 | +# Copyright 2021 Canonical Ltd. This software is licensed under the |
234 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
235 | + |
236 | +"""Tests for `provisioningserver.drivers.power.proxmox`.""" |
237 | +import json |
238 | +import random |
239 | +from unittest.mock import ANY |
240 | + |
241 | +from testtools import ExpectedException |
242 | +from twisted.internet.defer import inlineCallbacks, succeed |
243 | + |
244 | +from maastesting.factory import factory |
245 | +from maastesting.matchers import MockCalledOnceWith, MockNotCalled |
246 | +from maastesting.testcase import MAASTestCase, MAASTwistedRunTest |
247 | +from provisioningserver.drivers.power import PowerActionError |
248 | +import provisioningserver.drivers.power.proxmox as proxmox_module |
249 | + |
250 | + |
251 | +class TestProxmoxPowerDriver(MAASTestCase): |
252 | + |
253 | + run_tests_with = MAASTwistedRunTest.make_factory(timeout=5) |
254 | + |
255 | + def setUp(self): |
256 | + super().setUp() |
257 | + self.proxmox = proxmox_module.ProxmoxPowerDriver() |
258 | + self.mock_webhook_request = self.patch( |
259 | + self.proxmox, "_webhook_request" |
260 | + ) |
261 | + |
262 | + def test_get_url(self): |
263 | + power_address = factory.make_name("power_address") |
264 | + endpoint = factory.make_name("endpoint") |
265 | + self.assertEqual( |
266 | + f"https://{power_address}:8006/api2/json/{endpoint}".encode(), |
267 | + self.proxmox._get_url({"power_address": power_address}, endpoint), |
268 | + ) |
269 | + |
270 | + def test_get_url_params(self): |
271 | + power_address = factory.make_name("power_address") |
272 | + endpoint = factory.make_name("endpoint") |
273 | + params = { |
274 | + factory.make_name("key"): factory.make_name("value") |
275 | + for _ in range(3) |
276 | + } |
277 | + params_str = "&".join( |
278 | + [f"{key}={value}" for key, value in params.items()] |
279 | + ) |
280 | + self.assertEqual( |
281 | + f"https://{power_address}:8006/api2/json/{endpoint}?{params_str}".encode(), |
282 | + self.proxmox._get_url( |
283 | + {"power_address": power_address}, endpoint, params |
284 | + ), |
285 | + ) |
286 | + |
287 | + @inlineCallbacks |
288 | + def test_login(self): |
289 | + system_id = factory.make_name("system_id") |
290 | + context = { |
291 | + "power_address": factory.make_name("power_address"), |
292 | + "power_user": factory.make_name("power_user"), |
293 | + "power_pass": factory.make_name("power_pass"), |
294 | + } |
295 | + ticket = factory.make_name("ticket") |
296 | + token = factory.make_name("token") |
297 | + self.mock_webhook_request.return_value = succeed( |
298 | + json.dumps( |
299 | + { |
300 | + "data": { |
301 | + "ticket": ticket, |
302 | + "CSRFPreventionToken": token, |
303 | + } |
304 | + } |
305 | + ) |
306 | + ) |
307 | + |
308 | + extra_headers = yield self.proxmox._login(system_id, context) |
309 | + |
310 | + self.assertEqual( |
311 | + { |
312 | + b"Cookie": [f"PVEAuthCookie={ticket}".encode()], |
313 | + b"CSRFPreventionToken": [token.encode()], |
314 | + }, |
315 | + extra_headers, |
316 | + ) |
317 | + self.assertThat( |
318 | + self.mock_webhook_request, |
319 | + MockCalledOnceWith( |
320 | + b"POST", |
321 | + self.proxmox._get_url(context, "access/ticket"), |
322 | + self.proxmox._make_auth_headers( |
323 | + system_id, |
324 | + {}, |
325 | + {b"Content-Type": [b"application/json; charset=utf-8"]}, |
326 | + ), |
327 | + False, |
328 | + # unittest doesn't know how to compare FileBodyProducer |
329 | + ANY, |
330 | + ), |
331 | + ) |
332 | + |
333 | + @inlineCallbacks |
334 | + def test_login_uses_api_token(self): |
335 | + system_id = factory.make_name("system_id") |
336 | + power_user = factory.make_name("power_user") |
337 | + context = { |
338 | + "power_address": factory.make_name("power_address"), |
339 | + "power_user": power_user, |
340 | + "power_pass": factory.make_name("power_pass"), |
341 | + "power_token_name": f"{power_user}!{factory.make_name('power_token_name')}", |
342 | + "power_token_secret": factory.make_name("power_token_secret"), |
343 | + } |
344 | + |
345 | + extra_headers = yield self.proxmox._login(system_id, context) |
346 | + |
347 | + self.assertEqual( |
348 | + { |
349 | + b"Authorization": [ |
350 | + f"PVEAPIToken={context['power_token_name']}=" |
351 | + f"{context['power_token_secret']}".encode() |
352 | + ] |
353 | + }, |
354 | + extra_headers, |
355 | + ) |
356 | + self.assertThat(self.mock_webhook_request, MockNotCalled()) |
357 | + |
358 | + @inlineCallbacks |
359 | + def test_login_uses_api_token_adds_username(self): |
360 | + system_id = factory.make_name("system_id") |
361 | + context = { |
362 | + "power_address": factory.make_name("power_address"), |
363 | + "power_user": factory.make_name("power_user"), |
364 | + "power_pass": factory.make_name("power_pass"), |
365 | + "power_token_name": factory.make_name("power_token_name"), |
366 | + "power_token_secret": factory.make_name("power_token_secret"), |
367 | + } |
368 | + |
369 | + extra_headers = yield self.proxmox._login(system_id, context) |
370 | + |
371 | + self.assertEqual( |
372 | + { |
373 | + b"Authorization": [ |
374 | + f"PVEAPIToken={context['power_user']}!" |
375 | + f"{context['power_token_name']}=" |
376 | + f"{context['power_token_secret']}".encode() |
377 | + ] |
378 | + }, |
379 | + extra_headers, |
380 | + ) |
381 | + self.assertThat(self.mock_webhook_request, MockNotCalled()) |
382 | + |
383 | + @inlineCallbacks |
384 | + def test_find_vm(self): |
385 | + system_id = factory.make_name("system_id") |
386 | + context = { |
387 | + "power_address": factory.make_name("power_address"), |
388 | + "power_vm_name": factory.make_name("power_vm_name"), |
389 | + } |
390 | + vm = {random.choice(["vmid", "name"]): context["power_vm_name"]} |
391 | + extra_headers = { |
392 | + factory.make_name("key").encode(): [ |
393 | + factory.make_name("value").encode() |
394 | + ] |
395 | + for _ in range(3) |
396 | + } |
397 | + self.mock_webhook_request.return_value = succeed( |
398 | + json.dumps({"data": [vm]}) |
399 | + ) |
400 | + |
401 | + found_vm = yield self.proxmox._find_vm( |
402 | + system_id, context, extra_headers |
403 | + ) |
404 | + |
405 | + self.assertEqual(vm, found_vm) |
406 | + self.assertThat( |
407 | + self.mock_webhook_request, |
408 | + MockCalledOnceWith( |
409 | + b"GET", |
410 | + self.proxmox._get_url( |
411 | + context, "cluster/resources", {"type": "vm"} |
412 | + ), |
413 | + self.proxmox._make_auth_headers(system_id, {}, extra_headers), |
414 | + False, |
415 | + ), |
416 | + ) |
417 | + |
418 | + @inlineCallbacks |
419 | + def test_find_vm_doesnt_find_vm(self): |
420 | + system_id = factory.make_name("system_id") |
421 | + context = { |
422 | + "power_address": factory.make_name("power_address"), |
423 | + "power_vm_name": factory.make_name("power_vm_name"), |
424 | + } |
425 | + vm = { |
426 | + random.choice(["vmid", "name"]): factory.make_name( |
427 | + "another_power_vm_name" |
428 | + ) |
429 | + } |
430 | + extra_headers = { |
431 | + factory.make_name("key").encode(): [ |
432 | + factory.make_name("value").encode() |
433 | + ] |
434 | + for _ in range(3) |
435 | + } |
436 | + self.mock_webhook_request.return_value = succeed( |
437 | + json.dumps({"data": [vm]}) |
438 | + ) |
439 | + |
440 | + with ExpectedException(PowerActionError): |
441 | + yield self.proxmox._find_vm(system_id, context, extra_headers) |
442 | + self.assertThat( |
443 | + self.mock_webhook_request, |
444 | + MockCalledOnceWith( |
445 | + b"GET", |
446 | + self.proxmox._get_url( |
447 | + context, "cluster/resources", {"type": "vm"} |
448 | + ), |
449 | + self.proxmox._make_auth_headers(system_id, {}, extra_headers), |
450 | + False, |
451 | + ), |
452 | + ) |
453 | + |
454 | + @inlineCallbacks |
455 | + def test_power_on(self): |
456 | + system_id = factory.make_name("system_id") |
457 | + context = {"power_address": factory.make_name("power_address")} |
458 | + extra_headers = { |
459 | + factory.make_name("key").encode(): [ |
460 | + factory.make_name("value").encode() |
461 | + ] |
462 | + for _ in range(3) |
463 | + } |
464 | + vm = { |
465 | + "node": factory.make_name("node"), |
466 | + "type": factory.make_name("type"), |
467 | + "vmid": factory.make_name("vmid"), |
468 | + "status": "stopped", |
469 | + } |
470 | + self.patch(self.proxmox, "_login").return_value = succeed( |
471 | + extra_headers |
472 | + ) |
473 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
474 | + |
475 | + yield self.proxmox.power_on(system_id, context) |
476 | + |
477 | + self.assertThat( |
478 | + self.mock_webhook_request, |
479 | + MockCalledOnceWith( |
480 | + b"POST", |
481 | + self.proxmox._get_url( |
482 | + context, |
483 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" |
484 | + "status/start", |
485 | + ), |
486 | + self.proxmox._make_auth_headers(system_id, {}, extra_headers), |
487 | + False, |
488 | + ), |
489 | + ) |
490 | + |
491 | + @inlineCallbacks |
492 | + def test_power_on_not_called_if_on(self): |
493 | + system_id = factory.make_name("system_id") |
494 | + context = {"power_address": factory.make_name("power_address")} |
495 | + extra_headers = { |
496 | + factory.make_name("key").encode(): [ |
497 | + factory.make_name("value").encode() |
498 | + ] |
499 | + for _ in range(3) |
500 | + } |
501 | + vm = { |
502 | + "node": factory.make_name("node"), |
503 | + "type": factory.make_name("type"), |
504 | + "vmid": factory.make_name("vmid"), |
505 | + "status": "running", |
506 | + } |
507 | + self.patch(self.proxmox, "_login").return_value = succeed( |
508 | + extra_headers |
509 | + ) |
510 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
511 | + |
512 | + yield self.proxmox.power_on(system_id, context) |
513 | + |
514 | + self.assertThat(self.mock_webhook_request, MockNotCalled()) |
515 | + |
516 | + @inlineCallbacks |
517 | + def test_power_off(self): |
518 | + system_id = factory.make_name("system_id") |
519 | + context = {"power_address": factory.make_name("power_address")} |
520 | + extra_headers = { |
521 | + factory.make_name("key").encode(): [ |
522 | + factory.make_name("value").encode() |
523 | + ] |
524 | + for _ in range(3) |
525 | + } |
526 | + vm = { |
527 | + "node": factory.make_name("node"), |
528 | + "type": factory.make_name("type"), |
529 | + "vmid": factory.make_name("vmid"), |
530 | + "status": "running", |
531 | + } |
532 | + self.patch(self.proxmox, "_login").return_value = succeed( |
533 | + extra_headers |
534 | + ) |
535 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
536 | + |
537 | + yield self.proxmox.power_off(system_id, context) |
538 | + |
539 | + self.assertThat( |
540 | + self.mock_webhook_request, |
541 | + MockCalledOnceWith( |
542 | + b"POST", |
543 | + self.proxmox._get_url( |
544 | + context, |
545 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" |
546 | + "status/stop", |
547 | + ), |
548 | + self.proxmox._make_auth_headers(system_id, {}, extra_headers), |
549 | + False, |
550 | + ), |
551 | + ) |
552 | + |
553 | + @inlineCallbacks |
554 | + def test_power_off_not_called_if_off(self): |
555 | + system_id = factory.make_name("system_id") |
556 | + context = {"power_address": factory.make_name("power_address")} |
557 | + extra_headers = { |
558 | + factory.make_name("key").encode(): [ |
559 | + factory.make_name("value").encode() |
560 | + ] |
561 | + for _ in range(3) |
562 | + } |
563 | + vm = { |
564 | + "node": factory.make_name("node"), |
565 | + "type": factory.make_name("type"), |
566 | + "vmid": factory.make_name("vmid"), |
567 | + "status": "stopped", |
568 | + } |
569 | + self.patch(self.proxmox, "_login").return_value = succeed( |
570 | + extra_headers |
571 | + ) |
572 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
573 | + |
574 | + yield self.proxmox.power_off(system_id, context) |
575 | + |
576 | + self.assertThat(self.mock_webhook_request, MockNotCalled()) |
577 | + |
578 | + @inlineCallbacks |
579 | + def test_power_query_on(self): |
580 | + system_id = factory.make_name("system_id") |
581 | + context = {"power_address": factory.make_name("power_address")} |
582 | + extra_headers = { |
583 | + factory.make_name("key").encode(): [ |
584 | + factory.make_name("value").encode() |
585 | + ] |
586 | + for _ in range(3) |
587 | + } |
588 | + vm = { |
589 | + "node": factory.make_name("node"), |
590 | + "type": factory.make_name("type"), |
591 | + "vmid": factory.make_name("vmid"), |
592 | + "status": "running", |
593 | + } |
594 | + self.patch(self.proxmox, "_login").return_value = succeed( |
595 | + extra_headers |
596 | + ) |
597 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
598 | + |
599 | + status = yield self.proxmox.power_query(system_id, context) |
600 | + |
601 | + self.assertEqual("on", status) |
602 | + |
603 | + @inlineCallbacks |
604 | + def test_power_query_off(self): |
605 | + system_id = factory.make_name("system_id") |
606 | + context = {"power_address": factory.make_name("power_address")} |
607 | + extra_headers = { |
608 | + factory.make_name("key").encode(): [ |
609 | + factory.make_name("value").encode() |
610 | + ] |
611 | + for _ in range(3) |
612 | + } |
613 | + vm = { |
614 | + "node": factory.make_name("node"), |
615 | + "type": factory.make_name("type"), |
616 | + "vmid": factory.make_name("vmid"), |
617 | + "status": "stopped", |
618 | + } |
619 | + self.patch(self.proxmox, "_login").return_value = succeed( |
620 | + extra_headers |
621 | + ) |
622 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
623 | + |
624 | + status = yield self.proxmox.power_query(system_id, context) |
625 | + |
626 | + self.assertEqual("off", status) |
627 | + |
628 | + @inlineCallbacks |
629 | + def test_power_query_unknown(self): |
630 | + system_id = factory.make_name("system_id") |
631 | + context = {"power_address": factory.make_name("power_address")} |
632 | + extra_headers = { |
633 | + factory.make_name("key").encode(): [ |
634 | + factory.make_name("value").encode() |
635 | + ] |
636 | + for _ in range(3) |
637 | + } |
638 | + vm = { |
639 | + "node": factory.make_name("node"), |
640 | + "type": factory.make_name("type"), |
641 | + "vmid": factory.make_name("vmid"), |
642 | + "status": factory.make_name("status"), |
643 | + } |
644 | + self.patch(self.proxmox, "_login").return_value = succeed( |
645 | + extra_headers |
646 | + ) |
647 | + self.patch(self.proxmox, "_find_vm").return_value = succeed(vm) |
648 | + |
649 | + status = yield self.proxmox.power_query(system_id, context) |
650 | + |
651 | + self.assertEqual("unknown", status) |
UNIT TESTS
-b proxmox lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS c1b2fce0418eb7e 00e71dbbf4
COMMIT: 74fe52ac1d44948