Merge lp:~blake-rouse/maas/vlan-relay into lp:~maas-committers/maas/trunk
- vlan-relay
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5585 |
Proposed branch: | lp:~blake-rouse/maas/vlan-relay |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1628 lines (+770/-208) 22 files modified
src/maasserver/api/tests/test_vlans.py (+40/-0) src/maasserver/api/vlans.py (+8/-1) src/maasserver/dhcp.py (+41/-61) src/maasserver/exceptions.py (+0/-4) src/maasserver/forms_vlan.py (+25/-0) src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py (+1/-1) src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py (+23/-0) src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py (+24/-0) src/maasserver/models/tests/test_vlan.py (+8/-0) src/maasserver/models/vlan.py (+5/-0) src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js (+56/-3) src/maasserver/static/js/angular/controllers/vlan_details.js (+126/-18) src/maasserver/static/js/angular/factories/tests/test_vlans.js (+6/-3) src/maasserver/static/js/angular/factories/vlans.js (+12/-7) src/maasserver/static/partials/vlan-details.html (+97/-22) src/maasserver/testing/factory.py (+3/-2) src/maasserver/tests/test_dhcp.py (+27/-80) src/maasserver/tests/test_forms_vlan.py (+70/-0) src/maasserver/triggers/system.py (+56/-0) src/maasserver/triggers/tests/test_system_listener.py (+114/-0) src/maasserver/websockets/handlers/tests/test_vlan.py (+15/-0) src/maasserver/websockets/handlers/vlan.py (+13/-6) |
To merge this branch: | bzr merge lp:~blake-rouse/maas/vlan-relay |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+312165@code.launchpad.net |
Commit message
Support the ability for a VLAN to act as a relay for another VLAN.
Description of the change
Blake Rouse (blake-rouse) wrote : | # |
Mike Pontillo (mpontillo) wrote : | # |
Looks good; I have a few questions and comments below before this lands.
Don't we also need a validation to ensure you cannot enable DHCP on a VLAN with a relay?
Mike Pontillo (mpontillo) wrote : | # |
Also, can you link bug #1602412 to this branch with --fixes?
Blake Rouse (blake-rouse) wrote : | # |
> Looks good; I have a few questions and comments below before this lands.
>
> Don't we also need a validation to ensure you cannot enable DHCP on a VLAN
> with a relay?
I did not want to block this. So if they enable DHCP it clears the relay_vlan and shows a warning in the UI when they go to enable.
Blake Rouse (blake-rouse) : | # |
Mike Pontillo (mpontillo) : | # |
Blake Rouse (blake-rouse) wrote : | # |
I am not making that change in my branch. That is not even related to the
change I am making. File a bug if its causing an issue.
On Wed, Nov 30, 2016 at 4:14 PM, Mike Pontillo <email address hidden>
wrote:
>
>
> Diff comments:
>
> >
> > === added file 'src/maasserver
> default_
> > --- src/maasserver/
> 1970-01-01 00:00:00 +0000
> > +++ src/maasserver/
> 2016-11-30 16:44:24 +0000
> > @@ -0,0 +1,24 @@
> > +# -*- coding: utf-8 -*-
> > +from __future__ import unicode_literals
> > +
> > +from django.db import (
> > + migrations,
> > + models,
> > +)
> > +import django.
> > +import maasserver.
> > +
> > +
> > +class Migration(
> > +
> > + dependencies = [
> > + ('maasserver', '0094_vlan_
> > + ]
> > +
> > + operations = [
> > + migrations.
> > + model_name=
> > + name='vlan',
> > + field=models.
> default=
> on_delete=
>
> Ah, my mistake. But I think there might still be an issue here, just not
> exactly where I pointed it out.
>
> According to the Django docs, CASCADE is the default when no `on_delete`
> option is set. Wouldn't that cause severe unintended consequences in the
> case of the `relay_vlan` field? I think we should use `on_delete=
> on the `relay_vlan` field.
>
> > + ),
> > + ]
>
>
> --
> https:/
> You are the owner of lp:~blake-rouse/maas/vlan-relay.
>
Mike Pontillo (mpontillo) : | # |
Blake Rouse (blake-rouse) wrote : | # |
Ah thanks for clarifying. I have fixed the issue and included a test to check.
Mike Pontillo (mpontillo) wrote : | # |
Thanks for the fixes. I found one other issue below; please at least fix the text. I'll leave it up to you what action to take in that scenario.
Preview Diff
1 | === modified file 'src/maasserver/api/tests/test_vlans.py' |
2 | --- src/maasserver/api/tests/test_vlans.py 2016-05-24 21:29:53 +0000 |
3 | +++ src/maasserver/api/tests/test_vlans.py 2016-12-06 08:04:41 +0000 |
4 | @@ -84,6 +84,29 @@ |
5 | self.assertEqual(vid, response_data['vid']) |
6 | self.assertEqual(mtu, response_data['mtu']) |
7 | |
8 | + def test_create_with_relay_vlan(self): |
9 | + self.become_admin() |
10 | + fabric = factory.make_Fabric() |
11 | + vlan_name = factory.make_name("fabric") |
12 | + vid = random.randint(1, 1000) |
13 | + mtu = random.randint(552, 1500) |
14 | + relay_vlan = factory.make_VLAN() |
15 | + uri = get_vlans_uri(fabric) |
16 | + response = self.client.post(uri, { |
17 | + "name": vlan_name, |
18 | + "vid": vid, |
19 | + "mtu": mtu, |
20 | + "relay_vlan": relay_vlan.id, |
21 | + }) |
22 | + self.assertEqual( |
23 | + http.client.OK, response.status_code, response.content) |
24 | + response_data = json.loads( |
25 | + response.content.decode(settings.DEFAULT_CHARSET)) |
26 | + self.assertEqual(vlan_name, response_data['name']) |
27 | + self.assertEqual(vid, response_data['vid']) |
28 | + self.assertEqual(mtu, response_data['mtu']) |
29 | + self.assertEqual(relay_vlan.vid, response_data['relay_vlan']['vid']) |
30 | + |
31 | def test_create_admin_only(self): |
32 | fabric = factory.make_Fabric() |
33 | vlan_name = factory.make_name("fabric") |
34 | @@ -182,6 +205,23 @@ |
35 | self.assertEqual(new_vid, parsed_vlan['vid']) |
36 | self.assertEqual(new_vid, vlan.vid) |
37 | |
38 | + def test_update_sets_relay_vlan(self): |
39 | + self.become_admin() |
40 | + fabric = factory.make_Fabric() |
41 | + vlan = factory.make_VLAN(fabric=fabric) |
42 | + uri = get_vlan_uri(vlan) |
43 | + relay_vlan = factory.make_VLAN() |
44 | + response = self.client.put(uri, { |
45 | + "relay_vlan": relay_vlan.id, |
46 | + }) |
47 | + self.assertEqual( |
48 | + http.client.OK, response.status_code, response.content) |
49 | + parsed_vlan = json.loads( |
50 | + response.content.decode(settings.DEFAULT_CHARSET)) |
51 | + vlan = reload_object(vlan) |
52 | + self.assertEqual(relay_vlan.vid, parsed_vlan['relay_vlan']['vid']) |
53 | + self.assertEqual(relay_vlan, vlan.relay_vlan) |
54 | + |
55 | def test_update_with_fabric(self): |
56 | self.become_admin() |
57 | fabric = factory.make_Fabric() |
58 | |
59 | === modified file 'src/maasserver/api/vlans.py' |
60 | --- src/maasserver/api/vlans.py 2016-04-27 20:40:24 +0000 |
61 | +++ src/maasserver/api/vlans.py 2016-12-06 08:04:41 +0000 |
62 | @@ -26,6 +26,7 @@ |
63 | 'secondary_rack', |
64 | 'dhcp_on', |
65 | 'external_dhcp', |
66 | + 'relay_vlan', |
67 | ) |
68 | |
69 | |
70 | @@ -165,12 +166,18 @@ |
71 | :type vid: integer |
72 | :param mtu: The MTU to use on the VLAN. |
73 | :type mtu: integer |
74 | - :Param dhcp_on: Whether or not DHCP should be managed on the VLAN. |
75 | + :param dhcp_on: Whether or not DHCP should be managed on the VLAN. |
76 | :type dhcp_on: boolean |
77 | :param primary_rack: The primary rack controller managing the VLAN. |
78 | :type primary_rack: system_id |
79 | :param secondary_rack: The secondary rack controller manging the VLAN. |
80 | :type secondary_rack: system_id |
81 | + :param relay_vlan: Only set when this VLAN will be using a DHCP relay |
82 | + to forward DHCP requests to another VLAN that MAAS is or will run |
83 | + the DHCP server. MAAS will not run the DHCP relay itself, it must |
84 | + be configured to proxy reqests to the primary and/or secondary |
85 | + rack controller interfaces for the VLAN specified in this field. |
86 | + :type relay_vlan: ID of VLAN |
87 | |
88 | Returns 404 if the fabric or VLAN is not found. |
89 | """ |
90 | |
91 | === modified file 'src/maasserver/dhcp.py' |
92 | --- src/maasserver/dhcp.py 2016-12-03 16:33:44 +0000 |
93 | +++ src/maasserver/dhcp.py 2016-12-06 08:04:41 +0000 |
94 | @@ -29,10 +29,7 @@ |
95 | IPRANGE_TYPE, |
96 | SERVICE_STATUS, |
97 | ) |
98 | -from maasserver.exceptions import ( |
99 | - DHCPConfigurationError, |
100 | - UnresolvableHost, |
101 | -) |
102 | +from maasserver.exceptions import UnresolvableHost |
103 | from maasserver.models import ( |
104 | Config, |
105 | DHCPSnippet, |
106 | @@ -194,18 +191,19 @@ |
107 | return [] |
108 | |
109 | |
110 | -def get_managed_vlans_for(rack_controller): |
111 | - """Return list of `VLAN` for the `rack_controller` when DHCP is enabled and |
112 | +def gen_managed_vlans_for(rack_controller): |
113 | + """Yeilds each `VLAN` for the `rack_controller` when DHCP is enabled and |
114 | `rack_controller` is either the `primary_rack` or the `secondary_rack`. |
115 | """ |
116 | interfaces = rack_controller.interface_set.filter( |
117 | Q(vlan__dhcp_on=True) & ( |
118 | Q(vlan__primary_rack=rack_controller) | |
119 | - Q(vlan__secondary_rack=rack_controller))).select_related("vlan") |
120 | - return { |
121 | - interface.vlan |
122 | - for interface in interfaces |
123 | - } |
124 | + Q(vlan__secondary_rack=rack_controller))) |
125 | + interfaces = interfaces.prefetch_related("vlan__relay_vlans") |
126 | + for interface in interfaces: |
127 | + yield interface.vlan |
128 | + for relayed_vlan in interface.vlan.relay_vlans.all(): |
129 | + yield relayed_vlan |
130 | |
131 | |
132 | def ip_is_on_vlan(ip_address, vlan): |
133 | @@ -460,12 +458,6 @@ |
134 | interfaces = get_interfaces_with_ip_on_vlan( |
135 | rack_controller, vlan, ip_version) |
136 | interface = get_best_interface(interfaces) |
137 | - if interface is None: |
138 | - raise DHCPConfigurationError( |
139 | - "No IPv%d interface on rack controller '%s' has an IP address on " |
140 | - "any subnet on VLAN '%s.%d'." % ( |
141 | - ip_version, rack_controller.hostname, vlan.fabric.name, |
142 | - vlan.vid)) |
143 | |
144 | # Generate the failover peer for this VLAN. |
145 | if vlan.secondary_rack_id is not None: |
146 | @@ -497,7 +489,7 @@ |
147 | hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets) |
148 | return ( |
149 | peer_config, sorted(subnet_configs, key=itemgetter("subnet")), |
150 | - hosts, interface.name) |
151 | + hosts, None if interface is None else interface.name) |
152 | |
153 | |
154 | @synchronous |
155 | @@ -506,7 +498,7 @@ |
156 | """Return tuple with IPv4 and IPv6 configurations for the |
157 | rack controller.""" |
158 | # Get list of all vlans that are being managed by the rack controller. |
159 | - vlans = get_managed_vlans_for(rack_controller) |
160 | + vlans = gen_managed_vlans_for(rack_controller) |
161 | |
162 | # Group the subnets on each VLAN into IPv4 and IPv6 subnets. |
163 | vlan_subnets = { |
164 | @@ -562,52 +554,40 @@ |
165 | for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items(): |
166 | # IPv4 |
167 | if len(subnets_v4) > 0: |
168 | - try: |
169 | - config = get_dhcp_configure_for( |
170 | - 4, rack_controller, vlan, subnets_v4, ntp_servers, |
171 | - default_domain, dhcp_snippets) |
172 | - except DHCPConfigurationError: |
173 | - # XXX bug #1602412: this silently breaks DHCPv4, but we cannot |
174 | - # allow it to crash here since DHCPv6 might be able to run. |
175 | - # This error may be irrelevant if there is an IPv4 network in |
176 | - # the MAAS model which is not configured on the rack, and the |
177 | - # user only wants to serve DHCPv6. But it is still something |
178 | - # worth noting, so log it and continue. |
179 | - log.err(None, "Failure configuring DHCPv4.") |
180 | - else: |
181 | - failover_peer, subnets, hosts, interface = config |
182 | - if failover_peer is not None: |
183 | - failover_peers_v4.append(failover_peer) |
184 | - shared_networks_v4.append({ |
185 | - "name": "vlan-%d" % vlan.id, |
186 | - "subnets": subnets, |
187 | - }) |
188 | - hosts_v4.extend(hosts) |
189 | + config = get_dhcp_configure_for( |
190 | + 4, rack_controller, vlan, subnets_v4, ntp_servers, |
191 | + default_domain, dhcp_snippets) |
192 | + failover_peer, subnets, hosts, interface = config |
193 | + if failover_peer is not None: |
194 | + failover_peers_v4.append(failover_peer) |
195 | + shared_networks_v4.append({ |
196 | + "name": "vlan-%d" % vlan.id, |
197 | + "subnets": subnets, |
198 | + }) |
199 | + hosts_v4.extend(hosts) |
200 | + if interface is not None: |
201 | interfaces_v4.add(interface) |
202 | # IPv6 |
203 | if len(subnets_v6) > 0: |
204 | - try: |
205 | - config = get_dhcp_configure_for( |
206 | - 6, rack_controller, vlan, subnets_v6, |
207 | - ntp_servers, default_domain, dhcp_snippets) |
208 | - except DHCPConfigurationError: |
209 | - # XXX bug #1602412: this silently breaks DHCPv6, but we cannot |
210 | - # allow it to crash here since DHCPv4 might be able to run. |
211 | - # This error may be irrelevant if there is an IPv6 network in |
212 | - # the MAAS model which is not configured on the rack, and the |
213 | - # user only wants to serve DHCPv4. But it is still something |
214 | - # worth noting, so log it and continue. |
215 | - log.err(None, "Failure configuring DHCPv6.") |
216 | - else: |
217 | - failover_peer, subnets, hosts, interface = config |
218 | - if failover_peer is not None: |
219 | - failover_peers_v6.append(failover_peer) |
220 | - shared_networks_v6.append({ |
221 | - "name": "vlan-%d" % vlan.id, |
222 | - "subnets": subnets, |
223 | - }) |
224 | - hosts_v6.extend(hosts) |
225 | + config = get_dhcp_configure_for( |
226 | + 6, rack_controller, vlan, subnets_v6, |
227 | + ntp_servers, default_domain, dhcp_snippets) |
228 | + failover_peer, subnets, hosts, interface = config |
229 | + if failover_peer is not None: |
230 | + failover_peers_v6.append(failover_peer) |
231 | + shared_networks_v6.append({ |
232 | + "name": "vlan-%d" % vlan.id, |
233 | + "subnets": subnets, |
234 | + }) |
235 | + hosts_v6.extend(hosts) |
236 | + if interface is not None: |
237 | interfaces_v6.add(interface) |
238 | + # When no interfaces exist for each IP version clear the shared networks |
239 | + # as DHCP server cannot be started and needs to be stopped. |
240 | + if len(interfaces_v4) == 0: |
241 | + shared_networks_v4 = {} |
242 | + if len(interfaces_v6) == 0: |
243 | + shared_networks_v6 = {} |
244 | return DHCPConfigurationForRack( |
245 | failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4, |
246 | failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6, |
247 | |
248 | === modified file 'src/maasserver/exceptions.py' |
249 | --- src/maasserver/exceptions.py 2016-03-28 13:54:47 +0000 |
250 | +++ src/maasserver/exceptions.py 2016-12-06 08:04:41 +0000 |
251 | @@ -199,7 +199,3 @@ |
252 | information. |
253 | """ |
254 | api_error = int(http.client.SERVICE_UNAVAILABLE) |
255 | - |
256 | - |
257 | -class DHCPConfigurationError(MAASException): |
258 | - """Raised when the configuration of DHCP hits a problem.""" |
259 | |
260 | === modified file 'src/maasserver/forms_vlan.py' |
261 | --- src/maasserver/forms_vlan.py 2016-04-27 20:38:06 +0000 |
262 | +++ src/maasserver/forms_vlan.py 2016-12-06 08:04:41 +0000 |
263 | @@ -31,6 +31,7 @@ |
264 | 'dhcp_on', |
265 | 'primary_rack', |
266 | 'secondary_rack', |
267 | + 'relay_vlan', |
268 | ) |
269 | |
270 | def __init__(self, *args, **kwargs): |
271 | @@ -40,6 +41,7 @@ |
272 | if instance is None and self.fabric is None: |
273 | raise ValueError("Form requires either a instance or a fabric.") |
274 | self._set_up_rack_fields() |
275 | + self._set_up_relay_vlan() |
276 | |
277 | def _set_up_rack_fields(self): |
278 | qs = RackController.objects.filter_by_vids([self.instance.vid]) |
279 | @@ -61,6 +63,22 @@ |
280 | secondary_rack = RackController.objects.get(id=secondary_rack_id) |
281 | self.initial['secondary_rack'] = secondary_rack.system_id |
282 | |
283 | + def _set_up_relay_vlan(self): |
284 | + # Configure the relay_vlan fields to include only VLAN's that are |
285 | + # not already on a relay_vlan. If this is an update then it cannot |
286 | + # be itself or never set when dhcp_on is True. |
287 | + possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True) |
288 | + if self.instance is not None: |
289 | + possible_relay_vlans = possible_relay_vlans.exclude( |
290 | + id=self.instance.id) |
291 | + if self.instance.dhcp_on: |
292 | + possible_relay_vlans = VLAN.objects.none() |
293 | + if self.instance.relay_vlan is not None: |
294 | + possible_relay_vlans = VLAN.objects.filter( |
295 | + id=self.instance.relay_vlan.id) |
296 | + self.fields['relay_vlan'] = forms.ModelChoiceField( |
297 | + queryset=possible_relay_vlans, required=False) |
298 | + |
299 | def clean(self): |
300 | cleaned_data = super(VLANForm, self).clean() |
301 | # Automatically promote the secondary rack controller to the primary |
302 | @@ -120,5 +138,12 @@ |
303 | interface = super(VLANForm, self).save(commit=False) |
304 | if self.fabric is not None: |
305 | interface.fabric = self.fabric |
306 | + if ('relay_vlan' in self.data and |
307 | + not self.cleaned_data.get('relay_vlan')): |
308 | + # relay_vlan is being cleared. |
309 | + interface.relay_vlan = None |
310 | + if interface.dhcp_on: |
311 | + # relay_vlan cannot be set when dhcp is on. |
312 | + interface.relay_vlan = None |
313 | interface.save() |
314 | return interface |
315 | |
316 | === modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py' |
317 | --- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-30 01:17:54 +0000 |
318 | +++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-12-06 08:04:41 +0000 |
319 | @@ -44,6 +44,6 @@ |
320 | migrations.AlterField( |
321 | model_name='subnet', |
322 | name='vlan', |
323 | - field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT), |
324 | + field=models.ForeignKey(to='maasserver.VLAN', default=None, on_delete=django.db.models.deletion.PROTECT), |
325 | ), |
326 | ] |
327 | |
328 | === added file 'src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py' |
329 | --- src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 1970-01-01 00:00:00 +0000 |
330 | +++ src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 2016-12-06 08:04:41 +0000 |
331 | @@ -0,0 +1,23 @@ |
332 | +# -*- coding: utf-8 -*- |
333 | +from __future__ import unicode_literals |
334 | + |
335 | +from django.db import ( |
336 | + migrations, |
337 | + models, |
338 | +) |
339 | +import django.db.models.deletion |
340 | + |
341 | + |
342 | +class Migration(migrations.Migration): |
343 | + |
344 | + dependencies = [ |
345 | + ('maasserver', '0094_add_unmanaged_subnets'), |
346 | + ] |
347 | + |
348 | + operations = [ |
349 | + migrations.AddField( |
350 | + model_name='vlan', |
351 | + name='relay_vlan', |
352 | + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name='relay_vlans', to='maasserver.VLAN'), |
353 | + ), |
354 | + ] |
355 | |
356 | === added file 'src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py' |
357 | --- src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 1970-01-01 00:00:00 +0000 |
358 | +++ src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 2016-12-06 08:04:41 +0000 |
359 | @@ -0,0 +1,24 @@ |
360 | +# -*- coding: utf-8 -*- |
361 | +from __future__ import unicode_literals |
362 | + |
363 | +from django.db import ( |
364 | + migrations, |
365 | + models, |
366 | +) |
367 | +import django.db.models.deletion |
368 | +import maasserver.models.subnet |
369 | + |
370 | + |
371 | +class Migration(migrations.Migration): |
372 | + |
373 | + dependencies = [ |
374 | + ('maasserver', '0095_vlan_relay_vlan'), |
375 | + ] |
376 | + |
377 | + operations = [ |
378 | + migrations.AlterField( |
379 | + model_name='subnet', |
380 | + name='vlan', |
381 | + field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT), |
382 | + ), |
383 | + ] |
384 | |
385 | === modified file 'src/maasserver/models/tests/test_vlan.py' |
386 | --- src/maasserver/models/tests/test_vlan.py 2016-10-19 18:06:01 +0000 |
387 | +++ src/maasserver/models/tests/test_vlan.py 2016-12-06 08:04:41 +0000 |
388 | @@ -88,6 +88,14 @@ |
389 | |
390 | class TestVLAN(MAASServerTestCase): |
391 | |
392 | + def test_delete_relay_vlan_doesnt_delete_vlan(self): |
393 | + relay_vlan = factory.make_VLAN() |
394 | + vlan = factory.make_VLAN(relay_vlan=relay_vlan) |
395 | + relay_vlan.delete() |
396 | + vlan = reload_object(vlan) |
397 | + self.assertIsNotNone(vlan) |
398 | + self.assertIsNone(vlan.relay_vlan) |
399 | + |
400 | def test_get_name_for_default_vlan_is_untagged(self): |
401 | fabric = factory.make_Fabric() |
402 | self.assertEqual("untagged", fabric.get_default_vlan().get_name()) |
403 | |
404 | === modified file 'src/maasserver/models/vlan.py' |
405 | --- src/maasserver/models/vlan.py 2016-10-20 19:39:48 +0000 |
406 | +++ src/maasserver/models/vlan.py 2016-12-06 08:04:41 +0000 |
407 | @@ -14,6 +14,7 @@ |
408 | from django.db.models import ( |
409 | BooleanField, |
410 | CharField, |
411 | + deletion, |
412 | ForeignKey, |
413 | IntegerField, |
414 | Manager, |
415 | @@ -169,6 +170,10 @@ |
416 | 'RackController', null=True, blank=True, editable=True, |
417 | related_name='+') |
418 | |
419 | + relay_vlan = ForeignKey( |
420 | + 'self', null=True, blank=True, editable=True, |
421 | + related_name='relay_vlans', on_delete=deletion.SET_NULL) |
422 | + |
423 | def __str__(self): |
424 | return "%s.%s" % (self.fabric.get_name(), self.get_name()) |
425 | |
426 | |
427 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js' |
428 | --- src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-10-19 18:06:01 +0000 |
429 | +++ src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-12-06 08:04:41 +0000 |
430 | @@ -427,6 +427,42 @@ |
431 | expect(controller.actionError).toBe(null); |
432 | }); |
433 | |
434 | + it("performAction for relay_dhcp called with all params", function() { |
435 | + var controller = makeControllerResolveSetActiveItem(); |
436 | + controller.actionOption = controller.RELAY_DHCP_ACTION; |
437 | + // This will populate the default values for the racks with |
438 | + // the current values from the mock objects. |
439 | + controller.actionOptionChanged(); |
440 | + controller.provideDHCPAction.subnet = 1; |
441 | + controller.provideDHCPAction.gatewayIP = "192.168.0.1"; |
442 | + controller.provideDHCPAction.startIP = "192.168.0.2"; |
443 | + controller.provideDHCPAction.endIP = "192.168.0.254"; |
444 | + var relay = { |
445 | + id: makeInteger(5001, 6000) |
446 | + }; |
447 | + VLANsManager._items = [relay]; |
448 | + controller.provideDHCPAction.relayVLAN = relay; |
449 | + var defer = $q.defer(); |
450 | + spyOn(VLANsManager, "configureDHCP").and.returnValue( |
451 | + defer.promise); |
452 | + controller.actionGo(); |
453 | + defer.resolve(); |
454 | + $scope.$digest(); |
455 | + expect(VLANsManager.configureDHCP).toHaveBeenCalledWith( |
456 | + controller.vlan, |
457 | + [], |
458 | + { |
459 | + subnet: 1, |
460 | + gateway: "192.168.0.1", |
461 | + start: "192.168.0.2", |
462 | + end: "192.168.0.254" |
463 | + }, |
464 | + relay.id |
465 | + ); |
466 | + expect(controller.actionOption).toBe(null); |
467 | + expect(controller.actionError).toBe(null); |
468 | + }); |
469 | + |
470 | it("performAction for disable_dhcp called with all params", function() { |
471 | var controller = makeControllerResolveSetActiveItem(); |
472 | controller.actionOption = controller.DISABLE_DHCP_ACTION; |
473 | @@ -457,6 +493,7 @@ |
474 | controller.actionOptionChanged(); |
475 | expect(controller.provideDHCPAction).toEqual({ |
476 | subnet: subnet.id, |
477 | + relayVLAN: null, |
478 | primaryRack: "p1", |
479 | secondaryRack: "p2", |
480 | maxIPs: 0, |
481 | @@ -488,6 +525,7 @@ |
482 | controller.actionOptionChanged(); |
483 | expect(controller.provideDHCPAction).toEqual({ |
484 | subnet: subnet.id, |
485 | + relayVLAN: null, |
486 | primaryRack: "p1", |
487 | secondaryRack: "p2", |
488 | maxIPs: 26, |
489 | @@ -551,30 +589,45 @@ |
490 | expect(controller.actionOptions).toEqual([]); |
491 | }); |
492 | |
493 | - it("returns enable_dhcp and delete when dhcp is off", |
494 | + it("returns enable_dhcp, relay_dhcp and delete when dhcp is off", |
495 | function() { |
496 | vlan.dhcp_on = false; |
497 | UsersManager._authUser = {is_superuser: true}; |
498 | var controller = makeControllerResolveSetActiveItem(); |
499 | expect(controller.actionOptions).toEqual([ |
500 | controller.PROVIDE_DHCP_ACTION, |
501 | + controller.RELAY_DHCP_ACTION, |
502 | controller.DELETE_ACTION |
503 | ]); |
504 | expect(controller.PROVIDE_DHCP_ACTION.title).toBe("Provide DHCP"); |
505 | }); |
506 | |
507 | - it("returns disable_dhcp, enable_dhcp (with new title) and delete "+ |
508 | + it("returns enable_dhcp (with new title), disable_dhcp and delete "+ |
509 | "when dhcp is on", function() { |
510 | vlan.dhcp_on = true; |
511 | UsersManager._authUser = {is_superuser: true}; |
512 | var controller = makeControllerResolveSetActiveItem(); |
513 | expect(controller.actionOptions).toEqual([ |
514 | + controller.PROVIDE_DHCP_ACTION, |
515 | controller.DISABLE_DHCP_ACTION, |
516 | - controller.PROVIDE_DHCP_ACTION, |
517 | controller.DELETE_ACTION |
518 | ]); |
519 | expect(controller.PROVIDE_DHCP_ACTION.title).toBe( |
520 | "Reconfigure DHCP"); |
521 | }); |
522 | + |
523 | + it("returns relay_dhcp (with new title), disable_dhcp and delete "+ |
524 | + "when relay_vlan is set", function() { |
525 | + vlan.relay_vlan = 5001; |
526 | + UsersManager._authUser = {is_superuser: true}; |
527 | + var controller = makeControllerResolveSetActiveItem(); |
528 | + expect(controller.actionOptions).toEqual([ |
529 | + controller.RELAY_DHCP_ACTION, |
530 | + controller.DISABLE_DHCP_ACTION, |
531 | + controller.DELETE_ACTION |
532 | + ]); |
533 | + expect(controller.RELAY_DHCP_ACTION.title).toBe( |
534 | + "Reconfigure DHCP relay"); |
535 | + }); |
536 | }); |
537 | }); |
538 | |
539 | === modified file 'src/maasserver/static/js/angular/controllers/vlan_details.js' |
540 | --- src/maasserver/static/js/angular/controllers/vlan_details.js 2016-10-19 18:06:01 +0000 |
541 | +++ src/maasserver/static/js/angular/controllers/vlan_details.js 2016-12-06 08:04:41 +0000 |
542 | @@ -4,6 +4,18 @@ |
543 | * MAAS VLAN Details Controller |
544 | */ |
545 | |
546 | +angular.module('MAAS').filter('ignoreSelf', function () { |
547 | + return function(objects, self) { |
548 | + var filtered = []; |
549 | + angular.forEach(objects, function(obj) { |
550 | + if(obj !== self) { |
551 | + filtered.push(obj); |
552 | + } |
553 | + }); |
554 | + return filtered; |
555 | + }; |
556 | +}); |
557 | + |
558 | angular.module('MAAS').controller('VLANDetailsController', [ |
559 | '$scope', '$rootScope', '$routeParams', '$filter', '$location', |
560 | 'VLANsManager', 'SubnetsManager', 'SpacesManager', 'FabricsManager', |
561 | @@ -27,10 +39,15 @@ |
562 | $rootScope.page = "networks"; |
563 | |
564 | vm.PROVIDE_DHCP_ACTION = { |
565 | - // Note: 'title' is setubndynamically depending on whether or not |
566 | + // Note: 'title' is set dynamically depending on whether or not |
567 | // DHCP is already enabled on this VLAN. |
568 | name: "enable_dhcp" |
569 | }; |
570 | + vm.RELAY_DHCP_ACTION = { |
571 | + // Note: 'title' is set ndynamically depending on whether or not |
572 | + // DHCP relay is already enabled on this VLAN. |
573 | + name: "relay_dhcp" |
574 | + }; |
575 | vm.DISABLE_DHCP_ACTION = { |
576 | name: "disable_dhcp", |
577 | title: "Disable DHCP" |
578 | @@ -47,6 +64,7 @@ |
579 | vm.actionOption = null; |
580 | vm.actionOptions = []; |
581 | vm.vlanManager = VLANsManager; |
582 | + vm.vlans = VLANsManager.getItems(); |
583 | vm.subnets = SubnetsManager.getItems(); |
584 | vm.spaces = SpacesManager.getItems(); |
585 | vm.fabrics = FabricsManager.getItems(); |
586 | @@ -78,10 +96,15 @@ |
587 | // Initialize the provideDHCPAction structure with the current primary |
588 | // and secondary rack, plus an indication regarding whether or not |
589 | // adding a dynamic IP range is required. |
590 | - vm.initProvideDHCP = function() { |
591 | + vm.initProvideDHCP = function(forRelay) { |
592 | vm.provideDHCPAction = {}; |
593 | var dhcp = vm.provideDHCPAction; |
594 | dhcp.subnet = null; |
595 | + dhcp.relayVLAN = null; |
596 | + if (angular.isNumber(vm.vlan.relay_vlan)) { |
597 | + dhcp.relayVLAN = VLANsManager.getItemFromList( |
598 | + vm.vlan.relay_vlan); |
599 | + } |
600 | if (angular.isObject(vm.primaryRack)) { |
601 | dhcp.primaryRack = vm.primaryRack.system_id; |
602 | } else if(vm.relatedControllers.length > 0) { |
603 | @@ -140,15 +163,19 @@ |
604 | } |
605 | // Since we are setting default values for these three options, |
606 | // ensure all the appropriate updates occur. |
607 | - vm.updatePrimaryRack(); |
608 | - vm.updateSecondaryRack(); |
609 | - vm.updateSubnet(); |
610 | + if(!forRelay) { |
611 | + vm.updatePrimaryRack(); |
612 | + vm.updateSecondaryRack(); |
613 | + } |
614 | + vm.updateSubnet(forRelay); |
615 | }; |
616 | |
617 | // Called when the actionOption has changed. |
618 | vm.actionOptionChanged = function() { |
619 | if(vm.actionOption.name === "enable_dhcp") { |
620 | - vm.initProvideDHCP(); |
621 | + vm.initProvideDHCP(false); |
622 | + } else if(vm.actionOption.name === "relay_dhcp") { |
623 | + vm.initProvideDHCP(true); |
624 | } |
625 | // Clear the action error. |
626 | vm.actionError = null; |
627 | @@ -200,7 +227,7 @@ |
628 | }; |
629 | |
630 | // Called from the Provide DHCP form when the subnet selection changes. |
631 | - vm.updateSubnet = function() { |
632 | + vm.updateSubnet = function(forRelay) { |
633 | var dhcp = vm.provideDHCPAction; |
634 | var subnet = SubnetsManager.getItemFromList(dhcp.subnet); |
635 | if(angular.isObject(subnet)) { |
636 | @@ -212,10 +239,17 @@ |
637 | } |
638 | if(angular.isObject(iprange) && iprange.num_addresses > 0) { |
639 | dhcp.maxIPs = iprange.num_addresses; |
640 | - dhcp.startIP = iprange.start; |
641 | - dhcp.endIP = iprange.end; |
642 | - dhcp.startPlaceholder = iprange.start; |
643 | - dhcp.endPlaceholder = iprange.end; |
644 | + if(forRelay) { |
645 | + dhcp.startIP = ""; |
646 | + dhcp.endIP = ""; |
647 | + dhcp.startPlaceholder = iprange.start + "( optional)"; |
648 | + dhcp.endPlaceholder = iprange.end + " (optional)"; |
649 | + } else { |
650 | + dhcp.startIP = iprange.start; |
651 | + dhcp.endIP = iprange.end; |
652 | + dhcp.startPlaceholder = iprange.start; |
653 | + dhcp.endPlaceholder = iprange.end; |
654 | + } |
655 | } else { |
656 | // Need to add a dynamic range, but according to our data, |
657 | // there is no room on the subnet for a dynamic range. |
658 | @@ -226,8 +260,14 @@ |
659 | dhcp.endPlaceholder = "(no available IPs)"; |
660 | } |
661 | if(angular.isString(suggested_gateway)) { |
662 | - dhcp.gatewayIP = suggested_gateway; |
663 | - dhcp.gatewayPlaceholder = suggested_gateway; |
664 | + if(forRelay) { |
665 | + dhcp.gatewayIP = ""; |
666 | + dhcp.gatewayPlaceholder = ( |
667 | + suggested_gateway + " (optional)"); |
668 | + } else { |
669 | + dhcp.gatewayIP = suggested_gateway; |
670 | + dhcp.gatewayPlaceholder = suggested_gateway; |
671 | + } |
672 | } else { |
673 | // This means the subnet already has a gateway, so don't |
674 | // bother populating it. |
675 | @@ -261,8 +301,24 @@ |
676 | vm.actionError = null; |
677 | }; |
678 | |
679 | + // Return True if the current action can be performed. |
680 | + vm.canPerformAction = function() { |
681 | + if(vm.actionOption.name === "enable_dhcp") { |
682 | + return vm.relatedSubnets.length > 0; |
683 | + } else if(vm.actionOption.name === "relay_dhcp") { |
684 | + return angular.isObject(vm.provideDHCPAction.relayVLAN); |
685 | + } else { |
686 | + return true; |
687 | + } |
688 | + }; |
689 | + |
690 | // Perform the action. |
691 | vm.actionGo = function() { |
692 | + // Do nothing if action cannot be performed. |
693 | + if(!vm.canPerformAction()) { |
694 | + return; |
695 | + } |
696 | + |
697 | if(vm.actionOption.name === "enable_dhcp") { |
698 | var dhcp = vm.provideDHCPAction; |
699 | var controllers = []; |
700 | @@ -294,6 +350,23 @@ |
701 | vm.actionError = result.error; |
702 | vm.actionOption = vm.PROVIDE_DHCP_ACTION; |
703 | }); |
704 | + } else if(vm.actionOption.name === "relay_dhcp") { |
705 | + // These will be undefined if they don't exist, and the region |
706 | + // will simply get an empty dictionary. |
707 | + var extraDHCP = {}; |
708 | + extraDHCP.subnet = vm.provideDHCPAction.subnet; |
709 | + extraDHCP.start = vm.provideDHCPAction.startIP; |
710 | + extraDHCP.end = vm.provideDHCPAction.endIP; |
711 | + extraDHCP.gateway = vm.provideDHCPAction.gatewayIP; |
712 | + var relay = vm.provideDHCPAction.relayVLAN.id; |
713 | + VLANsManager.configureDHCP( |
714 | + vm.vlan, [], extraDHCP, relay).then(function() { |
715 | + vm.actionOption = null; |
716 | + vm.actionError = null; |
717 | + }, function(result) { |
718 | + vm.actionError = result.error; |
719 | + vm.actionOption = vm.RELAY_DHCP_ACTION; |
720 | + }); |
721 | } else if(vm.actionOption.name === "disable_dhcp") { |
722 | VLANsManager.disableDHCP(vm.vlan).then(function() { |
723 | vm.actionOption = null; |
724 | @@ -319,6 +392,30 @@ |
725 | return vm.actionError !== null; |
726 | }; |
727 | |
728 | + // Return the name of the VLAN. |
729 | + vm.getFullVLANName = function(vlan_id) { |
730 | + var vlan = VLANsManager.getItemFromList(vlan_id); |
731 | + var fabric = FabricsManager.getItemFromList(vlan.fabric); |
732 | + return ( |
733 | + FabricsManager.getName(fabric) + "." + |
734 | + VLANsManager.getName(vlan)); |
735 | + }; |
736 | + |
737 | + // Return the current DHCP status. |
738 | + vm.getDHCPStatus = function() { |
739 | + if(vm.vlan) { |
740 | + if(vm.vlan.dhcp_on) { |
741 | + return "Enabled"; |
742 | + } else if(vm.vlan.relay_vlan) { |
743 | + return "Relayed via " + vm.getFullVLANName(vm.vlan.relay_vlan); |
744 | + } else { |
745 | + return "Disabled"; |
746 | + } |
747 | + } else { |
748 | + return ""; |
749 | + } |
750 | + }; |
751 | + |
752 | // Updates the page title. |
753 | function updateTitle() { |
754 | var vlan = vm.vlan; |
755 | @@ -413,13 +510,22 @@ |
756 | // object, since it's watched from $scope.) |
757 | vm.actionOptions.length = 0; |
758 | if(UsersManager.isSuperUser()) { |
759 | - if(vlan.dhcp_on === true) { |
760 | - vm.actionOptions.push(vm.DISABLE_DHCP_ACTION); |
761 | - vm.PROVIDE_DHCP_ACTION.title = "Reconfigure DHCP"; |
762 | + if(!vlan.relay_vlan) { |
763 | + if(vlan.dhcp_on === true) { |
764 | + vm.PROVIDE_DHCP_ACTION.title = "Reconfigure DHCP"; |
765 | + vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION); |
766 | + vm.actionOptions.push(vm.DISABLE_DHCP_ACTION); |
767 | + } else { |
768 | + vm.PROVIDE_DHCP_ACTION.title = "Provide DHCP"; |
769 | + vm.RELAY_DHCP_ACTION.title = "Relay DHCP"; |
770 | + vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION); |
771 | + vm.actionOptions.push(vm.RELAY_DHCP_ACTION); |
772 | + } |
773 | } else { |
774 | - vm.PROVIDE_DHCP_ACTION.title = "Provide DHCP"; |
775 | + vm.actionOptions.push(vm.RELAY_DHCP_ACTION); |
776 | + vm.actionOptions.push(vm.DISABLE_DHCP_ACTION); |
777 | + vm.RELAY_DHCP_ACTION.title = "Reconfigure DHCP relay"; |
778 | } |
779 | - vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION); |
780 | if(!vm.isFabricDefault) { |
781 | vm.actionOptions.push(vm.DELETE_ACTION); |
782 | } |
783 | @@ -467,6 +573,8 @@ |
784 | $scope.$watch("vlanDetails.vlan.name", updateTitle); |
785 | $scope.$watch("vlanDetails.vlan.vid", updateTitle); |
786 | $scope.$watch("vlanDetails.vlan.dhcp_on", updatePossibleActions); |
787 | + $scope.$watch( |
788 | + "vlanDetails.vlan.relay_vlan", updatePossibleActions); |
789 | $scope.$watch("vlanDetails.fabric.name", updateTitle); |
790 | $scope.$watch( |
791 | "vlanDetails.vlan.primary_rack", updateManagementRacks); |
792 | |
793 | === modified file 'src/maasserver/static/js/angular/factories/tests/test_vlans.js' |
794 | --- src/maasserver/static/js/angular/factories/tests/test_vlans.js 2016-05-11 19:01:48 +0000 |
795 | +++ src/maasserver/static/js/angular/factories/tests/test_vlans.js 2016-12-06 08:04:41 +0000 |
796 | @@ -38,14 +38,16 @@ |
797 | var result = {}; |
798 | var controllers = ["a", "b"]; |
799 | var extra = {"c": "d"}; |
800 | + var relay = makeInteger(1, 500); |
801 | spyOn(RegionConnection, "callMethod").and.returnValue(result); |
802 | expect(VLANsManager.configureDHCP( |
803 | - obj, controllers, extra)).toBe(result); |
804 | + obj, controllers, extra, relay)).toBe(result); |
805 | expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
806 | "vlan.configure_dhcp", { |
807 | id: obj.id, |
808 | controllers: controllers, |
809 | - extra: extra |
810 | + extra: extra, |
811 | + relay_vlan: relay |
812 | }, true); |
813 | }); |
814 | }); |
815 | @@ -60,7 +62,8 @@ |
816 | expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
817 | "vlan.configure_dhcp", { |
818 | id: obj.id, |
819 | - controllers: [] |
820 | + controllers: [], |
821 | + relay_vlan: null |
822 | }, true); |
823 | }); |
824 | }); |
825 | |
826 | === modified file 'src/maasserver/static/js/angular/factories/vlans.js' |
827 | --- src/maasserver/static/js/angular/factories/vlans.js 2016-05-11 19:01:48 +0000 |
828 | +++ src/maasserver/static/js/angular/factories/vlans.js 2016-12-06 08:04:41 +0000 |
829 | @@ -53,13 +53,17 @@ |
830 | |
831 | // Configure DHCP on the VLAN |
832 | VLANsManager.prototype.configureDHCP = function( |
833 | - vlan, controllers, extra) { |
834 | + vlan, controllers, extra, relay_vlan) { |
835 | + var params = { |
836 | + "id": vlan.id, |
837 | + "controllers": controllers, |
838 | + "extra": extra |
839 | + }; |
840 | + if(relay_vlan === null || angular.isNumber(relay_vlan)) { |
841 | + params.relay_vlan = relay_vlan; |
842 | + } |
843 | return RegionConnection.callMethod( |
844 | - "vlan.configure_dhcp", { |
845 | - "id": vlan.id, |
846 | - "controllers": controllers, |
847 | - "extra": extra |
848 | - }, true); |
849 | + "vlan.configure_dhcp", params, true); |
850 | }; |
851 | |
852 | // Disable DHCP on the VLAN |
853 | @@ -67,7 +71,8 @@ |
854 | return RegionConnection.callMethod( |
855 | "vlan.configure_dhcp", { |
856 | "id": vlan.id, |
857 | - "controllers": [] |
858 | + "controllers": [], |
859 | + "relay_vlan": null |
860 | }, true); |
861 | }; |
862 | |
863 | |
864 | === modified file 'src/maasserver/static/partials/vlan-details.html' |
865 | --- src/maasserver/static/partials/vlan-details.html 2016-11-17 11:46:08 +0000 |
866 | +++ src/maasserver/static/partials/vlan-details.html 2016-12-06 08:04:41 +0000 |
867 | @@ -1,27 +1,27 @@ |
868 | -<div data-ng-hide="vlanDetails.loaded"> |
869 | +<div data-ng-if="!vlanDetails.loaded"> |
870 | <header class="page-header" sticky> |
871 | <div class="wrapper--inner"> |
872 | <h1 class="page-header__title">Loading...</h1> |
873 | </div> |
874 | </header> |
875 | </div> |
876 | -<div class="ng-hide" data-ng-show="vlanDetails.loaded"> |
877 | +<div data-ng-if="vlanDetails.loaded"> |
878 | <header class="page-header" sticky> |
879 | <div class="wrapper--inner"> |
880 | <h1 class="page-header__title">{$ vlanDetails.title $}</h1> |
881 | <!-- "Take action" dropdown --> |
882 | - <div class="page-header__controls" data-ng-show="vlanDetails.actionOptions.length"> |
883 | + <div class="page-header__controls" data-ng-if="vlanDetails.actionOptions.length"> |
884 | <div data-maas-cta="vlanDetails.actionOptions" |
885 | data-ng-model="vlanDetails.actionOption" |
886 | data-ng-change="vlanDetails.actionOptionChanged()"> |
887 | </div> |
888 | </div> |
889 | - <div class="page-header__dropdown" data-ng-class="{ 'is-open': vlanDetails.actionOption }"> |
890 | - <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'"> |
891 | - <h3 class="page-header__dropdown-title" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'">Provide DHCP</h3> |
892 | + <div class="page-header__dropdown" data-ng-class="{ 'is-open': vlanDetails.actionOption }" data-ng-if="vlanDetails.actionOption"> |
893 | + <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp'"> |
894 | + <h3 class="page-header__dropdown-title">Provide DHCP</h3> |
895 | <form class="form form--stack"> |
896 | <!-- This is just for visual reasons, since we need an additional border to begin the form if there is no error. --> |
897 | - <fieldset class="form__fieldset six-col" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'"> |
898 | + <fieldset class="form__fieldset six-col"> |
899 | <div class="form__group" data-ng-hide="vlanDetails.relatedSubnets.length === 0"> |
900 | <label for="primary-rack" class="form__group-label two-col">{$ vlanDetails.relatedControllers.length <= 1 ? "Rack controller" : "Primary controller" $}</label> |
901 | <div class="form__group-input three-col"> |
902 | @@ -32,7 +32,7 @@ |
903 | <option value="" |
904 | disabled="disabled" |
905 | selected="selected" |
906 | - data-ng-show="vlanDetails.provideDHCPAction.primaryRack === ''">Choose primary controller</option> |
907 | + data-ng-if="vlanDetails.provideDHCPAction.primaryRack === ''">Choose primary controller</option> |
908 | </select> |
909 | </div> |
910 | </div> |
911 | @@ -40,14 +40,14 @@ |
912 | <label for="secondary-rack" class="form__group-label two-col">Secondary controller</label> |
913 | <div class="form__group-input three-col"> |
914 | <select name="secondary-rack" |
915 | - data-ng-show="vlanDetails.relatedControllers.length > 1" |
916 | + data-ng-if="vlanDetails.relatedControllers.length > 1" |
917 | data-ng-disabled="!vlanDetails.provideDHCPAction.primaryRack && vlanDetails.relatedControllers.length > 1" |
918 | data-ng-model="vlanDetails.provideDHCPAction.secondaryRack" |
919 | data-ng-change="vlanDetails.updateSecondaryRack()" |
920 | data-ng-options="rack.system_id as rack.hostname for rack in vlanDetails.relatedControllers | filter:vlanDetails.filterPrimaryRack"> |
921 | <option value="" |
922 | selected="selected" |
923 | - data-ng-show="vlanDetails.relatedControllers.length >= 2"></option> |
924 | + data-ng-if="vlanDetails.relatedControllers.length >= 2"></option> |
925 | </select> |
926 | </div> |
927 | </div> |
928 | @@ -57,7 +57,7 @@ |
929 | <div class="form__group-input three-col"> |
930 | <select name="subnet" |
931 | data-ng-model="vlanDetails.provideDHCPAction.subnet" |
932 | - data-ng-change="vlanDetails.updateSubnet()" |
933 | + data-ng-change="vlanDetails.updateSubnet(false)" |
934 | data-ng-options="row.subnet.id as row.subnet.cidr for row in vlanDetails.relatedSubnets"> |
935 | <option value="" disabled="disabled" selected="selected">Choose subnet</option> |
936 | <option value="" data-ng-if=""></option> |
937 | @@ -65,7 +65,7 @@ |
938 | </div> |
939 | </div> |
940 | </fieldset> |
941 | - <fieldset class="form__fieldset six-col last-col" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'"> |
942 | + <fieldset class="form__fieldset six-col last-col" data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp'"> |
943 | <div class="form__group" |
944 | data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0"> |
945 | <label for="start-ip" class="form__group-label two-col">Dynamic range start IP</label> |
946 | @@ -114,26 +114,101 @@ |
947 | </fieldset> |
948 | </form> |
949 | </section> |
950 | - <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="vlanDetails.isActionError()"> |
951 | + <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="vlanDetails.actionOption.name === 'relay_dhcp'"> |
952 | + <h3 class="page-header__dropdown-title">Relay DHCP</h3> |
953 | + <form class="form form--stack"> |
954 | + <!-- This is just for visual reasons, since we need an additional border to begin the form if there is no error. --> |
955 | + <fieldset class="form__fieldset six-col"> |
956 | + <div class="form__group"> |
957 | + <label for="relay_vlan" class="form__group-label two-col">Relay VLAN</label> |
958 | + <div class="form__group-input three-col"> |
959 | + <select name="relay_vlan" |
960 | + data-ng-model="vlanDetails.provideDHCPAction.relayVLAN" |
961 | + data-ng-options="vlan as vlanDetails.getFullVLANName(vlan.id) for vlan in vlanDetails.vlans | ignoreSelf:vlanDetails.vlan"> |
962 | + <option value="" disabled="disabled" selected="selected">Choose relay VLAN</option> |
963 | + </select> |
964 | + </div> |
965 | + </div> |
966 | + <div class="form__group" |
967 | + data-ng-hide="vlanDetails.relatedSubnets.length === 0 || (vlanDetails.provideDHCPAction.needsDynamicRange === false && vlanDetails.provideDHCPAction.needsGatewayIP === false)"> |
968 | + <label for="subnet" class="form__group-label two-col">Subnet</label> |
969 | + <div class="form__group-input three-col"> |
970 | + <select name="subnet" |
971 | + data-ng-model="vlanDetails.provideDHCPAction.subnet" |
972 | + data-ng-change="vlanDetails.updateSubnet(true)" |
973 | + data-ng-options="row.subnet.id as row.subnet.cidr for row in vlanDetails.relatedSubnets"> |
974 | + <option value="" disabled="disabled" selected="selected">Choose subnet</option> |
975 | + <option value="" data-ng-if=""></option> |
976 | + </select> |
977 | + </div> |
978 | + </div> |
979 | + </fieldset> |
980 | + <fieldset class="form__fieldset six-col last-col"> |
981 | + <div class="form__group" |
982 | + data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0"> |
983 | + <label for="start-ip" class="form__group-label two-col">Dynamic range start IP</label> |
984 | + <div class="form__group-input three-col"> |
985 | + <input type="text" |
986 | + name="start-ip" |
987 | + size="39" |
988 | + data-ng-placeholder="vlanDetails.provideDHCPAction.startPlaceholder" |
989 | + data-ng-model="vlanDetails.provideDHCPAction.startIP" |
990 | + data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
991 | + data-ng-change="vlanDetails.updateStartIP()"> |
992 | + </div> |
993 | + </div> |
994 | + <div class="form__group" data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0"> |
995 | + <label for="end-ip" class="form__group-label two-col">Dynamic range end IP</label> |
996 | + <div class="form__group-input three-col"> |
997 | + <input type="text" |
998 | + name="end-ip" |
999 | + size="39" |
1000 | + data-ng-placeholder="vlanDetails.provideDHCPAction.endPlaceholder" |
1001 | + data-ng-model="vlanDetails.provideDHCPAction.endIP" |
1002 | + data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
1003 | + data-ng-change="vlanDetails.updateEndIP()"> |
1004 | + </div> |
1005 | + </div> |
1006 | + <div class="form__group" |
1007 | + data-ng-hide="vlanDetails.provideDHCPAction.needsGatewayIP === false || vlanDetails.provideDHCPAction.subnetMissingGatewayIP === false || vlanDetails.relatedSubnets.length === 0"> |
1008 | + <label for="gateway-ip" class="form__group-label two-col">Gateway IP</label> |
1009 | + <div class="form__group-input three-col"> |
1010 | + <input type="text" |
1011 | + name="gateway-ip" |
1012 | + size="39" |
1013 | + data-ng-placeholder="vlanDetails.provideDHCPAction.gatewayPlaceholder" |
1014 | + data-ng-model="vlanDetails.provideDHCPAction.gatewayIP" |
1015 | + data-ng-disabled="!vlanDetails.provideDHCPAction.subnet" |
1016 | + data-ng-change="vlanDetails.updatendIP()"> |
1017 | + </div> |
1018 | + </div> |
1019 | + </fieldset> |
1020 | + </form> |
1021 | + </section> |
1022 | + <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="!vlanDetails.isActionError()"> |
1023 | <p class="page-header__message page-header__message--warning" |
1024 | - data-ng-show="vlanDetails.actionOption.name === 'disable_dhcp'"> |
1025 | + data-ng-if="vlanDetails.actionOption.name === 'disable_dhcp' && vlanDetails.vlan.dhcp_on"> |
1026 | Are you sure you want to disable DHCP on this VLAN? All subnets on this VLAN will be affected. |
1027 | </p> |
1028 | + <p class="page-header__message page-header__message--warning" |
1029 | + data-ng-if="vlanDetails.actionOption.name === 'disable_dhcp' && vlanDetails.vlan.relay_vlan"> |
1030 | + Are you sure you want to disable DHCP relay on this VLAN? All subnets on this VLAN will be affected. |
1031 | + </p> |
1032 | <p class="page-header__message page-header__message--error" |
1033 | - data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp' && vlanDetails.relatedSubnets.length === 0"> |
1034 | + data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp' && vlanDetails.relatedSubnets.length === 0"> |
1035 | No subnets are available on this VLAN. DHCP cannot be enabled. |
1036 | </p> |
1037 | <p class="page-header__message page-header__message--warning" |
1038 | - data-ng-show="vlanDetails.actionOption.name === 'delete'"> |
1039 | + data-ng-if="vlanDetails.actionOption.name === 'delete'"> |
1040 | Are you sure you want to delete this VLAN? |
1041 | </p> |
1042 | <div class="page-header__controls"> |
1043 | <a href="" class="button--base button--inline" data-ng-click="vlanDetails.actionCancel()">Cancel</a> |
1044 | - <button class="button--primary button--inline" data-ng-click="vlanDetails.actionGo()" data-ng-disabled="vlanDetails.actionOption.name === 'enable_dhcp' && vlanDetails.relatedSubnets.length === 0">{$ vlanDetails.actionOption.title $}</button> |
1045 | + <button class="button--primary button--inline" data-ng-click="vlanDetails.actionGo()" data-ng-disabled="!vlanDetails.canPerformAction()">{$ vlanDetails.actionOption.title $}</button> |
1046 | </div> |
1047 | </section> |
1048 | - <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" |
1049 | - data-ng-show="vlanDetails.isActionError()"> |
1050 | + <section class="page-header__section twelve-col u-margin--bottom-none" |
1051 | + data-ng-if="vlanDetails.isActionError()"> |
1052 | <p class="page-header__message page-header__message--error">{$ vlanDetails.actionError $}</p> |
1053 | <div class="page-header__controls"> |
1054 | <a href="" class="button--base button--inline u-margin--right" data-ng-click="vlanDetails.actionCancel()">Cancel</a> |
1055 | @@ -166,17 +241,17 @@ |
1056 | <dd class="four-col last-col"> |
1057 | <a href="#/fabric/{$ vlanDetails.fabric.id $}">{$ vlanDetails.fabric.name $}</a> |
1058 | </dd> |
1059 | - <dt class="two-col">DHCP</dt><dd class="four-col last-col">{$ vlanDetails.vlan.dhcp_on ? "Enabled" : "Disabled" $}</dd> |
1060 | + <dt class="two-col">DHCP</dt><dd class="four-col last-col">{$ vlanDetails.getDHCPStatus() $}</dd> |
1061 | <div data-ng-if="vlanDetails.vlan.external_dhcp"> |
1062 | <dt class="two-col">External DHCP</dt> |
1063 | <dd class="four-col last-col">{$ vlanDetails.vlan.external_dhcp $} |
1064 | <i class="icon icon--warning tooltip" aria-label="Another DHCP server has been discovered on this VLAN. Enabling DHCP is not recommended."></i> |
1065 | </dd> |
1066 | </div> |
1067 | - <div class="ng-hide" data-ng-show="vlanDetails.primaryRack"> |
1068 | + <div data-ng-if="vlanDetails.primaryRack"> |
1069 | <dt class="two-col">Primary controller <span class="icon icon--help tooltip" aria-label="The rack controller where DHCP service runs on."></span></dt><dd class="four-col last-col">{$ vlanDetails.primaryRack.hostname $}</dd> |
1070 | </div> |
1071 | - <div class="ng-hide" data-ng-show="vlanDetails.secondaryRack"> |
1072 | + <div data-ng-if="vlanDetails.secondaryRack"> |
1073 | <dt class="two-col">Secondary controller <span class="icon icon--help tooltip" aria-label="The rack controller that will take over DHCP services if the primary fails."></span></dt><dd class="four-col last-col">{$ vlanDetails.secondaryRack.hostname $}</dd> |
1074 | </div> |
1075 | <dt class="two-col">Rack controllers <span class="icon icon--help tooltip" aria-label="A rack controller controls hosts and images and runs network services
like DHCP for connected VLANs."></span></dt> |
1076 | |
1077 | === modified file 'src/maasserver/testing/factory.py' |
1078 | --- src/maasserver/testing/factory.py 2016-11-29 08:19:43 +0000 |
1079 | +++ src/maasserver/testing/factory.py 2016-12-06 08:04:41 +0000 |
1080 | @@ -1022,7 +1022,7 @@ |
1081 | |
1082 | def make_VLAN( |
1083 | self, name=None, vid=None, fabric=None, dhcp_on=False, |
1084 | - primary_rack=None, secondary_rack=None): |
1085 | + primary_rack=None, secondary_rack=None, relay_vlan=None): |
1086 | assert vid != 0, "VID=0 VLANs are auto-created" |
1087 | if fabric is None: |
1088 | fabric = Fabric.objects.get_default_fabric() |
1089 | @@ -1031,7 +1031,8 @@ |
1090 | vid = self._get_available_vid(fabric) |
1091 | vlan = VLAN( |
1092 | name=name, vid=vid, fabric=fabric, dhcp_on=dhcp_on, |
1093 | - primary_rack=primary_rack, secondary_rack=secondary_rack) |
1094 | + primary_rack=primary_rack, secondary_rack=secondary_rack, |
1095 | + relay_vlan=relay_vlan) |
1096 | vlan.save() |
1097 | return vlan |
1098 | |
1099 | |
1100 | === modified file 'src/maasserver/tests/test_dhcp.py' |
1101 | --- src/maasserver/tests/test_dhcp.py 2016-12-03 16:33:44 +0000 |
1102 | +++ src/maasserver/tests/test_dhcp.py 2016-12-06 08:04:41 +0000 |
1103 | @@ -17,7 +17,6 @@ |
1104 | IPADDRESS_TYPE, |
1105 | SERVICE_STATUS, |
1106 | ) |
1107 | -from maasserver.exceptions import DHCPConfigurationError |
1108 | from maasserver.models import ( |
1109 | Config, |
1110 | DHCPSnippet, |
1111 | @@ -45,7 +44,6 @@ |
1112 | from maastesting.twisted import ( |
1113 | always_fail_with, |
1114 | always_succeed_with, |
1115 | - TwistedLoggerFixture, |
1116 | ) |
1117 | from netaddr import ( |
1118 | IPAddress, |
1119 | @@ -422,8 +420,8 @@ |
1120 | rack_controller, vlan, subnet.get_ipnetwork().version)) |
1121 | |
1122 | |
1123 | -class TestGetManagedVLANsFor(MAASServerTestCase): |
1124 | - """Tests for `get_managed_vlans_for`.""" |
1125 | +class TestGenManagedVLANsFor(MAASServerTestCase): |
1126 | + """Tests for `gen_managed_vlans_for`.""" |
1127 | |
1128 | def test__returns_all_managed_vlans(self): |
1129 | rack_controller = factory.make_RackController() |
1130 | @@ -509,7 +507,31 @@ |
1131 | self.assertEquals({ |
1132 | vlan_one, |
1133 | vlan_two, |
1134 | - }, dhcp.get_managed_vlans_for(rack_controller)) |
1135 | + }, set(dhcp.gen_managed_vlans_for(rack_controller))) |
1136 | + |
1137 | + def test__returns_managed_vlan_with_relay_vlans(self): |
1138 | + rack_controller = factory.make_RackController() |
1139 | + vlan_one = factory.make_VLAN( |
1140 | + dhcp_on=True, primary_rack=rack_controller, name="1") |
1141 | + primary_interface = factory.make_Interface( |
1142 | + INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan_one) |
1143 | + managed_ipv4_subnet = factory.make_Subnet( |
1144 | + cidr=str(factory.make_ipv4_network().cidr), vlan=vlan_one) |
1145 | + factory.make_StaticIPAddress( |
1146 | + alloc_type=IPADDRESS_TYPE.STICKY, subnet=managed_ipv4_subnet, |
1147 | + interface=primary_interface) |
1148 | + |
1149 | + # Relay VLANs atteched to the vlan. |
1150 | + relay_vlans = { |
1151 | + factory.make_VLAN(relay_vlan=vlan_one) |
1152 | + for _ in range(3) |
1153 | + } |
1154 | + |
1155 | + # Should only contain the subnets that are managed by the rack |
1156 | + # controller and the best interface should have been selected. |
1157 | + self.assertEquals( |
1158 | + relay_vlans.union(set([vlan_one])), |
1159 | + set(dhcp.gen_managed_vlans_for(rack_controller))) |
1160 | |
1161 | |
1162 | class TestIPIsOnVLAN(MAASServerTestCase): |
1163 | @@ -1300,31 +1322,6 @@ |
1164 | class TestGetDHCPConfigureFor(MAASServerTestCase): |
1165 | """Tests for `get_dhcp_configure_for`.""" |
1166 | |
1167 | - def test__raises_DHCPConfigurationError_for_ipv4(self): |
1168 | - primary_rack = factory.make_RackController() |
1169 | - secondary_rack = factory.make_RackController() |
1170 | - |
1171 | - # VLAN for primary that has a secondary with multiple subnets. |
1172 | - ha_vlan = factory.make_VLAN( |
1173 | - dhcp_on=True, primary_rack=primary_rack, |
1174 | - secondary_rack=secondary_rack) |
1175 | - ha_subnet = factory.make_ipv4_Subnet_with_IPRanges(vlan=ha_vlan) |
1176 | - factory.make_Interface( |
1177 | - INTERFACE_TYPE.PHYSICAL, node=primary_rack, vlan=ha_vlan) |
1178 | - secondary_interface = factory.make_Interface( |
1179 | - INTERFACE_TYPE.PHYSICAL, node=secondary_rack, vlan=ha_vlan) |
1180 | - factory.make_StaticIPAddress( |
1181 | - alloc_type=IPADDRESS_TYPE.AUTO, subnet=ha_subnet, |
1182 | - interface=secondary_interface) |
1183 | - other_subnet = factory.make_ipv4_Subnet_with_IPRanges(vlan=ha_vlan) |
1184 | - |
1185 | - ntp_servers = [factory.make_name("ntp")] |
1186 | - default_domain = Domain.objects.get_default_domain() |
1187 | - self.assertRaises( |
1188 | - DHCPConfigurationError, dhcp.get_dhcp_configure_for, |
1189 | - 4, primary_rack, ha_vlan, [ha_subnet, other_subnet], |
1190 | - ntp_servers, default_domain) |
1191 | - |
1192 | def test__returns_for_ipv4(self): |
1193 | primary_rack = factory.make_RackController() |
1194 | secondary_rack = factory.make_RackController() |
1195 | @@ -1423,36 +1420,6 @@ |
1196 | dhcp.make_hosts_for_subnets([ha_subnet]), observed_hosts) |
1197 | self.assertEqual(primary_interface.name, observed_interface) |
1198 | |
1199 | - def test__raises_DHCPConfigurationError_for_ipv6(self): |
1200 | - primary_rack = factory.make_RackController() |
1201 | - secondary_rack = factory.make_RackController() |
1202 | - |
1203 | - # VLAN for primary that has a secondary with multiple subnets. |
1204 | - ha_vlan = factory.make_VLAN( |
1205 | - dhcp_on=True, primary_rack=primary_rack, |
1206 | - secondary_rack=secondary_rack) |
1207 | - ha_subnet = factory.make_Subnet( |
1208 | - vlan=ha_vlan, cidr="fd38:c341:27da:c831::/64") |
1209 | - factory.make_IPRange( |
1210 | - ha_subnet, "fd38:c341:27da:c831:0:1::", |
1211 | - "fd38:c341:27da:c831:0:1:ffff:0") |
1212 | - factory.make_Interface( |
1213 | - INTERFACE_TYPE.PHYSICAL, node=primary_rack, vlan=ha_vlan) |
1214 | - secondary_interface = factory.make_Interface( |
1215 | - INTERFACE_TYPE.PHYSICAL, node=secondary_rack, vlan=ha_vlan) |
1216 | - factory.make_StaticIPAddress( |
1217 | - alloc_type=IPADDRESS_TYPE.AUTO, subnet=ha_subnet, |
1218 | - interface=secondary_interface) |
1219 | - other_subnet = factory.make_Subnet( |
1220 | - vlan=ha_vlan, cidr="fd38:c341:27da:c832::/64") |
1221 | - |
1222 | - ntp_servers = [factory.make_name("ntp")] |
1223 | - default_domain = Domain.objects.get_default_domain() |
1224 | - self.assertRaises( |
1225 | - DHCPConfigurationError, dhcp.get_dhcp_configure_for, |
1226 | - 6, primary_rack, ha_vlan, [ha_subnet, other_subnet], |
1227 | - ntp_servers, default_domain) |
1228 | - |
1229 | def test__returns_for_ipv6(self): |
1230 | primary_rack = factory.make_RackController() |
1231 | secondary_rack = factory.make_RackController() |
1232 | @@ -1747,26 +1714,6 @@ |
1233 | |
1234 | @wait_for_reactor |
1235 | @inlineCallbacks |
1236 | - def test__logs_DHCPConfigurationError_ipv4(self): |
1237 | - self.patch(dhcp.settings, "DHCP_CONNECT", True) |
1238 | - with TwistedLoggerFixture() as logger: |
1239 | - yield deferToDatabase( |
1240 | - self.create_rack_controller, missing_ipv4=True) |
1241 | - self.assertDocTestMatches( |
1242 | - "...No IPv4 interface...", logger.output) |
1243 | - |
1244 | - @wait_for_reactor |
1245 | - @inlineCallbacks |
1246 | - def test__logs_DHCPConfigurationError_ipv6(self): |
1247 | - self.patch(dhcp.settings, "DHCP_CONNECT", True) |
1248 | - with TwistedLoggerFixture() as logger: |
1249 | - yield deferToDatabase( |
1250 | - self.create_rack_controller, missing_ipv6=True) |
1251 | - self.assertDocTestMatches( |
1252 | - "...No IPv6 interface...", logger.output) |
1253 | - |
1254 | - @wait_for_reactor |
1255 | - @inlineCallbacks |
1256 | def test__doesnt_call_configure_for_both_ipv4_and_ipv6(self): |
1257 | # ... when DHCP_CONNECT is False. |
1258 | rack_controller, config = yield deferToDatabase( |
1259 | |
1260 | === modified file 'src/maasserver/tests/test_forms_vlan.py' |
1261 | --- src/maasserver/tests/test_forms_vlan.py 2016-04-27 20:38:06 +0000 |
1262 | +++ src/maasserver/tests/test_forms_vlan.py 2016-12-06 08:04:41 +0000 |
1263 | @@ -8,6 +8,7 @@ |
1264 | import random |
1265 | |
1266 | from maasserver.forms_vlan import VLANForm |
1267 | +from maasserver.models.fabric import Fabric |
1268 | from maasserver.models.vlan import DEFAULT_MTU |
1269 | from maasserver.testing.factory import factory |
1270 | from maasserver.testing.testcase import MAASServerTestCase |
1271 | @@ -27,6 +28,27 @@ |
1272 | ], |
1273 | }, form.errors) |
1274 | |
1275 | + def test__vlans_already_using_relay_vlan_not_shown(self): |
1276 | + fabric = Fabric.objects.get_default_fabric() |
1277 | + relay_vlan = factory.make_VLAN() |
1278 | + factory.make_VLAN(relay_vlan=relay_vlan) |
1279 | + form = VLANForm(fabric=fabric, data={}) |
1280 | + self.assertItemsEqual( |
1281 | + [fabric.get_default_vlan(), relay_vlan], |
1282 | + form.fields['relay_vlan'].queryset) |
1283 | + |
1284 | + def test__self_vlan_not_used_in_relay_vlan_field(self): |
1285 | + fabric = Fabric.objects.get_default_fabric() |
1286 | + relay_vlan = fabric.get_default_vlan() |
1287 | + form = VLANForm(instance=relay_vlan, data={}) |
1288 | + self.assertItemsEqual([], form.fields['relay_vlan'].queryset) |
1289 | + |
1290 | + def test__no_relay_vlans_allowed_when_dhcp_on(self): |
1291 | + vlan = factory.make_VLAN(dhcp_on=True) |
1292 | + factory.make_VLAN() |
1293 | + form = VLANForm(instance=vlan, data={}) |
1294 | + self.assertItemsEqual([], form.fields['relay_vlan'].queryset) |
1295 | + |
1296 | def test__creates_vlan(self): |
1297 | fabric = factory.make_Fabric() |
1298 | vlan_name = factory.make_name("vlan") |
1299 | @@ -211,6 +233,54 @@ |
1300 | vlan = reload_object(vlan) |
1301 | self.assertTrue(vlan.dhcp_on) |
1302 | |
1303 | + def test_update_sets_relay_vlan(self): |
1304 | + vlan = factory.make_VLAN() |
1305 | + relay_vlan = factory.make_VLAN() |
1306 | + form = VLANForm(instance=vlan, data={ |
1307 | + "relay_vlan": relay_vlan.id, |
1308 | + }) |
1309 | + self.assertTrue(form.is_valid(), form.errors) |
1310 | + form.save() |
1311 | + vlan = reload_object(vlan) |
1312 | + self.assertEquals(relay_vlan.id, vlan.relay_vlan.id) |
1313 | + |
1314 | + def test_update_clears_relay_vlan_when_None(self): |
1315 | + relay_vlan = factory.make_VLAN() |
1316 | + vlan = factory.make_VLAN(relay_vlan=relay_vlan) |
1317 | + form = VLANForm(instance=vlan, data={ |
1318 | + "relay_vlan": None, |
1319 | + }) |
1320 | + self.assertTrue(form.is_valid(), form.errors) |
1321 | + form.save() |
1322 | + vlan = reload_object(vlan) |
1323 | + self.assertIsNone(vlan.relay_vlan) |
1324 | + |
1325 | + def test_update_clears_relay_vlan_when_empty(self): |
1326 | + relay_vlan = factory.make_VLAN() |
1327 | + vlan = factory.make_VLAN(relay_vlan=relay_vlan) |
1328 | + form = VLANForm(instance=vlan, data={ |
1329 | + "relay_vlan": "", |
1330 | + }) |
1331 | + self.assertTrue(form.is_valid(), form.errors) |
1332 | + form.save() |
1333 | + vlan = reload_object(vlan) |
1334 | + self.assertIsNone(vlan.relay_vlan) |
1335 | + |
1336 | + def test_update_disables_relay_vlan_when_dhcp_turned_on(self): |
1337 | + relay_vlan = factory.make_VLAN() |
1338 | + vlan = factory.make_VLAN(relay_vlan=relay_vlan) |
1339 | + factory.make_ipv4_Subnet_with_IPRanges(vlan=vlan) |
1340 | + rack = factory.make_RackController(vlan=vlan) |
1341 | + vlan.primary_rack = rack |
1342 | + vlan.save() |
1343 | + form = VLANForm(instance=reload_object(vlan), data={ |
1344 | + "dhcp_on": "true", |
1345 | + }) |
1346 | + self.assertTrue(form.is_valid(), form.errors) |
1347 | + form.save() |
1348 | + vlan = reload_object(vlan) |
1349 | + self.assertIsNone(vlan.relay_vlan) |
1350 | + |
1351 | def test_update_validates_primary_rack_with_dhcp_on(self): |
1352 | vlan = factory.make_VLAN() |
1353 | form = VLANForm(instance=vlan, data={ |
1354 | |
1355 | === modified file 'src/maasserver/triggers/system.py' |
1356 | --- src/maasserver/triggers/system.py 2016-10-12 15:26:17 +0000 |
1357 | +++ src/maasserver/triggers/system.py 2016-12-06 08:04:41 +0000 |
1358 | @@ -268,6 +268,8 @@ |
1359 | DHCP_VLAN_UPDATE = dedent("""\ |
1360 | CREATE OR REPLACE FUNCTION sys_dhcp_vlan_update() |
1361 | RETURNS trigger as $$ |
1362 | + DECLARE |
1363 | + relay_vlan maasserver_vlan; |
1364 | BEGIN |
1365 | -- DHCP was turned off. |
1366 | IF OLD.dhcp_on AND NOT NEW.dhcp_on THEN |
1367 | @@ -303,6 +305,60 @@ |
1368 | PERFORM pg_notify(CONCAT('sys_dhcp_', NEW.secondary_rack_id), ''); |
1369 | END IF; |
1370 | END IF; |
1371 | + |
1372 | + -- Relay VLAN was set when it was previously unset. |
1373 | + IF OLD.relay_vlan_id IS NULL AND NEW.relay_vlan_id IS NOT NULL THEN |
1374 | + SELECT maasserver_vlan.* INTO relay_vlan |
1375 | + FROM maasserver_vlan |
1376 | + WHERE maasserver_vlan.id = NEW.relay_vlan_id; |
1377 | + IF relay_vlan.primary_rack_id IS NOT NULL THEN |
1378 | + PERFORM pg_notify( |
1379 | + CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), ''); |
1380 | + IF relay_vlan.secondary_rack_id IS NOT NULL THEN |
1381 | + PERFORM pg_notify( |
1382 | + CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), ''); |
1383 | + END IF; |
1384 | + END IF; |
1385 | + -- Relay VLAN was unset when it was previously set. |
1386 | + ELSIF OLD.relay_vlan_id IS NOT NULL AND NEW.relay_vlan_id IS NULL THEN |
1387 | + SELECT maasserver_vlan.* INTO relay_vlan |
1388 | + FROM maasserver_vlan |
1389 | + WHERE maasserver_vlan.id = OLD.relay_vlan_id; |
1390 | + IF relay_vlan.primary_rack_id IS NOT NULL THEN |
1391 | + PERFORM pg_notify( |
1392 | + CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), ''); |
1393 | + IF relay_vlan.secondary_rack_id IS NOT NULL THEN |
1394 | + PERFORM pg_notify( |
1395 | + CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), ''); |
1396 | + END IF; |
1397 | + END IF; |
1398 | + -- Relay VLAN has changed on the VLAN. |
1399 | + ELSIF OLD.relay_vlan_id != NEW.relay_vlan_id THEN |
1400 | + -- Alert old VLAN if required. |
1401 | + SELECT maasserver_vlan.* INTO relay_vlan |
1402 | + FROM maasserver_vlan |
1403 | + WHERE maasserver_vlan.id = OLD.relay_vlan_id; |
1404 | + IF relay_vlan.primary_rack_id IS NOT NULL THEN |
1405 | + PERFORM pg_notify( |
1406 | + CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), ''); |
1407 | + IF relay_vlan.secondary_rack_id IS NOT NULL THEN |
1408 | + PERFORM pg_notify( |
1409 | + CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), ''); |
1410 | + END IF; |
1411 | + END IF; |
1412 | + -- Alert new VLAN if required. |
1413 | + SELECT maasserver_vlan.* INTO relay_vlan |
1414 | + FROM maasserver_vlan |
1415 | + WHERE maasserver_vlan.id = NEW.relay_vlan_id; |
1416 | + IF relay_vlan.primary_rack_id IS NOT NULL THEN |
1417 | + PERFORM pg_notify( |
1418 | + CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), ''); |
1419 | + IF relay_vlan.secondary_rack_id IS NOT NULL THEN |
1420 | + PERFORM pg_notify( |
1421 | + CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), ''); |
1422 | + END IF; |
1423 | + END IF; |
1424 | + END IF; |
1425 | RETURN NEW; |
1426 | END; |
1427 | $$ LANGUAGE plpgsql; |
1428 | |
1429 | === modified file 'src/maasserver/triggers/tests/test_system_listener.py' |
1430 | --- src/maasserver/triggers/tests/test_system_listener.py 2016-10-12 15:26:17 +0000 |
1431 | +++ src/maasserver/triggers/tests/test_system_listener.py 2016-12-06 08:04:41 +0000 |
1432 | @@ -806,6 +806,120 @@ |
1433 | finally: |
1434 | yield listener.stopService() |
1435 | |
1436 | + @wait_for_reactor |
1437 | + @inlineCallbacks |
1438 | + def test_sends_messages_when_relay_vlan_set(self): |
1439 | + yield deferToDatabase(register_system_triggers) |
1440 | + primary_rack = yield deferToDatabase(self.create_rack_controller) |
1441 | + secondary_rack = yield deferToDatabase(self.create_rack_controller) |
1442 | + relay_vlan = yield deferToDatabase(self.create_vlan, params={ |
1443 | + "dhcp_on": True, |
1444 | + "primary_rack": primary_rack, |
1445 | + "secondary_rack": secondary_rack, |
1446 | + }) |
1447 | + vlan = yield deferToDatabase(self.create_vlan) |
1448 | + primary_rack_dv = DeferredValue() |
1449 | + secondary_rack_dv = DeferredValue() |
1450 | + listener = self.make_listener_without_delay() |
1451 | + listener.register( |
1452 | + "sys_dhcp_%s" % primary_rack.id, |
1453 | + lambda *args: primary_rack_dv.set(args)) |
1454 | + listener.register( |
1455 | + "sys_dhcp_%s" % secondary_rack.id, |
1456 | + lambda *args: secondary_rack_dv.set(args)) |
1457 | + yield listener.startService() |
1458 | + try: |
1459 | + yield deferToDatabase(self.update_vlan, vlan.id, { |
1460 | + "relay_vlan": relay_vlan, |
1461 | + }) |
1462 | + yield primary_rack_dv.get(timeout=2) |
1463 | + yield secondary_rack_dv.get(timeout=2) |
1464 | + finally: |
1465 | + yield listener.stopService() |
1466 | + |
1467 | + @wait_for_reactor |
1468 | + @inlineCallbacks |
1469 | + def test_sends_messages_when_relay_vlan_unset(self): |
1470 | + yield deferToDatabase(register_system_triggers) |
1471 | + primary_rack = yield deferToDatabase(self.create_rack_controller) |
1472 | + secondary_rack = yield deferToDatabase(self.create_rack_controller) |
1473 | + relay_vlan = yield deferToDatabase(self.create_vlan, params={ |
1474 | + "dhcp_on": True, |
1475 | + "primary_rack": primary_rack, |
1476 | + "secondary_rack": secondary_rack, |
1477 | + }) |
1478 | + vlan = yield deferToDatabase(self.create_vlan, { |
1479 | + "relay_vlan": relay_vlan, |
1480 | + }) |
1481 | + primary_rack_dv = DeferredValue() |
1482 | + secondary_rack_dv = DeferredValue() |
1483 | + listener = self.make_listener_without_delay() |
1484 | + listener.register( |
1485 | + "sys_dhcp_%s" % primary_rack.id, |
1486 | + lambda *args: primary_rack_dv.set(args)) |
1487 | + listener.register( |
1488 | + "sys_dhcp_%s" % secondary_rack.id, |
1489 | + lambda *args: secondary_rack_dv.set(args)) |
1490 | + yield listener.startService() |
1491 | + try: |
1492 | + yield deferToDatabase(self.update_vlan, vlan.id, { |
1493 | + "relay_vlan": None, |
1494 | + }) |
1495 | + yield primary_rack_dv.get(timeout=2) |
1496 | + yield secondary_rack_dv.get(timeout=2) |
1497 | + finally: |
1498 | + yield listener.stopService() |
1499 | + |
1500 | + @wait_for_reactor |
1501 | + @inlineCallbacks |
1502 | + def test_sends_messages_when_relay_vlan_changed(self): |
1503 | + yield deferToDatabase(register_system_triggers) |
1504 | + old_primary_rack = yield deferToDatabase(self.create_rack_controller) |
1505 | + old_secondary_rack = yield deferToDatabase(self.create_rack_controller) |
1506 | + old_relay_vlan = yield deferToDatabase(self.create_vlan, params={ |
1507 | + "dhcp_on": True, |
1508 | + "primary_rack": old_primary_rack, |
1509 | + "secondary_rack": old_secondary_rack, |
1510 | + }) |
1511 | + new_primary_rack = yield deferToDatabase(self.create_rack_controller) |
1512 | + new_secondary_rack = yield deferToDatabase(self.create_rack_controller) |
1513 | + new_relay_vlan = yield deferToDatabase(self.create_vlan, params={ |
1514 | + "dhcp_on": True, |
1515 | + "primary_rack": new_primary_rack, |
1516 | + "secondary_rack": new_secondary_rack, |
1517 | + }) |
1518 | + vlan = yield deferToDatabase(self.create_vlan, { |
1519 | + "relay_vlan": old_relay_vlan, |
1520 | + }) |
1521 | + old_primary_rack_dv = DeferredValue() |
1522 | + old_secondary_rack_dv = DeferredValue() |
1523 | + new_primary_rack_dv = DeferredValue() |
1524 | + new_secondary_rack_dv = DeferredValue() |
1525 | + listener = self.make_listener_without_delay() |
1526 | + listener.register( |
1527 | + "sys_dhcp_%s" % old_primary_rack.id, |
1528 | + lambda *args: old_primary_rack_dv.set(args)) |
1529 | + listener.register( |
1530 | + "sys_dhcp_%s" % old_secondary_rack.id, |
1531 | + lambda *args: old_secondary_rack_dv.set(args)) |
1532 | + listener.register( |
1533 | + "sys_dhcp_%s" % new_primary_rack.id, |
1534 | + lambda *args: new_primary_rack_dv.set(args)) |
1535 | + listener.register( |
1536 | + "sys_dhcp_%s" % new_secondary_rack.id, |
1537 | + lambda *args: new_secondary_rack_dv.set(args)) |
1538 | + yield listener.startService() |
1539 | + try: |
1540 | + yield deferToDatabase(self.update_vlan, vlan.id, { |
1541 | + "relay_vlan": new_relay_vlan, |
1542 | + }) |
1543 | + yield old_primary_rack_dv.get(timeout=2) |
1544 | + yield old_secondary_rack_dv.get(timeout=2) |
1545 | + yield new_primary_rack_dv.get(timeout=2) |
1546 | + yield new_secondary_rack_dv.get(timeout=2) |
1547 | + finally: |
1548 | + yield listener.stopService() |
1549 | + |
1550 | |
1551 | class TestDHCPSubnetListener( |
1552 | MAASTransactionServerTestCase, TransactionalHelpersMixin): |
1553 | |
1554 | === modified file 'src/maasserver/websockets/handlers/tests/test_vlan.py' |
1555 | --- src/maasserver/websockets/handlers/tests/test_vlan.py 2016-10-19 18:06:01 +0000 |
1556 | +++ src/maasserver/websockets/handlers/tests/test_vlan.py 2016-12-06 08:04:41 +0000 |
1557 | @@ -42,6 +42,7 @@ |
1558 | "external_dhcp": vlan.external_dhcp, |
1559 | "primary_rack": vlan.primary_rack, |
1560 | "secondary_rack": vlan.secondary_rack, |
1561 | + "relay_vlan": vlan.relay_vlan_id, |
1562 | } |
1563 | data['rack_sids'] = sorted(list({ |
1564 | interface.node.system_id |
1565 | @@ -206,6 +207,20 @@ |
1566 | self.assertThat(vlan.primary_rack, Is(None)) |
1567 | self.assertThat(vlan.secondary_rack, Is(None)) |
1568 | |
1569 | + def test__configure_dhcp_with_relay_vlan(self): |
1570 | + user = factory.make_admin() |
1571 | + handler = VLANHandler(user, {}) |
1572 | + vlan = factory.make_VLAN() |
1573 | + relay_vlan = factory.make_VLAN() |
1574 | + handler.configure_dhcp({ |
1575 | + "id": vlan.id, |
1576 | + "controllers": [], |
1577 | + "relay_vlan": relay_vlan.id, |
1578 | + }) |
1579 | + vlan = reload_object(vlan) |
1580 | + self.assertThat(vlan.dhcp_on, Equals(False)) |
1581 | + self.assertThat(vlan.relay_vlan, Equals(relay_vlan)) |
1582 | + |
1583 | def test__non_superuser_asserts(self): |
1584 | user = factory.make_User() |
1585 | handler = VLANHandler(user, {}) |
1586 | |
1587 | === modified file 'src/maasserver/websockets/handlers/vlan.py' |
1588 | --- src/maasserver/websockets/handlers/vlan.py 2016-10-20 19:39:48 +0000 |
1589 | +++ src/maasserver/websockets/handlers/vlan.py 2016-12-06 08:04:41 +0000 |
1590 | @@ -104,6 +104,10 @@ |
1591 | NODE_PERMISSION.ADMIN, vlan), "Permission denied." |
1592 | vlan.delete() |
1593 | |
1594 | + def update(self, parameters): |
1595 | + """Delete this VLAN.""" |
1596 | + return super(VLANHandler, self).update(parameters) |
1597 | + |
1598 | def _configure_iprange_and_gateway(self, parameters): |
1599 | if 'subnet' in parameters and parameters['subnet'] is not None: |
1600 | subnet = Subnet.objects.get(id=parameters['subnet']) |
1601 | @@ -171,18 +175,21 @@ |
1602 | # of parameters, to prevent spurious log statements. |
1603 | if 'extra' in parameters: |
1604 | self._configure_iprange_and_gateway(parameters['extra']) |
1605 | - iprange_count = IPRange.objects.filter( |
1606 | - type=IPRANGE_TYPE.DYNAMIC, subnet__vlan=vlan).count() |
1607 | - if iprange_count == 0: |
1608 | - raise ValueError( |
1609 | - "Cannot configure DHCP: At least one dynamic range is " |
1610 | - "required.") |
1611 | + if 'relay_vlan' not in parameters: |
1612 | + iprange_count = IPRange.objects.filter( |
1613 | + type=IPRANGE_TYPE.DYNAMIC, subnet__vlan=vlan).count() |
1614 | + if iprange_count == 0: |
1615 | + raise ValueError( |
1616 | + "Cannot configure DHCP: At least one dynamic range is " |
1617 | + "required.") |
1618 | controllers = parameters.get('controllers', []) |
1619 | data = { |
1620 | "dhcp_on": True if len(controllers) > 0 else False, |
1621 | "primary_rack": controllers[0] if len(controllers) > 0 else None, |
1622 | "secondary_rack": controllers[1] if len(controllers) > 1 else None, |
1623 | } |
1624 | + if 'relay_vlan' in parameters: |
1625 | + data['relay_vlan'] = parameters['relay_vlan'] |
1626 | form = VLANForm(instance=vlan, data=data) |
1627 | if form.is_valid(): |
1628 | form.save() |
I have tested this and the dhcpd.conf is getting written correctly. Still waiting on review from design before landing.