Merge ~cgrabowski/maas:LXD_create_cluster into maas:master

Proposed by Christian Grabowski
Status: Merged
Approved by: Christian Grabowski
Approved revision: 237545542a6c49b6caa2274de3b1bb3c5928ff1f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~cgrabowski/maas:LXD_create_cluster
Merge into: maas:master
Diff against target: 976 lines (+621/-54)
13 files modified
src/maasserver/clusterrpc/pods.py (+24/-1)
src/maasserver/clusterrpc/tests/test_pods.py (+50/-0)
src/maasserver/models/bmc.py (+7/-4)
src/maasserver/models/tests/test_bmc.py (+14/-0)
src/maasserver/testing/factory.py (+12/-0)
src/maasserver/tests/test_vmhost.py (+193/-0)
src/maasserver/vmhost.py (+115/-16)
src/provisioningserver/drivers/pod/__init__.py (+15/-0)
src/provisioningserver/drivers/pod/lxd.py (+25/-1)
src/provisioningserver/drivers/pod/tests/test_lxd.py (+130/-29)
src/provisioningserver/rpc/cluster.py (+11/-2)
src/provisioningserver/rpc/pods.py (+3/-0)
src/provisioningserver/rpc/tests/test_pods.py (+22/-1)
Reviewer Review Type Date Requested Status
Alexsander de Souza Approve
MAAS Lander Approve
Review via email: mp+409324@code.launchpad.net

Commit message

correctly fetch commissioning info

cluster and pod naming tweaks

save discovered clusters and their containing pods

WIP save clusters

read discovered clusters from the region controller

return cluster on discover_pod

add discover cluster tests

discover all pods in a cluster

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b LXD_create_cluster lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/11115/console
COMMIT: de2476ab55b3a053b7207a691c8eaa4c62f7c631

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b LXD_create_cluster lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 237545542a6c49b6caa2274de3b1bb3c5928ff1f

