Merge lp:~blake-rouse/maas/nic-no-fabric into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 5205
Proposed branch: lp:~blake-rouse/maas/nic-no-fabric
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 858 lines (+389/-69)
16 files modified
src/maasserver/api/interfaces.py (+8/-4)
src/maasserver/api/tests/test_interfaces.py (+2/-3)
src/maasserver/forms_interface.py (+22/-9)
src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py (+1/-1)
src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py (+23/-0)
src/maasserver/models/interface.py (+27/-16)
src/maasserver/models/node.py (+4/-0)
src/maasserver/models/signals/interfaces.py (+18/-15)
src/maasserver/models/signals/tests/test_interfaces.py (+22/-0)
src/maasserver/models/tests/test_interface.py (+31/-0)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+13/-3)
src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+67/-0)
src/maasserver/static/partials/node-details.html (+4/-1)
src/maasserver/testing/factory.py (+16/-15)
src/maasserver/tests/test_forms_interface.py (+129/-1)
src/maasserver/websockets/handlers/node.py (+2/-1)
To merge this branch: bzr merge lp:~blake-rouse/maas/nic-no-fabric
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+300381@code.launchpad.net

Commit message

Add the ability for an interface to be disconnected (not connected to any VLAN).

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for taking on this much-needed change.

I've been testing this code out on my local MAAS, and it's working great. I really like how we don't drop new nodes into a default fabric/VLAN any more. Land it!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.4 MiB)

