Merge ~mpontillo/maas:compose-machine-form--interpret-interfaces-constraint into maas:master

Proposed by Mike Pontillo
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)
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.

To post a comment you must log in.
559c5d8... by Mike Pontillo

Format imports.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

We should be using MACVLAN_MODE and MACVLAN_MODE_CHOICES from maasserver.enum in several places. See inline comments.

review: Needs Fixing
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.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Looks good. Thanks for the fixes!

review: Approve
df002f8... by Mike Pontillo

Properly format imports.

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

UNIT TESTS
-b compose-machine-form--interpret-interfaces-constraint lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3461/console
COMMIT: ac287a5cba10e8ec323883f10b6c36c93ac761bd

review: Needs Fixing
7a78b35... by Mike Pontillo

Move common enums into provisioningserver code.

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

UNIT TESTS
-b compose-machine-form--interpret-interfaces-constraint lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3477/console
COMMIT: f61551b9847df0ae77e750b72ae456ce11d8e74b

review: Needs Fixing
4d7ee79... by Mike Pontillo

Fix merge conflict.

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

UNIT TESTS
-b compose-machine-form--interpret-interfaces-constraint lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 4d7ee797ccb3e36258ba203c7d44e01b9e5f9e83

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/tests/test_pods.py b/src/maasserver/api/tests/test_pods.py
2index 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
25diff --git a/src/maasserver/enum.py b/src/maasserver/enum.py
26index 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-)
58diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py
59index 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."""
196diff --git a/src/maasserver/forms/tests/test_pods.py b/src/maasserver/forms/tests/test_pods.py
197index 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()
421diff --git a/src/maasserver/models/bmc.py b/src/maasserver/models/bmc.py
422index 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
441diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
442index 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
469diff --git a/src/maastesting/factory.py b/src/maastesting/factory.py
470index 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
497diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py
498index 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
523diff --git a/src/provisioningserver/drivers/pod/tests/test_virsh.py b/src/provisioningserver/drivers/pod/tests/test_virsh.py
524index 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
565diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py
566index 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'.
599diff --git a/src/provisioningserver/enum.py b/src/provisioningserver/enum.py
600new file mode 100644
601index 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+)

Subscribers

People subscribed via source and target branches