Merge ~cgrabowski/maas:expanded_text_filter into maas:master
- Git
- lp:~cgrabowski/maas
- expanded_text_filter
- Merge into master
Status: | Merged |
---|---|
Approved by: | Christian Grabowski |
Approved revision: | 5c01b76229dc726321269b724e59fec9fc6ba7d4 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~cgrabowski/maas:expanded_text_filter |
Merge into: | maas:master |
Diff against target: |
677 lines (+544/-18) 3 files modified
src/maasserver/node_constraint_filter_forms.py (+302/-13) src/maasserver/tests/test_node_constraint_filter_forms.py (+238/-1) src/maasserver/websockets/handlers/node.py (+4/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Alexsander de Souza | Approve | ||
Review via email: mp+426332@code.launchpad.net |
Commit message
move websocket handler node constraint form to child class
substring match multiple substrings
add substring filter
Description of the change
Alexsander de Souza (alexsander-souza) wrote : | # |
Some testcases broke (see TestFilterNodeForm)
also a few nits inline
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: FAILED
LOG: http://
COMMIT: 221e8b8f94a6bf5
Christian Grabowski (cgrabowski) wrote : | # |
jenkins: !test
Christian Grabowski (cgrabowski) : | # |
Alexsander de Souza (alexsander-souza) wrote : | # |
+1
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: SUCCESS
COMMIT: 221e8b8f94a6bf5
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: SUCCESS
COMMIT: 5c01b76229dc726
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b expanded_
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b expanded_
STATUS: FAILED BUILD
LOG: http://
Preview Diff
1 | diff --git a/src/maasserver/node_constraint_filter_forms.py b/src/maasserver/node_constraint_filter_forms.py |
2 | index bfdee5e..1a4a765 100644 |
3 | --- a/src/maasserver/node_constraint_filter_forms.py |
4 | +++ b/src/maasserver/node_constraint_filter_forms.py |
5 | @@ -29,6 +29,7 @@ from maasserver.forms import ( |
6 | ) |
7 | from maasserver.models import ( |
8 | BlockDevice, |
9 | + BootResource, |
10 | Filesystem, |
11 | Interface, |
12 | Node, |
13 | @@ -876,6 +877,13 @@ class FilterNodeForm(RenamableFieldsForm): |
14 | ) |
15 | return None |
16 | |
17 | + def _set_clean_tags_error(self, tag_names, db_tag_names): |
18 | + unknown_tags = tag_names.difference(db_tag_names) |
19 | + error_msg = "No such tag(s): %s." % ", ".join( |
20 | + "'%s'" % tag for tag in unknown_tags |
21 | + ) |
22 | + set_form_error(self, self.get_field_name("tags"), error_msg) |
23 | + |
24 | def clean_tags(self): |
25 | value = self.cleaned_data[self.get_field_name("tags")] |
26 | if value: |
27 | @@ -888,22 +896,24 @@ class FilterNodeForm(RenamableFieldsForm): |
28 | ) |
29 | ) |
30 | if len(tag_names) != len(db_tag_names): |
31 | - unknown_tags = tag_names.difference(db_tag_names) |
32 | - error_msg = "No such tag(s): %s." % ", ".join( |
33 | - "'%s'" % tag for tag in unknown_tags |
34 | - ) |
35 | - set_form_error(self, self.get_field_name("tags"), error_msg) |
36 | + self._set_clean_tags_error(tag_names, db_tag_names) |
37 | return None |
38 | return tag_names |
39 | return None |
40 | |
41 | + def _set_zone_error(self, value, field): |
42 | + if type(value) == list: |
43 | + error_msg = "No such zone(s): %s." % ", ".join(value) |
44 | + else: |
45 | + error_msg = "No such zone: '%s'." % value |
46 | + set_form_error(self, self.get_field_name(field), error_msg) |
47 | + |
48 | def clean_zone(self): |
49 | value = self.cleaned_data[self.get_field_name("zone")] |
50 | if value: |
51 | nonexistent_names = detect_nonexistent_names(Zone, [value]) |
52 | if nonexistent_names: |
53 | - error_msg = "No such zone: '%s'." % value |
54 | - set_form_error(self, self.get_field_name("zone"), error_msg) |
55 | + self._set_zone_error(value, "zone") |
56 | return None |
57 | return value |
58 | return None |
59 | @@ -914,18 +924,23 @@ class FilterNodeForm(RenamableFieldsForm): |
60 | return None |
61 | nonexistent_names = detect_nonexistent_names(Zone, value) |
62 | if nonexistent_names: |
63 | - error_msg = "No such zone(s): %s." % ", ".join(nonexistent_names) |
64 | - set_form_error(self, self.get_field_name("not_in_zone"), error_msg) |
65 | + self._set_zone_error(nonexistent_names, "not_in_zone") |
66 | return None |
67 | return value |
68 | |
69 | + def _set_pool_error(self, value, field): |
70 | + if type(value) == list: |
71 | + error_msg = "No such pool(s): %s." % ", ".join(value) |
72 | + else: |
73 | + error_msg = "No such pool: '%s'." % value |
74 | + set_form_error(self, self.get_field_name(field), error_msg) |
75 | + |
76 | def clean_pool(self): |
77 | value = self.cleaned_data[self.get_field_name("pool")] |
78 | if value: |
79 | nonexistent_names = detect_nonexistent_names(ResourcePool, [value]) |
80 | if nonexistent_names: |
81 | - error_msg = "No such pool: '%s'." % value |
82 | - set_form_error(self, self.get_field_name("pool"), error_msg) |
83 | + self._set_pool_error(value, "pool") |
84 | return None |
85 | return value |
86 | return None |
87 | @@ -936,8 +951,7 @@ class FilterNodeForm(RenamableFieldsForm): |
88 | return None |
89 | nonexistent_names = detect_nonexistent_names(ResourcePool, value) |
90 | if nonexistent_names: |
91 | - error_msg = "No such pool(s): %s." % ", ".join(nonexistent_names) |
92 | - set_form_error(self, self.get_field_name("not_in_pool"), error_msg) |
93 | + self._set_pool_error(nonexistent_names, "not_in_pool") |
94 | return None |
95 | return value |
96 | |
97 | @@ -1407,3 +1421,278 @@ class ReadNodesForm(FilterNodeForm): |
98 | status_id = getattr(NODE_STATUS, status.upper()) |
99 | filtered_nodes = filtered_nodes.filter(status=status_id) |
100 | return filtered_nodes |
101 | + |
102 | + |
103 | +class FreeTextFilterNodeForm(ReadNodesForm): |
104 | + |
105 | + mac_address = UnconstrainedMultipleChoiceField( |
106 | + label="MAC addresses to filter on", |
107 | + required=False, |
108 | + ) |
109 | + |
110 | + def _substring_filter(self, queryset, field, substring, exclude=False): |
111 | + if type(substring) != str: # assume substring is a list of substrings |
112 | + query = Q() |
113 | + for substr in substring: |
114 | + substring_filter = {f"{field}__contains": substr} |
115 | + query = query | Q(**substring_filter) |
116 | + if exclude: |
117 | + return queryset.exclude(query) |
118 | + return queryset.filter(query) |
119 | + |
120 | + substring_filter = {f"{field}__contains": substring} |
121 | + if exclude: |
122 | + return queryset.exclude(**substring_filter) |
123 | + return queryset.filter(**substring_filter) |
124 | + |
125 | + def clean_tags(self): |
126 | + value = self.cleaned_data[self.get_field_name("tags")] |
127 | + if value: |
128 | + tag_names = parse_legacy_tags(value) |
129 | + # Validate tags. |
130 | + tag_names = set(tag_names) |
131 | + db_tag_names = set( |
132 | + self._substring_filter( |
133 | + Tag.objects, "name", tag_names |
134 | + ).values_list("name", flat=True) |
135 | + ) |
136 | + if len(tag_names) < len(db_tag_names): |
137 | + self._set_clean_tags_error(tag_names, db_tag_names) |
138 | + return None |
139 | + return db_tag_names |
140 | + |
141 | + def clean_arch(self): |
142 | + value = self.cleaned_data[self.get_field_name("arch")] |
143 | + if value: |
144 | + archs = self._substring_filter( |
145 | + BootResource.objects, "architecture", value |
146 | + ).values_list("architecture", flat=True) |
147 | + if not archs: |
148 | + set_form_error( |
149 | + self, |
150 | + self.get_field_name("arch"), |
151 | + "Architecture not recognised.", |
152 | + ) |
153 | + return None |
154 | + return archs |
155 | + |
156 | + def _clean_zones(self, value, field): |
157 | + zones = self._substring_filter(Zone.objects, "name", value) |
158 | + if not zones: |
159 | + self._set_zone_error(value + "*", field) |
160 | + return None |
161 | + return zones |
162 | + |
163 | + def clean_zone(self): |
164 | + value = self.cleaned_data[self.get_field_name("zone")] |
165 | + if value: |
166 | + return self._clean_zones(value, "zone") |
167 | + |
168 | + def clean_not_in_zone(self): |
169 | + value = self.cleaned_data[self.get_field_name("not_in_zone")] |
170 | + if value: |
171 | + return self._clean_zones(value, "not_in_zone") |
172 | + |
173 | + def filter_by_zone(self, filtered_nodes): |
174 | + zones = self.cleaned_data.get(self.get_field_name("zone")) |
175 | + if zones: |
176 | + filtered_nodes = filtered_nodes.filter(zone__in=zones) |
177 | + not_in_zones = self.cleaned_data.get( |
178 | + self.get_field_name("not_in_zone") |
179 | + ) |
180 | + if not_in_zones: |
181 | + filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) |
182 | + return filtered_nodes |
183 | + |
184 | + def _clean_pools(self, value, field): |
185 | + pool = self._substring_filter(ResourcePool.objects, "name", value) |
186 | + if not pool: |
187 | + self._set_pool_error(value + "*", field) |
188 | + return None |
189 | + return pool |
190 | + |
191 | + def clean_pool(self): |
192 | + value = self.cleaned_data[self.get_field_name("pool")] |
193 | + if value: |
194 | + return self._clean_pools(value, "pool") |
195 | + |
196 | + def clean_not_in_pool(self): |
197 | + value = self.cleaned_data[self.get_field_name("not_in_pool")] |
198 | + if value: |
199 | + return self._clean_pools(value, "not_in_pool") |
200 | + |
201 | + def filter_by_pool(self, filtered_nodes): |
202 | + pools = self.cleaned_data.get(self.get_field_name("pool")) |
203 | + if pools: |
204 | + filtered_nodes = filtered_nodes.filter(pool__in=pools) |
205 | + not_in_pools = self.cleaned_data.get( |
206 | + self.get_field_name("not_in_pool") |
207 | + ) |
208 | + if not_in_pools: |
209 | + filtered_nodes = filtered_nodes.exclude(pool__in=not_in_pools) |
210 | + return filtered_nodes |
211 | + |
212 | + def filter_by_pod_or_pod_type(self, filtered_nodes): |
213 | + pod_name = self.cleaned_data[self.get_field_name("pod")] |
214 | + not_pod_name = self.cleaned_data[self.get_field_name("not_pod")] |
215 | + pod_type = self.cleaned_data[self.get_field_name("pod_type")] |
216 | + not_pod_type = self.cleaned_data[self.get_field_name("not_pod_type")] |
217 | + if pod_name: |
218 | + filtered_nodes = self._substring_filter( |
219 | + filtered_nodes, "bmc__name", pod_name |
220 | + ) |
221 | + if not_pod_name: |
222 | + filtered_nodes = self._substring_filter( |
223 | + filtered_nodes, "bmc__name", not_pod_name, exclude=True |
224 | + ) |
225 | + if pod_type: |
226 | + filtered_nodes = self._substring_filter( |
227 | + filtered_nodes, "bmc__type", pod_type |
228 | + ) |
229 | + if not_pod_type: |
230 | + filtered_nodes = self._substring_filter( |
231 | + Pod.objects, "bmc__type", not_pod_type, exclude=True |
232 | + ) |
233 | + return filtered_nodes |
234 | + |
235 | + def filter_by_fabric_classes(self, filtered_nodes): |
236 | + fabric_classes = self.cleaned_data.get( |
237 | + self.get_field_name("fabric_classes") |
238 | + ) |
239 | + if fabric_classes: |
240 | + filtered_nodes = self._substring_filter( |
241 | + filtered_nodes, |
242 | + "current_config__interface__vlan__fabric__class_type", |
243 | + fabric_classes, |
244 | + ) |
245 | + not_fabric_classes = self.cleaned_data.get( |
246 | + self.get_field_name("not_fabric_classes") |
247 | + ) |
248 | + if not_fabric_classes: |
249 | + filtered_nodes = self._substring_filter( |
250 | + filtered_nodes, |
251 | + "current_config__interface__vlan__fabric__class_type", |
252 | + not_fabric_classes, |
253 | + exclude=True, |
254 | + ) |
255 | + return filtered_nodes |
256 | + |
257 | + def filter_by_fabrics(self, filtered_nodes): |
258 | + fabrics = self.cleaned_data.get(self.get_field_name("fabrics")) |
259 | + if fabrics: |
260 | + filtered_nodes = self._substring_filter( |
261 | + filtered_nodes, |
262 | + "current_config__interface__vlan__fabric__name", |
263 | + fabrics, |
264 | + ) |
265 | + not_fabrics = self.cleaned_data.get(self.get_field_name("not_fabrics")) |
266 | + if not_fabrics: |
267 | + filtered_nodes = self._substring_filter( |
268 | + filtered_nodes, |
269 | + "current_config__interface__vlan__fabric__name", |
270 | + not_fabrics, |
271 | + exclude=True, |
272 | + ) |
273 | + return filtered_nodes |
274 | + |
275 | + def clean_vlans(self): |
276 | + value = self.cleaned_data.get(self.get_field_name("vlans")) |
277 | + if value: |
278 | + vlans = self._substring_filter(VLAN.objects, "name", value) |
279 | + if not vlans: |
280 | + set_form_error( |
281 | + self, |
282 | + self.get_field_name("vlans"), |
283 | + "no vlan found for %s" % value, |
284 | + ) |
285 | + return None |
286 | + return value |
287 | + |
288 | + def filter_by_vlans(self, filtered_nodes): |
289 | + vlans = self.cleaned_data.get(self.get_field_name("vlans")) |
290 | + if vlans: |
291 | + filtered_nodes = self._substring_filter( |
292 | + filtered_nodes, "current_config__interface__vlan__name", vlans |
293 | + ) |
294 | + not_vlans = self.cleaned_data.get(self.get_field_name("not_vlans")) |
295 | + if not_vlans: |
296 | + filtered_nodes = self._substring_filter( |
297 | + filtered_nodes, |
298 | + "current_config__interface__vlan__name", |
299 | + vlans, |
300 | + exclude=True, |
301 | + ) |
302 | + return filtered_nodes |
303 | + |
304 | + def clean_subnets(self): |
305 | + value = self.cleaned_data.get(self.get_field_name("subnets")) |
306 | + if value: |
307 | + subnets = self._substring_filter(Subnet.objects, "cidr", value) |
308 | + if not subnets: |
309 | + set_form_error( |
310 | + self, |
311 | + self.get_field_name("subnets"), |
312 | + "no subnet found for %s" % value, |
313 | + ) |
314 | + return None |
315 | + return value |
316 | + |
317 | + def filter_by_subnets(self, filtered_nodes): |
318 | + subnets = self.cleaned_data.get(self.get_field_name("subnets")) |
319 | + if subnets: |
320 | + filtered_nodes = self._substring_filter( |
321 | + filtered_nodes, |
322 | + "current_config__interface__ip_addresses__subnet__cidr", |
323 | + subnets, |
324 | + ) |
325 | + not_subnets = self.cleaned_data.get(self.get_field_name("not_subnets")) |
326 | + if not_subnets: |
327 | + filtered_nodes = self._substring_filter( |
328 | + filtered_nodes, |
329 | + "current_config__interface__ip_addresses__subnet__cidr", |
330 | + not_subnets, |
331 | + exclude=True, |
332 | + ) |
333 | + return filtered_nodes |
334 | + |
335 | + def filter_by_hostnames(self, filtered_nodes): |
336 | + hostnames = self.cleaned_data.get(self.get_field_name("hostname")) |
337 | + if hostnames: |
338 | + filtered_nodes = self._substring_filter( |
339 | + filtered_nodes, "hostname", hostnames |
340 | + ) |
341 | + return filtered_nodes |
342 | + |
343 | + def clean_mac_address(self): |
344 | + value = self.cleaned_data.get(self.get_field_name("mac_address")) |
345 | + if value: |
346 | + interfaces = self._substring_filter( |
347 | + Interface.objects, "mac_address", value |
348 | + ) |
349 | + if not interfaces: |
350 | + set_form_error( |
351 | + self, |
352 | + self.get_field_name("mac_address"), |
353 | + "no mac address found for %s" % value, |
354 | + ) |
355 | + return None |
356 | + return value |
357 | + |
358 | + def filter_by_mac_addresses(self, filtered_nodes): |
359 | + value = self.cleaned_data.get(self.get_field_name("mac_address")) |
360 | + if value: |
361 | + interfaces = self._substring_filter( |
362 | + Interface.objects, "mac_address", value |
363 | + ) |
364 | + filtered_nodes = filtered_nodes.filter( |
365 | + current_config__interface__in=interfaces |
366 | + ) |
367 | + return filtered_nodes |
368 | + |
369 | + def filter_by_agent_name(self, filtered_nodes): |
370 | + agent_name = self.cleaned_data[self.get_field_name("agent_name")] |
371 | + if agent_name: |
372 | + filtered_nodes = self._substring_filter( |
373 | + filtered_nodes, "agent_name", agent_name |
374 | + ) |
375 | + return filtered_nodes |
376 | diff --git a/src/maasserver/tests/test_node_constraint_filter_forms.py b/src/maasserver/tests/test_node_constraint_filter_forms.py |
377 | index 9df891b..df67c69 100644 |
378 | --- a/src/maasserver/tests/test_node_constraint_filter_forms.py |
379 | +++ b/src/maasserver/tests/test_node_constraint_filter_forms.py |
380 | @@ -24,11 +24,12 @@ from maasserver.enum import ( |
381 | NODE_STATUS, |
382 | POWER_STATE, |
383 | ) |
384 | -from maasserver.models import Domain, Machine, NodeDevice, Zone |
385 | +from maasserver.models import Domain, Machine, NodeDevice, Tag, Zone |
386 | from maasserver.node_constraint_filter_forms import ( |
387 | AcquireNodeForm, |
388 | detect_nonexistent_names, |
389 | FilterNodeForm, |
390 | + FreeTextFilterNodeForm, |
391 | generate_architecture_wildcards, |
392 | get_architecture_wildcards, |
393 | get_storage_constraints_from_string, |
394 | @@ -1592,6 +1593,242 @@ class TestFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin): |
395 | self.assertEqual(constraints.keys(), described_constraints) |
396 | |
397 | |
398 | +class TestFreeTextFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin): |
399 | + def test_substring_filter_one_substring(self): |
400 | + name = factory.make_name("hostname") |
401 | + node1 = factory.make_Node(hostname=name) |
402 | + node2 = factory.make_Node() |
403 | + form = FreeTextFilterNodeForm(data={}) |
404 | + result = form._substring_filter( |
405 | + Machine.objects, "hostname", name[0 : len("hostname-") + 1] |
406 | + ) |
407 | + self.assertIn(node1, list(result)) |
408 | + self.assertNotIn(node2, list(result)) |
409 | + |
410 | + def test_substring_filter_list_of_substrings(self): |
411 | + tags = [ |
412 | + factory.make_Tag(name=factory.make_name("tag")) for _ in range(3) |
413 | + ] |
414 | + form = FreeTextFilterNodeForm(data={}) |
415 | + # +1 to include first letter in random suffix |
416 | + result = form._substring_filter( |
417 | + Tag.objects, |
418 | + "name", |
419 | + [tag.name[0 : len("tag-") + 1] for tag in tags], |
420 | + ) |
421 | + self.assertCountEqual(tags, list(result)) |
422 | + |
423 | + def test_substring_filter_exact_match(self): |
424 | + name = factory.make_name("hostname") |
425 | + node1 = factory.make_Node(hostname=name) |
426 | + node2 = factory.make_Node() |
427 | + form = FreeTextFilterNodeForm(data={}) |
428 | + result = form._substring_filter(Machine.objects, "hostname", name) |
429 | + self.assertIn(node1, list(result)) |
430 | + self.assertNotIn(node2, list(result)) |
431 | + |
432 | + def test_substring_arch_filter(self): |
433 | + architecture = factory.make_name("arch") |
434 | + subarch = factory.make_name() |
435 | + arch = "/".join([architecture, subarch]) |
436 | + factory.make_usable_boot_resource(architecture=arch) |
437 | + node1 = factory.make_Node(architecture=arch) |
438 | + node2 = factory.make_Node() |
439 | + constraints = { |
440 | + "arch": arch[:2], |
441 | + } |
442 | + form = FreeTextFilterNodeForm(data=constraints) |
443 | + self.assertTrue(form.is_valid()) |
444 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
445 | + self.assertIn(node1, filtered_nodes) |
446 | + self.assertNotIn(node2, filtered_nodes) |
447 | + |
448 | + def test_substring_tag_filter(self): |
449 | + tags = [ |
450 | + factory.make_Tag(name=factory.make_name("tag")) for _ in range(3) |
451 | + ] |
452 | + node1 = factory.make_Node() |
453 | + node2 = factory.make_Node() |
454 | + [node1.tags.add(tag) for tag in tags] |
455 | + constraints = { |
456 | + "tags": [tag.name[: len("tag-") + 1] for tag in tags], |
457 | + } |
458 | + form = FreeTextFilterNodeForm(data=constraints) |
459 | + self.assertTrue(form.is_valid()) |
460 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
461 | + self.assertIn(node1, filtered_nodes) |
462 | + self.assertNotIn(node2, filtered_nodes) |
463 | + |
464 | + def test_substring_zone_filter(self): |
465 | + zone = factory.make_Zone() |
466 | + node1 = factory.make_Node(zone=zone) |
467 | + node2 = factory.make_Node() |
468 | + constraints = { |
469 | + "zone": zone.name[:2], |
470 | + } |
471 | + form = FreeTextFilterNodeForm(data=constraints) |
472 | + self.assertTrue(form.is_valid()) |
473 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
474 | + self.assertIn(node1, filtered_nodes) |
475 | + self.assertNotIn(node2, filtered_nodes) |
476 | + |
477 | + def test_substring_not_in_zone_filter(self): |
478 | + zone = factory.make_Zone() |
479 | + node1 = factory.make_Node(zone=zone) |
480 | + node2 = factory.make_Node() |
481 | + constraints = { |
482 | + "not_in_zone": [zone.name[:2]], |
483 | + } |
484 | + form = FreeTextFilterNodeForm(data=constraints) |
485 | + self.assertTrue(form.is_valid()) |
486 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
487 | + self.assertNotIn(node1, filtered_nodes) |
488 | + self.assertIn(node2, filtered_nodes) |
489 | + |
490 | + def test_substring_pool_filter(self): |
491 | + pool = factory.make_ResourcePool() |
492 | + node1 = factory.make_Node(pool=pool) |
493 | + node2 = factory.make_Node() |
494 | + constraints = { |
495 | + "pool": pool.name[: len("resourcepool-") + 1], |
496 | + } |
497 | + form = FreeTextFilterNodeForm(data=constraints) |
498 | + self.assertTrue(form.is_valid()) |
499 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
500 | + self.assertIn(node1, filtered_nodes) |
501 | + self.assertNotIn(node2, filtered_nodes) |
502 | + |
503 | + def test_substring_not_in_pool_filter(self): |
504 | + pool = factory.make_ResourcePool() |
505 | + node1 = factory.make_Node(pool=pool) |
506 | + node2 = factory.make_Node() |
507 | + constraints = { |
508 | + "not_in_pool": [pool.name[: len("resourcepool-") + 1]], |
509 | + } |
510 | + form = FreeTextFilterNodeForm(data=constraints) |
511 | + self.assertTrue(form.is_valid()) |
512 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
513 | + self.assertNotIn(node1, filtered_nodes) |
514 | + self.assertIn(node2, filtered_nodes) |
515 | + |
516 | + def test_substring_pod_filter(self): |
517 | + pod = factory.make_Pod() |
518 | + node1 = factory.make_Node(bmc=pod.as_bmc()) |
519 | + node2 = factory.make_Node() |
520 | + constraints = {"pod": pod.name[: len("pod-") + 1]} |
521 | + form = FreeTextFilterNodeForm(data=constraints) |
522 | + self.assertTrue(form.is_valid()) |
523 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
524 | + self.assertIn(node1, filtered_nodes) |
525 | + self.assertNotIn(node2, filtered_nodes) |
526 | + |
527 | + def test_substring_fabrics_filter(self): |
528 | + fabric = factory.make_Fabric() |
529 | + vlan = factory.make_VLAN(fabric=fabric) |
530 | + interface = factory.make_Interface(vlan=vlan) |
531 | + node1 = factory.make_Node() |
532 | + node1.current_config.interface_set.add(interface) |
533 | + node2 = factory.make_Node() |
534 | + constraints = { |
535 | + "fabrics": [fabric.name[:2]], |
536 | + } |
537 | + form = FreeTextFilterNodeForm(data=constraints) |
538 | + self.assertTrue(form.is_valid()) |
539 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
540 | + self.assertIn(node1, filtered_nodes) |
541 | + self.assertNotIn(node2, filtered_nodes) |
542 | + |
543 | + def test_substring_fabric_classes_filter(self): |
544 | + fabric_class = factory.make_name() |
545 | + fabric = factory.make_Fabric(class_type=fabric_class) |
546 | + vlan = factory.make_VLAN(fabric=fabric) |
547 | + interface = factory.make_Interface(vlan=vlan) |
548 | + node1 = factory.make_Node() |
549 | + node1.current_config.interface_set.add(interface) |
550 | + node2 = factory.make_Node() |
551 | + constraints = { |
552 | + "fabric_classes": [fabric_class[:2]], |
553 | + } |
554 | + form = FreeTextFilterNodeForm(data=constraints) |
555 | + form.is_valid() |
556 | + print(form.errors) |
557 | + self.assertTrue(form.is_valid()) |
558 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
559 | + self.assertIn(node1, filtered_nodes) |
560 | + self.assertNotIn(node2, filtered_nodes) |
561 | + |
562 | + def test_substring_vlans_filter(self): |
563 | + vlan = factory.make_VLAN(name=factory.make_name()) |
564 | + interface = factory.make_Interface(vlan=vlan) |
565 | + node1 = factory.make_Node() |
566 | + node1.current_config.interface_set.add(interface) |
567 | + node2 = factory.make_Node() |
568 | + constraints = { |
569 | + "vlans": [vlan.name[:2]], |
570 | + } |
571 | + form = FreeTextFilterNodeForm(data=constraints) |
572 | + self.assertTrue(form.is_valid()) |
573 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
574 | + self.assertIn(node1, filtered_nodes) |
575 | + self.assertNotIn(node2, filtered_nodes) |
576 | + |
577 | + def test_substring_subnet_filter(self): |
578 | + subnet = factory.make_Subnet() |
579 | + interface = factory.make_Interface(subnet=subnet) |
580 | + node1 = factory.make_Node() |
581 | + node1.current_config.interface_set.add(interface) |
582 | + node2 = factory.make_Node() |
583 | + constraints = { |
584 | + "subnets": [str(subnet.cidr)[:3]], |
585 | + } |
586 | + form = FreeTextFilterNodeForm(data=constraints) |
587 | + self.assertTrue(form.is_valid()) |
588 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
589 | + self.assertIn(node1, filtered_nodes) |
590 | + self.assertNotIn(node2, filtered_nodes) |
591 | + |
592 | + def test_substring_hostnames_filter(self): |
593 | + hostname = factory.make_name() |
594 | + node1 = factory.make_Node(hostname=hostname) |
595 | + node2 = factory.make_Node() |
596 | + constraints = { |
597 | + "hostname": [hostname[:2]], |
598 | + } |
599 | + form = FreeTextFilterNodeForm(data=constraints) |
600 | + self.assertTrue(form.is_valid()) |
601 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
602 | + self.assertIn(node1, filtered_nodes) |
603 | + self.assertNotIn(node2, filtered_nodes) |
604 | + |
605 | + def test_substring_mac_addresses_filter(self): |
606 | + mac_address = factory.make_mac_address() |
607 | + interface = factory.make_Interface(mac_address=mac_address) |
608 | + node1 = factory.make_Node() |
609 | + node1.current_config.interface_set.add(interface) |
610 | + node2 = factory.make_Node() |
611 | + constraints = { |
612 | + "mac_address": [str(mac_address)[:4]], |
613 | + } |
614 | + form = FreeTextFilterNodeForm(data=constraints) |
615 | + self.assertTrue(form.is_valid()) |
616 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
617 | + self.assertIn(node1, filtered_nodes) |
618 | + self.assertNotIn(node2, filtered_nodes) |
619 | + |
620 | + def test_substring_agent_name_filter(self): |
621 | + name = factory.make_name() |
622 | + node1 = factory.make_Node(agent_name=name) |
623 | + node2 = factory.make_Node() |
624 | + constraints = { |
625 | + "agent_name": name, |
626 | + } |
627 | + form = FreeTextFilterNodeForm(data=constraints) |
628 | + self.assertTrue(form.is_valid()) |
629 | + filtered_nodes = form.filter_nodes(Machine.objects.all())[0] |
630 | + self.assertIn(node1, filtered_nodes) |
631 | + self.assertNotIn(node2, filtered_nodes) |
632 | + |
633 | + |
634 | class TestAcquireNodeForm(MAASServerTestCase, FilterConstraintsMixin): |
635 | |
636 | form_class = AcquireNodeForm |
637 | diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py |
638 | index 15a4999..7a9fc53 100644 |
639 | --- a/src/maasserver/websockets/handlers/node.py |
640 | +++ b/src/maasserver/websockets/handlers/node.py |
641 | @@ -45,8 +45,8 @@ from maasserver.models import ( |
642 | from maasserver.models.nodeprobeddetails import script_output_nsmap |
643 | from maasserver.node_action import compile_node_actions |
644 | from maasserver.node_constraint_filter_forms import ( |
645 | + FreeTextFilterNodeForm, |
646 | GROUPABLE_FIELDS, |
647 | - ReadNodesForm, |
648 | STATIC_FILTER_FIELDS, |
649 | ) |
650 | from maasserver.permissions import NodePermission |
651 | @@ -1200,7 +1200,7 @@ class NodeHandler(TimestampedModelHandler): |
652 | |
653 | def _filter(self, qs, action, params): |
654 | qs = super()._filter(qs, action, params) |
655 | - form = ReadNodesForm(data=params) |
656 | + form = FreeTextFilterNodeForm(data=params) |
657 | if not form.is_valid(): |
658 | raise HandlerValidationError(form.errors) |
659 | qs, _, _ = form.filter_nodes(qs) |
660 | @@ -1215,7 +1215,7 @@ class NodeHandler(TimestampedModelHandler): |
661 | "dynamic": name not in STATIC_FILTER_FIELDS, |
662 | "for_grouping": name in GROUPABLE_FIELDS, |
663 | } |
664 | - for name, field in ReadNodesForm.declared_fields.items() |
665 | + for name, field in FreeTextFilterNodeForm.declared_fields.items() |
666 | ] |
667 | |
668 | def _get_dynamic_filter_options(self, key): |
669 | @@ -1316,7 +1316,7 @@ class NodeHandler(TimestampedModelHandler): |
670 | "a 'group_key' param must be provided for filter_options" |
671 | ) |
672 | else: |
673 | - if key not in ReadNodesForm.declared_fields.keys(): |
674 | + if key not in FreeTextFilterNodeForm.declared_fields.keys(): |
675 | raise HandlerValidationError( |
676 | f"{key} is not a valid 'group_key' for filter_options" |
677 | ) |
UNIT TESTS text_filter lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas
-b expanded_
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 13161/console 41cd9bd8074aa94 9594e62ef0
LOG: http://
COMMIT: 84ff165ae2cac6a