Merge ~mpontillo/maas:compose-machine-form--interpret-interfaces-constraint into maas:master
- Git
- lp:~mpontillo/maas
- compose-machine-form--interpret-interfaces-constraint
- Merge into master
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | 4d7ee797ccb3e36258ba203c7d44e01b9e5f9e83 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~mpontillo/maas:compose-machine-form--interpret-interfaces-constraint |
Merge into: | maas:master |
Diff against target: |
634 lines (+338/-54) 11 files modified
src/maasserver/api/tests/test_pods.py (+2/-4) src/maasserver/enum.py (+0/-18) src/maasserver/forms/pods.py (+69/-6) src/maasserver/forms/tests/test_pods.py (+194/-1) src/maasserver/models/bmc.py (+1/-1) src/maasserver/testing/factory.py (+0/-17) src/maastesting/factory.py (+17/-0) src/provisioningserver/drivers/pod/__init__.py (+15/-0) src/provisioningserver/drivers/pod/tests/test_virsh.py (+6/-4) src/provisioningserver/drivers/pod/virsh.py (+4/-3) src/provisioningserver/enum.py (+30/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Newell Jensen (community) | Approve | ||
Review via email: mp+351400@code.launchpad.net |
Commit message
Interpret interfaces constraint when composing machines from a pod.
Description of the change
- 559c5d8... by Mike Pontillo
-
Format imports.
- cfdd414... by Mike Pontillo
-
Address review comments. Add constants for use with interface attach type. Use attach type constants in virsh driver.
- bc2a305... by Mike Pontillo
-
Move factory.
pick_choice( ) to parent class. Use new constants in virsh pod driver tests.
Newell Jensen (newell-jensen) wrote : | # |
Looks good. Thanks for the fixes!
- df002f8... by Mike Pontillo
-
Properly format imports.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b compose-
STATUS: FAILED
LOG: http://
COMMIT: ac287a5cba10e8e
- 7a78b35... by Mike Pontillo
-
Move common enums into provisioningserver code.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b compose-
STATUS: FAILED
LOG: http://
COMMIT: f61551b9847df0a
- 4d7ee79... by Mike Pontillo
-
Fix merge conflict.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b compose-
STATUS: SUCCESS
COMMIT: 4d7ee797ccb3e36
Preview Diff
1 | diff --git a/src/maasserver/api/tests/test_pods.py b/src/maasserver/api/tests/test_pods.py |
2 | index be26a44..0caf498 100644 |
3 | --- a/src/maasserver/api/tests/test_pods.py |
4 | +++ b/src/maasserver/api/tests/test_pods.py |
5 | @@ -9,10 +9,7 @@ import http.client |
6 | import random |
7 | from unittest.mock import MagicMock |
8 | |
9 | -from maasserver.enum import ( |
10 | - MACVLAN_MODE_CHOICES, |
11 | - NODE_CREATION_TYPE, |
12 | -) |
13 | +from maasserver.enum import NODE_CREATION_TYPE |
14 | from maasserver.forms import pods |
15 | from maasserver.models.bmc import Pod |
16 | from maasserver.models.node import Machine |
17 | @@ -28,6 +25,7 @@ from provisioningserver.drivers.pod import ( |
18 | DiscoveredPod, |
19 | DiscoveredPodHints, |
20 | ) |
21 | +from provisioningserver.enum import MACVLAN_MODE_CHOICES |
22 | from twisted.internet.defer import succeed |
23 | |
24 | |
25 | diff --git a/src/maasserver/enum.py b/src/maasserver/enum.py |
26 | index 9f066c0..b9e8a4a 100644 |
27 | --- a/src/maasserver/enum.py |
28 | +++ b/src/maasserver/enum.py |
29 | @@ -16,8 +16,6 @@ __all__ = [ |
30 | 'INTERFACE_TYPE_CHOICES', |
31 | 'INTERFACE_TYPE_CHOICES_DICT', |
32 | 'IPADDRESS_TYPE', |
33 | - 'MACVLAN_MODE', |
34 | - 'MACVLAN_MODE_CHOICES', |
35 | 'NODE_PERMISSION', |
36 | 'NODE_STATUS', |
37 | 'NODE_STATUS_CHOICES', |
38 | @@ -778,19 +776,3 @@ ENDPOINT_CHOICES = ( |
39 | (ENDPOINT.API, "API"), |
40 | (ENDPOINT.UI, "WebUI"), |
41 | ) |
42 | - |
43 | - |
44 | -class MACVLAN_MODE: |
45 | - |
46 | - BRIDGE = "bridge" |
47 | - PASSTHRU = "passthru" |
48 | - PRIVATE = "private" |
49 | - VEPA = "vepa" |
50 | - |
51 | - |
52 | -MACVLAN_MODE_CHOICES = ( |
53 | - (MACVLAN_MODE.BRIDGE, "bridge"), |
54 | - (MACVLAN_MODE.PASSTHRU, "passthru"), |
55 | - (MACVLAN_MODE.PRIVATE, "private"), |
56 | - (MACVLAN_MODE.VEPA, "vepa"), |
57 | -) |
58 | diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py |
59 | index 779a015..764bff8 100644 |
60 | --- a/src/maasserver/forms/pods.py |
61 | +++ b/src/maasserver/forms/pods.py |
62 | @@ -30,7 +30,7 @@ from maasserver.clusterrpc.pods import ( |
63 | ) |
64 | from maasserver.enum import ( |
65 | BMC_TYPE, |
66 | - MACVLAN_MODE_CHOICES, |
67 | + INTERFACE_TYPE, |
68 | NODE_CREATION_TYPE, |
69 | ) |
70 | from maasserver.exceptions import PodProblem |
71 | @@ -40,6 +40,7 @@ from maasserver.models import ( |
72 | BMC, |
73 | BMCRoutableRackControllerRelationship, |
74 | Domain, |
75 | + Interface, |
76 | Machine, |
77 | Node, |
78 | Pod, |
79 | @@ -50,6 +51,9 @@ from maasserver.models import ( |
80 | ) |
81 | from maasserver.node_constraint_filter_forms import ( |
82 | get_storage_constraints_from_string, |
83 | + interfaces_validator, |
84 | + LabeledConstraintMapField, |
85 | + nodes_by_interface, |
86 | storage_validator, |
87 | ) |
88 | from maasserver.rpc import getClientFromIdentifiers |
89 | @@ -63,10 +67,15 @@ import petname |
90 | from provisioningserver.drivers import SETTING_SCOPE |
91 | from provisioningserver.drivers.pod import ( |
92 | Capabilities, |
93 | + InterfaceAttachType, |
94 | RequestedMachine, |
95 | RequestedMachineBlockDevice, |
96 | RequestedMachineInterface, |
97 | ) |
98 | +from provisioningserver.enum import ( |
99 | + MACVLAN_MODE, |
100 | + MACVLAN_MODE_CHOICES, |
101 | +) |
102 | from provisioningserver.utils.twisted import asynchronous |
103 | from twisted.python.threadable import isInIOThread |
104 | |
105 | @@ -398,6 +407,10 @@ class ComposeMachineForm(forms.Form): |
106 | self.fields['storage'] = CharField( |
107 | validators=[storage_validator], required=False) |
108 | self.initial['storage'] = 'root:8(local)' |
109 | + self.fields['interfaces'] = LabeledConstraintMapField( |
110 | + validators=[interfaces_validator], label="Interface constraints", |
111 | + required=False) |
112 | + self.initial['interfaces'] = None |
113 | self.fields['skip_commissioning'] = BooleanField(required=False) |
114 | self.initial['skip_commissioning'] = False |
115 | |
116 | @@ -414,16 +427,24 @@ class ComposeMachineForm(forms.Form): |
117 | |
118 | def get_requested_machine(self): |
119 | """Return the `RequestedMachine`.""" |
120 | - # XXX blake_r 2017-04-04: Interfaces are hard coded at the |
121 | - # moment. Will be extended later. |
122 | block_devices = [] |
123 | - constraints = get_storage_constraints_from_string( |
124 | + |
125 | + storage_constraints = get_storage_constraints_from_string( |
126 | self.get_value_for('storage')) |
127 | - for _, size, tags in constraints: |
128 | + for _, size, tags in storage_constraints: |
129 | if tags is None: |
130 | tags = [] |
131 | block_devices.append( |
132 | RequestedMachineBlockDevice(size=size, tags=tags)) |
133 | + interfaces_label_map = self.get_value_for('interfaces') |
134 | + if interfaces_label_map is not None: |
135 | + requested_machine_interfaces = ( |
136 | + self._get_requested_machine_interfaces_via_constraints( |
137 | + interfaces_label_map) |
138 | + ) |
139 | + else: |
140 | + requested_machine_interfaces = [RequestedMachineInterface()] |
141 | + |
142 | return RequestedMachine( |
143 | hostname=self.get_value_for('hostname'), |
144 | architecture=self.get_value_for('architecture'), |
145 | @@ -431,7 +452,49 @@ class ComposeMachineForm(forms.Form): |
146 | memory=self.get_value_for('memory'), |
147 | cpu_speed=self.get_value_for('cpu_speed'), |
148 | block_devices=block_devices, |
149 | - interfaces=[RequestedMachineInterface()]) |
150 | + interfaces=requested_machine_interfaces) |
151 | + |
152 | + def _get_requested_machine_interfaces_via_constraints( |
153 | + self, interfaces_label_map): |
154 | + requested_machine_interfaces = [] |
155 | + if self.pod.host is None: |
156 | + raise ValidationError( |
157 | + "Pod must be on a known host if interfaces are specified.") |
158 | + node_ids, compatible_interfaces = nodes_by_interface( |
159 | + interfaces_label_map) |
160 | + pod_node_id = self.pod.host.id |
161 | + if pod_node_id not in node_ids: |
162 | + raise ValidationError( |
163 | + "This pod does not match the specified networks.") |
164 | + for label in compatible_interfaces.keys(): |
165 | + interface_ids = compatible_interfaces[label][pod_node_id] |
166 | + # XXX: We might want to use the "deepest" interface in the |
167 | + # heirarchy, to ensure we get a bridge or bond (if configured) |
168 | + # rather than its parent. max() is a good approximation, since |
169 | + # child interfaces will be created after their parents. |
170 | + requested_machine_interfaces.append( |
171 | + self.get_requested_machine_interface_by_interface_id( |
172 | + max(interface_ids)) |
173 | + ) |
174 | + return requested_machine_interfaces |
175 | + |
176 | + def get_requested_machine_interface_by_interface_id(self, interface_id): |
177 | + interface = Interface.objects.get(id=interface_id) |
178 | + if interface.type == INTERFACE_TYPE.BRIDGE: |
179 | + rmi = RequestedMachineInterface( |
180 | + attach_name=interface.name, |
181 | + attach_type=InterfaceAttachType.BRIDGE) |
182 | + else: |
183 | + attach_options = self.pod.default_macvlan_mode |
184 | + if attach_options is None: |
185 | + # Default macvlan mode is 'bridge' if not specified, since that |
186 | + # provides the best chance for connectivity. |
187 | + attach_options = MACVLAN_MODE.BRIDGE |
188 | + rmi = RequestedMachineInterface( |
189 | + attach_name=interface.name, |
190 | + attach_type=InterfaceAttachType.MACVLAN, |
191 | + attach_options=attach_options) |
192 | + return rmi |
193 | |
194 | def save(self): |
195 | """Prevent from usage.""" |
196 | diff --git a/src/maasserver/forms/tests/test_pods.py b/src/maasserver/forms/tests/test_pods.py |
197 | index 525ec03..97bd687 100644 |
198 | --- a/src/maasserver/forms/tests/test_pods.py |
199 | +++ b/src/maasserver/forms/tests/test_pods.py |
200 | @@ -20,8 +20,8 @@ from django.core.validators import ( |
201 | ) |
202 | from maasserver.enum import ( |
203 | BMC_TYPE, |
204 | - MACVLAN_MODE_CHOICES, |
205 | NODE_CREATION_TYPE, |
206 | + NODE_STATUS, |
207 | ) |
208 | from maasserver.exceptions import PodProblem |
209 | from maasserver.forms import pods as pods_module |
210 | @@ -53,10 +53,15 @@ from provisioningserver.drivers.pod import ( |
211 | DiscoveredPod, |
212 | DiscoveredPodHints, |
213 | DiscoveredPodStoragePool, |
214 | + InterfaceAttachType, |
215 | RequestedMachine, |
216 | RequestedMachineBlockDevice, |
217 | RequestedMachineInterface, |
218 | ) |
219 | +from provisioningserver.enum import ( |
220 | + MACVLAN_MODE, |
221 | + MACVLAN_MODE_CHOICES, |
222 | +) |
223 | from testtools import ExpectedException |
224 | from testtools.matchers import ( |
225 | Equals, |
226 | @@ -910,6 +915,194 @@ class TestComposeMachineForm(MAASTransactionServerTestCase): |
227 | size=Equals(disk_2), tags=Equals([]))), |
228 | ])))) |
229 | |
230 | + def test__get_machine_with_interfaces_fails_for_no_pod_host(self): |
231 | + request = MagicMock() |
232 | + host = factory.make_Machine_with_Interface_on_Subnet() |
233 | + pod = make_pod_with_hints() |
234 | + interfaces = "eth0:subnet=%s" % ( |
235 | + host.boot_interface.vlan.subnet_set.first().cidr) |
236 | + form = ComposeMachineForm(data={ |
237 | + 'interfaces': interfaces, |
238 | + }, request=request, pod=pod) |
239 | + self.assertTrue(form.is_valid(), form.errors) |
240 | + with ExpectedException( |
241 | + ValidationError, |
242 | + ".*must be on a known host if interfaces are specified.*"): |
243 | + form.get_requested_machine() |
244 | + |
245 | + def test__get_machine_with_interfaces_fails_for_no_matching_network(self): |
246 | + request = MagicMock() |
247 | + host = factory.make_Machine_with_Interface_on_Subnet() |
248 | + # Make a subnet that won't match the host via the constraint. |
249 | + subnet = factory.make_Subnet() |
250 | + pod = make_pod_with_hints() |
251 | + pod.host = host |
252 | + pod.save() |
253 | + interfaces = "eth0:subnet=%s" % ( |
254 | + subnet.cidr) |
255 | + form = ComposeMachineForm(data={ |
256 | + 'interfaces': interfaces, |
257 | + }, request=request, pod=pod) |
258 | + self.assertTrue(form.is_valid(), form.errors) |
259 | + with ExpectedException( |
260 | + ValidationError, |
261 | + ".*does not match the specified network.*"): |
262 | + form.get_requested_machine() |
263 | + |
264 | + def test__get_machine_with_interfaces_by_subnet(self): |
265 | + request = MagicMock() |
266 | + host = factory.make_Machine_with_Interface_on_Subnet() |
267 | + space = factory.make_Space('dmz') |
268 | + host.boot_interface.vlan.space = space |
269 | + host.boot_interface.vlan.save() |
270 | + pod = make_pod_with_hints() |
271 | + pod.host = host |
272 | + pod.save() |
273 | + interfaces = "eth0:subnet=%s" % ( |
274 | + host.boot_interface.vlan.subnet_set.first().cidr) |
275 | + form = ComposeMachineForm(data={ |
276 | + 'interfaces': interfaces, |
277 | + }, request=request, pod=pod) |
278 | + self.assertTrue(form.is_valid(), form.errors) |
279 | + request_machine = form.get_requested_machine() |
280 | + self.assertThat(request_machine, MatchesAll( |
281 | + IsInstance(RequestedMachine), |
282 | + MatchesStructure( |
283 | + interfaces=MatchesListwise([ |
284 | + MatchesAll( |
285 | + IsInstance(RequestedMachineInterface), |
286 | + MatchesStructure( |
287 | + attach_name=Equals(host.boot_interface.name), |
288 | + attach_type=Equals(InterfaceAttachType.MACVLAN), |
289 | + attach_options=Equals(MACVLAN_MODE.BRIDGE))) |
290 | + ])))) |
291 | + |
292 | + def test__get_machine_with_interfaces_by_subnet_with_default_mode(self): |
293 | + request = MagicMock() |
294 | + host = factory.make_Machine_with_Interface_on_Subnet() |
295 | + space = factory.make_Space('dmz') |
296 | + host.boot_interface.vlan.space = space |
297 | + host.boot_interface.vlan.save() |
298 | + pod = make_pod_with_hints() |
299 | + pod.host = host |
300 | + attach_mode = factory.pick_choice(MACVLAN_MODE_CHOICES) |
301 | + pod.default_macvlan_mode = attach_mode |
302 | + pod.save() |
303 | + interfaces = "eth0:subnet=%s" % ( |
304 | + host.boot_interface.vlan.subnet_set.first().cidr) |
305 | + form = ComposeMachineForm(data={ |
306 | + 'interfaces': interfaces, |
307 | + }, request=request, pod=pod) |
308 | + self.assertTrue(form.is_valid(), form.errors) |
309 | + request_machine = form.get_requested_machine() |
310 | + self.assertThat(request_machine, MatchesAll( |
311 | + IsInstance(RequestedMachine), |
312 | + MatchesStructure( |
313 | + interfaces=MatchesListwise([ |
314 | + MatchesAll( |
315 | + IsInstance(RequestedMachineInterface), |
316 | + MatchesStructure( |
317 | + attach_name=Equals(host.boot_interface.name), |
318 | + attach_type=Equals(InterfaceAttachType.MACVLAN), |
319 | + attach_options=Equals(attach_mode))) |
320 | + ])))) |
321 | + |
322 | + def test__get_machine_with_interfaces_by_space(self): |
323 | + request = MagicMock() |
324 | + host = factory.make_Machine_with_Interface_on_Subnet() |
325 | + space = factory.make_Space('dmz') |
326 | + host.boot_interface.vlan.space = space |
327 | + host.boot_interface.vlan.save() |
328 | + pod = make_pod_with_hints() |
329 | + pod.host = host |
330 | + pod.save() |
331 | + interfaces = "eth0:space=dmz" |
332 | + form = ComposeMachineForm(data={ |
333 | + 'interfaces': interfaces, |
334 | + }, request=request, pod=pod) |
335 | + self.assertTrue(form.is_valid(), form.errors) |
336 | + request_machine = form.get_requested_machine() |
337 | + self.assertThat(request_machine, MatchesAll( |
338 | + IsInstance(RequestedMachine), |
339 | + MatchesStructure( |
340 | + interfaces=MatchesListwise([ |
341 | + MatchesAll( |
342 | + IsInstance(RequestedMachineInterface), |
343 | + MatchesStructure( |
344 | + attach_name=Equals(host.boot_interface.name), |
345 | + attach_type=Equals(InterfaceAttachType.MACVLAN), |
346 | + attach_options=Equals(MACVLAN_MODE.BRIDGE))) |
347 | + ])))) |
348 | + |
349 | + def test__get_machine_with_interfaces_by_spaces(self): |
350 | + request = MagicMock() |
351 | + host = factory.make_Machine_with_Interface_on_Subnet() |
352 | + dmz_space = factory.make_Space('dmz') |
353 | + storage_space = factory.make_Space('storage') |
354 | + # Because PXE booting from the DMZ is /always/ a great idea. ;-) |
355 | + host.boot_interface.vlan.space = dmz_space |
356 | + host.boot_interface.vlan.save() |
357 | + storage_vlan = factory.make_VLAN(space=storage_space) |
358 | + storage_if = factory.make_Interface(node=host, vlan=storage_vlan) |
359 | + pod = make_pod_with_hints() |
360 | + pod.host = host |
361 | + pod.save() |
362 | + interfaces = "eth0:space=dmz;eth1:space=storage" |
363 | + form = ComposeMachineForm(data={ |
364 | + 'interfaces': interfaces, |
365 | + }, request=request, pod=pod) |
366 | + self.assertTrue(form.is_valid(), form.errors) |
367 | + request_machine = form.get_requested_machine() |
368 | + self.assertThat(request_machine, MatchesAll( |
369 | + IsInstance(RequestedMachine), |
370 | + MatchesStructure( |
371 | + interfaces=MatchesListwise([ |
372 | + MatchesAll( |
373 | + IsInstance(RequestedMachineInterface), |
374 | + MatchesStructure( |
375 | + attach_name=Equals(host.boot_interface.name), |
376 | + attach_type=Equals(InterfaceAttachType.MACVLAN), |
377 | + attach_options=Equals(MACVLAN_MODE.BRIDGE))), |
378 | + MatchesAll( |
379 | + IsInstance(RequestedMachineInterface), |
380 | + MatchesStructure( |
381 | + attach_name=Equals(storage_if.name), |
382 | + attach_type=Equals(InterfaceAttachType.MACVLAN), |
383 | + attach_options=Equals(MACVLAN_MODE.BRIDGE))), |
384 | + ])))) |
385 | + |
386 | + def test__get_machine_with_interfaces_by_space_as_bridge(self): |
387 | + request = MagicMock() |
388 | + host = factory.make_Machine_with_Interface_on_Subnet( |
389 | + status=NODE_STATUS.READY) |
390 | + space = factory.make_Space('dmz') |
391 | + host.boot_interface.vlan.space = space |
392 | + host.boot_interface.vlan.save() |
393 | + # This is just to make sure a bridge will be available for attachment. |
394 | + # We expect bridge mode to be preferred, when available. |
395 | + host.acquire(factory.make_User(), bridge_all=True) |
396 | + pod = make_pod_with_hints() |
397 | + pod.host = host |
398 | + pod.save() |
399 | + interfaces = "eth0:space=dmz" |
400 | + form = ComposeMachineForm(data={ |
401 | + 'interfaces': interfaces, |
402 | + }, request=request, pod=pod) |
403 | + self.assertTrue(form.is_valid(), form.errors) |
404 | + request_machine = form.get_requested_machine() |
405 | + self.assertThat(request_machine, MatchesAll( |
406 | + IsInstance(RequestedMachine), |
407 | + MatchesStructure( |
408 | + interfaces=MatchesListwise([ |
409 | + MatchesAll( |
410 | + IsInstance(RequestedMachineInterface), |
411 | + MatchesStructure( |
412 | + attach_name=Equals( |
413 | + "br-" + host.boot_interface.name), |
414 | + attach_type=Equals(InterfaceAttachType.BRIDGE), |
415 | + attach_options=Is(None))) |
416 | + ])))) |
417 | + |
418 | def test__save_raises_AttributeError(self): |
419 | request = MagicMock() |
420 | pod = make_pod_with_hints() |
421 | diff --git a/src/maasserver/models/bmc.py b/src/maasserver/models/bmc.py |
422 | index df7e69e..52aebe0 100644 |
423 | --- a/src/maasserver/models/bmc.py |
424 | +++ b/src/maasserver/models/bmc.py |
425 | @@ -40,7 +40,6 @@ from maasserver.enum import ( |
426 | BMC_TYPE_CHOICES, |
427 | INTERFACE_TYPE, |
428 | IPADDRESS_TYPE, |
429 | - MACVLAN_MODE_CHOICES, |
430 | NODE_CREATION_TYPE, |
431 | NODE_STATUS, |
432 | ) |
433 | @@ -78,6 +77,7 @@ import petname |
434 | from provisioningserver.drivers import SETTING_SCOPE |
435 | from provisioningserver.drivers.pod import BlockDeviceType |
436 | from provisioningserver.drivers.power.registry import PowerDriverRegistry |
437 | +from provisioningserver.enum import MACVLAN_MODE_CHOICES |
438 | from provisioningserver.logger import get_maas_logger |
439 | from provisioningserver.utils.twisted import asynchronous |
440 | from twisted.internet.defer import inlineCallbacks |
441 | diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py |
442 | index 8ab4018..8a50f8b 100644 |
443 | --- a/src/maasserver/testing/factory.py |
444 | +++ b/src/maasserver/testing/factory.py |
445 | @@ -242,23 +242,6 @@ class Factory(maastesting.factory.Factory): |
446 | upload.name = name |
447 | return upload |
448 | |
449 | - def pick_choice(self, choices, but_not=None): |
450 | - """Pick a random item from `choices`. |
451 | - |
452 | - :param choices: A sequence of choices in Django form choices format: |
453 | - [ |
454 | - ('choice_id_1', "Choice name 1"), |
455 | - ('choice_id_2', "Choice name 2"), |
456 | - ] |
457 | - :param but_not: A list of choices' IDs to exclude. |
458 | - :type but_not: Sequence. |
459 | - :return: The "id" portion of a random choice out of `choices`. |
460 | - """ |
461 | - if but_not is None: |
462 | - but_not = () |
463 | - return random.choice( |
464 | - [choice for choice in choices if choice[0] not in but_not])[0] |
465 | - |
466 | def pick_power_type(self, but_not=None): |
467 | """Pick a random power type and return it. |
468 | |
469 | diff --git a/src/maastesting/factory.py b/src/maastesting/factory.py |
470 | index 7589626..db61d5d 100644 |
471 | --- a/src/maastesting/factory.py |
472 | +++ b/src/maastesting/factory.py |
473 | @@ -194,6 +194,23 @@ class Factory: |
474 | assert port_min >= 0 and port_max <= 65535 |
475 | return random.randint(port_min, port_max) |
476 | |
477 | + def pick_choice(self, choices, but_not=None): |
478 | + """Pick a random item from `choices`. |
479 | + |
480 | + :param choices: A sequence of choices in Django form choices format: |
481 | + [ |
482 | + ('choice_id_1', "Choice name 1"), |
483 | + ('choice_id_2', "Choice name 2"), |
484 | + ] |
485 | + :param but_not: A list of choices' IDs to exclude. |
486 | + :type but_not: Sequence. |
487 | + :return: The "id" portion of a random choice out of `choices`. |
488 | + """ |
489 | + if but_not is None: |
490 | + but_not = () |
491 | + return random.choice( |
492 | + [choice for choice in choices if choice[0] not in but_not])[0] |
493 | + |
494 | def make_vlan_tag(self, allow_none=False, *, but_not=EMPTY_SET): |
495 | """Create a random VLAN tag. |
496 | |
497 | diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py |
498 | index 602b893..654b46c 100644 |
499 | --- a/src/provisioningserver/drivers/pod/__init__.py |
500 | +++ b/src/provisioningserver/drivers/pod/__init__.py |
501 | @@ -172,6 +172,21 @@ class BlockDeviceType: |
502 | ISCSI = 'iscsi' |
503 | |
504 | |
505 | +class InterfaceAttachType: |
506 | + """Different interface attachment types.""" |
507 | + |
508 | + # Interface attached to a network predefined in the hypervisor. |
509 | + # (This is the default if no constraints are specified; MAAS will look for |
510 | + # a 'maas' network, and then fall back to a 'default' network.) |
511 | + NETWORK = 'network' |
512 | + |
513 | + # Interface attached to a bridge interface on the hypervisor. |
514 | + BRIDGE = 'bridge' |
515 | + |
516 | + # Interface attached via a non-bridge interface using the macvlan driver. |
517 | + MACVLAN = 'macvlan' |
518 | + |
519 | + |
520 | class AttrHelperMixin: |
521 | """Mixin to add the `fromdict` and `asdict` to the classes.""" |
522 | |
523 | diff --git a/src/provisioningserver/drivers/pod/tests/test_virsh.py b/src/provisioningserver/drivers/pod/tests/test_virsh.py |
524 | index fe4a766..c109c5c 100644 |
525 | --- a/src/provisioningserver/drivers/pod/tests/test_virsh.py |
526 | +++ b/src/provisioningserver/drivers/pod/tests/test_virsh.py |
527 | @@ -29,6 +29,7 @@ import pexpect |
528 | from provisioningserver.drivers.pod import ( |
529 | Capabilities, |
530 | DiscoveredPodStoragePool, |
531 | + InterfaceAttachType, |
532 | RequestedMachine, |
533 | RequestedMachineBlockDevice, |
534 | RequestedMachineInterface, |
535 | @@ -45,6 +46,7 @@ from provisioningserver.drivers.pod.virsh import ( |
536 | DOM_TEMPLATE_S390X, |
537 | VirshPodDriver, |
538 | ) |
539 | +from provisioningserver.enum import MACVLAN_MODE_CHOICES |
540 | from provisioningserver.rpc.exceptions import PodInvalidResources |
541 | from provisioningserver.utils.shell import ( |
542 | get_env_with_locale, |
543 | @@ -1275,8 +1277,8 @@ class TestVirshSSH(MAASTestCase): |
544 | fake_mac = factory.make_mac_address() |
545 | interface = RequestedMachineInterface( |
546 | attach_name=factory.make_name('name'), |
547 | - attach_type='macvlan', attach_options=random.choice([ |
548 | - 'private', 'vepa', 'bridge', 'passthru'])) |
549 | + attach_type=InterfaceAttachType.MACVLAN, |
550 | + attach_options=factory.pick_choice(MACVLAN_MODE_CHOICES)) |
551 | self.patch(virsh, 'generate_mac_address').return_value = fake_mac |
552 | NamedTemporaryFile = self.patch(virsh_module, "NamedTemporaryFile") |
553 | tmpfile = NamedTemporaryFile.return_value |
554 | @@ -1309,8 +1311,8 @@ class TestVirshSSH(MAASTestCase): |
555 | fake_mac = factory.make_mac_address() |
556 | interface = RequestedMachineInterface( |
557 | attach_name=factory.make_name('ifname'), |
558 | - attach_type='bridge', attach_options=random.choice([ |
559 | - 'private', 'vepa', 'bridge', 'passthru'])) |
560 | + attach_type=InterfaceAttachType.BRIDGE, |
561 | + attach_options=factory.pick_choice(MACVLAN_MODE_CHOICES)) |
562 | self.patch(virsh, 'generate_mac_address').return_value = fake_mac |
563 | NamedTemporaryFile = self.patch(virsh_module, "NamedTemporaryFile") |
564 | tmpfile = NamedTemporaryFile.return_value |
565 | diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py |
566 | index 42914c0..b16e0c4 100644 |
567 | --- a/src/provisioningserver/drivers/pod/virsh.py |
568 | +++ b/src/provisioningserver/drivers/pod/virsh.py |
569 | @@ -29,6 +29,7 @@ from provisioningserver.drivers.pod import ( |
570 | DiscoveredPod, |
571 | DiscoveredPodHints, |
572 | DiscoveredPodStoragePool, |
573 | + InterfaceAttachType, |
574 | PodDriver, |
575 | ) |
576 | from provisioningserver.logger import get_maas_logger |
577 | @@ -853,7 +854,7 @@ class VirshSSH(pexpect.spawn): |
578 | """Attach new network interface on `domain` to `network`.""" |
579 | mac = generate_mac_address() |
580 | # If attachment type is not specified, default to network. |
581 | - if interface.attach_type in (None, 'network'): |
582 | + if interface.attach_type in (None, InterfaceAttachType.NETWORK): |
583 | # Set the network if we are explicity attaching a network |
584 | # specified by the user. |
585 | if interface.attach_type is not None: |
586 | @@ -869,10 +870,10 @@ class VirshSSH(pexpect.spawn): |
587 | 'mac_address': mac, |
588 | 'attach_name': interface.attach_name, |
589 | } |
590 | - if interface.attach_type == 'macvlan': |
591 | + if interface.attach_type == InterfaceAttachType.MACVLAN: |
592 | device_params['attach_options'] = interface.attach_options |
593 | device_xml = DOM_TEMPLATE_MACVLAN_INTERFACE.format(**device_params) |
594 | - if interface.attach_type == 'bridge': |
595 | + elif interface.attach_type == InterfaceAttachType.BRIDGE: |
596 | device_xml = DOM_TEMPLATE_BRIDGE_INTERFACE.format(**device_params) |
597 | |
598 | # Rewrite the XML in a temporary file to use with 'virsh define'. |
599 | diff --git a/src/provisioningserver/enum.py b/src/provisioningserver/enum.py |
600 | new file mode 100644 |
601 | index 0000000..ef8ff9d |
602 | --- /dev/null |
603 | +++ b/src/provisioningserver/enum.py |
604 | @@ -0,0 +1,30 @@ |
605 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
606 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
607 | + |
608 | +"""Enumerations meaningful to the rack contoller (and possibly the region).""" |
609 | + |
610 | +__all__ = [ |
611 | + "MACVLAN_MODE", |
612 | + "MACVLAN_MODE_CHOICES", |
613 | +] |
614 | + |
615 | +# *** IMPORTANT *** |
616 | +# Note to all ye who enter here: comments beginning with #: are special |
617 | +# to Sphinx. They are extracted and form part of the documentation of |
618 | +# the field they directly precede. |
619 | + |
620 | + |
621 | +class MACVLAN_MODE: |
622 | + |
623 | + BRIDGE = "bridge" |
624 | + PASSTHRU = "passthru" |
625 | + PRIVATE = "private" |
626 | + VEPA = "vepa" |
627 | + |
628 | + |
629 | +MACVLAN_MODE_CHOICES = ( |
630 | + (MACVLAN_MODE.BRIDGE, "bridge"), |
631 | + (MACVLAN_MODE.PASSTHRU, "passthru"), |
632 | + (MACVLAN_MODE.PRIVATE, "private"), |
633 | + (MACVLAN_MODE.VEPA, "vepa"), |
634 | +) |
We should be using MACVLAN_MODE and MACVLAN_ MODE_CHOICES from maasserver.enum in several places. See inline comments.