Merge ~cgrabowski/maas:add_filter_list_websocket_endpoint into maas:master
- Git
- lp:~cgrabowski/maas
- add_filter_list_websocket_endpoint
- Merge into master
Status: | Merged |
---|---|
Approved by: | Christian Grabowski |
Approved revision: | c7e1643ef72b54b9ab4a0253560cf3eed39caeee |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~cgrabowski/maas:add_filter_list_websocket_endpoint |
Merge into: | maas:master |
Diff against target: |
592 lines (+528/-3) 3 files modified
src/maasserver/node_constraint_filter_forms.py (+24/-0) src/maasserver/websockets/handlers/node.py (+158/-3) src/maasserver/websockets/handlers/tests/test_machine.py (+346/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
Alexsander de Souza | Approve | ||
Review via email: mp+424830@code.launchpad.net |
Commit message
add filter_options endpoint
list node filter groups
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b add_filter_
STATUS: FAILED
LOG: http://
COMMIT: a7d38164d2d1395
Alexsander de Souza (alexsander-souza) wrote : | # |
comment inline
Christian Grabowski (cgrabowski) wrote : | # |
jenkins: !test
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b add_filter_
STATUS: FAILED
LOG: http://
COMMIT: a7d38164d2d1395
Alexsander de Souza (alexsander-souza) wrote : | # |
It seems the UI is OK with having the 'not' filters
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b add_filter_
STATUS: FAILED
LOG: http://
COMMIT: c39f68e47c93a8e
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b add_filter_
STATUS: SUCCESS
COMMIT: c7e1643ef72b54b
Preview Diff
1 | diff --git a/src/maasserver/node_constraint_filter_forms.py b/src/maasserver/node_constraint_filter_forms.py |
2 | index 09862b5..944cf2e 100644 |
3 | --- a/src/maasserver/node_constraint_filter_forms.py |
4 | +++ b/src/maasserver/node_constraint_filter_forms.py |
5 | @@ -645,6 +645,30 @@ class LabeledConstraintMapField(Field): |
6 | return LabeledConstraintMap(value) |
7 | |
8 | |
9 | +STATIC_FILTER_FIELDS = ( |
10 | + "arch", |
11 | + "pod_type", |
12 | + "not_pod_type", |
13 | + "status", |
14 | +) |
15 | + |
16 | + |
17 | +GROUPABLE_FIELDS = ( |
18 | + "arch", |
19 | + "tags", |
20 | + "fabrics", |
21 | + "fabric_classes", |
22 | + "subnets", |
23 | + "vlans", |
24 | + "zone", |
25 | + "pool", |
26 | + "pod", |
27 | + "pod_type", |
28 | + "domain", |
29 | + "status", |
30 | +) |
31 | + |
32 | + |
33 | class FilterNodeForm(RenamableFieldsForm): |
34 | """A form for filtering nodes.""" |
35 | |
36 | diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py |
37 | index bc3ced5..b3b3155 100644 |
38 | --- a/src/maasserver/websockets/handlers/node.py |
39 | +++ b/src/maasserver/websockets/handlers/node.py |
40 | @@ -4,12 +4,12 @@ |
41 | """The node handler for the WebSocket connection.""" |
42 | |
43 | |
44 | -from collections import Counter |
45 | +from collections import Counter, Iterable |
46 | from itertools import chain |
47 | import logging |
48 | from operator import attrgetter, itemgetter |
49 | |
50 | -from django.db.models import Prefetch |
51 | +from django.db.models import Model, Prefetch |
52 | from lxml import etree |
53 | |
54 | from maasserver.enum import ( |
55 | @@ -18,23 +18,36 @@ from maasserver.enum import ( |
56 | INTERFACE_TYPE, |
57 | IPADDRESS_TYPE, |
58 | NODE_STATUS, |
59 | + NODE_STATUS_CHOICES, |
60 | NODE_TYPE, |
61 | POWER_STATE, |
62 | ) |
63 | +from maasserver.forms import list_all_usable_architectures |
64 | from maasserver.models import ( |
65 | CacheSet, |
66 | Config, |
67 | Event, |
68 | + Fabric, |
69 | Interface, |
70 | + Node, |
71 | + NodeDevice, |
72 | NUMANode, |
73 | + Partition, |
74 | PhysicalBlockDevice, |
75 | + Pod, |
76 | + Subnet, |
77 | Tag, |
78 | VirtualBlockDevice, |
79 | + VLAN, |
80 | VolumeGroup, |
81 | ) |
82 | from maasserver.models.nodeprobeddetails import script_output_nsmap |
83 | from maasserver.node_action import compile_node_actions |
84 | -from maasserver.node_constraint_filter_forms import ReadNodesForm |
85 | +from maasserver.node_constraint_filter_forms import ( |
86 | + GROUPABLE_FIELDS, |
87 | + ReadNodesForm, |
88 | + STATIC_FILTER_FIELDS, |
89 | +) |
90 | from maasserver.permissions import NodePermission |
91 | from maasserver.storage_layouts import get_applied_storage_layout_for_node |
92 | from maasserver.third_party_drivers import get_third_party_driver |
93 | @@ -1191,3 +1204,145 @@ class NodeHandler(TimestampedModelHandler): |
94 | raise HandlerValidationError(form.errors) |
95 | qs, _, _ = form.filter_nodes(qs) |
96 | return qs |
97 | + |
98 | + def filter_groups(self, params): |
99 | + """List available fields to filter on""" |
100 | + return [ |
101 | + { |
102 | + "key": name, |
103 | + "label": field.label, |
104 | + "dynamic": name not in STATIC_FILTER_FIELDS, |
105 | + "for_grouping": name in GROUPABLE_FIELDS, |
106 | + } |
107 | + for name, field in ReadNodesForm.declared_fields.items() |
108 | + ] |
109 | + |
110 | + def _get_dynamic_filter_options(self, key): |
111 | + results = [] |
112 | + if key == "tags": |
113 | + return [ |
114 | + {"key": tag.name, "label": tag.name} |
115 | + for tag in Tag.objects.all() |
116 | + ] |
117 | + elif key == "fabrics": |
118 | + results += [ |
119 | + {"key": value.id, "label": value.name} |
120 | + for value in Fabric.objects.all() |
121 | + ] |
122 | + elif key == "fabric_classes": |
123 | + results += [ |
124 | + {"key": fabric.class_type, "label": fabric.class_type} |
125 | + for fabric in Fabric.objects.all() |
126 | + ] |
127 | + elif key == "subnets": |
128 | + results += [ |
129 | + {"key": value.id, "label": value.name} |
130 | + for value in Subnet.objects.all() |
131 | + ] |
132 | + elif key == "vlans": |
133 | + results += [ |
134 | + {"key": value.id, "label": value.name} |
135 | + for value in VLAN.objects.all() |
136 | + ] |
137 | + elif key == "link_speed": |
138 | + results += [ |
139 | + {"key": link_speed, "label": human_readable_bytes(link_speed)} |
140 | + for link_speeds in Interface.objects.order_by() |
141 | + .values_list("link_speed") |
142 | + .distinct() |
143 | + for link_speed in link_speeds |
144 | + ] |
145 | + elif key == "storage": |
146 | + results += [ |
147 | + {"key": value, "label": value} |
148 | + for value in NodeDevice.objects.filter( |
149 | + hardware_type=HARDWARE_TYPE.STORAGE |
150 | + ) |
151 | + .order_by() |
152 | + .value_list("tags") |
153 | + .distinct() |
154 | + ] |
155 | + results += [ |
156 | + {"key": value, "label": value} |
157 | + for value in Partition.objects.order_by() |
158 | + .value_list("tags") |
159 | + .distinct() |
160 | + ] |
161 | + elif key == "interfaces": |
162 | + for field in Interface._meta.get_fields(): |
163 | + results += [ |
164 | + {"key": val, "label": val} |
165 | + for val in Interface.objects.order_by.value_list( |
166 | + field |
167 | + ).distinct() |
168 | + ] |
169 | + elif key == "devices": |
170 | + for field in NodeDevice._meta.get_fields(): |
171 | + results += [ |
172 | + {"key": f"{field}={val}", "label": f"{field}={val}"} |
173 | + for val in NodeDevice.objects.order_by() |
174 | + .value_list(field) |
175 | + .distinct() |
176 | + ] |
177 | + elif key == "mac_address": |
178 | + results += [ |
179 | + {"key": iface.mac_address, "label": iface.mac_address} |
180 | + for iface in Interface.objects.all() |
181 | + ] |
182 | + elif key == "pod": |
183 | + results += [ |
184 | + {"key": pod.id, "label": pod.name} for pod in Pod.objects.all() |
185 | + ] |
186 | + else: |
187 | + for value in Node.objects.order_by().values_list(key).distinct(): |
188 | + if isinstance(value, Node): |
189 | + results.append( |
190 | + {"key": value.system_id, "label": value.hostname} |
191 | + ) |
192 | + elif isinstance(value, Model): |
193 | + results.append({"key": value.id, "label": value.name}) |
194 | + elif isinstance(value, Iterable): |
195 | + results += [{"key": v, "label": str(v)} for v in value] |
196 | + else: |
197 | + results.append({"key": value, "label": str(value)}) |
198 | + return results |
199 | + |
200 | + def filter_options(self, params): |
201 | + try: |
202 | + key = params["group_key"] |
203 | + except KeyError: |
204 | + raise HandlerValidationError( |
205 | + "a 'group_key' param must be provided for filter_options" |
206 | + ) |
207 | + else: |
208 | + if key not in ReadNodesForm.declared_fields.keys(): |
209 | + raise HandlerValidationError( |
210 | + f"{key} is not a valid 'group_key' for filter_options" |
211 | + ) |
212 | + |
213 | + if key in STATIC_FILTER_FIELDS: |
214 | + if key == "arch": |
215 | + return [ |
216 | + {"key": arch, "label": arch} |
217 | + for arch in list_all_usable_architectures() |
218 | + ] |
219 | + if key == "pod_type" or key == "not_pod_type": |
220 | + return [ |
221 | + {"key": "lxd", "label": "LXD"}, |
222 | + {"key": "virsh", "label": "Virsh"}, |
223 | + ] |
224 | + if key == "status": |
225 | + return [ |
226 | + {"key": choice[0], "label": choice[1]} |
227 | + for choice in NODE_STATUS_CHOICES |
228 | + ] |
229 | + else: |
230 | + if key.startswith("not_in_"): |
231 | + return self._get_dynamic_filter_options( |
232 | + key[len("not_in_") :] |
233 | + ) |
234 | + if key.startswith("not_"): |
235 | + return self._get_dynamic_filter_options(key[len("not_") :]) |
236 | + if key == "mem": |
237 | + return self._get_dynamic_filter_options("memory") |
238 | + return self._get_dynamic_filter_options(key) |
239 | diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py |
240 | index 1c6d7a8..0dd40ea 100644 |
241 | --- a/src/maasserver/websockets/handlers/tests/test_machine.py |
242 | +++ b/src/maasserver/websockets/handlers/tests/test_machine.py |
243 | @@ -5821,3 +5821,349 @@ class TestMachineHandlerFilter(MAASServerTestCase): |
244 | self.assertEqual(machine.status, NODE_STATUS.ALLOCATED) |
245 | else: |
246 | self.assertEqual(machine.status, NODE_STATUS.READY) |
247 | + |
248 | + def test_filter_groups(self): |
249 | + self.maxDiff = None |
250 | + user = factory.make_User() |
251 | + handler = MachineHandler(user, {}, None) |
252 | + self.assertCountEqual( |
253 | + [ |
254 | + { |
255 | + "key": "id", |
256 | + "label": "System IDs to filter on", |
257 | + "dynamic": True, |
258 | + "for_grouping": False, |
259 | + }, |
260 | + { |
261 | + "key": "arch", |
262 | + "label": "Architecture", |
263 | + "dynamic": False, |
264 | + "for_grouping": True, |
265 | + }, |
266 | + { |
267 | + "key": "tags", |
268 | + "label": "Tags", |
269 | + "dynamic": True, |
270 | + "for_grouping": True, |
271 | + }, |
272 | + { |
273 | + "key": "not_tags", |
274 | + "label": "Not having tags", |
275 | + "dynamic": True, |
276 | + "for_grouping": False, |
277 | + }, |
278 | + { |
279 | + "key": "fabrics", |
280 | + "label": "Attached to fabrics", |
281 | + "dynamic": True, |
282 | + "for_grouping": True, |
283 | + }, |
284 | + { |
285 | + "key": "not_fabrics", |
286 | + "label": "Not attached to fabrics", |
287 | + "dynamic": True, |
288 | + "for_grouping": False, |
289 | + }, |
290 | + { |
291 | + "key": "fabric_classes", |
292 | + "label": "Attached to fabric with specified classes", |
293 | + "dynamic": True, |
294 | + "for_grouping": True, |
295 | + }, |
296 | + { |
297 | + "key": "not_fabric_classes", |
298 | + "label": "Not attached to fabric with specified classes", |
299 | + "dynamic": True, |
300 | + "for_grouping": False, |
301 | + }, |
302 | + { |
303 | + "key": "subnets", |
304 | + "label": "Attached to subnets", |
305 | + "dynamic": True, |
306 | + "for_grouping": True, |
307 | + }, |
308 | + { |
309 | + "key": "not_subnets", |
310 | + "label": "Not attached to subnets", |
311 | + "dynamic": True, |
312 | + "for_grouping": False, |
313 | + }, |
314 | + { |
315 | + "key": "link_speed", |
316 | + "label": "Link speed", |
317 | + "dynamic": True, |
318 | + "for_grouping": False, |
319 | + }, |
320 | + { |
321 | + "key": "vlans", |
322 | + "label": "Attached to VLANs", |
323 | + "dynamic": True, |
324 | + "for_grouping": True, |
325 | + }, |
326 | + { |
327 | + "key": "not_vlans", |
328 | + "label": "Not attached to VLANs", |
329 | + "dynamic": True, |
330 | + "for_grouping": False, |
331 | + }, |
332 | + { |
333 | + "key": "connected_to", |
334 | + "label": "Connected to", |
335 | + "dynamic": True, |
336 | + "for_grouping": False, |
337 | + }, |
338 | + { |
339 | + "key": "not_connected_to", |
340 | + "label": "Not connected to", |
341 | + "dynamic": True, |
342 | + "for_grouping": False, |
343 | + }, |
344 | + { |
345 | + "key": "zone", |
346 | + "label": "Physical zone", |
347 | + "dynamic": True, |
348 | + "for_grouping": True, |
349 | + }, |
350 | + { |
351 | + "key": "not_in_zone", |
352 | + "label": "Not in zone", |
353 | + "dynamic": True, |
354 | + "for_grouping": False, |
355 | + }, |
356 | + { |
357 | + "key": "pool", |
358 | + "label": "Resource pool", |
359 | + "dynamic": True, |
360 | + "for_grouping": True, |
361 | + }, |
362 | + { |
363 | + "key": "not_in_pool", |
364 | + "label": "Not in resource pool", |
365 | + "dynamic": True, |
366 | + "for_grouping": False, |
367 | + }, |
368 | + { |
369 | + "key": "storage", |
370 | + "label": "Storage", |
371 | + "dynamic": True, |
372 | + "for_grouping": False, |
373 | + }, |
374 | + { |
375 | + "key": "interfaces", |
376 | + "label": "Interfaces", |
377 | + "dynamic": True, |
378 | + "for_grouping": False, |
379 | + }, |
380 | + { |
381 | + "key": "devices", |
382 | + "label": "Devices", |
383 | + "dynamic": True, |
384 | + "for_grouping": False, |
385 | + }, |
386 | + { |
387 | + "key": "cpu_count", |
388 | + "label": "CPU count", |
389 | + "dynamic": True, |
390 | + "for_grouping": False, |
391 | + }, |
392 | + { |
393 | + "key": "mem", |
394 | + "label": "Memory", |
395 | + "dynamic": True, |
396 | + "for_grouping": False, |
397 | + }, |
398 | + { |
399 | + "key": "pod", |
400 | + "label": "The name of the desired pod", |
401 | + "dynamic": True, |
402 | + "for_grouping": True, |
403 | + }, |
404 | + { |
405 | + "key": "not_pod", |
406 | + "label": "The name of the undesired pod", |
407 | + "dynamic": True, |
408 | + "for_grouping": False, |
409 | + }, |
410 | + { |
411 | + "key": "pod_type", |
412 | + "label": "The power_type of the desired pod", |
413 | + "dynamic": False, |
414 | + "for_grouping": True, |
415 | + }, |
416 | + { |
417 | + "key": "not_pod_type", |
418 | + "label": "The power_type of the undesired pod", |
419 | + "dynamic": False, |
420 | + "for_grouping": False, |
421 | + }, |
422 | + { |
423 | + "key": "hostname", |
424 | + "label": "Hostnames to filter on", |
425 | + "dynamic": True, |
426 | + "for_grouping": False, |
427 | + }, |
428 | + { |
429 | + "key": "mac_address", |
430 | + "label": "MAC addresses to filter on", |
431 | + "dynamic": True, |
432 | + "for_grouping": False, |
433 | + }, |
434 | + { |
435 | + "key": "domain", |
436 | + "label": "Domain names to filter on", |
437 | + "dynamic": True, |
438 | + "for_grouping": True, |
439 | + }, |
440 | + { |
441 | + "key": "agent_name", |
442 | + "label": "Only include nodes with events matching the agent name", |
443 | + "dynamic": True, |
444 | + "for_grouping": False, |
445 | + }, |
446 | + { |
447 | + "key": "status", |
448 | + "label": "Only includes nodes with the specified status", |
449 | + "dynamic": False, |
450 | + "for_grouping": True, |
451 | + }, |
452 | + ], |
453 | + handler.filter_groups({}), |
454 | + ) |
455 | + |
456 | + def test_filter_options(self): |
457 | + user = factory.make_User() |
458 | + handler = MachineHandler(user, {}, None) |
459 | + architectures = [ |
460 | + "amd64/generic", |
461 | + "arm64/generic", |
462 | + "ppc64el/generic", |
463 | + "s390x/generic", |
464 | + ] |
465 | + [ |
466 | + factory.make_usable_boot_resource(architecture=arch) |
467 | + for arch in architectures |
468 | + ] |
469 | + machines = [ |
470 | + factory.make_Machine_with_Interface_on_Subnet( |
471 | + architecture=architectures[i % len(architectures)], |
472 | + bmc=factory.make_Pod(), |
473 | + ) |
474 | + for i in range(5) |
475 | + ] |
476 | + |
477 | + def _assert_value_in(value, field_name): |
478 | + self.assertIn( |
479 | + value, |
480 | + [ |
481 | + option["key"] |
482 | + for option in handler.filter_options( |
483 | + {"group_key": field_name} |
484 | + ) |
485 | + ], |
486 | + ) |
487 | + |
488 | + def _assert_subset(subset, field_name): |
489 | + self.assertTrue( |
490 | + subset |
491 | + <= { |
492 | + option["key"] |
493 | + for option in handler.filter_options( |
494 | + {"group_key": field_name} |
495 | + ) |
496 | + } |
497 | + ) |
498 | + |
499 | + for machine in machines: |
500 | + machine.tags.add(factory.make_Tag()) |
501 | + _assert_value_in(machine.architecture, "arch") |
502 | + _assert_subset(set(machine.tag_names()), "tags") |
503 | + _assert_subset(set(machine.tag_names()), "not_tags") |
504 | + _assert_subset( |
505 | + set( |
506 | + iface.vlan.fabric.id |
507 | + for iface in machine.current_config.interface_set.all() |
508 | + ), |
509 | + "fabrics", |
510 | + ) |
511 | + _assert_subset( |
512 | + set( |
513 | + iface.vlan.fabric.id |
514 | + for iface in machine.current_config.interface_set.all() |
515 | + ), |
516 | + "not_fabrics", |
517 | + ) |
518 | + _assert_subset( |
519 | + set( |
520 | + iface.vlan.fabric.class_type |
521 | + for iface in machine.current_config.interface_set.all() |
522 | + if iface.vlan.fabric.class_type |
523 | + ), |
524 | + "fabric_classes", |
525 | + ) |
526 | + _assert_subset( |
527 | + set( |
528 | + iface.vlan.fabric.class_type |
529 | + for iface in machine.current_config.interface_set.all() |
530 | + if iface.vlan.fabric.class_type |
531 | + ), |
532 | + "not_fabric_classes", |
533 | + ) |
534 | + _assert_subset( |
535 | + set( |
536 | + link["subnet"].id |
537 | + for iface in machine.current_config.interface_set.all() |
538 | + for link in iface.get_links() |
539 | + ), |
540 | + "subnets", |
541 | + ) |
542 | + _assert_subset( |
543 | + set( |
544 | + link["subnet"].id |
545 | + for iface in machine.current_config.interface_set.all() |
546 | + for link in iface.get_links() |
547 | + ), |
548 | + "not_subnets", |
549 | + ) |
550 | + _assert_subset( |
551 | + set( |
552 | + iface.link_speed |
553 | + for iface in machine.current_config.interface_set.all() |
554 | + ), |
555 | + "link_speed", |
556 | + ) |
557 | + _assert_subset( |
558 | + set( |
559 | + iface.vlan.id |
560 | + for iface in machine.current_config.interface_set.all() |
561 | + ), |
562 | + "vlans", |
563 | + ) |
564 | + _assert_subset( |
565 | + set( |
566 | + iface.vlan.id |
567 | + for iface in machine.current_config.interface_set.all() |
568 | + ), |
569 | + "not_vlans", |
570 | + ) |
571 | + _assert_value_in(machine.zone.id, "zone") |
572 | + _assert_value_in(machine.zone.id, "not_in_zone") |
573 | + _assert_value_in(machine.pool.id, "pool") |
574 | + _assert_value_in(machine.pool.id, "not_in_pool") |
575 | + _assert_value_in(machine.cpu_count, "cpu_count") |
576 | + _assert_value_in(machine.memory, "mem") |
577 | + _assert_value_in(machine.hostname, "hostname") |
578 | + _assert_value_in(machine.status, "status") |
579 | + _assert_subset( |
580 | + set( |
581 | + iface.mac_address |
582 | + for iface in machine.current_config.interface_set.all() |
583 | + ), |
584 | + "mac_address", |
585 | + ) |
586 | + _assert_value_in(machine.domain.id, "domain") |
587 | + _assert_value_in(machine.agent_name, "agent_name") |
588 | + if machine.bmc.power_type == "lxd": |
589 | + _assert_value_in(machine.bmc.power_type, "pod_type") |
590 | + _assert_value_in(machine.bmc.power_type, "not_pod_type") |
591 | + _assert_value_in(machine.bmc.id, "pod") |
592 | + _assert_value_in(machine.bmc.id, "not_pod") |
UNIT TESTS list_websocket_ endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas
-b add_filter_
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 12968/console e5ac3d01834461e b7732bd626
LOG: http://
COMMIT: 5ae468b4096558a