Merge lp:~mpontillo/maas/bridge-interface-model-and-region-contract into lp:~maas-committers/maas/trunk
- bridge-interface-model-and-region-contract
- Merge into trunk
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 |
Related bugs: |
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
(ChildInterf
* Refactor rack code to report bridges to the region.
* Minor changes to Javascript code to ensure bridges are
consistently displayed (similar to bonds).
Description of the change
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.
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
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", |
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.