Merge ~cgrabowski/maas:add_filter_list_websocket_endpoint into maas:master

Proposed by Christian Grabowski
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)
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

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b add_filter_list_websocket_endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/12968/console
COMMIT: 5ae468b4096558ae5ac3d01834461eb7732bd626

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b add_filter_list_websocket_endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/12974/console
COMMIT: a7d38164d2d1395b286a6155a8d6da658055682b

review: Needs Fixing
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

comment inline

review: Needs Information
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

jenkins: !test

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b add_filter_list_websocket_endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/13033/console
COMMIT: a7d38164d2d1395b286a6155a8d6da658055682b

review: Needs Fixing
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

It seems the UI is OK with having the 'not' filters

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b add_filter_list_websocket_endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/13045/console
COMMIT: c39f68e47c93a8ecc4e3d0924fa8541972f5499c

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b add_filter_list_websocket_endpoint lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: c7e1643ef72b54b9ab4a0253560cf3eed39caeee

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/node_constraint_filter_forms.py b/src/maasserver/node_constraint_filter_forms.py
2index 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
36diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
37index 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)
239diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
240index 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")

Subscribers

People subscribed via source and target branches