Merge lp:~mpontillo/maas/l2-spaces--constraints into lp:~maas-committers/maas/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
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.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

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. ;-)

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),