Merge ~mpontillo/maas:move-vlans-between-fabrics into maas:master

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: 7dd47edf9712531d4380fcfe361877e0dfc7383a
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:move-vlans-between-fabrics
Merge into: maas:master
Diff against target: 240 lines (+107/-13)
6 files modified
src/maasserver/forms/tests/test_vlan.py (+65/-0)
src/maasserver/forms/vlan.py (+5/-0)
src/maasserver/models/vlan.py (+16/-0)
src/maasserver/static/partials/vlan-details.html (+5/-8)
src/maasserver/stats.py (+6/-2)
src/maasserver/tests/test_stats.py (+10/-3)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
Review via email: mp+342522@code.launchpad.net

Commit message

LP: #1704501 - Allow users to change which Fabric a VLAN is on.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

lgtm +1

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
7dd47ed... by Mike Pontillo

Fix off-by-one random failures in maasserver.tests.test_stats:TestMAASStats.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/forms/tests/test_vlan.py b/src/maasserver/forms/tests/test_vlan.py
2index 8db5f5a..5c5aacb 100644
3--- a/src/maasserver/forms/tests/test_vlan.py
4+++ b/src/maasserver/forms/tests/test_vlan.py
5@@ -13,6 +13,11 @@ from maasserver.models.vlan import DEFAULT_MTU
6 from maasserver.testing.factory import factory
7 from maasserver.testing.testcase import MAASServerTestCase
8 from maasserver.utils.orm import reload_object
9+from testtools import ExpectedException
10+from testtools.matchers import (
11+ Equals,
12+ Not,
13+)
14
15
16 class TestVLANForm(MAASServerTestCase):
17@@ -371,3 +376,63 @@ class TestVLANForm(MAASServerTestCase):
18 self.assertEqual(secondary_rack, vlan.primary_rack)
19 self.assertEqual(None, vlan.secondary_rack)
20 self.assertTrue(vlan.dhcp_on)
21+
22+
23+class TestVLANFormFabricModification(MAASServerTestCase):
24+
25+ def test__cannot_move_vlan_with_overlapping_vid(self):
26+ fabric0 = Fabric.objects.get_default_fabric()
27+ fabric1 = factory.make_Fabric()
28+ fabric1_untagged = fabric1.get_default_vlan()
29+ form = VLANForm(instance=fabric1_untagged, data={
30+ "fabric": fabric0.id
31+ })
32+ is_valid = form.is_valid()
33+ self.assertThat(is_valid, Equals(False))
34+ self.assertThat(dict(form.errors), Equals(
35+ {'__all__': [
36+ 'A VLAN with the specified VID already '
37+ 'exists in the destination fabric.'
38+ ]}
39+ ))
40+ with ExpectedException(ValueError):
41+ form.save()
42+
43+ def test__allows_moving_vlan_to_new_fabric_if_vid_is_unique(self):
44+ fabric0 = Fabric.objects.get_default_fabric()
45+ fabric1 = factory.make_Fabric()
46+ fabric1_untagged = fabric1.get_default_vlan()
47+ form = VLANForm(instance=fabric1_untagged, data={
48+ "fabric": fabric0.id,
49+ "vid": 10
50+ })
51+ is_valid = form.is_valid()
52+ self.assertThat(is_valid, Equals(True))
53+ form.save()
54+
55+ def test__deletes_empty_fabrics(self):
56+ fabric0 = Fabric.objects.get_default_fabric()
57+ fabric1 = factory.make_Fabric()
58+ fabric1_untagged = fabric1.get_default_vlan()
59+ form = VLANForm(instance=fabric1_untagged, data={
60+ "fabric": fabric0.id,
61+ "vid": 10
62+ })
63+ is_valid = form.is_valid()
64+ self.assertThat(is_valid, Equals(True))
65+ form.save()
66+ self.assertThat(reload_object(fabric1), Equals(None))
67+
68+ def test__does_not_delete_non_empty_fabrics(self):
69+ fabric0 = Fabric.objects.get_default_fabric()
70+ fabric1 = factory.make_Fabric()
71+ factory.make_VLAN(fabric=fabric1)
72+ fabric1_untagged = fabric1.get_default_vlan()
73+ form = VLANForm(instance=fabric1_untagged, data={
74+ "fabric": fabric0.id,
75+ "vid": 10
76+ })
77+ is_valid = form.is_valid()
78+ form.save()
79+ self.assertThat(is_valid, Equals(True))
80+ self.assertThat(reload_object(fabric1), Not(Equals(None)))
81diff --git a/src/maasserver/forms/vlan.py b/src/maasserver/forms/vlan.py
82index 13fad27..89e5f4d 100644
83--- a/src/maasserver/forms/vlan.py
84+++ b/src/maasserver/forms/vlan.py
85@@ -15,6 +15,7 @@ from maasserver.fields import (
86 )
87 from maasserver.forms import MAASModelForm
88 from maasserver.models import (
89+ Fabric,
90 RackController,
91 Space,
92 )
93@@ -30,6 +31,9 @@ class VLANForm(MAASModelForm):
94 space = SpecifierOrModelChoiceField(
95 queryset=Space.objects.all(), required=False, empty_label="")
96
97+ fabric = SpecifierOrModelChoiceField(
98+ queryset=Fabric.objects.all(), required=False, empty_label="")
99+
100 class Meta:
101 model = VLAN
102 fields = (
103@@ -42,6 +46,7 @@ class VLANForm(MAASModelForm):
104 'secondary_rack',
105 'relay_vlan',
106 'space',
107+ 'fabric',
108 )
109
110 def __init__(self, *args, **kwargs):
111diff --git a/src/maasserver/models/vlan.py b/src/maasserver/models/vlan.py
112index e25658d..d520e16 100644
113--- a/src/maasserver/models/vlan.py
114+++ b/src/maasserver/models/vlan.py
115@@ -14,6 +14,7 @@ from django.db.models import (
116 BooleanField,
117 CASCADE,
118 CharField,
119+ Count,
120 deletion,
121 ForeignKey,
122 IntegerField,
123@@ -243,6 +244,15 @@ class VLAN(CleanSave, TimestampedModel):
124 subnet.vlan = self.fabric.get_default_vlan()
125 subnet.save()
126
127+ def unique_error_message(self, model_class, unique_check):
128+ if set(unique_check) == {'vid', 'fabric'}:
129+ return (
130+ 'A VLAN with the specified VID already exists in the '
131+ 'destination fabric.'
132+ )
133+ else:
134+ return super().unique_error_message(model_class, unique_check)
135+
136 def delete(self):
137 if self.is_fabric_default():
138 raise ValidationError(
139@@ -268,3 +278,9 @@ class VLAN(CleanSave, TimestampedModel):
140 "DHCP server is being used.",
141 ident="dhcp_disabled_all_vlans")
142 super().save(*args, **kwargs)
143+ # Circular dependencies.
144+ from maasserver.models import Fabric
145+ # Delete any now-empty fabrics.
146+ fabrics_with_vlan_count = Fabric.objects.annotate(
147+ vlan_count=Count("vlan"))
148+ fabrics_with_vlan_count.filter(vlan_count=0).delete()
149diff --git a/src/maasserver/static/partials/vlan-details.html b/src/maasserver/static/partials/vlan-details.html
150index 59ebb6e..8a2826a 100644
151--- a/src/maasserver/static/partials/vlan-details.html
152+++ b/src/maasserver/static/partials/vlan-details.html
153@@ -326,14 +326,8 @@
154 label-width="2" input-width="5" blur-on-enter="true"></maas-obj-field>
155 </div>
156 <div class="col-6">
157- <div class="p-form__group">
158- <div class="p-form__label">
159- <p><strong>Fabric</strong></p>
160- </div>
161- <div class="p-form__control">
162- <p><a href="#/fabric/{$ vlanDetails.fabric.id $}">{$ vlanDetails.fabric.name $}</a></p>
163- </div>
164- </div>
165+ <maas-obj-field type="options" key="fabric" label="Fabric"
166+ options="fabric.id as fabric.name for fabric in vlanDetails.fabrics"></maas-obj-field>
167 <div class="p-form__group" data-ng-if="vlanDetails.relatedControllers">
168 <div class="p-form__label">
169 <p><strong>Rack controllers</strong>
170@@ -351,6 +345,9 @@
171 </div>
172 </div>
173 <div class="row u-no-margin--top">
174+ <div class="col-8 u-vertically-center">
175+ <maas-obj-errors></maas-obj-errors>
176+ </div>
177 <div class="row u-align--right">
178 <button class="p-button--base" type="button"
179 data-ng-click="vlanDetails.exitEditSummary()">Cancel</button>
180diff --git a/src/maasserver/stats.py b/src/maasserver/stats.py
181index f6da73e..978af87 100644
182--- a/src/maasserver/stats.py
183+++ b/src/maasserver/stats.py
184@@ -30,16 +30,20 @@ from maasserver.utils import get_maas_user_agent
185 import requests
186
187
188-def get_maas_stats():
189+def get_machine_stats():
190 nodes = Node.objects.all()
191 machines = nodes.filter(node_type=NODE_TYPE.MACHINE)
192 # Rather overall amount of stats for machines.
193- stats = machines.aggregate(
194+ return machines.aggregate(
195 total_cpu=Sum('cpu_count'), total_mem=Sum('memory'),
196 total_storage=Sum('blockdevice__size'))
197+
198+
199+def get_maas_stats():
200 # Get all node types to get count values
201 node_types = Node.objects.values_list('node_type', flat=True)
202 node_types = Counter(node_types)
203+ stats = get_machine_stats()
204
205 return json.dumps({
206 "controllers": {
207diff --git a/src/maasserver/tests/test_stats.py b/src/maasserver/tests/test_stats.py
208index 68cc2b1..d0200f7 100644
209--- a/src/maasserver/tests/test_stats.py
210+++ b/src/maasserver/tests/test_stats.py
211@@ -13,6 +13,7 @@ from maasserver import stats
212 from maasserver.models import Config
213 from maasserver.stats import (
214 get_maas_stats,
215+ get_machine_stats,
216 get_request_params,
217 make_maas_user_agent_request,
218 )
219@@ -40,12 +41,18 @@ class TestMAASStats(MAASServerTestCase):
220 factory.make_RegionRackController()
221 factory.make_RegionController()
222 factory.make_RackController()
223- m1 = factory.make_Machine(cpu_count=2, memory=200)
224- m2 = factory.make_Machine(cpu_count=3, memory=100)
225+ factory.make_Machine(cpu_count=2, memory=200)
226+ factory.make_Machine(cpu_count=3, memory=100)
227 factory.make_Device()
228
229 stats = get_maas_stats()
230- total_storage = int((m1.storage + m2.storage) * 1000000)
231+ machine_stats = get_machine_stats()
232+
233+ # Due to floating point calculation subtleties, sometimes the value the
234+ # database returns is off by one compared to the value Python
235+ # calculates, so just get it directly from the database for the test.
236+ total_storage = machine_stats['total_storage']
237+
238 compare = {
239 "controllers": {
240 "regionracks": 1,

Subscribers

People subscribed via source and target branches