Merge ~cgrabowski/maas:LXD_create_cluster into maas:master
- Git
- lp:~cgrabowski/maas
- LXD_create_cluster
- Merge into 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) |
Related bugs: |
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
Description of the change
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: SUCCESS
COMMIT: 237545542a6c49b
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
1 | diff --git a/src/maasserver/clusterrpc/pods.py b/src/maasserver/clusterrpc/pods.py |
2 | index 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 |
41 | diff --git a/src/maasserver/clusterrpc/tests/test_pods.py b/src/maasserver/clusterrpc/tests/test_pods.py |
42 | index 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): |
109 | diff --git a/src/maasserver/models/bmc.py b/src/maasserver/models/bmc.py |
110 | index 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: |
160 | diff --git a/src/maasserver/models/tests/test_bmc.py b/src/maasserver/models/tests/test_bmc.py |
161 | index 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() |
185 | diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py |
186 | index 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, |
215 | diff --git a/src/maasserver/tests/test_vmhost.py b/src/maasserver/tests/test_vmhost.py |
216 | index 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) |
437 | diff --git a/src/maasserver/vmhost.py b/src/maasserver/vmhost.py |
438 | index 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()] |
607 | diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py |
608 | index 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 |
633 | diff --git a/src/provisioningserver/drivers/pod/lxd.py b/src/provisioningserver/drivers/pod/lxd.py |
634 | index 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 |
684 | diff --git a/src/provisioningserver/drivers/pod/tests/test_lxd.py b/src/provisioningserver/drivers/pod/tests/test_lxd.py |
685 | index 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(), []) |
887 | diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py |
888 | index 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", |
911 | diff --git a/src/provisioningserver/rpc/pods.py b/src/provisioningserver/rpc/pods.py |
912 | index 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." |
932 | diff --git a/src/provisioningserver/rpc/tests/test_pods.py b/src/provisioningserver/rpc/tests/test_pods.py |
933 | index 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 |
UNIT TESTS
-b LXD_create_cluster lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 11115/console 3b7207a691c8eaa 4c62f7c631
LOG: http://
COMMIT: de2476ab55b3a05