Merge ~cgrabowski/maas:expanded_text_filter into maas:master

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

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

UNIT TESTS
-b expanded_text_filter 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/13161/console
COMMIT: 84ff165ae2cac6a41cd9bd8074aa949594e62ef0

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

Some testcases broke (see TestFilterNodeForm)

also a few nits inline

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

UNIT TESTS
-b expanded_text_filter 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/13172/console
COMMIT: 221e8b8f94a6bf54f8b2095193b9fe92846aae50

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

jenkins: !test

Revision history for this message
Christian Grabowski (cgrabowski) :
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

+1

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

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

STATUS: SUCCESS
COMMIT: 221e8b8f94a6bf54f8b2095193b9fe92846aae50

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

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

STATUS: SUCCESS
COMMIT: 5c01b76229dc726321269b724e59fec9fc6ba7d4

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/node_constraint_filter_forms.py b/src/maasserver/node_constraint_filter_forms.py
index bfdee5e..1a4a765 100644
--- a/src/maasserver/node_constraint_filter_forms.py
+++ b/src/maasserver/node_constraint_filter_forms.py
@@ -29,6 +29,7 @@ from maasserver.forms import (
29)29)
30from maasserver.models import (30from maasserver.models import (
31 BlockDevice,31 BlockDevice,
32 BootResource,
32 Filesystem,33 Filesystem,
33 Interface,34 Interface,
34 Node,35 Node,
@@ -876,6 +877,13 @@ class FilterNodeForm(RenamableFieldsForm):
876 )877 )
877 return None878 return None
878879
880 def _set_clean_tags_error(self, tag_names, db_tag_names):
881 unknown_tags = tag_names.difference(db_tag_names)
882 error_msg = "No such tag(s): %s." % ", ".join(
883 "'%s'" % tag for tag in unknown_tags
884 )
885 set_form_error(self, self.get_field_name("tags"), error_msg)
886
879 def clean_tags(self):887 def clean_tags(self):
880 value = self.cleaned_data[self.get_field_name("tags")]888 value = self.cleaned_data[self.get_field_name("tags")]
881 if value:889 if value:
@@ -888,22 +896,24 @@ class FilterNodeForm(RenamableFieldsForm):
888 )896 )
889 )897 )
890 if len(tag_names) != len(db_tag_names):898 if len(tag_names) != len(db_tag_names):
891 unknown_tags = tag_names.difference(db_tag_names)899 self._set_clean_tags_error(tag_names, db_tag_names)
892 error_msg = "No such tag(s): %s." % ", ".join(
893 "'%s'" % tag for tag in unknown_tags
894 )
895 set_form_error(self, self.get_field_name("tags"), error_msg)
896 return None900 return None
897 return tag_names901 return tag_names
898 return None902 return None
899903
904 def _set_zone_error(self, value, field):
905 if type(value) == list:
906 error_msg = "No such zone(s): %s." % ", ".join(value)
907 else:
908 error_msg = "No such zone: '%s'." % value
909 set_form_error(self, self.get_field_name(field), error_msg)
910
900 def clean_zone(self):911 def clean_zone(self):
901 value = self.cleaned_data[self.get_field_name("zone")]912 value = self.cleaned_data[self.get_field_name("zone")]
902 if value:913 if value:
903 nonexistent_names = detect_nonexistent_names(Zone, [value])914 nonexistent_names = detect_nonexistent_names(Zone, [value])
904 if nonexistent_names:915 if nonexistent_names:
905 error_msg = "No such zone: '%s'." % value916 self._set_zone_error(value, "zone")
906 set_form_error(self, self.get_field_name("zone"), error_msg)
907 return None917 return None
908 return value918 return value
909 return None919 return None
@@ -914,18 +924,23 @@ class FilterNodeForm(RenamableFieldsForm):
914 return None924 return None
915 nonexistent_names = detect_nonexistent_names(Zone, value)925 nonexistent_names = detect_nonexistent_names(Zone, value)
916 if nonexistent_names:926 if nonexistent_names:
917 error_msg = "No such zone(s): %s." % ", ".join(nonexistent_names)927 self._set_zone_error(nonexistent_names, "not_in_zone")
918 set_form_error(self, self.get_field_name("not_in_zone"), error_msg)
919 return None928 return None
920 return value929 return value
921930
931 def _set_pool_error(self, value, field):
932 if type(value) == list:
933 error_msg = "No such pool(s): %s." % ", ".join(value)
934 else:
935 error_msg = "No such pool: '%s'." % value
936 set_form_error(self, self.get_field_name(field), error_msg)
937
922 def clean_pool(self):938 def clean_pool(self):
923 value = self.cleaned_data[self.get_field_name("pool")]939 value = self.cleaned_data[self.get_field_name("pool")]
924 if value:940 if value:
925 nonexistent_names = detect_nonexistent_names(ResourcePool, [value])941 nonexistent_names = detect_nonexistent_names(ResourcePool, [value])
926 if nonexistent_names:942 if nonexistent_names:
927 error_msg = "No such pool: '%s'." % value943 self._set_pool_error(value, "pool")
928 set_form_error(self, self.get_field_name("pool"), error_msg)
929 return None944 return None
930 return value945 return value
931 return None946 return None
@@ -936,8 +951,7 @@ class FilterNodeForm(RenamableFieldsForm):
936 return None951 return None
937 nonexistent_names = detect_nonexistent_names(ResourcePool, value)952 nonexistent_names = detect_nonexistent_names(ResourcePool, value)
938 if nonexistent_names:953 if nonexistent_names:
939 error_msg = "No such pool(s): %s." % ", ".join(nonexistent_names)954 self._set_pool_error(nonexistent_names, "not_in_pool")
940 set_form_error(self, self.get_field_name("not_in_pool"), error_msg)
941 return None955 return None
942 return value956 return value
943957
@@ -1407,3 +1421,278 @@ class ReadNodesForm(FilterNodeForm):
1407 status_id = getattr(NODE_STATUS, status.upper())1421 status_id = getattr(NODE_STATUS, status.upper())
1408 filtered_nodes = filtered_nodes.filter(status=status_id)1422 filtered_nodes = filtered_nodes.filter(status=status_id)
1409 return filtered_nodes1423 return filtered_nodes
1424
1425
1426class FreeTextFilterNodeForm(ReadNodesForm):
1427
1428 mac_address = UnconstrainedMultipleChoiceField(
1429 label="MAC addresses to filter on",
1430 required=False,
1431 )
1432
1433 def _substring_filter(self, queryset, field, substring, exclude=False):
1434 if type(substring) != str: # assume substring is a list of substrings
1435 query = Q()
1436 for substr in substring:
1437 substring_filter = {f"{field}__contains": substr}
1438 query = query | Q(**substring_filter)
1439 if exclude:
1440 return queryset.exclude(query)
1441 return queryset.filter(query)
1442
1443 substring_filter = {f"{field}__contains": substring}
1444 if exclude:
1445 return queryset.exclude(**substring_filter)
1446 return queryset.filter(**substring_filter)
1447
1448 def clean_tags(self):
1449 value = self.cleaned_data[self.get_field_name("tags")]
1450 if value:
1451 tag_names = parse_legacy_tags(value)
1452 # Validate tags.
1453 tag_names = set(tag_names)
1454 db_tag_names = set(
1455 self._substring_filter(
1456 Tag.objects, "name", tag_names
1457 ).values_list("name", flat=True)
1458 )
1459 if len(tag_names) < len(db_tag_names):
1460 self._set_clean_tags_error(tag_names, db_tag_names)
1461 return None
1462 return db_tag_names
1463
1464 def clean_arch(self):
1465 value = self.cleaned_data[self.get_field_name("arch")]
1466 if value:
1467 archs = self._substring_filter(
1468 BootResource.objects, "architecture", value
1469 ).values_list("architecture", flat=True)
1470 if not archs:
1471 set_form_error(
1472 self,
1473 self.get_field_name("arch"),
1474 "Architecture not recognised.",
1475 )
1476 return None
1477 return archs
1478
1479 def _clean_zones(self, value, field):
1480 zones = self._substring_filter(Zone.objects, "name", value)
1481 if not zones:
1482 self._set_zone_error(value + "*", field)
1483 return None
1484 return zones
1485
1486 def clean_zone(self):
1487 value = self.cleaned_data[self.get_field_name("zone")]
1488 if value:
1489 return self._clean_zones(value, "zone")
1490
1491 def clean_not_in_zone(self):
1492 value = self.cleaned_data[self.get_field_name("not_in_zone")]
1493 if value:
1494 return self._clean_zones(value, "not_in_zone")
1495
1496 def filter_by_zone(self, filtered_nodes):
1497 zones = self.cleaned_data.get(self.get_field_name("zone"))
1498 if zones:
1499 filtered_nodes = filtered_nodes.filter(zone__in=zones)
1500 not_in_zones = self.cleaned_data.get(
1501 self.get_field_name("not_in_zone")
1502 )
1503 if not_in_zones:
1504 filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones)
1505 return filtered_nodes
1506
1507 def _clean_pools(self, value, field):
1508 pool = self._substring_filter(ResourcePool.objects, "name", value)
1509 if not pool:
1510 self._set_pool_error(value + "*", field)
1511 return None
1512 return pool
1513
1514 def clean_pool(self):
1515 value = self.cleaned_data[self.get_field_name("pool")]
1516 if value:
1517 return self._clean_pools(value, "pool")
1518
1519 def clean_not_in_pool(self):
1520 value = self.cleaned_data[self.get_field_name("not_in_pool")]
1521 if value:
1522 return self._clean_pools(value, "not_in_pool")
1523
1524 def filter_by_pool(self, filtered_nodes):
1525 pools = self.cleaned_data.get(self.get_field_name("pool"))
1526 if pools:
1527 filtered_nodes = filtered_nodes.filter(pool__in=pools)
1528 not_in_pools = self.cleaned_data.get(
1529 self.get_field_name("not_in_pool")
1530 )
1531 if not_in_pools:
1532 filtered_nodes = filtered_nodes.exclude(pool__in=not_in_pools)
1533 return filtered_nodes
1534
1535 def filter_by_pod_or_pod_type(self, filtered_nodes):
1536 pod_name = self.cleaned_data[self.get_field_name("pod")]
1537 not_pod_name = self.cleaned_data[self.get_field_name("not_pod")]
1538 pod_type = self.cleaned_data[self.get_field_name("pod_type")]
1539 not_pod_type = self.cleaned_data[self.get_field_name("not_pod_type")]
1540 if pod_name:
1541 filtered_nodes = self._substring_filter(
1542 filtered_nodes, "bmc__name", pod_name
1543 )
1544 if not_pod_name:
1545 filtered_nodes = self._substring_filter(
1546 filtered_nodes, "bmc__name", not_pod_name, exclude=True
1547 )
1548 if pod_type:
1549 filtered_nodes = self._substring_filter(
1550 filtered_nodes, "bmc__type", pod_type
1551 )
1552 if not_pod_type:
1553 filtered_nodes = self._substring_filter(
1554 Pod.objects, "bmc__type", not_pod_type, exclude=True
1555 )
1556 return filtered_nodes
1557
1558 def filter_by_fabric_classes(self, filtered_nodes):
1559 fabric_classes = self.cleaned_data.get(
1560 self.get_field_name("fabric_classes")
1561 )
1562 if fabric_classes:
1563 filtered_nodes = self._substring_filter(
1564 filtered_nodes,
1565 "current_config__interface__vlan__fabric__class_type",
1566 fabric_classes,
1567 )
1568 not_fabric_classes = self.cleaned_data.get(
1569 self.get_field_name("not_fabric_classes")
1570 )
1571 if not_fabric_classes:
1572 filtered_nodes = self._substring_filter(
1573 filtered_nodes,
1574 "current_config__interface__vlan__fabric__class_type",
1575 not_fabric_classes,
1576 exclude=True,
1577 )
1578 return filtered_nodes
1579
1580 def filter_by_fabrics(self, filtered_nodes):
1581 fabrics = self.cleaned_data.get(self.get_field_name("fabrics"))
1582 if fabrics:
1583 filtered_nodes = self._substring_filter(
1584 filtered_nodes,
1585 "current_config__interface__vlan__fabric__name",
1586 fabrics,
1587 )
1588 not_fabrics = self.cleaned_data.get(self.get_field_name("not_fabrics"))
1589 if not_fabrics:
1590 filtered_nodes = self._substring_filter(
1591 filtered_nodes,
1592 "current_config__interface__vlan__fabric__name",
1593 not_fabrics,
1594 exclude=True,
1595 )
1596 return filtered_nodes
1597
1598 def clean_vlans(self):
1599 value = self.cleaned_data.get(self.get_field_name("vlans"))
1600 if value:
1601 vlans = self._substring_filter(VLAN.objects, "name", value)
1602 if not vlans:
1603 set_form_error(
1604 self,
1605 self.get_field_name("vlans"),
1606 "no vlan found for %s" % value,
1607 )
1608 return None
1609 return value
1610
1611 def filter_by_vlans(self, filtered_nodes):
1612 vlans = self.cleaned_data.get(self.get_field_name("vlans"))
1613 if vlans:
1614 filtered_nodes = self._substring_filter(
1615 filtered_nodes, "current_config__interface__vlan__name", vlans
1616 )
1617 not_vlans = self.cleaned_data.get(self.get_field_name("not_vlans"))
1618 if not_vlans:
1619 filtered_nodes = self._substring_filter(
1620 filtered_nodes,
1621 "current_config__interface__vlan__name",
1622 vlans,
1623 exclude=True,
1624 )
1625 return filtered_nodes
1626
1627 def clean_subnets(self):
1628 value = self.cleaned_data.get(self.get_field_name("subnets"))
1629 if value:
1630 subnets = self._substring_filter(Subnet.objects, "cidr", value)
1631 if not subnets:
1632 set_form_error(
1633 self,
1634 self.get_field_name("subnets"),
1635 "no subnet found for %s" % value,
1636 )
1637 return None
1638 return value
1639
1640 def filter_by_subnets(self, filtered_nodes):
1641 subnets = self.cleaned_data.get(self.get_field_name("subnets"))
1642 if subnets:
1643 filtered_nodes = self._substring_filter(
1644 filtered_nodes,
1645 "current_config__interface__ip_addresses__subnet__cidr",
1646 subnets,
1647 )
1648 not_subnets = self.cleaned_data.get(self.get_field_name("not_subnets"))
1649 if not_subnets:
1650 filtered_nodes = self._substring_filter(
1651 filtered_nodes,
1652 "current_config__interface__ip_addresses__subnet__cidr",
1653 not_subnets,
1654 exclude=True,
1655 )
1656 return filtered_nodes
1657
1658 def filter_by_hostnames(self, filtered_nodes):
1659 hostnames = self.cleaned_data.get(self.get_field_name("hostname"))
1660 if hostnames:
1661 filtered_nodes = self._substring_filter(
1662 filtered_nodes, "hostname", hostnames
1663 )
1664 return filtered_nodes
1665
1666 def clean_mac_address(self):
1667 value = self.cleaned_data.get(self.get_field_name("mac_address"))
1668 if value:
1669 interfaces = self._substring_filter(
1670 Interface.objects, "mac_address", value
1671 )
1672 if not interfaces:
1673 set_form_error(
1674 self,
1675 self.get_field_name("mac_address"),
1676 "no mac address found for %s" % value,
1677 )
1678 return None
1679 return value
1680
1681 def filter_by_mac_addresses(self, filtered_nodes):
1682 value = self.cleaned_data.get(self.get_field_name("mac_address"))
1683 if value:
1684 interfaces = self._substring_filter(
1685 Interface.objects, "mac_address", value
1686 )
1687 filtered_nodes = filtered_nodes.filter(
1688 current_config__interface__in=interfaces
1689 )
1690 return filtered_nodes
1691
1692 def filter_by_agent_name(self, filtered_nodes):
1693 agent_name = self.cleaned_data[self.get_field_name("agent_name")]
1694 if agent_name:
1695 filtered_nodes = self._substring_filter(
1696 filtered_nodes, "agent_name", agent_name
1697 )
1698 return filtered_nodes
diff --git a/src/maasserver/tests/test_node_constraint_filter_forms.py b/src/maasserver/tests/test_node_constraint_filter_forms.py
index 9df891b..df67c69 100644
--- a/src/maasserver/tests/test_node_constraint_filter_forms.py
+++ b/src/maasserver/tests/test_node_constraint_filter_forms.py
@@ -24,11 +24,12 @@ from maasserver.enum import (
24 NODE_STATUS,24 NODE_STATUS,
25 POWER_STATE,25 POWER_STATE,
26)26)
27from maasserver.models import Domain, Machine, NodeDevice, Zone27from maasserver.models import Domain, Machine, NodeDevice, Tag, Zone
28from maasserver.node_constraint_filter_forms import (28from maasserver.node_constraint_filter_forms import (
29 AcquireNodeForm,29 AcquireNodeForm,
30 detect_nonexistent_names,30 detect_nonexistent_names,
31 FilterNodeForm,31 FilterNodeForm,
32 FreeTextFilterNodeForm,
32 generate_architecture_wildcards,33 generate_architecture_wildcards,
33 get_architecture_wildcards,34 get_architecture_wildcards,
34 get_storage_constraints_from_string,35 get_storage_constraints_from_string,
@@ -1592,6 +1593,242 @@ class TestFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin):
1592 self.assertEqual(constraints.keys(), described_constraints)1593 self.assertEqual(constraints.keys(), described_constraints)
15931594
15941595
1596class TestFreeTextFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin):
1597 def test_substring_filter_one_substring(self):
1598 name = factory.make_name("hostname")
1599 node1 = factory.make_Node(hostname=name)
1600 node2 = factory.make_Node()
1601 form = FreeTextFilterNodeForm(data={})
1602 result = form._substring_filter(
1603 Machine.objects, "hostname", name[0 : len("hostname-") + 1]
1604 )
1605 self.assertIn(node1, list(result))
1606 self.assertNotIn(node2, list(result))
1607
1608 def test_substring_filter_list_of_substrings(self):
1609 tags = [
1610 factory.make_Tag(name=factory.make_name("tag")) for _ in range(3)
1611 ]
1612 form = FreeTextFilterNodeForm(data={})
1613 # +1 to include first letter in random suffix
1614 result = form._substring_filter(
1615 Tag.objects,
1616 "name",
1617 [tag.name[0 : len("tag-") + 1] for tag in tags],
1618 )
1619 self.assertCountEqual(tags, list(result))
1620
1621 def test_substring_filter_exact_match(self):
1622 name = factory.make_name("hostname")
1623 node1 = factory.make_Node(hostname=name)
1624 node2 = factory.make_Node()
1625 form = FreeTextFilterNodeForm(data={})
1626 result = form._substring_filter(Machine.objects, "hostname", name)
1627 self.assertIn(node1, list(result))
1628 self.assertNotIn(node2, list(result))
1629
1630 def test_substring_arch_filter(self):
1631 architecture = factory.make_name("arch")
1632 subarch = factory.make_name()
1633 arch = "/".join([architecture, subarch])
1634 factory.make_usable_boot_resource(architecture=arch)
1635 node1 = factory.make_Node(architecture=arch)
1636 node2 = factory.make_Node()
1637 constraints = {
1638 "arch": arch[:2],
1639 }
1640 form = FreeTextFilterNodeForm(data=constraints)
1641 self.assertTrue(form.is_valid())
1642 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1643 self.assertIn(node1, filtered_nodes)
1644 self.assertNotIn(node2, filtered_nodes)
1645
1646 def test_substring_tag_filter(self):
1647 tags = [
1648 factory.make_Tag(name=factory.make_name("tag")) for _ in range(3)
1649 ]
1650 node1 = factory.make_Node()
1651 node2 = factory.make_Node()
1652 [node1.tags.add(tag) for tag in tags]
1653 constraints = {
1654 "tags": [tag.name[: len("tag-") + 1] for tag in tags],
1655 }
1656 form = FreeTextFilterNodeForm(data=constraints)
1657 self.assertTrue(form.is_valid())
1658 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1659 self.assertIn(node1, filtered_nodes)
1660 self.assertNotIn(node2, filtered_nodes)
1661
1662 def test_substring_zone_filter(self):
1663 zone = factory.make_Zone()
1664 node1 = factory.make_Node(zone=zone)
1665 node2 = factory.make_Node()
1666 constraints = {
1667 "zone": zone.name[:2],
1668 }
1669 form = FreeTextFilterNodeForm(data=constraints)
1670 self.assertTrue(form.is_valid())
1671 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1672 self.assertIn(node1, filtered_nodes)
1673 self.assertNotIn(node2, filtered_nodes)
1674
1675 def test_substring_not_in_zone_filter(self):
1676 zone = factory.make_Zone()
1677 node1 = factory.make_Node(zone=zone)
1678 node2 = factory.make_Node()
1679 constraints = {
1680 "not_in_zone": [zone.name[:2]],
1681 }
1682 form = FreeTextFilterNodeForm(data=constraints)
1683 self.assertTrue(form.is_valid())
1684 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1685 self.assertNotIn(node1, filtered_nodes)
1686 self.assertIn(node2, filtered_nodes)
1687
1688 def test_substring_pool_filter(self):
1689 pool = factory.make_ResourcePool()
1690 node1 = factory.make_Node(pool=pool)
1691 node2 = factory.make_Node()
1692 constraints = {
1693 "pool": pool.name[: len("resourcepool-") + 1],
1694 }
1695 form = FreeTextFilterNodeForm(data=constraints)
1696 self.assertTrue(form.is_valid())
1697 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1698 self.assertIn(node1, filtered_nodes)
1699 self.assertNotIn(node2, filtered_nodes)
1700
1701 def test_substring_not_in_pool_filter(self):
1702 pool = factory.make_ResourcePool()
1703 node1 = factory.make_Node(pool=pool)
1704 node2 = factory.make_Node()
1705 constraints = {
1706 "not_in_pool": [pool.name[: len("resourcepool-") + 1]],
1707 }
1708 form = FreeTextFilterNodeForm(data=constraints)
1709 self.assertTrue(form.is_valid())
1710 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1711 self.assertNotIn(node1, filtered_nodes)
1712 self.assertIn(node2, filtered_nodes)
1713
1714 def test_substring_pod_filter(self):
1715 pod = factory.make_Pod()
1716 node1 = factory.make_Node(bmc=pod.as_bmc())
1717 node2 = factory.make_Node()
1718 constraints = {"pod": pod.name[: len("pod-") + 1]}
1719 form = FreeTextFilterNodeForm(data=constraints)
1720 self.assertTrue(form.is_valid())
1721 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1722 self.assertIn(node1, filtered_nodes)
1723 self.assertNotIn(node2, filtered_nodes)
1724
1725 def test_substring_fabrics_filter(self):
1726 fabric = factory.make_Fabric()
1727 vlan = factory.make_VLAN(fabric=fabric)
1728 interface = factory.make_Interface(vlan=vlan)
1729 node1 = factory.make_Node()
1730 node1.current_config.interface_set.add(interface)
1731 node2 = factory.make_Node()
1732 constraints = {
1733 "fabrics": [fabric.name[:2]],
1734 }
1735 form = FreeTextFilterNodeForm(data=constraints)
1736 self.assertTrue(form.is_valid())
1737 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1738 self.assertIn(node1, filtered_nodes)
1739 self.assertNotIn(node2, filtered_nodes)
1740
1741 def test_substring_fabric_classes_filter(self):
1742 fabric_class = factory.make_name()
1743 fabric = factory.make_Fabric(class_type=fabric_class)
1744 vlan = factory.make_VLAN(fabric=fabric)
1745 interface = factory.make_Interface(vlan=vlan)
1746 node1 = factory.make_Node()
1747 node1.current_config.interface_set.add(interface)
1748 node2 = factory.make_Node()
1749 constraints = {
1750 "fabric_classes": [fabric_class[:2]],
1751 }
1752 form = FreeTextFilterNodeForm(data=constraints)
1753 form.is_valid()
1754 print(form.errors)
1755 self.assertTrue(form.is_valid())
1756 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1757 self.assertIn(node1, filtered_nodes)
1758 self.assertNotIn(node2, filtered_nodes)
1759
1760 def test_substring_vlans_filter(self):
1761 vlan = factory.make_VLAN(name=factory.make_name())
1762 interface = factory.make_Interface(vlan=vlan)
1763 node1 = factory.make_Node()
1764 node1.current_config.interface_set.add(interface)
1765 node2 = factory.make_Node()
1766 constraints = {
1767 "vlans": [vlan.name[:2]],
1768 }
1769 form = FreeTextFilterNodeForm(data=constraints)
1770 self.assertTrue(form.is_valid())
1771 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1772 self.assertIn(node1, filtered_nodes)
1773 self.assertNotIn(node2, filtered_nodes)
1774
1775 def test_substring_subnet_filter(self):
1776 subnet = factory.make_Subnet()
1777 interface = factory.make_Interface(subnet=subnet)
1778 node1 = factory.make_Node()
1779 node1.current_config.interface_set.add(interface)
1780 node2 = factory.make_Node()
1781 constraints = {
1782 "subnets": [str(subnet.cidr)[:3]],
1783 }
1784 form = FreeTextFilterNodeForm(data=constraints)
1785 self.assertTrue(form.is_valid())
1786 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1787 self.assertIn(node1, filtered_nodes)
1788 self.assertNotIn(node2, filtered_nodes)
1789
1790 def test_substring_hostnames_filter(self):
1791 hostname = factory.make_name()
1792 node1 = factory.make_Node(hostname=hostname)
1793 node2 = factory.make_Node()
1794 constraints = {
1795 "hostname": [hostname[:2]],
1796 }
1797 form = FreeTextFilterNodeForm(data=constraints)
1798 self.assertTrue(form.is_valid())
1799 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1800 self.assertIn(node1, filtered_nodes)
1801 self.assertNotIn(node2, filtered_nodes)
1802
1803 def test_substring_mac_addresses_filter(self):
1804 mac_address = factory.make_mac_address()
1805 interface = factory.make_Interface(mac_address=mac_address)
1806 node1 = factory.make_Node()
1807 node1.current_config.interface_set.add(interface)
1808 node2 = factory.make_Node()
1809 constraints = {
1810 "mac_address": [str(mac_address)[:4]],
1811 }
1812 form = FreeTextFilterNodeForm(data=constraints)
1813 self.assertTrue(form.is_valid())
1814 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1815 self.assertIn(node1, filtered_nodes)
1816 self.assertNotIn(node2, filtered_nodes)
1817
1818 def test_substring_agent_name_filter(self):
1819 name = factory.make_name()
1820 node1 = factory.make_Node(agent_name=name)
1821 node2 = factory.make_Node()
1822 constraints = {
1823 "agent_name": name,
1824 }
1825 form = FreeTextFilterNodeForm(data=constraints)
1826 self.assertTrue(form.is_valid())
1827 filtered_nodes = form.filter_nodes(Machine.objects.all())[0]
1828 self.assertIn(node1, filtered_nodes)
1829 self.assertNotIn(node2, filtered_nodes)
1830
1831
1595class TestAcquireNodeForm(MAASServerTestCase, FilterConstraintsMixin):1832class TestAcquireNodeForm(MAASServerTestCase, FilterConstraintsMixin):
15961833
1597 form_class = AcquireNodeForm1834 form_class = AcquireNodeForm
diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
index 15a4999..7a9fc53 100644
--- a/src/maasserver/websockets/handlers/node.py
+++ b/src/maasserver/websockets/handlers/node.py
@@ -45,8 +45,8 @@ from maasserver.models import (
45from maasserver.models.nodeprobeddetails import script_output_nsmap45from maasserver.models.nodeprobeddetails import script_output_nsmap
46from maasserver.node_action import compile_node_actions46from maasserver.node_action import compile_node_actions
47from maasserver.node_constraint_filter_forms import (47from maasserver.node_constraint_filter_forms import (
48 FreeTextFilterNodeForm,
48 GROUPABLE_FIELDS,49 GROUPABLE_FIELDS,
49 ReadNodesForm,
50 STATIC_FILTER_FIELDS,50 STATIC_FILTER_FIELDS,
51)51)
52from maasserver.permissions import NodePermission52from maasserver.permissions import NodePermission
@@ -1200,7 +1200,7 @@ class NodeHandler(TimestampedModelHandler):
12001200
1201 def _filter(self, qs, action, params):1201 def _filter(self, qs, action, params):
1202 qs = super()._filter(qs, action, params)1202 qs = super()._filter(qs, action, params)
1203 form = ReadNodesForm(data=params)1203 form = FreeTextFilterNodeForm(data=params)
1204 if not form.is_valid():1204 if not form.is_valid():
1205 raise HandlerValidationError(form.errors)1205 raise HandlerValidationError(form.errors)
1206 qs, _, _ = form.filter_nodes(qs)1206 qs, _, _ = form.filter_nodes(qs)
@@ -1215,7 +1215,7 @@ class NodeHandler(TimestampedModelHandler):
1215 "dynamic": name not in STATIC_FILTER_FIELDS,1215 "dynamic": name not in STATIC_FILTER_FIELDS,
1216 "for_grouping": name in GROUPABLE_FIELDS,1216 "for_grouping": name in GROUPABLE_FIELDS,
1217 }1217 }
1218 for name, field in ReadNodesForm.declared_fields.items()1218 for name, field in FreeTextFilterNodeForm.declared_fields.items()
1219 ]1219 ]
12201220
1221 def _get_dynamic_filter_options(self, key):1221 def _get_dynamic_filter_options(self, key):
@@ -1316,7 +1316,7 @@ class NodeHandler(TimestampedModelHandler):
1316 "a 'group_key' param must be provided for filter_options"1316 "a 'group_key' param must be provided for filter_options"
1317 )1317 )
1318 else:1318 else:
1319 if key not in ReadNodesForm.declared_fields.keys():1319 if key not in FreeTextFilterNodeForm.declared_fields.keys():
1320 raise HandlerValidationError(1320 raise HandlerValidationError(
1321 f"{key} is not a valid 'group_key' for filter_options"1321 f"{key} is not a valid 'group_key' for filter_options"
1322 )1322 )

Subscribers

People subscribed via source and target branches