Merge lp:~mpontillo/maas/autocreate-fabrics-and-vlans into lp:~maas-committers/maas/trunk
- autocreate-fabrics-and-vlans
- Merge into trunk
Proposed by
Mike Pontillo
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4408 |
Proposed branch: | lp:~mpontillo/maas/autocreate-fabrics-and-vlans |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1069 lines (+553/-100) 12 files modified
src/maasserver/forms.py (+108/-50) src/maasserver/models/fabric.py (+23/-0) src/maasserver/models/nodegroupinterface.py (+2/-1) src/maasserver/models/tests/test_nodegroupinterface.py (+12/-12) src/maasserver/models/vlan.py (+8/-0) src/maasserver/tests/test_forms_nodegroupinterface.py (+107/-2) src/maasserver/utils/interfaces.py (+3/-1) src/provisioningserver/network.py (+146/-17) src/provisioningserver/rpc/clusterservice.py (+1/-0) src/provisioningserver/tests/test_network.py (+33/-1) src/provisioningserver/utils/ipaddr.py (+63/-4) src/provisioningserver/utils/tests/test_ipaddr.py (+47/-12) |
To merge this branch: | bzr merge lp:~mpontillo/maas/autocreate-fabrics-and-vlans |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
LaMont Jones (community) | Approve | ||
Review via email: mp+275516@code.launchpad.net |
Commit message
Automatically create VLANs and Fabrics when cluster interfaces are created. (and when it's appropriate)
Description of the change
To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote : | # |
Revision history for this message
LaMont Jones (lamont) wrote : | # |
Nothing leaps out at me as broken (other than collision detection for the multiple ip addresses) - I'd prefer to not see the randint() call at all, for repeatability sake (which might allow a change in the testcase.)
review:
Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Just move that nasty hash calculation to its own function.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/forms.py' |
2 | --- src/maasserver/forms.py 2015-10-23 18:56:34 +0000 |
3 | +++ src/maasserver/forms.py 2015-10-23 21:41:16 +0000 |
4 | @@ -46,9 +46,10 @@ |
5 | import collections |
6 | from collections import Counter |
7 | from functools import partial |
8 | +import hashlib |
9 | import json |
10 | +import os |
11 | import pipes |
12 | -from random import randint |
13 | import re |
14 | |
15 | from crochet import TimeoutError |
16 | @@ -126,6 +127,7 @@ |
17 | Config, |
18 | Device, |
19 | DownloadProgress, |
20 | + Fabric, |
21 | Filesystem, |
22 | Interface, |
23 | LargeFile, |
24 | @@ -142,6 +144,7 @@ |
25 | SSLKey, |
26 | Tag, |
27 | VirtualBlockDevice, |
28 | + VLAN, |
29 | VolumeGroup, |
30 | Zone, |
31 | ) |
32 | @@ -192,11 +195,15 @@ |
33 | valid_ipv6, |
34 | ) |
35 | from provisioningserver.logger import get_maas_logger |
36 | -from provisioningserver.network import filter_likely_unmanaged_networks |
37 | +from provisioningserver.network import ( |
38 | + filter_and_annotate_networks, |
39 | + sort_networks_by_priority, |
40 | +) |
41 | from provisioningserver.rpc.exceptions import ( |
42 | NoConnectionsAvailable, |
43 | NoSuchOperatingSystem, |
44 | ) |
45 | +from provisioningserver.utils.ipaddr import get_vid_from_ifname |
46 | from provisioningserver.utils.network import ( |
47 | ip_range_within_network, |
48 | make_network, |
49 | @@ -1546,35 +1553,6 @@ |
50 | return True |
51 | |
52 | |
53 | -def disambiguate_name(original_name, ip_address): |
54 | - """Return a unique variant of `original_name` for a cluster interface. |
55 | - |
56 | - This function has no knowledge of other existing cluster interfaces. It |
57 | - disambiguates based purely on the given data and random numbers. |
58 | - |
59 | - :param original_name: The originally proposed name for the cluster |
60 | - interface, which presumably turned out to be ambiguous. |
61 | - :param ip_address: IP address for the cluster interface (either as a |
62 | - string or as an `IPAddress`). Used to determine whether the interface |
63 | - is an IPv4 one or an IPv6 one. |
64 | - :return: A version of `original_name` with a disambiguating suffix. The |
65 | - suffix contains both the IP version (`ipv4` or `ipv6`) and possibly a |
66 | - random part to avoid further clashes. |
67 | - """ |
68 | - ip_version = IPAddress(ip_address).version |
69 | - assert ip_version in (4, 6) |
70 | - if ip_version == 6: |
71 | - # IPv6 cluster interface. In principle there could be many of these |
72 | - # on the same network interface, so add a random suffix. |
73 | - suffix = 'ipv6-%d' % randint(10000, 99999) |
74 | - else: |
75 | - # IPv4 cluster interface. There can be only one of these on the |
76 | - # network interface, so just suffixing '-ipv4' to the name should make |
77 | - # it unique. |
78 | - suffix = 'ipv4' |
79 | - return '%s-%s' % (original_name, suffix) |
80 | - |
81 | - |
82 | class NodeGroupInterfaceForm(MAASModelForm): |
83 | |
84 | management = forms.TypedChoiceField( |
85 | @@ -1628,6 +1606,7 @@ |
86 | |
87 | subnet = None |
88 | if self.instance and self.instance.subnet and ip and subnet_mask: |
89 | + # Existing NodeGroupInterface, so just update the existing Subnet. |
90 | cidr = create_cidr(ip, subnet_mask) |
91 | subnet = self.instance.subnet |
92 | subnet.update_cidr(cidr) |
93 | @@ -1635,14 +1614,55 @@ |
94 | subnet.gateway_ip = router_ip |
95 | subnet.save() |
96 | elif subnet_mask: |
97 | + # New NodeGroupInterface, so create the Subnet (if we have enough |
98 | + # information). |
99 | cidr = create_cidr(ip, subnet_mask) |
100 | - subnet, _ = Subnet.objects.get_or_create( |
101 | + subnet, created = Subnet.objects.get_or_create( |
102 | cidr=cidr, defaults={ |
103 | 'name': cidr, |
104 | 'cidr': cidr, |
105 | 'gateway_ip': router_ip, |
106 | 'space': Space.objects.get_default_space() |
107 | }) |
108 | + if created: |
109 | + # Check if a Subnet was already created for a matching |
110 | + # interface. (this could happen if multiple IP addresses |
111 | + # are assigned to the same interface.) |
112 | + vlan = None |
113 | + ifname = self.cleaned_data.get('interface') |
114 | + if self.instance: |
115 | + vlan = VLAN.objects.filter_by_nodegroup_interface( |
116 | + self.instance.nodegroup, ifname).order_by('id').first() |
117 | + if vlan is not None: |
118 | + # Found an existing VLAN that matches this interface. |
119 | + # No need to create a new one; just assign that VLAN |
120 | + # to the subnet that was found. |
121 | + subnet.vlan = vlan |
122 | + subnet.save() |
123 | + elif self.data.get('type') == 'ethernet.vlan': |
124 | + # If we're creating a VLAN, we must be intending to put |
125 | + # the VLAN on a Fabric that already exists. |
126 | + # Check the VLAN's parent interface to see if it is |
127 | + # already associated with a VLAN (and hence a Fabric). |
128 | + vid = self.data.get('vid', get_vid_from_ifname(ifname)) |
129 | + parent = self.data.get('parent') |
130 | + fabric = Fabric.objects.filter_by_nodegroup_interface( |
131 | + self.instance.nodegroup, parent).first() |
132 | + if fabric is None: |
133 | + fabric = Fabric.objects.get_or_create_for_subnet( |
134 | + subnet) |
135 | + vlan = VLAN.objects.create(vid=vid, fabric=fabric) |
136 | + subnet.vlan = vlan |
137 | + subnet.save() |
138 | + else: |
139 | + # If we created a new Subnet, and it's *not* a tagged VLAN |
140 | + # (and we didn't find an existing interface above), that |
141 | + # means we need to create a new Fabric and place the |
142 | + # subnet in its default VLAN. |
143 | + fabric = Fabric.objects.get_or_create_for_subnet( |
144 | + subnet) |
145 | + subnet.vlan = fabric.get_default_vlan() |
146 | + subnet.save() |
147 | |
148 | interface = super(NodeGroupInterfaceForm, self).save(*args, **kwargs) |
149 | if interface.subnet != subnet: |
150 | @@ -1650,6 +1670,45 @@ |
151 | interface.save() |
152 | return interface |
153 | |
154 | + def _ensure_unique_cluster_interface_name(self, name, interface): |
155 | + """ |
156 | + Ensures that the specified interface name is unique. Appends a portion |
157 | + of a unique sha256 hex string (and the IP version) if not. |
158 | + |
159 | + :param name: The tentative "friendly" name of this cluster interface. |
160 | + :param interface: The name of the network interface. |
161 | + :return:str |
162 | + """ |
163 | + # Get the cluster to which this interface is attached. There may not |
164 | + # be one, since it may be a placeholder instance that is still being |
165 | + # initialised by the same request that is also creating the interface. |
166 | + # In that case, self.instance.nodegroup will not be None, but rather |
167 | + # an ORM stub which crashes when accessed. |
168 | + cluster = get_one( |
169 | + NodeGroup.objects.filter(id=self.instance.nodegroup_id)) |
170 | + if cluster is not None and interface: |
171 | + siblings = cluster.nodegroupinterface_set |
172 | + while siblings.filter(name=name).exists(): |
173 | + # In case the interface name already exists, use a |
174 | + # cryptographic hash based on the interface name and address |
175 | + # (plus some random bytes for good measure). |
176 | + ip_address = self.cleaned_data['ip'] |
177 | + ip_version = IPAddress(ip_address).version |
178 | + sha = hashlib.sha256() |
179 | + sha.update(name) |
180 | + sha.update(ip_address) |
181 | + sha.update(self.cleaned_data['subnet_mask']) |
182 | + sha.update(os.urandom(32)) |
183 | + digest = sha.hexdigest() |
184 | + for i in range(4, 65): |
185 | + # The upper bound of this range() will be 64, so that |
186 | + # we can get the full sha256 hex string if needed. |
187 | + suffix = digest[:i] |
188 | + new_name = '%s-ipv%d-%s' % (name, ip_version, suffix) |
189 | + if not siblings.filter(name=new_name).exists(): |
190 | + return new_name |
191 | + return name |
192 | + |
193 | def compute_name(self): |
194 | """Return the value the `name` field should have. |
195 | |
196 | @@ -1670,20 +1729,9 @@ |
197 | # compatibility with clients that expect the pre-1.6 behaviour, where |
198 | # the 'name' and 'interface' fields were the same thing. |
199 | interface = self.cleaned_data.get('interface') |
200 | - name = make_name_from_interface(interface) |
201 | - # Get the cluster to which this interface is attached. There may not |
202 | - # be one, since it may be a placeholder instance that is still being |
203 | - # initialised by the same request that is also creating the interface. |
204 | - # In that case, self.instance.nodegroup will not be None, but rather |
205 | - # an ORM stub which crashes when accessed. |
206 | - cluster = get_one( |
207 | - NodeGroup.objects.filter(id=self.instance.nodegroup_id)) |
208 | - if cluster is not None and interface: |
209 | - siblings = cluster.nodegroupinterface_set |
210 | - if siblings.filter(name=name).exists(): |
211 | - # This name is already in use. Add a suffix to make it unique. |
212 | - return disambiguate_name(name, self.cleaned_data['ip']) |
213 | - |
214 | + name = make_name_from_interface( |
215 | + interface, alias=self.data.get('alias')) |
216 | + name = self._ensure_unique_cluster_interface_name(name, interface) |
217 | return name |
218 | |
219 | def get_duplicate_fqdns(self): |
220 | @@ -1706,6 +1754,18 @@ |
221 | |
222 | return set(duplicates) |
223 | |
224 | + def clean_interface(self): |
225 | + # Interface names must be consistent, so that if eth0 and eth0:1 |
226 | + # are added as cluster interfaces, they can be associated with the |
227 | + # same VLANs. Save the alias so that we can use it in to |
228 | + # disambiguate the interface name. |
229 | + ifname = self.cleaned_data['interface'] |
230 | + if ':' in ifname: |
231 | + alias = ifname.strip().split(':', 1)[1] |
232 | + self.data['alias'] = alias |
233 | + ifname = ifname.strip().split(':')[0] |
234 | + return ifname |
235 | + |
236 | def clean_management(self): |
237 | management = self.cleaned_data['management'] |
238 | |
239 | @@ -2018,7 +2078,7 @@ |
240 | |
241 | # Try to only manage physical Ethernet interfaces. |
242 | ip_addr_json = self.data.get('ip_addr_json', None) |
243 | - interfaces = filter_likely_unmanaged_networks(interfaces, ip_addr_json) |
244 | + interfaces = filter_and_annotate_networks(interfaces, ip_addr_json) |
245 | |
246 | for interface in interfaces: |
247 | validate_nodegroupinterface_definition(interface) |
248 | @@ -2033,9 +2093,7 @@ |
249 | # unique names for cluster interfaces on the same network interface, |
250 | # the IPv4 one will get first stab at getting the exact same name as |
251 | # the network interface. |
252 | - interfaces = sorted( |
253 | - self.cleaned_data['interfaces'], |
254 | - key=lambda definition: IPAddress(definition['ip']).version) |
255 | + interfaces = sort_networks_by_priority(self.cleaned_data['interfaces']) |
256 | for interface in interfaces: |
257 | instance = NodeGroupInterface(nodegroup=nodegroup) |
258 | form = NodeGroupInterfaceForm(data=interface, instance=instance) |
259 | |
260 | === modified file 'src/maasserver/models/fabric.py' |
261 | --- src/maasserver/models/fabric.py 2015-10-21 21:13:01 +0000 |
262 | +++ src/maasserver/models/fabric.py 2015-10-23 21:41:16 +0000 |
263 | @@ -70,6 +70,29 @@ |
264 | fabric._create_default_vlan() |
265 | return fabric |
266 | |
267 | + def get_or_create_for_subnet(self, subnet): |
268 | + """Given an existing fabric_id (by default, the default fabric) |
269 | + creates and returns a new Fabric if there is an existing Subnet in |
270 | + the fabric already. Exclude the specified subnet (which will be one |
271 | + that was just created). |
272 | + """ |
273 | + from maasserver.models import Subnet |
274 | + default_fabric = self.get_default_fabric() |
275 | + if Subnet.objects.filter( |
276 | + vlan__fabric=default_fabric).exclude( |
277 | + id=subnet.id).count() == 0: |
278 | + return default_fabric |
279 | + else: |
280 | + return Fabric.objects.create() |
281 | + |
282 | + def filter_by_nodegroup_interface(self, nodegroup, ifname): |
283 | + """Query for the Fabric associated with the specified NodeGroup, |
284 | + where the NodeGroupInterface matches the specified name. |
285 | + """ |
286 | + return self.filter( |
287 | + vlan__subnet__nodegroupinterface__nodegroup=nodegroup, |
288 | + vlan__subnet__nodegroupinterface__interface=ifname) |
289 | + |
290 | def get_fabric_or_404(self, id, user, perm): |
291 | """Fetch a `Fabric` by its id. Raise exceptions if no `Fabric` with |
292 | this id exist or if the provided user has not the required permission |
293 | |
294 | === modified file 'src/maasserver/models/nodegroupinterface.py' |
295 | --- src/maasserver/models/nodegroupinterface.py 2015-09-24 16:22:12 +0000 |
296 | +++ src/maasserver/models/nodegroupinterface.py 2015-10-23 21:41:16 +0000 |
297 | @@ -400,7 +400,8 @@ |
298 | return |
299 | ip_version = IPAddress(self.ip).version |
300 | similar_interfaces = self.nodegroup.nodegroupinterface_set.filter( |
301 | - interface=self.interface) |
302 | + interface=self.interface).exclude( |
303 | + management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
304 | if self.id is not None: |
305 | similar_interfaces = similar_interfaces.exclude(id=self.id) |
306 | potential_clashes = [ |
307 | |
308 | === modified file 'src/maasserver/models/tests/test_nodegroupinterface.py' |
309 | --- src/maasserver/models/tests/test_nodegroupinterface.py 2015-09-24 16:22:12 +0000 |
310 | +++ src/maasserver/models/tests/test_nodegroupinterface.py 2015-10-23 21:41:16 +0000 |
311 | @@ -627,25 +627,25 @@ |
312 | NodeGroupInterface.objects.filter(interface=net_interface), |
313 | HasLength(2)) |
314 | |
315 | - def test_validation_rejects_two_IPv4_interfaces_on_net_interface(self): |
316 | + def test_rejects_two_managed_IPv4_interfaces_on_net_interface(self): |
317 | cluster = factory.make_NodeGroup() |
318 | net_interface = factory.make_name('eth') |
319 | factory.make_NodeGroupInterface( |
320 | cluster, interface=net_interface, |
321 | + management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
322 | network=factory.make_ipv4_network()) |
323 | network = factory.make_ipv4_network() |
324 | extra_interface = NodeGroupInterface( |
325 | nodegroup=cluster, interface=net_interface, |
326 | + management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
327 | ip=factory.pick_ip_in_network(network)) |
328 | |
329 | error = self.assertRaises(ValidationError, extra_interface.save) |
330 | - self.assertEqual( |
331 | - [ |
332 | + self.assertThat( |
333 | + error.messages, Contains( |
334 | "Another cluster interface already connects " |
335 | "network interface %s to an IPv4 network." |
336 | - % net_interface |
337 | - ], |
338 | - error.messages) |
339 | + % net_interface)) |
340 | |
341 | def test_validation_accepts_two_IPv6_interfaces_on_net_interface(self): |
342 | cluster = factory.make_NodeGroup() |
343 | @@ -660,29 +660,29 @@ |
344 | extra_interface.save() |
345 | self.assertThat(cluster.nodegroupinterface_set.all(), HasLength(2)) |
346 | |
347 | - def test_validation_rejects_two_IPv6_static_ranges_on_net_interface(self): |
348 | + def test_rejects_two_managed_IPv6_static_ranges_on_net_interface(self): |
349 | cluster = factory.make_NodeGroup() |
350 | net_interface = factory.make_name('eth') |
351 | factory.make_NodeGroupInterface( |
352 | cluster, interface=net_interface, |
353 | + management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
354 | network=factory.make_ipv6_network(slash=64)) |
355 | network = factory.make_ipv6_network(slash=64) |
356 | static_low = unicode(IPAddress(network.first + 1)) |
357 | static_high = unicode(IPAddress(network.last - 1)) |
358 | extra_interface = NodeGroupInterface( |
359 | nodegroup=cluster, interface=net_interface, |
360 | + management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
361 | ip=factory.pick_ip_in_network(network), |
362 | static_ip_range_low=static_low, |
363 | static_ip_range_high=static_high) |
364 | |
365 | error = self.assertRaises(ValidationError, extra_interface.save) |
366 | - self.assertEqual( |
367 | - [ |
368 | + self.assertThat( |
369 | + error.messages, Contains( |
370 | "Another cluster interface with a static address range " |
371 | "already connects network interface %s to an IPv6 network." |
372 | - % net_interface |
373 | - ], |
374 | - error.messages) |
375 | + % net_interface)) |
376 | |
377 | def test_validation_knows_update_from_new_interface(self): |
378 | cluster = factory.make_NodeGroup() |
379 | |
380 | === modified file 'src/maasserver/models/vlan.py' |
381 | --- src/maasserver/models/vlan.py 2015-10-09 06:06:47 +0000 |
382 | +++ src/maasserver/models/vlan.py 2015-10-23 21:41:16 +0000 |
383 | @@ -49,6 +49,14 @@ |
384 | from maasserver.models.fabric import Fabric |
385 | return Fabric.objects.get_default_fabric().get_default_vlan() |
386 | |
387 | + def filter_by_nodegroup_interface(self, nodegroup, ifname): |
388 | + """Query fot the VLAN that matches the specified NodeGroup, whose |
389 | + interface matches the specified name. |
390 | + """ |
391 | + return self.filter( |
392 | + subnet__nodegroupinterface__nodegroup=nodegroup, |
393 | + subnet__nodegroupinterface__interface=ifname) |
394 | + |
395 | |
396 | class VLAN(CleanSave, TimestampedModel): |
397 | """A `VLAN`. |
398 | |
399 | === modified file 'src/maasserver/tests/test_forms_nodegroupinterface.py' |
400 | --- src/maasserver/tests/test_forms_nodegroupinterface.py 2015-09-08 18:41:57 +0000 |
401 | +++ src/maasserver/tests/test_forms_nodegroupinterface.py 2015-10-23 21:41:16 +0000 |
402 | @@ -23,7 +23,11 @@ |
403 | ERROR_MESSAGE_STATIC_RANGE_IN_USE, |
404 | NodeGroupInterfaceForm, |
405 | ) |
406 | -from maasserver.models import NodeGroupInterface |
407 | +from maasserver.models import ( |
408 | + Fabric, |
409 | + NodeGroupInterface, |
410 | + VLAN, |
411 | +) |
412 | from maasserver.models.staticipaddress import StaticIPAddress |
413 | from maasserver.testing.factory import factory |
414 | from maasserver.testing.testcase import MAASServerTestCase |
415 | @@ -182,7 +186,9 @@ |
416 | data=int_settings, instance=make_ngi_instance(cluster)) |
417 | self.assertTrue(form.is_valid()) |
418 | interface = form.save() |
419 | - self.assertEqual('%s-ipv4' % int_settings['interface'], interface.name) |
420 | + self.assertThat( |
421 | + interface.name, |
422 | + StartsWith('%s-ipv4-' % int_settings['interface'])) |
423 | |
424 | def test__disambiguates_IPv6_interface_with_ipv6_suffix(self): |
425 | cluster = factory.make_NodeGroup() |
426 | @@ -458,3 +464,102 @@ |
427 | # print(list(Subnet.objects.all())) |
428 | # self.assertThat(ngi.network, Equals(new_network.cidr)) |
429 | self.assertThat(Subnet.objects.count(), Equals(1)) |
430 | + |
431 | + def test_multiple_subnets_on_single_interface_uses_existing_vlan(self): |
432 | + ng = factory.make_NodeGroup() |
433 | + ngi1 = NodeGroupInterface(nodegroup=ng) |
434 | + form = NodeGroupInterfaceForm(data=dict( |
435 | + interface='eth0', ip='192.168.0.1', subnet_mask='255.255.255.0'), |
436 | + instance=ngi1) |
437 | + self.assertThat(form.is_valid(), Equals(True)) |
438 | + ngi1 = form.save() |
439 | + self.assertIsNotNone(ngi1) |
440 | + ngi2 = NodeGroupInterface(nodegroup=ng) |
441 | + form = NodeGroupInterfaceForm(data=dict( |
442 | + interface='eth0', ip='192.168.1.1', subnet_mask='255.255.255.0'), |
443 | + instance=ngi2) |
444 | + self.assertThat(form.is_valid(), Equals(True)) |
445 | + ngi2 = form.save() |
446 | + self.assertIsNotNone(ngi2) |
447 | + self.assertThat(VLAN.objects.all().count(), Equals(1)) |
448 | + self.assertThat(ngi1.vlan, Equals(ngi2.vlan)) |
449 | + |
450 | + def test_subnet_vlan_creation_uses_default_fabric_if_empty(self): |
451 | + ng = factory.make_NodeGroup() |
452 | + ngi1 = NodeGroupInterface(nodegroup=ng) |
453 | + form = NodeGroupInterfaceForm(data=dict( |
454 | + interface='eth0', ip='192.168.0.1', subnet_mask='255.255.255.0'), |
455 | + instance=ngi1) |
456 | + self.assertThat(form.is_valid(), Equals(True)) |
457 | + ngi1 = form.save() |
458 | + self.assertIsNotNone(ngi1) |
459 | + self.assertThat(Fabric.objects.all().count(), Equals(1)) |
460 | + self.assertThat(ngi1.vlan.fabric.id, Equals(0)) |
461 | + |
462 | + def test_creates_new_fabric_if_alt_subnet_exists_in_default_fabric(self): |
463 | + ng = factory.make_NodeGroup() |
464 | + ngi1 = NodeGroupInterface(nodegroup=ng) |
465 | + form = NodeGroupInterfaceForm(data=dict( |
466 | + interface='eth0', ip='192.168.0.1', subnet_mask='255.255.255.0'), |
467 | + instance=ngi1) |
468 | + self.assertThat(form.is_valid(), Equals(True)) |
469 | + ngi1 = form.save() |
470 | + self.assertIsNotNone(ngi1) |
471 | + self.assertThat(Fabric.objects.all().count(), Equals(1)) |
472 | + self.assertThat(ngi1.vlan.fabric.id, Equals(0)) |
473 | + ngi2 = NodeGroupInterface(nodegroup=ng) |
474 | + form = NodeGroupInterfaceForm(data=dict( |
475 | + interface='eth1', ip='192.168.1.1', subnet_mask='255.255.255.0'), |
476 | + instance=ngi2) |
477 | + self.assertThat(form.is_valid(), Equals(True)) |
478 | + ngi2 = form.save() |
479 | + self.assertIsNotNone(ngi2) |
480 | + self.assertThat(Fabric.objects.all().count(), Equals(2)) |
481 | + # The first NodeGroupInterface we saved should be using the default |
482 | + # Fabric |
483 | + self.assertThat(ngi1.vlan.fabric.id, Equals(0)) |
484 | + self.assertIsNotNone(ngi2.vlan.fabric) |
485 | + |
486 | + def test_creates_vlan_interface_if_interface_type_and_parent_known(self): |
487 | + ng = factory.make_NodeGroup() |
488 | + ngi1 = NodeGroupInterface(nodegroup=ng) |
489 | + form = NodeGroupInterfaceForm(data=dict( |
490 | + interface='eth0', ip='192.168.0.1', subnet_mask='255.255.255.0'), |
491 | + instance=ngi1) |
492 | + self.assertThat(form.is_valid(), Equals(True)) |
493 | + ngi1 = form.save() |
494 | + self.assertIsNotNone(ngi1) |
495 | + self.assertThat(Fabric.objects.all().count(), Equals(1)) |
496 | + self.assertThat(ngi1.vlan.fabric.id, Equals(0)) |
497 | + ngi2 = NodeGroupInterface(nodegroup=ng) |
498 | + form = NodeGroupInterfaceForm(data=dict( |
499 | + interface='vlan12', ip='192.168.1.1', subnet_mask='255.255.255.0', |
500 | + parent='eth0', type='ethernet.vlan'), instance=ngi2) |
501 | + self.assertThat(form.is_valid(), Equals(True)) |
502 | + ngi2 = form.save() |
503 | + self.assertIsNotNone(ngi2) |
504 | + self.assertThat(Fabric.objects.all().count(), Equals(1)) |
505 | + self.assertThat(VLAN.objects.filter(vid=12).count(), Equals(1)) |
506 | + |
507 | + def test_creates_vlan_plus_new_fabric_if_no_parent_untagged_exists(self): |
508 | + ng = factory.make_NodeGroup() |
509 | + ngi1 = NodeGroupInterface(nodegroup=ng) |
510 | + form = NodeGroupInterfaceForm(data=dict( |
511 | + interface='eth0', ip='192.168.0.1', subnet_mask='255.255.255.0'), |
512 | + instance=ngi1) |
513 | + self.assertThat(form.is_valid(), Equals(True)) |
514 | + ngi1 = form.save() |
515 | + self.assertIsNotNone(ngi1) |
516 | + self.assertThat(Fabric.objects.all().count(), Equals(1)) |
517 | + self.assertThat(ngi1.vlan.fabric.id, Equals(0)) |
518 | + ngi2 = NodeGroupInterface(nodegroup=ng) |
519 | + form = NodeGroupInterfaceForm(data=dict( |
520 | + interface='eth0.12', ip='192.168.1.1', subnet_mask='255.255.255.0', |
521 | + type='ethernet.vlan'), instance=ngi2) |
522 | + self.assertThat(form.is_valid(), Equals(True)) |
523 | + ngi2 = form.save() |
524 | + self.assertIsNotNone(ngi2) |
525 | + self.assertThat(Fabric.objects.all().count(), Equals(2)) |
526 | + # Check that VLAN 12 was created on the non-default VLAN. |
527 | + self.assertThat(VLAN.objects.filter( |
528 | + vid=12, fabric__id__gt=0).count(), Equals(1)) |
529 | |
530 | === modified file 'src/maasserver/utils/interfaces.py' |
531 | --- src/maasserver/utils/interfaces.py 2014-11-13 20:11:48 +0000 |
532 | +++ src/maasserver/utils/interfaces.py 2015-10-23 21:41:16 +0000 |
533 | @@ -21,7 +21,7 @@ |
534 | import re |
535 | |
536 | |
537 | -def make_name_from_interface(interface): |
538 | +def make_name_from_interface(interface, alias=None): |
539 | """Generate a cluster interface name based on a network interface name. |
540 | |
541 | The name is used as an identifier in API URLs, so awkward characters are |
542 | @@ -30,6 +30,8 @@ |
543 | |
544 | If `interface` is `None`, or empty, a name will be made up. |
545 | """ |
546 | + if alias: |
547 | + interface = "%s:%s" % (interface, alias) |
548 | if interface is None or interface == u'': |
549 | base_name = u'unnamed-%d' % randint(1000000, 9999999) |
550 | else: |
551 | |
552 | === modified file 'src/provisioningserver/network.py' |
553 | --- src/provisioningserver/network.py 2015-10-21 23:36:45 +0000 |
554 | +++ src/provisioningserver/network.py 2015-10-23 21:41:16 +0000 |
555 | @@ -149,11 +149,10 @@ |
556 | return ip_addr_json |
557 | |
558 | |
559 | -def _filter_likely_physical_networks_using_interface_name(networks): |
560 | +def _filter_managed_networks_by_ifname(networks): |
561 | """ |
562 | - Given the specified list of networks and corresponding JSON, filters |
563 | - the list of networks and returns any that may be physical interfaces |
564 | - (based on the interface name). |
565 | + Given the specified list of networks, filters the list of networks and |
566 | + returns any that may be physical interfaces (based on the interface name). |
567 | |
568 | :param networks: A list of network dictionaries. Must contain an |
569 | 'interface' key containing the interface name. |
570 | @@ -170,12 +169,90 @@ |
571 | ] |
572 | |
573 | |
574 | -def _filter_likely_physical_networks_using_json_data(networks, ip_addr_json): |
575 | +def _annotate_network_with_interface_information(network, addr_info): |
576 | + """Adds a 'type' field to a specified dictionary which represents a network |
577 | + interface. |
578 | + """ |
579 | + iface = addr_info.get(network['interface'], None) |
580 | + if iface is not None and 'type' in iface: |
581 | + network['type'] = iface['type'] |
582 | + if 'vid' in iface: |
583 | + network['vid'] = iface['vid'] |
584 | + if 'bridged_interfaces' in iface: |
585 | + network['bridged_interfaces'] = ' '.join( |
586 | + iface['bridged_interfaces']) |
587 | + if 'bonded_interfaces' in iface: |
588 | + network['bonded_interfaces'] = ' '.join( |
589 | + iface['bonded_interfaces']) |
590 | + if 'parent' in iface: |
591 | + network['parent'] = iface['parent'] |
592 | + return network |
593 | + |
594 | + |
595 | +def _bridges_a_physical_interface(ifname, addr_info): |
596 | + """Returns True if the bridge interface with the specified name bridges |
597 | + at least one physical Ethernet interface. Otherwise, returns False. |
598 | + """ |
599 | + bridge_interface = addr_info.get(ifname) |
600 | + for interface_name in bridge_interface.get('bridged_interfaces', []): |
601 | + iface = addr_info.get(interface_name, {}) |
602 | + if iface.get('type') == 'ethernet.physical': |
603 | + return True |
604 | + return False |
605 | + |
606 | + |
607 | +def _belongs_to_a_vlan(ifname, addr_info): |
608 | + """Returns True if the interface with the specified name is needed |
609 | + because a VLAN interface depends on it. |
610 | + """ |
611 | + for interface_name in addr_info: |
612 | + iface = addr_info.get(interface_name, {}) |
613 | + if iface.get('type') == 'ethernet.vlan': |
614 | + if iface.get('parent') == ifname: |
615 | + return True |
616 | + return False |
617 | + |
618 | + |
619 | +def _network_name(network): |
620 | + """Returns interface name for the specified network. (removes a trailing |
621 | + alias, if present.) |
622 | + """ |
623 | + return network['interface'].split(':')[0] |
624 | + |
625 | + |
626 | +def _should_manage_network(network, addr_info): |
627 | + """Returns True if this network should be managed; otherwise returns False. |
628 | + """ |
629 | + ifname = _network_name(network) |
630 | + addrinfo = addr_info.get(ifname, {}) |
631 | + iftype = addrinfo.get('type', '') |
632 | + # In general, only physical Ethernet interfaces, VLANs, and bonds |
633 | + # are going to be managed. Since they are most likely irrelevant, (and |
634 | + # we don't want them to create superfluous subnets) filter out virtual |
635 | + # interfaces (whose specific type cannot be determined) and bridges. |
636 | + # However, reconsider bridges as "possibly managed" if they are |
637 | + # present in support of a physical Ethernet device, or a VLAN is |
638 | + # defined on top of the bridge. |
639 | + return ( |
640 | + addrinfo and |
641 | + (_belongs_to_a_vlan(ifname, addr_info) or |
642 | + iftype == 'ethernet.physical' or |
643 | + iftype == 'ethernet.vlan' or |
644 | + iftype == 'ethernet.bond' or |
645 | + (iftype == 'ethernet.bridge' and |
646 | + _bridges_a_physical_interface(ifname, addr_info)) |
647 | + ) |
648 | + ) |
649 | + |
650 | + |
651 | +def _filter_and_annotate_managed_networks(networks, ip_addr_json): |
652 | """ |
653 | Given the specified list of networks and corresponding JSON, filters |
654 | the list of networks and returns any that are known to be physical |
655 | interfaces. (based on driver information gathered from /sys) |
656 | |
657 | + Also annotates the list of networks with each network's type. |
658 | + |
659 | :param networks: A list of network dictionaries. Must contain an |
660 | 'interface' key containing the interface name. |
661 | :param ip_addr_json: A JSON string returned from `get_ip_addr_json()`. |
662 | @@ -184,26 +261,26 @@ |
663 | addr_info = json.loads(ip_addr_json) |
664 | assert isinstance(addr_info, dict) |
665 | return [ |
666 | - network |
667 | + _annotate_network_with_interface_information(network, addr_info) |
668 | for network in networks |
669 | - if (network['interface'] in addr_info and |
670 | - 'type' in addr_info[network['interface']] and |
671 | - (addr_info[network['interface']]['type'] == 'ethernet.physical' or |
672 | - addr_info[network['interface']]['type'] == 'ethernet.vlan' or |
673 | - addr_info[network['interface']]['type'] == 'ethernet.bond')) |
674 | + if _should_manage_network(network, addr_info) |
675 | ] |
676 | |
677 | |
678 | -def filter_likely_unmanaged_networks(networks, ip_addr_json=None): |
679 | +def filter_and_annotate_networks(networks, ip_addr_json=None): |
680 | """ |
681 | Given the specified list of networks and optional corresponding JSON, |
682 | - filters the list of networks and returns any that are known to be physical |
683 | - interfaces. |
684 | + filters the list of networks and returns any that may correspond to managed |
685 | + networks. (that is, any physical Ethernet interfaces, plus bonds and |
686 | + VLANs.) |
687 | |
688 | If no interfaces are found, fall back to using the interface name to |
689 | filter the list in a reasonable manner. (this allows support for running |
690 | on LXCs, where all interfaces may be virtual.) |
691 | |
692 | + Also annotates the list of networks with their type, and other metadata |
693 | + such as VLAN VID, bonded/bridged interfaces, or parent. |
694 | + |
695 | :param networks: A list of network dictionaries. Must contain an |
696 | 'interface' key containing the interface name. |
697 | :param ip_addr_json: A JSON string returned from `get_ip_addr_json()`. |
698 | @@ -211,9 +288,9 @@ |
699 | """ |
700 | assert networks is not None |
701 | if ip_addr_json is None: |
702 | - return _filter_likely_physical_networks_using_interface_name(networks) |
703 | + return _filter_managed_networks_by_ifname(networks) |
704 | else: |
705 | - physical_networks = _filter_likely_physical_networks_using_json_data( |
706 | + physical_networks = _filter_and_annotate_managed_networks( |
707 | networks, ip_addr_json) |
708 | if len(physical_networks) > 0: |
709 | return physical_networks |
710 | @@ -221,5 +298,57 @@ |
711 | # Since we couldn't find anything, fall back to using the heuristic |
712 | # based on names. (we could be running inside a container with only |
713 | # virtual interfaces, etc.) |
714 | - return _filter_likely_physical_networks_using_interface_name( |
715 | + return _filter_managed_networks_by_ifname( |
716 | networks) |
717 | + |
718 | + |
719 | +def _get_interface_type_priority(iface): |
720 | + """Returns a sort key based on interface types we prefer to process |
721 | + first when adding them to a NodeGroup. |
722 | + |
723 | + The most important thing is that we need to process VLANs last, since they |
724 | + require the underlying Fabric to be created first. |
725 | + """ |
726 | + iftype = iface.get('type') |
727 | + # Physical interfaces first, followed by bonds, followed by bridges. |
728 | + # VLAN interfaces last. |
729 | + # This will ensure that underlying Fabric objects can be created before |
730 | + # any VLANs that may belong to each Fabric. |
731 | + if iftype == "ethernet.physical": |
732 | + return 0 |
733 | + elif iftype == "ethernet.wireless": |
734 | + return 1 |
735 | + elif iftype == "ethernet": |
736 | + return 2 |
737 | + elif iftype == "ethernet.bond": |
738 | + return 3 |
739 | + elif iftype == "ethernet.bridge": |
740 | + return 4 |
741 | + elif iftype == "ethernet.vlan": |
742 | + return 5 |
743 | + else: |
744 | + # We don't really care what the sort order is; they should be filtered |
745 | + # out anyway. |
746 | + return -1 |
747 | + |
748 | + |
749 | +def _network_priority_sort_key(iface): |
750 | + """Returns a sort key used for processing interfaces before adding them |
751 | + to a NodeGroup. |
752 | + |
753 | + First sorts by interface type, then interface name, then address family. |
754 | + (Since MAAS usually manages IPv4 addresses, and we have a name |
755 | + disambiguation funciton that can produce somewhat unfriendly names, |
756 | + make sure the IPv4 interfaces get to go first.) |
757 | + """ |
758 | + return ( |
759 | + _get_interface_type_priority(iface), |
760 | + iface['interface'], |
761 | + IPAddress(iface['ip']).version |
762 | + ) |
763 | + |
764 | + |
765 | +def sort_networks_by_priority(networks): |
766 | + """Sorts the specified list of networks in the order in which we would |
767 | + prefer to add them to a NodeGroup.""" |
768 | + return sorted(networks, key=_network_priority_sort_key) |
769 | |
770 | === modified file 'src/provisioningserver/rpc/clusterservice.py' |
771 | --- src/provisioningserver/rpc/clusterservice.py 2015-10-21 18:38:54 +0000 |
772 | +++ src/provisioningserver/rpc/clusterservice.py 2015-10-23 21:41:16 +0000 |
773 | @@ -525,6 +525,7 @@ |
774 | |
775 | networks = discover_networks() |
776 | |
777 | + ip_addr_json = None |
778 | try: |
779 | ip_addr_json = get_ip_addr_json() |
780 | except ExternalProcessError as epe: |
781 | |
782 | === modified file 'src/provisioningserver/tests/test_network.py' |
783 | --- src/provisioningserver/tests/test_network.py 2015-10-21 23:36:45 +0000 |
784 | +++ src/provisioningserver/tests/test_network.py 2015-10-23 21:41:16 +0000 |
785 | @@ -25,7 +25,11 @@ |
786 | AF_INET6, |
787 | ) |
788 | from provisioningserver import network |
789 | -from testtools.matchers import HasLength |
790 | +from provisioningserver.network import sort_networks_by_priority |
791 | +from testtools.matchers import ( |
792 | + Equals, |
793 | + HasLength, |
794 | +) |
795 | |
796 | |
797 | def make_inet_address(subnet=None): |
798 | @@ -216,3 +220,31 @@ |
799 | self.assertEqual( |
800 | network.filter_unique_networks(networks), |
801 | network.filter_unique_networks(reversed(networks))) |
802 | + |
803 | + |
804 | +class TestSortNetworksByPriority(MAASTestCase): |
805 | + |
806 | + def test__sorts_by_type_then_ip_version(self): |
807 | + interfaces = [ |
808 | + {'ip': "2001:db8::1", |
809 | + 'type': "ethernet.vlan", |
810 | + 'interface': 'vlan40'}, |
811 | + {'ip': "10.0.0.1", |
812 | + 'type': "ethernet.vlan", |
813 | + 'interface': 'vlan40'}, |
814 | + {'ip': "2001:db8:1::1", |
815 | + 'type': "ethernet.physical", |
816 | + 'interface': 'eth1'}, |
817 | + {'ip': "10.0.1.1", |
818 | + 'type': "ethernet.physical", |
819 | + 'interface': 'eth1'}, |
820 | + {'ip': "10.0.2.1", |
821 | + 'type': "ethernet.bridge", |
822 | + 'interface': 'br0'}, |
823 | + ] |
824 | + sorted_interfaces = sort_networks_by_priority(interfaces) |
825 | + self.expectThat(sorted_interfaces[0], Equals(interfaces[3])) |
826 | + self.expectThat(sorted_interfaces[1], Equals(interfaces[2])) |
827 | + self.expectThat(sorted_interfaces[2], Equals(interfaces[4])) |
828 | + self.expectThat(sorted_interfaces[3], Equals(interfaces[1])) |
829 | + self.expectThat(sorted_interfaces[4], Equals(interfaces[0])) |
830 | |
831 | === modified file 'src/provisioningserver/utils/ipaddr.py' |
832 | --- src/provisioningserver/utils/ipaddr.py 2015-10-22 00:48:38 +0000 |
833 | +++ src/provisioningserver/utils/ipaddr.py 2015-10-23 21:41:16 +0000 |
834 | @@ -108,7 +108,10 @@ |
835 | string.strip, line.split(':')) |
836 | |
837 | interface['index'] = int(index) |
838 | - interface['name'] = name.split('@')[0] |
839 | + names = name.split('@') |
840 | + interface['name'] = names[0] |
841 | + if len(names) > 1: |
842 | + interface['parent'] = names[1] |
843 | |
844 | # Now parse the <properties> part from above. |
845 | # This will be in the form "<FLAG1,FLAG2> key1 value1 key2 value2 ..." |
846 | @@ -196,6 +199,35 @@ |
847 | raise ValueError("Unknown IP address family: %s" % network.version) |
848 | |
849 | |
850 | +def get_bonded_interfaces(ifname, sys_class_net="/sys/class/net"): |
851 | + """Returns a list of interface names which are part of the specified |
852 | + Ethernet bond. |
853 | + |
854 | + :return:list |
855 | + """ |
856 | + bonding_slaves_file = os.path.join( |
857 | + sys_class_net, ifname, 'bonding', 'slaves') |
858 | + if os.path.isfile(bonding_slaves_file): |
859 | + with open(bonding_slaves_file) as f: |
860 | + return f.read().split() |
861 | + else: |
862 | + return [] |
863 | + |
864 | + |
865 | +def get_bridged_interfaces(ifname, sys_class_net="/sys/class/net"): |
866 | + """Returns a list of interface names which are part of the specified |
867 | + Ethernet bridge interface. |
868 | + |
869 | + :return:list |
870 | + """ |
871 | + bridged_interfaces_dir = os.path.join( |
872 | + sys_class_net, ifname, 'brif') |
873 | + if os.path.isdir(bridged_interfaces_dir): |
874 | + return os.listdir(bridged_interfaces_dir) |
875 | + else: |
876 | + return [] |
877 | + |
878 | + |
879 | def get_interface_type( |
880 | ifname, sys_class_net="/sys/class/net", |
881 | proc_net_vlan='/proc/net/vlan'): |
882 | @@ -229,11 +261,11 @@ |
883 | If the interface can be determined to be a non-Ethernet type, the type |
884 | that is found will be returned. (For example, 'loopback' or 'ipip'.) |
885 | """ |
886 | - sys_path = '%s/%s' % (sys_class_net, ifname) |
887 | + sys_path = os.path.join(sys_class_net, ifname) |
888 | if not os.path.isdir(sys_path): |
889 | return 'missing' |
890 | |
891 | - sys_type_path = '%s/type' % sys_path |
892 | + sys_type_path = os.path.join(sys_path, 'type') |
893 | with open(sys_type_path) as f: |
894 | iftype = int(f.read().strip()) |
895 | |
896 | @@ -278,5 +310,32 @@ |
897 | (if found) for each interface. |
898 | """ |
899 | for name in interfaces: |
900 | - interfaces[name]['type'] = get_interface_type(name) |
901 | + iface = interfaces[name] |
902 | + iftype = get_interface_type(name) |
903 | + interfaces[name]['type'] = iftype |
904 | + if iftype == 'ethernet.bond': |
905 | + iface['bonded_interfaces'] = get_bonded_interfaces(name) |
906 | + elif iftype == 'ethernet.vlan': |
907 | + iface['vid'] = get_vid_from_ifname(name) |
908 | + elif iftype == 'ethernet.bridge': |
909 | + iface['bridged_interfaces'] = get_bridged_interfaces(name) |
910 | return interfaces |
911 | + |
912 | + |
913 | +def get_vid_from_ifname(ifname): |
914 | + """Returns the VID for the specified VLAN interface name. |
915 | + |
916 | + Returns 0 if the VID could not be determined. |
917 | + |
918 | + :return:int |
919 | + """ |
920 | + vid = 0 |
921 | + iface_vid_re = re.compile('.*\.([0-9]+)$') |
922 | + iface_vid_match = iface_vid_re.match(ifname) |
923 | + vlan_vid_re = re.compile('vlan([0-9]+)$') |
924 | + vlan_vid_match = vlan_vid_re.match(ifname) |
925 | + if iface_vid_match: |
926 | + vid = int(iface_vid_match.group(1)) |
927 | + elif vlan_vid_match: |
928 | + vid = int(vlan_vid_match.group(1)) |
929 | + return vid |
930 | |
931 | === modified file 'src/provisioningserver/utils/tests/test_ipaddr.py' |
932 | --- src/provisioningserver/utils/tests/test_ipaddr.py 2015-10-22 00:48:38 +0000 |
933 | +++ src/provisioningserver/utils/tests/test_ipaddr.py 2015-10-23 21:41:16 +0000 |
934 | @@ -22,12 +22,13 @@ |
935 | from textwrap import dedent |
936 | |
937 | from maastesting.testcase import MAASTestCase |
938 | -from provisioningserver.network import filter_likely_unmanaged_networks |
939 | +from provisioningserver.network import filter_and_annotate_networks |
940 | from provisioningserver.utils.ipaddr import ( |
941 | _add_additional_interface_properties, |
942 | _get_settings_dict, |
943 | _parse_interface_definition, |
944 | annotate_with_driver_information, |
945 | + get_bonded_interfaces, |
946 | get_interface_type, |
947 | parse_ip_addr, |
948 | ) |
949 | @@ -328,7 +329,8 @@ |
950 | |
951 | def createInterfaceType( |
952 | self, ifname, iftype, is_bridge=False, is_vlan=False, |
953 | - is_bond=False, is_wireless=False, is_physical=False): |
954 | + is_bond=False, is_wireless=False, is_physical=False, |
955 | + bonded_interfaces=None): |
956 | ifdir = os.path.join(self.tmp_sys_net, ifname) |
957 | os.mkdir(ifdir) |
958 | type_file = os.path.join(ifdir, 'type') |
959 | @@ -342,6 +344,10 @@ |
960 | f.close() |
961 | if is_bond: |
962 | os.mkdir(os.path.join(ifdir, 'bonding')) |
963 | + if bonded_interfaces is not None: |
964 | + f = open(os.path.join(ifdir, 'bonding', 'slaves'), 'w') |
965 | + f.write(b"%s\n" % ' '.join(bonded_interfaces).encode('utf-8')) |
966 | + f.close() |
967 | if is_physical or is_wireless: |
968 | os.mkdir(os.path.join(ifdir, 'device')) |
969 | os.mkdir(os.path.join(ifdir, 'device', 'driver')) |
970 | @@ -384,6 +390,14 @@ |
971 | Equals('ethernet.bond') |
972 | ) |
973 | |
974 | + def test__identifies_bonded_interfaces(self): |
975 | + self.createEthernetInterface( |
976 | + 'bond0', is_bond=True, bonded_interfaces=['eth0', 'eth1']) |
977 | + self.assertThat(get_bonded_interfaces( |
978 | + 'bond0', sys_class_net=self.tmp_sys_net), |
979 | + Equals(['eth0', 'eth1']) |
980 | + ) |
981 | + |
982 | def test__identifies_vlan_interface(self): |
983 | self.createEthernetInterface('vlan42', is_vlan=True) |
984 | self.assertThat(get_interface_type( |
985 | @@ -452,7 +466,14 @@ |
986 | interfaces = parse_ip_addr(ip_addr_output) |
987 | interfaces_with_types = annotate_with_driver_information(interfaces) |
988 | for name in interfaces: |
989 | - self.assertThat(interfaces_with_types[name], Contains('type')) |
990 | + iface = interfaces_with_types[name] |
991 | + self.assertThat(iface, Contains('type')) |
992 | + if iface['type'] == 'ethernet.vlan': |
993 | + self.expectThat(iface, Contains('vid')) |
994 | + elif iface['type'] == 'ethernet.bond': |
995 | + self.expectThat(iface, Contains('bonded_interfaces')) |
996 | + elif iface['type'] == 'ethernet.bridge': |
997 | + self.expectThat(iface, Contains('bridged_interfaces')) |
998 | |
999 | |
1000 | class TestFilterLikelyUnmanagedNetworks(MAASTestCase): |
1001 | @@ -467,7 +488,7 @@ |
1002 | {"interface": "wlan0"}, |
1003 | {"interface": "avian0"}, |
1004 | ] |
1005 | - actual_networks = filter_likely_unmanaged_networks(input_networks) |
1006 | + actual_networks = filter_and_annotate_networks(input_networks) |
1007 | expected_networks = [ |
1008 | {"interface": "em0"}, |
1009 | {"interface": "eth0"}, |
1010 | @@ -476,7 +497,7 @@ |
1011 | ] |
1012 | self.assertThat(actual_networks, Equals(expected_networks)) |
1013 | |
1014 | - def test__filters_based_on_json_data_if_available(self): |
1015 | + def test__filters_and_annotates_based_on_json_data_if_available(self): |
1016 | input_networks = [ |
1017 | {"interface": "em0"}, |
1018 | {"interface": "eth0"}, |
1019 | @@ -484,21 +505,35 @@ |
1020 | {"interface": "bond0"}, |
1021 | {"interface": "avian0"}, |
1022 | {"interface": "br0"}, |
1023 | + {"interface": "br1"}, |
1024 | + {"interface": "br2"}, |
1025 | + {"interface": "br3"}, |
1026 | {"interface": "wlan0"}, |
1027 | ] |
1028 | # Wow, these are some poorly named interfaces. |
1029 | # Though I guess technically an avian carrier is a physical interface. |
1030 | input_json = { |
1031 | "avian0": {"type": "ethernet.physical"}, |
1032 | - "br0": {"type": "ethernet.vlan"}, |
1033 | - "wlan0": {"type": "ethernet.bond"} |
1034 | + "br0": {"type": "ethernet.vlan", "vid": 123, "parent": "br3"}, |
1035 | + "br1": {"type": "ethernet.bridge", |
1036 | + "bridged_interfaces": ["eth1", "eth2"]}, |
1037 | + "br2": {"type": "ethernet.bridge", |
1038 | + "bridged_interfaces": ["avian0"]}, |
1039 | + "br3": {"type": "ethernet.bridge"}, |
1040 | + "wlan0": {"type": "ethernet.bond", |
1041 | + "bonded_interfaces": ["em0", "eth0"]} |
1042 | } |
1043 | - actual_networks = filter_likely_unmanaged_networks( |
1044 | + actual_networks = filter_and_annotate_networks( |
1045 | input_networks, json.dumps(input_json)) |
1046 | expected_networks = [ |
1047 | - {"interface": "avian0"}, |
1048 | - {"interface": "br0"}, |
1049 | - {"interface": "wlan0"}, |
1050 | + {"interface": "avian0", 'type': "ethernet.physical"}, |
1051 | + {"interface": "br0", 'type': "ethernet.vlan", "vid": 123, |
1052 | + "parent": "br3"}, |
1053 | + {"interface": "br2", 'type': "ethernet.bridge", |
1054 | + "bridged_interfaces": "avian0"}, |
1055 | + {"interface": "br3", 'type': "ethernet.bridge"}, |
1056 | + {"interface": "wlan0", 'type': "ethernet.bond", |
1057 | + "bonded_interfaces": "em0 eth0"}, |
1058 | ] |
1059 | self.assertThat(actual_networks, Equals(expected_networks)) |
1060 | |
1061 | @@ -514,7 +549,7 @@ |
1062 | ] |
1063 | input_json = { |
1064 | } |
1065 | - actual_networks = filter_likely_unmanaged_networks( |
1066 | + actual_networks = filter_and_annotate_networks( |
1067 | input_networks, json.dumps(input_json)) |
1068 | expected_networks = [ |
1069 | {"interface": "em0"}, |
This still needs more unit tests written. But I'd like some additional eyes on this code before I move forward.
I've been testing this end-to-end and it seems to do what we want, at least for creating VLANs and fabrics automatically when the cluster registers.
Adding cluster interfaces by hand, (and editing existing interfaces to make changes that SHOULD probably put them in a different VLAN or Fabric) will most likely do the wrong thing still.