Merge lp:~mpontillo/maas/bridge-interface-model-and-region-contract into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 4830
Proposed branch: lp:~mpontillo/maas/bridge-interface-model-and-region-contract
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 2189 lines (+1258/-242)
21 files modified
src/maasserver/api/tests/test_interfaces.py (+2/-2)
src/maasserver/enum.py (+2/-0)
src/maasserver/forms_interface.py (+131/-73)
src/maasserver/migrations/builtin/maasserver/0046_add_bridge_interface_type.py (+44/-0)
src/maasserver/models/__init__.py (+2/-0)
src/maasserver/models/interface.py (+154/-33)
src/maasserver/models/node.py (+90/-58)
src/maasserver/models/signals/interfaces.py (+7/-5)
src/maasserver/models/signals/tests/test_interfaces.py (+8/-3)
src/maasserver/models/tests/test_interface.py (+122/-2)
src/maasserver/models/tests/test_node.py (+388/-1)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+20/-15)
src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+83/-6)
src/maasserver/static/partials/node-details.html (+1/-1)
src/maasserver/testing/factory.py (+3/-0)
src/maasserver/tests/test_forms_interface.py (+190/-3)
src/provisioningserver/utils/__init__.py (+0/-3)
src/provisioningserver/utils/ipaddr.py (+1/-12)
src/provisioningserver/utils/network.py (+6/-0)
src/provisioningserver/utils/tests/test_ipaddr.py (+1/-23)
src/provisioningserver/utils/tests/test_network.py (+3/-2)
To merge this branch: bzr merge lp:~mpontillo/maas/bridge-interface-model-and-region-contract
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+289751@code.launchpad.net

Commit message

Add BridgeInterface model and update region contract with rack.

 * Remove workaround that filtered interfaces with no parents on
   the rack. (they are now filtered on the region)
 * Create initial BridgeInterface model. Update enums.
 * Extract abstract base class for all interfaces with parents
   (ChildInterface). Create a corresponding ChildInterfaceForm.
 * Refactor rack code to report bridges to the region.
 * Minor changes to Javascript code to ensure bridges are
   consistently displayed (similar to bonds).

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Wow, thats a lot of code. I would have preferred to see this in a few branches.

1. The model change.
2. The form change.
3. The rack interfaces change.
4. The UI change.

This really is 4 branches in 1. The good thing about it is this looks really, really good. You have really covered all the areas, including the UI which is great! Just a few comments that need to be fixed, look like just copy and paste failures.