review: Approve
Revision history for this message
Alexsander de Souza (alexsander-souza) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/clusterrpc/pods.py b/src/maasserver/clusterrpc/pods.py
2index 68c37c0..f925fcb 100644
3--- a/src/maasserver/clusterrpc/pods.py
4+++ b/src/maasserver/clusterrpc/pods.py
5@@ -69,7 +69,9 @@ def discover_pod(pod_type, context, pod_id=None, name=None, timeout=120):
6
7 clients = getAllClients()
8 dl = DeferredList(map(discover, clients), consumeErrors=True)
9- return dl.addCallback(_collect_results_and_failures, clients, "pod")
10+ return dl.addCallback(
11+ _collect_xor_results_and_failures, clients, "cluster", "pod"
12+ )
13
14
15 def get_best_discovered_result(discovered):
16@@ -253,3 +255,24 @@ def _collect_results_and_failures(results, clients, result_key):
17 else:
18 failures[client.ident] = result.value
19 return discovered, failures
20+
21+
22+def _collect_xor_results_and_failures(
23+ results, clients, result_key_a, result_key_b
24+):
25+ discovered, failures = {}, {}
26+ for client, (success, result) in zip(clients, results):
27+ if success:
28+ if result_key_a in result:
29+ discovered[client.ident] = result[result_key_a]
30+ elif result_key_b in result:
31+ discovered[client.ident] = result[result_key_b]
32+ else:
33+ raise ValueError(
34+ "neither %s or %s where found in result",
35+ result_key_a,
36+ result_key_b,
37+ )
38+ else:
39+ failures[client.ident] = result.value
40+ return discovered, failures
41diff --git a/src/maasserver/clusterrpc/tests/test_pods.py b/src/maasserver/clusterrpc/tests/test_pods.py
42index da49d55..fcb2ba6 100644
43--- a/src/maasserver/clusterrpc/tests/test_pods.py
44+++ b/src/maasserver/clusterrpc/tests/test_pods.py
45@@ -37,6 +37,7 @@ from maastesting.matchers import MockCalledOnceWith
46 from maastesting.testcase import MAASTestCase
47 from metadataserver.models import NodeKey
48 from provisioningserver.drivers.pod import (
49+ DiscoveredCluster,
50 DiscoveredPod,
51 DiscoveredPodHints,
52 DiscoveredPodProject,
53@@ -216,6 +217,55 @@ class TestDiscoverPod(MAASTransactionServerTestCase):
54 discovered[1], MatchesDict({rack_id: IsInstance(CancelledError)})
55 )
56
57+ @wait_for_reactor
58+ @inlineCallbacks
59+ def test_discovers_cluster(self):
60+ rack_id = factory.make_name("system_id")
61+ client = Mock()
62+ client.ident = rack_id
63+ pod_type = factory.make_name("pod")
64+ cluster = DiscoveredCluster(
65+ name=factory.make_name("cluster"),
66+ project=factory.make_name("project"),
67+ pods=[
68+ DiscoveredPod(
69+ architectures=["amd64/generic"],
70+ cores=random.randint(1, 8),
71+ cpu_speed=random.randint(1000, 3000),
72+ memory=random.randint(1024, 4096),
73+ local_storage=random.randint(500, 1000),
74+ hints=DiscoveredPodHints(
75+ cores=random.randint(1, 8),
76+ cpu_speed=random.randint(1000, 3000),
77+ memory=random.randint(1024, 4096),
78+ local_storage=random.randint(500, 1000),
79+ ),
80+ ),
81+ DiscoveredPod(
82+ architectures=["amd64/generic"],
83+ cores=random.randint(1, 8),
84+ cpu_speed=random.randint(1000, 3000),
85+ memory=random.randint(1024, 4096),
86+ local_storage=random.randint(500, 1000),
87+ hints=DiscoveredPodHints(
88+ cores=random.randint(1, 8),
89+ cpu_speed=random.randint(1000, 3000),
90+ memory=random.randint(1024, 4096),
91+ local_storage=random.randint(500, 1000),
92+ ),
93+ ),
94+ ],
95+ )
96+ clients = []
97+ client.return_value = succeed({"cluster": cluster})
98+ clients.append(client)
99+
100+ self.patch(pods_module, "getAllClients").return_value = clients
101+
102+ discovered = yield discover_pod(pod_type, {})
103+
104+ self.assertEqual(({rack_id: cluster}, {}), discovered)
105+
106
107 class TestGetBestDiscoveredResult(MAASTestCase):
108 def test_returns_one_of_the_discovered(self):
109diff --git a/src/maasserver/models/bmc.py b/src/maasserver/models/bmc.py
110index be9ba2a..3909ed5 100644
111--- a/src/maasserver/models/bmc.py
112+++ b/src/maasserver/models/bmc.py
113@@ -665,8 +665,9 @@ class Pod(BMC):
114 else:
115 return None
116
117- def sync_hints(self, discovered_hints):
118+ def sync_hints(self, discovered_hints, cluster=None):
119 """Sync the hints with `discovered_hints`."""
120+
121 try:
122 hints = self.hints
123 except PodHints.DoesNotExist:
124@@ -679,6 +680,8 @@ class Pod(BMC):
125 hints.memory = discovered_hints.memory
126 if discovered_hints.local_storage != -1:
127 hints.local_storage = discovered_hints.local_storage
128+ if cluster is not None:
129+ hints.cluster = cluster
130 hints.save()
131
132 def add_tag(self, tag):
133@@ -1478,7 +1481,7 @@ class Pod(BMC):
134 % (self.name, pool.name)
135 )
136
137- def sync(self, discovered_pod, commissioning_user):
138+ def sync(self, discovered_pod, commissioning_user, cluster=None):
139 """Sync the pod and machines from the `discovered_pod`.
140
141 This method ensures consistency with what is discovered by a pod
142@@ -1497,7 +1500,7 @@ class Pod(BMC):
143 self.power_parameters = power_params
144 self.version = discovered_pod.version
145 self.architectures = discovered_pod.architectures
146- if not self.name and discovered_pod.name:
147+ if not self.name or cluster is not None and discovered_pod.name:
148 self.name = discovered_pod.name
149 self.capabilities = discovered_pod.capabilities
150 if discovered_pod.cores != -1:
151@@ -1510,7 +1513,7 @@ class Pod(BMC):
152 self.local_storage = discovered_pod.local_storage
153 self.tags = list(set(self.tags).union(discovered_pod.tags))
154 self.save()
155- self.sync_hints(discovered_pod.hints)
156+ self.sync_hints(discovered_pod.hints, cluster=cluster)
157 self.sync_storage_pools(discovered_pod.storage_pools)
158 self.sync_machines(discovered_pod.machines, commissioning_user)
159 if discovered_pod.mac_addresses:
160diff --git a/src/maasserver/models/tests/test_bmc.py b/src/maasserver/models/tests/test_bmc.py
161index b12b0f5..460146b 100644
162--- a/src/maasserver/models/tests/test_bmc.py
163+++ b/src/maasserver/models/tests/test_bmc.py
164@@ -1250,6 +1250,20 @@ class TestPod(MAASServerTestCase, PodTestMixin):
165 self.assertNotEqual(-1, pod.hints.memory)
166 self.assertNotEqual(-1, pod.hints.local_storage)
167
168+ def test_sync_pod_with_cluster_saves_cluster_hint(self):
169+ pod = factory.make_Pod()
170+ cluster = factory.make_VMCluster()
171+
172+ discovered_pod = self.make_discovered_pod(
173+ machines=[], storage_pools=[]
174+ )
175+ user = factory.make_User()
176+ pod.sync(discovered_pod, user, cluster=cluster)
177+
178+ self.assertIsNotNone(pod.hints.cluster)
179+ self.assertEqual(pod.hints.cluster.name, cluster.name)
180+ self.assertEqual(pod.hints.cluster.project, cluster.project)
181+
182 def test_create_machine_ensures_unique_hostname(self):
183 existing_machine = factory.make_Node()
184 discovered_machine = self.make_discovered_machine()
185diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
186index c6cc75a..083d7d3 100644
187--- a/src/maasserver/testing/factory.py
188+++ b/src/maasserver/testing/factory.py
189@@ -102,6 +102,7 @@ from maasserver.models import (
190 VersionedTextFile,
191 VirtualBlockDevice,
192 VLAN,
193+ VMCluster,
194 VolumeGroup,
195 Zone,
196 )
197@@ -2958,6 +2959,17 @@ class Factory(maastesting.factory.Factory):
198 *args, group_type=FILESYSTEM_GROUP_TYPE.VMFS6, **kwargs
199 )
200
201+ def make_VMCluster(self, name=None, project=None):
202+ if name is None:
203+ name = factory.make_name("name")
204+ if project is None:
205+ project = factory.make_name("project")
206+
207+ return VMCluster.objects.create(
208+ name=name,
209+ project=project,
210+ )
211+
212 def make_VirtualBlockDevice(
213 self,
214 name=None,
215diff --git a/src/maasserver/tests/test_vmhost.py b/src/maasserver/tests/test_vmhost.py
216index 7217ac6..8198315 100644
217--- a/src/maasserver/tests/test_vmhost.py
218+++ b/src/maasserver/tests/test_vmhost.py
219@@ -9,6 +9,7 @@ from twisted.internet.defer import succeed
220 from maasserver import vmhost as vmhost_module
221 from maasserver.enum import BMC_TYPE
222 from maasserver.exceptions import PodProblem
223+from maasserver.models import PodHints
224 from maasserver.testing.factory import factory
225 from maasserver.testing.testcase import (
226 MAASServerTestCase,
227@@ -17,6 +18,7 @@ from maasserver.testing.testcase import (
228 from maasserver.utils.threads import deferToDatabase
229 from maastesting.crochet import wait_for
230 from provisioningserver.drivers.pod import (
231+ DiscoveredCluster,
232 DiscoveredPod,
233 DiscoveredPodHints,
234 DiscoveredPodStoragePool,
235@@ -75,6 +77,58 @@ def fake_pod_discovery(testcase):
236 )
237
238
239+def fake_cluster_discovery(testcase):
240+ discovered_cluster = DiscoveredCluster(
241+ name=factory.make_name("cluster"),
242+ project=factory.make_name("project"),
243+ pods=[
244+ DiscoveredPod(
245+ name=factory.make_name("pod"),
246+ architectures=["amd64/generic"],
247+ cores=random.randint(2, 4),
248+ memory=random.randint(2048, 4096),
249+ local_storage=random.randint(1024, 1024 * 1024),
250+ cpu_speed=random.randint(2048, 4048),
251+ hints=DiscoveredPodHints(
252+ cores=random.randint(2, 4),
253+ memory=random.randint(1024, 4096),
254+ local_storage=random.randint(1024, 1024 * 1024),
255+ cpu_speed=random.randint(2048, 4048),
256+ ),
257+ storage_pools=[
258+ DiscoveredPodStoragePool(
259+ id=factory.make_name("pool_id"),
260+ name=factory.make_name("name"),
261+ type=factory.make_name("type"),
262+ path="/var/lib/path/%s" % factory.make_name("path"),
263+ storage=random.randint(1024, 1024 * 1024),
264+ )
265+ for _ in range(3)
266+ ],
267+ clustered=True,
268+ )
269+ for _ in range(3)
270+ ],
271+ pod_addresses=["https://lxd-%d" % i for i in range(3)],
272+ )
273+ discovered_rack_1 = factory.make_RackController()
274+ discovered_rack_2 = factory.make_RackController()
275+ failed_rack = factory.make_RackController()
276+ testcase.patch(vmhost_module, "post_commit_do")
277+ testcase.patch(vmhost_module, "discover_pod").return_value = (
278+ {
279+ discovered_rack_1.system_id: discovered_cluster.pods[0],
280+ discovered_rack_2.system_id: discovered_cluster.pods[0],
281+ },
282+ {failed_rack.system_id: factory.make_exception()},
283+ )
284+ return (
285+ discovered_cluster,
286+ [discovered_rack_1, discovered_rack_2],
287+ [failed_rack],
288+ )
289+
290+
291 class TestDiscoverAndSyncVMHost(MAASServerTestCase):
292 def test_sync_details(self):
293 (
294@@ -212,3 +266,142 @@ class TestDiscoverAndSyncVMHostAsync(MAASTransactionServerTestCase):
295 self.assertEqual(str(exc), str(error))
296 else:
297 self.fail("No exception raised")
298+
299+
300+class TestSyncVMCluster(MAASServerTestCase):
301+ def test_sync_vmcluster_creates_cluster(self):
302+ (
303+ discovered_cluster,
304+ discovered_racks,
305+ failed_racks,
306+ ) = fake_cluster_discovery(self)
307+ zone = factory.make_Zone()
308+ pod_info = make_pod_info()
309+ power_parameters = {"power_address": pod_info["power_address"]}
310+ orig_vmhost = factory.make_Pod(
311+ zone=zone, pod_type=pod_info["type"], parameters=power_parameters
312+ )
313+ successes = {
314+ rack_id: discovered_cluster for rack_id in discovered_racks
315+ }
316+ failures = {
317+ rack_id: factory.make_exception() for rack_id in failed_racks
318+ }
319+ self.patch(vmhost_module, "discover_pod").return_value = (
320+ successes,
321+ failures,
322+ )
323+ vmhost = vmhost_module.discover_and_sync_vmhost(
324+ orig_vmhost, factory.make_User()
325+ )
326+ self.assertEqual(vmhost.hints.cluster.name, discovered_cluster.name)
327+ self.assertEqual(
328+ vmhost.hints.cluster.project, discovered_cluster.project
329+ )
330+
331+ def test_sync_vmcluster_creates_additional_pods(self):
332+ (
333+ discovered_cluster,
334+ discovered_racks,
335+ failed_racks,
336+ ) = fake_cluster_discovery(self)
337+ zone = factory.make_Zone()
338+ pod_info = make_pod_info()
339+ power_parameters = {"power_address": pod_info["power_address"]}
340+ orig_vmhost = factory.make_Pod(
341+ zone=zone, pod_type=pod_info["type"], parameters=power_parameters
342+ )
343+ successes = {
344+ rack_id: discovered_cluster for rack_id in discovered_racks
345+ }
346+ failures = {
347+ rack_id: factory.make_exception() for rack_id in failed_racks
348+ }
349+ self.patch(vmhost_module, "discover_pod").return_value = (
350+ successes,
351+ failures,
352+ )
353+ vmhost = vmhost_module.discover_and_sync_vmhost(
354+ orig_vmhost, factory.make_User()
355+ )
356+ hints = PodHints.objects.filter(cluster=vmhost.hints.cluster)
357+ pod_names = [hint.pod.name for hint in hints]
358+ expected_names = [pod.name for pod in discovered_cluster.pods]
359+ self.assertCountEqual(pod_names, expected_names)
360+
361+
362+class TestSyncVMClusterAsync(MAASTransactionServerTestCase):
363+
364+ wait_for_reactor = wait_for(30)
365+
366+ @wait_for_reactor
367+ async def test_sync_vmcluster_async_creates_cluster(self):
368+ (
369+ discovered_cluster,
370+ discovered_racks,
371+ failed_racks,
372+ ) = await deferToDatabase(fake_cluster_discovery, self)
373+ successes = {
374+ rack_id: discovered_cluster for rack_id in discovered_racks
375+ }
376+ failures = {
377+ rack_id: factory.make_exception() for rack_id in failed_racks
378+ }
379+ vmhost_module.discover_pod.return_value = succeed(
380+ (successes, failures)
381+ )
382+ zone = await deferToDatabase(factory.make_Zone)
383+ pod_info = await deferToDatabase(make_pod_info)
384+ power_parameters = {"power_address": pod_info["power_address"]}
385+ orig_vmhost = await deferToDatabase(
386+ factory.make_Pod,
387+ zone=zone,
388+ pod_type=pod_info["type"],
389+ parameters=power_parameters,
390+ )
391+ user = await deferToDatabase(factory.make_User)
392+ vmhost = await vmhost_module.discover_and_sync_vmhost_async(
393+ orig_vmhost, user
394+ )
395+ self.assertEqual(vmhost.hints.cluster.name, discovered_cluster.name)
396+ self.assertEqual(
397+ vmhost.hints.cluster.project, discovered_cluster.project
398+ )
399+
400+ @wait_for_reactor
401+ async def test_sync_vmcluster_async_creates_additional(self):
402+ (
403+ discovered_cluster,
404+ discovered_racks,
405+ failed_racks,
406+ ) = await deferToDatabase(fake_cluster_discovery, self)
407+ successes = {
408+ rack_id: discovered_cluster for rack_id in discovered_racks
409+ }
410+ failures = {
411+ rack_id: factory.make_exception() for rack_id in failed_racks
412+ }
413+ vmhost_module.discover_pod.return_value = succeed(
414+ (successes, failures)
415+ )
416+ zone = await deferToDatabase(factory.make_Zone)
417+ pod_info = await deferToDatabase(make_pod_info)
418+ power_parameters = {"power_address": pod_info["power_address"]}
419+ orig_vmhost = await deferToDatabase(
420+ factory.make_Pod,
421+ zone=zone,
422+ pod_type=pod_info["type"],
423+ parameters=power_parameters,
424+ )
425+ user = await deferToDatabase(factory.make_User)
426+ vmhost = await vmhost_module.discover_and_sync_vmhost_async(
427+ orig_vmhost, user
428+ )
429+
430+ def _get_cluster_pod_names():
431+ hints = PodHints.objects.filter(cluster=vmhost.hints.cluster)
432+ return [hint.pod.name for hint in hints]
433+
434+ pod_names = await deferToDatabase(_get_cluster_pod_names)
435+ expected_names = [pod.name for pod in discovered_cluster.pods]
436+ self.assertCountEqual(pod_names, expected_names)
437diff --git a/src/maasserver/vmhost.py b/src/maasserver/vmhost.py
438index 1e55004..d907427 100644
439--- a/src/maasserver/vmhost.py
440+++ b/src/maasserver/vmhost.py
441@@ -12,13 +12,16 @@ from maasserver.exceptions import PodProblem
442 from maasserver.models import (
443 BMCRoutableRackControllerRelationship,
444 Event,
445+ Pod,
446 RackController,
447+ VMCluster,
448 )
449 from maasserver.rpc import getClientFromIdentifiers
450 from maasserver.utils import absolute_reverse
451 from maasserver.utils.orm import post_commit_do, transactional
452 from maasserver.utils.threads import deferToDatabase
453 from metadataserver.models import NodeKey
454+from provisioningserver.drivers.pod import DiscoveredCluster
455 from provisioningserver.events import EVENT_TYPES
456
457
458@@ -76,17 +79,21 @@ def discover_and_sync_vmhost(vmhost, user):
459 "Unable to start the VM host discovery process. "
460 "No rack controllers connected."
461 )
462- _update_db(discovered_pod, discovered, vmhost, user)
463- # The data isn't committed to the database until the transaction is
464- # complete. The commissioning results must be sent after the
465- # transaction completes so the metadata server can process the
466- # data.
467- post_commit_do(
468- reactor.callLater,
469- 0,
470- request_commissioning_results,
471- vmhost,
472- )
473+ elif isinstance(discovered_pod, DiscoveredCluster):
474+ vmhost = sync_vmcluster(discovered_pod, discovered, vmhost, user)
475+ else:
476+ vmhost = _update_db(discovered_pod, discovered, vmhost, user)
477+ # The data isn't committed to the database until the transaction is
478+ # complete. The commissioning results must be sent after the
479+ # transaction completes so the metadata server can process the
480+ # data.
481+ post_commit_do(
482+ reactor.callLater,
483+ 0,
484+ request_commissioning_results,
485+ vmhost,
486+ )
487+
488 return vmhost
489
490
491@@ -108,18 +115,110 @@ async def discover_and_sync_vmhost_async(vmhost, user):
492 "Unable to start the VM host discovery process. "
493 "No rack controllers connected."
494 )
495+ elif isinstance(discovered_pod, DiscoveredCluster):
496+ vmhost = await sync_vmcluster_async(
497+ discovered_pod, discovered, vmhost, user
498+ )
499+ else:
500+ await deferToDatabase(
501+ transactional(_update_db), discovered_pod, discovered, vmhost, user
502+ )
503+ await request_commissioning_results(vmhost)
504+
505+ return vmhost
506+
507+
508+def _generate_cluster_power_params(pod, pod_address, first_host):
509+ new_params = first_host.power_parameters.copy()
510+ if pod_address.startswith("http://") or pod_address.startswith("https://"):
511+ pod_address = pod_address.split("://")[1]
512+ new_params["power_address"] = pod_address
513+ new_params["instance_name"] = pod.name
514+ return new_params
515+
516
517- await deferToDatabase(
518- transactional(_update_db), discovered_pod, discovered, vmhost, user
519+def sync_vmcluster(discovered_cluster, discovered, vmhost, user):
520+ cluster = VMCluster.objects.create(
521+ name=discovered_cluster.name or vmhost.name,
522+ project=discovered_cluster.project,
523 )
524- await request_commissioning_results(vmhost)
525+ new_host = vmhost
526+ for i, pod in enumerate(discovered_cluster.pods):
527+ power_parameters = _generate_cluster_power_params(
528+ pod, discovered_cluster.pod_addresses[i], vmhost
529+ )
530+ if (
531+ power_parameters["power_address"]
532+ != vmhost.power_parameters["power_address"]
533+ ):
534+ new_host = Pod.objects.create(
535+ name=pod.name,
536+ architectures=pod.architectures,
537+ capabilities=pod.capabilities,
538+ version=pod.version,
539+ cores=pod.cores,
540+ cpu_speed=pod.cpu_speed,
541+ power_parameters=power_parameters,
542+ power_type="lxd", # VM clusters are only supported in LXD
543+ )
544+ new_host = _update_db(pod, discovered, new_host, user, cluster)
545+ post_commit_do(
546+ reactor.callLater,
547+ 0,
548+ request_commissioning_results,
549+ new_host,
550+ )
551+ if i == 0:
552+ vmhost = new_host
553 return vmhost
554
555
556-def _update_db(discovered_pod, discovered, vmhost, user):
557+async def sync_vmcluster_async(discovered_cluster, discovered, vmhost, user):
558+ def _transaction(discovered_cluster, discovered, vmhost, user):
559+ cluster = VMCluster.objects.create(
560+ name=discovered_cluster.name or vmhost.name,
561+ project=discovered_cluster.project,
562+ )
563+ new_hosts = []
564+ for i, pod in enumerate(discovered_cluster.pods):
565+ power_parameters = _generate_cluster_power_params(
566+ pod, discovered_cluster.pod_addresses[i], vmhost
567+ )
568+ new_host = vmhost
569+ if (
570+ power_parameters["power_address"]
571+ != vmhost.power_parameters["power_address"]
572+ ):
573+ new_host = Pod.objects.create(
574+ name=pod.name,
575+ architectures=pod.architectures,
576+ capabilities=pod.capabilities,
577+ version=pod.version,
578+ cores=pod.cores,
579+ cpu_speed=pod.cpu_speed,
580+ power_parameters=power_parameters,
581+ power_type="lxd", # VM clusters are only supported in LXD
582+ )
583+ new_host = _update_db(pod, discovered, new_host, user, cluster)
584+ new_hosts.append(new_host)
585+ return new_hosts
586+
587+ new_hosts = await deferToDatabase(
588+ transactional(_transaction),
589+ discovered_cluster,
590+ discovered,
591+ vmhost,
592+ user,
593+ )
594+ for new_host in new_hosts:
595+ await request_commissioning_results(new_host)
596+ return new_hosts[0]
597+
598+
599+def _update_db(discovered_pod, discovered, vmhost, user, cluster=None):
600 # If this is a new instance it will be stored in the database at the end of
601 # sync.
602- vmhost.sync(discovered_pod, user)
603+ vmhost.sync(discovered_pod, user, cluster=cluster)
604
605 # Save which rack controllers can route and which cannot.
606 discovered_rack_ids = [rack_id for rack_id, _ in discovered[0].items()]
607diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py
608index fa85a33..7494950 100644
609--- a/src/provisioningserver/drivers/pod/__init__.py
610+++ b/src/provisioningserver/drivers/pod/__init__.py
611@@ -295,6 +295,21 @@ class DiscoveredPod:
612 converter=converter_list(DiscoveredPodStoragePool),
613 default=attr.Factory(list),
614 )
615+ clustered = attr.ib(converter=bool, default=False)
616+
617+
618+@attr.s
619+class DiscoveredCluster:
620+ """Discovered cluster information"""
621+
622+ name = attr.ib(converter=converter_obj(str, optional=True), default=None)
623+ project = attr.ib(converter=str, default="")
624+ pods = attr.ib(
625+ converter=converter_list(DiscoveredPod), default=attr.Factory(list)
626+ )
627+ pod_addresses = attr.ib(
628+ converter=converter_list(str), default=attr.Factory(list)
629+ )
630
631
632 @attr.s
633diff --git a/src/provisioningserver/drivers/pod/lxd.py b/src/provisioningserver/drivers/pod/lxd.py
634index 9296378..9261931 100644
635--- a/src/provisioningserver/drivers/pod/lxd.py
636+++ b/src/provisioningserver/drivers/pod/lxd.py
637@@ -29,6 +29,7 @@ from provisioningserver.drivers import (
638 )
639 from provisioningserver.drivers.pod import (
640 Capabilities,
641+ DiscoveredCluster,
642 DiscoveredMachine,
643 DiscoveredMachineBlockDevice,
644 DiscoveredMachineInterface,
645@@ -310,7 +311,29 @@ class LXDPodDriver(PodDriver):
646 def discover(self, pod_id: int, context: dict):
647 """Discover all Pod host resources."""
648 with self._get_client(pod_id, context) as client:
649- return self._discover(client, pod_id, context)
650+ discovered_pod = self._discover(client, pod_id, context)
651+ if discovered_pod.clustered:
652+ return self._discover_cluster(client, context)
653+ return discovered_pod
654+
655+ def _discover_cluster(self, client: Client, context: dict):
656+ discovered_cluster = DiscoveredCluster(
657+ name=context.get("instance_name"),
658+ project=client.project,
659+ )
660+
661+ cluster_members = client.cluster.members.all()
662+
663+ for member in cluster_members:
664+ discovered_context = context.copy()
665+ discovered_context["instance_name"] = member.server_name
666+ discovered_context["power_address"] = member.url
667+ with self._get_client(-1, discovered_context) as client:
668+ discovered_cluster.pods.append(
669+ self._discover(client, -1, discovered_context)
670+ )
671+ discovered_cluster.pod_addresses.append(member.url)
672+ return discovered_cluster
673
674 def _discover(self, client: Client, pod_id: int, context: dict):
675 self._check_required_extensions(client)
676@@ -345,6 +368,7 @@ class LXDPodDriver(PodDriver):
677 Capabilities.OVER_COMMIT,
678 Capabilities.STORAGE_POOLS,
679 ],
680+ clustered=environment["server_clustered"],
681 )
682
683 # Discover networks. "unknown" interfaces are considered too to match
684diff --git a/src/provisioningserver/drivers/pod/tests/test_lxd.py b/src/provisioningserver/drivers/pod/tests/test_lxd.py
685index cee23a4..05e8397 100644
686--- a/src/provisioningserver/drivers/pod/tests/test_lxd.py
687+++ b/src/provisioningserver/drivers/pod/tests/test_lxd.py
688@@ -147,15 +147,16 @@ class FakeClient:
689 class FakeLXD:
690 """A fake LXD server."""
691
692- def __init__(self):
693+ def __init__(self, name="lxd-server", clustered=False):
694 # global details
695 self.host_info = {
696 "api_extensions": sorted(lxd_module.LXD_REQUIRED_EXTENSIONS),
697 "environment": {
698 "architectures": ["x86_64", "i686"],
699 "kernel_architecture": "x86_64",
700- "server_name": "lxd-server",
701+ "server_name": name,
702 "server_version": "4.1",
703+ "server_clustered": clustered,
704 },
705 }
706 self.resources = {}
707@@ -166,6 +167,9 @@ class FakeLXD:
708 self.projects = MagicMock()
709 self.storage_pools = MagicMock()
710 self.virtual_machines = MagicMock()
711+ if clustered:
712+ self.cluster = MagicMock()
713+ self.cluster.members = MagicMock()
714
715 self._client_behaviors = None
716
717@@ -203,9 +207,131 @@ class FakeLXD:
718 self._client_behaviors.append(behaviors)
719
720
721+class FakeLXDCluster:
722+ """A fake cluster of LXD servers"""
723+
724+ def __init__(self, num_pods=1):
725+ self.pods = [
726+ FakeLXD(name="lxd-server-%d" % i, clustered=True)
727+ for i in range(0, num_pods)
728+ ]
729+ self.pod_addresses = []
730+
731+ self.clients = []
732+ self.client_idx = 0
733+ for pod in self.pods:
734+ self.clients.append(pod.make_client)
735+
736+ self._make_members()
737+
738+ def _make_members(self):
739+ members = []
740+ for i, pod in enumerate(self.pods):
741+ member = Mock()
742+ member.architectures = pod.host_info["environment"][
743+ "architectures"
744+ ]
745+ member.server_name = pod.host_info["environment"]["server_name"]
746+ member.url = "http://lxd-%d" % i
747+ self.pod_addresses.append(member.url)
748+ members.append(member)
749+ for pod in self.pods:
750+ pod.cluster.members.all.return_value = members
751+
752+ def make_client(
753+ self,
754+ endpoint="https://lxd",
755+ project="default",
756+ cert=None,
757+ verify=False,
758+ ):
759+ if self.client_idx == len(self.clients):
760+ self.client_idx = 0
761+ client = self.clients[self.client_idx](endpoint, project, cert, verify)
762+ client._PROXIES += ("cluster", "cluster/members")
763+ self.client_idx += 1
764+ return client
765+
766+
767 SAMPLE_CERT = generate_certificate("maas")
768
769
770+def _make_maas_certs(test_case):
771+ tempdir = Path(test_case.useFixture(TempDir()).path)
772+ test_case.useFixture(EnvironmentVariable("MAAS_ROOT", str(tempdir)))
773+ test_case.certs_dir = tempdir / "etc/maas/certificates"
774+ test_case.certs_dir.mkdir(parents=True)
775+ maas_cert = test_case.certs_dir / "maas.crt"
776+ maas_cert.touch()
777+ maas_key = test_case.certs_dir / "maas.key"
778+ maas_key.touch()
779+ return str(maas_cert), str(maas_key)
780+
781+
782+def _make_context(with_cert=True, with_password=True, extra=None):
783+ params = {
784+ "power_address": "".join(
785+ [
786+ factory.make_name("power_address"),
787+ ":%s" % factory.pick_port(),
788+ ]
789+ ),
790+ "instance_name": factory.make_name("instance_name"),
791+ "project": factory.make_name("project"),
792+ }
793+ if with_cert:
794+ params["certificate"] = SAMPLE_CERT.certificate_pem()
795+ params["key"] = SAMPLE_CERT.private_key_pem()
796+ if with_password:
797+ params["password"] = factory.make_name("password")
798+ if not extra:
799+ extra = {}
800+ return {**params, **extra}
801+
802+
803+class TestClusteredLXDPodDriver(MAASTestCase):
804+ run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
805+
806+ def setUp(self):
807+ super().setUp()
808+ self.fake_lxd_cluster = FakeLXDCluster(num_pods=3)
809+ self.fake_lxd = self.fake_lxd_cluster.pods[0]
810+ self.driver = lxd_module.LXDPodDriver()
811+ self.driver._pylxd_client_class = self.fake_lxd_cluster.make_client
812+
813+ def make_maas_certs(self):
814+ return _make_maas_certs(self)
815+
816+ def make_context(self, with_cert=True, with_password=True, extra=None):
817+ return _make_context(with_cert, with_password, extra)
818+
819+ @inlineCallbacks
820+ def test_discover_discovers_cluster(self):
821+ mac_address = factory.make_mac_address()
822+ lxd_net1 = Mock(type="physical")
823+ lxd_net1.state.return_value = FakeNetworkState(mac_address)
824+ # virtual interfaces are excluded
825+ lxd_net2 = Mock(type="bridge")
826+ lxd_net2.state.return_value = FakeNetworkState(
827+ factory.make_mac_address()
828+ )
829+ self.fake_lxd.networks.all.return_value = [lxd_net1, lxd_net2]
830+ context = self.make_context()
831+ expected_names = [
832+ pod.host_info["environment"]["server_name"]
833+ for pod in self.fake_lxd_cluster.pods
834+ ]
835+ discovered_cluster = yield self.driver.discover(None, context)
836+ self.assertEqual(context["instance_name"], discovered_cluster.name)
837+ self.assertEqual(context["project"], discovered_cluster.project)
838+ discovered_names = [pod.name for pod in discovered_cluster.pods]
839+ self.assertCountEqual(expected_names, discovered_names)
840+ self.assertCountEqual(
841+ self.fake_lxd_cluster.pod_addresses,
842+ discovered_cluster.pod_addresses,
843+ )
844+
845+
846 class TestLXDPodDriver(MAASTestCase):
847
848 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
849@@ -217,35 +343,10 @@ class TestLXDPodDriver(MAASTestCase):
850 self.driver._pylxd_client_class = self.fake_lxd.make_client
851
852 def make_maas_certs(self):
853- tempdir = Path(self.useFixture(TempDir()).path)
854- self.useFixture(EnvironmentVariable("MAAS_ROOT", str(tempdir)))
855- self.certs_dir = tempdir / "etc/maas/certificates"
856- self.certs_dir.mkdir(parents=True)
857- maas_cert = self.certs_dir / "maas.crt"
858- maas_cert.touch()
859- maas_key = self.certs_dir / "maas.key"
860- maas_key.touch()
861- return str(maas_cert), str(maas_key)
862+ return _make_maas_certs(self)
863
864 def make_context(self, with_cert=True, with_password=True, extra=None):
865- params = {
866- "power_address": "".join(
867- [
868- factory.make_name("power_address"),
869- ":%s" % factory.pick_port(),
870- ]
871- ),
872- "instance_name": factory.make_name("instance_name"),
873- "project": factory.make_name("project"),
874- }
875- if with_cert:
876- params["certificate"] = SAMPLE_CERT.certificate_pem()
877- params["key"] = SAMPLE_CERT.private_key_pem()
878- if with_password:
879- params["password"] = factory.make_name("password")
880- if not extra:
881- extra = {}
882- return {**params, **extra}
883+ return _make_context(with_cert, with_password, extra)
884
885 def test_missing_packages(self):
886 self.assertEqual(self.driver.detect_missing_packages(), [])
887diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py
888index b245c61..7d208af 100644
889--- a/src/provisioningserver/rpc/cluster.py
890+++ b/src/provisioningserver/rpc/cluster.py
891@@ -628,8 +628,17 @@ class DiscoverPod(amp.Command):
892 response = [
893 (
894 b"pod",
895- AttrsClassArgument("provisioningserver.drivers.pod.DiscoveredPod"),
896- )
897+ AttrsClassArgument(
898+ "provisioningserver.drivers.pod.DiscoveredPod", optional=True
899+ ),
900+ ),
901+ (
902+ b"cluster",
903+ AttrsClassArgument(
904+ "provisioningserver.drivers.pod.DiscoveredCluster",
905+ optional=True,
906+ ),
907+ ),
908 ]
909 errors = {
910 exceptions.UnknownPodType: b"UnknownPodType",
911diff --git a/src/provisioningserver/rpc/pods.py b/src/provisioningserver/rpc/pods.py
912index 9173ae5..96c5e98 100644
913--- a/src/provisioningserver/rpc/pods.py
914+++ b/src/provisioningserver/rpc/pods.py
915@@ -10,6 +10,7 @@ from twisted.internet.defer import Deferred, ensureDeferred
916 from twisted.internet.threads import deferToThread
917
918 from provisioningserver.drivers.pod import (
919+ DiscoveredCluster,
920 DiscoveredMachine,
921 DiscoveredPod,
922 DiscoveredPodHints,
923@@ -85,6 +86,8 @@ def discover_pod(pod_type, context, pod_id=None, name=None):
924 """Convert the result to send over RPC."""
925 if result is None:
926 raise PodActionFail("unable to discover pod information.")
927+ elif isinstance(result, DiscoveredCluster):
928+ return {"cluster": result}
929 elif not isinstance(result, DiscoveredPod):
930 raise PodActionFail(
931 "bad pod driver '%s'; 'discover' returned invalid result."
932diff --git a/src/provisioningserver/rpc/tests/test_pods.py b/src/provisioningserver/rpc/tests/test_pods.py
933index e0e078d..058005a 100644
934--- a/src/provisioningserver/rpc/tests/test_pods.py
935+++ b/src/provisioningserver/rpc/tests/test_pods.py
936@@ -11,12 +11,13 @@ from unittest.mock import call, MagicMock
937 from urllib.parse import urlparse
938
939 from testtools import ExpectedException
940-from twisted.internet.defer import fail, inlineCallbacks, succeed
941+from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed
942
943 from maastesting.factory import factory
944 from maastesting.matchers import MockCallsMatch
945 from maastesting.testcase import MAASTestCase, MAASTwistedRunTest
946 from provisioningserver.drivers.pod import (
947+ DiscoveredCluster,
948 DiscoveredMachine,
949 DiscoveredPod,
950 DiscoveredPodHints,
951@@ -173,6 +174,26 @@ class TestDiscoverPod(MAASTestCase):
952 ):
953 yield pods.discover_pod(fake_driver.name, {})
954
955+ @inlineCallbacks
956+ def test_handlers_driver_returning_cluster(self):
957+ fake_driver = MagicMock()
958+ fake_driver.name = factory.make_name("pod")
959+ expected_cluster = DiscoveredCluster(
960+ name=factory.make_name("name"),
961+ project=factory.make_name("project"),
962+ pods=[
963+ DiscoveredPod(name=factory.make_name("pod-name"))
964+ for _ in range(0, 3)
965+ ],
966+ )
967+ fake_driver.discover.return_value = returnValue(expected_cluster)
968+ self.patch(PodDriverRegistry, "get_item").return_value = fake_driver
969+ result = yield pods.discover_pod(fake_driver.name, {})
970+ discovered_cluster = result["cluster"]
971+ self.assertEqual(expected_cluster.name, discovered_cluster.name)
972+ self.assertEqual(expected_cluster.project, discovered_cluster.project)
973+ self.assertCountEqual(expected_cluster.pods, discovered_cluster.pods)
974+
975
976 class TestComposeMachine(MAASTestCase):
977

Subscribers

People subscribed via source and target branches