Merge lp:~mpontillo/maas/l2-spaces--constraints into lp:~maas-committers/maas/trunk
- l2-spaces--constraints
- Merge into trunk
Proposed by
Mike Pontillo
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5601 |
Proposed branch: | lp:~mpontillo/maas/l2-spaces--constraints |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~mpontillo/maas/l2-spaces--phase1 |
Diff against target: |
658 lines (+349/-134) 7 files modified
src/maasserver/models/interface.py (+10/-1) src/maasserver/models/tests/test_interface.py (+25/-1) src/maasserver/models/tests/test_vlan.py (+20/-0) src/maasserver/models/vlan.py (+10/-0) src/maasserver/node_constraint_filter_forms.py (+189/-130) src/maasserver/testing/factory.py (+4/-0) src/maasserver/tests/test_node_constraint_filter_forms.py (+91/-2) |
To merge this branch: | bzr merge lp:~mpontillo/maas/l2-spaces--constraints |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+312588@code.launchpad.net |
Commit message
Add machine acquisition constraint support for L2 spaces.
* Change specifier matching to allow a space match based on VLAN (in addition to subnet).
* Reduce complexity of `filter_nodes()` (in order to pass new lint checks).
* Fix clean_* methods on machine acquisition form to handle 'vlans' and 'not_vlans'.
* Add tests for 'vlans' and 'not_vlans' machine acquisition constraints.
* Add 'space' specifier support to VLAN model.
* Add tests for 'space' specified handling in VLAN model.
* Add tests for machine acquisition form errors for specifier-based queries.
Description of the change
To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote : | # |
Revision history for this message
Mike Pontillo (mpontillo) wrote : | # |
Fixed the above. This is ready for a final review.
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Thanks for the cleanup of the allocation, will be modifying that soon for composition!
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/models/interface.py' |
2 | --- src/maasserver/models/interface.py 2016-12-07 12:46:14 +0000 |
3 | +++ src/maasserver/models/interface.py 2016-12-08 16:07:44 +0000 |
4 | @@ -106,7 +106,7 @@ |
5 | 'name': '__name', |
6 | 'hostname': 'node__hostname', |
7 | 'subnet': (Subnet.objects, 'staticipaddress__interface'), |
8 | - 'space': 'subnet{s}space'.format(s=separator), |
9 | + 'space': self._add_space_query, |
10 | 'subnet_cidr': 'subnet{s}cidr'.format(s=separator), |
11 | 'type': '__type', |
12 | 'vlan': (VLAN.objects, 'interface'), |
13 | @@ -125,6 +125,15 @@ |
14 | else: |
15 | return op(current_q, Q(id=item)) |
16 | |
17 | + def _add_space_query(self, current_q, op, space): |
18 | + """Query for a related VLAN or subnet with the specified space.""" |
19 | + # Circular imports. |
20 | + from maasserver.models import Space |
21 | + space = Space.objects.get_object_by_specifiers_or_raise(space) |
22 | + current_q = op( |
23 | + current_q, Q(vlan__subnet__space=space) | Q(vlan__space=space)) |
24 | + return current_q |
25 | + |
26 | def _add_default_query(self, current_q, op, item): |
27 | # First, just try treating this as an interface ID. |
28 | try: |
29 | |
30 | === modified file 'src/maasserver/models/tests/test_interface.py' |
31 | --- src/maasserver/models/tests/test_interface.py 2016-12-07 12:46:14 +0000 |
32 | +++ src/maasserver/models/tests/test_interface.py 2016-12-08 16:07:44 +0000 |
33 | @@ -533,7 +533,7 @@ |
34 | ["subnet_cidr:%s" % subnet1.cidr, |
35 | "subnet_cidr:%s" % subnet2.cidr], ), [iface1, iface2]) |
36 | |
37 | - def test__filter_by_specifiers_matches_space(self): |
38 | + def test__filter_by_specifiers_matches_space_by_subnet(self): |
39 | space1 = factory.make_Space() |
40 | space2 = factory.make_Space() |
41 | subnet1 = factory.make_Subnet(space=space1) |
42 | @@ -555,6 +555,30 @@ |
43 | ["space:%s" % space1.name, |
44 | "space:%s" % space2.name], ), [iface1, iface2]) |
45 | |
46 | + def test__filter_by_specifiers_matches_space_by_vlan(self): |
47 | + space1 = factory.make_Space() |
48 | + space2 = factory.make_Space() |
49 | + vlan1 = factory.make_VLAN(space=space1) |
50 | + vlan2 = factory.make_VLAN(space=space2) |
51 | + subnet1 = factory.make_Subnet(vlan=vlan1) |
52 | + subnet2 = factory.make_Subnet(vlan=vlan2) |
53 | + node1 = factory.make_Node_with_Interface_on_Subnet( |
54 | + subnet=subnet1, with_dhcp_rack_primary=False) |
55 | + node2 = factory.make_Node_with_Interface_on_Subnet( |
56 | + subnet=subnet2, with_dhcp_rack_primary=False) |
57 | + iface1 = node1.get_boot_interface() |
58 | + iface2 = node2.get_boot_interface() |
59 | + self.assertItemsEqual( |
60 | + Interface.objects.filter_by_specifiers( |
61 | + "space:%s" % space1.name), [iface1]) |
62 | + self.assertItemsEqual( |
63 | + Interface.objects.filter_by_specifiers( |
64 | + "space:%s" % space2.name), [iface2]) |
65 | + self.assertItemsEqual( |
66 | + Interface.objects.filter_by_specifiers( |
67 | + ["space:%s" % space1.name, |
68 | + "space:%s" % space2.name], ), [iface1, iface2]) |
69 | + |
70 | def test__filter_by_specifiers_matches_type(self): |
71 | physical = factory.make_Interface() |
72 | bond = factory.make_Interface( |
73 | |
74 | === modified file 'src/maasserver/models/tests/test_vlan.py' |
75 | --- src/maasserver/models/tests/test_vlan.py 2016-12-07 12:46:14 +0000 |
76 | +++ src/maasserver/models/tests/test_vlan.py 2016-12-08 16:07:44 +0000 |
77 | @@ -65,6 +65,26 @@ |
78 | [vlan] |
79 | ) |
80 | |
81 | + def test__space_specifier_matches_space_by_name(self): |
82 | + factory.make_VLAN() |
83 | + space = factory.make_Space() |
84 | + vlan = factory.make_VLAN(space=space) |
85 | + factory.make_VLAN() |
86 | + self.assertItemsEqual( |
87 | + VLAN.objects.filter_by_specifiers('space:%s' % space.name), |
88 | + [vlan] |
89 | + ) |
90 | + |
91 | + def test__space_specifier_matches_space_by_id(self): |
92 | + factory.make_VLAN() |
93 | + space = factory.make_Space() |
94 | + vlan = factory.make_VLAN(space=space) |
95 | + factory.make_VLAN() |
96 | + self.assertItemsEqual( |
97 | + VLAN.objects.filter_by_specifiers('space:%s' % space.id), |
98 | + [vlan] |
99 | + ) |
100 | + |
101 | def test__class_specifier_matches_attached_subnet(self): |
102 | factory.make_VLAN() |
103 | vlan = factory.make_VLAN() |
104 | |
105 | === modified file 'src/maasserver/models/vlan.py' |
106 | --- src/maasserver/models/vlan.py 2016-12-07 16:40:35 +0000 |
107 | +++ src/maasserver/models/vlan.py 2016-12-08 16:07:44 +0000 |
108 | @@ -29,6 +29,7 @@ |
109 | from maasserver.models.interface import VLANInterface |
110 | from maasserver.models.timestampedmodel import TimestampedModel |
111 | from maasserver.utils.orm import MAASQueriesMixin |
112 | +from netaddr import AddrFormatError |
113 | from provisioningserver.utils.network import parse_integer |
114 | |
115 | |
116 | @@ -53,6 +54,7 @@ |
117 | # Circular imports. |
118 | from maasserver.models import ( |
119 | Fabric, |
120 | + Space, |
121 | Subnet, |
122 | ) |
123 | |
124 | @@ -65,6 +67,7 @@ |
125 | 'id': "__id", |
126 | 'name': "__name", |
127 | 'subnet': (Subnet.objects, 'vlan'), |
128 | + 'space': (Space.objects, 'vlan'), |
129 | 'vid': self._add_vid_query, |
130 | } |
131 | return super(VLANQueriesMixin, self).get_specifiers_q( |
132 | @@ -105,6 +108,13 @@ |
133 | current_q = op(current_q, Q(vid=vid)) |
134 | return current_q |
135 | |
136 | + def validate_filter_specifiers(self, specifiers): |
137 | + """Validate the given filter string.""" |
138 | + try: |
139 | + self.filter_by_specifiers(specifiers) |
140 | + except (ValueError, AddrFormatError) as e: |
141 | + raise ValidationError(e.message) |
142 | + |
143 | |
144 | class VLANQuerySet(QuerySet, VLANQueriesMixin): |
145 | """Custom QuerySet which mixes in some additional queries specific to |
146 | |
147 | === modified file 'src/maasserver/node_constraint_filter_forms.py' |
148 | --- src/maasserver/node_constraint_filter_forms.py 2016-11-02 23:46:43 +0000 |
149 | +++ src/maasserver/node_constraint_filter_forms.py 2016-12-08 16:07:44 +0000 |
150 | @@ -30,6 +30,7 @@ |
151 | PhysicalBlockDevice, |
152 | Subnet, |
153 | Tag, |
154 | + VLAN, |
155 | Zone, |
156 | ) |
157 | from maasserver.models.zone import ZONE_NAME_VALIDATOR |
158 | @@ -516,6 +517,22 @@ |
159 | "Invalid parameter: list of subnet specifiers required.", |
160 | }) |
161 | |
162 | + vlans = ValidatorMultipleChoiceField( |
163 | + validator=VLAN.objects.validate_filter_specifiers, |
164 | + label="Attached to VLANs", |
165 | + required=False, error_messages={ |
166 | + 'invalid_list': |
167 | + "Invalid parameter: list of VLAN specifiers required.", |
168 | + }) |
169 | + |
170 | + not_vlans = ValidatorMultipleChoiceField( |
171 | + validator=VLAN.objects.validate_filter_specifiers, |
172 | + label="Not attached to VLANs", |
173 | + required=False, error_messages={ |
174 | + 'invalid_list': |
175 | + "Invalid parameter: list of VLAN specifiers required.", |
176 | + }) |
177 | + |
178 | connected_to = ValidatorMultipleChoiceField( |
179 | validator=mac_validator, label="Connected to", required=False, |
180 | error_messages={ |
181 | @@ -616,21 +633,30 @@ |
182 | return None |
183 | return value |
184 | |
185 | - def _clean_subnet_specifiers(self, specifiers): |
186 | + def _clean_specifiers(self, model, specifiers): |
187 | if not specifiers: |
188 | return None |
189 | - subnets = set(Subnet.objects.filter_by_specifiers(specifiers)) |
190 | - if len(subnets) == 0: |
191 | - raise ValidationError("No matching subnets found.") |
192 | - return subnets |
193 | + objects = set(model.objects.filter_by_specifiers(specifiers)) |
194 | + if len(objects) == 0: |
195 | + raise ValidationError( |
196 | + "No matching %s found." % model._meta.verbose_name_plural) |
197 | + return objects |
198 | |
199 | def clean_subnets(self): |
200 | value = self.cleaned_data[self.get_field_name('subnets')] |
201 | - return self._clean_subnet_specifiers(value) |
202 | + return self._clean_specifiers(Subnet, value) |
203 | |
204 | def clean_not_subnets(self): |
205 | value = self.cleaned_data[self.get_field_name('not_subnets')] |
206 | - return self._clean_subnet_specifiers(value) |
207 | + return self._clean_specifiers(Subnet, value) |
208 | + |
209 | + def clean_vlans(self): |
210 | + value = self.cleaned_data[self.get_field_name('vlans')] |
211 | + return self._clean_specifiers(VLAN, value) |
212 | + |
213 | + def clean_not_vlans(self): |
214 | + value = self.cleaned_data[self.get_field_name('not_vlans')] |
215 | + return self._clean_specifiers(VLAN, value) |
216 | |
217 | def __init__(self, *args, **kwargs): |
218 | super(AcquireNodeForm, self).__init__(*args, **kwargs) |
219 | @@ -694,7 +720,161 @@ |
220 | :rtype: `django.db.models.query.QuerySet` |
221 | """ |
222 | filtered_nodes = nodes |
223 | - |
224 | + filtered_nodes = self.filter_by_hostname(filtered_nodes) |
225 | + filtered_nodes = self.filter_by_system_id(filtered_nodes) |
226 | + filtered_nodes = self.filter_by_arch(filtered_nodes) |
227 | + filtered_nodes = self.filter_by_cpu_count(filtered_nodes) |
228 | + filtered_nodes = self.filter_by_mem(filtered_nodes) |
229 | + filtered_nodes = self.filter_by_tags(filtered_nodes) |
230 | + filtered_nodes = self.filter_by_zone(filtered_nodes) |
231 | + filtered_nodes = self.filter_by_subnets(filtered_nodes) |
232 | + filtered_nodes = self.filter_by_vlans(filtered_nodes) |
233 | + filtered_nodes = self.filter_by_fabrics(filtered_nodes) |
234 | + filtered_nodes = self.filter_by_fabric_classes(filtered_nodes) |
235 | + compatible_nodes, filtered_nodes = self.filter_by_storage( |
236 | + filtered_nodes) |
237 | + compatible_interfaces, filtered_nodes = self.filter_by_interfaces( |
238 | + filtered_nodes) |
239 | + filtered_nodes = self.reorder_nodes_by_cost(filtered_nodes) |
240 | + return filtered_nodes, compatible_nodes, compatible_interfaces |
241 | + |
242 | + def reorder_nodes_by_cost(self, filtered_nodes): |
243 | + # This uses a very simple procedure to compute a machine's |
244 | + # cost. This procedure is loosely based on how ec2 computes |
245 | + # the costs of machines. This is here to give a hint to let |
246 | + # the call to acquire() decide which machine to return based |
247 | + # on the machine's cost when multiple machines match the |
248 | + # constraints. |
249 | + filtered_nodes = filtered_nodes.distinct().extra( |
250 | + select={'cost': "cpu_count + memory / 1024."}) |
251 | + return filtered_nodes.order_by("cost") |
252 | + |
253 | + def filter_by_interfaces(self, filtered_nodes): |
254 | + compatible_interfaces = {} |
255 | + interfaces_label_map = self.cleaned_data.get( |
256 | + self.get_field_name('interfaces')) |
257 | + if interfaces_label_map is not None: |
258 | + node_ids, compatible_interfaces = nodes_by_interface( |
259 | + interfaces_label_map) |
260 | + if node_ids is not None: |
261 | + filtered_nodes = filtered_nodes.filter(id__in=node_ids) |
262 | + |
263 | + return compatible_interfaces, filtered_nodes |
264 | + |
265 | + def filter_by_storage(self, filtered_nodes): |
266 | + compatible_nodes = {} # Maps node/storage to named storage constraints |
267 | + storage = self.cleaned_data.get( |
268 | + self.get_field_name('storage')) |
269 | + if storage: |
270 | + compatible_nodes = nodes_by_storage(storage) |
271 | + node_ids = list(compatible_nodes) |
272 | + if node_ids is not None: |
273 | + filtered_nodes = filtered_nodes.filter(id__in=node_ids) |
274 | + return compatible_nodes, filtered_nodes |
275 | + |
276 | + def filter_by_fabric_classes(self, filtered_nodes): |
277 | + fabric_classes = self.cleaned_data.get(self.get_field_name( |
278 | + 'fabric_classes')) |
279 | + if fabric_classes is not None and len(fabric_classes) > 0: |
280 | + filtered_nodes = filtered_nodes.filter( |
281 | + interface__vlan__fabric__class_type__in=fabric_classes) |
282 | + not_fabric_classes = self.cleaned_data.get(self.get_field_name( |
283 | + 'not_fabric_classes')) |
284 | + if not_fabric_classes is not None and len(not_fabric_classes) > 0: |
285 | + filtered_nodes = filtered_nodes.exclude( |
286 | + interface__vlan__fabric__class_type__in=not_fabric_classes) |
287 | + return filtered_nodes |
288 | + |
289 | + def filter_by_fabrics(self, filtered_nodes): |
290 | + fabrics = self.cleaned_data.get(self.get_field_name('fabrics')) |
291 | + if fabrics is not None and len(fabrics) > 0: |
292 | + # XXX mpontillo 2015-10-30 need to also handle fabrics whose name |
293 | + # is null (fabric-<id>). |
294 | + filtered_nodes = filtered_nodes.filter( |
295 | + interface__vlan__fabric__name__in=fabrics) |
296 | + not_fabrics = self.cleaned_data.get(self.get_field_name('not_fabrics')) |
297 | + if not_fabrics is not None and len(not_fabrics) > 0: |
298 | + # XXX mpontillo 2015-10-30 need to also handle fabrics whose name |
299 | + # is null (fabric-<id>). |
300 | + filtered_nodes = filtered_nodes.exclude( |
301 | + interface__vlan__fabric__name__in=not_fabrics) |
302 | + return filtered_nodes |
303 | + |
304 | + def filter_by_vlans(self, filtered_nodes): |
305 | + vlans = self.cleaned_data.get(self.get_field_name('vlans')) |
306 | + if vlans is not None and len(vlans) > 0: |
307 | + for vlan in set(vlans): |
308 | + filtered_nodes = filtered_nodes.filter( |
309 | + interface__vlan=vlan) |
310 | + not_vlans = self.cleaned_data.get(self.get_field_name('not_vlans')) |
311 | + if not_vlans is not None and len(not_vlans) > 0: |
312 | + for not_vlan in set(not_vlans): |
313 | + filtered_nodes = filtered_nodes.exclude( |
314 | + interface__vlan=not_vlan) |
315 | + return filtered_nodes |
316 | + |
317 | + def filter_by_subnets(self, filtered_nodes): |
318 | + subnets = self.cleaned_data.get(self.get_field_name('subnets')) |
319 | + if subnets is not None and len(subnets) > 0: |
320 | + for subnet in set(subnets): |
321 | + filtered_nodes = filtered_nodes.filter( |
322 | + interface__ip_addresses__subnet=subnet) |
323 | + not_subnets = self.cleaned_data.get( |
324 | + self.get_field_name('not_subnets')) |
325 | + if not_subnets is not None and len(not_subnets) > 0: |
326 | + for not_subnet in set(not_subnets): |
327 | + filtered_nodes = filtered_nodes.exclude( |
328 | + interface__ip_addresses__subnet=not_subnet) |
329 | + return filtered_nodes |
330 | + |
331 | + def filter_by_zone(self, filtered_nodes): |
332 | + zone = self.cleaned_data.get(self.get_field_name('zone')) |
333 | + if zone: |
334 | + zone_obj = Zone.objects.get(name=zone) |
335 | + filtered_nodes = filtered_nodes.filter(zone=zone_obj) |
336 | + not_in_zone = self.cleaned_data.get(self.get_field_name('not_in_zone')) |
337 | + if not_in_zone is not None and len(not_in_zone) > 0: |
338 | + not_in_zones = Zone.objects.filter(name__in=not_in_zone) |
339 | + filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) |
340 | + return filtered_nodes |
341 | + |
342 | + def filter_by_tags(self, filtered_nodes): |
343 | + tags = self.cleaned_data.get(self.get_field_name('tags')) |
344 | + if tags: |
345 | + for tag in tags: |
346 | + filtered_nodes = filtered_nodes.filter(tags__name=tag) |
347 | + not_tags = self.cleaned_data.get(self.get_field_name('not_tags')) |
348 | + if len(not_tags) > 0: |
349 | + for not_tag in not_tags: |
350 | + filtered_nodes = filtered_nodes.exclude(tags__name=not_tag) |
351 | + return filtered_nodes |
352 | + |
353 | + def filter_by_mem(self, filtered_nodes): |
354 | + mem = self.cleaned_data.get(self.get_field_name('mem')) |
355 | + if mem: |
356 | + filtered_nodes = filtered_nodes.filter(memory__gte=mem) |
357 | + return filtered_nodes |
358 | + |
359 | + def filter_by_cpu_count(self, filtered_nodes): |
360 | + cpu_count = self.cleaned_data.get(self.get_field_name('cpu_count')) |
361 | + if cpu_count: |
362 | + filtered_nodes = filtered_nodes.filter(cpu_count__gte=cpu_count) |
363 | + return filtered_nodes |
364 | + |
365 | + def filter_by_arch(self, filtered_nodes): |
366 | + arch = self.cleaned_data.get(self.get_field_name('arch')) |
367 | + if arch: |
368 | + filtered_nodes = filtered_nodes.filter(architecture__in=arch) |
369 | + return filtered_nodes |
370 | + |
371 | + def filter_by_system_id(self, filtered_nodes): |
372 | + # Filter by system_id. |
373 | + system_id = self.cleaned_data.get(self.get_field_name('system_id')) |
374 | + if system_id: |
375 | + filtered_nodes = filtered_nodes.filter(system_id=system_id) |
376 | + return filtered_nodes |
377 | + |
378 | + def filter_by_hostname(self, filtered_nodes): |
379 | # Filter by hostname. |
380 | hostname = self.cleaned_data.get(self.get_field_name('name')) |
381 | if hostname: |
382 | @@ -708,125 +888,4 @@ |
383 | else: |
384 | clause = Q(hostname=hostname) |
385 | filtered_nodes = filtered_nodes.filter(clause) |
386 | - |
387 | - # Filter by system_id. |
388 | - system_id = self.cleaned_data.get(self.get_field_name('system_id')) |
389 | - if system_id: |
390 | - filtered_nodes = filtered_nodes.filter(system_id=system_id) |
391 | - |
392 | - # Filter by architecture. |
393 | - arch = self.cleaned_data.get(self.get_field_name('arch')) |
394 | - if arch: |
395 | - filtered_nodes = filtered_nodes.filter(architecture__in=arch) |
396 | - |
397 | - # Filter by cpu_count. |
398 | - cpu_count = self.cleaned_data.get(self.get_field_name('cpu_count')) |
399 | - if cpu_count: |
400 | - filtered_nodes = filtered_nodes.filter(cpu_count__gte=cpu_count) |
401 | - |
402 | - # Filter by memory. |
403 | - mem = self.cleaned_data.get(self.get_field_name('mem')) |
404 | - if mem: |
405 | - filtered_nodes = filtered_nodes.filter(memory__gte=mem) |
406 | - |
407 | - # Filter by tags. |
408 | - tags = self.cleaned_data.get(self.get_field_name('tags')) |
409 | - if tags: |
410 | - for tag in tags: |
411 | - filtered_nodes = filtered_nodes.filter(tags__name=tag) |
412 | - |
413 | - # Filter by not_tags. |
414 | - not_tags = self.cleaned_data.get(self.get_field_name('not_tags')) |
415 | - if len(not_tags) > 0: |
416 | - for not_tag in not_tags: |
417 | - filtered_nodes = filtered_nodes.exclude(tags__name=not_tag) |
418 | - |
419 | - # Filter by zone. |
420 | - zone = self.cleaned_data.get(self.get_field_name('zone')) |
421 | - if zone: |
422 | - zone_obj = Zone.objects.get(name=zone) |
423 | - filtered_nodes = filtered_nodes.filter(zone=zone_obj) |
424 | - |
425 | - # Filter by not_in_zone. |
426 | - not_in_zone = self.cleaned_data.get(self.get_field_name('not_in_zone')) |
427 | - if not_in_zone is not None and len(not_in_zone) > 0: |
428 | - not_in_zones = Zone.objects.filter(name__in=not_in_zone) |
429 | - filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) |
430 | - |
431 | - # Filter by subnet. |
432 | - subnets = self.cleaned_data.get(self.get_field_name('subnets')) |
433 | - if subnets is not None and len(subnets) > 0: |
434 | - for subnet in set(subnets): |
435 | - filtered_nodes = filtered_nodes.filter( |
436 | - interface__ip_addresses__subnet=subnet) |
437 | - |
438 | - # Filter by not_subnets. |
439 | - not_subnets = self.cleaned_data.get( |
440 | - self.get_field_name('not_subnets')) |
441 | - if not_subnets is not None and len(not_subnets) > 0: |
442 | - for not_subnet in set(not_subnets): |
443 | - filtered_nodes = filtered_nodes.exclude( |
444 | - interface__ip_addresses__subnet=not_subnet) |
445 | - |
446 | - # Filter by fabrics. |
447 | - fabrics = self.cleaned_data.get(self.get_field_name('fabrics')) |
448 | - if fabrics is not None and len(fabrics) > 0: |
449 | - # XXX mpontillo 2015-10-30 need to also handle fabrics whose name |
450 | - # is null (fabric-<id>). |
451 | - filtered_nodes = filtered_nodes.filter( |
452 | - interface__vlan__fabric__name__in=fabrics) |
453 | - |
454 | - # Filter by not_fabrics. |
455 | - not_fabrics = self.cleaned_data.get(self.get_field_name('not_fabrics')) |
456 | - if not_fabrics is not None and len(not_fabrics) > 0: |
457 | - # XXX mpontillo 2015-10-30 need to also handle fabrics whose name |
458 | - # is null (fabric-<id>). |
459 | - filtered_nodes = filtered_nodes.exclude( |
460 | - interface__vlan__fabric__name__in=not_fabrics) |
461 | - |
462 | - # Filter by fabric classes. |
463 | - fabric_classes = self.cleaned_data.get(self.get_field_name( |
464 | - 'fabric_classes')) |
465 | - if fabric_classes is not None and len(fabric_classes) > 0: |
466 | - filtered_nodes = filtered_nodes.filter( |
467 | - interface__vlan__fabric__class_type__in=fabric_classes) |
468 | - |
469 | - # Filter by not_fabric_classes. |
470 | - not_fabric_classes = self.cleaned_data.get(self.get_field_name( |
471 | - 'not_fabric_classes')) |
472 | - if not_fabric_classes is not None and len(not_fabric_classes) > 0: |
473 | - filtered_nodes = filtered_nodes.exclude( |
474 | - interface__vlan__fabric__class_type__in=not_fabric_classes) |
475 | - |
476 | - # Filter by storage. |
477 | - compatible_nodes = {} # Maps node/storage to named storage constraints |
478 | - storage = self.cleaned_data.get( |
479 | - self.get_field_name('storage')) |
480 | - if storage: |
481 | - compatible_nodes = nodes_by_storage(storage) |
482 | - node_ids = list(compatible_nodes) |
483 | - if node_ids is not None: |
484 | - filtered_nodes = filtered_nodes.filter(id__in=node_ids) |
485 | - |
486 | - # Filter by interfaces (networking) |
487 | - compatible_interfaces = {} |
488 | - interfaces_label_map = self.cleaned_data.get( |
489 | - self.get_field_name('interfaces')) |
490 | - if interfaces_label_map is not None: |
491 | - node_ids, compatible_interfaces = nodes_by_interface( |
492 | - interfaces_label_map) |
493 | - if node_ids is not None: |
494 | - filtered_nodes = filtered_nodes.filter(id__in=node_ids) |
495 | - |
496 | - # This uses a very simple procedure to compute a machine's |
497 | - # cost. This procedure is loosely based on how ec2 computes |
498 | - # the costs of machines. This is here to give a hint to let |
499 | - # the call to acquire() decide which machine to return based |
500 | - # on the machine's cost when multiple machines match the |
501 | - # constraints. |
502 | - filtered_nodes = filtered_nodes.distinct().extra( |
503 | - select={'cost': "cpu_count + memory / 1024."}) |
504 | - return ( |
505 | - filtered_nodes.order_by("cost"), compatible_nodes, |
506 | - compatible_interfaces |
507 | - ) |
508 | + return filtered_nodes |
509 | |
510 | === modified file 'src/maasserver/testing/factory.py' |
511 | --- src/maasserver/testing/factory.py 2016-12-07 16:40:35 +0000 |
512 | +++ src/maasserver/testing/factory.py 2016-12-08 16:07:44 +0000 |
513 | @@ -1040,11 +1040,15 @@ |
514 | self, name=None, vid=None, fabric=None, dhcp_on=False, space=None, |
515 | primary_rack=None, secondary_rack=None, relay_vlan=None): |
516 | assert vid != 0, "VID=0 VLANs are auto-created" |
517 | + if name is RANDOM: |
518 | + name = factory.make_name() |
519 | if fabric is None: |
520 | fabric = Fabric.objects.get_default_fabric() |
521 | if vid is None: |
522 | # Don't create the vid=0 VLAN, it's auto-created. |
523 | vid = self._get_available_vid(fabric) |
524 | + if space is RANDOM: |
525 | + space = factory.make_Space() |
526 | vlan = VLAN( |
527 | name=name, vid=vid, fabric=fabric, dhcp_on=dhcp_on, space=space, |
528 | primary_rack=primary_rack, secondary_rack=secondary_rack, |
529 | |
530 | === modified file 'src/maasserver/tests/test_node_constraint_filter_forms.py' |
531 | --- src/maasserver/tests/test_node_constraint_filter_forms.py 2016-10-27 23:07:18 +0000 |
532 | +++ src/maasserver/tests/test_node_constraint_filter_forms.py 2016-12-08 16:07:44 +0000 |
533 | @@ -31,12 +31,16 @@ |
534 | RenamableFieldsForm, |
535 | ) |
536 | from maasserver.testing.architecture import patch_usable_architectures |
537 | -from maasserver.testing.factory import factory |
538 | +from maasserver.testing.factory import ( |
539 | + factory, |
540 | + RANDOM, |
541 | +) |
542 | from maasserver.testing.testcase import MAASServerTestCase |
543 | from maasserver.utils import ignore_unused |
544 | from testtools.matchers import ( |
545 | Contains, |
546 | ContainsAll, |
547 | + Equals, |
548 | MatchesDict, |
549 | MatchesListwise, |
550 | StartsWith, |
551 | @@ -356,6 +360,89 @@ |
552 | ] |
553 | }) |
554 | |
555 | + def test_vlans_filters_by_space(self): |
556 | + vlans = [ |
557 | + factory.make_VLAN(space=RANDOM) |
558 | + for _ in range(3) |
559 | + ] |
560 | + subnets = [ |
561 | + factory.make_Subnet(vlan=vlan) |
562 | + for vlan in vlans |
563 | + ] |
564 | + nodes = [ |
565 | + factory.make_Node_with_Interface_on_Subnet(subnet=subnet) |
566 | + for subnet in subnets |
567 | + ] |
568 | + # Filter for this subnet. Take one in the middle to avoid |
569 | + # coincidental success based on ordering. |
570 | + pick = 1 |
571 | + self.assertConstrainedNodes( |
572 | + {nodes[pick]}, |
573 | + {'vlans': ["space:%s" % vlans[pick].space.name]}) |
574 | + |
575 | + def test_vlans_filters_by_multiple_not_space_arguments(self): |
576 | + # Create 3 different VLANs (will be on 3 random spaces) |
577 | + vlans = [ |
578 | + factory.make_VLAN(space=RANDOM) |
579 | + for _ in range(3) |
580 | + ] |
581 | + subnets = [ |
582 | + factory.make_Subnet(vlan=vlan) |
583 | + for vlan in vlans |
584 | + ] |
585 | + nodes = [ |
586 | + factory.make_Node_with_Interface_on_Subnet(subnet=subnet) |
587 | + for subnet in subnets |
588 | + ] |
589 | + expected_selection = randint(0, len(vlans) - 1) |
590 | + expected_node = nodes[expected_selection] |
591 | + # Remove the expected subnet from the list of subnets; we'll use the |
592 | + # remaining subnets to filter the list. |
593 | + del vlans[expected_selection] |
594 | + self.assertConstrainedNodes( |
595 | + {expected_node}, |
596 | + { |
597 | + 'not_vlans': [ |
598 | + "space:%s" % vlan.space.name |
599 | + for vlan in vlans |
600 | + ] |
601 | + }) |
602 | + |
603 | + def test_fails_validation_for_no_matching_vlans(self): |
604 | + form = AcquireNodeForm(data={ |
605 | + 'vlans': ["space:foo"] |
606 | + }) |
607 | + self.assertThat(form.is_valid(), Equals(False)) |
608 | + self.assertThat( |
609 | + dict(form.errors)['vlans'], Equals(["No matching VLANs found."])) |
610 | + |
611 | + def test_fails_validation_for_no_matching_not_vlans(self): |
612 | + form = AcquireNodeForm(data={ |
613 | + 'not_vlans': ["space:foo"] |
614 | + }) |
615 | + self.assertThat(form.is_valid(), Equals(False)) |
616 | + self.assertThat( |
617 | + dict(form.errors)['not_vlans'], |
618 | + Equals(["No matching VLANs found."])) |
619 | + |
620 | + def test_fails_validation_for_no_matching_subnets(self): |
621 | + form = AcquireNodeForm(data={ |
622 | + 'subnets': ["foo"] |
623 | + }) |
624 | + self.assertThat(form.is_valid(), Equals(False)) |
625 | + self.assertThat( |
626 | + dict(form.errors)['subnets'], |
627 | + Equals(["No matching subnets found."])) |
628 | + |
629 | + def test_fails_validation_for_no_matching_not_subnets(self): |
630 | + form = AcquireNodeForm(data={ |
631 | + 'not_subnets': ["foo"] |
632 | + }) |
633 | + self.assertThat(form.is_valid(), Equals(False)) |
634 | + self.assertThat( |
635 | + dict(form.errors)['not_subnets'], |
636 | + Equals(["No matching subnets found."])) |
637 | + |
638 | def test_subnets_filters_by_ip(self): |
639 | subnets = [ |
640 | factory.make_Subnet() |
641 | @@ -1129,6 +1216,8 @@ |
642 | 'not_tags': [factory.make_Tag().name], |
643 | 'subnets': [factory.make_Subnet().name], |
644 | 'not_subnets': [factory.make_Subnet().name], |
645 | + 'vlans': ['name:' + factory.make_VLAN(name=RANDOM).name], |
646 | + 'not_vlans': ['name:' + factory.make_VLAN(name=RANDOM).name], |
647 | 'connected_to': [factory.make_mac_address()], |
648 | 'not_connected_to': [factory.make_mac_address()], |
649 | 'zone': factory.make_Zone(), |
650 | @@ -1158,7 +1247,7 @@ |
651 | |
652 | class TestAcquireNodeFormOrdersResults(MAASServerTestCase): |
653 | |
654 | - def test_describe_constraints_shows_all_constraints(self): |
655 | + def test_describe_constraints_orders_based_on_cost(self): |
656 | nodes = [ |
657 | factory.make_Node( |
658 | cpu_count=randint(5, 32), |
Needs fixing: I forgot to add the 'space' key to the VLAN specifiers, which is needed to do `vlan` and `not_vlan` constraints on machine acquisition (plus, need testing for that).
I'm not going to formally review this "Needs Fixing" as such, because I don't want others to assume it was already reviewed and not find other things. ;-)