The attempt to merge lp:~blake-rouse/maas/nic-no-fabric into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Hit:2 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [95.7 kB]
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [300 kB]
Fetched 395 kB in 0s (795 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5....

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/interfaces.py'
--- src/maasserver/api/interfaces.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/api/interfaces.py 2016-07-26 10:11:30 +0000
@@ -109,7 +109,8 @@
109 :param name: Name of the interface.109 :param name: Name of the interface.
110 :param mac_address: MAC address of the interface.110 :param mac_address: MAC address of the interface.
111 :param tags: Tags for the interface.111 :param tags: Tags for the interface.
112 :param vlan: Untagged VLAN the interface is connected to.112 :param vlan: Untagged VLAN the interface is connected to. If not
113 provided then the interface is considered disconnected.
113114
114 Following are extra parameters that can be set on the interface:115 Following are extra parameters that can be set on the interface:
115116
@@ -151,7 +152,8 @@
151 :param name: Name of the interface.152 :param name: Name of the interface.
152 :param mac_address: MAC address of the interface.153 :param mac_address: MAC address of the interface.
153 :param tags: Tags for the interface.154 :param tags: Tags for the interface.
154 :param vlan: VLAN the interface is connected to.155 :param vlan: VLAN the interface is connected to. If not
156 provided then the interface is considered disconnected.
155 :param parents: Parent interfaces that make this bond.157 :param parents: Parent interfaces that make this bond.
156158
157 Following are parameters specific to bonds:159 Following are parameters specific to bonds:
@@ -318,13 +320,15 @@
318 :param name: Name of the interface.320 :param name: Name of the interface.
319 :param mac_address: MAC address of the interface.321 :param mac_address: MAC address of the interface.
320 :param tags: Tags for the interface.322 :param tags: Tags for the interface.
321 :param vlan: Untagged VLAN the interface is connected to.323 :param vlan: Untagged VLAN the interface is connected to. If not set
324 then the interface is considered disconnected.
322325
323 Fields for bond interface:326 Fields for bond interface:
324 :param name: Name of the interface.327 :param name: Name of the interface.
325 :param mac_address: MAC address of the interface.328 :param mac_address: MAC address of the interface.
326 :param tags: Tags for the interface.329 :param tags: Tags for the interface.
327 :param vlan: Tagged VLAN the interface is connected to.330 :param vlan: Tagged VLAN the interface is connected to. If not set
331 then the interface is considered disconnected.
328 :param parents: Parent interfaces that make this bond.332 :param parents: Parent interfaces that make this bond.
329333
330 Fields for VLAN interface:334 Fields for VLAN interface:
331335
=== modified file 'src/maasserver/api/tests/test_interfaces.py'
--- src/maasserver/api/tests/test_interfaces.py 2016-05-26 13:28:46 +0000
+++ src/maasserver/api/tests/test_interfaces.py 2016-07-26 10:11:30 +0000
@@ -257,7 +257,7 @@
257 self.assertEqual(257 self.assertEqual(
258 http.client.CONFLICT, response.status_code, response.content)258 http.client.CONFLICT, response.status_code, response.content)
259259
260 def test_create_physical_requires_mac_name_and_vlan(self):260 def test_create_physical_requires_mac_and_name(self):
261 self.become_admin()261 self.become_admin()
262 node = factory.make_Node(status=NODE_STATUS.READY)262 node = factory.make_Node(status=NODE_STATUS.READY)
263 uri = get_interfaces_uri(node)263 uri = get_interfaces_uri(node)
@@ -269,7 +269,6 @@
269 self.assertEqual({269 self.assertEqual({
270 "mac_address": ["This field is required."],270 "mac_address": ["This field is required."],
271 "name": ["This field is required."],271 "name": ["This field is required."],
272 "vlan": ["This field is required."],
273 }, json_load_bytes(response.content))272 }, json_load_bytes(response.content))
274273
275 def test_create_physical_doesnt_allow_mac_already_register(self):274 def test_create_physical_doesnt_allow_mac_already_register(self):
@@ -490,7 +489,7 @@
490 self.assertEqual(489 self.assertEqual(
491 http.client.BAD_REQUEST, response.status_code, response.content)490 http.client.BAD_REQUEST, response.status_code, response.content)
492 self.assertEqual({491 self.assertEqual({
493 "vlan": ["This field is required."],492 "vlan": ["A VLAN interface must be connected to a tagged VLAN."],
494 "parent": ["A VLAN interface must have exactly one parent."],493 "parent": ["A VLAN interface must have exactly one parent."],
495 }, json_load_bytes(response.content))494 }, json_load_bytes(response.content))
496495
497496
=== modified file 'src/maasserver/forms_interface.py'
--- src/maasserver/forms_interface.py 2016-04-22 17:28:15 +0000
+++ src/maasserver/forms_interface.py 2016-07-26 10:11:30 +0000
@@ -99,6 +99,11 @@
99 rel = interface.parent_relationships.filter(99 rel = interface.parent_relationships.filter(
100 parent=parent_to_del)100 parent=parent_to_del)
101 rel.delete()101 rel.delete()
102 # Allow setting the VLAN to None.
103 new_vlan = self.cleaned_data.get('vlan')
104 vlan_was_set = 'vlan' in self.data
105 if new_vlan is None and vlan_was_set:
106 interface.vlan = new_vlan
102 self.set_extra_parameters(interface, created)107 self.set_extra_parameters(interface, created)
103 interface.save()108 interface.save()
104 if created:109 if created:
@@ -168,6 +173,17 @@
168 'vlan',173 'vlan',
169 )174 )
170175
176 def save(self, *args, **kwargs):
177 """Persist the interface into the database."""
178 interface = super(ControllerInterfaceForm, self).save(commit=False)
179 # Allow setting the VLAN to None.
180 new_vlan = self.cleaned_data.get('vlan')
181 vlan_was_set = 'vlan' in self.data
182 if new_vlan is None and vlan_was_set:
183 interface.vlan = new_vlan
184 interface.save()
185 return interface
186
171187
172class PhysicalInterfaceForm(InterfaceForm):188class PhysicalInterfaceForm(InterfaceForm):
173 """Form used to create/edit a physical interface."""189 """Form used to create/edit a physical interface."""
@@ -251,7 +267,13 @@
251 return parents267 return parents
252268
253 def clean_vlan(self):269 def clean_vlan(self):
270 created = self.instance.id is None
254 new_vlan = self.cleaned_data.get('vlan')271 new_vlan = self.cleaned_data.get('vlan')
272 vlan_was_set = 'vlan' in self.data
273 if (created and new_vlan is None) or (
274 not created and new_vlan is None and vlan_was_set):
275 raise ValidationError(
276 "A VLAN interface must be connected to a tagged VLAN.")
255 if new_vlan and new_vlan.fabric.get_default_vlan() == new_vlan:277 if new_vlan and new_vlan.fabric.get_default_vlan() == new_vlan:
256 raise ValidationError(278 raise ValidationError(
257 "A VLAN interface can only belong to a tagged VLAN.")279 "A VLAN interface can only belong to a tagged VLAN.")
@@ -281,15 +303,6 @@
281 bridges.303 bridges.
282 """304 """
283305
284 def __init__(self, *args, **kwargs):
285 super().__init__(*args, **kwargs)
286 # Allow VLAN to be blank when creating.
287 instance = kwargs.get("instance", None)
288 if instance is not None and instance.id is not None:
289 self.fields['vlan'].required = True
290 else:
291 self.fields['vlan'].required = False
292
293 def clean_parents(self):306 def clean_parents(self):
294 """Validate that child interfaces cannot be created unless at least one307 """Validate that child interfaces cannot be created unless at least one
295 parent is present.308 parent is present.
296309
=== modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py'
--- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-05-11 19:01:48 +0000
+++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-26 10:11:30 +0000
@@ -39,7 +39,7 @@
39 migrations.AlterField(39 migrations.AlterField(
40 model_name='interface',40 model_name='interface',
41 name='vlan',41 name='vlan',
42 field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.interface.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),42 field=models.ForeignKey(to='maasserver.VLAN', default=None, on_delete=django.db.models.deletion.PROTECT),
43 ),43 ),
44 migrations.AlterField(44 migrations.AlterField(
45 model_name='subnet',45 model_name='subnet',
4646
=== added file 'src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py'
--- src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py 2016-07-26 10:11:30 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9
10
11class Migration(migrations.Migration):
12
13 dependencies = [
14 ('maasserver', '0069_add_previous_node_status_to_node'),
15 ]
16
17 operations = [
18 migrations.AlterField(
19 model_name='interface',
20 name='vlan',
21 field=models.ForeignKey(null=True, blank=True, to='maasserver.VLAN', on_delete=django.db.models.deletion.PROTECT),
22 ),
23 ]
024
=== modified file 'src/maasserver/models/interface.py'
--- src/maasserver/models/interface.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/models/interface.py 2016-07-26 10:11:30 +0000
@@ -67,11 +67,6 @@
67INTERFACE_NAME_REGEXP = '^[\w\-_.:]+$'67INTERFACE_NAME_REGEXP = '^[\w\-_.:]+$'
6868
6969
70def get_default_vlan():
71 from maasserver.models.vlan import VLAN
72 return VLAN.objects.get_default_vlan().id
73
74
75def get_subnet_family(subnet):70def get_subnet_family(subnet):
76 """Return the IPADDRESS_FAMILY for the `subnet`."""71 """Return the IPADDRESS_FAMILY for the `subnet`."""
77 if subnet is not None:72 if subnet is not None:
@@ -375,8 +370,7 @@
375 through='InterfaceRelationship', symmetrical=False)370 through='InterfaceRelationship', symmetrical=False)
376371
377 vlan = ForeignKey(372 vlan = ForeignKey(
378 'VLAN', default=get_default_vlan, editable=True, blank=False,373 'VLAN', editable=True, blank=True, null=True, on_delete=PROTECT)
379 null=False, on_delete=PROTECT)
380374
381 ip_addresses = ManyToManyField(375 ip_addresses = ManyToManyField(
382 'StaticIPAddress', editable=True, blank=True)376 'StaticIPAddress', editable=True, blank=True)
@@ -438,8 +432,13 @@
438 mtu = None432 mtu = None
439 if self.params:433 if self.params:
440 mtu = self.params.get('mtu', None)434 mtu = self.params.get('mtu', None)
435 if mtu is None and self.vlan is not None:
436 mtu = self.vlan.mtu
441 if mtu is None:437 if mtu is None:
442 mtu = self.vlan.mtu438 # Use default MTU for the interface when the interface has no
439 # MTU set and it is disconnected.
440 from maasserver.models.vlan import DEFAULT_MTU
441 mtu = DEFAULT_MTU
443 return mtu442 return mtu
444443
445 def get_links(self):444 def get_links(self):
@@ -752,11 +751,12 @@
752 # subnets into the default VLAN, this assumption might be incorrect in751 # subnets into the default VLAN, this assumption might be incorrect in
753 # many cases, leading to interfaces being configured as AUTO when752 # many cases, leading to interfaces being configured as AUTO when
754 # they should be configured as DHCP.753 # they should be configured as DHCP.
755 found_subnet = self.vlan.subnet_set.first()754 if self.vlan is not None:
756 if found_subnet is not None:755 found_subnet = self.vlan.subnet_set.first()
757 return self.link_subnet(INTERFACE_LINK_TYPE.AUTO, found_subnet)756 if found_subnet is not None:
758 else:757 return self.link_subnet(INTERFACE_LINK_TYPE.AUTO, found_subnet)
759 return self.link_subnet(INTERFACE_LINK_TYPE.DHCP, None)758 else:
759 return self.link_subnet(INTERFACE_LINK_TYPE.DHCP, None)
760760
761 def ensure_link_up(self):761 def ensure_link_up(self):
762 """Ensure that if no subnet links exists that at least a LINK_UP762 """Ensure that if no subnet links exists that at least a LINK_UP
@@ -766,7 +766,7 @@
766 if has_links:766 if has_links:
767 # Nothing to do, already has links.767 # Nothing to do, already has links.
768 return768 return
769 else:769 elif self.vlan is not None:
770 # Use an associated subnet if it exists and its on the same VLAN770 # Use an associated subnet if it exists and its on the same VLAN
771 # the interface is currently connected, else it will just be a771 # the interface is currently connected, else it will just be a
772 # LINK_UP without a subnet.772 # LINK_UP without a subnet.
@@ -1265,20 +1265,31 @@
1265 "parents": ["VLAN interface must have exactly one parent."]1265 "parents": ["VLAN interface must have exactly one parent."]
1266 })1266 })
1267 parent = parents[0]1267 parent = parents[0]
1268 # We do not allow a bridge interface to be a parent for a VLAN
1269 # interface.
1268 allowed_vlan_parent_types = (1270 allowed_vlan_parent_types = (
1269 INTERFACE_TYPE.PHYSICAL,1271 INTERFACE_TYPE.PHYSICAL,
1270 INTERFACE_TYPE.BOND,1272 INTERFACE_TYPE.BOND,
1271 INTERFACE_TYPE.BRIDGE1273 INTERFACE_TYPE.BRIDGE
1272 )1274 )
1273 if parent.get_type() not in allowed_vlan_parent_types:1275 if parent.get_type() not in allowed_vlan_parent_types:
1274 # XXX mpontillo 2016-06-23: we won't mention bridges in this1276 # XXX blake_r 2016-07-18: we won't mention bridges in this
1275 # error message, since users can't configure bridges on nodes.1277 # error message, since users can't configure VLAN interfaces
1278 # on bridges.
1276 raise ValidationError({1279 raise ValidationError({
1277 "parents": [1280 "parents": [
1278 "VLAN interface can only be created on a physical "1281 "VLAN interface can only be created on a physical "
1279 "or bond interface."1282 "or bond interface."
1280 ]1283 ]
1281 })1284 })
1285 # VLAN interface must be connected to a VLAN, it cannot be
1286 # disconnected like physical and bond interfaces.
1287 if self.vlan is None:
1288 raise ValidationError({
1289 "vlan": [
1290 "VLAN interface requires connection to a VLAN."
1291 ]
1292 })
12821293
1283 def save(self, *args, **kwargs):1294 def save(self, *args, **kwargs):
1284 # Set the node of this VLAN to the same as its parents.1295 # Set the node of this VLAN to the same as its parents.
12851296
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2016-07-25 20:13:54 +0000
+++ src/maasserver/models/node.py 2016-07-26 10:11:30 +0000
@@ -3407,6 +3407,10 @@
3407 new_vlan = new_fabric.get_default_vlan()3407 new_vlan = new_fabric.get_default_vlan()
3408 interface.vlan = new_vlan3408 interface.vlan = new_vlan
3409 interface.save()3409 interface.save()
3410 else:
3411 interface.vlan = (
3412 Fabric.objects.get_default_fabric().get_default_vlan())
3413 interface.save()
3410 else:3414 else:
3411 if interface.node.id != self.id:3415 if interface.node.id != self.id:
3412 # MAC address was on a different node. We need to move3416 # MAC address was on a different node. We need to move
34133417
=== modified file 'src/maasserver/models/signals/interfaces.py'
--- src/maasserver/models/signals/interfaces.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/models/signals/interfaces.py 2016-07-26 10:11:30 +0000
@@ -197,22 +197,25 @@
197 NODE_TYPE.RACK_CONTROLLER,197 NODE_TYPE.RACK_CONTROLLER,
198 NODE_TYPE.REGION_AND_RACK_CONTROLLER):198 NODE_TYPE.REGION_AND_RACK_CONTROLLER):
199 # Interface VLAN was changed on a controller. Move all linked subnets199 # Interface VLAN was changed on a controller. Move all linked subnets
200 # to that new VLAN.200 # to that new VLAN, unless the new VLAN is None. When the VLAN is
201 for ip_address in instance.ip_addresses.all():201 # None then the administrator is say that the interface is now
202 if ip_address.subnet is not None:202 # disconnected.
203 ip_address.subnet.vlan = new_vlan203 if new_vlan is not None:
204 ip_address.subnet.save()204 for ip_address in instance.ip_addresses.all():
205 if ip_address.subnet is not None:
206 ip_address.subnet.vlan = new_vlan
207 ip_address.subnet.save()
205208
206 # If any children are VLAN interfaces then we need to move those209 # If any children are VLAN interfaces then we need to move those
207 # VLANs into the same fabric as the parent.210 # VLANs into the same fabric as the parent.
208 for rel in instance.children_relationships.all():211 for rel in instance.children_relationships.all():
209 if rel.child.type == INTERFACE_TYPE.VLAN:212 if rel.child.type == INTERFACE_TYPE.VLAN:
210 new_child_vlan, _ = VLAN.objects.get_or_create(213 new_child_vlan, _ = VLAN.objects.get_or_create(
211 fabric=new_vlan.fabric, vid=rel.child.vlan.vid)214 fabric=new_vlan.fabric, vid=rel.child.vlan.vid)
212 rel.child.vlan = new_child_vlan215 rel.child.vlan = new_child_vlan
213 rel.child.save()216 rel.child.save()
214 # No need to update the IP addresses here this function217 # No need to update the IP addresses here this function
215 # will be called again because the child has been saved.218 # will be called again because the child has been saved.
216219
217 else:220 else:
218 # Interface VLAN was changed on a machine or device. Remove all its221 # Interface VLAN was changed on a machine or device. Remove all its
219222
=== modified file 'src/maasserver/models/signals/tests/test_interfaces.py'
--- src/maasserver/models/signals/tests/test_interfaces.py 2016-04-11 16:23:26 +0000
+++ src/maasserver/models/signals/tests/test_interfaces.py 2016-07-26 10:11:30 +0000
@@ -194,6 +194,17 @@
194 self.assertIsNone(reload_object(static_ip))194 self.assertIsNone(reload_object(static_ip))
195 self.assertIsNotNone(reload_object(discovered_ip))195 self.assertIsNotNone(reload_object(discovered_ip))
196196
197 def test__removes_links_when_goes_to_disconnected(self):
198 node = self.maker()
199 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
200 static_ip = factory.make_StaticIPAddress(interface=interface)
201 discovered_ip = factory.make_StaticIPAddress(
202 alloc_type=IPADDRESS_TYPE.DISCOVERED, interface=interface)
203 interface.vlan = None
204 interface.save()
205 self.assertIsNone(reload_object(static_ip))
206 self.assertIsNotNone(reload_object(discovered_ip))
207
197208
198class TestInterfaceVLANUpdateController(MAASServerTestCase):209class TestInterfaceVLANUpdateController(MAASServerTestCase):
199210
@@ -222,6 +233,17 @@
222 interface.save()233 interface.save()
223 self.assertEquals(new_vlan, reload_object(subnet).vlan)234 self.assertEquals(new_vlan, reload_object(subnet).vlan)
224235
236 def test__doesnt_move_link_subnets_when_vlan_is_None(self):
237 node = self.maker()
238 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
239 old_vlan = interface.vlan
240 subnet = factory.make_Subnet(vlan=old_vlan)
241 factory.make_StaticIPAddress(
242 subnet=subnet, interface=interface)
243 interface.vlan = None
244 interface.save()
245 self.assertEquals(old_vlan, reload_object(subnet).vlan)
246
225 def test__moves_children_vlans_to_same_fabric(self):247 def test__moves_children_vlans_to_same_fabric(self):
226 node = self.maker()248 node = self.maker()
227 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)249 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
228250
=== modified file 'src/maasserver/models/tests/test_interface.py'
--- src/maasserver/models/tests/test_interface.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/models/tests/test_interface.py 2016-07-26 10:11:30 +0000
@@ -42,6 +42,7 @@
42 UnknownInterface,42 UnknownInterface,
43 VLANInterface,43 VLANInterface,
44)44)
45from maasserver.models.vlan import DEFAULT_MTU
45from maasserver.testing.factory import factory46from maasserver.testing.factory import factory
46from maasserver.testing.orm import reload_objects47from maasserver.testing.orm import reload_objects
47from maasserver.testing.testcase import (48from maasserver.testing.testcase import (
@@ -527,6 +528,17 @@
527 name=name, node=node, mac_address=mac,528 name=name, node=node, mac_address=mac,
528 type=INTERFACE_TYPE.PHYSICAL))529 type=INTERFACE_TYPE.PHYSICAL))
529530
531 def test_allows_null_vlan(self):
532 name = factory.make_name('name')
533 node = factory.make_Node()
534 mac = factory.make_MAC()
535 interface = factory.make_Interface(
536 INTERFACE_TYPE.PHYSICAL,
537 name=name, node=node, mac_address=mac, disconnected=True)
538 self.assertThat(interface, MatchesStructure.byEquality(
539 name=name, node=node, mac_address=mac,
540 type=INTERFACE_TYPE.PHYSICAL, vlan=None))
541
530 def test_string_representation_contains_essential_data(self):542 def test_string_representation_contains_essential_data(self):
531 name = factory.make_name('name')543 name = factory.make_name('name')
532 node = factory.make_Node()544 node = factory.make_Node()
@@ -570,6 +582,11 @@
570 nic1.vlan.save()582 nic1.vlan.save()
571 self.assertEqual(vlan_mtu, nic1.get_effective_mtu())583 self.assertEqual(vlan_mtu, nic1.get_effective_mtu())
572584
585 def test_get_effective_mtu_returns_default_mtu(self):
586 nic1 = factory.make_Interface(
587 INTERFACE_TYPE.PHYSICAL, disconnected=True)
588 self.assertEqual(DEFAULT_MTU, nic1.get_effective_mtu())
589
573 def test_get_links_returns_links_for_each_type(self):590 def test_get_links_returns_links_for_each_type(self):
574 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)591 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
575 links = []592 links = []
@@ -1595,6 +1612,11 @@
1595class TestForceAutoOrDHCPLink(MAASServerTestCase):1612class TestForceAutoOrDHCPLink(MAASServerTestCase):
1596 """Tests for `Interface.force_auto_or_dhcp_link`."""1613 """Tests for `Interface.force_auto_or_dhcp_link`."""
15971614
1615 def test__does_nothing_when_disconnected(self):
1616 interface = factory.make_Interface(
1617 INTERFACE_TYPE.PHYSICAL, disconnected=True)
1618 self.assertIsNone(interface.force_auto_or_dhcp_link())
1619
1598 def test__sets_to_AUTO_on_subnet(self):1620 def test__sets_to_AUTO_on_subnet(self):
1599 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)1621 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1600 subnet = factory.make_Subnet(vlan=interface.vlan)1622 subnet = factory.make_Subnet(vlan=interface.vlan)
@@ -1622,6 +1644,15 @@
1622 1, interface.ip_addresses.count(),1644 1, interface.ip_addresses.count(),
1623 "Should only have one IP address assigned.")1645 "Should only have one IP address assigned.")
16241646
1647 def test__does_nothing_if_no_vlan(self):
1648 interface = factory.make_Interface(
1649 INTERFACE_TYPE.PHYSICAL, disconnected=True)
1650 interface.ensure_link_up()
1651 interface = reload_object(interface)
1652 self.assertEqual(
1653 0, interface.ip_addresses.count(),
1654 "Should only have no IP address assigned.")
1655
1625 def test__creates_link_up_to_discovered_subnet_on_same_vlan(self):1656 def test__creates_link_up_to_discovered_subnet_on_same_vlan(self):
1626 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)1657 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1627 subnet = factory.make_Subnet(vlan=interface.vlan)1658 subnet = factory.make_Subnet(vlan=interface.vlan)
16281659
=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-07-11 18:29:06 +0000
+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-07-26 10:11:30 +0000
@@ -654,11 +654,17 @@
654 if($scope.isInterfaceNameInvalid(nic)) {654 if($scope.isInterfaceNameInvalid(nic)) {
655 nic.name = originalInteface.name;655 nic.name = originalInteface.name;
656 } else if(originalInteface.name !== nic.name ||656 } else if(originalInteface.name !== nic.name ||
657 (originalInteface.vlan_id === null && nic.vlan !== null) ||
658 (originalInteface.vlan_id !== null && nic.vlan === null) ||
657 originalInteface.vlan_id !== nic.vlan.id) {659 originalInteface.vlan_id !== nic.vlan.id) {
658 var params = {660 var params = {
659 "name": nic.name,661 "name": nic.name
660 "vlan": nic.vlan.id
661 };662 };
663 if(nic.vlan !== null) {
664 params.vlan = nic.vlan.id;
665 } else {
666 params.vlan = null;
667 }
662 $scope.$parent.nodesManager.updateInterface(668 $scope.$parent.nodesManager.updateInterface(
663 $scope.node, nic.id, params).then(null, function(error) {669 $scope.node, nic.id, params).then(null, function(error) {
664 // XXX blake_r: Just log the error in the console, but670 // XXX blake_r: Just log the error in the console, but
@@ -716,7 +722,11 @@
716 $scope.fabricChanged = function(nic) {722 $scope.fabricChanged = function(nic) {
717 // Update the VLAN on the node to be the default VLAN for that723 // Update the VLAN on the node to be the default VLAN for that
718 // fabric. The first VLAN for the fabric is the default.724 // fabric. The first VLAN for the fabric is the default.
719 nic.vlan = getDefaultVLAN(nic.fabric);725 if(nic.fabric !== null) {
726 nic.vlan = getDefaultVLAN(nic.fabric);
727 } else {
728 nic.vlan = null;
729 }
720 $scope.saveInterface(nic);730 $scope.saveInterface(nic);
721 };731 };
722732
723733
=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js'
--- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-07-11 18:29:06 +0000
+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-07-26 10:11:30 +0000
@@ -1685,6 +1685,62 @@
1685 "vlan": vlan.id1685 "vlan": vlan.id
1686 });1686 });
1687 });1687 });
1688
1689 it("calls MachinesManager.updateInterface if vlan set", function() {
1690 var controller = makeController();
1691 var id = makeInteger(0, 100);
1692 var name = makeName("nic");
1693 var vlan = { id: makeInteger(0, 100) };
1694 var original_nic = {
1695 id: id,
1696 name: name,
1697 vlan_id: null
1698 };
1699 var nic = {
1700 id: id,
1701 name: name,
1702 vlan: vlan
1703 };
1704 $scope.originalInterfaces[id] = original_nic;
1705 $scope.interfaces = [nic];
1706
1707 spyOn(MachinesManager, "updateInterface").and.returnValue(
1708 $q.defer().promise);
1709 $scope.saveInterface(nic);
1710 expect(MachinesManager.updateInterface).toHaveBeenCalledWith(
1711 node, id, {
1712 "name": name,
1713 "vlan": vlan.id
1714 });
1715 });
1716
1717 it("calls MachinesManager.updateInterface if vlan unset", function() {
1718 var controller = makeController();
1719 var id = makeInteger(0, 100);
1720 var name = makeName("nic");
1721 var vlan = { id: makeInteger(0, 100) };
1722 var original_nic = {
1723 id: id,
1724 name: name,
1725 vlan_id: makeInteger(200, 300)
1726 };
1727 var nic = {
1728 id: id,
1729 name: name,
1730 vlan: null
1731 };
1732 $scope.originalInterfaces[id] = original_nic;
1733 $scope.interfaces = [nic];
1734
1735 spyOn(MachinesManager, "updateInterface").and.returnValue(
1736 $q.defer().promise);
1737 $scope.saveInterface(nic);
1738 expect(MachinesManager.updateInterface).toHaveBeenCalledWith(
1739 node, id, {
1740 "name": name,
1741 "vlan": null
1742 });
1743 });
1688 });1744 });
16891745
1690 describe("setFocusInterface", function() {1746 describe("setFocusInterface", function() {
@@ -1847,6 +1903,17 @@
1847 expect(nic.vlan).toBe(vlan);1903 expect(nic.vlan).toBe(vlan);
1848 });1904 });
18491905
1906 it("sets vlan to null", function() {
1907 var controller = makeController();
1908 var nic = {
1909 vlan: {},
1910 fabric: null
1911 };
1912 spyOn($scope, "saveInterface");
1913 $scope.fabricChanged(nic);
1914 expect(nic.vlan).toBeNull();
1915 });
1916
1850 it("calls saveInterface", function() {1917 it("calls saveInterface", function() {
1851 var controller = makeController();1918 var controller = makeController();
1852 var fabric = {1919 var fabric = {
18531920
=== modified file 'src/maasserver/static/partials/node-details.html'
--- src/maasserver/static/partials/node-details.html 2016-07-15 00:42:25 +0000
+++ src/maasserver/static/partials/node-details.html 2016-07-26 10:11:30 +0000
@@ -617,11 +617,13 @@
617 data-ng-disabled="interface.type == 'alias' || interface.type == 'vlan' || !isNodeEditingAllowed()"617 data-ng-disabled="interface.type == 'alias' || interface.type == 'vlan' || !isNodeEditingAllowed()"
618 data-ng-change="fabricChanged(interface)"618 data-ng-change="fabricChanged(interface)"
619 data-ng-options="fabric as fabric.name for fabric in fabrics">619 data-ng-options="fabric as fabric.name for fabric in fabrics">
620 <option value="">Disconnected</option>
620 </select>621 </select>
621 </div>622 </div>
622 <div class="table__data table-col--14">623 <div class="table__data table-col--14">
623 <select class="table__input" name="vlan" id="vlan"624 <select class="table__input" name="vlan" id="vlan"
624 data-ng-model="interface.vlan"625 data-ng-model="interface.vlan"
626 data-ng-if="interface.fabric"
625 data-ng-disabled="isController || interface.type == 'alias' || interface.vlan.vid === 0 || !isNodeEditingAllowed()"627 data-ng-disabled="isController || interface.type == 'alias' || interface.vlan.vid === 0 || !isNodeEditingAllowed()"
626 data-ng-change="saveInterface(interface)"628 data-ng-change="saveInterface(interface)"
627 data-ng-options="vlan as getVLANText(vlan) for vlan in vlans | removeDefaultVLANIfVLAN:interface.type | filterByFabric:interface.fabric">629 data-ng-options="vlan as getVLANText(vlan) for vlan in vlans | removeDefaultVLANIfVLAN:interface.type | filterByFabric:interface.fabric">
@@ -629,6 +631,7 @@
629 </div>631 </div>
630 <div class="table__data table-col--18">632 <div class="table__data table-col--18">
631 <select class="table__input" name="subnet" id="subnet"633 <select class="table__input" name="subnet" id="subnet"
634 data-ng-if="interface.fabric"
632 data-ng-hide="isAllNetworkingDisabled() && interface.discovered[0].subnet_id"635 data-ng-hide="isAllNetworkingDisabled() && interface.discovered[0].subnet_id"
633 data-ng-disabled="isController || !isNodeEditingAllowed()"636 data-ng-disabled="isController || !isNodeEditingAllowed()"
634 data-ng-model="interface.subnet"637 data-ng-model="interface.subnet"
@@ -641,7 +644,7 @@
641 </span>644 </span>
642 </div>645 </div>
643 <div class="table__data table-col--21">646 <div class="table__data table-col--21">
644 <ul class="no-bullets no-margin-bottom" data-ng-if="!isController && !isAllNetworkingDisabled()">647 <ul class="no-bullets no-margin-bottom" data-ng-if="!isController && !isAllNetworkingDisabled() && interface.fabric">
645 <li class="no-margin">648 <li class="no-margin">
646 <select class="table__input" name="link-mode" id="link-mode"649 <select class="table__input" name="link-mode" id="link-mode"
647 data-ng-model="interface.mode"650 data-ng-model="interface.mode"
648651
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-07-20 19:27:20 +0000
+++ src/maasserver/testing/factory.py 2016-07-26 10:11:30 +0000
@@ -902,7 +902,7 @@
902 def make_Interface(902 def make_Interface(
903 self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None,903 self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None,
904 vlan=None, parents=None, name=None, cluster_interface=None,904 vlan=None, parents=None, name=None, cluster_interface=None,
905 ip=None, enabled=True, fabric=None, tags=None):905 ip=None, enabled=True, fabric=None, tags=None, disconnected=False):
906 if name is None:906 if name is None:
907 if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN):907 if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN):
908 name = self.make_name('eth')908 name = self.make_name('eth')
@@ -919,20 +919,21 @@
919 name = None919 name = None
920 if iftype is None:920 if iftype is None:
921 iftype = INTERFACE_TYPE.PHYSICAL921 iftype = INTERFACE_TYPE.PHYSICAL
922 if vlan is None:922 if not disconnected:
923 if fabric is not None:923 if vlan is None:
924 if iftype == INTERFACE_TYPE.VLAN:924 if fabric is not None:
925 vlan = self.make_VLAN(fabric=fabric)925 if iftype == INTERFACE_TYPE.VLAN:
926 else:926 vlan = self.make_VLAN(fabric=fabric)
927 vlan = fabric.get_default_vlan()927 else:
928 else:928 vlan = fabric.get_default_vlan()
929 if iftype == INTERFACE_TYPE.VLAN and parents:929 else:
930 vlan = self.make_VLAN(fabric=parents[0].vlan.fabric)930 if iftype == INTERFACE_TYPE.VLAN and parents:
931 elif iftype == INTERFACE_TYPE.BOND and parents:931 vlan = self.make_VLAN(fabric=parents[0].vlan.fabric)
932 vlan = parents[0].vlan932 elif iftype == INTERFACE_TYPE.BOND and parents:
933 else:933 vlan = parents[0].vlan
934 fabric = self.make_Fabric()934 else:
935 vlan = fabric.get_default_vlan()935 fabric = self.make_Fabric()
936 vlan = fabric.get_default_vlan()
936 if (mac_address is None and937 if (mac_address is None and
937 iftype in [938 iftype in [
938 INTERFACE_TYPE.PHYSICAL,939 INTERFACE_TYPE.PHYSICAL,
939940
=== modified file 'src/maasserver/tests/test_forms_interface.py'
--- src/maasserver/tests/test_forms_interface.py 2016-04-22 17:28:15 +0000
+++ src/maasserver/tests/test_forms_interface.py 2016-07-26 10:11:30 +0000
@@ -86,6 +86,22 @@
86 MatchesStructure.byEquality(86 MatchesStructure.byEquality(
87 name=interface.name, vlan=new_vlan, enabled=interface.enabled))87 name=interface.name, vlan=new_vlan, enabled=interface.enabled))
8888
89 def test__allows_no_vlan(self):
90 node = self.maker()
91 interface = factory.make_Interface(
92 INTERFACE_TYPE.PHYSICAL, node=node)
93 form = ControllerInterfaceForm(
94 instance=interface,
95 data={
96 'vlan': None,
97 })
98 self.assertTrue(form.is_valid(), form.errors)
99 interface = form.save()
100 self.assertThat(
101 interface,
102 MatchesStructure.byEquality(
103 name=interface.name, vlan=None, enabled=interface.enabled))
104
89105
90class PhysicalInterfaceFormTest(MAASServerTestCase):106class PhysicalInterfaceFormTest(MAASServerTestCase):
91107
@@ -116,6 +132,30 @@
116 type=INTERFACE_TYPE.PHYSICAL, tags=tags))132 type=INTERFACE_TYPE.PHYSICAL, tags=tags))
117 self.assertItemsEqual([], interface.parents.all())133 self.assertItemsEqual([], interface.parents.all())
118134
135 def test__creates_physical_interface_disconnected(self):
136 node = factory.make_Node()
137 mac_address = factory.make_mac_address()
138 interface_name = 'eth0'
139 tags = [
140 factory.make_name("tag")
141 for _ in range(3)
142 ]
143 form = PhysicalInterfaceForm(
144 node=node,
145 data={
146 'name': interface_name,
147 'mac_address': mac_address,
148 'tags': ",".join(tags),
149 })
150 self.assertTrue(form.is_valid(), form.errors)
151 interface = form.save()
152 self.assertThat(
153 interface,
154 MatchesStructure.byEquality(
155 node=node, mac_address=mac_address, name=interface_name,
156 type=INTERFACE_TYPE.PHYSICAL, tags=tags, vlan=None))
157 self.assertItemsEqual([], interface.parents.all())
158
119 def test__create_ensures_link_up(self):159 def test__create_ensures_link_up(self):
120 node = factory.make_Node()160 node = factory.make_Node()
121 mac_address = factory.make_mac_address()161 mac_address = factory.make_mac_address()
@@ -255,6 +295,26 @@
255 name=new_name, vlan=new_vlan, enabled=False, tags=[]))295 name=new_name, vlan=new_vlan, enabled=False, tags=[]))
256 self.assertItemsEqual([], interface.parents.all())296 self.assertItemsEqual([], interface.parents.all())
257297
298 def test__edits_interface_disconnected(self):
299 interface = factory.make_Interface(
300 INTERFACE_TYPE.PHYSICAL, name='eth0')
301 new_name = 'eth1'
302 form = PhysicalInterfaceForm(
303 instance=interface,
304 data={
305 'name': new_name,
306 'vlan': None,
307 'enabled': False,
308 'tags': "",
309 })
310 self.assertTrue(form.is_valid(), form.errors)
311 interface = form.save()
312 self.assertThat(
313 interface,
314 MatchesStructure.byEquality(
315 name=new_name, vlan=None, enabled=False, tags=[]))
316 self.assertItemsEqual([], interface.parents.all())
317
258 def test__create_sets_interface_parameters(self):318 def test__create_sets_interface_parameters(self):
259 node = factory.make_Node()319 node = factory.make_Node()
260 mac_address = factory.make_mac_address()320 mac_address = factory.make_mac_address()
@@ -403,6 +463,20 @@
403 self.assertIsNotNone(463 self.assertIsNotNone(
404 interface.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.STICKY))464 interface.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.STICKY))
405465
466 def test__create_rejects_interface_without_vlan(self):
467 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
468 form = VLANInterfaceForm(
469 node=parent.node,
470 data={
471 'parents': [parent.id],
472 })
473 self.assertFalse(form.is_valid(), form.errors)
474 self.assertItemsEqual(
475 ['vlan'], form.errors.keys(), form.errors)
476 self.assertIn(
477 "A VLAN interface must be connected to a tagged VLAN.",
478 form.errors['vlan'][0])
479
406 def test_rejects_interface_with_duplicate_name(self):480 def test_rejects_interface_with_duplicate_name(self):
407 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)481 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
408 vlan = factory.make_VLAN(fabric=parent.vlan.fabric, vid=10)482 vlan = factory.make_VLAN(fabric=parent.vlan.fabric, vid=10)
@@ -468,6 +542,20 @@
468 "VLAN interface can't have another VLAN interface as parent.",542 "VLAN interface can't have another VLAN interface as parent.",
469 form.errors['parents'][0])543 form.errors['parents'][0])
470544
545 def test__rejects_no_vlan(self):
546 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
547 form = VLANInterfaceForm(
548 node=parent.node,
549 data={
550 'vlan': None,
551 'parents': [parent.id],
552 })
553 self.assertFalse(form.is_valid(), form.errors)
554 self.assertItemsEqual(['vlan'], form.errors.keys())
555 self.assertIn(
556 "A VLAN interface must be connected to a tagged VLAN.",
557 form.errors['vlan'][0])
558
471 def test__rejects_vlan_not_on_same_fabric(self):559 def test__rejects_vlan_not_on_same_fabric(self):
472 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)560 parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
473 factory.make_VLAN(fabric=parent.vlan.fabric, vid=10)561 factory.make_VLAN(fabric=parent.vlan.fabric, vid=10)
@@ -576,7 +664,8 @@
576 self.assertThat(664 self.assertThat(
577 interface,665 interface,
578 MatchesStructure.byEquality(666 MatchesStructure.byEquality(
579 name=interface_name, type=INTERFACE_TYPE.BOND))667 name=interface_name, type=INTERFACE_TYPE.BOND,
668 vlan=parent1.vlan))
580 self.assertIn(669 self.assertIn(
581 interface.mac_address, [parent1.mac_address, parent2.mac_address])670 interface.mac_address, [parent1.mac_address, parent2.mac_address])
582 self.assertItemsEqual([parent1, parent2], interface.parents.all())671 self.assertItemsEqual([parent1, parent2], interface.parents.all())
@@ -787,6 +876,25 @@
787 for parent in [parent1, parent2, new_parent]876 for parent in [parent1, parent2, new_parent]
788 ))877 ))
789878
879 def test__edits_interface_allows_disconnected(self):
880 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
881 parent2 = factory.make_Interface(
882 INTERFACE_TYPE.PHYSICAL, node=parent1.node)
883 interface = factory.make_Interface(
884 INTERFACE_TYPE.BOND, parents=[parent1, parent2])
885 form = BondInterfaceForm(
886 instance=interface,
887 data={
888 'vlan': None,
889 })
890 self.assertTrue(form.is_valid(), form.errors)
891 interface = form.save()
892 self.assertThat(
893 interface,
894 MatchesStructure.byEquality(
895 mac_address=interface.mac_address, vlan=None,
896 type=INTERFACE_TYPE.BOND))
897
790 def test__edits_interface_removes_parents(self):898 def test__edits_interface_removes_parents(self):
791 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)899 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
792 parent2 = factory.make_Interface(900 parent2 = factory.make_Interface(
@@ -1119,6 +1227,26 @@
1119 self.assertItemsEqual(1227 self.assertItemsEqual(
1120 [parent1, parent2, new_parent], interface.parents.all())1228 [parent1, parent2, new_parent], interface.parents.all())
11211229
1230 def test__edits_interface_allows_disconnected(self):
1231 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1232 parent2 = factory.make_Interface(
1233 INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan)
1234 interface = factory.make_Interface(
1235 INTERFACE_TYPE.BRIDGE,
1236 parents=[parent1, parent2])
1237 form = BridgeInterfaceForm(
1238 instance=interface,
1239 data={
1240 'vlan': None,
1241 })
1242 self.assertTrue(form.is_valid(), form.errors)
1243 interface = form.save()
1244 self.assertThat(
1245 interface,
1246 MatchesStructure.byEquality(
1247 mac_address=interface.mac_address,
1248 vlan=None, type=INTERFACE_TYPE.BRIDGE))
1249
1122 def test__edits_interface_removes_parents(self):1250 def test__edits_interface_removes_parents(self):
1123 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)1251 parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
1124 parent2 = factory.make_Interface(1252 parent2 = factory.make_Interface(
11251253
=== modified file 'src/maasserver/websockets/handlers/node.py'
--- src/maasserver/websockets/handlers/node.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/websockets/handlers/node.py 2016-07-26 10:11:30 +0000
@@ -455,7 +455,8 @@
455 def get_all_fabric_names(self, obj, subnets):455 def get_all_fabric_names(self, obj, subnets):
456 fabric_names = set()456 fabric_names = set()
457 for interface in obj.interface_set.all():457 for interface in obj.interface_set.all():
458 fabric_names.add(interface.vlan.fabric.name)458 if interface.vlan is not None:
459 fabric_names.add(interface.vlan.fabric.name)
459 for subnet in subnets:460 for subnet in subnets:
460 fabric_names.add(subnet.vlan.fabric.name)461 fabric_names.add(subnet.vlan.fabric.name)
461 return list(fabric_names)462 return list(fabric_names)