Merge ~ltrager/maas:lp1916093_2.9 into maas:2.9

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 42b9a3de4c33fa73bd0108f8bfb02ea341b47620
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1916093_2.9
Merge into: maas:2.9
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)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+399315@code.launchpad.net

Commit message

LP: #1916093 - Fix adding more than one Proxmox BMC and add chassis support

Backport of 0125208

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
2index e84adac..ebae0f2 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@@ -2575,6 +2575,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@@ -2604,18 +2605,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@@ -2655,9 +2669,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@@ -2673,12 +2709,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@@ -2730,6 +2767,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@@ -2786,6 +2827,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(
121diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py
122index 0736ffe..7eba13f 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
353diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
354index 04245e9..1d5b74f 100644
355--- a/src/maasserver/models/node.py
356+++ b/src/maasserver/models/node.py
357@@ -6982,6 +6982,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@@ -7002,6 +7005,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
377diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
378index 560df0a..375ab21 100644
379--- a/src/maasserver/models/tests/test_node.py
380+++ b/src/maasserver/models/tests/test_node.py
381@@ -13677,6 +13677,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@@ -13690,6 +13693,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@@ -13707,6 +13713,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@@ -13730,6 +13739,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@@ -13744,6 +13756,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(
431diff --git a/src/provisioningserver/drivers/power/proxmox.py b/src/provisioningserver/drivers/power/proxmox.py
432index 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
613diff --git a/src/provisioningserver/drivers/power/tests/test_proxmox.py b/src/provisioningserver/drivers/power/tests/test_proxmox.py
614index 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())
1019diff --git a/src/provisioningserver/drivers/power/webhook.py b/src/provisioningserver/drivers/power/webhook.py
1020index 2deccac..22c3d04 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:
1050diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py
1051index 4b67c45..b2def33 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@@ -745,6 +745,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
1070diff --git a/src/provisioningserver/rpc/clusterservice.py b/src/provisioningserver/rpc/clusterservice.py
1071index bc8f429..d8b269d 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@@ -785,6 +786,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@@ -802,6 +806,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,
1120diff --git a/src/provisioningserver/rpc/tests/test_clusterservice.py b/src/provisioningserver/rpc/tests/test_clusterservice.py
1121index 647ed6d..4485515 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@@ -3486,6 +3486,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"

Subscribers

People subscribed via source and target branches