Merge lp:~blake-rouse/maas/nic-no-fabric into lp:~maas-committers/maas/trunk
- nic-no-fabric
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email:
|
Commit message
Add the ability for an interface to be disconnected (not connected to any VLAN).
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
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://
Hit:2 http://
Get:3 http://
Hit:4 http://
Get:5 http://
Fetched 395 kB in 0s (795 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
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.20160115ubun
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+
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5....
Preview Diff
1 | === modified file 'src/maasserver/api/interfaces.py' | |||
2 | --- src/maasserver/api/interfaces.py 2016-05-12 19:07:37 +0000 | |||
3 | +++ src/maasserver/api/interfaces.py 2016-07-26 10:11:30 +0000 | |||
4 | @@ -109,7 +109,8 @@ | |||
5 | 109 | :param name: Name of the interface. | 109 | :param name: Name of the interface. |
6 | 110 | :param mac_address: MAC address of the interface. | 110 | :param mac_address: MAC address of the interface. |
7 | 111 | :param tags: Tags for the interface. | 111 | :param tags: Tags for the interface. |
9 | 112 | :param vlan: Untagged VLAN the interface is connected to. | 112 | :param vlan: Untagged VLAN the interface is connected to. If not |
10 | 113 | provided then the interface is considered disconnected. | ||
11 | 113 | 114 | ||
12 | 114 | Following are extra parameters that can be set on the interface: | 115 | Following are extra parameters that can be set on the interface: |
13 | 115 | 116 | ||
14 | @@ -151,7 +152,8 @@ | |||
15 | 151 | :param name: Name of the interface. | 152 | :param name: Name of the interface. |
16 | 152 | :param mac_address: MAC address of the interface. | 153 | :param mac_address: MAC address of the interface. |
17 | 153 | :param tags: Tags for the interface. | 154 | :param tags: Tags for the interface. |
19 | 154 | :param vlan: VLAN the interface is connected to. | 155 | :param vlan: VLAN the interface is connected to. If not |
20 | 156 | provided then the interface is considered disconnected. | ||
21 | 155 | :param parents: Parent interfaces that make this bond. | 157 | :param parents: Parent interfaces that make this bond. |
22 | 156 | 158 | ||
23 | 157 | Following are parameters specific to bonds: | 159 | Following are parameters specific to bonds: |
24 | @@ -318,13 +320,15 @@ | |||
25 | 318 | :param name: Name of the interface. | 320 | :param name: Name of the interface. |
26 | 319 | :param mac_address: MAC address of the interface. | 321 | :param mac_address: MAC address of the interface. |
27 | 320 | :param tags: Tags for the interface. | 322 | :param tags: Tags for the interface. |
29 | 321 | :param vlan: Untagged VLAN the interface is connected to. | 323 | :param vlan: Untagged VLAN the interface is connected to. If not set |
30 | 324 | then the interface is considered disconnected. | ||
31 | 322 | 325 | ||
32 | 323 | Fields for bond interface: | 326 | Fields for bond interface: |
33 | 324 | :param name: Name of the interface. | 327 | :param name: Name of the interface. |
34 | 325 | :param mac_address: MAC address of the interface. | 328 | :param mac_address: MAC address of the interface. |
35 | 326 | :param tags: Tags for the interface. | 329 | :param tags: Tags for the interface. |
37 | 327 | :param vlan: Tagged VLAN the interface is connected to. | 330 | :param vlan: Tagged VLAN the interface is connected to. If not set |
38 | 331 | then the interface is considered disconnected. | ||
39 | 328 | :param parents: Parent interfaces that make this bond. | 332 | :param parents: Parent interfaces that make this bond. |
40 | 329 | 333 | ||
41 | 330 | Fields for VLAN interface: | 334 | Fields for VLAN interface: |
42 | 331 | 335 | ||
43 | === modified file 'src/maasserver/api/tests/test_interfaces.py' | |||
44 | --- src/maasserver/api/tests/test_interfaces.py 2016-05-26 13:28:46 +0000 | |||
45 | +++ src/maasserver/api/tests/test_interfaces.py 2016-07-26 10:11:30 +0000 | |||
46 | @@ -257,7 +257,7 @@ | |||
47 | 257 | self.assertEqual( | 257 | self.assertEqual( |
48 | 258 | http.client.CONFLICT, response.status_code, response.content) | 258 | http.client.CONFLICT, response.status_code, response.content) |
49 | 259 | 259 | ||
51 | 260 | def test_create_physical_requires_mac_name_and_vlan(self): | 260 | def test_create_physical_requires_mac_and_name(self): |
52 | 261 | self.become_admin() | 261 | self.become_admin() |
53 | 262 | node = factory.make_Node(status=NODE_STATUS.READY) | 262 | node = factory.make_Node(status=NODE_STATUS.READY) |
54 | 263 | uri = get_interfaces_uri(node) | 263 | uri = get_interfaces_uri(node) |
55 | @@ -269,7 +269,6 @@ | |||
56 | 269 | self.assertEqual({ | 269 | self.assertEqual({ |
57 | 270 | "mac_address": ["This field is required."], | 270 | "mac_address": ["This field is required."], |
58 | 271 | "name": ["This field is required."], | 271 | "name": ["This field is required."], |
59 | 272 | "vlan": ["This field is required."], | ||
60 | 273 | }, json_load_bytes(response.content)) | 272 | }, json_load_bytes(response.content)) |
61 | 274 | 273 | ||
62 | 275 | def test_create_physical_doesnt_allow_mac_already_register(self): | 274 | def test_create_physical_doesnt_allow_mac_already_register(self): |
63 | @@ -490,7 +489,7 @@ | |||
64 | 490 | self.assertEqual( | 489 | self.assertEqual( |
65 | 491 | http.client.BAD_REQUEST, response.status_code, response.content) | 490 | http.client.BAD_REQUEST, response.status_code, response.content) |
66 | 492 | self.assertEqual({ | 491 | self.assertEqual({ |
68 | 493 | "vlan": ["This field is required."], | 492 | "vlan": ["A VLAN interface must be connected to a tagged VLAN."], |
69 | 494 | "parent": ["A VLAN interface must have exactly one parent."], | 493 | "parent": ["A VLAN interface must have exactly one parent."], |
70 | 495 | }, json_load_bytes(response.content)) | 494 | }, json_load_bytes(response.content)) |
71 | 496 | 495 | ||
72 | 497 | 496 | ||
73 | === modified file 'src/maasserver/forms_interface.py' | |||
74 | --- src/maasserver/forms_interface.py 2016-04-22 17:28:15 +0000 | |||
75 | +++ src/maasserver/forms_interface.py 2016-07-26 10:11:30 +0000 | |||
76 | @@ -99,6 +99,11 @@ | |||
77 | 99 | rel = interface.parent_relationships.filter( | 99 | rel = interface.parent_relationships.filter( |
78 | 100 | parent=parent_to_del) | 100 | parent=parent_to_del) |
79 | 101 | rel.delete() | 101 | rel.delete() |
80 | 102 | # Allow setting the VLAN to None. | ||
81 | 103 | new_vlan = self.cleaned_data.get('vlan') | ||
82 | 104 | vlan_was_set = 'vlan' in self.data | ||
83 | 105 | if new_vlan is None and vlan_was_set: | ||
84 | 106 | interface.vlan = new_vlan | ||
85 | 102 | self.set_extra_parameters(interface, created) | 107 | self.set_extra_parameters(interface, created) |
86 | 103 | interface.save() | 108 | interface.save() |
87 | 104 | if created: | 109 | if created: |
88 | @@ -168,6 +173,17 @@ | |||
89 | 168 | 'vlan', | 173 | 'vlan', |
90 | 169 | ) | 174 | ) |
91 | 170 | 175 | ||
92 | 176 | def save(self, *args, **kwargs): | ||
93 | 177 | """Persist the interface into the database.""" | ||
94 | 178 | interface = super(ControllerInterfaceForm, self).save(commit=False) | ||
95 | 179 | # Allow setting the VLAN to None. | ||
96 | 180 | new_vlan = self.cleaned_data.get('vlan') | ||
97 | 181 | vlan_was_set = 'vlan' in self.data | ||
98 | 182 | if new_vlan is None and vlan_was_set: | ||
99 | 183 | interface.vlan = new_vlan | ||
100 | 184 | interface.save() | ||
101 | 185 | return interface | ||
102 | 186 | |||
103 | 171 | 187 | ||
104 | 172 | class PhysicalInterfaceForm(InterfaceForm): | 188 | class PhysicalInterfaceForm(InterfaceForm): |
105 | 173 | """Form used to create/edit a physical interface.""" | 189 | """Form used to create/edit a physical interface.""" |
106 | @@ -251,7 +267,13 @@ | |||
107 | 251 | return parents | 267 | return parents |
108 | 252 | 268 | ||
109 | 253 | def clean_vlan(self): | 269 | def clean_vlan(self): |
110 | 270 | created = self.instance.id is None | ||
111 | 254 | new_vlan = self.cleaned_data.get('vlan') | 271 | new_vlan = self.cleaned_data.get('vlan') |
112 | 272 | vlan_was_set = 'vlan' in self.data | ||
113 | 273 | if (created and new_vlan is None) or ( | ||
114 | 274 | not created and new_vlan is None and vlan_was_set): | ||
115 | 275 | raise ValidationError( | ||
116 | 276 | "A VLAN interface must be connected to a tagged VLAN.") | ||
117 | 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: |
118 | 256 | raise ValidationError( | 278 | raise ValidationError( |
119 | 257 | "A VLAN interface can only belong to a tagged VLAN.") | 279 | "A VLAN interface can only belong to a tagged VLAN.") |
120 | @@ -281,15 +303,6 @@ | |||
121 | 281 | bridges. | 303 | bridges. |
122 | 282 | """ | 304 | """ |
123 | 283 | 305 | ||
124 | 284 | def __init__(self, *args, **kwargs): | ||
125 | 285 | super().__init__(*args, **kwargs) | ||
126 | 286 | # Allow VLAN to be blank when creating. | ||
127 | 287 | instance = kwargs.get("instance", None) | ||
128 | 288 | if instance is not None and instance.id is not None: | ||
129 | 289 | self.fields['vlan'].required = True | ||
130 | 290 | else: | ||
131 | 291 | self.fields['vlan'].required = False | ||
132 | 292 | |||
133 | 293 | def clean_parents(self): | 306 | def clean_parents(self): |
134 | 294 | """Validate that child interfaces cannot be created unless at least one | 307 | """Validate that child interfaces cannot be created unless at least one |
135 | 295 | parent is present. | 308 | parent is present. |
136 | 296 | 309 | ||
137 | === modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py' | |||
138 | --- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-05-11 19:01:48 +0000 | |||
139 | +++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-26 10:11:30 +0000 | |||
140 | @@ -39,7 +39,7 @@ | |||
141 | 39 | migrations.AlterField( | 39 | migrations.AlterField( |
142 | 40 | model_name='interface', | 40 | model_name='interface', |
143 | 41 | name='vlan', | 41 | name='vlan', |
145 | 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), |
146 | 43 | ), | 43 | ), |
147 | 44 | migrations.AlterField( | 44 | migrations.AlterField( |
148 | 45 | model_name='subnet', | 45 | model_name='subnet', |
149 | 46 | 46 | ||
150 | === added file 'src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py' | |||
151 | --- src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py 1970-01-01 00:00:00 +0000 | |||
152 | +++ src/maasserver/migrations/builtin/maasserver/0070_allow_null_vlan_on_interface.py 2016-07-26 10:11:30 +0000 | |||
153 | @@ -0,0 +1,23 @@ | |||
154 | 1 | # -*- coding: utf-8 -*- | ||
155 | 2 | from __future__ import unicode_literals | ||
156 | 3 | |||
157 | 4 | from django.db import ( | ||
158 | 5 | migrations, | ||
159 | 6 | models, | ||
160 | 7 | ) | ||
161 | 8 | import django.db.models.deletion | ||
162 | 9 | |||
163 | 10 | |||
164 | 11 | class Migration(migrations.Migration): | ||
165 | 12 | |||
166 | 13 | dependencies = [ | ||
167 | 14 | ('maasserver', '0069_add_previous_node_status_to_node'), | ||
168 | 15 | ] | ||
169 | 16 | |||
170 | 17 | operations = [ | ||
171 | 18 | migrations.AlterField( | ||
172 | 19 | model_name='interface', | ||
173 | 20 | name='vlan', | ||
174 | 21 | field=models.ForeignKey(null=True, blank=True, to='maasserver.VLAN', on_delete=django.db.models.deletion.PROTECT), | ||
175 | 22 | ), | ||
176 | 23 | ] | ||
177 | 0 | 24 | ||
178 | === modified file 'src/maasserver/models/interface.py' | |||
179 | --- src/maasserver/models/interface.py 2016-05-12 19:07:37 +0000 | |||
180 | +++ src/maasserver/models/interface.py 2016-07-26 10:11:30 +0000 | |||
181 | @@ -67,11 +67,6 @@ | |||
182 | 67 | INTERFACE_NAME_REGEXP = '^[\w\-_.:]+$' | 67 | INTERFACE_NAME_REGEXP = '^[\w\-_.:]+$' |
183 | 68 | 68 | ||
184 | 69 | 69 | ||
185 | 70 | def get_default_vlan(): | ||
186 | 71 | from maasserver.models.vlan import VLAN | ||
187 | 72 | return VLAN.objects.get_default_vlan().id | ||
188 | 73 | |||
189 | 74 | |||
190 | 75 | def get_subnet_family(subnet): | 70 | def get_subnet_family(subnet): |
191 | 76 | """Return the IPADDRESS_FAMILY for the `subnet`.""" | 71 | """Return the IPADDRESS_FAMILY for the `subnet`.""" |
192 | 77 | if subnet is not None: | 72 | if subnet is not None: |
193 | @@ -375,8 +370,7 @@ | |||
194 | 375 | through='InterfaceRelationship', symmetrical=False) | 370 | through='InterfaceRelationship', symmetrical=False) |
195 | 376 | 371 | ||
196 | 377 | vlan = ForeignKey( | 372 | vlan = ForeignKey( |
199 | 378 | 'VLAN', default=get_default_vlan, editable=True, blank=False, | 373 | 'VLAN', editable=True, blank=True, null=True, on_delete=PROTECT) |
198 | 379 | null=False, on_delete=PROTECT) | ||
200 | 380 | 374 | ||
201 | 381 | ip_addresses = ManyToManyField( | 375 | ip_addresses = ManyToManyField( |
202 | 382 | 'StaticIPAddress', editable=True, blank=True) | 376 | 'StaticIPAddress', editable=True, blank=True) |
203 | @@ -438,8 +432,13 @@ | |||
204 | 438 | mtu = None | 432 | mtu = None |
205 | 439 | if self.params: | 433 | if self.params: |
206 | 440 | mtu = self.params.get('mtu', None) | 434 | mtu = self.params.get('mtu', None) |
207 | 435 | if mtu is None and self.vlan is not None: | ||
208 | 436 | mtu = self.vlan.mtu | ||
209 | 441 | if mtu is None: | 437 | if mtu is None: |
211 | 442 | mtu = self.vlan.mtu | 438 | # Use default MTU for the interface when the interface has no |
212 | 439 | # MTU set and it is disconnected. | ||
213 | 440 | from maasserver.models.vlan import DEFAULT_MTU | ||
214 | 441 | mtu = DEFAULT_MTU | ||
215 | 443 | return mtu | 442 | return mtu |
216 | 444 | 443 | ||
217 | 445 | def get_links(self): | 444 | def get_links(self): |
218 | @@ -752,11 +751,12 @@ | |||
219 | 752 | # subnets into the default VLAN, this assumption might be incorrect in | 751 | # subnets into the default VLAN, this assumption might be incorrect in |
220 | 753 | # many cases, leading to interfaces being configured as AUTO when | 752 | # many cases, leading to interfaces being configured as AUTO when |
221 | 754 | # they should be configured as DHCP. | 753 | # they should be configured as DHCP. |
227 | 755 | found_subnet = self.vlan.subnet_set.first() | 754 | if self.vlan is not None: |
228 | 756 | if found_subnet is not None: | 755 | found_subnet = self.vlan.subnet_set.first() |
229 | 757 | return self.link_subnet(INTERFACE_LINK_TYPE.AUTO, found_subnet) | 756 | if found_subnet is not None: |
230 | 758 | else: | 757 | return self.link_subnet(INTERFACE_LINK_TYPE.AUTO, found_subnet) |
231 | 759 | return self.link_subnet(INTERFACE_LINK_TYPE.DHCP, None) | 758 | else: |
232 | 759 | return self.link_subnet(INTERFACE_LINK_TYPE.DHCP, None) | ||
233 | 760 | 760 | ||
234 | 761 | def ensure_link_up(self): | 761 | def ensure_link_up(self): |
235 | 762 | """Ensure that if no subnet links exists that at least a LINK_UP | 762 | """Ensure that if no subnet links exists that at least a LINK_UP |
236 | @@ -766,7 +766,7 @@ | |||
237 | 766 | if has_links: | 766 | if has_links: |
238 | 767 | # Nothing to do, already has links. | 767 | # Nothing to do, already has links. |
239 | 768 | return | 768 | return |
241 | 769 | else: | 769 | elif self.vlan is not None: |
242 | 770 | # Use an associated subnet if it exists and its on the same VLAN | 770 | # Use an associated subnet if it exists and its on the same VLAN |
243 | 771 | # the interface is currently connected, else it will just be a | 771 | # the interface is currently connected, else it will just be a |
244 | 772 | # LINK_UP without a subnet. | 772 | # LINK_UP without a subnet. |
245 | @@ -1265,20 +1265,31 @@ | |||
246 | 1265 | "parents": ["VLAN interface must have exactly one parent."] | 1265 | "parents": ["VLAN interface must have exactly one parent."] |
247 | 1266 | }) | 1266 | }) |
248 | 1267 | parent = parents[0] | 1267 | parent = parents[0] |
249 | 1268 | # We do not allow a bridge interface to be a parent for a VLAN | ||
250 | 1269 | # interface. | ||
251 | 1268 | allowed_vlan_parent_types = ( | 1270 | allowed_vlan_parent_types = ( |
252 | 1269 | INTERFACE_TYPE.PHYSICAL, | 1271 | INTERFACE_TYPE.PHYSICAL, |
253 | 1270 | INTERFACE_TYPE.BOND, | 1272 | INTERFACE_TYPE.BOND, |
254 | 1271 | INTERFACE_TYPE.BRIDGE | 1273 | INTERFACE_TYPE.BRIDGE |
255 | 1272 | ) | 1274 | ) |
256 | 1273 | if parent.get_type() not in allowed_vlan_parent_types: | 1275 | if parent.get_type() not in allowed_vlan_parent_types: |
259 | 1274 | # XXX mpontillo 2016-06-23: we won't mention bridges in this | 1276 | # XXX blake_r 2016-07-18: we won't mention bridges in this |
260 | 1275 | # error message, since users can't configure bridges on nodes. | 1277 | # error message, since users can't configure VLAN interfaces |
261 | 1278 | # on bridges. | ||
262 | 1276 | raise ValidationError({ | 1279 | raise ValidationError({ |
263 | 1277 | "parents": [ | 1280 | "parents": [ |
264 | 1278 | "VLAN interface can only be created on a physical " | 1281 | "VLAN interface can only be created on a physical " |
265 | 1279 | "or bond interface." | 1282 | "or bond interface." |
266 | 1280 | ] | 1283 | ] |
267 | 1281 | }) | 1284 | }) |
268 | 1285 | # VLAN interface must be connected to a VLAN, it cannot be | ||
269 | 1286 | # disconnected like physical and bond interfaces. | ||
270 | 1287 | if self.vlan is None: | ||
271 | 1288 | raise ValidationError({ | ||
272 | 1289 | "vlan": [ | ||
273 | 1290 | "VLAN interface requires connection to a VLAN." | ||
274 | 1291 | ] | ||
275 | 1292 | }) | ||
276 | 1282 | 1293 | ||
277 | 1283 | def save(self, *args, **kwargs): | 1294 | def save(self, *args, **kwargs): |
278 | 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. |
279 | 1285 | 1296 | ||
280 | === modified file 'src/maasserver/models/node.py' | |||
281 | --- src/maasserver/models/node.py 2016-07-25 20:13:54 +0000 | |||
282 | +++ src/maasserver/models/node.py 2016-07-26 10:11:30 +0000 | |||
283 | @@ -3407,6 +3407,10 @@ | |||
284 | 3407 | new_vlan = new_fabric.get_default_vlan() | 3407 | new_vlan = new_fabric.get_default_vlan() |
285 | 3408 | interface.vlan = new_vlan | 3408 | interface.vlan = new_vlan |
286 | 3409 | interface.save() | 3409 | interface.save() |
287 | 3410 | else: | ||
288 | 3411 | interface.vlan = ( | ||
289 | 3412 | Fabric.objects.get_default_fabric().get_default_vlan()) | ||
290 | 3413 | interface.save() | ||
291 | 3410 | else: | 3414 | else: |
292 | 3411 | if interface.node.id != self.id: | 3415 | if interface.node.id != self.id: |
293 | 3412 | # MAC address was on a different node. We need to move | 3416 | # MAC address was on a different node. We need to move |
294 | 3413 | 3417 | ||
295 | === modified file 'src/maasserver/models/signals/interfaces.py' | |||
296 | --- src/maasserver/models/signals/interfaces.py 2016-05-12 19:07:37 +0000 | |||
297 | +++ src/maasserver/models/signals/interfaces.py 2016-07-26 10:11:30 +0000 | |||
298 | @@ -197,22 +197,25 @@ | |||
299 | 197 | NODE_TYPE.RACK_CONTROLLER, | 197 | NODE_TYPE.RACK_CONTROLLER, |
300 | 198 | NODE_TYPE.REGION_AND_RACK_CONTROLLER): | 198 | NODE_TYPE.REGION_AND_RACK_CONTROLLER): |
301 | 199 | # Interface VLAN was changed on a controller. Move all linked subnets | 199 | # Interface VLAN was changed on a controller. Move all linked subnets |
307 | 200 | # to that new VLAN. | 200 | # to that new VLAN, unless the new VLAN is None. When the VLAN is |
308 | 201 | for ip_address in instance.ip_addresses.all(): | 201 | # None then the administrator is say that the interface is now |
309 | 202 | if ip_address.subnet is not None: | 202 | # disconnected. |
310 | 203 | ip_address.subnet.vlan = new_vlan | 203 | if new_vlan is not None: |
311 | 204 | ip_address.subnet.save() | 204 | for ip_address in instance.ip_addresses.all(): |
312 | 205 | if ip_address.subnet is not None: | ||
313 | 206 | ip_address.subnet.vlan = new_vlan | ||
314 | 207 | ip_address.subnet.save() | ||
315 | 205 | 208 | ||
326 | 206 | # If any children are VLAN interfaces then we need to move those | 209 | # If any children are VLAN interfaces then we need to move those |
327 | 207 | # VLANs into the same fabric as the parent. | 210 | # VLANs into the same fabric as the parent. |
328 | 208 | for rel in instance.children_relationships.all(): | 211 | for rel in instance.children_relationships.all(): |
329 | 209 | if rel.child.type == INTERFACE_TYPE.VLAN: | 212 | if rel.child.type == INTERFACE_TYPE.VLAN: |
330 | 210 | new_child_vlan, _ = VLAN.objects.get_or_create( | 213 | new_child_vlan, _ = VLAN.objects.get_or_create( |
331 | 211 | fabric=new_vlan.fabric, vid=rel.child.vlan.vid) | 214 | fabric=new_vlan.fabric, vid=rel.child.vlan.vid) |
332 | 212 | rel.child.vlan = new_child_vlan | 215 | rel.child.vlan = new_child_vlan |
333 | 213 | rel.child.save() | 216 | rel.child.save() |
334 | 214 | # No need to update the IP addresses here this function | 217 | # No need to update the IP addresses here this function |
335 | 215 | # will be called again because the child has been saved. | 218 | # will be called again because the child has been saved. |
336 | 216 | 219 | ||
337 | 217 | else: | 220 | else: |
338 | 218 | # Interface VLAN was changed on a machine or device. Remove all its | 221 | # Interface VLAN was changed on a machine or device. Remove all its |
339 | 219 | 222 | ||
340 | === modified file 'src/maasserver/models/signals/tests/test_interfaces.py' | |||
341 | --- src/maasserver/models/signals/tests/test_interfaces.py 2016-04-11 16:23:26 +0000 | |||
342 | +++ src/maasserver/models/signals/tests/test_interfaces.py 2016-07-26 10:11:30 +0000 | |||
343 | @@ -194,6 +194,17 @@ | |||
344 | 194 | self.assertIsNone(reload_object(static_ip)) | 194 | self.assertIsNone(reload_object(static_ip)) |
345 | 195 | self.assertIsNotNone(reload_object(discovered_ip)) | 195 | self.assertIsNotNone(reload_object(discovered_ip)) |
346 | 196 | 196 | ||
347 | 197 | def test__removes_links_when_goes_to_disconnected(self): | ||
348 | 198 | node = self.maker() | ||
349 | 199 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) | ||
350 | 200 | static_ip = factory.make_StaticIPAddress(interface=interface) | ||
351 | 201 | discovered_ip = factory.make_StaticIPAddress( | ||
352 | 202 | alloc_type=IPADDRESS_TYPE.DISCOVERED, interface=interface) | ||
353 | 203 | interface.vlan = None | ||
354 | 204 | interface.save() | ||
355 | 205 | self.assertIsNone(reload_object(static_ip)) | ||
356 | 206 | self.assertIsNotNone(reload_object(discovered_ip)) | ||
357 | 207 | |||
358 | 197 | 208 | ||
359 | 198 | class TestInterfaceVLANUpdateController(MAASServerTestCase): | 209 | class TestInterfaceVLANUpdateController(MAASServerTestCase): |
360 | 199 | 210 | ||
361 | @@ -222,6 +233,17 @@ | |||
362 | 222 | interface.save() | 233 | interface.save() |
363 | 223 | self.assertEquals(new_vlan, reload_object(subnet).vlan) | 234 | self.assertEquals(new_vlan, reload_object(subnet).vlan) |
364 | 224 | 235 | ||
365 | 236 | def test__doesnt_move_link_subnets_when_vlan_is_None(self): | ||
366 | 237 | node = self.maker() | ||
367 | 238 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) | ||
368 | 239 | old_vlan = interface.vlan | ||
369 | 240 | subnet = factory.make_Subnet(vlan=old_vlan) | ||
370 | 241 | factory.make_StaticIPAddress( | ||
371 | 242 | subnet=subnet, interface=interface) | ||
372 | 243 | interface.vlan = None | ||
373 | 244 | interface.save() | ||
374 | 245 | self.assertEquals(old_vlan, reload_object(subnet).vlan) | ||
375 | 246 | |||
376 | 225 | def test__moves_children_vlans_to_same_fabric(self): | 247 | def test__moves_children_vlans_to_same_fabric(self): |
377 | 226 | node = self.maker() | 248 | node = self.maker() |
378 | 227 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) | 249 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
379 | 228 | 250 | ||
380 | === modified file 'src/maasserver/models/tests/test_interface.py' | |||
381 | --- src/maasserver/models/tests/test_interface.py 2016-05-12 19:07:37 +0000 | |||
382 | +++ src/maasserver/models/tests/test_interface.py 2016-07-26 10:11:30 +0000 | |||
383 | @@ -42,6 +42,7 @@ | |||
384 | 42 | UnknownInterface, | 42 | UnknownInterface, |
385 | 43 | VLANInterface, | 43 | VLANInterface, |
386 | 44 | ) | 44 | ) |
387 | 45 | from maasserver.models.vlan import DEFAULT_MTU | ||
388 | 45 | from maasserver.testing.factory import factory | 46 | from maasserver.testing.factory import factory |
389 | 46 | from maasserver.testing.orm import reload_objects | 47 | from maasserver.testing.orm import reload_objects |
390 | 47 | from maasserver.testing.testcase import ( | 48 | from maasserver.testing.testcase import ( |
391 | @@ -527,6 +528,17 @@ | |||
392 | 527 | name=name, node=node, mac_address=mac, | 528 | name=name, node=node, mac_address=mac, |
393 | 528 | type=INTERFACE_TYPE.PHYSICAL)) | 529 | type=INTERFACE_TYPE.PHYSICAL)) |
394 | 529 | 530 | ||
395 | 531 | def test_allows_null_vlan(self): | ||
396 | 532 | name = factory.make_name('name') | ||
397 | 533 | node = factory.make_Node() | ||
398 | 534 | mac = factory.make_MAC() | ||
399 | 535 | interface = factory.make_Interface( | ||
400 | 536 | INTERFACE_TYPE.PHYSICAL, | ||
401 | 537 | name=name, node=node, mac_address=mac, disconnected=True) | ||
402 | 538 | self.assertThat(interface, MatchesStructure.byEquality( | ||
403 | 539 | name=name, node=node, mac_address=mac, | ||
404 | 540 | type=INTERFACE_TYPE.PHYSICAL, vlan=None)) | ||
405 | 541 | |||
406 | 530 | def test_string_representation_contains_essential_data(self): | 542 | def test_string_representation_contains_essential_data(self): |
407 | 531 | name = factory.make_name('name') | 543 | name = factory.make_name('name') |
408 | 532 | node = factory.make_Node() | 544 | node = factory.make_Node() |
409 | @@ -570,6 +582,11 @@ | |||
410 | 570 | nic1.vlan.save() | 582 | nic1.vlan.save() |
411 | 571 | self.assertEqual(vlan_mtu, nic1.get_effective_mtu()) | 583 | self.assertEqual(vlan_mtu, nic1.get_effective_mtu()) |
412 | 572 | 584 | ||
413 | 585 | def test_get_effective_mtu_returns_default_mtu(self): | ||
414 | 586 | nic1 = factory.make_Interface( | ||
415 | 587 | INTERFACE_TYPE.PHYSICAL, disconnected=True) | ||
416 | 588 | self.assertEqual(DEFAULT_MTU, nic1.get_effective_mtu()) | ||
417 | 589 | |||
418 | 573 | def test_get_links_returns_links_for_each_type(self): | 590 | def test_get_links_returns_links_for_each_type(self): |
419 | 574 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 591 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
420 | 575 | links = [] | 592 | links = [] |
421 | @@ -1595,6 +1612,11 @@ | |||
422 | 1595 | class TestForceAutoOrDHCPLink(MAASServerTestCase): | 1612 | class TestForceAutoOrDHCPLink(MAASServerTestCase): |
423 | 1596 | """Tests for `Interface.force_auto_or_dhcp_link`.""" | 1613 | """Tests for `Interface.force_auto_or_dhcp_link`.""" |
424 | 1597 | 1614 | ||
425 | 1615 | def test__does_nothing_when_disconnected(self): | ||
426 | 1616 | interface = factory.make_Interface( | ||
427 | 1617 | INTERFACE_TYPE.PHYSICAL, disconnected=True) | ||
428 | 1618 | self.assertIsNone(interface.force_auto_or_dhcp_link()) | ||
429 | 1619 | |||
430 | 1598 | def test__sets_to_AUTO_on_subnet(self): | 1620 | def test__sets_to_AUTO_on_subnet(self): |
431 | 1599 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 1621 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
432 | 1600 | subnet = factory.make_Subnet(vlan=interface.vlan) | 1622 | subnet = factory.make_Subnet(vlan=interface.vlan) |
433 | @@ -1622,6 +1644,15 @@ | |||
434 | 1622 | 1, interface.ip_addresses.count(), | 1644 | 1, interface.ip_addresses.count(), |
435 | 1623 | "Should only have one IP address assigned.") | 1645 | "Should only have one IP address assigned.") |
436 | 1624 | 1646 | ||
437 | 1647 | def test__does_nothing_if_no_vlan(self): | ||
438 | 1648 | interface = factory.make_Interface( | ||
439 | 1649 | INTERFACE_TYPE.PHYSICAL, disconnected=True) | ||
440 | 1650 | interface.ensure_link_up() | ||
441 | 1651 | interface = reload_object(interface) | ||
442 | 1652 | self.assertEqual( | ||
443 | 1653 | 0, interface.ip_addresses.count(), | ||
444 | 1654 | "Should only have no IP address assigned.") | ||
445 | 1655 | |||
446 | 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): |
447 | 1626 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 1657 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
448 | 1627 | subnet = factory.make_Subnet(vlan=interface.vlan) | 1658 | subnet = factory.make_Subnet(vlan=interface.vlan) |
449 | 1628 | 1659 | ||
450 | === modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js' | |||
451 | --- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-07-11 18:29:06 +0000 | |||
452 | +++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-07-26 10:11:30 +0000 | |||
453 | @@ -654,11 +654,17 @@ | |||
454 | 654 | if($scope.isInterfaceNameInvalid(nic)) { | 654 | if($scope.isInterfaceNameInvalid(nic)) { |
455 | 655 | nic.name = originalInteface.name; | 655 | nic.name = originalInteface.name; |
456 | 656 | } else if(originalInteface.name !== nic.name || | 656 | } else if(originalInteface.name !== nic.name || |
457 | 657 | (originalInteface.vlan_id === null && nic.vlan !== null) || | ||
458 | 658 | (originalInteface.vlan_id !== null && nic.vlan === null) || | ||
459 | 657 | originalInteface.vlan_id !== nic.vlan.id) { | 659 | originalInteface.vlan_id !== nic.vlan.id) { |
460 | 658 | var params = { | 660 | var params = { |
463 | 659 | "name": nic.name, | 661 | "name": nic.name |
462 | 660 | "vlan": nic.vlan.id | ||
464 | 661 | }; | 662 | }; |
465 | 663 | if(nic.vlan !== null) { | ||
466 | 664 | params.vlan = nic.vlan.id; | ||
467 | 665 | } else { | ||
468 | 666 | params.vlan = null; | ||
469 | 667 | } | ||
470 | 662 | $scope.$parent.nodesManager.updateInterface( | 668 | $scope.$parent.nodesManager.updateInterface( |
471 | 663 | $scope.node, nic.id, params).then(null, function(error) { | 669 | $scope.node, nic.id, params).then(null, function(error) { |
472 | 664 | // XXX blake_r: Just log the error in the console, but | 670 | // XXX blake_r: Just log the error in the console, but |
473 | @@ -716,7 +722,11 @@ | |||
474 | 716 | $scope.fabricChanged = function(nic) { | 722 | $scope.fabricChanged = function(nic) { |
475 | 717 | // Update the VLAN on the node to be the default VLAN for that | 723 | // Update the VLAN on the node to be the default VLAN for that |
476 | 718 | // fabric. The first VLAN for the fabric is the default. | 724 | // fabric. The first VLAN for the fabric is the default. |
478 | 719 | nic.vlan = getDefaultVLAN(nic.fabric); | 725 | if(nic.fabric !== null) { |
479 | 726 | nic.vlan = getDefaultVLAN(nic.fabric); | ||
480 | 727 | } else { | ||
481 | 728 | nic.vlan = null; | ||
482 | 729 | } | ||
483 | 720 | $scope.saveInterface(nic); | 730 | $scope.saveInterface(nic); |
484 | 721 | }; | 731 | }; |
485 | 722 | 732 | ||
486 | 723 | 733 | ||
487 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js' | |||
488 | --- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-07-11 18:29:06 +0000 | |||
489 | +++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-07-26 10:11:30 +0000 | |||
490 | @@ -1685,6 +1685,62 @@ | |||
491 | 1685 | "vlan": vlan.id | 1685 | "vlan": vlan.id |
492 | 1686 | }); | 1686 | }); |
493 | 1687 | }); | 1687 | }); |
494 | 1688 | |||
495 | 1689 | it("calls MachinesManager.updateInterface if vlan set", function() { | ||
496 | 1690 | var controller = makeController(); | ||
497 | 1691 | var id = makeInteger(0, 100); | ||
498 | 1692 | var name = makeName("nic"); | ||
499 | 1693 | var vlan = { id: makeInteger(0, 100) }; | ||
500 | 1694 | var original_nic = { | ||
501 | 1695 | id: id, | ||
502 | 1696 | name: name, | ||
503 | 1697 | vlan_id: null | ||
504 | 1698 | }; | ||
505 | 1699 | var nic = { | ||
506 | 1700 | id: id, | ||
507 | 1701 | name: name, | ||
508 | 1702 | vlan: vlan | ||
509 | 1703 | }; | ||
510 | 1704 | $scope.originalInterfaces[id] = original_nic; | ||
511 | 1705 | $scope.interfaces = [nic]; | ||
512 | 1706 | |||
513 | 1707 | spyOn(MachinesManager, "updateInterface").and.returnValue( | ||
514 | 1708 | $q.defer().promise); | ||
515 | 1709 | $scope.saveInterface(nic); | ||
516 | 1710 | expect(MachinesManager.updateInterface).toHaveBeenCalledWith( | ||
517 | 1711 | node, id, { | ||
518 | 1712 | "name": name, | ||
519 | 1713 | "vlan": vlan.id | ||
520 | 1714 | }); | ||
521 | 1715 | }); | ||
522 | 1716 | |||
523 | 1717 | it("calls MachinesManager.updateInterface if vlan unset", function() { | ||
524 | 1718 | var controller = makeController(); | ||
525 | 1719 | var id = makeInteger(0, 100); | ||
526 | 1720 | var name = makeName("nic"); | ||
527 | 1721 | var vlan = { id: makeInteger(0, 100) }; | ||
528 | 1722 | var original_nic = { | ||
529 | 1723 | id: id, | ||
530 | 1724 | name: name, | ||
531 | 1725 | vlan_id: makeInteger(200, 300) | ||
532 | 1726 | }; | ||
533 | 1727 | var nic = { | ||
534 | 1728 | id: id, | ||
535 | 1729 | name: name, | ||
536 | 1730 | vlan: null | ||
537 | 1731 | }; | ||
538 | 1732 | $scope.originalInterfaces[id] = original_nic; | ||
539 | 1733 | $scope.interfaces = [nic]; | ||
540 | 1734 | |||
541 | 1735 | spyOn(MachinesManager, "updateInterface").and.returnValue( | ||
542 | 1736 | $q.defer().promise); | ||
543 | 1737 | $scope.saveInterface(nic); | ||
544 | 1738 | expect(MachinesManager.updateInterface).toHaveBeenCalledWith( | ||
545 | 1739 | node, id, { | ||
546 | 1740 | "name": name, | ||
547 | 1741 | "vlan": null | ||
548 | 1742 | }); | ||
549 | 1743 | }); | ||
550 | 1688 | }); | 1744 | }); |
551 | 1689 | 1745 | ||
552 | 1690 | describe("setFocusInterface", function() { | 1746 | describe("setFocusInterface", function() { |
553 | @@ -1847,6 +1903,17 @@ | |||
554 | 1847 | expect(nic.vlan).toBe(vlan); | 1903 | expect(nic.vlan).toBe(vlan); |
555 | 1848 | }); | 1904 | }); |
556 | 1849 | 1905 | ||
557 | 1906 | it("sets vlan to null", function() { | ||
558 | 1907 | var controller = makeController(); | ||
559 | 1908 | var nic = { | ||
560 | 1909 | vlan: {}, | ||
561 | 1910 | fabric: null | ||
562 | 1911 | }; | ||
563 | 1912 | spyOn($scope, "saveInterface"); | ||
564 | 1913 | $scope.fabricChanged(nic); | ||
565 | 1914 | expect(nic.vlan).toBeNull(); | ||
566 | 1915 | }); | ||
567 | 1916 | |||
568 | 1850 | it("calls saveInterface", function() { | 1917 | it("calls saveInterface", function() { |
569 | 1851 | var controller = makeController(); | 1918 | var controller = makeController(); |
570 | 1852 | var fabric = { | 1919 | var fabric = { |
571 | 1853 | 1920 | ||
572 | === modified file 'src/maasserver/static/partials/node-details.html' | |||
573 | --- src/maasserver/static/partials/node-details.html 2016-07-15 00:42:25 +0000 | |||
574 | +++ src/maasserver/static/partials/node-details.html 2016-07-26 10:11:30 +0000 | |||
575 | @@ -617,11 +617,13 @@ | |||
576 | 617 | data-ng-disabled="interface.type == 'alias' || interface.type == 'vlan' || !isNodeEditingAllowed()" | 617 | data-ng-disabled="interface.type == 'alias' || interface.type == 'vlan' || !isNodeEditingAllowed()" |
577 | 618 | data-ng-change="fabricChanged(interface)" | 618 | data-ng-change="fabricChanged(interface)" |
578 | 619 | data-ng-options="fabric as fabric.name for fabric in fabrics"> | 619 | data-ng-options="fabric as fabric.name for fabric in fabrics"> |
579 | 620 | <option value="">Disconnected</option> | ||
580 | 620 | </select> | 621 | </select> |
581 | 621 | </div> | 622 | </div> |
582 | 622 | <div class="table__data table-col--14"> | 623 | <div class="table__data table-col--14"> |
583 | 623 | <select class="table__input" name="vlan" id="vlan" | 624 | <select class="table__input" name="vlan" id="vlan" |
584 | 624 | data-ng-model="interface.vlan" | 625 | data-ng-model="interface.vlan" |
585 | 626 | data-ng-if="interface.fabric" | ||
586 | 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()" |
587 | 626 | data-ng-change="saveInterface(interface)" | 628 | data-ng-change="saveInterface(interface)" |
588 | 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"> |
589 | @@ -629,6 +631,7 @@ | |||
590 | 629 | </div> | 631 | </div> |
591 | 630 | <div class="table__data table-col--18"> | 632 | <div class="table__data table-col--18"> |
592 | 631 | <select class="table__input" name="subnet" id="subnet" | 633 | <select class="table__input" name="subnet" id="subnet" |
593 | 634 | data-ng-if="interface.fabric" | ||
594 | 632 | data-ng-hide="isAllNetworkingDisabled() && interface.discovered[0].subnet_id" | 635 | data-ng-hide="isAllNetworkingDisabled() && interface.discovered[0].subnet_id" |
595 | 633 | data-ng-disabled="isController || !isNodeEditingAllowed()" | 636 | data-ng-disabled="isController || !isNodeEditingAllowed()" |
596 | 634 | data-ng-model="interface.subnet" | 637 | data-ng-model="interface.subnet" |
597 | @@ -641,7 +644,7 @@ | |||
598 | 641 | </span> | 644 | </span> |
599 | 642 | </div> | 645 | </div> |
600 | 643 | <div class="table__data table-col--21"> | 646 | <div class="table__data table-col--21"> |
602 | 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"> |
603 | 645 | <li class="no-margin"> | 648 | <li class="no-margin"> |
604 | 646 | <select class="table__input" name="link-mode" id="link-mode" | 649 | <select class="table__input" name="link-mode" id="link-mode" |
605 | 647 | data-ng-model="interface.mode" | 650 | data-ng-model="interface.mode" |
606 | 648 | 651 | ||
607 | === modified file 'src/maasserver/testing/factory.py' | |||
608 | --- src/maasserver/testing/factory.py 2016-07-20 19:27:20 +0000 | |||
609 | +++ src/maasserver/testing/factory.py 2016-07-26 10:11:30 +0000 | |||
610 | @@ -902,7 +902,7 @@ | |||
611 | 902 | def make_Interface( | 902 | def make_Interface( |
612 | 903 | self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None, | 903 | self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None, |
613 | 904 | vlan=None, parents=None, name=None, cluster_interface=None, | 904 | vlan=None, parents=None, name=None, cluster_interface=None, |
615 | 905 | ip=None, enabled=True, fabric=None, tags=None): | 905 | ip=None, enabled=True, fabric=None, tags=None, disconnected=False): |
616 | 906 | if name is None: | 906 | if name is None: |
617 | 907 | if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN): | 907 | if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN): |
618 | 908 | name = self.make_name('eth') | 908 | name = self.make_name('eth') |
619 | @@ -919,20 +919,21 @@ | |||
620 | 919 | name = None | 919 | name = None |
621 | 920 | if iftype is None: | 920 | if iftype is None: |
622 | 921 | iftype = INTERFACE_TYPE.PHYSICAL | 921 | iftype = INTERFACE_TYPE.PHYSICAL |
637 | 922 | if vlan is None: | 922 | if not disconnected: |
638 | 923 | if fabric is not None: | 923 | if vlan is None: |
639 | 924 | if iftype == INTERFACE_TYPE.VLAN: | 924 | if fabric is not None: |
640 | 925 | vlan = self.make_VLAN(fabric=fabric) | 925 | if iftype == INTERFACE_TYPE.VLAN: |
641 | 926 | else: | 926 | vlan = self.make_VLAN(fabric=fabric) |
642 | 927 | vlan = fabric.get_default_vlan() | 927 | else: |
643 | 928 | else: | 928 | vlan = fabric.get_default_vlan() |
644 | 929 | if iftype == INTERFACE_TYPE.VLAN and parents: | 929 | else: |
645 | 930 | vlan = self.make_VLAN(fabric=parents[0].vlan.fabric) | 930 | if iftype == INTERFACE_TYPE.VLAN and parents: |
646 | 931 | elif iftype == INTERFACE_TYPE.BOND and parents: | 931 | vlan = self.make_VLAN(fabric=parents[0].vlan.fabric) |
647 | 932 | vlan = parents[0].vlan | 932 | elif iftype == INTERFACE_TYPE.BOND and parents: |
648 | 933 | else: | 933 | vlan = parents[0].vlan |
649 | 934 | fabric = self.make_Fabric() | 934 | else: |
650 | 935 | vlan = fabric.get_default_vlan() | 935 | fabric = self.make_Fabric() |
651 | 936 | vlan = fabric.get_default_vlan() | ||
652 | 936 | if (mac_address is None and | 937 | if (mac_address is None and |
653 | 937 | iftype in [ | 938 | iftype in [ |
654 | 938 | INTERFACE_TYPE.PHYSICAL, | 939 | INTERFACE_TYPE.PHYSICAL, |
655 | 939 | 940 | ||
656 | === modified file 'src/maasserver/tests/test_forms_interface.py' | |||
657 | --- src/maasserver/tests/test_forms_interface.py 2016-04-22 17:28:15 +0000 | |||
658 | +++ src/maasserver/tests/test_forms_interface.py 2016-07-26 10:11:30 +0000 | |||
659 | @@ -86,6 +86,22 @@ | |||
660 | 86 | MatchesStructure.byEquality( | 86 | MatchesStructure.byEquality( |
661 | 87 | name=interface.name, vlan=new_vlan, enabled=interface.enabled)) | 87 | name=interface.name, vlan=new_vlan, enabled=interface.enabled)) |
662 | 88 | 88 | ||
663 | 89 | def test__allows_no_vlan(self): | ||
664 | 90 | node = self.maker() | ||
665 | 91 | interface = factory.make_Interface( | ||
666 | 92 | INTERFACE_TYPE.PHYSICAL, node=node) | ||
667 | 93 | form = ControllerInterfaceForm( | ||
668 | 94 | instance=interface, | ||
669 | 95 | data={ | ||
670 | 96 | 'vlan': None, | ||
671 | 97 | }) | ||
672 | 98 | self.assertTrue(form.is_valid(), form.errors) | ||
673 | 99 | interface = form.save() | ||
674 | 100 | self.assertThat( | ||
675 | 101 | interface, | ||
676 | 102 | MatchesStructure.byEquality( | ||
677 | 103 | name=interface.name, vlan=None, enabled=interface.enabled)) | ||
678 | 104 | |||
679 | 89 | 105 | ||
680 | 90 | class PhysicalInterfaceFormTest(MAASServerTestCase): | 106 | class PhysicalInterfaceFormTest(MAASServerTestCase): |
681 | 91 | 107 | ||
682 | @@ -116,6 +132,30 @@ | |||
683 | 116 | type=INTERFACE_TYPE.PHYSICAL, tags=tags)) | 132 | type=INTERFACE_TYPE.PHYSICAL, tags=tags)) |
684 | 117 | self.assertItemsEqual([], interface.parents.all()) | 133 | self.assertItemsEqual([], interface.parents.all()) |
685 | 118 | 134 | ||
686 | 135 | def test__creates_physical_interface_disconnected(self): | ||
687 | 136 | node = factory.make_Node() | ||
688 | 137 | mac_address = factory.make_mac_address() | ||
689 | 138 | interface_name = 'eth0' | ||
690 | 139 | tags = [ | ||
691 | 140 | factory.make_name("tag") | ||
692 | 141 | for _ in range(3) | ||
693 | 142 | ] | ||
694 | 143 | form = PhysicalInterfaceForm( | ||
695 | 144 | node=node, | ||
696 | 145 | data={ | ||
697 | 146 | 'name': interface_name, | ||
698 | 147 | 'mac_address': mac_address, | ||
699 | 148 | 'tags': ",".join(tags), | ||
700 | 149 | }) | ||
701 | 150 | self.assertTrue(form.is_valid(), form.errors) | ||
702 | 151 | interface = form.save() | ||
703 | 152 | self.assertThat( | ||
704 | 153 | interface, | ||
705 | 154 | MatchesStructure.byEquality( | ||
706 | 155 | node=node, mac_address=mac_address, name=interface_name, | ||
707 | 156 | type=INTERFACE_TYPE.PHYSICAL, tags=tags, vlan=None)) | ||
708 | 157 | self.assertItemsEqual([], interface.parents.all()) | ||
709 | 158 | |||
710 | 119 | def test__create_ensures_link_up(self): | 159 | def test__create_ensures_link_up(self): |
711 | 120 | node = factory.make_Node() | 160 | node = factory.make_Node() |
712 | 121 | mac_address = factory.make_mac_address() | 161 | mac_address = factory.make_mac_address() |
713 | @@ -255,6 +295,26 @@ | |||
714 | 255 | name=new_name, vlan=new_vlan, enabled=False, tags=[])) | 295 | name=new_name, vlan=new_vlan, enabled=False, tags=[])) |
715 | 256 | self.assertItemsEqual([], interface.parents.all()) | 296 | self.assertItemsEqual([], interface.parents.all()) |
716 | 257 | 297 | ||
717 | 298 | def test__edits_interface_disconnected(self): | ||
718 | 299 | interface = factory.make_Interface( | ||
719 | 300 | INTERFACE_TYPE.PHYSICAL, name='eth0') | ||
720 | 301 | new_name = 'eth1' | ||
721 | 302 | form = PhysicalInterfaceForm( | ||
722 | 303 | instance=interface, | ||
723 | 304 | data={ | ||
724 | 305 | 'name': new_name, | ||
725 | 306 | 'vlan': None, | ||
726 | 307 | 'enabled': False, | ||
727 | 308 | 'tags': "", | ||
728 | 309 | }) | ||
729 | 310 | self.assertTrue(form.is_valid(), form.errors) | ||
730 | 311 | interface = form.save() | ||
731 | 312 | self.assertThat( | ||
732 | 313 | interface, | ||
733 | 314 | MatchesStructure.byEquality( | ||
734 | 315 | name=new_name, vlan=None, enabled=False, tags=[])) | ||
735 | 316 | self.assertItemsEqual([], interface.parents.all()) | ||
736 | 317 | |||
737 | 258 | def test__create_sets_interface_parameters(self): | 318 | def test__create_sets_interface_parameters(self): |
738 | 259 | node = factory.make_Node() | 319 | node = factory.make_Node() |
739 | 260 | mac_address = factory.make_mac_address() | 320 | mac_address = factory.make_mac_address() |
740 | @@ -403,6 +463,20 @@ | |||
741 | 403 | self.assertIsNotNone( | 463 | self.assertIsNotNone( |
742 | 404 | interface.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.STICKY)) | 464 | interface.ip_addresses.filter(alloc_type=IPADDRESS_TYPE.STICKY)) |
743 | 405 | 465 | ||
744 | 466 | def test__create_rejects_interface_without_vlan(self): | ||
745 | 467 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | ||
746 | 468 | form = VLANInterfaceForm( | ||
747 | 469 | node=parent.node, | ||
748 | 470 | data={ | ||
749 | 471 | 'parents': [parent.id], | ||
750 | 472 | }) | ||
751 | 473 | self.assertFalse(form.is_valid(), form.errors) | ||
752 | 474 | self.assertItemsEqual( | ||
753 | 475 | ['vlan'], form.errors.keys(), form.errors) | ||
754 | 476 | self.assertIn( | ||
755 | 477 | "A VLAN interface must be connected to a tagged VLAN.", | ||
756 | 478 | form.errors['vlan'][0]) | ||
757 | 479 | |||
758 | 406 | def test_rejects_interface_with_duplicate_name(self): | 480 | def test_rejects_interface_with_duplicate_name(self): |
759 | 407 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 481 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
760 | 408 | vlan = factory.make_VLAN(fabric=parent.vlan.fabric, vid=10) | 482 | vlan = factory.make_VLAN(fabric=parent.vlan.fabric, vid=10) |
761 | @@ -468,6 +542,20 @@ | |||
762 | 468 | "VLAN interface can't have another VLAN interface as parent.", | 542 | "VLAN interface can't have another VLAN interface as parent.", |
763 | 469 | form.errors['parents'][0]) | 543 | form.errors['parents'][0]) |
764 | 470 | 544 | ||
765 | 545 | def test__rejects_no_vlan(self): | ||
766 | 546 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | ||
767 | 547 | form = VLANInterfaceForm( | ||
768 | 548 | node=parent.node, | ||
769 | 549 | data={ | ||
770 | 550 | 'vlan': None, | ||
771 | 551 | 'parents': [parent.id], | ||
772 | 552 | }) | ||
773 | 553 | self.assertFalse(form.is_valid(), form.errors) | ||
774 | 554 | self.assertItemsEqual(['vlan'], form.errors.keys()) | ||
775 | 555 | self.assertIn( | ||
776 | 556 | "A VLAN interface must be connected to a tagged VLAN.", | ||
777 | 557 | form.errors['vlan'][0]) | ||
778 | 558 | |||
779 | 471 | def test__rejects_vlan_not_on_same_fabric(self): | 559 | def test__rejects_vlan_not_on_same_fabric(self): |
780 | 472 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 560 | parent = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
781 | 473 | factory.make_VLAN(fabric=parent.vlan.fabric, vid=10) | 561 | factory.make_VLAN(fabric=parent.vlan.fabric, vid=10) |
782 | @@ -576,7 +664,8 @@ | |||
783 | 576 | self.assertThat( | 664 | self.assertThat( |
784 | 577 | interface, | 665 | interface, |
785 | 578 | MatchesStructure.byEquality( | 666 | MatchesStructure.byEquality( |
787 | 579 | name=interface_name, type=INTERFACE_TYPE.BOND)) | 667 | name=interface_name, type=INTERFACE_TYPE.BOND, |
788 | 668 | vlan=parent1.vlan)) | ||
789 | 580 | self.assertIn( | 669 | self.assertIn( |
790 | 581 | interface.mac_address, [parent1.mac_address, parent2.mac_address]) | 670 | interface.mac_address, [parent1.mac_address, parent2.mac_address]) |
791 | 582 | self.assertItemsEqual([parent1, parent2], interface.parents.all()) | 671 | self.assertItemsEqual([parent1, parent2], interface.parents.all()) |
792 | @@ -787,6 +876,25 @@ | |||
793 | 787 | for parent in [parent1, parent2, new_parent] | 876 | for parent in [parent1, parent2, new_parent] |
794 | 788 | )) | 877 | )) |
795 | 789 | 878 | ||
796 | 879 | def test__edits_interface_allows_disconnected(self): | ||
797 | 880 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | ||
798 | 881 | parent2 = factory.make_Interface( | ||
799 | 882 | INTERFACE_TYPE.PHYSICAL, node=parent1.node) | ||
800 | 883 | interface = factory.make_Interface( | ||
801 | 884 | INTERFACE_TYPE.BOND, parents=[parent1, parent2]) | ||
802 | 885 | form = BondInterfaceForm( | ||
803 | 886 | instance=interface, | ||
804 | 887 | data={ | ||
805 | 888 | 'vlan': None, | ||
806 | 889 | }) | ||
807 | 890 | self.assertTrue(form.is_valid(), form.errors) | ||
808 | 891 | interface = form.save() | ||
809 | 892 | self.assertThat( | ||
810 | 893 | interface, | ||
811 | 894 | MatchesStructure.byEquality( | ||
812 | 895 | mac_address=interface.mac_address, vlan=None, | ||
813 | 896 | type=INTERFACE_TYPE.BOND)) | ||
814 | 897 | |||
815 | 790 | def test__edits_interface_removes_parents(self): | 898 | def test__edits_interface_removes_parents(self): |
816 | 791 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 899 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
817 | 792 | parent2 = factory.make_Interface( | 900 | parent2 = factory.make_Interface( |
818 | @@ -1119,6 +1227,26 @@ | |||
819 | 1119 | self.assertItemsEqual( | 1227 | self.assertItemsEqual( |
820 | 1120 | [parent1, parent2, new_parent], interface.parents.all()) | 1228 | [parent1, parent2, new_parent], interface.parents.all()) |
821 | 1121 | 1229 | ||
822 | 1230 | def test__edits_interface_allows_disconnected(self): | ||
823 | 1231 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | ||
824 | 1232 | parent2 = factory.make_Interface( | ||
825 | 1233 | INTERFACE_TYPE.PHYSICAL, node=parent1.node, vlan=parent1.vlan) | ||
826 | 1234 | interface = factory.make_Interface( | ||
827 | 1235 | INTERFACE_TYPE.BRIDGE, | ||
828 | 1236 | parents=[parent1, parent2]) | ||
829 | 1237 | form = BridgeInterfaceForm( | ||
830 | 1238 | instance=interface, | ||
831 | 1239 | data={ | ||
832 | 1240 | 'vlan': None, | ||
833 | 1241 | }) | ||
834 | 1242 | self.assertTrue(form.is_valid(), form.errors) | ||
835 | 1243 | interface = form.save() | ||
836 | 1244 | self.assertThat( | ||
837 | 1245 | interface, | ||
838 | 1246 | MatchesStructure.byEquality( | ||
839 | 1247 | mac_address=interface.mac_address, | ||
840 | 1248 | vlan=None, type=INTERFACE_TYPE.BRIDGE)) | ||
841 | 1249 | |||
842 | 1122 | def test__edits_interface_removes_parents(self): | 1250 | def test__edits_interface_removes_parents(self): |
843 | 1123 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) | 1251 | parent1 = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
844 | 1124 | parent2 = factory.make_Interface( | 1252 | parent2 = factory.make_Interface( |
845 | 1125 | 1253 | ||
846 | === modified file 'src/maasserver/websockets/handlers/node.py' | |||
847 | --- src/maasserver/websockets/handlers/node.py 2016-05-12 19:07:37 +0000 | |||
848 | +++ src/maasserver/websockets/handlers/node.py 2016-07-26 10:11:30 +0000 | |||
849 | @@ -455,7 +455,8 @@ | |||
850 | 455 | def get_all_fabric_names(self, obj, subnets): | 455 | def get_all_fabric_names(self, obj, subnets): |
851 | 456 | fabric_names = set() | 456 | fabric_names = set() |
852 | 457 | for interface in obj.interface_set.all(): | 457 | for interface in obj.interface_set.all(): |
854 | 458 | fabric_names.add(interface.vlan.fabric.name) | 458 | if interface.vlan is not None: |
855 | 459 | fabric_names.add(interface.vlan.fabric.name) | ||
856 | 459 | for subnet in subnets: | 460 | for subnet in subnets: |
857 | 460 | fabric_names.add(subnet.vlan.fabric.name) | 461 | fabric_names.add(subnet.vlan.fabric.name) |
858 | 461 | return list(fabric_names) | 462 | return list(fabric_names) |
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!