Merge ~ltrager/maas:lp1916093 into maas:master
- Git
- lp:~ltrager/maas
- lp1916093
- Merge into master
Proposed by
Lee Trager
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Lee Trager | ||||
Approved revision: | 6edf03727fb6b7a3bb750d0ae9373171df453e4c | ||||
Merge reported by: | MAAS Lander | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~ltrager/maas:lp1916093 | ||||
Merge into: | maas:master | ||||
Diff against target: |
1227 lines (+802/-22) 10 files modified
src/maasserver/api/machines.py (+48/-4) src/maasserver/api/tests/test_machines.py (+116/-3) src/maasserver/models/node.py (+6/-0) src/maasserver/models/tests/test_node.py (+15/-0) src/provisioningserver/drivers/power/proxmox.py (+117/-7) src/provisioningserver/drivers/power/tests/test_proxmox.py (+381/-2) src/provisioningserver/drivers/power/webhook.py (+3/-3) src/provisioningserver/rpc/cluster.py (+4/-1) src/provisioningserver/rpc/clusterservice.py (+20/-1) src/provisioningserver/rpc/tests/test_clusterservice.py (+92/-1) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Adam Collard (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+398754@code.launchpad.net |
Commit message
LP: #1916093 - Fix adding more than one Proxmox BMC and add chassis support
Description of the change
To post a comment you must log in.
Revision history for this message
Adam Collard (adam-collard) : | # |
review:
Needs Fixing
~ltrager/maas:lp1916093
updated
- fdd07d5... by Lee Trager
-
adam-collard fixes
Revision history for this message
Lee Trager (ltrager) wrote : | # |
Thanks for the review. Updated as suggested.
Revision history for this message
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b lp1916093 lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS
COMMIT: fdd07d50818e6c2
review:
Approve
Revision history for this message
Adam Collard (adam-collard) wrote : | # |
api/tests/
Revision history for this message
Adam Collard (adam-collard) : | # |
review:
Approve
~ltrager/maas:lp1916093
updated
- 11df01e... by Lee Trager
-
Merge branch 'master' into lp1916093
- 6edf037... by Lee Trager
-
adam-collard patch
Revision history for this message
Lee Trager (ltrager) wrote : | # |
Thanks for the review. Applied the patch as suggested :)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py |
2 | index 54533b7..0909ff3 100644 |
3 | --- a/src/maasserver/api/machines.py |
4 | +++ b/src/maasserver/api/machines.py |
5 | @@ -1,4 +1,4 @@ |
6 | -# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
7 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
8 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | |
10 | __all__ = [ |
11 | @@ -2577,6 +2577,7 @@ class MachinesHandler(NodesHandler, PowersMixin): |
12 | - ``mscm``: Moonshot Chassis Manager. |
13 | - ``msftocs``: Microsoft OCS Chassis Manager. |
14 | - ``powerkvm``: Virtual Machines on Power KVM, managed by Virsh. |
15 | + - ``proxmox``: Virtual Machines managed by Proxmox |
16 | - ``recs_box``: Christmann RECS|Box servers. |
17 | - ``sm15k``: Seamicro 1500 Chassis. |
18 | - ``ucsm``: Cisco UCS Manager. |
19 | @@ -2606,18 +2607,31 @@ class MachinesHandler(NodesHandler, PowersMixin): |
20 | machine added should use. |
21 | |
22 | @param (string) "prefix_filter" [required=false] (``virsh``, |
23 | - ``vmware``, ``powerkvm`` only.) Filter machines with supplied prefix. |
24 | + ``vmware``, ``powerkvm``, ``proxmox`` only.) Filter machines with |
25 | + supplied prefix. |
26 | |
27 | @param (string) "power_control" [required=false] (``seamicro15k`` only) |
28 | The power_control to use, either ipmi (default), restapi, or restapi2. |
29 | |
30 | + The following are optional if you are adding a proxmox chassis. |
31 | + |
32 | + @param (string) "token_name" [required=false] The name the |
33 | + authentication token to be used instead of a password. |
34 | + |
35 | + @param (string) "token_secret" [required=false] The token secret |
36 | + to be used in combination with the power_token_name used in place of |
37 | + a password. |
38 | + |
39 | + @param (boolean) "verify_ssl" [required=false] Whether SSL |
40 | + connections should be verified. |
41 | + |
42 | The following are optional if you are adding a recs_box, vmware or |
43 | msftocs chassis. |
44 | |
45 | @param (int) "port" [required=false] (``recs_box``, ``vmware``, |
46 | ``msftocs`` only) The port to use when accessing the chassis. |
47 | |
48 | - The following are optioanl if you are adding a vmware chassis: |
49 | + The following are optional if you are adding a vmware chassis: |
50 | |
51 | @param (string) "protocol" [required=false] (``vmware`` only) The |
52 | protocol to use when accessing the VMware chassis (default: https). |
53 | @@ -2657,9 +2671,31 @@ class MachinesHandler(NodesHandler, PowersMixin): |
54 | ): |
55 | username = get_mandatory_param(request.POST, "username") |
56 | password = get_mandatory_param(request.POST, "password") |
57 | + token_name = None |
58 | + token_secret = None |
59 | + elif chassis_type == "proxmox": |
60 | + username = get_mandatory_param(request.POST, "username") |
61 | + password = get_optional_param(request.POST, "password") |
62 | + token_name = get_optional_param(request.POST, "token_name") |
63 | + token_secret = get_optional_param(request.POST, "token_secret") |
64 | + if not any([password, token_name, token_secret]): |
65 | + return HttpResponseBadRequest( |
66 | + "You must use a password or token with Proxmox." |
67 | + ) |
68 | + elif all([password, token_name, token_secret]): |
69 | + return HttpResponseBadRequest( |
70 | + "You may only use a password or token with Proxmox, " |
71 | + "not both." |
72 | + ) |
73 | + elif password is None and not all([token_name, token_secret]): |
74 | + return HttpResponseBadRequest( |
75 | + "Proxmox requires both a token_name and token_secret." |
76 | + ) |
77 | else: |
78 | username = get_optional_param(request.POST, "username") |
79 | password = get_optional_param(request.POST, "password") |
80 | + token_name = None |
81 | + token_secret = None |
82 | if username is not None and chassis_type in ("powerkvm", "virsh"): |
83 | return HttpResponseBadRequest( |
84 | "username can not be specified when using the %s chassis." |
85 | @@ -2675,12 +2711,13 @@ class MachinesHandler(NodesHandler, PowersMixin): |
86 | else: |
87 | accept_all = False |
88 | |
89 | - # Only available with virsh, vmware, and powerkvm |
90 | + # Only available with virsh, vmware, powerkvm, and proxmox |
91 | prefix_filter = get_optional_param(request.POST, "prefix_filter") |
92 | if prefix_filter is not None and chassis_type not in ( |
93 | "powerkvm", |
94 | "virsh", |
95 | "vmware", |
96 | + "proxmox", |
97 | ): |
98 | return HttpResponseBadRequest( |
99 | "prefix_filter is unavailable with the %s chassis type" |
100 | @@ -2732,6 +2769,10 @@ class MachinesHandler(NodesHandler, PowersMixin): |
101 | ), |
102 | ) |
103 | |
104 | + verify_ssl = get_optional_param( |
105 | + request.POST, "verify_ssl", default=False, validator=StringBool |
106 | + ) |
107 | + |
108 | # If given a domain make sure it exists first |
109 | domain_name = get_optional_param(request.POST, "domain") |
110 | if domain_name is not None: |
111 | @@ -2788,6 +2829,9 @@ class MachinesHandler(NodesHandler, PowersMixin): |
112 | power_control, |
113 | port, |
114 | protocol, |
115 | + token_name, |
116 | + token_secret, |
117 | + verify_ssl, |
118 | ) |
119 | |
120 | return HttpResponse( |
121 | diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py |
122 | index 8d394ec..39bb5da 100644 |
123 | --- a/src/maasserver/api/tests/test_machines.py |
124 | +++ b/src/maasserver/api/tests/test_machines.py |
125 | @@ -1,4 +1,4 @@ |
126 | -# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
127 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
128 | # GNU Affero General Public License version 3 (see the file LICENSE). |
129 | |
130 | """Tests for the machines API.""" |
131 | @@ -2843,6 +2843,85 @@ class TestMachinesAPI(APITestCase.ForUser): |
132 | ) |
133 | self.assertEqual(b"No provided password!", response.content) |
134 | |
135 | + def test_POST_add_chassis_proxmox_requires_password_or_token(self): |
136 | + self.become_admin() |
137 | + rack = factory.make_RackController() |
138 | + chassis_mock = self.patch(rack, "add_chassis") |
139 | + response = self.client.post( |
140 | + reverse("machines_handler"), |
141 | + { |
142 | + "op": "add_chassis", |
143 | + "rack_controller": rack.system_id, |
144 | + "chassis_type": "proxmox", |
145 | + "hostname": factory.make_url(), |
146 | + "username": factory.make_name("username"), |
147 | + }, |
148 | + ) |
149 | + self.assertEqual( |
150 | + http.client.BAD_REQUEST, response.status_code, response.content |
151 | + ) |
152 | + self.assertEqual( |
153 | + ("You must use a password or token with Proxmox.").encode("utf-8"), |
154 | + response.content, |
155 | + ) |
156 | + self.assertEqual(chassis_mock.call_count, 0) |
157 | + |
158 | + def test_POST_add_chassis_proxmox_requires_password_xor_token(self): |
159 | + self.become_admin() |
160 | + rack = factory.make_RackController() |
161 | + chassis_mock = self.patch(rack, "add_chassis") |
162 | + response = self.client.post( |
163 | + reverse("machines_handler"), |
164 | + { |
165 | + "op": "add_chassis", |
166 | + "rack_controller": rack.system_id, |
167 | + "chassis_type": "proxmox", |
168 | + "hostname": factory.make_url(), |
169 | + "username": factory.make_name("username"), |
170 | + "password": factory.make_name("password"), |
171 | + "token_name": factory.make_name("token_name"), |
172 | + "token_secret": factory.make_name("token_secret"), |
173 | + }, |
174 | + ) |
175 | + self.assertEqual( |
176 | + http.client.BAD_REQUEST, response.status_code, response.content |
177 | + ) |
178 | + self.assertEqual( |
179 | + ( |
180 | + "You may only use a password or token with Proxmox, not both." |
181 | + ).encode("utf-8"), |
182 | + response.content, |
183 | + ) |
184 | + self.assertEqual(chassis_mock.call_count, 0) |
185 | + |
186 | + def test_POST_add_chassis_proxmox_requires_token_name_and_secret(self): |
187 | + self.become_admin() |
188 | + rack = factory.make_RackController() |
189 | + chassis_mock = self.patch(rack, "add_chassis") |
190 | + response = self.client.post( |
191 | + reverse("machines_handler"), |
192 | + { |
193 | + "op": "add_chassis", |
194 | + "rack_controller": rack.system_id, |
195 | + "chassis_type": "proxmox", |
196 | + "hostname": factory.make_url(), |
197 | + "username": factory.make_name("username"), |
198 | + random.choice( |
199 | + ["token_name", "token_secret"] |
200 | + ): factory.make_name("token"), |
201 | + }, |
202 | + ) |
203 | + self.assertEqual( |
204 | + http.client.BAD_REQUEST, response.status_code, response.content |
205 | + ) |
206 | + self.assertEqual( |
207 | + ("Proxmox requires both a token_name and token_secret.").encode( |
208 | + "utf-8" |
209 | + ), |
210 | + response.content, |
211 | + ) |
212 | + self.assertEqual(chassis_mock.call_count, 0) |
213 | + |
214 | def test_POST_add_chassis_username_disallowed_on_virsh_and_powerkvm(self): |
215 | self.become_admin() |
216 | rack = factory.make_RackController() |
217 | @@ -2907,6 +2986,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
218 | None, |
219 | None, |
220 | None, |
221 | + None, |
222 | + None, |
223 | + False, |
224 | ), |
225 | ) |
226 | |
227 | @@ -2945,6 +3027,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
228 | None, |
229 | None, |
230 | None, |
231 | + None, |
232 | + None, |
233 | + False, |
234 | ), |
235 | ) |
236 | |
237 | @@ -2957,7 +3042,7 @@ class TestMachinesAPI(APITestCase.ForUser): |
238 | accessible_by_url.return_value = rack |
239 | add_chassis = self.patch(rack, "add_chassis") |
240 | hostname = factory.make_url() |
241 | - for chassis_type in ("powerkvm", "virsh", "vmware"): |
242 | + for chassis_type in ("powerkvm", "virsh", "vmware", "proxmox"): |
243 | prefix_filter = factory.make_name("prefix_filter") |
244 | password = factory.make_name("password") |
245 | params = { |
246 | @@ -2967,7 +3052,7 @@ class TestMachinesAPI(APITestCase.ForUser): |
247 | "password": password, |
248 | "prefix_filter": prefix_filter, |
249 | } |
250 | - if chassis_type == "vmware": |
251 | + if chassis_type in {"vmware", "proxmox"}: |
252 | username = factory.make_name("username") |
253 | params["username"] = username |
254 | else: |
255 | @@ -2990,6 +3075,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
256 | None, |
257 | None, |
258 | None, |
259 | + None, |
260 | + None, |
261 | + False, |
262 | ), |
263 | ) |
264 | |
265 | @@ -3086,6 +3174,7 @@ class TestMachinesAPI(APITestCase.ForUser): |
266 | "virsh", |
267 | "vmware", |
268 | "powerkvm", |
269 | + "proxmox", |
270 | ): |
271 | params = { |
272 | "op": "add_chassis", |
273 | @@ -3149,6 +3238,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
274 | None, |
275 | port, |
276 | None, |
277 | + None, |
278 | + None, |
279 | + False, |
280 | ), |
281 | ) |
282 | |
283 | @@ -3266,6 +3358,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
284 | None, |
285 | None, |
286 | protocol, |
287 | + None, |
288 | + None, |
289 | + False, |
290 | ), |
291 | ) |
292 | |
293 | @@ -3336,6 +3431,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
294 | None, |
295 | None, |
296 | None, |
297 | + None, |
298 | + None, |
299 | + False, |
300 | ), |
301 | ) |
302 | |
303 | @@ -3375,6 +3473,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
304 | None, |
305 | None, |
306 | None, |
307 | + None, |
308 | + None, |
309 | + False, |
310 | ), |
311 | ) |
312 | |
313 | @@ -3435,6 +3536,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
314 | None, |
315 | None, |
316 | None, |
317 | + None, |
318 | + None, |
319 | + False, |
320 | ), |
321 | ) |
322 | |
323 | @@ -3475,6 +3579,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
324 | None, |
325 | None, |
326 | None, |
327 | + None, |
328 | + None, |
329 | + False, |
330 | ), |
331 | ) |
332 | |
333 | @@ -3544,6 +3651,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
334 | None, |
335 | None, |
336 | None, |
337 | + None, |
338 | + None, |
339 | + False, |
340 | ), |
341 | ) |
342 | self.assertThat( |
343 | @@ -3560,6 +3670,9 @@ class TestMachinesAPI(APITestCase.ForUser): |
344 | None, |
345 | None, |
346 | None, |
347 | + None, |
348 | + None, |
349 | + False, |
350 | ), |
351 | ) |
352 | |
353 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
354 | index 41c092a..78df62e 100644 |
355 | --- a/src/maasserver/models/node.py |
356 | +++ b/src/maasserver/models/node.py |
357 | @@ -6957,6 +6957,9 @@ class RackController(Controller): |
358 | power_control=None, |
359 | port=None, |
360 | protocol=None, |
361 | + token_name=None, |
362 | + token_secret=None, |
363 | + verify_ssl=False, |
364 | ): |
365 | self._register_request_event( |
366 | self.owner, |
367 | @@ -6977,6 +6980,9 @@ class RackController(Controller): |
368 | power_control=power_control, |
369 | port=port, |
370 | protocol=protocol, |
371 | + token_name=token_name, |
372 | + token_secret=token_secret, |
373 | + verify_ssl=verify_ssl, |
374 | ) |
375 | call.wait(30) |
376 | |
377 | diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py |
378 | index a6f1838..f26c657 100644 |
379 | --- a/src/maasserver/models/tests/test_node.py |
380 | +++ b/src/maasserver/models/tests/test_node.py |
381 | @@ -13552,6 +13552,9 @@ class TestRackController(MAASTransactionServerTestCase): |
382 | power_control = factory.make_name("power_control") |
383 | port = random.randint(0, 65535) |
384 | given_protocol = factory.make_name("protocol") |
385 | + token_name = factory.make_name("token_name") |
386 | + token_secret = factory.make_name("token_secret") |
387 | + verify_ssl = factory.pick_bool() |
388 | |
389 | rackcontroller.add_chassis( |
390 | user, |
391 | @@ -13565,6 +13568,9 @@ class TestRackController(MAASTransactionServerTestCase): |
392 | power_control, |
393 | port, |
394 | given_protocol, |
395 | + token_name, |
396 | + token_secret, |
397 | + verify_ssl, |
398 | ) |
399 | |
400 | self.expectThat( |
401 | @@ -13582,6 +13588,9 @@ class TestRackController(MAASTransactionServerTestCase): |
402 | power_control=power_control, |
403 | port=port, |
404 | protocol=given_protocol, |
405 | + token_name=token_name, |
406 | + token_secret=token_secret, |
407 | + verify_ssl=verify_ssl, |
408 | ), |
409 | ) |
410 | |
411 | @@ -13605,6 +13614,9 @@ class TestRackController(MAASTransactionServerTestCase): |
412 | power_control = factory.make_name("power_control") |
413 | port = random.randint(0, 65535) |
414 | given_protocol = factory.make_name("protocol") |
415 | + token_name = factory.make_name("token_name") |
416 | + token_secret = factory.make_name("token_secret") |
417 | + verify_ssl = factory.pick_bool() |
418 | |
419 | register_event = self.patch(rackcontroller, "_register_request_event") |
420 | rackcontroller.add_chassis( |
421 | @@ -13619,6 +13631,9 @@ class TestRackController(MAASTransactionServerTestCase): |
422 | power_control, |
423 | port, |
424 | given_protocol, |
425 | + token_name, |
426 | + token_secret, |
427 | + verify_ssl, |
428 | ) |
429 | post_commit_hooks.reset() # Ignore these for now. |
430 | self.assertThat( |
431 | diff --git a/src/provisioningserver/drivers/power/proxmox.py b/src/provisioningserver/drivers/power/proxmox.py |
432 | index 7e141e9..cf7a8db 100644 |
433 | --- a/src/provisioningserver/drivers/power/proxmox.py |
434 | +++ b/src/provisioningserver/drivers/power/proxmox.py |
435 | @@ -5,6 +5,7 @@ |
436 | |
437 | from io import BytesIO |
438 | import json |
439 | +import re |
440 | from urllib.parse import urlencode, urlparse |
441 | |
442 | from twisted.internet.defer import inlineCallbacks, succeed |
443 | @@ -20,17 +21,18 @@ from provisioningserver.drivers.power import PowerActionError |
444 | from provisioningserver.drivers.power.webhook import ( |
445 | SSL_INSECURE_CHOICES, |
446 | SSL_INSECURE_NO, |
447 | + SSL_INSECURE_YES, |
448 | WebhookPowerDriver, |
449 | ) |
450 | +from provisioningserver.rpc.utils import commission_node, create_node |
451 | from provisioningserver.utils.twisted import asynchronous |
452 | |
453 | |
454 | class ProxmoxPowerDriver(WebhookPowerDriver): |
455 | |
456 | name = "proxmox" |
457 | - chassis = False |
458 | - # XXX ltrager - 2021-01-11 - Support for probing and Pods could be added. |
459 | - can_probe = False |
460 | + chassis = True |
461 | + can_probe = True |
462 | description = "Proxmox" |
463 | settings = [ |
464 | make_setting_field( |
465 | @@ -117,7 +119,7 @@ class ProxmoxPowerDriver(WebhookPowerDriver): |
466 | {}, |
467 | {b"Content-Type": [b"application/json; charset=utf-8"]}, |
468 | ), |
469 | - context.get("power_verify_ssl") is True, |
470 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
471 | FileBodyProducer( |
472 | BytesIO( |
473 | json.dumps( |
474 | @@ -150,7 +152,7 @@ class ProxmoxPowerDriver(WebhookPowerDriver): |
475 | b"GET", |
476 | self._get_url(context, "cluster/resources", {"type": "vm"}), |
477 | self._make_auth_headers(system_id, {}, extra_headers), |
478 | - context.get("power_verify_ssl") is True, |
479 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
480 | ) |
481 | |
482 | def cb(response_data): |
483 | @@ -177,7 +179,7 @@ class ProxmoxPowerDriver(WebhookPowerDriver): |
484 | "status/start", |
485 | ), |
486 | self._make_auth_headers(system_id, {}, extra_headers), |
487 | - context.get("power_verify_ssl") is True, |
488 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
489 | ) |
490 | |
491 | @asynchronous |
492 | @@ -194,7 +196,7 @@ class ProxmoxPowerDriver(WebhookPowerDriver): |
493 | "status/stop", |
494 | ), |
495 | self._make_auth_headers(system_id, {}, extra_headers), |
496 | - context.get("power_verify_ssl") is True, |
497 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
498 | ) |
499 | |
500 | @asynchronous |
501 | @@ -208,3 +210,111 @@ class ProxmoxPowerDriver(WebhookPowerDriver): |
502 | return "off" |
503 | else: |
504 | return "unknown" |
505 | + |
506 | + |
507 | +def probe_proxmox_and_enlist( |
508 | + user, |
509 | + hostname, |
510 | + username, |
511 | + password, |
512 | + token_name, |
513 | + token_secret, |
514 | + verify_ssl, |
515 | + accept_all, |
516 | + domain, |
517 | + prefix_filter, |
518 | +): |
519 | + """Extracts all of the VMs from Proxmox and enlists them into MAAS. |
520 | + |
521 | + :param user: user for the nodes. |
522 | + :param hostname: Hostname for Proxmox |
523 | + :param username: The username to connect to Proxmox to |
524 | + :param password: The password to connect to Proxmox with. |
525 | + :param token_name: The name of the token to use instead of a password. |
526 | + :param token_secret: The token secret to use instead of a password. |
527 | + :param verify_ssl: Whether SSL connections should be verified. |
528 | + :param accept_all: If True, commission enlisted nodes. |
529 | + :param domain: What domain discovered machines to be apart of. |
530 | + :param prefix_filter: only enlist nodes that have the prefix. |
531 | + """ |
532 | + proxmox = ProxmoxPowerDriver() |
533 | + context = { |
534 | + "power_address": hostname, |
535 | + "power_user": username, |
536 | + "power_pass": password, |
537 | + "power_token_name": token_name, |
538 | + "power_token_secret": token_secret, |
539 | + "power_verify_ssl": SSL_INSECURE_YES |
540 | + if verify_ssl |
541 | + else SSL_INSECURE_NO, |
542 | + } |
543 | + mac_regex = re.compile(r"(([\dA-F]{2}[:]){5}[\dA-F]{2})", re.I) |
544 | + |
545 | + d = proxmox._login("", context) |
546 | + |
547 | + @asynchronous |
548 | + @inlineCallbacks |
549 | + def get_vms(extra_headers): |
550 | + vms = yield proxmox._webhook_request( |
551 | + b"GET", |
552 | + proxmox._get_url(context, "cluster/resources", {"type": "vm"}), |
553 | + proxmox._make_auth_headers("", {}, extra_headers), |
554 | + verify_ssl, |
555 | + ) |
556 | + return extra_headers, vms |
557 | + |
558 | + d.addCallback(get_vms) |
559 | + |
560 | + @asynchronous |
561 | + @inlineCallbacks |
562 | + def process_vms(data): |
563 | + extra_headers, response_data = data |
564 | + for vm in json.loads(response_data)["data"]: |
565 | + if prefix_filter and not vm["name"].startswith(prefix_filter): |
566 | + continue |
567 | + # Proxmox doesn't have an easy way to get the MAC address, it |
568 | + # includes it with a bunch of other data in the config. |
569 | + vm_config_data = yield proxmox._webhook_request( |
570 | + b"GET", |
571 | + proxmox._get_url( |
572 | + context, |
573 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/config", |
574 | + ), |
575 | + proxmox._make_auth_headers("", {}, extra_headers), |
576 | + verify_ssl, |
577 | + ) |
578 | + macs = [ |
579 | + mac[0] for mac in mac_regex.findall(vm_config_data.decode()) |
580 | + ] |
581 | + |
582 | + system_id = yield create_node( |
583 | + macs, |
584 | + "amd64", |
585 | + "proxmox", |
586 | + {"power_vm_name": vm["vmid"], **context}, |
587 | + domain, |
588 | + hostname=vm["name"].replace(" ", "-"), |
589 | + ) |
590 | + |
591 | + # If the system_id is None an error occured when creating the machine. |
592 | + # Most likely the error is the node already exists. |
593 | + if system_id is None: |
594 | + continue |
595 | + |
596 | + if vm["status"] != "stopped": |
597 | + yield proxmox._webhook_request( |
598 | + b"POST", |
599 | + proxmox._get_url( |
600 | + context, |
601 | + f"nodes/{vm['node']}/{vm['type']}/{vm['vmid']}/" |
602 | + "status/stop", |
603 | + ), |
604 | + proxmox._make_auth_headers(system_id, {}, extra_headers), |
605 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
606 | + ) |
607 | + |
608 | + if accept_all: |
609 | + yield commission_node(system_id, user) |
610 | + |
611 | + d.addCallback(process_vms) |
612 | + return d |
613 | diff --git a/src/provisioningserver/drivers/power/tests/test_proxmox.py b/src/provisioningserver/drivers/power/tests/test_proxmox.py |
614 | index 881ac5f..6292eae 100644 |
615 | --- a/src/provisioningserver/drivers/power/tests/test_proxmox.py |
616 | +++ b/src/provisioningserver/drivers/power/tests/test_proxmox.py |
617 | @@ -4,16 +4,22 @@ |
618 | """Tests for `provisioningserver.drivers.power.proxmox`.""" |
619 | import json |
620 | import random |
621 | -from unittest.mock import ANY |
622 | +from unittest.mock import ANY, call |
623 | |
624 | from testtools import ExpectedException |
625 | from twisted.internet.defer import inlineCallbacks, succeed |
626 | |
627 | from maastesting.factory import factory |
628 | -from maastesting.matchers import MockCalledOnceWith, MockNotCalled |
629 | +from maastesting.matchers import ( |
630 | + MockCalledOnceWith, |
631 | + MockCalledWith, |
632 | + MockCallsMatch, |
633 | + MockNotCalled, |
634 | +) |
635 | from maastesting.testcase import MAASTestCase, MAASTwistedRunTest |
636 | from provisioningserver.drivers.power import PowerActionError |
637 | import provisioningserver.drivers.power.proxmox as proxmox_module |
638 | +from provisioningserver.drivers.power.webhook import SSL_INSECURE_NO |
639 | |
640 | |
641 | class TestProxmoxPowerDriver(MAASTestCase): |
642 | @@ -437,3 +443,376 @@ class TestProxmoxPowerDriver(MAASTestCase): |
643 | status = yield self.proxmox.power_query(system_id, context) |
644 | |
645 | self.assertEqual("unknown", status) |
646 | + |
647 | + |
648 | +class TestProxmoxProbeAndEnlist(MAASTestCase): |
649 | + |
650 | + run_tests_with = MAASTwistedRunTest.make_factory(timeout=5) |
651 | + |
652 | + def setUp(self): |
653 | + super().setUp() |
654 | + self.mock_login = self.patch( |
655 | + proxmox_module.ProxmoxPowerDriver, "_login" |
656 | + ) |
657 | + self.mock_login.return_value = succeed({}) |
658 | + self.system_id = factory.make_name("system_id") |
659 | + self.mock_create_node = self.patch(proxmox_module, "create_node") |
660 | + self.mock_create_node.return_value = succeed(self.system_id) |
661 | + self.mock_commission_node = self.patch( |
662 | + proxmox_module, "commission_node" |
663 | + ) |
664 | + self.mock_commission_node.return_value = succeed(None) |
665 | + |
666 | + @inlineCallbacks |
667 | + def test_probe_and_enlist(self): |
668 | + user = factory.make_name("user") |
669 | + hostname = factory.make_ipv4_address() |
670 | + username = factory.make_name("username") |
671 | + password = factory.make_name("password") |
672 | + token_name = factory.make_name("token_name") |
673 | + token_secret = factory.make_name("token_secret") |
674 | + domain = factory.make_name("domain") |
675 | + node1 = factory.make_name("node1") |
676 | + vmid1 = random.randint(0, 100) |
677 | + mac11 = factory.make_mac_address() |
678 | + mac12 = factory.make_mac_address() |
679 | + node2 = factory.make_name("node2") |
680 | + vmid2 = random.randint(0, 100) |
681 | + mac21 = factory.make_mac_address() |
682 | + mac22 = factory.make_mac_address() |
683 | + mock_webhook_request = self.patch( |
684 | + proxmox_module.ProxmoxPowerDriver, "_webhook_request" |
685 | + ) |
686 | + mock_webhook_request.side_effect = [ |
687 | + succeed( |
688 | + json.dumps( |
689 | + { |
690 | + "data": [ |
691 | + { |
692 | + "node": node1, |
693 | + "vmid": vmid1, |
694 | + "name": f"vm {vmid1}", |
695 | + "type": "qemu", |
696 | + "status": "stopped", |
697 | + }, |
698 | + { |
699 | + "node": node2, |
700 | + "vmid": vmid2, |
701 | + "name": f"vm {vmid2}", |
702 | + "type": "qemu", |
703 | + "status": "stopped", |
704 | + }, |
705 | + ] |
706 | + } |
707 | + ).encode() |
708 | + ), |
709 | + succeed( |
710 | + b"{'data': {" |
711 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
712 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
713 | + b"}}" % (mac11.encode(), mac12.encode()) |
714 | + ), |
715 | + succeed( |
716 | + b"{'data': {" |
717 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
718 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
719 | + b"}}" % (mac21.encode(), mac22.encode()) |
720 | + ), |
721 | + ] |
722 | + |
723 | + yield proxmox_module.probe_proxmox_and_enlist( |
724 | + user, |
725 | + hostname, |
726 | + username, |
727 | + password, |
728 | + token_name, |
729 | + token_secret, |
730 | + False, |
731 | + False, |
732 | + domain, |
733 | + None, |
734 | + ) |
735 | + |
736 | + self.assertThat( |
737 | + self.mock_create_node, |
738 | + MockCallsMatch( |
739 | + call( |
740 | + [mac11, mac12], |
741 | + "amd64", |
742 | + "proxmox", |
743 | + { |
744 | + "power_vm_name": vmid1, |
745 | + "power_address": hostname, |
746 | + "power_user": username, |
747 | + "power_pass": password, |
748 | + "power_token_name": token_name, |
749 | + "power_token_secret": token_secret, |
750 | + "power_verify_ssl": SSL_INSECURE_NO, |
751 | + }, |
752 | + domain, |
753 | + hostname=f"vm-{vmid1}", |
754 | + ), |
755 | + call( |
756 | + [mac21, mac22], |
757 | + "amd64", |
758 | + "proxmox", |
759 | + { |
760 | + "power_vm_name": vmid2, |
761 | + "power_address": hostname, |
762 | + "power_user": username, |
763 | + "power_pass": password, |
764 | + "power_token_name": token_name, |
765 | + "power_token_secret": token_secret, |
766 | + "power_verify_ssl": SSL_INSECURE_NO, |
767 | + }, |
768 | + domain, |
769 | + hostname=f"vm-{vmid2}", |
770 | + ), |
771 | + ), |
772 | + ) |
773 | + self.assertThat(self.mock_commission_node, MockNotCalled()) |
774 | + |
775 | + @inlineCallbacks |
776 | + def test_probe_and_enlist_filters(self): |
777 | + user = factory.make_name("user") |
778 | + hostname = factory.make_ipv4_address() |
779 | + username = factory.make_name("username") |
780 | + password = factory.make_name("password") |
781 | + token_name = factory.make_name("token_name") |
782 | + token_secret = factory.make_name("token_secret") |
783 | + domain = factory.make_name("domain") |
784 | + node1 = factory.make_name("node1") |
785 | + mac11 = factory.make_mac_address() |
786 | + mac12 = factory.make_mac_address() |
787 | + node2 = factory.make_name("node2") |
788 | + mac21 = factory.make_mac_address() |
789 | + mac22 = factory.make_mac_address() |
790 | + mock_webhook_request = self.patch( |
791 | + proxmox_module.ProxmoxPowerDriver, "_webhook_request" |
792 | + ) |
793 | + mock_webhook_request.side_effect = [ |
794 | + succeed( |
795 | + json.dumps( |
796 | + { |
797 | + "data": [ |
798 | + { |
799 | + "node": node1, |
800 | + "vmid": 100, |
801 | + "name": f"vm 100", |
802 | + "type": "qemu", |
803 | + "status": "stopped", |
804 | + }, |
805 | + { |
806 | + "node": node2, |
807 | + "vmid": 200, |
808 | + "name": f"vm 200", |
809 | + "type": "qemu", |
810 | + "status": "stopped", |
811 | + }, |
812 | + ] |
813 | + } |
814 | + ).encode() |
815 | + ), |
816 | + succeed( |
817 | + b"{'data': {" |
818 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
819 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
820 | + b"}}" % (mac11.encode(), mac12.encode()) |
821 | + ), |
822 | + succeed( |
823 | + b"{'data': {" |
824 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
825 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
826 | + b"}}" % (mac21.encode(), mac22.encode()) |
827 | + ), |
828 | + ] |
829 | + |
830 | + yield proxmox_module.probe_proxmox_and_enlist( |
831 | + user, |
832 | + hostname, |
833 | + username, |
834 | + password, |
835 | + token_name, |
836 | + token_secret, |
837 | + False, |
838 | + False, |
839 | + domain, |
840 | + "vm 1", |
841 | + ) |
842 | + |
843 | + self.assertThat( |
844 | + self.mock_create_node, |
845 | + MockCalledOnceWith( |
846 | + [mac11, mac12], |
847 | + "amd64", |
848 | + "proxmox", |
849 | + { |
850 | + "power_vm_name": 100, |
851 | + "power_address": hostname, |
852 | + "power_user": username, |
853 | + "power_pass": password, |
854 | + "power_token_name": token_name, |
855 | + "power_token_secret": token_secret, |
856 | + "power_verify_ssl": SSL_INSECURE_NO, |
857 | + }, |
858 | + domain, |
859 | + hostname=f"vm-100", |
860 | + ), |
861 | + ) |
862 | + self.assertThat(self.mock_commission_node, MockNotCalled()) |
863 | + |
864 | + @inlineCallbacks |
865 | + def test_probe_and_enlist_stops_and_commissions(self): |
866 | + user = factory.make_name("user") |
867 | + hostname = factory.make_ipv4_address() |
868 | + username = factory.make_name("username") |
869 | + password = factory.make_name("password") |
870 | + token_name = factory.make_name("token_name") |
871 | + token_secret = factory.make_name("token_secret") |
872 | + domain = factory.make_name("domain") |
873 | + node1 = factory.make_name("node1") |
874 | + vmid1 = random.randint(0, 100) |
875 | + mac11 = factory.make_mac_address() |
876 | + mac12 = factory.make_mac_address() |
877 | + mock_webhook_request = self.patch( |
878 | + proxmox_module.ProxmoxPowerDriver, "_webhook_request" |
879 | + ) |
880 | + mock_webhook_request.side_effect = [ |
881 | + succeed( |
882 | + json.dumps( |
883 | + { |
884 | + "data": [ |
885 | + { |
886 | + "node": node1, |
887 | + "vmid": vmid1, |
888 | + "name": f"vm {vmid1}", |
889 | + "type": "qemu", |
890 | + "status": "running", |
891 | + }, |
892 | + ] |
893 | + } |
894 | + ).encode() |
895 | + ), |
896 | + succeed( |
897 | + b"{'data': {" |
898 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
899 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
900 | + b"}}" % (mac11.encode(), mac12.encode()) |
901 | + ), |
902 | + succeed(None), |
903 | + ] |
904 | + |
905 | + yield proxmox_module.probe_proxmox_and_enlist( |
906 | + user, |
907 | + hostname, |
908 | + username, |
909 | + password, |
910 | + token_name, |
911 | + token_secret, |
912 | + False, |
913 | + True, |
914 | + domain, |
915 | + None, |
916 | + ) |
917 | + |
918 | + self.assertThat( |
919 | + self.mock_create_node, |
920 | + MockCalledOnceWith( |
921 | + [mac11, mac12], |
922 | + "amd64", |
923 | + "proxmox", |
924 | + { |
925 | + "power_vm_name": vmid1, |
926 | + "power_address": hostname, |
927 | + "power_user": username, |
928 | + "power_pass": password, |
929 | + "power_token_name": token_name, |
930 | + "power_token_secret": token_secret, |
931 | + "power_verify_ssl": SSL_INSECURE_NO, |
932 | + }, |
933 | + domain, |
934 | + hostname=f"vm-{vmid1}", |
935 | + ), |
936 | + ) |
937 | + self.assertThat( |
938 | + mock_webhook_request, MockCalledWith(b"POST", ANY, ANY, False) |
939 | + ) |
940 | + self.assertThat( |
941 | + self.mock_commission_node, MockCalledOnceWith(self.system_id, user) |
942 | + ) |
943 | + |
944 | + @inlineCallbacks |
945 | + def test_probe_and_enlist_ignores_create_node_error(self): |
946 | + user = factory.make_name("user") |
947 | + hostname = factory.make_ipv4_address() |
948 | + username = factory.make_name("username") |
949 | + password = factory.make_name("password") |
950 | + token_name = factory.make_name("token_name") |
951 | + token_secret = factory.make_name("token_secret") |
952 | + domain = factory.make_name("domain") |
953 | + node1 = factory.make_name("node1") |
954 | + vmid1 = random.randint(0, 100) |
955 | + mac11 = factory.make_mac_address() |
956 | + mac12 = factory.make_mac_address() |
957 | + self.mock_create_node.return_value = succeed(None) |
958 | + mock_webhook_request = self.patch( |
959 | + proxmox_module.ProxmoxPowerDriver, "_webhook_request" |
960 | + ) |
961 | + mock_webhook_request.side_effect = [ |
962 | + succeed( |
963 | + json.dumps( |
964 | + { |
965 | + "data": [ |
966 | + { |
967 | + "node": node1, |
968 | + "vmid": vmid1, |
969 | + "name": f"vm {vmid1}", |
970 | + "type": "qemu", |
971 | + "status": "running", |
972 | + }, |
973 | + ] |
974 | + } |
975 | + ).encode() |
976 | + ), |
977 | + succeed( |
978 | + b"{'data': {" |
979 | + b"'net1':'virtio=%s,bridge=vmbr0,firewall=1'" |
980 | + b"'net2':'virtio=%s,bridge=vmbr0,firewall=1'" |
981 | + b"}}" % (mac11.encode(), mac12.encode()) |
982 | + ), |
983 | + succeed(None), |
984 | + ] |
985 | + |
986 | + yield proxmox_module.probe_proxmox_and_enlist( |
987 | + user, |
988 | + hostname, |
989 | + username, |
990 | + password, |
991 | + token_name, |
992 | + token_secret, |
993 | + False, |
994 | + True, |
995 | + domain, |
996 | + None, |
997 | + ) |
998 | + |
999 | + self.assertThat( |
1000 | + self.mock_create_node, |
1001 | + MockCalledOnceWith( |
1002 | + [mac11, mac12], |
1003 | + "amd64", |
1004 | + "proxmox", |
1005 | + { |
1006 | + "power_vm_name": vmid1, |
1007 | + "power_address": hostname, |
1008 | + "power_user": username, |
1009 | + "power_pass": password, |
1010 | + "power_token_name": token_name, |
1011 | + "power_token_secret": token_secret, |
1012 | + "power_verify_ssl": SSL_INSECURE_NO, |
1013 | + }, |
1014 | + domain, |
1015 | + hostname=f"vm-{vmid1}", |
1016 | + ), |
1017 | + ) |
1018 | + self.assertThat(self.mock_commission_node, MockNotCalled()) |
1019 | diff --git a/src/provisioningserver/drivers/power/webhook.py b/src/provisioningserver/drivers/power/webhook.py |
1020 | index 120e32d..589a04d 100644 |
1021 | --- a/src/provisioningserver/drivers/power/webhook.py |
1022 | +++ b/src/provisioningserver/drivers/power/webhook.py |
1023 | @@ -180,7 +180,7 @@ class WebhookPowerDriver(PowerDriver): |
1024 | b"POST", |
1025 | context["power_on_uri"].encode(), |
1026 | self._make_auth_headers(system_id, context), |
1027 | - context.get("power_verify_ssl") is True, |
1028 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
1029 | ) |
1030 | |
1031 | @asynchronous |
1032 | @@ -191,7 +191,7 @@ class WebhookPowerDriver(PowerDriver): |
1033 | b"POST", |
1034 | context["power_off_uri"].encode(), |
1035 | self._make_auth_headers(system_id, context), |
1036 | - context.get("power_verify_ssl") is True, |
1037 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
1038 | ) |
1039 | |
1040 | @asynchronous |
1041 | @@ -205,7 +205,7 @@ class WebhookPowerDriver(PowerDriver): |
1042 | b"GET", |
1043 | context["power_query_uri"].encode(), |
1044 | self._make_auth_headers(system_id, context), |
1045 | - context.get("power_verify_ssl") is True, |
1046 | + context.get("power_verify_ssl") == SSL_INSECURE_YES, |
1047 | ) |
1048 | node_data = node_data.decode() |
1049 | if power_on_regex and re.search(power_on_regex, node_data) is not None: |
1050 | diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py |
1051 | index d73e9cf..44fb7be 100644 |
1052 | --- a/src/provisioningserver/rpc/cluster.py |
1053 | +++ b/src/provisioningserver/rpc/cluster.py |
1054 | @@ -1,4 +1,4 @@ |
1055 | -# Copyright 2014-2020 Canonical Ltd. This software is licensed under the |
1056 | +# Copyright 2014-2021 Canonical Ltd. This software is licensed under the |
1057 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1058 | |
1059 | """RPC declarations for clusters. |
1060 | @@ -742,6 +742,9 @@ class AddChassis(amp.Command): |
1061 | (b"power_control", amp.Unicode(optional=True)), |
1062 | (b"port", amp.Integer(optional=True)), |
1063 | (b"protocol", amp.Unicode(optional=True)), |
1064 | + (b"token_name", amp.Unicode(optional=True)), |
1065 | + (b"token_secret", amp.Unicode(optional=True)), |
1066 | + (b"verify_ssl", amp.Boolean(optional=True)), |
1067 | ] |
1068 | errors = {} |
1069 | |
1070 | diff --git a/src/provisioningserver/rpc/clusterservice.py b/src/provisioningserver/rpc/clusterservice.py |
1071 | index 6d48f98..dca3b46 100644 |
1072 | --- a/src/provisioningserver/rpc/clusterservice.py |
1073 | +++ b/src/provisioningserver/rpc/clusterservice.py |
1074 | @@ -1,4 +1,4 @@ |
1075 | -# Copyright 2014-2020 Canonical Ltd. This software is licensed under the |
1076 | +# Copyright 2014-2021 Canonical Ltd. This software is licensed under the |
1077 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1078 | |
1079 | """RPC implementation for clusters.""" |
1080 | @@ -49,6 +49,7 @@ from provisioningserver.drivers.hardware.vmware import probe_vmware_and_enlist |
1081 | from provisioningserver.drivers.nos.registry import NOSDriverRegistry |
1082 | from provisioningserver.drivers.power.mscm import probe_and_enlist_mscm |
1083 | from provisioningserver.drivers.power.msftocs import probe_and_enlist_msftocs |
1084 | +from provisioningserver.drivers.power.proxmox import probe_proxmox_and_enlist |
1085 | from provisioningserver.drivers.power.recs import probe_and_enlist_recs |
1086 | from provisioningserver.drivers.power.registry import PowerDriverRegistry |
1087 | from provisioningserver.logger import get_maas_logger, LegacyLogger |
1088 | @@ -787,6 +788,9 @@ class Cluster(RPCProtocol): |
1089 | power_control=None, |
1090 | port=None, |
1091 | protocol=None, |
1092 | + token_name=None, |
1093 | + token_secret=None, |
1094 | + verify_ssl=False, |
1095 | ): |
1096 | """AddChassis() |
1097 | |
1098 | @@ -804,6 +808,21 @@ class Cluster(RPCProtocol): |
1099 | domain, |
1100 | ) |
1101 | d.addErrback(partial(catch_probe_and_enlist_error, "virsh")) |
1102 | + elif chassis_type == "proxmox": |
1103 | + d = deferToThread( |
1104 | + probe_proxmox_and_enlist, |
1105 | + user, |
1106 | + hostname, |
1107 | + username, |
1108 | + password, |
1109 | + token_name, |
1110 | + token_secret, |
1111 | + verify_ssl, |
1112 | + accept_all, |
1113 | + domain, |
1114 | + prefix_filter, |
1115 | + ) |
1116 | + d.addErrback(partial(catch_probe_and_enlist_error, "proxmox")) |
1117 | elif chassis_type == "vmware": |
1118 | d = deferToThread( |
1119 | probe_vmware_and_enlist, |
1120 | diff --git a/src/provisioningserver/rpc/tests/test_clusterservice.py b/src/provisioningserver/rpc/tests/test_clusterservice.py |
1121 | index 5d459b8..431b3f2 100644 |
1122 | --- a/src/provisioningserver/rpc/tests/test_clusterservice.py |
1123 | +++ b/src/provisioningserver/rpc/tests/test_clusterservice.py |
1124 | @@ -1,4 +1,4 @@ |
1125 | -# Copyright 2014-2020 Canonical Ltd. This software is licensed under the |
1126 | +# Copyright 2014-2021 Canonical Ltd. This software is licensed under the |
1127 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1128 | |
1129 | """Tests for the cluster's RPC implementation.""" |
1130 | @@ -3487,6 +3487,97 @@ class TestClusterProtocol_AddChassis(MAASTestCase): |
1131 | ), |
1132 | ) |
1133 | |
1134 | + def test_chassis_type_proxmox_calls_probe_proxmoxand_enlist(self): |
1135 | + mock_deferToThread = self.patch_autospec( |
1136 | + clusterservice, "deferToThread" |
1137 | + ) |
1138 | + user = factory.make_name("user") |
1139 | + hostname = factory.make_hostname() |
1140 | + username = factory.make_name("username") |
1141 | + password = factory.make_name("password") |
1142 | + token_name = factory.make_name("token_name") |
1143 | + token_secret = factory.make_name("token_secret") |
1144 | + verify_ssl = factory.pick_bool() |
1145 | + accept_all = factory.pick_bool() |
1146 | + domain = factory.make_name("domain") |
1147 | + prefix_filter = factory.make_name("prefix_filter") |
1148 | + call_responder( |
1149 | + Cluster(), |
1150 | + cluster.AddChassis, |
1151 | + { |
1152 | + "user": user, |
1153 | + "chassis_type": "proxmox", |
1154 | + "hostname": hostname, |
1155 | + "username": username, |
1156 | + "password": password, |
1157 | + "token_name": token_name, |
1158 | + "token_secret": token_secret, |
1159 | + "verify_ssl": verify_ssl, |
1160 | + "accept_all": accept_all, |
1161 | + "domain": domain, |
1162 | + "prefix_filter": prefix_filter, |
1163 | + }, |
1164 | + ) |
1165 | + self.assertThat( |
1166 | + mock_deferToThread, |
1167 | + MockCalledOnceWith( |
1168 | + clusterservice.probe_proxmox_and_enlist, |
1169 | + user, |
1170 | + hostname, |
1171 | + username, |
1172 | + password, |
1173 | + token_name, |
1174 | + token_secret, |
1175 | + verify_ssl, |
1176 | + accept_all, |
1177 | + domain, |
1178 | + prefix_filter, |
1179 | + ), |
1180 | + ) |
1181 | + |
1182 | + def test_chassis_type_proxmox_logs_error_to_maaslog(self): |
1183 | + fake_error = factory.make_name("error") |
1184 | + self.patch(clusterservice, "maaslog") |
1185 | + mock_deferToThread = self.patch_autospec( |
1186 | + clusterservice, "deferToThread" |
1187 | + ) |
1188 | + mock_deferToThread.return_value = fail(Exception(fake_error)) |
1189 | + user = factory.make_name("user") |
1190 | + hostname = factory.make_hostname() |
1191 | + username = factory.make_name("username") |
1192 | + password = factory.make_name("password") |
1193 | + token_name = factory.make_name("token_name") |
1194 | + token_secret = factory.make_name("token_secret") |
1195 | + verify_ssl = factory.pick_bool() |
1196 | + accept_all = factory.pick_bool() |
1197 | + domain = factory.make_name("domain") |
1198 | + prefix_filter = factory.make_name("prefix_filter") |
1199 | + call_responder( |
1200 | + Cluster(), |
1201 | + cluster.AddChassis, |
1202 | + { |
1203 | + "user": user, |
1204 | + "chassis_type": "proxmox", |
1205 | + "hostname": hostname, |
1206 | + "username": username, |
1207 | + "password": password, |
1208 | + "token_name": token_name, |
1209 | + "token_secret": token_secret, |
1210 | + "verify_ssl": verify_ssl, |
1211 | + "accept_all": accept_all, |
1212 | + "domain": domain, |
1213 | + "prefix_filter": prefix_filter, |
1214 | + }, |
1215 | + ) |
1216 | + self.assertThat( |
1217 | + clusterservice.maaslog.error, |
1218 | + MockAnyCall( |
1219 | + "Failed to probe and enlist %s nodes: %s", |
1220 | + "proxmox", |
1221 | + fake_error, |
1222 | + ), |
1223 | + ) |
1224 | + |
1225 | def test_chassis_type_vmware_calls_probe_vmware_and_enlist(self): |
1226 | mock_deferToThread = self.patch_autospec( |
1227 | clusterservice, "deferToThread" |
UNIT TESTS
-b lp1916093 lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS c8e337d15ce3700 588255b3f2
COMMIT: 8c45838d789c90a