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
=== modified file 'src/maasserver/api/tests/test_vlans.py'
--- src/maasserver/api/tests/test_vlans.py 2016-05-24 21:29:53 +0000
+++ src/maasserver/api/tests/test_vlans.py 2016-12-06 08:04:41 +0000
@@ -84,6 +84,29 @@
84 self.assertEqual(vid, response_data['vid'])84 self.assertEqual(vid, response_data['vid'])
85 self.assertEqual(mtu, response_data['mtu'])85 self.assertEqual(mtu, response_data['mtu'])
8686
87 def test_create_with_relay_vlan(self):
88 self.become_admin()
89 fabric = factory.make_Fabric()
90 vlan_name = factory.make_name("fabric")
91 vid = random.randint(1, 1000)
92 mtu = random.randint(552, 1500)
93 relay_vlan = factory.make_VLAN()
94 uri = get_vlans_uri(fabric)
95 response = self.client.post(uri, {
96 "name": vlan_name,
97 "vid": vid,
98 "mtu": mtu,
99 "relay_vlan": relay_vlan.id,
100 })
101 self.assertEqual(
102 http.client.OK, response.status_code, response.content)
103 response_data = json.loads(
104 response.content.decode(settings.DEFAULT_CHARSET))
105 self.assertEqual(vlan_name, response_data['name'])
106 self.assertEqual(vid, response_data['vid'])
107 self.assertEqual(mtu, response_data['mtu'])
108 self.assertEqual(relay_vlan.vid, response_data['relay_vlan']['vid'])
109
87 def test_create_admin_only(self):110 def test_create_admin_only(self):
88 fabric = factory.make_Fabric()111 fabric = factory.make_Fabric()
89 vlan_name = factory.make_name("fabric")112 vlan_name = factory.make_name("fabric")
@@ -182,6 +205,23 @@
182 self.assertEqual(new_vid, parsed_vlan['vid'])205 self.assertEqual(new_vid, parsed_vlan['vid'])
183 self.assertEqual(new_vid, vlan.vid)206 self.assertEqual(new_vid, vlan.vid)
184207
208 def test_update_sets_relay_vlan(self):
209 self.become_admin()
210 fabric = factory.make_Fabric()
211 vlan = factory.make_VLAN(fabric=fabric)
212 uri = get_vlan_uri(vlan)
213 relay_vlan = factory.make_VLAN()
214 response = self.client.put(uri, {
215 "relay_vlan": relay_vlan.id,
216 })
217 self.assertEqual(
218 http.client.OK, response.status_code, response.content)
219 parsed_vlan = json.loads(
220 response.content.decode(settings.DEFAULT_CHARSET))
221 vlan = reload_object(vlan)
222 self.assertEqual(relay_vlan.vid, parsed_vlan['relay_vlan']['vid'])
223 self.assertEqual(relay_vlan, vlan.relay_vlan)
224
185 def test_update_with_fabric(self):225 def test_update_with_fabric(self):
186 self.become_admin()226 self.become_admin()
187 fabric = factory.make_Fabric()227 fabric = factory.make_Fabric()
188228
=== modified file 'src/maasserver/api/vlans.py'
--- src/maasserver/api/vlans.py 2016-04-27 20:40:24 +0000
+++ src/maasserver/api/vlans.py 2016-12-06 08:04:41 +0000
@@ -26,6 +26,7 @@
26 'secondary_rack',26 'secondary_rack',
27 'dhcp_on',27 'dhcp_on',
28 'external_dhcp',28 'external_dhcp',
29 'relay_vlan',
29)30)
3031
3132
@@ -165,12 +166,18 @@
165 :type vid: integer166 :type vid: integer
166 :param mtu: The MTU to use on the VLAN.167 :param mtu: The MTU to use on the VLAN.
167 :type mtu: integer168 :type mtu: integer
168 :Param dhcp_on: Whether or not DHCP should be managed on the VLAN.169 :param dhcp_on: Whether or not DHCP should be managed on the VLAN.
169 :type dhcp_on: boolean170 :type dhcp_on: boolean
170 :param primary_rack: The primary rack controller managing the VLAN.171 :param primary_rack: The primary rack controller managing the VLAN.
171 :type primary_rack: system_id172 :type primary_rack: system_id
172 :param secondary_rack: The secondary rack controller manging the VLAN.173 :param secondary_rack: The secondary rack controller manging the VLAN.
173 :type secondary_rack: system_id174 :type secondary_rack: system_id
175 :param relay_vlan: Only set when this VLAN will be using a DHCP relay
176 to forward DHCP requests to another VLAN that MAAS is or will run
177 the DHCP server. MAAS will not run the DHCP relay itself, it must
178 be configured to proxy reqests to the primary and/or secondary
179 rack controller interfaces for the VLAN specified in this field.
180 :type relay_vlan: ID of VLAN
174181
175 Returns 404 if the fabric or VLAN is not found.182 Returns 404 if the fabric or VLAN is not found.
176 """183 """
177184
=== modified file 'src/maasserver/dhcp.py'
--- src/maasserver/dhcp.py 2016-12-03 16:33:44 +0000
+++ src/maasserver/dhcp.py 2016-12-06 08:04:41 +0000
@@ -29,10 +29,7 @@
29 IPRANGE_TYPE,29 IPRANGE_TYPE,
30 SERVICE_STATUS,30 SERVICE_STATUS,
31)31)
32from maasserver.exceptions import (32from maasserver.exceptions import UnresolvableHost
33 DHCPConfigurationError,
34 UnresolvableHost,
35)
36from maasserver.models import (33from maasserver.models import (
37 Config,34 Config,
38 DHCPSnippet,35 DHCPSnippet,
@@ -194,18 +191,19 @@
194 return []191 return []
195192
196193
197def get_managed_vlans_for(rack_controller):194def gen_managed_vlans_for(rack_controller):
198 """Return list of `VLAN` for the `rack_controller` when DHCP is enabled and195 """Yeilds each `VLAN` for the `rack_controller` when DHCP is enabled and
199 `rack_controller` is either the `primary_rack` or the `secondary_rack`.196 `rack_controller` is either the `primary_rack` or the `secondary_rack`.
200 """197 """
201 interfaces = rack_controller.interface_set.filter(198 interfaces = rack_controller.interface_set.filter(
202 Q(vlan__dhcp_on=True) & (199 Q(vlan__dhcp_on=True) & (
203 Q(vlan__primary_rack=rack_controller) |200 Q(vlan__primary_rack=rack_controller) |
204 Q(vlan__secondary_rack=rack_controller))).select_related("vlan")201 Q(vlan__secondary_rack=rack_controller)))
205 return {202 interfaces = interfaces.prefetch_related("vlan__relay_vlans")
206 interface.vlan203 for interface in interfaces:
207 for interface in interfaces204 yield interface.vlan
208 }205 for relayed_vlan in interface.vlan.relay_vlans.all():
206 yield relayed_vlan
209207
210208
211def ip_is_on_vlan(ip_address, vlan):209def ip_is_on_vlan(ip_address, vlan):
@@ -460,12 +458,6 @@
460 interfaces = get_interfaces_with_ip_on_vlan(458 interfaces = get_interfaces_with_ip_on_vlan(
461 rack_controller, vlan, ip_version)459 rack_controller, vlan, ip_version)
462 interface = get_best_interface(interfaces)460 interface = get_best_interface(interfaces)
463 if interface is None:
464 raise DHCPConfigurationError(
465 "No IPv%d interface on rack controller '%s' has an IP address on "
466 "any subnet on VLAN '%s.%d'." % (
467 ip_version, rack_controller.hostname, vlan.fabric.name,
468 vlan.vid))
469461
470 # Generate the failover peer for this VLAN.462 # Generate the failover peer for this VLAN.
471 if vlan.secondary_rack_id is not None:463 if vlan.secondary_rack_id is not None:
@@ -497,7 +489,7 @@
497 hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets)489 hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets)
498 return (490 return (
499 peer_config, sorted(subnet_configs, key=itemgetter("subnet")),491 peer_config, sorted(subnet_configs, key=itemgetter("subnet")),
500 hosts, interface.name)492 hosts, None if interface is None else interface.name)
501493
502494
503@synchronous495@synchronous
@@ -506,7 +498,7 @@
506 """Return tuple with IPv4 and IPv6 configurations for the498 """Return tuple with IPv4 and IPv6 configurations for the
507 rack controller."""499 rack controller."""
508 # Get list of all vlans that are being managed by the rack controller.500 # Get list of all vlans that are being managed by the rack controller.
509 vlans = get_managed_vlans_for(rack_controller)501 vlans = gen_managed_vlans_for(rack_controller)
510502
511 # Group the subnets on each VLAN into IPv4 and IPv6 subnets.503 # Group the subnets on each VLAN into IPv4 and IPv6 subnets.
512 vlan_subnets = {504 vlan_subnets = {
@@ -562,52 +554,40 @@
562 for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items():554 for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items():
563 # IPv4555 # IPv4
564 if len(subnets_v4) > 0:556 if len(subnets_v4) > 0:
565 try:557 config = get_dhcp_configure_for(
566 config = get_dhcp_configure_for(558 4, rack_controller, vlan, subnets_v4, ntp_servers,
567 4, rack_controller, vlan, subnets_v4, ntp_servers,559 default_domain, dhcp_snippets)
568 default_domain, dhcp_snippets)560 failover_peer, subnets, hosts, interface = config
569 except DHCPConfigurationError:561 if failover_peer is not None:
570 # XXX bug #1602412: this silently breaks DHCPv4, but we cannot562 failover_peers_v4.append(failover_peer)
571 # allow it to crash here since DHCPv6 might be able to run.563 shared_networks_v4.append({
572 # This error may be irrelevant if there is an IPv4 network in564 "name": "vlan-%d" % vlan.id,
573 # the MAAS model which is not configured on the rack, and the565 "subnets": subnets,
574 # user only wants to serve DHCPv6. But it is still something566 })
575 # worth noting, so log it and continue.567 hosts_v4.extend(hosts)
576 log.err(None, "Failure configuring DHCPv4.")568 if interface is not None:
577 else:
578 failover_peer, subnets, hosts, interface = config
579 if failover_peer is not None:
580 failover_peers_v4.append(failover_peer)
581 shared_networks_v4.append({
582 "name": "vlan-%d" % vlan.id,
583 "subnets": subnets,
584 })
585 hosts_v4.extend(hosts)
586 interfaces_v4.add(interface)569 interfaces_v4.add(interface)
587 # IPv6570 # IPv6
588 if len(subnets_v6) > 0:571 if len(subnets_v6) > 0:
589 try:572 config = get_dhcp_configure_for(
590 config = get_dhcp_configure_for(573 6, rack_controller, vlan, subnets_v6,
591 6, rack_controller, vlan, subnets_v6,574 ntp_servers, default_domain, dhcp_snippets)
592 ntp_servers, default_domain, dhcp_snippets)575 failover_peer, subnets, hosts, interface = config
593 except DHCPConfigurationError:576 if failover_peer is not None:
594 # XXX bug #1602412: this silently breaks DHCPv6, but we cannot577 failover_peers_v6.append(failover_peer)
595 # allow it to crash here since DHCPv4 might be able to run.578 shared_networks_v6.append({
596 # This error may be irrelevant if there is an IPv6 network in579 "name": "vlan-%d" % vlan.id,
597 # the MAAS model which is not configured on the rack, and the580 "subnets": subnets,
598 # user only wants to serve DHCPv4. But it is still something581 })
599 # worth noting, so log it and continue.582 hosts_v6.extend(hosts)
600 log.err(None, "Failure configuring DHCPv6.")583 if interface is not None:
601 else:
602 failover_peer, subnets, hosts, interface = config
603 if failover_peer is not None:
604 failover_peers_v6.append(failover_peer)
605 shared_networks_v6.append({
606 "name": "vlan-%d" % vlan.id,
607 "subnets": subnets,
608 })
609 hosts_v6.extend(hosts)
610 interfaces_v6.add(interface)584 interfaces_v6.add(interface)
585 # When no interfaces exist for each IP version clear the shared networks
586 # as DHCP server cannot be started and needs to be stopped.
587 if len(interfaces_v4) == 0:
588 shared_networks_v4 = {}
589 if len(interfaces_v6) == 0:
590 shared_networks_v6 = {}
611 return DHCPConfigurationForRack(591 return DHCPConfigurationForRack(
612 failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4,592 failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4,
613 failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6,593 failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6,
614594
=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/exceptions.py 2016-12-06 08:04:41 +0000
@@ -199,7 +199,3 @@
199 information.199 information.
200 """200 """
201 api_error = int(http.client.SERVICE_UNAVAILABLE)201 api_error = int(http.client.SERVICE_UNAVAILABLE)
202
203
204class DHCPConfigurationError(MAASException):
205 """Raised when the configuration of DHCP hits a problem."""
206202
=== modified file 'src/maasserver/forms_vlan.py'
--- src/maasserver/forms_vlan.py 2016-04-27 20:38:06 +0000
+++ src/maasserver/forms_vlan.py 2016-12-06 08:04:41 +0000
@@ -31,6 +31,7 @@
31 'dhcp_on',31 'dhcp_on',
32 'primary_rack',32 'primary_rack',
33 'secondary_rack',33 'secondary_rack',
34 'relay_vlan',
34 )35 )
3536
36 def __init__(self, *args, **kwargs):37 def __init__(self, *args, **kwargs):
@@ -40,6 +41,7 @@
40 if instance is None and self.fabric is None:41 if instance is None and self.fabric is None:
41 raise ValueError("Form requires either a instance or a fabric.")42 raise ValueError("Form requires either a instance or a fabric.")
42 self._set_up_rack_fields()43 self._set_up_rack_fields()
44 self._set_up_relay_vlan()
4345
44 def _set_up_rack_fields(self):46 def _set_up_rack_fields(self):
45 qs = RackController.objects.filter_by_vids([self.instance.vid])47 qs = RackController.objects.filter_by_vids([self.instance.vid])
@@ -61,6 +63,22 @@
61 secondary_rack = RackController.objects.get(id=secondary_rack_id)63 secondary_rack = RackController.objects.get(id=secondary_rack_id)
62 self.initial['secondary_rack'] = secondary_rack.system_id64 self.initial['secondary_rack'] = secondary_rack.system_id
6365
66 def _set_up_relay_vlan(self):
67 # Configure the relay_vlan fields to include only VLAN's that are
68 # not already on a relay_vlan. If this is an update then it cannot
69 # be itself or never set when dhcp_on is True.
70 possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True)
71 if self.instance is not None:
72 possible_relay_vlans = possible_relay_vlans.exclude(
73 id=self.instance.id)
74 if self.instance.dhcp_on:
75 possible_relay_vlans = VLAN.objects.none()
76 if self.instance.relay_vlan is not None:
77 possible_relay_vlans = VLAN.objects.filter(
78 id=self.instance.relay_vlan.id)
79 self.fields['relay_vlan'] = forms.ModelChoiceField(
80 queryset=possible_relay_vlans, required=False)
81
64 def clean(self):82 def clean(self):
65 cleaned_data = super(VLANForm, self).clean()83 cleaned_data = super(VLANForm, self).clean()
66 # Automatically promote the secondary rack controller to the primary84 # Automatically promote the secondary rack controller to the primary
@@ -120,5 +138,12 @@
120 interface = super(VLANForm, self).save(commit=False)138 interface = super(VLANForm, self).save(commit=False)
121 if self.fabric is not None:139 if self.fabric is not None:
122 interface.fabric = self.fabric140 interface.fabric = self.fabric
141 if ('relay_vlan' in self.data and
142 not self.cleaned_data.get('relay_vlan')):
143 # relay_vlan is being cleared.
144 interface.relay_vlan = None
145 if interface.dhcp_on:
146 # relay_vlan cannot be set when dhcp is on.
147 interface.relay_vlan = None
123 interface.save()148 interface.save()
124 return interface149 return interface
125150
=== modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py'
--- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-30 01:17:54 +0000
+++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-12-06 08:04:41 +0000
@@ -44,6 +44,6 @@
44 migrations.AlterField(44 migrations.AlterField(
45 model_name='subnet',45 model_name='subnet',
46 name='vlan',46 name='vlan',
47 field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),47 field=models.ForeignKey(to='maasserver.VLAN', default=None, on_delete=django.db.models.deletion.PROTECT),
48 ),48 ),
49 ]49 ]
5050
=== added file 'src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py'
--- src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 2016-12-06 08:04:41 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9
10
11class Migration(migrations.Migration):
12
13 dependencies = [
14 ('maasserver', '0094_add_unmanaged_subnets'),
15 ]
16
17 operations = [
18 migrations.AddField(
19 model_name='vlan',
20 name='relay_vlan',
21 field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name='relay_vlans', to='maasserver.VLAN'),
22 ),
23 ]
024
=== added file 'src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py'
--- src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 2016-12-06 08:04:41 +0000
@@ -0,0 +1,24 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9import maasserver.models.subnet
10
11
12class Migration(migrations.Migration):
13
14 dependencies = [
15 ('maasserver', '0095_vlan_relay_vlan'),
16 ]
17
18 operations = [
19 migrations.AlterField(
20 model_name='subnet',
21 name='vlan',
22 field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),
23 ),
24 ]
025
=== modified file 'src/maasserver/models/tests/test_vlan.py'
--- src/maasserver/models/tests/test_vlan.py 2016-10-19 18:06:01 +0000
+++ src/maasserver/models/tests/test_vlan.py 2016-12-06 08:04:41 +0000
@@ -88,6 +88,14 @@
8888
89class TestVLAN(MAASServerTestCase):89class TestVLAN(MAASServerTestCase):
9090
91 def test_delete_relay_vlan_doesnt_delete_vlan(self):
92 relay_vlan = factory.make_VLAN()
93 vlan = factory.make_VLAN(relay_vlan=relay_vlan)
94 relay_vlan.delete()
95 vlan = reload_object(vlan)
96 self.assertIsNotNone(vlan)
97 self.assertIsNone(vlan.relay_vlan)
98
91 def test_get_name_for_default_vlan_is_untagged(self):99 def test_get_name_for_default_vlan_is_untagged(self):
92 fabric = factory.make_Fabric()100 fabric = factory.make_Fabric()
93 self.assertEqual("untagged", fabric.get_default_vlan().get_name())101 self.assertEqual("untagged", fabric.get_default_vlan().get_name())
94102
=== modified file 'src/maasserver/models/vlan.py'
--- src/maasserver/models/vlan.py 2016-10-20 19:39:48 +0000
+++ src/maasserver/models/vlan.py 2016-12-06 08:04:41 +0000
@@ -14,6 +14,7 @@
14from django.db.models import (14from django.db.models import (
15 BooleanField,15 BooleanField,
16 CharField,16 CharField,
17 deletion,
17 ForeignKey,18 ForeignKey,
18 IntegerField,19 IntegerField,
19 Manager,20 Manager,
@@ -169,6 +170,10 @@
169 'RackController', null=True, blank=True, editable=True,170 'RackController', null=True, blank=True, editable=True,
170 related_name='+')171 related_name='+')
171172
173 relay_vlan = ForeignKey(
174 'self', null=True, blank=True, editable=True,
175 related_name='relay_vlans', on_delete=deletion.SET_NULL)
176
172 def __str__(self):177 def __str__(self):
173 return "%s.%s" % (self.fabric.get_name(), self.get_name())178 return "%s.%s" % (self.fabric.get_name(), self.get_name())
174179
175180
=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js'
--- src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-10-19 18:06:01 +0000
+++ src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2016-12-06 08:04:41 +0000
@@ -427,6 +427,42 @@
427 expect(controller.actionError).toBe(null);427 expect(controller.actionError).toBe(null);
428 });428 });
429429
430 it("performAction for relay_dhcp called with all params", function() {
431 var controller = makeControllerResolveSetActiveItem();
432 controller.actionOption = controller.RELAY_DHCP_ACTION;
433 // This will populate the default values for the racks with
434 // the current values from the mock objects.
435 controller.actionOptionChanged();
436 controller.provideDHCPAction.subnet = 1;
437 controller.provideDHCPAction.gatewayIP = "192.168.0.1";
438 controller.provideDHCPAction.startIP = "192.168.0.2";
439 controller.provideDHCPAction.endIP = "192.168.0.254";
440 var relay = {
441 id: makeInteger(5001, 6000)
442 };
443 VLANsManager._items = [relay];
444 controller.provideDHCPAction.relayVLAN = relay;
445 var defer = $q.defer();
446 spyOn(VLANsManager, "configureDHCP").and.returnValue(
447 defer.promise);
448 controller.actionGo();
449 defer.resolve();
450 $scope.$digest();
451 expect(VLANsManager.configureDHCP).toHaveBeenCalledWith(
452 controller.vlan,
453 [],
454 {
455 subnet: 1,
456 gateway: "192.168.0.1",
457 start: "192.168.0.2",
458 end: "192.168.0.254"
459 },
460 relay.id
461 );
462 expect(controller.actionOption).toBe(null);
463 expect(controller.actionError).toBe(null);
464 });
465
430 it("performAction for disable_dhcp called with all params", function() {466 it("performAction for disable_dhcp called with all params", function() {
431 var controller = makeControllerResolveSetActiveItem();467 var controller = makeControllerResolveSetActiveItem();
432 controller.actionOption = controller.DISABLE_DHCP_ACTION;468 controller.actionOption = controller.DISABLE_DHCP_ACTION;
@@ -457,6 +493,7 @@
457 controller.actionOptionChanged();493 controller.actionOptionChanged();
458 expect(controller.provideDHCPAction).toEqual({494 expect(controller.provideDHCPAction).toEqual({
459 subnet: subnet.id,495 subnet: subnet.id,
496 relayVLAN: null,
460 primaryRack: "p1",497 primaryRack: "p1",
461 secondaryRack: "p2",498 secondaryRack: "p2",
462 maxIPs: 0,499 maxIPs: 0,
@@ -488,6 +525,7 @@
488 controller.actionOptionChanged();525 controller.actionOptionChanged();
489 expect(controller.provideDHCPAction).toEqual({526 expect(controller.provideDHCPAction).toEqual({
490 subnet: subnet.id,527 subnet: subnet.id,
528 relayVLAN: null,
491 primaryRack: "p1",529 primaryRack: "p1",
492 secondaryRack: "p2",530 secondaryRack: "p2",
493 maxIPs: 26,531 maxIPs: 26,
@@ -551,30 +589,45 @@
551 expect(controller.actionOptions).toEqual([]);589 expect(controller.actionOptions).toEqual([]);
552 });590 });
553591
554 it("returns enable_dhcp and delete when dhcp is off",592 it("returns enable_dhcp, relay_dhcp and delete when dhcp is off",
555 function() {593 function() {
556 vlan.dhcp_on = false;594 vlan.dhcp_on = false;
557 UsersManager._authUser = {is_superuser: true};595 UsersManager._authUser = {is_superuser: true};
558 var controller = makeControllerResolveSetActiveItem();596 var controller = makeControllerResolveSetActiveItem();
559 expect(controller.actionOptions).toEqual([597 expect(controller.actionOptions).toEqual([
560 controller.PROVIDE_DHCP_ACTION,598 controller.PROVIDE_DHCP_ACTION,
599 controller.RELAY_DHCP_ACTION,
561 controller.DELETE_ACTION600 controller.DELETE_ACTION
562 ]);601 ]);
563 expect(controller.PROVIDE_DHCP_ACTION.title).toBe("Provide DHCP");602 expect(controller.PROVIDE_DHCP_ACTION.title).toBe("Provide DHCP");
564 });603 });
565604
566 it("returns disable_dhcp, enable_dhcp (with new title) and delete "+605 it("returns enable_dhcp (with new title), disable_dhcp and delete "+
567 "when dhcp is on", function() {606 "when dhcp is on", function() {
568 vlan.dhcp_on = true;607 vlan.dhcp_on = true;
569 UsersManager._authUser = {is_superuser: true};608 UsersManager._authUser = {is_superuser: true};
570 var controller = makeControllerResolveSetActiveItem();609 var controller = makeControllerResolveSetActiveItem();
571 expect(controller.actionOptions).toEqual([610 expect(controller.actionOptions).toEqual([
611 controller.PROVIDE_DHCP_ACTION,
572 controller.DISABLE_DHCP_ACTION,612 controller.DISABLE_DHCP_ACTION,
573 controller.PROVIDE_DHCP_ACTION,
574 controller.DELETE_ACTION613 controller.DELETE_ACTION
575 ]);614 ]);
576 expect(controller.PROVIDE_DHCP_ACTION.title).toBe(615 expect(controller.PROVIDE_DHCP_ACTION.title).toBe(
577 "Reconfigure DHCP");616 "Reconfigure DHCP");
578 });617 });
618
619 it("returns relay_dhcp (with new title), disable_dhcp and delete "+
620 "when relay_vlan is set", function() {
621 vlan.relay_vlan = 5001;
622 UsersManager._authUser = {is_superuser: true};
623 var controller = makeControllerResolveSetActiveItem();
624 expect(controller.actionOptions).toEqual([
625 controller.RELAY_DHCP_ACTION,
626 controller.DISABLE_DHCP_ACTION,
627 controller.DELETE_ACTION
628 ]);
629 expect(controller.RELAY_DHCP_ACTION.title).toBe(
630 "Reconfigure DHCP relay");
631 });
579 });632 });
580});633});
581634
=== modified file 'src/maasserver/static/js/angular/controllers/vlan_details.js'
--- src/maasserver/static/js/angular/controllers/vlan_details.js 2016-10-19 18:06:01 +0000
+++ src/maasserver/static/js/angular/controllers/vlan_details.js 2016-12-06 08:04:41 +0000
@@ -4,6 +4,18 @@
4 * MAAS VLAN Details Controller4 * MAAS VLAN Details Controller
5 */5 */
66
7angular.module('MAAS').filter('ignoreSelf', function () {
8 return function(objects, self) {
9 var filtered = [];
10 angular.forEach(objects, function(obj) {
11 if(obj !== self) {
12 filtered.push(obj);
13 }
14 });
15 return filtered;
16 };
17});
18
7angular.module('MAAS').controller('VLANDetailsController', [19angular.module('MAAS').controller('VLANDetailsController', [
8 '$scope', '$rootScope', '$routeParams', '$filter', '$location',20 '$scope', '$rootScope', '$routeParams', '$filter', '$location',
9 'VLANsManager', 'SubnetsManager', 'SpacesManager', 'FabricsManager',21 'VLANsManager', 'SubnetsManager', 'SpacesManager', 'FabricsManager',
@@ -27,10 +39,15 @@
27 $rootScope.page = "networks";39 $rootScope.page = "networks";
2840
29 vm.PROVIDE_DHCP_ACTION = {41 vm.PROVIDE_DHCP_ACTION = {
30 // Note: 'title' is setubndynamically depending on whether or not42 // Note: 'title' is set dynamically depending on whether or not
31 // DHCP is already enabled on this VLAN.43 // DHCP is already enabled on this VLAN.
32 name: "enable_dhcp"44 name: "enable_dhcp"
33 };45 };
46 vm.RELAY_DHCP_ACTION = {
47 // Note: 'title' is set ndynamically depending on whether or not
48 // DHCP relay is already enabled on this VLAN.
49 name: "relay_dhcp"
50 };
34 vm.DISABLE_DHCP_ACTION = {51 vm.DISABLE_DHCP_ACTION = {
35 name: "disable_dhcp",52 name: "disable_dhcp",
36 title: "Disable DHCP"53 title: "Disable DHCP"
@@ -47,6 +64,7 @@
47 vm.actionOption = null;64 vm.actionOption = null;
48 vm.actionOptions = [];65 vm.actionOptions = [];
49 vm.vlanManager = VLANsManager;66 vm.vlanManager = VLANsManager;
67 vm.vlans = VLANsManager.getItems();
50 vm.subnets = SubnetsManager.getItems();68 vm.subnets = SubnetsManager.getItems();
51 vm.spaces = SpacesManager.getItems();69 vm.spaces = SpacesManager.getItems();
52 vm.fabrics = FabricsManager.getItems();70 vm.fabrics = FabricsManager.getItems();
@@ -78,10 +96,15 @@
78 // Initialize the provideDHCPAction structure with the current primary96 // Initialize the provideDHCPAction structure with the current primary
79 // and secondary rack, plus an indication regarding whether or not97 // and secondary rack, plus an indication regarding whether or not
80 // adding a dynamic IP range is required.98 // adding a dynamic IP range is required.
81 vm.initProvideDHCP = function() {99 vm.initProvideDHCP = function(forRelay) {
82 vm.provideDHCPAction = {};100 vm.provideDHCPAction = {};
83 var dhcp = vm.provideDHCPAction;101 var dhcp = vm.provideDHCPAction;
84 dhcp.subnet = null;102 dhcp.subnet = null;
103 dhcp.relayVLAN = null;
104 if (angular.isNumber(vm.vlan.relay_vlan)) {
105 dhcp.relayVLAN = VLANsManager.getItemFromList(
106 vm.vlan.relay_vlan);
107 }
85 if (angular.isObject(vm.primaryRack)) {108 if (angular.isObject(vm.primaryRack)) {
86 dhcp.primaryRack = vm.primaryRack.system_id;109 dhcp.primaryRack = vm.primaryRack.system_id;
87 } else if(vm.relatedControllers.length > 0) {110 } else if(vm.relatedControllers.length > 0) {
@@ -140,15 +163,19 @@
140 }163 }
141 // Since we are setting default values for these three options,164 // Since we are setting default values for these three options,
142 // ensure all the appropriate updates occur.165 // ensure all the appropriate updates occur.
143 vm.updatePrimaryRack();166 if(!forRelay) {
144 vm.updateSecondaryRack();167 vm.updatePrimaryRack();
145 vm.updateSubnet();168 vm.updateSecondaryRack();
169 }
170 vm.updateSubnet(forRelay);
146 };171 };
147172
148 // Called when the actionOption has changed.173 // Called when the actionOption has changed.
149 vm.actionOptionChanged = function() {174 vm.actionOptionChanged = function() {
150 if(vm.actionOption.name === "enable_dhcp") {175 if(vm.actionOption.name === "enable_dhcp") {
151 vm.initProvideDHCP();176 vm.initProvideDHCP(false);
177 } else if(vm.actionOption.name === "relay_dhcp") {
178 vm.initProvideDHCP(true);
152 }179 }
153 // Clear the action error.180 // Clear the action error.
154 vm.actionError = null;181 vm.actionError = null;
@@ -200,7 +227,7 @@
200 };227 };
201228
202 // Called from the Provide DHCP form when the subnet selection changes.229 // Called from the Provide DHCP form when the subnet selection changes.
203 vm.updateSubnet = function() {230 vm.updateSubnet = function(forRelay) {
204 var dhcp = vm.provideDHCPAction;231 var dhcp = vm.provideDHCPAction;
205 var subnet = SubnetsManager.getItemFromList(dhcp.subnet);232 var subnet = SubnetsManager.getItemFromList(dhcp.subnet);
206 if(angular.isObject(subnet)) {233 if(angular.isObject(subnet)) {
@@ -212,10 +239,17 @@
212 }239 }
213 if(angular.isObject(iprange) && iprange.num_addresses > 0) {240 if(angular.isObject(iprange) && iprange.num_addresses > 0) {
214 dhcp.maxIPs = iprange.num_addresses;241 dhcp.maxIPs = iprange.num_addresses;
215 dhcp.startIP = iprange.start;242 if(forRelay) {
216 dhcp.endIP = iprange.end;243 dhcp.startIP = "";
217 dhcp.startPlaceholder = iprange.start;244 dhcp.endIP = "";
218 dhcp.endPlaceholder = iprange.end;245 dhcp.startPlaceholder = iprange.start + "( optional)";
246 dhcp.endPlaceholder = iprange.end + " (optional)";
247 } else {
248 dhcp.startIP = iprange.start;
249 dhcp.endIP = iprange.end;
250 dhcp.startPlaceholder = iprange.start;
251 dhcp.endPlaceholder = iprange.end;
252 }
219 } else {253 } else {
220 // Need to add a dynamic range, but according to our data,254 // Need to add a dynamic range, but according to our data,
221 // there is no room on the subnet for a dynamic range.255 // there is no room on the subnet for a dynamic range.
@@ -226,8 +260,14 @@
226 dhcp.endPlaceholder = "(no available IPs)";260 dhcp.endPlaceholder = "(no available IPs)";
227 }261 }
228 if(angular.isString(suggested_gateway)) {262 if(angular.isString(suggested_gateway)) {
229 dhcp.gatewayIP = suggested_gateway;263 if(forRelay) {
230 dhcp.gatewayPlaceholder = suggested_gateway;264 dhcp.gatewayIP = "";
265 dhcp.gatewayPlaceholder = (
266 suggested_gateway + " (optional)");
267 } else {
268 dhcp.gatewayIP = suggested_gateway;
269 dhcp.gatewayPlaceholder = suggested_gateway;
270 }
231 } else {271 } else {
232 // This means the subnet already has a gateway, so don't272 // This means the subnet already has a gateway, so don't
233 // bother populating it.273 // bother populating it.
@@ -261,8 +301,24 @@
261 vm.actionError = null;301 vm.actionError = null;
262 };302 };
263303
304 // Return True if the current action can be performed.
305 vm.canPerformAction = function() {
306 if(vm.actionOption.name === "enable_dhcp") {
307 return vm.relatedSubnets.length > 0;
308 } else if(vm.actionOption.name === "relay_dhcp") {
309 return angular.isObject(vm.provideDHCPAction.relayVLAN);
310 } else {
311 return true;
312 }
313 };
314
264 // Perform the action.315 // Perform the action.
265 vm.actionGo = function() {316 vm.actionGo = function() {
317 // Do nothing if action cannot be performed.
318 if(!vm.canPerformAction()) {
319 return;
320 }
321
266 if(vm.actionOption.name === "enable_dhcp") {322 if(vm.actionOption.name === "enable_dhcp") {
267 var dhcp = vm.provideDHCPAction;323 var dhcp = vm.provideDHCPAction;
268 var controllers = [];324 var controllers = [];
@@ -294,6 +350,23 @@
294 vm.actionError = result.error;350 vm.actionError = result.error;
295 vm.actionOption = vm.PROVIDE_DHCP_ACTION;351 vm.actionOption = vm.PROVIDE_DHCP_ACTION;
296 });352 });
353 } else if(vm.actionOption.name === "relay_dhcp") {
354 // These will be undefined if they don't exist, and the region
355 // will simply get an empty dictionary.
356 var extraDHCP = {};
357 extraDHCP.subnet = vm.provideDHCPAction.subnet;
358 extraDHCP.start = vm.provideDHCPAction.startIP;
359 extraDHCP.end = vm.provideDHCPAction.endIP;
360 extraDHCP.gateway = vm.provideDHCPAction.gatewayIP;
361 var relay = vm.provideDHCPAction.relayVLAN.id;
362 VLANsManager.configureDHCP(
363 vm.vlan, [], extraDHCP, relay).then(function() {
364 vm.actionOption = null;
365 vm.actionError = null;
366 }, function(result) {
367 vm.actionError = result.error;
368 vm.actionOption = vm.RELAY_DHCP_ACTION;
369 });
297 } else if(vm.actionOption.name === "disable_dhcp") {370 } else if(vm.actionOption.name === "disable_dhcp") {
298 VLANsManager.disableDHCP(vm.vlan).then(function() {371 VLANsManager.disableDHCP(vm.vlan).then(function() {
299 vm.actionOption = null;372 vm.actionOption = null;
@@ -319,6 +392,30 @@
319 return vm.actionError !== null;392 return vm.actionError !== null;
320 };393 };
321394
395 // Return the name of the VLAN.
396 vm.getFullVLANName = function(vlan_id) {
397 var vlan = VLANsManager.getItemFromList(vlan_id);
398 var fabric = FabricsManager.getItemFromList(vlan.fabric);
399 return (
400 FabricsManager.getName(fabric) + "." +
401 VLANsManager.getName(vlan));
402 };
403
404 // Return the current DHCP status.
405 vm.getDHCPStatus = function() {
406 if(vm.vlan) {
407 if(vm.vlan.dhcp_on) {
408 return "Enabled";
409 } else if(vm.vlan.relay_vlan) {
410 return "Relayed via " + vm.getFullVLANName(vm.vlan.relay_vlan);
411 } else {
412 return "Disabled";
413 }
414 } else {
415 return "";
416 }
417 };
418
322 // Updates the page title.419 // Updates the page title.
323 function updateTitle() {420 function updateTitle() {
324 var vlan = vm.vlan;421 var vlan = vm.vlan;
@@ -413,13 +510,22 @@
413 // object, since it's watched from $scope.)510 // object, since it's watched from $scope.)
414 vm.actionOptions.length = 0;511 vm.actionOptions.length = 0;
415 if(UsersManager.isSuperUser()) {512 if(UsersManager.isSuperUser()) {
416 if(vlan.dhcp_on === true) {513 if(!vlan.relay_vlan) {
417 vm.actionOptions.push(vm.DISABLE_DHCP_ACTION);514 if(vlan.dhcp_on === true) {
418 vm.PROVIDE_DHCP_ACTION.title = "Reconfigure DHCP";515 vm.PROVIDE_DHCP_ACTION.title = "Reconfigure DHCP";
516 vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION);
517 vm.actionOptions.push(vm.DISABLE_DHCP_ACTION);
518 } else {
519 vm.PROVIDE_DHCP_ACTION.title = "Provide DHCP";
520 vm.RELAY_DHCP_ACTION.title = "Relay DHCP";
521 vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION);
522 vm.actionOptions.push(vm.RELAY_DHCP_ACTION);
523 }
419 } else {524 } else {
420 vm.PROVIDE_DHCP_ACTION.title = "Provide DHCP";525 vm.actionOptions.push(vm.RELAY_DHCP_ACTION);
526 vm.actionOptions.push(vm.DISABLE_DHCP_ACTION);
527 vm.RELAY_DHCP_ACTION.title = "Reconfigure DHCP relay";
421 }528 }
422 vm.actionOptions.push(vm.PROVIDE_DHCP_ACTION);
423 if(!vm.isFabricDefault) {529 if(!vm.isFabricDefault) {
424 vm.actionOptions.push(vm.DELETE_ACTION);530 vm.actionOptions.push(vm.DELETE_ACTION);
425 }531 }
@@ -467,6 +573,8 @@
467 $scope.$watch("vlanDetails.vlan.name", updateTitle);573 $scope.$watch("vlanDetails.vlan.name", updateTitle);
468 $scope.$watch("vlanDetails.vlan.vid", updateTitle);574 $scope.$watch("vlanDetails.vlan.vid", updateTitle);
469 $scope.$watch("vlanDetails.vlan.dhcp_on", updatePossibleActions);575 $scope.$watch("vlanDetails.vlan.dhcp_on", updatePossibleActions);
576 $scope.$watch(
577 "vlanDetails.vlan.relay_vlan", updatePossibleActions);
470 $scope.$watch("vlanDetails.fabric.name", updateTitle);578 $scope.$watch("vlanDetails.fabric.name", updateTitle);
471 $scope.$watch(579 $scope.$watch(
472 "vlanDetails.vlan.primary_rack", updateManagementRacks);580 "vlanDetails.vlan.primary_rack", updateManagementRacks);
473581
=== modified file 'src/maasserver/static/js/angular/factories/tests/test_vlans.js'
--- src/maasserver/static/js/angular/factories/tests/test_vlans.js 2016-05-11 19:01:48 +0000
+++ src/maasserver/static/js/angular/factories/tests/test_vlans.js 2016-12-06 08:04:41 +0000
@@ -38,14 +38,16 @@
38 var result = {};38 var result = {};
39 var controllers = ["a", "b"];39 var controllers = ["a", "b"];
40 var extra = {"c": "d"};40 var extra = {"c": "d"};
41 var relay = makeInteger(1, 500);
41 spyOn(RegionConnection, "callMethod").and.returnValue(result);42 spyOn(RegionConnection, "callMethod").and.returnValue(result);
42 expect(VLANsManager.configureDHCP(43 expect(VLANsManager.configureDHCP(
43 obj, controllers, extra)).toBe(result);44 obj, controllers, extra, relay)).toBe(result);
44 expect(RegionConnection.callMethod).toHaveBeenCalledWith(45 expect(RegionConnection.callMethod).toHaveBeenCalledWith(
45 "vlan.configure_dhcp", {46 "vlan.configure_dhcp", {
46 id: obj.id,47 id: obj.id,
47 controllers: controllers,48 controllers: controllers,
48 extra: extra49 extra: extra,
50 relay_vlan: relay
49 }, true);51 }, true);
50 });52 });
51 });53 });
@@ -60,7 +62,8 @@
60 expect(RegionConnection.callMethod).toHaveBeenCalledWith(62 expect(RegionConnection.callMethod).toHaveBeenCalledWith(
61 "vlan.configure_dhcp", {63 "vlan.configure_dhcp", {
62 id: obj.id,64 id: obj.id,
63 controllers: []65 controllers: [],
66 relay_vlan: null
64 }, true);67 }, true);
65 });68 });
66 });69 });
6770
=== modified file 'src/maasserver/static/js/angular/factories/vlans.js'
--- src/maasserver/static/js/angular/factories/vlans.js 2016-05-11 19:01:48 +0000
+++ src/maasserver/static/js/angular/factories/vlans.js 2016-12-06 08:04:41 +0000
@@ -53,13 +53,17 @@
5353
54 // Configure DHCP on the VLAN54 // Configure DHCP on the VLAN
55 VLANsManager.prototype.configureDHCP = function(55 VLANsManager.prototype.configureDHCP = function(
56 vlan, controllers, extra) {56 vlan, controllers, extra, relay_vlan) {
57 var params = {
58 "id": vlan.id,
59 "controllers": controllers,
60 "extra": extra
61 };
62 if(relay_vlan === null || angular.isNumber(relay_vlan)) {
63 params.relay_vlan = relay_vlan;
64 }
57 return RegionConnection.callMethod(65 return RegionConnection.callMethod(
58 "vlan.configure_dhcp", {66 "vlan.configure_dhcp", params, true);
59 "id": vlan.id,
60 "controllers": controllers,
61 "extra": extra
62 }, true);
63 };67 };
6468
65 // Disable DHCP on the VLAN69 // Disable DHCP on the VLAN
@@ -67,7 +71,8 @@
67 return RegionConnection.callMethod(71 return RegionConnection.callMethod(
68 "vlan.configure_dhcp", {72 "vlan.configure_dhcp", {
69 "id": vlan.id,73 "id": vlan.id,
70 "controllers": []74 "controllers": [],
75 "relay_vlan": null
71 }, true);76 }, true);
72 };77 };
7378
7479
=== modified file 'src/maasserver/static/partials/vlan-details.html'
--- src/maasserver/static/partials/vlan-details.html 2016-11-17 11:46:08 +0000
+++ src/maasserver/static/partials/vlan-details.html 2016-12-06 08:04:41 +0000
@@ -1,27 +1,27 @@
1<div data-ng-hide="vlanDetails.loaded">1<div data-ng-if="!vlanDetails.loaded">
2 <header class="page-header" sticky>2 <header class="page-header" sticky>
3 <div class="wrapper--inner">3 <div class="wrapper--inner">
4 <h1 class="page-header__title">Loading...</h1>4 <h1 class="page-header__title">Loading...</h1>
5 </div>5 </div>
6 </header>6 </header>
7</div>7</div>
8<div class="ng-hide" data-ng-show="vlanDetails.loaded">8<div data-ng-if="vlanDetails.loaded">
9 <header class="page-header" sticky>9 <header class="page-header" sticky>
10 <div class="wrapper--inner">10 <div class="wrapper--inner">
11 <h1 class="page-header__title">{$ vlanDetails.title $}</h1>11 <h1 class="page-header__title">{$ vlanDetails.title $}</h1>
12 <!-- "Take action" dropdown -->12 <!-- "Take action" dropdown -->
13 <div class="page-header__controls" data-ng-show="vlanDetails.actionOptions.length">13 <div class="page-header__controls" data-ng-if="vlanDetails.actionOptions.length">
14 <div data-maas-cta="vlanDetails.actionOptions"14 <div data-maas-cta="vlanDetails.actionOptions"
15 data-ng-model="vlanDetails.actionOption"15 data-ng-model="vlanDetails.actionOption"
16 data-ng-change="vlanDetails.actionOptionChanged()">16 data-ng-change="vlanDetails.actionOptionChanged()">
17 </div>17 </div>
18 </div>18 </div>
19 <div class="page-header__dropdown" data-ng-class="{ 'is-open': vlanDetails.actionOption }">19 <div class="page-header__dropdown" data-ng-class="{ 'is-open': vlanDetails.actionOption }" data-ng-if="vlanDetails.actionOption">
20 <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'">20 <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp'">
21 <h3 class="page-header__dropdown-title" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'">Provide DHCP</h3>21 <h3 class="page-header__dropdown-title">Provide DHCP</h3>
22 <form class="form form--stack">22 <form class="form form--stack">
23 <!-- This is just for visual reasons, since we need an additional border to begin the form if there is no error. -->23 <!-- This is just for visual reasons, since we need an additional border to begin the form if there is no error. -->
24 <fieldset class="form__fieldset six-col" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'">24 <fieldset class="form__fieldset six-col">
25 <div class="form__group" data-ng-hide="vlanDetails.relatedSubnets.length === 0">25 <div class="form__group" data-ng-hide="vlanDetails.relatedSubnets.length === 0">
26 <label for="primary-rack" class="form__group-label two-col">{$ vlanDetails.relatedControllers.length <= 1 ? "Rack controller" : "Primary controller" $}</label>26 <label for="primary-rack" class="form__group-label two-col">{$ vlanDetails.relatedControllers.length <= 1 ? "Rack controller" : "Primary controller" $}</label>
27 <div class="form__group-input three-col">27 <div class="form__group-input three-col">
@@ -32,7 +32,7 @@
32 <option value=""32 <option value=""
33 disabled="disabled"33 disabled="disabled"
34 selected="selected"34 selected="selected"
35 data-ng-show="vlanDetails.provideDHCPAction.primaryRack === ''">Choose primary controller</option>35 data-ng-if="vlanDetails.provideDHCPAction.primaryRack === ''">Choose primary controller</option>
36 </select>36 </select>
37 </div>37 </div>
38 </div>38 </div>
@@ -40,14 +40,14 @@
40 <label for="secondary-rack" class="form__group-label two-col">Secondary controller</label>40 <label for="secondary-rack" class="form__group-label two-col">Secondary controller</label>
41 <div class="form__group-input three-col">41 <div class="form__group-input three-col">
42 <select name="secondary-rack"42 <select name="secondary-rack"
43 data-ng-show="vlanDetails.relatedControllers.length > 1"43 data-ng-if="vlanDetails.relatedControllers.length > 1"
44 data-ng-disabled="!vlanDetails.provideDHCPAction.primaryRack && vlanDetails.relatedControllers.length > 1"44 data-ng-disabled="!vlanDetails.provideDHCPAction.primaryRack && vlanDetails.relatedControllers.length > 1"
45 data-ng-model="vlanDetails.provideDHCPAction.secondaryRack"45 data-ng-model="vlanDetails.provideDHCPAction.secondaryRack"
46 data-ng-change="vlanDetails.updateSecondaryRack()"46 data-ng-change="vlanDetails.updateSecondaryRack()"
47 data-ng-options="rack.system_id as rack.hostname for rack in vlanDetails.relatedControllers | filter:vlanDetails.filterPrimaryRack">47 data-ng-options="rack.system_id as rack.hostname for rack in vlanDetails.relatedControllers | filter:vlanDetails.filterPrimaryRack">
48 <option value=""48 <option value=""
49 selected="selected"49 selected="selected"
50 data-ng-show="vlanDetails.relatedControllers.length >= 2"></option>50 data-ng-if="vlanDetails.relatedControllers.length >= 2"></option>
51 </select>51 </select>
52 </div>52 </div>
53 </div>53 </div>
@@ -57,7 +57,7 @@
57 <div class="form__group-input three-col">57 <div class="form__group-input three-col">
58 <select name="subnet"58 <select name="subnet"
59 data-ng-model="vlanDetails.provideDHCPAction.subnet"59 data-ng-model="vlanDetails.provideDHCPAction.subnet"
60 data-ng-change="vlanDetails.updateSubnet()"60 data-ng-change="vlanDetails.updateSubnet(false)"
61 data-ng-options="row.subnet.id as row.subnet.cidr for row in vlanDetails.relatedSubnets">61 data-ng-options="row.subnet.id as row.subnet.cidr for row in vlanDetails.relatedSubnets">
62 <option value="" disabled="disabled" selected="selected">Choose subnet</option>62 <option value="" disabled="disabled" selected="selected">Choose subnet</option>
63 <option value="" data-ng-if=""></option>63 <option value="" data-ng-if=""></option>
@@ -65,7 +65,7 @@
65 </div>65 </div>
66 </div>66 </div>
67 </fieldset>67 </fieldset>
68 <fieldset class="form__fieldset six-col last-col" data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp'">68 <fieldset class="form__fieldset six-col last-col" data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp'">
69 <div class="form__group"69 <div class="form__group"
70 data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0">70 data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0">
71 <label for="start-ip" class="form__group-label two-col">Dynamic range start IP</label>71 <label for="start-ip" class="form__group-label two-col">Dynamic range start IP</label>
@@ -114,26 +114,101 @@
114 </fieldset>114 </fieldset>
115 </form>115 </form>
116 </section>116 </section>
117 <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="vlanDetails.isActionError()">117 <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="vlanDetails.actionOption.name === 'relay_dhcp'">
118 <h3 class="page-header__dropdown-title">Relay DHCP</h3>
119 <form class="form form--stack">
120 <!-- This is just for visual reasons, since we need an additional border to begin the form if there is no error. -->
121 <fieldset class="form__fieldset six-col">
122 <div class="form__group">
123 <label for="relay_vlan" class="form__group-label two-col">Relay VLAN</label>
124 <div class="form__group-input three-col">
125 <select name="relay_vlan"
126 data-ng-model="vlanDetails.provideDHCPAction.relayVLAN"
127 data-ng-options="vlan as vlanDetails.getFullVLANName(vlan.id) for vlan in vlanDetails.vlans | ignoreSelf:vlanDetails.vlan">
128 <option value="" disabled="disabled" selected="selected">Choose relay VLAN</option>
129 </select>
130 </div>
131 </div>
132 <div class="form__group"
133 data-ng-hide="vlanDetails.relatedSubnets.length === 0 || (vlanDetails.provideDHCPAction.needsDynamicRange === false && vlanDetails.provideDHCPAction.needsGatewayIP === false)">
134 <label for="subnet" class="form__group-label two-col">Subnet</label>
135 <div class="form__group-input three-col">
136 <select name="subnet"
137 data-ng-model="vlanDetails.provideDHCPAction.subnet"
138 data-ng-change="vlanDetails.updateSubnet(true)"
139 data-ng-options="row.subnet.id as row.subnet.cidr for row in vlanDetails.relatedSubnets">
140 <option value="" disabled="disabled" selected="selected">Choose subnet</option>
141 <option value="" data-ng-if=""></option>
142 </select>
143 </div>
144 </div>
145 </fieldset>
146 <fieldset class="form__fieldset six-col last-col">
147 <div class="form__group"
148 data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0">
149 <label for="start-ip" class="form__group-label two-col">Dynamic range start IP</label>
150 <div class="form__group-input three-col">
151 <input type="text"
152 name="start-ip"
153 size="39"
154 data-ng-placeholder="vlanDetails.provideDHCPAction.startPlaceholder"
155 data-ng-model="vlanDetails.provideDHCPAction.startIP"
156 data-ng-disabled="!vlanDetails.provideDHCPAction.subnet"
157 data-ng-change="vlanDetails.updateStartIP()">
158 </div>
159 </div>
160 <div class="form__group" data-ng-hide="vlanDetails.provideDHCPAction.needsDynamicRange === false || vlanDetails.relatedSubnets.length === 0">
161 <label for="end-ip" class="form__group-label two-col">Dynamic range end IP</label>
162 <div class="form__group-input three-col">
163 <input type="text"
164 name="end-ip"
165 size="39"
166 data-ng-placeholder="vlanDetails.provideDHCPAction.endPlaceholder"
167 data-ng-model="vlanDetails.provideDHCPAction.endIP"
168 data-ng-disabled="!vlanDetails.provideDHCPAction.subnet"
169 data-ng-change="vlanDetails.updateEndIP()">
170 </div>
171 </div>
172 <div class="form__group"
173 data-ng-hide="vlanDetails.provideDHCPAction.needsGatewayIP === false || vlanDetails.provideDHCPAction.subnetMissingGatewayIP === false || vlanDetails.relatedSubnets.length === 0">
174 <label for="gateway-ip" class="form__group-label two-col">Gateway IP</label>
175 <div class="form__group-input three-col">
176 <input type="text"
177 name="gateway-ip"
178 size="39"
179 data-ng-placeholder="vlanDetails.provideDHCPAction.gatewayPlaceholder"
180 data-ng-model="vlanDetails.provideDHCPAction.gatewayIP"
181 data-ng-disabled="!vlanDetails.provideDHCPAction.subnet"
182 data-ng-change="vlanDetails.updatendIP()">
183 </div>
184 </div>
185 </fieldset>
186 </form>
187 </section>
188 <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-if="!vlanDetails.isActionError()">
118 <p class="page-header__message page-header__message--warning"189 <p class="page-header__message page-header__message--warning"
119 data-ng-show="vlanDetails.actionOption.name === 'disable_dhcp'">190 data-ng-if="vlanDetails.actionOption.name === 'disable_dhcp' && vlanDetails.vlan.dhcp_on">
120 Are you sure you want to disable DHCP on this VLAN? All subnets on this VLAN will be affected.191 Are you sure you want to disable DHCP on this VLAN? All subnets on this VLAN will be affected.
121 </p>192 </p>
193 <p class="page-header__message page-header__message--warning"
194 data-ng-if="vlanDetails.actionOption.name === 'disable_dhcp' && vlanDetails.vlan.relay_vlan">
195 Are you sure you want to disable DHCP relay on this VLAN? All subnets on this VLAN will be affected.
196 </p>
122 <p class="page-header__message page-header__message--error"197 <p class="page-header__message page-header__message--error"
123 data-ng-show="vlanDetails.actionOption.name === 'enable_dhcp' && vlanDetails.relatedSubnets.length === 0">198 data-ng-if="vlanDetails.actionOption.name === 'enable_dhcp' && vlanDetails.relatedSubnets.length === 0">
124 No subnets are available on this VLAN. DHCP cannot be enabled.199 No subnets are available on this VLAN. DHCP cannot be enabled.
125 </p>200 </p>
126 <p class="page-header__message page-header__message--warning"201 <p class="page-header__message page-header__message--warning"
127 data-ng-show="vlanDetails.actionOption.name === 'delete'">202 data-ng-if="vlanDetails.actionOption.name === 'delete'">
128 Are you sure you want to delete this VLAN?203 Are you sure you want to delete this VLAN?
129 </p>204 </p>
130 <div class="page-header__controls">205 <div class="page-header__controls">
131 <a href="" class="button--base button--inline" data-ng-click="vlanDetails.actionCancel()">Cancel</a>206 <a href="" class="button--base button--inline" data-ng-click="vlanDetails.actionCancel()">Cancel</a>
132 <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>207 <button class="button--primary button--inline" data-ng-click="vlanDetails.actionGo()" data-ng-disabled="!vlanDetails.canPerformAction()">{$ vlanDetails.actionOption.title $}</button>
133 </div>208 </div>
134 </section>209 </section>
135 <section class="page-header__section twelve-col u-margin--bottom-none ng-hide"210 <section class="page-header__section twelve-col u-margin--bottom-none"
136 data-ng-show="vlanDetails.isActionError()">211 data-ng-if="vlanDetails.isActionError()">
137 <p class="page-header__message page-header__message--error">{$ vlanDetails.actionError $}</p>212 <p class="page-header__message page-header__message--error">{$ vlanDetails.actionError $}</p>
138 <div class="page-header__controls">213 <div class="page-header__controls">
139 <a href="" class="button--base button--inline u-margin--right" data-ng-click="vlanDetails.actionCancel()">Cancel</a>214 <a href="" class="button--base button--inline u-margin--right" data-ng-click="vlanDetails.actionCancel()">Cancel</a>
@@ -166,17 +241,17 @@
166 <dd class="four-col last-col">241 <dd class="four-col last-col">
167 <a href="#/fabric/{$ vlanDetails.fabric.id $}">{$ vlanDetails.fabric.name $}</a>242 <a href="#/fabric/{$ vlanDetails.fabric.id $}">{$ vlanDetails.fabric.name $}</a>
168 </dd>243 </dd>
169 <dt class="two-col">DHCP</dt><dd class="four-col last-col">{$ vlanDetails.vlan.dhcp_on ? "Enabled" : "Disabled" $}</dd>244 <dt class="two-col">DHCP</dt><dd class="four-col last-col">{$ vlanDetails.getDHCPStatus() $}</dd>
170 <div data-ng-if="vlanDetails.vlan.external_dhcp">245 <div data-ng-if="vlanDetails.vlan.external_dhcp">
171 <dt class="two-col">External DHCP</dt>246 <dt class="two-col">External DHCP</dt>
172 <dd class="four-col last-col">{$ vlanDetails.vlan.external_dhcp $}247 <dd class="four-col last-col">{$ vlanDetails.vlan.external_dhcp $}
173 <i class="icon icon--warning tooltip" aria-label="Another DHCP server has been discovered on this VLAN. Enabling DHCP is not recommended."></i>248 <i class="icon icon--warning tooltip" aria-label="Another DHCP server has been discovered on this VLAN. Enabling DHCP is not recommended."></i>
174 </dd>249 </dd>
175 </div>250 </div>
176 <div class="ng-hide" data-ng-show="vlanDetails.primaryRack">251 <div data-ng-if="vlanDetails.primaryRack">
177 <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>252 <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>
178 </div>253 </div>
179 <div class="ng-hide" data-ng-show="vlanDetails.secondaryRack">254 <div data-ng-if="vlanDetails.secondaryRack">
180 <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>255 <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>
181 </div>256 </div>
182 <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>257 <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>
183258
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-11-29 08:19:43 +0000
+++ src/maasserver/testing/factory.py 2016-12-06 08:04:41 +0000
@@ -1022,7 +1022,7 @@
10221022
1023 def make_VLAN(1023 def make_VLAN(
1024 self, name=None, vid=None, fabric=None, dhcp_on=False,1024 self, name=None, vid=None, fabric=None, dhcp_on=False,
1025 primary_rack=None, secondary_rack=None):1025 primary_rack=None, secondary_rack=None, relay_vlan=None):
1026 assert vid != 0, "VID=0 VLANs are auto-created"1026 assert vid != 0, "VID=0 VLANs are auto-created"
1027 if fabric is None:1027 if fabric is None:
1028 fabric = Fabric.objects.get_default_fabric()1028 fabric = Fabric.objects.get_default_fabric()
@@ -1031,7 +1031,8 @@
1031 vid = self._get_available_vid(fabric)1031 vid = self._get_available_vid(fabric)
1032 vlan = VLAN(1032 vlan = VLAN(
1033 name=name, vid=vid, fabric=fabric, dhcp_on=dhcp_on,1033 name=name, vid=vid, fabric=fabric, dhcp_on=dhcp_on,
1034 primary_rack=primary_rack, secondary_rack=secondary_rack)1034 primary_rack=primary_rack, secondary_rack=secondary_rack,
1035 relay_vlan=relay_vlan)
1035 vlan.save()1036 vlan.save()
1036 return vlan1037 return vlan
10371038
10381039
=== modified file 'src/maasserver/tests/test_dhcp.py'
--- src/maasserver/tests/test_dhcp.py 2016-12-03 16:33:44 +0000
+++ src/maasserver/tests/test_dhcp.py 2016-12-06 08:04:41 +0000
@@ -17,7 +17,6 @@
17 IPADDRESS_TYPE,17 IPADDRESS_TYPE,
18 SERVICE_STATUS,18 SERVICE_STATUS,
19)19)
20from maasserver.exceptions import DHCPConfigurationError
21from maasserver.models import (20from maasserver.models import (
22 Config,21 Config,
23 DHCPSnippet,22 DHCPSnippet,
@@ -45,7 +44,6 @@
45from maastesting.twisted import (44from maastesting.twisted import (
46 always_fail_with,45 always_fail_with,
47 always_succeed_with,46 always_succeed_with,
48 TwistedLoggerFixture,
49)47)
50from netaddr import (48from netaddr import (
51 IPAddress,49 IPAddress,
@@ -422,8 +420,8 @@
422 rack_controller, vlan, subnet.get_ipnetwork().version))420 rack_controller, vlan, subnet.get_ipnetwork().version))
423421
424422
425class TestGetManagedVLANsFor(MAASServerTestCase):423class TestGenManagedVLANsFor(MAASServerTestCase):
426 """Tests for `get_managed_vlans_for`."""424 """Tests for `gen_managed_vlans_for`."""
427425
428 def test__returns_all_managed_vlans(self):426 def test__returns_all_managed_vlans(self):
429 rack_controller = factory.make_RackController()427 rack_controller = factory.make_RackController()
@@ -509,7 +507,31 @@
509 self.assertEquals({507 self.assertEquals({
510 vlan_one,508 vlan_one,
511 vlan_two,509 vlan_two,
512 }, dhcp.get_managed_vlans_for(rack_controller))510 }, set(dhcp.gen_managed_vlans_for(rack_controller)))
511
512 def test__returns_managed_vlan_with_relay_vlans(self):
513 rack_controller = factory.make_RackController()
514 vlan_one = factory.make_VLAN(
515 dhcp_on=True, primary_rack=rack_controller, name="1")
516 primary_interface = factory.make_Interface(
517 INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan_one)
518 managed_ipv4_subnet = factory.make_Subnet(
519 cidr=str(factory.make_ipv4_network().cidr), vlan=vlan_one)
520 factory.make_StaticIPAddress(
521 alloc_type=IPADDRESS_TYPE.STICKY, subnet=managed_ipv4_subnet,
522 interface=primary_interface)
523
524 # Relay VLANs atteched to the vlan.
525 relay_vlans = {
526 factory.make_VLAN(relay_vlan=vlan_one)
527 for _ in range(3)
528 }
529
530 # Should only contain the subnets that are managed by the rack
531 # controller and the best interface should have been selected.
532 self.assertEquals(
533 relay_vlans.union(set([vlan_one])),
534 set(dhcp.gen_managed_vlans_for(rack_controller)))
513535
514536
515class TestIPIsOnVLAN(MAASServerTestCase):537class TestIPIsOnVLAN(MAASServerTestCase):
@@ -1300,31 +1322,6 @@
1300class TestGetDHCPConfigureFor(MAASServerTestCase):1322class TestGetDHCPConfigureFor(MAASServerTestCase):
1301 """Tests for `get_dhcp_configure_for`."""1323 """Tests for `get_dhcp_configure_for`."""
13021324
1303 def test__raises_DHCPConfigurationError_for_ipv4(self):
1304 primary_rack = factory.make_RackController()
1305 secondary_rack = factory.make_RackController()
1306
1307 # VLAN for primary that has a secondary with multiple subnets.
1308 ha_vlan = factory.make_VLAN(
1309 dhcp_on=True, primary_rack=primary_rack,
1310 secondary_rack=secondary_rack)
1311 ha_subnet = factory.make_ipv4_Subnet_with_IPRanges(vlan=ha_vlan)
1312 factory.make_Interface(
1313 INTERFACE_TYPE.PHYSICAL, node=primary_rack, vlan=ha_vlan)
1314 secondary_interface = factory.make_Interface(
1315 INTERFACE_TYPE.PHYSICAL, node=secondary_rack, vlan=ha_vlan)
1316 factory.make_StaticIPAddress(
1317 alloc_type=IPADDRESS_TYPE.AUTO, subnet=ha_subnet,
1318 interface=secondary_interface)
1319 other_subnet = factory.make_ipv4_Subnet_with_IPRanges(vlan=ha_vlan)
1320
1321 ntp_servers = [factory.make_name("ntp")]
1322 default_domain = Domain.objects.get_default_domain()
1323 self.assertRaises(
1324 DHCPConfigurationError, dhcp.get_dhcp_configure_for,
1325 4, primary_rack, ha_vlan, [ha_subnet, other_subnet],
1326 ntp_servers, default_domain)
1327
1328 def test__returns_for_ipv4(self):1325 def test__returns_for_ipv4(self):
1329 primary_rack = factory.make_RackController()1326 primary_rack = factory.make_RackController()
1330 secondary_rack = factory.make_RackController()1327 secondary_rack = factory.make_RackController()
@@ -1423,36 +1420,6 @@
1423 dhcp.make_hosts_for_subnets([ha_subnet]), observed_hosts)1420 dhcp.make_hosts_for_subnets([ha_subnet]), observed_hosts)
1424 self.assertEqual(primary_interface.name, observed_interface)1421 self.assertEqual(primary_interface.name, observed_interface)
14251422
1426 def test__raises_DHCPConfigurationError_for_ipv6(self):
1427 primary_rack = factory.make_RackController()
1428 secondary_rack = factory.make_RackController()
1429
1430 # VLAN for primary that has a secondary with multiple subnets.
1431 ha_vlan = factory.make_VLAN(
1432 dhcp_on=True, primary_rack=primary_rack,
1433 secondary_rack=secondary_rack)
1434 ha_subnet = factory.make_Subnet(
1435 vlan=ha_vlan, cidr="fd38:c341:27da:c831::/64")
1436 factory.make_IPRange(
1437 ha_subnet, "fd38:c341:27da:c831:0:1::",
1438 "fd38:c341:27da:c831:0:1:ffff:0")
1439 factory.make_Interface(
1440 INTERFACE_TYPE.PHYSICAL, node=primary_rack, vlan=ha_vlan)
1441 secondary_interface = factory.make_Interface(
1442 INTERFACE_TYPE.PHYSICAL, node=secondary_rack, vlan=ha_vlan)
1443 factory.make_StaticIPAddress(
1444 alloc_type=IPADDRESS_TYPE.AUTO, subnet=ha_subnet,
1445 interface=secondary_interface)
1446 other_subnet = factory.make_Subnet(
1447 vlan=ha_vlan, cidr="fd38:c341:27da:c832::/64")
1448
1449 ntp_servers = [factory.make_name("ntp")]
1450 default_domain = Domain.objects.get_default_domain()
1451 self.assertRaises(
1452 DHCPConfigurationError, dhcp.get_dhcp_configure_for,
1453 6, primary_rack, ha_vlan, [ha_subnet, other_subnet],
1454 ntp_servers, default_domain)
1455
1456 def test__returns_for_ipv6(self):1423 def test__returns_for_ipv6(self):
1457 primary_rack = factory.make_RackController()1424 primary_rack = factory.make_RackController()
1458 secondary_rack = factory.make_RackController()1425 secondary_rack = factory.make_RackController()
@@ -1747,26 +1714,6 @@
17471714
1748 @wait_for_reactor1715 @wait_for_reactor
1749 @inlineCallbacks1716 @inlineCallbacks
1750 def test__logs_DHCPConfigurationError_ipv4(self):
1751 self.patch(dhcp.settings, "DHCP_CONNECT", True)
1752 with TwistedLoggerFixture() as logger:
1753 yield deferToDatabase(
1754 self.create_rack_controller, missing_ipv4=True)
1755 self.assertDocTestMatches(
1756 "...No IPv4 interface...", logger.output)
1757
1758 @wait_for_reactor
1759 @inlineCallbacks
1760 def test__logs_DHCPConfigurationError_ipv6(self):
1761 self.patch(dhcp.settings, "DHCP_CONNECT", True)
1762 with TwistedLoggerFixture() as logger:
1763 yield deferToDatabase(
1764 self.create_rack_controller, missing_ipv6=True)
1765 self.assertDocTestMatches(
1766 "...No IPv6 interface...", logger.output)
1767
1768 @wait_for_reactor
1769 @inlineCallbacks
1770 def test__doesnt_call_configure_for_both_ipv4_and_ipv6(self):1717 def test__doesnt_call_configure_for_both_ipv4_and_ipv6(self):
1771 # ... when DHCP_CONNECT is False.1718 # ... when DHCP_CONNECT is False.
1772 rack_controller, config = yield deferToDatabase(1719 rack_controller, config = yield deferToDatabase(
17731720
=== modified file 'src/maasserver/tests/test_forms_vlan.py'
--- src/maasserver/tests/test_forms_vlan.py 2016-04-27 20:38:06 +0000
+++ src/maasserver/tests/test_forms_vlan.py 2016-12-06 08:04:41 +0000
@@ -8,6 +8,7 @@
8import random8import random
99
10from maasserver.forms_vlan import VLANForm10from maasserver.forms_vlan import VLANForm
11from maasserver.models.fabric import Fabric
11from maasserver.models.vlan import DEFAULT_MTU12from maasserver.models.vlan import DEFAULT_MTU
12from maasserver.testing.factory import factory13from maasserver.testing.factory import factory
13from maasserver.testing.testcase import MAASServerTestCase14from maasserver.testing.testcase import MAASServerTestCase
@@ -27,6 +28,27 @@
27 ],28 ],
28 }, form.errors)29 }, form.errors)
2930
31 def test__vlans_already_using_relay_vlan_not_shown(self):
32 fabric = Fabric.objects.get_default_fabric()
33 relay_vlan = factory.make_VLAN()
34 factory.make_VLAN(relay_vlan=relay_vlan)
35 form = VLANForm(fabric=fabric, data={})
36 self.assertItemsEqual(
37 [fabric.get_default_vlan(), relay_vlan],
38 form.fields['relay_vlan'].queryset)
39
40 def test__self_vlan_not_used_in_relay_vlan_field(self):
41 fabric = Fabric.objects.get_default_fabric()
42 relay_vlan = fabric.get_default_vlan()
43 form = VLANForm(instance=relay_vlan, data={})
44 self.assertItemsEqual([], form.fields['relay_vlan'].queryset)
45
46 def test__no_relay_vlans_allowed_when_dhcp_on(self):
47 vlan = factory.make_VLAN(dhcp_on=True)
48 factory.make_VLAN()
49 form = VLANForm(instance=vlan, data={})
50 self.assertItemsEqual([], form.fields['relay_vlan'].queryset)
51
30 def test__creates_vlan(self):52 def test__creates_vlan(self):
31 fabric = factory.make_Fabric()53 fabric = factory.make_Fabric()
32 vlan_name = factory.make_name("vlan")54 vlan_name = factory.make_name("vlan")
@@ -211,6 +233,54 @@
211 vlan = reload_object(vlan)233 vlan = reload_object(vlan)
212 self.assertTrue(vlan.dhcp_on)234 self.assertTrue(vlan.dhcp_on)
213235
236 def test_update_sets_relay_vlan(self):
237 vlan = factory.make_VLAN()
238 relay_vlan = factory.make_VLAN()
239 form = VLANForm(instance=vlan, data={
240 "relay_vlan": relay_vlan.id,
241 })
242 self.assertTrue(form.is_valid(), form.errors)
243 form.save()
244 vlan = reload_object(vlan)
245 self.assertEquals(relay_vlan.id, vlan.relay_vlan.id)
246
247 def test_update_clears_relay_vlan_when_None(self):
248 relay_vlan = factory.make_VLAN()
249 vlan = factory.make_VLAN(relay_vlan=relay_vlan)
250 form = VLANForm(instance=vlan, data={
251 "relay_vlan": None,
252 })
253 self.assertTrue(form.is_valid(), form.errors)
254 form.save()
255 vlan = reload_object(vlan)
256 self.assertIsNone(vlan.relay_vlan)
257
258 def test_update_clears_relay_vlan_when_empty(self):
259 relay_vlan = factory.make_VLAN()
260 vlan = factory.make_VLAN(relay_vlan=relay_vlan)
261 form = VLANForm(instance=vlan, data={
262 "relay_vlan": "",
263 })
264 self.assertTrue(form.is_valid(), form.errors)
265 form.save()
266 vlan = reload_object(vlan)
267 self.assertIsNone(vlan.relay_vlan)
268
269 def test_update_disables_relay_vlan_when_dhcp_turned_on(self):
270 relay_vlan = factory.make_VLAN()
271 vlan = factory.make_VLAN(relay_vlan=relay_vlan)
272 factory.make_ipv4_Subnet_with_IPRanges(vlan=vlan)
273 rack = factory.make_RackController(vlan=vlan)
274 vlan.primary_rack = rack
275 vlan.save()
276 form = VLANForm(instance=reload_object(vlan), data={
277 "dhcp_on": "true",
278 })
279 self.assertTrue(form.is_valid(), form.errors)
280 form.save()
281 vlan = reload_object(vlan)
282 self.assertIsNone(vlan.relay_vlan)
283
214 def test_update_validates_primary_rack_with_dhcp_on(self):284 def test_update_validates_primary_rack_with_dhcp_on(self):
215 vlan = factory.make_VLAN()285 vlan = factory.make_VLAN()
216 form = VLANForm(instance=vlan, data={286 form = VLANForm(instance=vlan, data={
217287
=== modified file 'src/maasserver/triggers/system.py'
--- src/maasserver/triggers/system.py 2016-10-12 15:26:17 +0000
+++ src/maasserver/triggers/system.py 2016-12-06 08:04:41 +0000
@@ -268,6 +268,8 @@
268DHCP_VLAN_UPDATE = dedent("""\268DHCP_VLAN_UPDATE = dedent("""\
269 CREATE OR REPLACE FUNCTION sys_dhcp_vlan_update()269 CREATE OR REPLACE FUNCTION sys_dhcp_vlan_update()
270 RETURNS trigger as $$270 RETURNS trigger as $$
271 DECLARE
272 relay_vlan maasserver_vlan;
271 BEGIN273 BEGIN
272 -- DHCP was turned off.274 -- DHCP was turned off.
273 IF OLD.dhcp_on AND NOT NEW.dhcp_on THEN275 IF OLD.dhcp_on AND NOT NEW.dhcp_on THEN
@@ -303,6 +305,60 @@
303 PERFORM pg_notify(CONCAT('sys_dhcp_', NEW.secondary_rack_id), '');305 PERFORM pg_notify(CONCAT('sys_dhcp_', NEW.secondary_rack_id), '');
304 END IF;306 END IF;
305 END IF;307 END IF;
308
309 -- Relay VLAN was set when it was previously unset.
310 IF OLD.relay_vlan_id IS NULL AND NEW.relay_vlan_id IS NOT NULL THEN
311 SELECT maasserver_vlan.* INTO relay_vlan
312 FROM maasserver_vlan
313 WHERE maasserver_vlan.id = NEW.relay_vlan_id;
314 IF relay_vlan.primary_rack_id IS NOT NULL THEN
315 PERFORM pg_notify(
316 CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), '');
317 IF relay_vlan.secondary_rack_id IS NOT NULL THEN
318 PERFORM pg_notify(
319 CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), '');
320 END IF;
321 END IF;
322 -- Relay VLAN was unset when it was previously set.
323 ELSIF OLD.relay_vlan_id IS NOT NULL AND NEW.relay_vlan_id IS NULL THEN
324 SELECT maasserver_vlan.* INTO relay_vlan
325 FROM maasserver_vlan
326 WHERE maasserver_vlan.id = OLD.relay_vlan_id;
327 IF relay_vlan.primary_rack_id IS NOT NULL THEN
328 PERFORM pg_notify(
329 CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), '');
330 IF relay_vlan.secondary_rack_id IS NOT NULL THEN
331 PERFORM pg_notify(
332 CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), '');
333 END IF;
334 END IF;
335 -- Relay VLAN has changed on the VLAN.
336 ELSIF OLD.relay_vlan_id != NEW.relay_vlan_id THEN
337 -- Alert old VLAN if required.
338 SELECT maasserver_vlan.* INTO relay_vlan
339 FROM maasserver_vlan
340 WHERE maasserver_vlan.id = OLD.relay_vlan_id;
341 IF relay_vlan.primary_rack_id IS NOT NULL THEN
342 PERFORM pg_notify(
343 CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), '');
344 IF relay_vlan.secondary_rack_id IS NOT NULL THEN
345 PERFORM pg_notify(
346 CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), '');
347 END IF;
348 END IF;
349 -- Alert new VLAN if required.
350 SELECT maasserver_vlan.* INTO relay_vlan
351 FROM maasserver_vlan
352 WHERE maasserver_vlan.id = NEW.relay_vlan_id;
353 IF relay_vlan.primary_rack_id IS NOT NULL THEN
354 PERFORM pg_notify(
355 CONCAT('sys_dhcp_', relay_vlan.primary_rack_id), '');
356 IF relay_vlan.secondary_rack_id IS NOT NULL THEN
357 PERFORM pg_notify(
358 CONCAT('sys_dhcp_', relay_vlan.secondary_rack_id), '');
359 END IF;
360 END IF;
361 END IF;
306 RETURN NEW;362 RETURN NEW;
307 END;363 END;
308 $$ LANGUAGE plpgsql;364 $$ LANGUAGE plpgsql;
309365
=== modified file 'src/maasserver/triggers/tests/test_system_listener.py'
--- src/maasserver/triggers/tests/test_system_listener.py 2016-10-12 15:26:17 +0000
+++ src/maasserver/triggers/tests/test_system_listener.py 2016-12-06 08:04:41 +0000
@@ -806,6 +806,120 @@
806 finally:806 finally:
807 yield listener.stopService()807 yield listener.stopService()
808808
809 @wait_for_reactor
810 @inlineCallbacks
811 def test_sends_messages_when_relay_vlan_set(self):
812 yield deferToDatabase(register_system_triggers)
813 primary_rack = yield deferToDatabase(self.create_rack_controller)
814 secondary_rack = yield deferToDatabase(self.create_rack_controller)
815 relay_vlan = yield deferToDatabase(self.create_vlan, params={
816 "dhcp_on": True,
817 "primary_rack": primary_rack,
818 "secondary_rack": secondary_rack,
819 })
820 vlan = yield deferToDatabase(self.create_vlan)
821 primary_rack_dv = DeferredValue()
822 secondary_rack_dv = DeferredValue()
823 listener = self.make_listener_without_delay()
824 listener.register(
825 "sys_dhcp_%s" % primary_rack.id,
826 lambda *args: primary_rack_dv.set(args))
827 listener.register(
828 "sys_dhcp_%s" % secondary_rack.id,
829 lambda *args: secondary_rack_dv.set(args))
830 yield listener.startService()
831 try:
832 yield deferToDatabase(self.update_vlan, vlan.id, {
833 "relay_vlan": relay_vlan,
834 })
835 yield primary_rack_dv.get(timeout=2)
836 yield secondary_rack_dv.get(timeout=2)
837 finally:
838 yield listener.stopService()
839
840 @wait_for_reactor
841 @inlineCallbacks
842 def test_sends_messages_when_relay_vlan_unset(self):
843 yield deferToDatabase(register_system_triggers)
844 primary_rack = yield deferToDatabase(self.create_rack_controller)
845 secondary_rack = yield deferToDatabase(self.create_rack_controller)
846 relay_vlan = yield deferToDatabase(self.create_vlan, params={
847 "dhcp_on": True,
848 "primary_rack": primary_rack,
849 "secondary_rack": secondary_rack,
850 })
851 vlan = yield deferToDatabase(self.create_vlan, {
852 "relay_vlan": relay_vlan,
853 })
854 primary_rack_dv = DeferredValue()
855 secondary_rack_dv = DeferredValue()
856 listener = self.make_listener_without_delay()
857 listener.register(
858 "sys_dhcp_%s" % primary_rack.id,
859 lambda *args: primary_rack_dv.set(args))
860 listener.register(
861 "sys_dhcp_%s" % secondary_rack.id,
862 lambda *args: secondary_rack_dv.set(args))
863 yield listener.startService()
864 try:
865 yield deferToDatabase(self.update_vlan, vlan.id, {
866 "relay_vlan": None,
867 })
868 yield primary_rack_dv.get(timeout=2)
869 yield secondary_rack_dv.get(timeout=2)
870 finally:
871 yield listener.stopService()
872
873 @wait_for_reactor
874 @inlineCallbacks
875 def test_sends_messages_when_relay_vlan_changed(self):
876 yield deferToDatabase(register_system_triggers)
877 old_primary_rack = yield deferToDatabase(self.create_rack_controller)
878 old_secondary_rack = yield deferToDatabase(self.create_rack_controller)
879 old_relay_vlan = yield deferToDatabase(self.create_vlan, params={
880 "dhcp_on": True,
881 "primary_rack": old_primary_rack,
882 "secondary_rack": old_secondary_rack,
883 })
884 new_primary_rack = yield deferToDatabase(self.create_rack_controller)
885 new_secondary_rack = yield deferToDatabase(self.create_rack_controller)
886 new_relay_vlan = yield deferToDatabase(self.create_vlan, params={
887 "dhcp_on": True,
888 "primary_rack": new_primary_rack,
889 "secondary_rack": new_secondary_rack,
890 })
891 vlan = yield deferToDatabase(self.create_vlan, {
892 "relay_vlan": old_relay_vlan,
893 })
894 old_primary_rack_dv = DeferredValue()
895 old_secondary_rack_dv = DeferredValue()
896 new_primary_rack_dv = DeferredValue()
897 new_secondary_rack_dv = DeferredValue()
898 listener = self.make_listener_without_delay()
899 listener.register(
900 "sys_dhcp_%s" % old_primary_rack.id,
901 lambda *args: old_primary_rack_dv.set(args))
902 listener.register(
903 "sys_dhcp_%s" % old_secondary_rack.id,
904 lambda *args: old_secondary_rack_dv.set(args))
905 listener.register(
906 "sys_dhcp_%s" % new_primary_rack.id,
907 lambda *args: new_primary_rack_dv.set(args))
908 listener.register(
909 "sys_dhcp_%s" % new_secondary_rack.id,
910 lambda *args: new_secondary_rack_dv.set(args))
911 yield listener.startService()
912 try:
913 yield deferToDatabase(self.update_vlan, vlan.id, {
914 "relay_vlan": new_relay_vlan,
915 })
916 yield old_primary_rack_dv.get(timeout=2)
917 yield old_secondary_rack_dv.get(timeout=2)
918 yield new_primary_rack_dv.get(timeout=2)
919 yield new_secondary_rack_dv.get(timeout=2)
920 finally:
921 yield listener.stopService()
922
809923
810class TestDHCPSubnetListener(924class TestDHCPSubnetListener(
811 MAASTransactionServerTestCase, TransactionalHelpersMixin):925 MAASTransactionServerTestCase, TransactionalHelpersMixin):
812926
=== modified file 'src/maasserver/websockets/handlers/tests/test_vlan.py'
--- src/maasserver/websockets/handlers/tests/test_vlan.py 2016-10-19 18:06:01 +0000
+++ src/maasserver/websockets/handlers/tests/test_vlan.py 2016-12-06 08:04:41 +0000
@@ -42,6 +42,7 @@
42 "external_dhcp": vlan.external_dhcp,42 "external_dhcp": vlan.external_dhcp,
43 "primary_rack": vlan.primary_rack,43 "primary_rack": vlan.primary_rack,
44 "secondary_rack": vlan.secondary_rack,44 "secondary_rack": vlan.secondary_rack,
45 "relay_vlan": vlan.relay_vlan_id,
45 }46 }
46 data['rack_sids'] = sorted(list({47 data['rack_sids'] = sorted(list({
47 interface.node.system_id48 interface.node.system_id
@@ -206,6 +207,20 @@
206 self.assertThat(vlan.primary_rack, Is(None))207 self.assertThat(vlan.primary_rack, Is(None))
207 self.assertThat(vlan.secondary_rack, Is(None))208 self.assertThat(vlan.secondary_rack, Is(None))
208209
210 def test__configure_dhcp_with_relay_vlan(self):
211 user = factory.make_admin()
212 handler = VLANHandler(user, {})
213 vlan = factory.make_VLAN()
214 relay_vlan = factory.make_VLAN()
215 handler.configure_dhcp({
216 "id": vlan.id,
217 "controllers": [],
218 "relay_vlan": relay_vlan.id,
219 })
220 vlan = reload_object(vlan)
221 self.assertThat(vlan.dhcp_on, Equals(False))
222 self.assertThat(vlan.relay_vlan, Equals(relay_vlan))
223
209 def test__non_superuser_asserts(self):224 def test__non_superuser_asserts(self):
210 user = factory.make_User()225 user = factory.make_User()
211 handler = VLANHandler(user, {})226 handler = VLANHandler(user, {})
212227
=== modified file 'src/maasserver/websockets/handlers/vlan.py'
--- src/maasserver/websockets/handlers/vlan.py 2016-10-20 19:39:48 +0000
+++ src/maasserver/websockets/handlers/vlan.py 2016-12-06 08:04:41 +0000
@@ -104,6 +104,10 @@
104 NODE_PERMISSION.ADMIN, vlan), "Permission denied."104 NODE_PERMISSION.ADMIN, vlan), "Permission denied."
105 vlan.delete()105 vlan.delete()
106106
107 def update(self, parameters):
108 """Delete this VLAN."""
109 return super(VLANHandler, self).update(parameters)
110
107 def _configure_iprange_and_gateway(self, parameters):111 def _configure_iprange_and_gateway(self, parameters):
108 if 'subnet' in parameters and parameters['subnet'] is not None:112 if 'subnet' in parameters and parameters['subnet'] is not None:
109 subnet = Subnet.objects.get(id=parameters['subnet'])113 subnet = Subnet.objects.get(id=parameters['subnet'])
@@ -171,18 +175,21 @@
171 # of parameters, to prevent spurious log statements.175 # of parameters, to prevent spurious log statements.
172 if 'extra' in parameters:176 if 'extra' in parameters:
173 self._configure_iprange_and_gateway(parameters['extra'])177 self._configure_iprange_and_gateway(parameters['extra'])
174 iprange_count = IPRange.objects.filter(178 if 'relay_vlan' not in parameters:
175 type=IPRANGE_TYPE.DYNAMIC, subnet__vlan=vlan).count()179 iprange_count = IPRange.objects.filter(
176 if iprange_count == 0:180 type=IPRANGE_TYPE.DYNAMIC, subnet__vlan=vlan).count()
177 raise ValueError(181 if iprange_count == 0:
178 "Cannot configure DHCP: At least one dynamic range is "182 raise ValueError(
179 "required.")183 "Cannot configure DHCP: At least one dynamic range is "
184 "required.")
180 controllers = parameters.get('controllers', [])185 controllers = parameters.get('controllers', [])
181 data = {186 data = {
182 "dhcp_on": True if len(controllers) > 0 else False,187 "dhcp_on": True if len(controllers) > 0 else False,
183 "primary_rack": controllers[0] if len(controllers) > 0 else None,188 "primary_rack": controllers[0] if len(controllers) > 0 else None,
184 "secondary_rack": controllers[1] if len(controllers) > 1 else None,189 "secondary_rack": controllers[1] if len(controllers) > 1 else None,
185 }190 }
191 if 'relay_vlan' in parameters:
192 data['relay_vlan'] = parameters['relay_vlan']
186 form = VLANForm(instance=vlan, data=data)193 form = VLANForm(instance=vlan, data=data)
187 if form.is_valid():194 if form.is_valid():
188 form.save()195 form.save()