Merge lp:~blake-rouse/maas/vlan-relay into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 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
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.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I have tested this and the dhcpd.conf is getting written correctly. Still waiting on review from design before landing.

Revision history for this message
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?

review: Needs Information
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Also, can you link bug #1602412 to this branch with --fixes?

Revision history for this message
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.

Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Mike Pontillo (mpontillo) :
Revision history for this message
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/migrations/builtin/maasserver/0095_set_
> default_vlan_field.py'
> > --- src/maasserver/migrations/builtin/maasserver/0095_set_default_vlan_field.py
> 1970-01-01 00:00:00 +0000
> > +++ src/maasserver/migrations/builtin/maasserver/0095_set_default_vlan_field.py
> 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.db.models.deletion
> > +import maasserver.models.subnet
> > +
> > +
> > +class Migration(migrations.Migration):
> > +
> > + dependencies = [
> > + ('maasserver', '0094_vlan_relay_vlan'),
> > + ]
> > +
> > + operations = [
> > + migrations.AlterField(
> > + model_name='subnet',
> > + name='vlan',
> > + field=models.ForeignKey(to='maasserver.VLAN',
> default=maasserver.models.subnet.get_default_vlan,
> on_delete=django.db.models.deletion.PROTECT),
>
> 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=SET_NULL`
> on the `relay_vlan` field.
>
> > + ),
> > + ]
>
>
> --
> https://code.launchpad.net/~blake-rouse/maas/vlan-relay/+merge/312165
> You are the owner of lp:~blake-rouse/maas/vlan-relay.
>

Revision history for this message
Mike Pontillo (mpontillo) :
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Ah thanks for clarifying. I have fixed the issue and included a test to check.

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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&#xa;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()