Really with this branch I wonder how far it is just to allow users to create bridges. Curtin already supports the ability.

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I think this is missing a migration to add the bridge type to interfaces. Not going to block you on it as its simple to add and generated anyway.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the review. I think I addressed most of your comments, with the exception of logging a warning rather than throwing an exception if we can't find all of the parents an interface depends on in the database. As discussed on IRC, we'll leave this for a future branch, since we don't want bridges that include tap interfaces to throw exceptions. (we'll consider filtering them in other ways, but the topic needs more investigation than we should cover in this branch)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/tests/test_interfaces.py'
2--- src/maasserver/api/tests/test_interfaces.py 2016-03-07 23:20:52 +0000
3+++ src/maasserver/api/tests/test_interfaces.py 2016-03-24 18:53:09 +0000
4@@ -291,7 +291,7 @@
5 self.assertEqual({
6 "mac_address": [
7 "This MAC address is already in use by %s." % (
8- interface_on_other_node.node.hostname)],
9+ interface_on_other_node.get_log_string())],
10 }, json_load_bytes(response.content))
11
12 def test_create_bond(self):
13@@ -416,7 +416,7 @@
14 self.assertEqual({
15 "mac_address": ["This field cannot be blank."],
16 "name": ["This field is required."],
17- "parents": ["A Bond interface must have one or more parents."],
18+ "parents": ["A bond interface must have one or more parents."],
19 }, json_load_bytes(response.content))
20
21 def test_create_vlan(self):
22
23=== modified file 'src/maasserver/enum.py'
24--- src/maasserver/enum.py 2016-03-18 18:50:58 +0000
25+++ src/maasserver/enum.py 2016-03-24 18:53:09 +0000
26@@ -512,6 +512,7 @@
27 # be changed.
28 PHYSICAL = 'physical'
29 BOND = 'bond'
30+ BRIDGE = 'bridge'
31 VLAN = 'vlan'
32 ALIAS = 'alias'
33 # Interface that is created when it is not linked to a node.
34@@ -521,6 +522,7 @@
35 INTERFACE_TYPE_CHOICES = (
36 (INTERFACE_TYPE.PHYSICAL, "Physical interface"),
37 (INTERFACE_TYPE.BOND, "Bond"),
38+ (INTERFACE_TYPE.BRIDGE, "Bridge"),
39 (INTERFACE_TYPE.VLAN, "VLAN interface"),
40 (INTERFACE_TYPE.ALIAS, "Alias"),
41 (INTERFACE_TYPE.UNKNOWN, "Unknown"),
42
43=== modified file 'src/maasserver/forms_interface.py'
44--- src/maasserver/forms_interface.py 2016-03-07 23:20:52 +0000
45+++ src/maasserver/forms_interface.py 2016-03-24 18:53:09 +0000
46@@ -24,6 +24,7 @@
47 )
48 from maasserver.models.interface import (
49 BondInterface,
50+ BridgeInterface,
51 build_vlan_interface_name,
52 Interface,
53 InterfaceRelationship,
54@@ -268,7 +269,94 @@
55 return cleaned_data
56
57
58-class BondInterfaceForm(InterfaceForm):
59+class ChildInterfaceForm(InterfaceForm):
60+ """Form used to create "child" interfaces (that is, interfaces which
61+ require their "parent" interfaces in order to exist, such as bonds and
62+ bridges.
63+ """
64+
65+ def __init__(self, *args, **kwargs):
66+ super().__init__(*args, **kwargs)
67+ # Allow VLAN to be blank when creating.
68+ instance = kwargs.get("instance", None)
69+ if instance is not None and instance.id is not None:
70+ self.fields['vlan'].required = True
71+ else:
72+ self.fields['vlan'].required = False
73+
74+ def clean_parents(self):
75+ """Validate that child interfaces cannot be created unless at least one
76+ parent is present.
77+ """
78+ parents = self.get_clean_parents()
79+ if parents is None:
80+ return
81+ # Ensure support for parthenogenesis.
82+ if len(parents) < 1:
83+ raise ValidationError(
84+ "A %s interface must have one or more parents." %
85+ self.Meta.model.get_type())
86+ return parents
87+
88+ def _set_default_child_mac(self, parents):
89+ """Sets the value of self.cleaned_data['mac_address'] based on either
90+ the first parent (if the child interface is new), or a remaining parent
91+ (if a parent with the current MAC was removed).
92+ """
93+ if self.instance.id is not None:
94+ parent_macs = {
95+ parent.mac_address.get_raw(): parent
96+ for parent in self.instance.parents.all()
97+ }
98+ else:
99+ parent_macs = {}
100+ mac_not_changed = (
101+ self.instance.id is not None and
102+ self.cleaned_data["mac_address"] == self.instance.mac_address
103+ )
104+ if self.instance.id is None and 'mac_address' not in self.data:
105+ # New bond without mac_address set, set it to the first
106+ # parent mac_address.
107+ self.cleaned_data['mac_address'] = str(
108+ parents[0].mac_address)
109+ elif (mac_not_changed and
110+ self.instance.mac_address in parent_macs and
111+ parent_macs[self.instance.mac_address] not in parents):
112+ # Updating child where its mac_address comes from its parent
113+ # and that parent is no longer part of this child. Update
114+ # the mac_address to be one of the new parent MAC
115+ # addresses.
116+ self.cleaned_data['mac_address'] = str(
117+ parents[0].mac_address)
118+
119+ def _set_default_vlan(self, parents):
120+ """When creating the child, set VLAN to the same as the first parent
121+ by default.
122+ """
123+ if self.instance.id is None:
124+ vlan = self.cleaned_data.get('vlan')
125+ if vlan is None:
126+ vlan = parents[0].vlan
127+ self.cleaned_data['vlan'] = vlan
128+
129+ def _validate_parental_fidelity(self, parents):
130+ """Check that all of the parent interfaces are not already in a
131+ relationship before committing them to this child.
132+ """
133+ parents_with_other_children = {
134+ parent.name
135+ for parent in parents
136+ for rel in parent.children_relationships.all()
137+ if rel.child.id != self.instance.id
138+ }
139+ if parents_with_other_children:
140+ set_form_error(
141+ self, 'parents',
142+ "Interfaces already in-use: %s." % (
143+ ', '.join(sorted(parents_with_other_children))))
144+
145+
146+class BondInterfaceForm(ChildInterfaceForm):
147 """Form used to create/edit a bond interface."""
148
149 bond_mode = forms.ChoiceField(
150@@ -305,24 +393,6 @@
151 'name',
152 )
153
154- def __init__(self, *args, **kwargs):
155- super(BondInterfaceForm, self).__init__(*args, **kwargs)
156- # Allow VLAN to be blank when creating.
157- instance = kwargs.get("instance", None)
158- if instance is not None and instance.id is not None:
159- self.fields['vlan'].required = True
160- else:
161- self.fields['vlan'].required = False
162-
163- def clean_parents(self):
164- parents = self.get_clean_parents()
165- if parents is None:
166- return
167- if len(parents) < 1:
168- raise ValidationError(
169- "A Bond interface must have one or more parents.")
170- return parents
171-
172 def clean_vlan(self):
173 new_vlan = self.cleaned_data.get('vlan')
174 if new_vlan and new_vlan.fabric.get_default_vlan() != new_vlan:
175@@ -331,70 +401,35 @@
176 return new_vlan
177
178 def clean(self):
179- cleaned_data = super(BondInterfaceForm, self).clean()
180+ cleaned_data = super().clean()
181 if self.fields_ok(['vlan', 'parents']):
182 parents = self.cleaned_data.get('parents')
183 # Set the mac_address if its missing and the interface is being
184 # created.
185 if parents:
186- if self.instance.id is not None:
187- parent_macs = {
188- parent.mac_address.get_raw(): parent
189- for parent in self.instance.parents.all()
190- }
191- mac_not_changed = (
192- self.instance.id is not None and
193- self.cleaned_data["mac_address"] == (
194- self.instance.mac_address))
195- if self.instance.id is None and 'mac_address' not in self.data:
196- # New bond without mac_address set, set it to the first
197- # parent mac_address.
198- self.cleaned_data['mac_address'] = str(
199- parents[0].mac_address)
200- elif (mac_not_changed and
201- self.instance.mac_address in parent_macs and
202- parent_macs[self.instance.mac_address] not in parents):
203- # Updating bond where its mac_address comes from its parent
204- # and that parent is no longer part of this bond. Update
205- # the mac_address to be one of the new parent MAC
206- # addresses.
207- self.cleaned_data['mac_address'] = str(
208- parents[0].mac_address)
209+ self._set_default_child_mac(parents)
210+ self._validate_parental_fidelity(parents)
211+ self._set_default_vlan(parents)
212+ self._validate_parent_vlans_match(parents)
213+ return cleaned_data
214
215- # Check that all of the parents are not already in use.
216- parents_with_other_children = {
217- parent.name
218- for parent in parents
219- for rel in parent.children_relationships.all()
220- if rel.child.id != self.instance.id
221+ def _validate_parent_vlans_match(self, parents):
222+ # When creating the bond set VLAN to the same as the parents
223+ # and check that the parents all belong to the same VLAN.
224+ if self.instance.id is None:
225+ vlan = self.cleaned_data.get('vlan')
226+ parent_vlans = {
227+ parent.vlan
228+ for parent in parents
229 }
230- if parents_with_other_children:
231- set_form_error(
232- self, 'parents',
233- "%s is already in-use by another interface." % (
234- ', '.join(sorted(parents_with_other_children))))
235-
236- # When creating the bond set VLAN to the same as the parents
237- # and check that the parents all belong to the same VLAN.
238- if self.instance.id is None:
239- vlan = self.cleaned_data.get('vlan')
240- if vlan is None:
241- vlan = parents[0].vlan
242- self.cleaned_data['vlan'] = vlan
243- parent_vlans = {
244- parent.vlan
245- for parent in parents
246- }
247- if parent_vlans != set([vlan]):
248- set_form_error(
249- self, 'parents',
250- "All parents must belong to the same VLAN.")
251-
252- return cleaned_data
253+ if parent_vlans != set([vlan]):
254+ set_form_error(
255+ self, 'parents',
256+ "All parents must belong to the same VLAN.")
257
258 def set_extra_parameters(self, interface, created):
259 """Set the bond parameters as well."""
260- super(BondInterfaceForm, self).set_extra_parameters(interface, created)
261+ super().set_extra_parameters(interface, created)
262 # Set all the bond_* parameters.
263 bond_fields = [
264 field_name
265@@ -414,8 +449,31 @@
266 interface.params[bond_field] = self.fields[bond_field].initial
267
268
269+class BridgeInterfaceForm(ChildInterfaceForm):
270+ """Form used to create/edit a bridge interface."""
271+
272+ class Meta:
273+ model = BridgeInterface
274+ fields = InterfaceForm.Meta.fields + (
275+ 'mac_address',
276+ 'name',
277+ )
278+
279+ def clean(self):
280+ cleaned_data = super().clean()
281+ if self.fields_ok(['vlan', 'parents']):
282+ parents = self.cleaned_data.get('parents')
283+ # Set the mac_address if its missing and the interface is being
284+ # created.
285+ if parents:
286+ self._set_default_child_mac(parents)
287+ self._validate_parental_fidelity(parents)
288+ self._set_default_vlan(parents)
289+ return cleaned_data
290+
291 INTERFACE_FORM_MAPPING = {
292 INTERFACE_TYPE.PHYSICAL: PhysicalInterfaceForm,
293 INTERFACE_TYPE.VLAN: VLANInterfaceForm,
294 INTERFACE_TYPE.BOND: BondInterfaceForm,
295+ INTERFACE_TYPE.BRIDGE: BridgeInterfaceForm,
296 }
297
298=== added file 'src/maasserver/migrations/builtin/maasserver/0046_add_bridge_interface_type.py'
299--- src/maasserver/migrations/builtin/maasserver/0046_add_bridge_interface_type.py 1970-01-01 00:00:00 +0000
300+++ src/maasserver/migrations/builtin/maasserver/0046_add_bridge_interface_type.py 2016-03-24 18:53:09 +0000
301@@ -0,0 +1,44 @@
302+# -*- coding: utf-8 -*-
303+from __future__ import unicode_literals
304+
305+from django.db import (
306+ migrations,
307+ models,
308+)
309+
310+
311+class Migration(migrations.Migration):
312+
313+ dependencies = [
314+ ('maasserver', '0045_add_node_to_filesystem'),
315+ ]
316+
317+ operations = [
318+ migrations.CreateModel(
319+ name='ChildInterface',
320+ fields=[
321+ ],
322+ options={
323+ 'abstract': False,
324+ 'proxy': True,
325+ },
326+ bases=('maasserver.interface',),
327+ ),
328+ migrations.AlterField(
329+ model_name='interface',
330+ name='type',
331+ field=models.CharField(choices=[('physical', 'Physical interface'), ('bond', 'Bond'), ('bridge', 'Bridge'), ('vlan', 'VLAN interface'), ('alias', 'Alias'), ('unknown', 'Unknown')], editable=False, max_length=20),
332+ ),
333+ migrations.CreateModel(
334+ name='BridgeInterface',
335+ fields=[
336+ ],
337+ options={
338+ 'verbose_name': 'Bridge',
339+ 'abstract': False,
340+ 'verbose_name_plural': 'Bridges',
341+ 'proxy': True,
342+ },
343+ bases=('maasserver.childinterface',),
344+ ),
345+ ]
346
347=== modified file 'src/maasserver/models/__init__.py'
348--- src/maasserver/models/__init__.py 2016-03-11 00:37:08 +0000
349+++ src/maasserver/models/__init__.py 2016-03-24 18:53:09 +0000
350@@ -102,6 +102,7 @@
351 )
352 from maasserver.models.interface import (
353 BondInterface,
354+ BridgeInterface,
355 Interface,
356 PhysicalInterface,
357 UnknownInterface,
358@@ -149,6 +150,7 @@
359 BMC,
360 Bcache,
361 BondInterface,
362+ BridgeInterface,
363 BootResource,
364 BootResourceFile,
365 BootResourceSet,
366
367=== modified file 'src/maasserver/models/interface.py'
368--- src/maasserver/models/interface.py 2016-03-10 23:04:24 +0000
369+++ src/maasserver/models/interface.py 2016-03-24 18:53:09 +0000
370@@ -210,6 +210,12 @@
371 else:
372 return qs.filter(type=interface_type)
373
374+ def get_interfaces_on_node_by_name(self, node, interface_names):
375+ """Returns a list of Inteface objects on the specified node whose
376+ names match the specified list of interface names.
377+ """
378+ return list(self.filter(node=node, name__in=interface_names))
379+
380 def get_by_ip(self, static_ip_address):
381 """Given the specified StaticIPAddress, return the Interface it's on.
382 """
383@@ -280,6 +286,52 @@
384 interface.save()
385 return interface, created
386
387+ def get_or_create_on_node(self, node, name, mac_address, parent_nics):
388+ """Create an interface on the specified node, with the specified MAC
389+ address and parent NICs.
390+
391+ This method is necessary because get_or_create() often offers too
392+ simplistic an approach to interface matching. For example, if an
393+ interface has been moved, its MAC has changed, or its dependencies
394+ on other interfaces have changed.
395+
396+ This method attempts to update an existing interface, if a
397+ match can be found. Otherwise, a new interface will be created.
398+
399+ If the interface being created is a replacement for an interface that
400+ already exists, the caller is responsible for deleting it.
401+ """
402+ interface = self.get_queryset().filter(
403+ Q(mac_address=mac_address) | Q(name=name) & Q(node=node)).first()
404+ if interface is not None:
405+ if interface.type != self.model.get_type():
406+ # This means we found the interface on this node, but the type
407+ # didn't match what we expected. This should not happen unless
408+ # we changed the modeling of this interface type, or the admin
409+ # intentionally changed the interface type.
410+ interface.delete()
411+ return self.get_or_create_on_node(
412+ node, name, mac_address, parent_nics)
413+ interface.mac_address = mac_address
414+ interface.name = name
415+ interface.parents.clear()
416+ for parent_nic in parent_nics:
417+ InterfaceRelationship(
418+ child=interface, parent=parent_nic).save()
419+ if interface.node.id != node.id:
420+ # Bond with MAC address was on a different node. We need to
421+ # move it to its new owner. In the process we delete all of its
422+ # current links because they are completely wrong.
423+ interface.ip_addresses.all().delete()
424+ interface.node = node
425+ else:
426+ interface = self.create(
427+ name=name, mac_address=mac_address, node=node)
428+ for parent_nic in parent_nics:
429+ InterfaceRelationship(
430+ child=interface, parent=parent_nic).save()
431+ return interface
432+
433
434 class Interface(CleanSave, TimestampedModel):
435
436@@ -356,7 +408,7 @@
437 node = self.get_node()
438 if node is not None:
439 hostname = node.hostname
440- return "%s on %s" % (self.get_name(), hostname)
441+ return "%s (%s) on %s" % (self.get_name(), self.type, hostname)
442
443 def get_name(self):
444 return self.name
445@@ -1021,8 +1073,8 @@
446 raise ValidationError({
447 "mac_address": [
448 "This MAC address is already in use by %s." % (
449- other_interfaces[0].node.hostname)]
450- })
451+ other_interfaces[0].get_log_string())]
452+ })
453
454 # No parents are allow for a physical interface.
455 if self.id is not None:
456@@ -1030,19 +1082,16 @@
457 if len(self.parents.all()) > 0:
458 raise ValidationError({
459 "parents": ["A physical interface cannot have parents."]
460- })
461-
462-
463-class BondInterface(Interface):
464+ })
465+
466+
467+class ChildInterface(Interface):
468+ """Abstract class to represent interfaces which require parents in order
469+ to operate.
470+ """
471
472 class Meta(Interface.Meta):
473 proxy = True
474- verbose_name = "Bond"
475- verbose_name_plural = "Bonds"
476-
477- @classmethod
478- def get_type(self):
479- return INTERFACE_TYPE.BOND
480
481 def get_node(self):
482 if self.id is None:
483@@ -1064,14 +1113,14 @@
484 }
485 return True in is_enabled
486
487- def clean(self):
488- super(BondInterface, self).clean()
489- # Validate that the MAC address is not None.
490- if not self.mac_address:
491- raise ValidationError({
492- "mac_address": ["This field cannot be blank."]
493- })
494+ def _validate_acceptable_parent_types(self, parent_types):
495+ """Raises a ValidationError if the interface has parents which are not
496+ allowed, given this interface type. (for example, only physical
497+ interfaces can be bonded, and bridges cannot bridge other bridges.)
498+ """
499+ raise NotImplementedError()
500
501+ def _validate_parent_interfaces(self):
502 # Parent interfaces on this bond must be from the same node and can
503 # only be physical interfaces.
504 if self.id is not None:
505@@ -1083,16 +1132,14 @@
506 raise ValidationError({
507 "parents": [
508 "Parent interfaces do not belong to the same node."]
509- })
510+ })
511 parent_types = {
512 parent.get_type()
513 for parent in self.parents.all()
514 }
515- if parent_types != set([INTERFACE_TYPE.PHYSICAL]):
516- raise ValidationError({
517- "parents": ["Only physical interfaces can be bonded."]
518- })
519+ self._validate_acceptable_parent_types(parent_types)
520
521+ def _validate_unique_or_parent_mac(self):
522 # Validate that this bond interface is using either a new MAC address
523 # or a MAC address from one of its parents. This validation is only
524 # done once the interface has been saved once. That is because if its
525@@ -1128,7 +1175,73 @@
526 "mac_address": [
527 "This MAC address is already in use by %s." % (
528 bad_interfaces[0].node.hostname)]
529- })
530+ })
531+
532+
533+class BridgeInterface(ChildInterface):
534+
535+ class Meta(Interface.Meta):
536+ proxy = True
537+ verbose_name = "Bridge"
538+ verbose_name_plural = "Bridges"
539+
540+ @classmethod
541+ def get_type(self):
542+ return INTERFACE_TYPE.BRIDGE
543+
544+ def _validate_acceptable_parent_types(self, parent_types):
545+ """Validates that bridges cannot contain other bridges."""
546+ if INTERFACE_TYPE.BRIDGE in parent_types:
547+ raise ValidationError({
548+ "parents": ["Bridges cannot contain other bridges."]
549+ })
550+
551+ def clean(self):
552+ super().clean()
553+ # Validate that the MAC address is not None.
554+ if not self.mac_address:
555+ raise ValidationError({
556+ "mac_address": ["This field cannot be blank."]
557+ })
558+ self._validate_parent_interfaces()
559+ self._validate_unique_or_parent_mac()
560+
561+ def save(self, *args, **kwargs):
562+ # Set the node of this bond to the same as its parents.
563+ self.node = self.get_node()
564+ # Set the enabled status based on its parents.
565+ self.enabled = self.is_enabled()
566+ super().save(*args, **kwargs)
567+
568+
569+class BondInterface(ChildInterface):
570+
571+ class Meta(Interface.Meta):
572+ proxy = True
573+ verbose_name = "Bond"
574+ verbose_name_plural = "Bonds"
575+
576+ @classmethod
577+ def get_type(self):
578+ return INTERFACE_TYPE.BOND
579+
580+ def clean(self):
581+ super(BondInterface, self).clean()
582+ # Validate that the MAC address is not None.
583+ if not self.mac_address:
584+ raise ValidationError({
585+ "mac_address": ["This field cannot be blank."]
586+ })
587+
588+ self._validate_parent_interfaces()
589+ self._validate_unique_or_parent_mac()
590+
591+ def _validate_acceptable_parent_types(self, parent_types):
592+ """Validates that bonds only include physical interfaces."""
593+ if parent_types != {INTERFACE_TYPE.PHYSICAL}:
594+ raise ValidationError({
595+ "parents": ["Only physical interfaces can be bonded."]
596+ })
597
598 def save(self, *args, **kwargs):
599 # Set the node of this bond to the same as its parents.
600@@ -1195,13 +1308,20 @@
601 "parents": ["VLAN interface must have exactly one parent."]
602 })
603 parent = parents[0]
604- if parent.get_type() not in [
605- INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.BOND]:
606+ allowed_vlan_parent_types = (
607+ INTERFACE_TYPE.PHYSICAL,
608+ INTERFACE_TYPE.BOND,
609+ INTERFACE_TYPE.BRIDGE
610+ )
611+ if parent.get_type() not in allowed_vlan_parent_types:
612+ # XXX mpontillo 2016-06-23: we won't mention bridges in this
613+ # error message, since users can't configure bridges on nodes.
614 raise ValidationError({
615 "parents": [
616 "VLAN interface can only be created on a physical "
617- "or bond interface."]
618- })
619+ "or bond interface."
620+ ]
621+ })
622
623 def save(self, *args, **kwargs):
624 # Set the node of this VLAN to the same as its parents.
625@@ -1242,7 +1362,7 @@
626 if self.node is not None:
627 raise ValidationError({
628 "node": ["This field must be blank."]
629- })
630+ })
631
632 # No other interfaces can have this MAC address.
633 other_interfaces = Interface.objects.filter(
634@@ -1254,7 +1374,7 @@
635 raise ValidationError({
636 "mac_address": [
637 "This MAC address is already in use by %s." % (
638- other_interfaces[0].node.hostname)]
639+ other_interfaces[0].get_log_string())]
640 })
641
642 # Cannot have any parents.
643@@ -1264,7 +1384,7 @@
644 if len(parents) > 0:
645 raise ValidationError({
646 "parents": ["A unknown interface cannot have parents."]
647- })
648+ })
649
650
651 INTERFACE_TYPE_MAPPING = {
652@@ -1273,6 +1393,7 @@
653 [
654 PhysicalInterface,
655 BondInterface,
656+ BridgeInterface,
657 VLANInterface,
658 UnknownInterface,
659 ]
660
661=== modified file 'src/maasserver/models/node.py'
662--- src/maasserver/models/node.py 2016-03-17 19:57:42 +0000
663+++ src/maasserver/models/node.py 2016-03-24 18:53:09 +0000
664@@ -91,8 +91,8 @@
665 from maasserver.models.filesystemgroup import FilesystemGroup
666 from maasserver.models.interface import (
667 BondInterface,
668+ BridgeInterface,
669 Interface,
670- InterfaceRelationship,
671 PhysicalInterface,
672 VLANInterface,
673 )
674@@ -2426,8 +2426,9 @@
675 selecting the best possible default gateway IP. The criteria below
676 is used to select the best possible gateway:
677 1. Managed subnets over unmanaged subnets.
678- 2. Bond interfaces over physical interfaces.
679- 3. Node's boot interface over all other interfaces except bonds.
680+ 2. Bond and bridge interfaces over physical interfaces.
681+ 3. Node's boot interface over all other interfaces except bonds
682+ and bridges.
683 4. Physical interfaces over VLAN interfaces.
684 5. Sticky IP links over user reserved IP links.
685 6. User reserved IP links over auto IP links.
686@@ -2470,6 +2471,7 @@
687 vlan.dhcp_on DESC,
688 CASE
689 WHEN interface.type = 'bond' THEN 1
690+ WHEN interface.type = 'bridge' THEN 1
691 WHEN interface.type = 'physical' AND
692 interface.id = node.boot_interface_id THEN 2
693 WHEN interface.type = 'physical' THEN 3
694@@ -2983,10 +2985,10 @@
695 }))
696 for name in process_order:
697 interface = self._update_interface(name, interfaces[name])
698- if interface.id in current_interfaces:
699+ if interface is not None and interface.id in current_interfaces:
700 del current_interfaces[interface.id]
701
702- # Remove all the interfaces that no longer exits. We do this in reverse
703+ # Remove all the interfaces that no longer exist. We do this in reverse
704 # order so the child is deleted before the parent.
705 deletion_order = {}
706 for nic_id, nic in current_interfaces.items():
707@@ -3015,6 +3017,8 @@
708 return self._update_vlan_interface(name, config)
709 elif config["type"] == "bond":
710 return self._update_bond_interface(name, config)
711+ elif config["type"] == "bridge":
712+ return self._update_bridge_interface(name, config)
713 else:
714 raise ValueError(
715 "Unkwown interface type '%s' for '%s'." % (
716@@ -3156,6 +3160,52 @@
717 interface, config["links"], force_vlan=True)
718 return interface
719
720+ def _update_child_interface(self, name, config, child_type):
721+ """Update a child interface.
722+
723+ :param name: Name of the interface.
724+ :param config: Interface dictionary that was parsed from
725+ /etc/network/interfaces on the rack controller.
726+ """
727+ # Get all the parent interfaces for this interface. All the parents
728+ # should exists because of the order the links are processed.
729+ ifnames = config["parents"]
730+ parent_nics = Interface.objects.get_interfaces_on_node_by_name(
731+ self, ifnames)
732+
733+ # If we didn't create the parents yet, we need to know about it,
734+ # because that indicates a potentially serious bug.
735+ if len(config["parents"]) != len(parent_nics):
736+ maaslog.warning(
737+ "Could not find all parent interfaces for {ifname} "
738+ "on {node_name}. Found: {modeled_interfaces}; "
739+ "expected: {expected_interfaces}".format(
740+ ifname=name,
741+ node_name=self.hostname,
742+ modeled_interfaces=[
743+ iface.name for iface in parent_nics
744+ ].join(", "),
745+ expected_interfaces=ifnames.join(", ")))
746+
747+ # Ignore child interfaces that don't have parents. MAAS won't know what
748+ # to do with them since they can't be connected to a fabric.
749+ if len(parent_nics) == 0:
750+ return None
751+
752+ mac_address = config["mac_address"]
753+ interface = child_type.objects.get_or_create_on_node(
754+ self, name, mac_address, parent_nics)
755+
756+ links = config["links"]
757+ self._configure_vlan_from_links(interface, parent_nics, links)
758+
759+ # Update all the IP address on this interface. Fix the VLAN the
760+ # interface belongs to so its the same as the links and all parents to
761+ # be on the same VLAN.
762+ update_ip_addresses = self._update_links(interface, links)
763+ self._update_parent_vlans(interface, parent_nics, update_ip_addresses)
764+ return interface
765+
766 def _update_bond_interface(self, name, config):
767 """Update a bond interface.
768
769@@ -3163,67 +3213,49 @@
770 :param config: Interface dictionary that was parsed from
771 /etc/network/interfaces on the rack controller.
772 """
773- # Get all the parent interfaces for this bond. All the parents should
774- # exists because of the order the links are processed.
775- parent_nics = [
776- Interface.objects.get(node=self, name=parent_name)
777- for parent_name in config["parents"]
778- ]
779-
780- # Create the bond interface. get_or_create will not work as the way
781- # parents of a bond can be removed or the MAC address can change. Do
782- # the best possible to update the existing bond, if not a new one will
783- # be created and the old one will be removed in `update_interfaces`.
784- mac_address = config["mac_address"]
785- interface = BondInterface.objects.filter(
786- Q(mac_address=mac_address) | Q(name=name) & Q(node=self)).first()
787- if interface is not None:
788- interface.mac_address = mac_address
789- interface.name = name
790- interface.parents.clear()
791- for parent_nic in parent_nics:
792- InterfaceRelationship(
793- child=interface, parent=parent_nic).save()
794- if interface.node.id != self.id:
795- # Bond with MAC address was on a different node. We need to
796- # move it to its new owner. In the process we delete all of its
797- # current links because they are completely wrong.
798- interface.ip_addresses.all().delete()
799- interface.node = self
800- else:
801- interface = BondInterface.objects.create(
802- name=name, mac_address=mac_address, node=self)
803- for parent_nic in parent_nics:
804- InterfaceRelationship(
805- child=interface, parent=parent_nic).save()
806-
807- # Make sure that the VLAN on the bond is correct. When
808- # links exists on this bond we place it into the correct
809+ return self._update_child_interface(name, config, BondInterface)
810+
811+ def _update_bridge_interface(self, name, config):
812+ """Update a bridge interface.
813+
814+ :param name: Name of the interface.
815+ :param config: Interface dictionary that was parsed from
816+ /etc/network/interfaces on the rack controller.
817+ """
818+ return self._update_child_interface(name, config, BridgeInterface)
819+
820+ def _update_parent_vlans(
821+ self, interface, parent_nics, update_ip_addresses):
822+ """Given the specified interface model object, the specified list of
823+ parent interfaces, and the specified list of static IP addresses,
824+ update the parent interfaces to correspond to the VLAN found on the
825+ subnet the IP address is allocated from.
826+
827+ If a static IP address is allocated, give preferential treatment to
828+ the VLAN that IP address resides on.
829+ """
830+ linked_vlan = self._get_first_sticky_vlan_from_ip_addresses(
831+ update_ip_addresses)
832+ if linked_vlan is not None:
833+ interface.vlan = linked_vlan
834+ interface.save()
835+ for parent_nic in parent_nics:
836+ if parent_nic.vlan_id != linked_vlan.id:
837+ parent_nic.vlan = linked_vlan
838+ parent_nic.save()
839+
840+ def _configure_vlan_from_links(self, interface, parent_nics, links):
841+ # Make sure that the VLAN on the interface is correct. When
842+ # links exists on this interface we place it into the correct
843 # VLAN. If it cannot be determined it is placed on the same fabric
844 # as its first parent interface.
845- connected_to_subnets = self._get_connected_subnets(
846- config["links"])
847+ connected_to_subnets = self._get_connected_subnets(links)
848 if len(connected_to_subnets) == 0:
849 # Not connected to any known subnets. We add it to the same
850 # VLAN as its first parent.
851 interface.vlan = parent_nics[0].vlan
852 interface.save()
853
854- # Update all the IP address on this bond. Fix the VLAN the
855- # bond belongs to so its the same as the links and all parents to
856- # be on the same VLAN.
857- update_ip_addresses = self._update_links(interface, config["links"])
858- linked_vlan = self._get_first_sticky_vlan_from_ip_addresses(
859- update_ip_addresses)
860- if linked_vlan is not None:
861- interface.vlan = linked_vlan
862- interface.save()
863- for parent_nic in parent_nics:
864- if parent_nic.vlan_id != linked_vlan.id:
865- parent_nic.vlan = linked_vlan
866- parent_nic.save()
867- return interface
868-
869 def _get_connected_subnets(self, links):
870 """Return a set of subnets that `links` belongs to."""
871 subnets = set()
872
873=== modified file 'src/maasserver/models/signals/interfaces.py'
874--- src/maasserver/models/signals/interfaces.py 2016-03-07 23:20:52 +0000
875+++ src/maasserver/models/signals/interfaces.py 2016-03-24 18:53:09 +0000
876@@ -1,4 +1,4 @@
877-# Copyright 2015 Canonical Ltd. This software is licensed under the
878+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
879 # GNU Affero General Public License version 3 (see the file LICENSE).
880
881 """Respond to interface changes."""
882@@ -15,6 +15,7 @@
883 )
884 from maasserver.models import (
885 BondInterface,
886+ BridgeInterface,
887 Interface,
888 PhysicalInterface,
889 VLAN,
890@@ -27,6 +28,7 @@
891 Interface,
892 PhysicalInterface,
893 BondInterface,
894+ BridgeInterface,
895 VLANInterface,
896 ]
897
898@@ -128,9 +130,9 @@
899 klass, ['params'], delete=False)
900
901
902-def update_bond_parents(sender, instance, created, **kwargs):
903- """Update bond parents when interface created."""
904- if instance.type == INTERFACE_TYPE.BOND:
905+def update_interface_parents(sender, instance, created, **kwargs):
906+ """Update parents when an interface is created."""
907+ if instance.type in (INTERFACE_TYPE.BOND, INTERFACE_TYPE.BRIDGE):
908 for parent in instance.parents.all():
909 # Make sure the parent has not links as well, just to be sure.
910 parent.clear_all_links(clearing_config=True)
911@@ -141,7 +143,7 @@
912
913 for klass in INTERFACE_CLASSES:
914 signals.watch(
915- post_save, update_bond_parents,
916+ post_save, update_interface_parents,
917 sender=klass)
918
919
920
921=== modified file 'src/maasserver/models/signals/tests/test_interfaces.py'
922--- src/maasserver/models/signals/tests/test_interfaces.py 2016-03-07 23:20:52 +0000
923+++ src/maasserver/models/signals/tests/test_interfaces.py 2016-03-24 18:53:09 +0000
924@@ -126,14 +126,19 @@
925 }, reload_object(physical3_interface).params)
926
927
928-class TestUpdateBondParents(MAASServerTestCase):
929+class TestUpdateChildInterfaceParents(MAASServerTestCase):
930+
931+ scenarios = (
932+ ("bond", {"iftype": INTERFACE_TYPE.BOND}),
933+ ("bridge", {"iftype": INTERFACE_TYPE.BRIDGE}),
934+ )
935
936 def test__updates_bond_parents(self):
937 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
938 parent2 = factory.make_Interface(
939 INTERFACE_TYPE.PHYSICAL, node=parent1.node)
940 bond = factory.make_Interface(
941- INTERFACE_TYPE.BOND, parents=[parent1, parent2])
942+ self.iftype, parents=[parent1, parent2])
943 self.assertEqual(bond.vlan, reload_object(parent1).vlan)
944 self.assertEqual(bond.vlan, reload_object(parent2).vlan)
945
946@@ -143,7 +148,7 @@
947 INTERFACE_TYPE.PHYSICAL, node=parent1.node)
948 static_ip = factory.make_StaticIPAddress(interface=parent1)
949 factory.make_Interface(
950- INTERFACE_TYPE.BOND, parents=[parent1, parent2])
951+ self.iftype, parents=[parent1, parent2])
952 self.assertIsNone(reload_object(static_ip))
953
954
955
956=== modified file 'src/maasserver/models/tests/test_interface.py'
957--- src/maasserver/models/tests/test_interface.py 2016-03-12 03:19:19 +0000
958+++ src/maasserver/models/tests/test_interface.py 2016-03-24 18:53:09 +0000
959@@ -33,6 +33,7 @@
960 )
961 from maasserver.models.interface import (
962 BondInterface,
963+ BridgeInterface,
964 Interface,
965 PhysicalInterface,
966 UnknownInterface,
967@@ -682,7 +683,7 @@
968 self.assertEqual({
969 "mac_address": [
970 "This MAC address is already in use by %s." % (
971- interface.node.hostname)]
972+ interface.get_log_string())]
973 }, error.message_dict)
974
975 def test_cannot_have_parents(self):
976@@ -969,6 +970,125 @@
977 self.assertFalse(reload_object(interface).enabled)
978
979
980+class BridgeInterfaceTest(MAASServerTestCase):
981+
982+ def test_manager_returns_bridge_interfaces(self):
983+ node = factory.make_Node()
984+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
985+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
986+ interface = factory.make_Interface(
987+ INTERFACE_TYPE.BRIDGE, parents=[parent1, parent2])
988+ self.assertItemsEqual(
989+ [interface], BridgeInterface.objects.all())
990+
991+ def test_get_node_returns_parent_node(self):
992+ node = factory.make_Node()
993+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
994+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
995+ interface = factory.make_Interface(
996+ INTERFACE_TYPE.BRIDGE, parents=[parent1, parent2])
997+ self.assertItemsEqual(
998+ [interface], BridgeInterface.objects.all())
999+ self.assertEqual(node, interface.get_node())
1000+
1001+ def test_removed_if_underlying_interfaces_gets_removed(self):
1002+ node = factory.make_Node()
1003+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1004+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1005+ interface = factory.make_Interface(
1006+ INTERFACE_TYPE.BRIDGE, parents=[parent1, parent2])
1007+ parent1.delete()
1008+ parent2.delete()
1009+ self.assertIsNone(reload_object(interface))
1010+
1011+ def test_requires_mac_address(self):
1012+ interface = BridgeInterface(
1013+ name=factory.make_name("bridge"), node=factory.make_Node())
1014+ error = self.assertRaises(ValidationError, interface.save)
1015+ self.assertEqual({
1016+ "mac_address": ["This field cannot be blank."]
1017+ }, error.message_dict)
1018+
1019+ def test_parent_interfaces_must_belong_to_same_node(self):
1020+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1021+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1022+ error = self.assertRaises(
1023+ ValidationError, factory.make_Interface,
1024+ INTERFACE_TYPE.BRIDGE, parents=[parent1, parent2])
1025+ self.assertEqual({
1026+ "parents": ["Parent interfaces do not belong to the same node."]
1027+ }, error.message_dict)
1028+
1029+ def test_can_use_parents_mac_address(self):
1030+ node = factory.make_Node()
1031+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1032+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1033+ # Test is that no error is raised.
1034+ factory.make_Interface(
1035+ INTERFACE_TYPE.BRIDGE, mac_address=parent1.mac_address,
1036+ parents=[parent1, parent2])
1037+
1038+ def test_can_use_unique_mac_address(self):
1039+ node = factory.make_Node()
1040+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1041+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1042+ # Test is that no error is raised.
1043+ factory.make_Interface(
1044+ INTERFACE_TYPE.BRIDGE, mac_address=factory.make_mac_address(),
1045+ parents=[parent1, parent2])
1046+
1047+ def test_cannot_use_none_unique_mac_address(self):
1048+ node = factory.make_Node()
1049+ other_nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1050+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1051+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1052+ # Test is that no error is raised.
1053+ error = self.assertRaises(
1054+ ValidationError, factory.make_Interface,
1055+ INTERFACE_TYPE.BRIDGE, mac_address=other_nic.mac_address,
1056+ parents=[parent1, parent2])
1057+ self.assertEqual({
1058+ "mac_address": [
1059+ "This MAC address is already in use by %s." % (
1060+ other_nic.node.hostname)]
1061+ }, error.message_dict)
1062+
1063+ def test_node_is_set_to_parents_node(self):
1064+ node = factory.make_Node()
1065+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1066+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1067+ interface = factory.make_Interface(
1068+ INTERFACE_TYPE.BRIDGE, mac_address=factory.make_mac_address(),
1069+ parents=[parent1, parent2])
1070+ self.assertEqual(interface.node, parent1.node)
1071+
1072+ def test_disable_one_parent_doesnt_disable_the_bridge(self):
1073+ node = factory.make_Node()
1074+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1075+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1076+ interface = factory.make_Interface(
1077+ INTERFACE_TYPE.BRIDGE, mac_address=factory.make_mac_address(),
1078+ parents=[parent1, parent2])
1079+ parent1.enabled = False
1080+ parent1.save()
1081+ self.assertTrue(interface.is_enabled())
1082+ self.assertTrue(reload_object(interface).enabled)
1083+
1084+ def test_disable_all_parents_disables_the_bridge(self):
1085+ node = factory.make_Node()
1086+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1087+ parent2 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
1088+ interface = factory.make_Interface(
1089+ INTERFACE_TYPE.BRIDGE, mac_address=factory.make_mac_address(),
1090+ parents=[parent1, parent2])
1091+ parent1.enabled = False
1092+ parent1.save()
1093+ parent2.enabled = False
1094+ parent2.save()
1095+ self.assertFalse(interface.is_enabled())
1096+ self.assertFalse(reload_object(interface).enabled)
1097+
1098+
1099 class UnknownInterfaceTest(MAASServerTestCase):
1100
1101 def test_manager_returns_unknown_interfaces(self):
1102@@ -998,7 +1118,7 @@
1103 self.assertEqual({
1104 "mac_address": [
1105 "This MAC address is already in use by %s." % (
1106- interface.node.hostname)]
1107+ interface.get_log_string())]
1108 }, error.message_dict)
1109
1110
1111
1112=== modified file 'src/maasserver/models/tests/test_node.py'
1113--- src/maasserver/models/tests/test_node.py 2016-03-18 00:13:14 +0000
1114+++ src/maasserver/models/tests/test_node.py 2016-03-24 18:53:09 +0000
1115@@ -36,6 +36,7 @@
1116 from maasserver.models import (
1117 bmc as bmc_module,
1118 BondInterface,
1119+ BridgeInterface,
1120 Config,
1121 Device,
1122 Domain,
1123@@ -5310,6 +5311,53 @@
1124 [parent.name for parent in bond_interface.parents.all()],
1125 MatchesSetwise(Equals("eth0"), Equals("eth1")))
1126
1127+ def test__bridge_with_existing_parents(self):
1128+ rack = self.create_empty_rack_controller()
1129+ fabric = factory.make_Fabric()
1130+ vlan = fabric.get_default_vlan()
1131+ eth0 = factory.make_Interface(
1132+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1133+ eth1 = factory.make_Interface(
1134+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1135+ interfaces = {
1136+ "eth0": {
1137+ "type": "physical",
1138+ "mac_address": eth0.mac_address,
1139+ "parents": [],
1140+ "links": [],
1141+ "enabled": True,
1142+ },
1143+ "eth1": {
1144+ "type": "physical",
1145+ "mac_address": eth1.mac_address,
1146+ "parents": [],
1147+ "links": [],
1148+ "enabled": True,
1149+ },
1150+ "br0": {
1151+ "type": "bridge",
1152+ "mac_address": factory.make_mac_address(),
1153+ "parents": ["eth0", "eth1"],
1154+ "links": [],
1155+ "enabled": True,
1156+ },
1157+ }
1158+ rack.update_interfaces(interfaces)
1159+ self.assertThat(rack.interface_set.count(), Equals(3))
1160+ bond_interface = BridgeInterface.objects.get(
1161+ node=rack, mac_address=interfaces["br0"]["mac_address"])
1162+ self.assertThat(
1163+ bond_interface, MatchesStructure.byEquality(
1164+ type=INTERFACE_TYPE.BRIDGE,
1165+ name="br0",
1166+ mac_address=interfaces["br0"]["mac_address"],
1167+ enabled=True,
1168+ vlan=vlan,
1169+ ))
1170+ self.assertThat(
1171+ [parent.name for parent in bond_interface.parents.all()],
1172+ MatchesSetwise(Equals("eth0"), Equals("eth1")))
1173+
1174 def test__bond_updates_existing_bond(self):
1175 rack = self.create_empty_rack_controller()
1176 fabric = factory.make_Fabric()
1177@@ -5358,6 +5406,54 @@
1178 [parent.name for parent in bond0.parents.all()],
1179 Equals(["eth0"]))
1180
1181+ def test__bridge_updates_existing_bridge(self):
1182+ rack = self.create_empty_rack_controller()
1183+ fabric = factory.make_Fabric()
1184+ vlan = fabric.get_default_vlan()
1185+ eth0 = factory.make_Interface(
1186+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1187+ eth1 = factory.make_Interface(
1188+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1189+ br0 = factory.make_Interface(
1190+ INTERFACE_TYPE.BRIDGE, vlan=vlan, parents=[eth0, eth1], node=rack,
1191+ name="br0", mac_address=factory.make_mac_address())
1192+ interfaces = {
1193+ "eth0": {
1194+ "type": "physical",
1195+ "mac_address": eth0.mac_address,
1196+ "parents": [],
1197+ "links": [],
1198+ "enabled": True,
1199+ },
1200+ "eth1": {
1201+ "type": "physical",
1202+ "mac_address": eth1.mac_address,
1203+ "parents": [],
1204+ "links": [],
1205+ "enabled": True,
1206+ },
1207+ "br0": {
1208+ "type": "bridge",
1209+ "mac_address": factory.make_mac_address(),
1210+ "parents": ["eth0"],
1211+ "links": [],
1212+ "enabled": True,
1213+ },
1214+ }
1215+ rack.update_interfaces(interfaces)
1216+ self.assertThat(rack.interface_set.count(), Equals(3))
1217+ self.assertThat(
1218+ reload_object(br0), MatchesStructure.byEquality(
1219+ type=INTERFACE_TYPE.BRIDGE,
1220+ name="br0",
1221+ mac_address=interfaces["br0"]["mac_address"],
1222+ enabled=True,
1223+ vlan=vlan,
1224+ ))
1225+ self.assertThat(
1226+ [parent.name for parent in br0.parents.all()],
1227+ Equals(["eth0"]))
1228+
1229 def test__bond_moves_bond_with_mac_address(self):
1230 rack = self.create_empty_rack_controller()
1231 fabric = factory.make_Fabric()
1232@@ -5411,6 +5507,59 @@
1233 [parent.name for parent in bond_to_move.parents.all()],
1234 MatchesSetwise(Equals("eth0"), Equals("eth1")))
1235
1236+ def test__bridge_moves_bridge_with_mac_address(self):
1237+ rack = self.create_empty_rack_controller()
1238+ fabric = factory.make_Fabric()
1239+ vlan = fabric.get_default_vlan()
1240+ eth0 = factory.make_Interface(
1241+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1242+ eth1 = factory.make_Interface(
1243+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1244+ other_node = factory.make_Node()
1245+ other_nic = factory.make_Interface(
1246+ INTERFACE_TYPE.PHYSICAL, node=other_node)
1247+ bridge_mac_address = factory.make_mac_address()
1248+ bridge_to_move = factory.make_Interface(
1249+ INTERFACE_TYPE.BRIDGE, parents=[other_nic], node=other_node,
1250+ mac_address=bridge_mac_address)
1251+ interfaces = {
1252+ "eth0": {
1253+ "type": "physical",
1254+ "mac_address": eth0.mac_address,
1255+ "parents": [],
1256+ "links": [],
1257+ "enabled": True,
1258+ },
1259+ "eth1": {
1260+ "type": "physical",
1261+ "mac_address": eth1.mac_address,
1262+ "parents": [],
1263+ "links": [],
1264+ "enabled": True,
1265+ },
1266+ "br0": {
1267+ "type": "bridge",
1268+ "mac_address": bridge_mac_address,
1269+ "parents": ["eth0", "eth1"],
1270+ "links": [],
1271+ "enabled": True,
1272+ },
1273+ }
1274+ rack.update_interfaces(interfaces)
1275+ self.assertThat(rack.interface_set.count(), Equals(3))
1276+ self.assertThat(
1277+ reload_object(bridge_to_move), MatchesStructure.byEquality(
1278+ type=INTERFACE_TYPE.BRIDGE,
1279+ name="br0",
1280+ mac_address=bridge_mac_address,
1281+ enabled=True,
1282+ node=rack,
1283+ vlan=vlan,
1284+ ))
1285+ self.assertThat(
1286+ [parent.name for parent in bridge_to_move.parents.all()],
1287+ MatchesSetwise(Equals("eth0"), Equals("eth1")))
1288+
1289 def test__bond_creates_link_updates_parent_vlan(self):
1290 rack = self.create_empty_rack_controller()
1291 fabric = factory.make_Fabric()
1292@@ -5483,6 +5632,78 @@
1293 [parent.name for parent in bond0.parents.all()],
1294 MatchesSetwise(Equals("eth0"), Equals("eth1")))
1295
1296+ def test__bridge_creates_link_updates_parent_vlan(self):
1297+ rack = self.create_empty_rack_controller()
1298+ fabric = factory.make_Fabric()
1299+ vlan = fabric.get_default_vlan()
1300+ eth0 = factory.make_Interface(
1301+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1302+ eth1 = factory.make_Interface(
1303+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1304+ br0 = factory.make_Interface(
1305+ INTERFACE_TYPE.BRIDGE, parents=[eth0, eth1], vlan=vlan)
1306+ other_fabric = factory.make_Fabric()
1307+ br0_vlan = other_fabric.get_default_vlan()
1308+ subnet = factory.make_Subnet(vlan=br0_vlan)
1309+ ip = factory.pick_ip_in_Subnet(subnet)
1310+ interfaces = {
1311+ "eth0": {
1312+ "type": "physical",
1313+ "mac_address": eth0.mac_address,
1314+ "parents": [],
1315+ "links": [],
1316+ "enabled": True,
1317+ },
1318+ "eth1": {
1319+ "type": "physical",
1320+ "mac_address": eth1.mac_address,
1321+ "parents": [],
1322+ "links": [],
1323+ "enabled": True,
1324+ },
1325+ "br0": {
1326+ "type": "bridge",
1327+ "mac_address": br0.mac_address,
1328+ "parents": ["eth0", "eth1"],
1329+ "links": [{
1330+ "mode": "static",
1331+ "address": "%s/%d" % (
1332+ str(ip), subnet.get_ipnetwork().prefixlen),
1333+ }],
1334+ "enabled": True,
1335+ },
1336+ }
1337+ rack.update_interfaces(interfaces)
1338+ self.assertThat(rack.interface_set.count(), Equals(3))
1339+ self.assertThat(
1340+ reload_object(eth0), MatchesStructure.byEquality(
1341+ type=INTERFACE_TYPE.PHYSICAL,
1342+ name="eth0",
1343+ mac_address=eth0.mac_address,
1344+ enabled=True,
1345+ vlan=br0_vlan,
1346+ ))
1347+ self.assertThat(
1348+ reload_object(eth1), MatchesStructure.byEquality(
1349+ type=INTERFACE_TYPE.PHYSICAL,
1350+ name="eth1",
1351+ mac_address=eth1.mac_address,
1352+ enabled=True,
1353+ vlan=br0_vlan,
1354+ ))
1355+ self.assertThat(
1356+ reload_object(br0), MatchesStructure.byEquality(
1357+ type=INTERFACE_TYPE.BRIDGE,
1358+ name="br0",
1359+ mac_address=br0.mac_address,
1360+ enabled=True,
1361+ node=rack,
1362+ vlan=br0_vlan,
1363+ ))
1364+ self.assertThat(
1365+ [parent.name for parent in br0.parents.all()],
1366+ MatchesSetwise(Equals("eth0"), Equals("eth1")))
1367+
1368 def test__removes_missing_interfaces(self):
1369 rack = self.create_empty_rack_controller()
1370 fabric = factory.make_Fabric()
1371@@ -5529,6 +5750,37 @@
1372 self.assertThat(reload_object(eth1), Is(None))
1373 self.assertThat(reload_object(bond0), Not(Is(None)))
1374
1375+ def test__removes_one_bridge_parent(self):
1376+ rack = self.create_empty_rack_controller()
1377+ fabric = factory.make_Fabric()
1378+ vlan = fabric.get_default_vlan()
1379+ eth0 = factory.make_Interface(
1380+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1381+ eth1 = factory.make_Interface(
1382+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1383+ br0 = factory.make_Interface(
1384+ INTERFACE_TYPE.BRIDGE, parents=[eth0, eth1], vlan=vlan)
1385+ interfaces = {
1386+ "eth0": {
1387+ "type": "physical",
1388+ "mac_address": eth0.mac_address,
1389+ "parents": [],
1390+ "links": [],
1391+ "enabled": True,
1392+ },
1393+ "br0": {
1394+ "type": "bridge",
1395+ "mac_address": br0.mac_address,
1396+ "parents": ["eth0"],
1397+ "links": [],
1398+ "enabled": True,
1399+ },
1400+ }
1401+ rack.update_interfaces(interfaces)
1402+ self.assertThat(reload_object(eth0), Not(Is(None)))
1403+ self.assertThat(reload_object(eth1), Is(None))
1404+ self.assertThat(reload_object(br0), Not(Is(None)))
1405+
1406 def test__removes_one_bond_and_one_parent(self):
1407 rack = self.create_empty_rack_controller()
1408 fabric = factory.make_Fabric()
1409@@ -5553,7 +5805,31 @@
1410 self.assertThat(reload_object(eth1), Is(None))
1411 self.assertThat(reload_object(bond0), Is(None))
1412
1413- def test__all_new_complex_interfaces(self):
1414+ def test__removes_one_bridge_and_one_parent(self):
1415+ rack = self.create_empty_rack_controller()
1416+ fabric = factory.make_Fabric()
1417+ vlan = fabric.get_default_vlan()
1418+ eth0 = factory.make_Interface(
1419+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1420+ eth1 = factory.make_Interface(
1421+ INTERFACE_TYPE.PHYSICAL, node=rack, vlan=vlan)
1422+ br0 = factory.make_Interface(
1423+ INTERFACE_TYPE.BRIDGE, parents=[eth0, eth1], vlan=vlan)
1424+ interfaces = {
1425+ "eth0": {
1426+ "type": "physical",
1427+ "mac_address": eth0.mac_address,
1428+ "parents": [],
1429+ "links": [],
1430+ "enabled": True,
1431+ },
1432+ }
1433+ rack.update_interfaces(interfaces)
1434+ self.assertThat(reload_object(eth0), Not(Is(None)))
1435+ self.assertThat(reload_object(eth1), Is(None))
1436+ self.assertThat(reload_object(br0), Is(None))
1437+
1438+ def test__all_new_bond_with_vlan(self):
1439 rack = self.create_empty_rack_controller()
1440 bond0_fabric = factory.make_Fabric()
1441 bond0_untagged = bond0_fabric.get_default_vlan()
1442@@ -5664,6 +5940,117 @@
1443 subnet=bond0_vlan_subnet,
1444 ))
1445
1446+ def test__all_new_bridge_with_vlan(self):
1447+ rack = self.create_empty_rack_controller()
1448+ br0_fabric = factory.make_Fabric()
1449+ br0_untagged = br0_fabric.get_default_vlan()
1450+ br0_subnet = factory.make_Subnet(vlan=br0_untagged)
1451+ br0_ip = factory.pick_ip_in_Subnet(br0_subnet)
1452+ br0_vlan = factory.make_VLAN(fabric=br0_fabric)
1453+ br0_vlan_subnet = factory.make_Subnet(vlan=br0_vlan)
1454+ br0_vlan_ip = factory.pick_ip_in_Subnet(br0_vlan_subnet)
1455+ interfaces = {
1456+ "eth0": {
1457+ "type": "physical",
1458+ "mac_address": factory.make_mac_address(),
1459+ "parents": [],
1460+ "links": [],
1461+ "enabled": True,
1462+ },
1463+ "eth1": {
1464+ "type": "physical",
1465+ "mac_address": factory.make_mac_address(),
1466+ "parents": [],
1467+ "links": [],
1468+ "enabled": True,
1469+ },
1470+ "br0": {
1471+ "type": "bridge",
1472+ "mac_address": factory.make_mac_address(),
1473+ "parents": ["eth0", "eth1"],
1474+ "links": [{
1475+ "mode": "static",
1476+ "address": "%s/%d" % (
1477+ str(br0_ip), br0_subnet.get_ipnetwork().prefixlen),
1478+ }],
1479+ "enabled": True,
1480+ },
1481+ }
1482+ interfaces["br0.%d" % br0_vlan.vid] = {
1483+ "type": "vlan",
1484+ "parents": ["br0"],
1485+ "links": [{
1486+ "mode": "static",
1487+ "address": "%s/%d" % (
1488+ str(br0_vlan_ip),
1489+ br0_vlan_subnet.get_ipnetwork().prefixlen),
1490+ }],
1491+ "vid": br0_vlan.vid,
1492+ "enabled": True,
1493+ }
1494+ rack.update_interfaces(interfaces)
1495+ eth0 = PhysicalInterface.objects.get(
1496+ node=rack, mac_address=interfaces["eth0"]["mac_address"])
1497+ self.assertThat(
1498+ eth0, MatchesStructure.byEquality(
1499+ type=INTERFACE_TYPE.PHYSICAL,
1500+ name="eth0",
1501+ mac_address=interfaces["eth0"]["mac_address"],
1502+ enabled=True,
1503+ vlan=br0_untagged,
1504+ ))
1505+ eth1 = PhysicalInterface.objects.get(
1506+ node=rack, mac_address=interfaces["eth1"]["mac_address"])
1507+ self.assertThat(
1508+ eth1, MatchesStructure.byEquality(
1509+ type=INTERFACE_TYPE.PHYSICAL,
1510+ name="eth1",
1511+ mac_address=interfaces["eth1"]["mac_address"],
1512+ enabled=True,
1513+ vlan=br0_untagged,
1514+ ))
1515+ br0 = BridgeInterface.objects.get(
1516+ node=rack, mac_address=interfaces["br0"]["mac_address"])
1517+ self.assertThat(
1518+ br0, MatchesStructure.byEquality(
1519+ type=INTERFACE_TYPE.BRIDGE,
1520+ name="br0",
1521+ mac_address=interfaces["br0"]["mac_address"],
1522+ enabled=True,
1523+ vlan=br0_untagged,
1524+ ))
1525+ self.assertThat(
1526+ [parent.name for parent in br0.parents.all()],
1527+ MatchesSetwise(Equals("eth0"), Equals("eth1")))
1528+ br0_addresses = list(br0.ip_addresses.all())
1529+ self.assertThat(br0_addresses, HasLength(1))
1530+ self.assertThat(
1531+ br0_addresses[0], MatchesStructure.byEquality(
1532+ alloc_type=IPADDRESS_TYPE.STICKY,
1533+ ip=br0_ip,
1534+ subnet=br0_subnet,
1535+ ))
1536+ br0_vlan_nic = VLANInterface.objects.get(
1537+ node=rack, vlan=br0_vlan)
1538+ self.assertThat(
1539+ br0_vlan_nic, MatchesStructure.byEquality(
1540+ type=INTERFACE_TYPE.VLAN,
1541+ name="br0.%d" % br0_vlan.vid,
1542+ enabled=True,
1543+ vlan=br0_vlan,
1544+ ))
1545+ self.assertThat(
1546+ [parent.name for parent in br0_vlan_nic.parents.all()],
1547+ Equals(["br0"]))
1548+ br0_vlan_nic_addresses = list(br0_vlan_nic.ip_addresses.all())
1549+ self.assertThat(br0_vlan_nic_addresses, HasLength(1))
1550+ self.assertThat(
1551+ br0_vlan_nic_addresses[0], MatchesStructure.byEquality(
1552+ alloc_type=IPADDRESS_TYPE.STICKY,
1553+ ip=br0_vlan_ip,
1554+ subnet=br0_vlan_subnet,
1555+ ))
1556+
1557
1558 class TestRackController(MAASServerTestCase):
1559
1560
1561=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
1562--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-03-16 20:41:33 +0000
1563+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-03-24 18:53:09 +0000
1564@@ -36,17 +36,17 @@
1565 // Filter that is specific to the NodeNetworkingController. Filters the
1566 // list of interfaces to not include the current parent interfaces being
1567 // bonded together.
1568-angular.module('MAAS').filter('removeBondParents', function() {
1569- return function(interfaces, bondInterface) {
1570- if(!angular.isObject(bondInterface) ||
1571- !angular.isArray(bondInterface.parents)) {
1572+angular.module('MAAS').filter('removeInterfaceParents', function() {
1573+ return function(interfaces, childInterface) {
1574+ if(!angular.isObject(childInterface) ||
1575+ !angular.isArray(childInterface.parents)) {
1576 return interfaces;
1577 }
1578 var filtered = [];
1579 angular.forEach(interfaces, function(nic) {
1580 var i, parent, found = false;
1581- for(i = 0; i < bondInterface.parents.length; i++) {
1582- parent = bondInterface.parents[i];
1583+ for(i = 0; i < childInterface.parents.length; i++) {
1584+ parent = childInterface.parents[i];
1585 if(parent.id === nic.id && parent.link_id === nic.link_id) {
1586 found = true;
1587 break;
1588@@ -130,12 +130,14 @@
1589 var INTERFACE_TYPE = {
1590 PHYSICAL: "physical",
1591 BOND: "bond",
1592+ BRIDGE: "bridge",
1593 VLAN: "vlan",
1594 ALIAS: "alias"
1595 };
1596 var INTERFACE_TYPE_TEXTS = {
1597 "physical": "Physical",
1598 "bond": "Bond",
1599+ "bridge": "Bridge",
1600 "vlan": "VLAN",
1601 "alias": "Alias"
1602 };
1603@@ -235,11 +237,12 @@
1604 // interface with a bond child can only have one child.
1605 if(nic.children.length === 1) {
1606 var child = $scope.originalInterfaces[nic.children[0]];
1607- if(child.type === INTERFACE_TYPE.BOND) {
1608- // This parent now has a bond for a child, if this was
1609- // the focusInterface then the focus needs to be
1610- // removed. We only need to check the "id" not the
1611- // "link_id", because if this interface did have
1612+ if(child.type === INTERFACE_TYPE.BOND ||
1613+ child.type === INTERFACE_TYPE.BRIDGE) {
1614+ // This parent now has a bond or bridge for a child.
1615+ // If this was the focusInterface, then the focus needs
1616+ // to be removed. We only need to check the "id" (not
1617+ // the "link_id"), because if this interface did have
1618 // aliases they have now been removed.
1619 if(angular.isObject($scope.focusInterface) &&
1620 $scope.focusInterface.id === nic.id) {
1621@@ -249,9 +252,10 @@
1622 }
1623 }
1624
1625- // When the interface is a bond, place the children as members
1626- // for that interface.
1627- if(nic.type === INTERFACE_TYPE.BOND) {
1628+ // When the interface is a bond or a bridge, place the children
1629+ // as members for that interface.
1630+ if(nic.type === INTERFACE_TYPE.BOND ||
1631+ nic.type === INTERFACE_TYPE.BRIDGE) {
1632 nic.members = [];
1633 angular.forEach(nic.parents, function(parent) {
1634 nic.members.push(
1635@@ -528,7 +532,8 @@
1636
1637 if(nic.is_boot && nic.type !== INTERFACE_TYPE.ALIAS) {
1638 return true;
1639- } else if(nic.type === INTERFACE_TYPE.BOND) {
1640+ } else if(nic.type === INTERFACE_TYPE.BOND ||
1641+ nic.type === INTERFACE_TYPE.BRIDGE) {
1642 var i;
1643 for(i = 0; i < nic.members.length; i++) {
1644 if(nic.members[i].is_boot) {
1645
1646=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js'
1647--- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-03-16 20:41:33 +0000
1648+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-03-24 18:53:09 +0000
1649@@ -77,15 +77,15 @@
1650 });
1651
1652
1653-describe("removeBondParents", function() {
1654+describe("removeInterfaceParents", function() {
1655
1656 // Load the MAAS module.
1657 beforeEach(module("MAAS"));
1658
1659- // Load the removeBondParents.
1660- var removeBondParents;
1661+ // Load the removeInterfaceParents.
1662+ var removeInterfaceParents;
1663 beforeEach(inject(function($filter) {
1664- removeBondParents = $filter("removeBondParents");
1665+ removeInterfaceParents = $filter("removeInterfaceParents");
1666 }));
1667
1668 it("returns empty if undefined bondInterface", function() {
1669@@ -97,7 +97,7 @@
1670 };
1671 interfaces.push(nic);
1672 }
1673- expect(removeBondParents(interfaces)).toEqual(interfaces);
1674+ expect(removeInterfaceParents(interfaces)).toEqual(interfaces);
1675 });
1676
1677 it("removes parents from interfaces", function() {
1678@@ -120,7 +120,7 @@
1679 var bondInterface = {
1680 parents: interfaces
1681 };
1682- expect(removeBondParents(interfaces, bondInterface)).toEqual([]);
1683+ expect(removeInterfaceParents(interfaces, bondInterface)).toEqual([]);
1684 });
1685 });
1686
1687@@ -510,6 +510,49 @@
1688 }]);
1689 });
1690
1691+ it("removes bridge parents and places them as members", function() {
1692+ var parent1 = {
1693+ id: 0,
1694+ name: "eth0",
1695+ type: "physical",
1696+ parents: [],
1697+ children: [2],
1698+ links: []
1699+ };
1700+ var parent2 = {
1701+ id: 1,
1702+ name: "eth1",
1703+ type: "physical",
1704+ parents: [],
1705+ children: [2],
1706+ links: []
1707+ };
1708+ var bridge = {
1709+ id: 2,
1710+ name: "br0",
1711+ type: "bridge",
1712+ parents: [0, 1],
1713+ children: [],
1714+ links: []
1715+ };
1716+ node.interfaces = [parent1, parent2, bridge];
1717+ updateInterfaces();
1718+ expect($scope.interfaces).toEqual([{
1719+ id: 2,
1720+ name: "br0",
1721+ type: "bridge",
1722+ parents: [0, 1],
1723+ children: [],
1724+ links: [],
1725+ members: [parent1, parent2],
1726+ vlan: null,
1727+ link_id: -1,
1728+ subnet: null,
1729+ mode: "link_up",
1730+ ip_address: ""
1731+ }]);
1732+ });
1733+
1734 it("clears focusInterface if parent is now in a bond", function() {
1735 var parent1 = {
1736 id: 0,
1737@@ -544,6 +587,40 @@
1738 expect($scope.focusInterface).toBeNull();
1739 });
1740
1741+ it("clears focusInterface if parent is now in a bridge", function() {
1742+ var parent1 = {
1743+ id: 0,
1744+ name: "eth0",
1745+ type: "physical",
1746+ parents: [],
1747+ children: [2],
1748+ links: []
1749+ };
1750+ var parent2 = {
1751+ id: 1,
1752+ name: "eth1",
1753+ type: "physical",
1754+ parents: [],
1755+ children: [2],
1756+ links: []
1757+ };
1758+ var bridge = {
1759+ id: 2,
1760+ name: "br0",
1761+ type: "bridge",
1762+ parents: [0, 1],
1763+ children: [],
1764+ links: []
1765+ };
1766+ node.interfaces = [parent1, parent2, bridge];
1767+ $scope.focusInterface = {
1768+ id: 0,
1769+ link_id: -1
1770+ };
1771+ updateInterfaces();
1772+ expect($scope.focusInterface).toBeNull();
1773+ });
1774+
1775 it("sets vlan and fabric on interface", function() {
1776 var fabric = {
1777 id: 0
1778
1779=== modified file 'src/maasserver/static/partials/node-details.html'
1780--- src/maasserver/static/partials/node-details.html 2016-03-24 11:47:04 +0000
1781+++ src/maasserver/static/partials/node-details.html 2016-03-24 18:53:09 +0000
1782@@ -543,7 +543,7 @@
1783 <main class="table__body" data-selected-rows>
1784 <div class="table__row"
1785 data-ng-class="{ active: isInterfaceSelected(interface), disabled: isDisabled(), noEdit: cannotEditInterface(interface) }"
1786- data-ng-repeat="interface in interfaces | removeBondParents:newBondInterface">
1787+ data-ng-repeat="interface in interfaces | removeInterfaceParents:newBondInterface">
1788 <div class="table__data table-col--3">
1789 <span data-ng-hide="isAllNetworkingDisabled()">
1790 <input class="checkbox" type="checkbox" id="{$ getUniqueKey(interface) $}"
1791
1792=== modified file 'src/maasserver/testing/factory.py'
1793--- src/maasserver/testing/factory.py 2016-03-24 04:11:19 +0000
1794+++ src/maasserver/testing/factory.py 2016-03-24 18:53:09 +0000
1795@@ -850,6 +850,8 @@
1796 name = self.make_name('eth', sep=':')
1797 elif iftype == INTERFACE_TYPE.BOND:
1798 name = self.make_name('bond')
1799+ elif iftype == INTERFACE_TYPE.BRIDGE:
1800+ name = self.make_name('br')
1801 elif iftype == INTERFACE_TYPE.UNKNOWN:
1802 name = self.make_name('eth')
1803 elif iftype == INTERFACE_TYPE.VLAN:
1804@@ -875,6 +877,7 @@
1805 iftype in [
1806 INTERFACE_TYPE.PHYSICAL,
1807 INTERFACE_TYPE.BOND,
1808+ INTERFACE_TYPE.BRIDGE,
1809 INTERFACE_TYPE.UNKNOWN]):
1810 mac_address = self.make_MAC()
1811 if node is None and iftype == INTERFACE_TYPE.PHYSICAL:
1812
1813=== modified file 'src/maasserver/tests/test_forms_interface.py'
1814--- src/maasserver/tests/test_forms_interface.py 2016-03-07 23:20:52 +0000
1815+++ src/maasserver/tests/test_forms_interface.py 2016-03-24 18:53:09 +0000
1816@@ -1,4 +1,4 @@
1817-# Copyright 2015 Canonical Ltd. This software is licensed under the
1818+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
1819 # GNU Affero General Public License version 3 (see the file LICENSE).
1820
1821 """Tests for Interface forms."""
1822@@ -17,6 +17,7 @@
1823 BOND_MODE_CHOICES,
1824 BOND_XMIT_HASH_POLICY_CHOICES,
1825 BondInterfaceForm,
1826+ BridgeInterfaceForm,
1827 ControllerInterfaceForm,
1828 InterfaceForm,
1829 PhysicalInterfaceForm,
1830@@ -675,7 +676,7 @@
1831 self.assertFalse(form.is_valid(), form.errors)
1832 self.assertItemsEqual(['parents', 'mac_address'], form.errors.keys())
1833 self.assertIn(
1834- "A Bond interface must have one or more parents.",
1835+ "A bond interface must have one or more parents.",
1836 form.errors['parents'][0])
1837
1838 def test__rejects_when_vlan_not_untagged(self):
1839@@ -713,7 +714,7 @@
1840 })
1841 self.assertFalse(form.is_valid(), form.errors)
1842 self.assertIn(
1843- "eth0, eth1 is already in-use by another interface.",
1844+ "Interfaces already in-use: eth0, eth1.",
1845 form.errors['parents'][0])
1846
1847 def test__rejects_when_parents_not_in_same_vlan(self):
1848@@ -965,3 +966,189 @@
1849 "bond_lacp_rate": new_bond_lacp_rate,
1850 "bond_xmit_hash_policy": new_bond_xmit_hash_policy,
1851 }, interface.params)
1852+
1853+
1854+class BridgeInterfaceFormTest(MAASServerTestCase):
1855+
1856+ def test__creates_bridge_interface(self):
1857+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1858+ parent2 = factory.make_Interface(
1859+ INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1860+ interface_name = factory.make_name()
1861+ form = BridgeInterfaceForm(
1862+ node=parent1.node,
1863+ data={
1864+ 'name': interface_name,
1865+ 'parents': [parent1.id, parent2.id],
1866+ })
1867+ self.assertTrue(form.is_valid(), form.errors)
1868+ interface = form.save()
1869+ self.assertThat(
1870+ interface,
1871+ MatchesStructure.byEquality(
1872+ name=interface_name, type=INTERFACE_TYPE.BRIDGE))
1873+ self.assertIn(
1874+ interface.mac_address, [parent1.mac_address, parent2.mac_address])
1875+ self.assertItemsEqual([parent1, parent2], interface.parents.all())
1876+
1877+ def test__create_removes_parent_links_and_sets_link_up_on_bridge(self):
1878+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1879+ parent1.ensure_link_up()
1880+ parent2 = factory.make_Interface(
1881+ INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1882+ parent2.ensure_link_up()
1883+ interface_name = factory.make_name()
1884+ form = BridgeInterfaceForm(
1885+ node=parent1.node,
1886+ data={
1887+ 'name': interface_name,
1888+ 'parents': [parent1.id, parent2.id],
1889+ })
1890+ self.assertTrue(form.is_valid(), form.errors)
1891+ interface = form.save()
1892+ self.assertEqual(
1893+ 0,
1894+ parent1.ip_addresses.exclude(
1895+ alloc_type=IPADDRESS_TYPE.DISCOVERED).count())
1896+ self.assertEqual(
1897+ 0,
1898+ parent2.ip_addresses.exclude(
1899+ alloc_type=IPADDRESS_TYPE.DISCOVERED).count())
1900+ self.assertIsNotNone(
1901+ interface.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.STICKY))
1902+
1903+ def test__creates_bridge_interface_with_parent_mac_address(self):
1904+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1905+ parent2 = factory.make_Interface(
1906+ INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1907+ interface_name = factory.make_name()
1908+ form = BridgeInterfaceForm(
1909+ node=parent1.node,
1910+ data={
1911+ 'name': interface_name,
1912+ 'parents': [parent1.id, parent2.id],
1913+ 'mac_address': parent1.mac_address,
1914+ })
1915+ self.assertTrue(form.is_valid(), form.errors)
1916+ interface = form.save()
1917+ self.assertThat(
1918+ interface,
1919+ MatchesStructure.byEquality(
1920+ name=interface_name, mac_address=parent1.mac_address,
1921+ type=INTERFACE_TYPE.BRIDGE))
1922+ self.assertItemsEqual([parent1, parent2], interface.parents.all())
1923+
1924+ def test__rejects_no_parents(self):
1925+ interface_name = factory.make_name()
1926+ form = BridgeInterfaceForm(
1927+ node=factory.make_Node(),
1928+ data={
1929+ 'name': interface_name,
1930+ })
1931+ self.assertFalse(form.is_valid(), form.errors)
1932+ self.assertItemsEqual(['parents', 'mac_address'], form.errors.keys())
1933+ self.assertIn(
1934+ "A bridge interface must have one or more parents.",
1935+ form.errors['parents'][0])
1936+
1937+ def test__rejects_when_parents_already_have_children(self):
1938+ node = factory.make_Node()
1939+ parent1 = factory.make_Interface(
1940+ INTERFACE_TYPE.PHYSICAL, node=node, name="eth0")
1941+ factory.make_Interface(INTERFACE_TYPE.VLAN, parents=[parent1])
1942+ parent2 = factory.make_Interface(
1943+ INTERFACE_TYPE.PHYSICAL, node=node, name="eth1", vlan=parent1.vlan)
1944+ factory.make_Interface(INTERFACE_TYPE.VLAN, parents=[parent2])
1945+ interface_name = factory.make_name()
1946+ form = BridgeInterfaceForm(
1947+ node=node,
1948+ data={
1949+ 'name': interface_name,
1950+ 'parents': [parent1.id, parent2.id]
1951+ })
1952+ self.assertFalse(form.is_valid(), form.errors)
1953+ self.assertIn(
1954+ "Interfaces already in-use: eth0, eth1.",
1955+ form.errors['parents'][0])
1956+
1957+ def test__edits_interface(self):
1958+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1959+ parent2 = factory.make_Interface(
1960+ INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1961+ interface = factory.make_Interface(
1962+ INTERFACE_TYPE.BRIDGE,
1963+ parents=[parent1, parent2])
1964+ new_fabric = factory.make_Fabric()
1965+ new_vlan = new_fabric.get_default_vlan()
1966+ new_name = factory.make_name()
1967+ new_parent = factory.make_Interface(
1968+ INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1969+ form = BridgeInterfaceForm(
1970+ instance=interface,
1971+ data={
1972+ 'vlan': new_vlan.id,
1973+ 'name': new_name,
1974+ 'parents': [parent1.id, parent2.id, new_parent.id],
1975+ })
1976+ self.assertTrue(form.is_valid(), form.errors)
1977+ interface = form.save()
1978+ self.assertThat(
1979+ interface,
1980+ MatchesStructure.byEquality(
1981+ mac_address=interface.mac_address, name=new_name,
1982+ vlan=new_vlan, type=INTERFACE_TYPE.BRIDGE))
1983+ self.assertItemsEqual(
1984+ [parent1, parent2, new_parent], interface.parents.all())
1985+
1986+ def test__edits_interface_removes_parents(self):
1987+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1988+ parent2 = factory.make_Interface(
1989+ INTERFACE_TYPE.PHYSICAL, node=parent1.node)
1990+ parent3 = factory.make_Interface(
1991+ INTERFACE_TYPE.PHYSICAL, node=parent1.node)
1992+ interface = factory.make_Interface(
1993+ INTERFACE_TYPE.BRIDGE,
1994+ parents=[parent1, parent2, parent3])
1995+ new_name = factory.make_name()
1996+ form = BridgeInterfaceForm(
1997+ instance=interface,
1998+ data={
1999+ 'name': new_name,
2000+ 'parents': [parent1.id, parent2.id],
2001+ })
2002+ self.assertTrue(form.is_valid(), form.errors)
2003+ interface = form.save()
2004+ self.assertThat(
2005+ interface,
2006+ MatchesStructure.byEquality(
2007+ mac_address=interface.mac_address, name=new_name,
2008+ type=INTERFACE_TYPE.BRIDGE))
2009+ self.assertItemsEqual(
2010+ [parent1, parent2], interface.parents.all())
2011+
2012+ def test__edits_interface_updates_mac_address_when_parent_removed(self):
2013+ parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
2014+ parent2 = factory.make_Interface(
2015+ INTERFACE_TYPE.PHYSICAL, node=parent1.node)
2016+ parent3 = factory.make_Interface(
2017+ INTERFACE_TYPE.PHYSICAL, node=parent1.node)
2018+ interface = factory.make_Interface(
2019+ INTERFACE_TYPE.BRIDGE, mac_address=parent3.mac_address,
2020+ parents=[parent1, parent2, parent3])
2021+ new_name = factory.make_name()
2022+ form = BridgeInterfaceForm(
2023+ instance=interface,
2024+ data={
2025+ 'name': new_name,
2026+ 'parents': [parent1.id, parent2.id],
2027+ })
2028+ self.assertTrue(form.is_valid(), form.errors)
2029+ interface = form.save()
2030+ self.assertThat(
2031+ interface,
2032+ MatchesStructure.byEquality(
2033+ name=new_name, type=INTERFACE_TYPE.BRIDGE))
2034+ self.assertItemsEqual(
2035+ [parent1, parent2], interface.parents.all())
2036+ self.assertIn(
2037+ interface.mac_address, [parent1.mac_address, parent2.mac_address])
2038
2039=== modified file 'src/provisioningserver/utils/__init__.py'
2040--- src/provisioningserver/utils/__init__.py 2016-03-07 20:18:45 +0000
2041+++ src/provisioningserver/utils/__init__.py 2016-03-24 18:53:09 +0000
2042@@ -343,17 +343,14 @@
2043 making it impossible to resolve their relative ordering.
2044 """
2045 empty = frozenset()
2046-
2047 # Copy data and discard self-referential dependencies.
2048 data = {thing: set(deps) for thing, deps in data.items()}
2049 for thing, deps in data.items():
2050 deps.discard(thing)
2051-
2052 # Find ghost dependencies and add them as "things".
2053 ghosts = reduce(set.union, data.values(), set()).difference(data)
2054 for ghost in ghosts:
2055 data[ghost] = empty
2056-
2057 # Skim batches off the top until we're done.
2058 while len(data) != 0:
2059 batch = {thing for thing, deps in data.items() if deps == empty}
2060
2061=== modified file 'src/provisioningserver/utils/ipaddr.py'
2062--- src/provisioningserver/utils/ipaddr.py 2016-03-08 18:50:36 +0000
2063+++ src/provisioningserver/utils/ipaddr.py 2016-03-24 18:53:09 +0000
2064@@ -342,15 +342,12 @@
2065 Annotates the given dictionary to update it with driver information
2066 (if found) for each interface.
2067
2068- Deletes bond interfaces if they are not configured.
2069-
2070 :param interfaces: interfaces dictionary from `parse_ip_addr()`.
2071 :param proc_net: path to /proc/net
2072 :param sys_class_net: path to /sys/class/net
2073 """
2074 interfaces = annotate_with_proc_net_bonding_original_macs(
2075 interfaces, proc_net=proc_net)
2076- bogus_interfaces = []
2077 for name in interfaces:
2078 iface = interfaces[name]
2079 iftype = get_interface_type(
2080@@ -359,20 +356,12 @@
2081 if iftype == 'ethernet.bond':
2082 bond_parents = get_bonded_interfaces(
2083 name, sys_class_net=sys_class_net)
2084- if len(bond_parents) > 0:
2085- iface['bonded_interfaces'] = bond_parents
2086- else:
2087- # If we found a bond interface with no parents, just pretend
2088- # it doesn't exist. The MAAS model assumes bonds must have
2089- # backing interfaces.
2090- bogus_interfaces.append(name)
2091+ iface['bonded_interfaces'] = bond_parents
2092 elif iftype == 'ethernet.vlan':
2093 iface['vid'] = get_vid_from_ifname(name)
2094 elif iftype == 'ethernet.bridge':
2095 iface['bridged_interfaces'] = get_bridged_interfaces(
2096 name, sys_class_net=sys_class_net)
2097- for name in bogus_interfaces:
2098- del interfaces[name]
2099 return interfaces
2100
2101
2102
2103=== modified file 'src/provisioningserver/utils/network.py'
2104--- src/provisioningserver/utils/network.py 2016-03-17 14:17:57 +0000
2105+++ src/provisioningserver/utils/network.py 2016-03-24 18:53:09 +0000
2106@@ -646,6 +646,12 @@
2107 iface_type = "vlan"
2108 parents.append(name.split(".", 1)[0])
2109 vid = ipaddr["vid"]
2110+ elif ipaddr["type"] == "ethernet.bridge":
2111+ iface_type = "bridge"
2112+ mac_address = ipaddr["mac"]
2113+ for bridge_nic in ipaddr["bridged_interfaces"]:
2114+ if bridge_nic in interfaces or bridge_nic in ipaddr_info:
2115+ parents.append(bridge_nic)
2116 else:
2117 mac_address = ipaddr["mac"]
2118
2119
2120=== modified file 'src/provisioningserver/utils/tests/test_ipaddr.py'
2121--- src/provisioningserver/utils/tests/test_ipaddr.py 2016-03-08 18:50:36 +0000
2122+++ src/provisioningserver/utils/tests/test_ipaddr.py 2016-03-24 18:53:09 +0000
2123@@ -1,4 +1,4 @@
2124-# Copyright 2015 Canonical Ltd. This software is licensed under the
2125+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2126 # GNU Affero General Public License version 3 (see the file LICENSE).
2127
2128 """Test parser for 'ip addr show'."""
2129@@ -34,7 +34,6 @@
2130 from testtools.matchers import (
2131 Contains,
2132 Equals,
2133- HasLength,
2134 Not,
2135 )
2136
2137@@ -507,27 +506,6 @@
2138 elif iface['type'] == 'ethernet.bridge':
2139 self.expectThat(iface, Contains('bridged_interfaces'))
2140
2141- def test__ignores_bond_interfaces_with_no_parents(self):
2142- interfaces = {
2143- 'eth0': {},
2144- 'eth1': {},
2145- 'bond0': {},
2146- 'bond1': {},
2147- }
2148- self.createEthernetInterface('eth0', is_physical=True)
2149- self.createEthernetInterface('eth1', is_physical=True)
2150- self.createEthernetInterface(
2151- 'bond0', is_bond=True, bonded_interfaces=['eth0, eth1'])
2152- self.createEthernetInterface(
2153- 'bond1', is_bond=True, bonded_interfaces=[])
2154- interfaces = annotate_with_driver_information(
2155- interfaces, sys_class_net=self.tmp_sys_net,
2156- proc_net=self.tmp_proc_net)
2157- self.expectThat(interfaces, HasLength(3))
2158- self.expectThat(interfaces, Contains('eth0'))
2159- self.expectThat(interfaces, Contains('eth1'))
2160- self.expectThat(interfaces, Contains('bond0'))
2161-
2162 def test__finds_bond_members_original_mac_addresses(self):
2163 testdata = dedent("""\
2164 Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)
2165
2166=== modified file 'src/provisioningserver/utils/tests/test_network.py'
2167--- src/provisioningserver/utils/tests/test_network.py 2016-03-17 14:17:57 +0000
2168+++ src/provisioningserver/utils/tests/test_network.py 2016-03-24 18:53:09 +0000
2169@@ -1058,6 +1058,7 @@
2170 },
2171 "br0": {
2172 "type": "ethernet.bridge",
2173+ "bridged_interfaces": ["eth0"],
2174 "mac": factory.make_mac_address(),
2175 "flags": ["UP"],
2176 "inet": ["192.168.124.2/24"],
2177@@ -1145,10 +1146,10 @@
2178 "source": Equals("ipaddr"),
2179 }),
2180 "br0": MatchesDict({
2181- "type": Equals("physical"),
2182+ "type": Equals("bridge"),
2183 "mac_address": Equals(ip_addr["br0"]["mac"]),
2184 "enabled": Is(True),
2185- "parents": Equals([]),
2186+ "parents": Equals(["eth0"]),
2187 "links": Equals([{
2188 "mode": "static",
2189 "address": "192.168.124.2/24",