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:
|
Commit message
move websocket handler node constraint form to child class
substring match multiple substrings
add substring filter
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alexsander de Souza (alexsander-souza) wrote : | # |
Some testcases broke (see TestFilterNodeForm)
also a few nits inline
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: FAILED
LOG: http://
COMMIT: 221e8b8f94a6bf5
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Christian Grabowski (cgrabowski) wrote : | # |
jenkins: !test
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Christian Grabowski (cgrabowski) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alexsander de Souza (alexsander-souza) wrote : | # |
+1
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: SUCCESS
COMMIT: 221e8b8f94a6bf5
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b expanded_
STATUS: SUCCESS
COMMIT: 5c01b76229dc726
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b expanded_
STATUS: FAILED BUILD
LOG: http://
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 | 29 | ) | 29 | ) |
7 | 30 | from maasserver.models import ( | 30 | from maasserver.models import ( |
8 | 31 | BlockDevice, | 31 | BlockDevice, |
9 | 32 | BootResource, | ||
10 | 32 | Filesystem, | 33 | Filesystem, |
11 | 33 | Interface, | 34 | Interface, |
12 | 34 | Node, | 35 | Node, |
13 | @@ -876,6 +877,13 @@ class FilterNodeForm(RenamableFieldsForm): | |||
14 | 876 | ) | 877 | ) |
15 | 877 | return None | 878 | return None |
16 | 878 | 879 | ||
17 | 880 | def _set_clean_tags_error(self, tag_names, db_tag_names): | ||
18 | 881 | unknown_tags = tag_names.difference(db_tag_names) | ||
19 | 882 | error_msg = "No such tag(s): %s." % ", ".join( | ||
20 | 883 | "'%s'" % tag for tag in unknown_tags | ||
21 | 884 | ) | ||
22 | 885 | set_form_error(self, self.get_field_name("tags"), error_msg) | ||
23 | 886 | |||
24 | 879 | def clean_tags(self): | 887 | def clean_tags(self): |
25 | 880 | value = self.cleaned_data[self.get_field_name("tags")] | 888 | value = self.cleaned_data[self.get_field_name("tags")] |
26 | 881 | if value: | 889 | if value: |
27 | @@ -888,22 +896,24 @@ class FilterNodeForm(RenamableFieldsForm): | |||
28 | 888 | ) | 896 | ) |
29 | 889 | ) | 897 | ) |
30 | 890 | if len(tag_names) != len(db_tag_names): | 898 | if len(tag_names) != len(db_tag_names): |
36 | 891 | unknown_tags = tag_names.difference(db_tag_names) | 899 | self._set_clean_tags_error(tag_names, db_tag_names) |
32 | 892 | error_msg = "No such tag(s): %s." % ", ".join( | ||
33 | 893 | "'%s'" % tag for tag in unknown_tags | ||
34 | 894 | ) | ||
35 | 895 | set_form_error(self, self.get_field_name("tags"), error_msg) | ||
37 | 896 | return None | 900 | return None |
38 | 897 | return tag_names | 901 | return tag_names |
39 | 898 | return None | 902 | return None |
40 | 899 | 903 | ||
41 | 904 | def _set_zone_error(self, value, field): | ||
42 | 905 | if type(value) == list: | ||
43 | 906 | error_msg = "No such zone(s): %s." % ", ".join(value) | ||
44 | 907 | else: | ||
45 | 908 | error_msg = "No such zone: '%s'." % value | ||
46 | 909 | set_form_error(self, self.get_field_name(field), error_msg) | ||
47 | 910 | |||
48 | 900 | def clean_zone(self): | 911 | def clean_zone(self): |
49 | 901 | value = self.cleaned_data[self.get_field_name("zone")] | 912 | value = self.cleaned_data[self.get_field_name("zone")] |
50 | 902 | if value: | 913 | if value: |
51 | 903 | nonexistent_names = detect_nonexistent_names(Zone, [value]) | 914 | nonexistent_names = detect_nonexistent_names(Zone, [value]) |
52 | 904 | if nonexistent_names: | 915 | if nonexistent_names: |
55 | 905 | error_msg = "No such zone: '%s'." % value | 916 | self._set_zone_error(value, "zone") |
54 | 906 | set_form_error(self, self.get_field_name("zone"), error_msg) | ||
56 | 907 | return None | 917 | return None |
57 | 908 | return value | 918 | return value |
58 | 909 | return None | 919 | return None |
59 | @@ -914,18 +924,23 @@ class FilterNodeForm(RenamableFieldsForm): | |||
60 | 914 | return None | 924 | return None |
61 | 915 | nonexistent_names = detect_nonexistent_names(Zone, value) | 925 | nonexistent_names = detect_nonexistent_names(Zone, value) |
62 | 916 | if nonexistent_names: | 926 | if nonexistent_names: |
65 | 917 | error_msg = "No such zone(s): %s." % ", ".join(nonexistent_names) | 927 | self._set_zone_error(nonexistent_names, "not_in_zone") |
64 | 918 | set_form_error(self, self.get_field_name("not_in_zone"), error_msg) | ||
66 | 919 | return None | 928 | return None |
67 | 920 | return value | 929 | return value |
68 | 921 | 930 | ||
69 | 931 | def _set_pool_error(self, value, field): | ||
70 | 932 | if type(value) == list: | ||
71 | 933 | error_msg = "No such pool(s): %s." % ", ".join(value) | ||
72 | 934 | else: | ||
73 | 935 | error_msg = "No such pool: '%s'." % value | ||
74 | 936 | set_form_error(self, self.get_field_name(field), error_msg) | ||
75 | 937 | |||
76 | 922 | def clean_pool(self): | 938 | def clean_pool(self): |
77 | 923 | value = self.cleaned_data[self.get_field_name("pool")] | 939 | value = self.cleaned_data[self.get_field_name("pool")] |
78 | 924 | if value: | 940 | if value: |
79 | 925 | nonexistent_names = detect_nonexistent_names(ResourcePool, [value]) | 941 | nonexistent_names = detect_nonexistent_names(ResourcePool, [value]) |
80 | 926 | if nonexistent_names: | 942 | if nonexistent_names: |
83 | 927 | error_msg = "No such pool: '%s'." % value | 943 | self._set_pool_error(value, "pool") |
82 | 928 | set_form_error(self, self.get_field_name("pool"), error_msg) | ||
84 | 929 | return None | 944 | return None |
85 | 930 | return value | 945 | return value |
86 | 931 | return None | 946 | return None |
87 | @@ -936,8 +951,7 @@ class FilterNodeForm(RenamableFieldsForm): | |||
88 | 936 | return None | 951 | return None |
89 | 937 | nonexistent_names = detect_nonexistent_names(ResourcePool, value) | 952 | nonexistent_names = detect_nonexistent_names(ResourcePool, value) |
90 | 938 | if nonexistent_names: | 953 | if nonexistent_names: |
93 | 939 | error_msg = "No such pool(s): %s." % ", ".join(nonexistent_names) | 954 | self._set_pool_error(nonexistent_names, "not_in_pool") |
92 | 940 | set_form_error(self, self.get_field_name("not_in_pool"), error_msg) | ||
94 | 941 | return None | 955 | return None |
95 | 942 | return value | 956 | return value |
96 | 943 | 957 | ||
97 | @@ -1407,3 +1421,278 @@ class ReadNodesForm(FilterNodeForm): | |||
98 | 1407 | status_id = getattr(NODE_STATUS, status.upper()) | 1421 | status_id = getattr(NODE_STATUS, status.upper()) |
99 | 1408 | filtered_nodes = filtered_nodes.filter(status=status_id) | 1422 | filtered_nodes = filtered_nodes.filter(status=status_id) |
100 | 1409 | return filtered_nodes | 1423 | return filtered_nodes |
101 | 1424 | |||
102 | 1425 | |||
103 | 1426 | class FreeTextFilterNodeForm(ReadNodesForm): | ||
104 | 1427 | |||
105 | 1428 | mac_address = UnconstrainedMultipleChoiceField( | ||
106 | 1429 | label="MAC addresses to filter on", | ||
107 | 1430 | required=False, | ||
108 | 1431 | ) | ||
109 | 1432 | |||
110 | 1433 | def _substring_filter(self, queryset, field, substring, exclude=False): | ||
111 | 1434 | if type(substring) != str: # assume substring is a list of substrings | ||
112 | 1435 | query = Q() | ||
113 | 1436 | for substr in substring: | ||
114 | 1437 | substring_filter = {f"{field}__contains": substr} | ||
115 | 1438 | query = query | Q(**substring_filter) | ||
116 | 1439 | if exclude: | ||
117 | 1440 | return queryset.exclude(query) | ||
118 | 1441 | return queryset.filter(query) | ||
119 | 1442 | |||
120 | 1443 | substring_filter = {f"{field}__contains": substring} | ||
121 | 1444 | if exclude: | ||
122 | 1445 | return queryset.exclude(**substring_filter) | ||
123 | 1446 | return queryset.filter(**substring_filter) | ||
124 | 1447 | |||
125 | 1448 | def clean_tags(self): | ||
126 | 1449 | value = self.cleaned_data[self.get_field_name("tags")] | ||
127 | 1450 | if value: | ||
128 | 1451 | tag_names = parse_legacy_tags(value) | ||
129 | 1452 | # Validate tags. | ||
130 | 1453 | tag_names = set(tag_names) | ||
131 | 1454 | db_tag_names = set( | ||
132 | 1455 | self._substring_filter( | ||
133 | 1456 | Tag.objects, "name", tag_names | ||
134 | 1457 | ).values_list("name", flat=True) | ||
135 | 1458 | ) | ||
136 | 1459 | if len(tag_names) < len(db_tag_names): | ||
137 | 1460 | self._set_clean_tags_error(tag_names, db_tag_names) | ||
138 | 1461 | return None | ||
139 | 1462 | return db_tag_names | ||
140 | 1463 | |||
141 | 1464 | def clean_arch(self): | ||
142 | 1465 | value = self.cleaned_data[self.get_field_name("arch")] | ||
143 | 1466 | if value: | ||
144 | 1467 | archs = self._substring_filter( | ||
145 | 1468 | BootResource.objects, "architecture", value | ||
146 | 1469 | ).values_list("architecture", flat=True) | ||
147 | 1470 | if not archs: | ||
148 | 1471 | set_form_error( | ||
149 | 1472 | self, | ||
150 | 1473 | self.get_field_name("arch"), | ||
151 | 1474 | "Architecture not recognised.", | ||
152 | 1475 | ) | ||
153 | 1476 | return None | ||
154 | 1477 | return archs | ||
155 | 1478 | |||
156 | 1479 | def _clean_zones(self, value, field): | ||
157 | 1480 | zones = self._substring_filter(Zone.objects, "name", value) | ||
158 | 1481 | if not zones: | ||
159 | 1482 | self._set_zone_error(value + "*", field) | ||
160 | 1483 | return None | ||
161 | 1484 | return zones | ||
162 | 1485 | |||
163 | 1486 | def clean_zone(self): | ||
164 | 1487 | value = self.cleaned_data[self.get_field_name("zone")] | ||
165 | 1488 | if value: | ||
166 | 1489 | return self._clean_zones(value, "zone") | ||
167 | 1490 | |||
168 | 1491 | def clean_not_in_zone(self): | ||
169 | 1492 | value = self.cleaned_data[self.get_field_name("not_in_zone")] | ||
170 | 1493 | if value: | ||
171 | 1494 | return self._clean_zones(value, "not_in_zone") | ||
172 | 1495 | |||
173 | 1496 | def filter_by_zone(self, filtered_nodes): | ||
174 | 1497 | zones = self.cleaned_data.get(self.get_field_name("zone")) | ||
175 | 1498 | if zones: | ||
176 | 1499 | filtered_nodes = filtered_nodes.filter(zone__in=zones) | ||
177 | 1500 | not_in_zones = self.cleaned_data.get( | ||
178 | 1501 | self.get_field_name("not_in_zone") | ||
179 | 1502 | ) | ||
180 | 1503 | if not_in_zones: | ||
181 | 1504 | filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) | ||
182 | 1505 | return filtered_nodes | ||
183 | 1506 | |||
184 | 1507 | def _clean_pools(self, value, field): | ||
185 | 1508 | pool = self._substring_filter(ResourcePool.objects, "name", value) | ||
186 | 1509 | if not pool: | ||
187 | 1510 | self._set_pool_error(value + "*", field) | ||
188 | 1511 | return None | ||
189 | 1512 | return pool | ||
190 | 1513 | |||
191 | 1514 | def clean_pool(self): | ||
192 | 1515 | value = self.cleaned_data[self.get_field_name("pool")] | ||
193 | 1516 | if value: | ||
194 | 1517 | return self._clean_pools(value, "pool") | ||
195 | 1518 | |||
196 | 1519 | def clean_not_in_pool(self): | ||
197 | 1520 | value = self.cleaned_data[self.get_field_name("not_in_pool")] | ||
198 | 1521 | if value: | ||
199 | 1522 | return self._clean_pools(value, "not_in_pool") | ||
200 | 1523 | |||
201 | 1524 | def filter_by_pool(self, filtered_nodes): | ||
202 | 1525 | pools = self.cleaned_data.get(self.get_field_name("pool")) | ||
203 | 1526 | if pools: | ||
204 | 1527 | filtered_nodes = filtered_nodes.filter(pool__in=pools) | ||
205 | 1528 | not_in_pools = self.cleaned_data.get( | ||
206 | 1529 | self.get_field_name("not_in_pool") | ||
207 | 1530 | ) | ||
208 | 1531 | if not_in_pools: | ||
209 | 1532 | filtered_nodes = filtered_nodes.exclude(pool__in=not_in_pools) | ||
210 | 1533 | return filtered_nodes | ||
211 | 1534 | |||
212 | 1535 | def filter_by_pod_or_pod_type(self, filtered_nodes): | ||
213 | 1536 | pod_name = self.cleaned_data[self.get_field_name("pod")] | ||
214 | 1537 | not_pod_name = self.cleaned_data[self.get_field_name("not_pod")] | ||
215 | 1538 | pod_type = self.cleaned_data[self.get_field_name("pod_type")] | ||
216 | 1539 | not_pod_type = self.cleaned_data[self.get_field_name("not_pod_type")] | ||
217 | 1540 | if pod_name: | ||
218 | 1541 | filtered_nodes = self._substring_filter( | ||
219 | 1542 | filtered_nodes, "bmc__name", pod_name | ||
220 | 1543 | ) | ||
221 | 1544 | if not_pod_name: | ||
222 | 1545 | filtered_nodes = self._substring_filter( | ||
223 | 1546 | filtered_nodes, "bmc__name", not_pod_name, exclude=True | ||
224 | 1547 | ) | ||
225 | 1548 | if pod_type: | ||
226 | 1549 | filtered_nodes = self._substring_filter( | ||
227 | 1550 | filtered_nodes, "bmc__type", pod_type | ||
228 | 1551 | ) | ||
229 | 1552 | if not_pod_type: | ||
230 | 1553 | filtered_nodes = self._substring_filter( | ||
231 | 1554 | Pod.objects, "bmc__type", not_pod_type, exclude=True | ||
232 | 1555 | ) | ||
233 | 1556 | return filtered_nodes | ||
234 | 1557 | |||
235 | 1558 | def filter_by_fabric_classes(self, filtered_nodes): | ||
236 | 1559 | fabric_classes = self.cleaned_data.get( | ||
237 | 1560 | self.get_field_name("fabric_classes") | ||
238 | 1561 | ) | ||
239 | 1562 | if fabric_classes: | ||
240 | 1563 | filtered_nodes = self._substring_filter( | ||
241 | 1564 | filtered_nodes, | ||
242 | 1565 | "current_config__interface__vlan__fabric__class_type", | ||
243 | 1566 | fabric_classes, | ||
244 | 1567 | ) | ||
245 | 1568 | not_fabric_classes = self.cleaned_data.get( | ||
246 | 1569 | self.get_field_name("not_fabric_classes") | ||
247 | 1570 | ) | ||
248 | 1571 | if not_fabric_classes: | ||
249 | 1572 | filtered_nodes = self._substring_filter( | ||
250 | 1573 | filtered_nodes, | ||
251 | 1574 | "current_config__interface__vlan__fabric__class_type", | ||
252 | 1575 | not_fabric_classes, | ||
253 | 1576 | exclude=True, | ||
254 | 1577 | ) | ||
255 | 1578 | return filtered_nodes | ||
256 | 1579 | |||
257 | 1580 | def filter_by_fabrics(self, filtered_nodes): | ||
258 | 1581 | fabrics = self.cleaned_data.get(self.get_field_name("fabrics")) | ||
259 | 1582 | if fabrics: | ||
260 | 1583 | filtered_nodes = self._substring_filter( | ||
261 | 1584 | filtered_nodes, | ||
262 | 1585 | "current_config__interface__vlan__fabric__name", | ||
263 | 1586 | fabrics, | ||
264 | 1587 | ) | ||
265 | 1588 | not_fabrics = self.cleaned_data.get(self.get_field_name("not_fabrics")) | ||
266 | 1589 | if not_fabrics: | ||
267 | 1590 | filtered_nodes = self._substring_filter( | ||
268 | 1591 | filtered_nodes, | ||
269 | 1592 | "current_config__interface__vlan__fabric__name", | ||
270 | 1593 | not_fabrics, | ||
271 | 1594 | exclude=True, | ||
272 | 1595 | ) | ||
273 | 1596 | return filtered_nodes | ||
274 | 1597 | |||
275 | 1598 | def clean_vlans(self): | ||
276 | 1599 | value = self.cleaned_data.get(self.get_field_name("vlans")) | ||
277 | 1600 | if value: | ||
278 | 1601 | vlans = self._substring_filter(VLAN.objects, "name", value) | ||
279 | 1602 | if not vlans: | ||
280 | 1603 | set_form_error( | ||
281 | 1604 | self, | ||
282 | 1605 | self.get_field_name("vlans"), | ||
283 | 1606 | "no vlan found for %s" % value, | ||
284 | 1607 | ) | ||
285 | 1608 | return None | ||
286 | 1609 | return value | ||
287 | 1610 | |||
288 | 1611 | def filter_by_vlans(self, filtered_nodes): | ||
289 | 1612 | vlans = self.cleaned_data.get(self.get_field_name("vlans")) | ||
290 | 1613 | if vlans: | ||
291 | 1614 | filtered_nodes = self._substring_filter( | ||
292 | 1615 | filtered_nodes, "current_config__interface__vlan__name", vlans | ||
293 | 1616 | ) | ||
294 | 1617 | not_vlans = self.cleaned_data.get(self.get_field_name("not_vlans")) | ||
295 | 1618 | if not_vlans: | ||
296 | 1619 | filtered_nodes = self._substring_filter( | ||
297 | 1620 | filtered_nodes, | ||
298 | 1621 | "current_config__interface__vlan__name", | ||
299 | 1622 | vlans, | ||
300 | 1623 | exclude=True, | ||
301 | 1624 | ) | ||
302 | 1625 | return filtered_nodes | ||
303 | 1626 | |||
304 | 1627 | def clean_subnets(self): | ||
305 | 1628 | value = self.cleaned_data.get(self.get_field_name("subnets")) | ||
306 | 1629 | if value: | ||
307 | 1630 | subnets = self._substring_filter(Subnet.objects, "cidr", value) | ||
308 | 1631 | if not subnets: | ||
309 | 1632 | set_form_error( | ||
310 | 1633 | self, | ||
311 | 1634 | self.get_field_name("subnets"), | ||
312 | 1635 | "no subnet found for %s" % value, | ||
313 | 1636 | ) | ||
314 | 1637 | return None | ||
315 | 1638 | return value | ||
316 | 1639 | |||
317 | 1640 | def filter_by_subnets(self, filtered_nodes): | ||
318 | 1641 | subnets = self.cleaned_data.get(self.get_field_name("subnets")) | ||
319 | 1642 | if subnets: | ||
320 | 1643 | filtered_nodes = self._substring_filter( | ||
321 | 1644 | filtered_nodes, | ||
322 | 1645 | "current_config__interface__ip_addresses__subnet__cidr", | ||
323 | 1646 | subnets, | ||
324 | 1647 | ) | ||
325 | 1648 | not_subnets = self.cleaned_data.get(self.get_field_name("not_subnets")) | ||
326 | 1649 | if not_subnets: | ||
327 | 1650 | filtered_nodes = self._substring_filter( | ||
328 | 1651 | filtered_nodes, | ||
329 | 1652 | "current_config__interface__ip_addresses__subnet__cidr", | ||
330 | 1653 | not_subnets, | ||
331 | 1654 | exclude=True, | ||
332 | 1655 | ) | ||
333 | 1656 | return filtered_nodes | ||
334 | 1657 | |||
335 | 1658 | def filter_by_hostnames(self, filtered_nodes): | ||
336 | 1659 | hostnames = self.cleaned_data.get(self.get_field_name("hostname")) | ||
337 | 1660 | if hostnames: | ||
338 | 1661 | filtered_nodes = self._substring_filter( | ||
339 | 1662 | filtered_nodes, "hostname", hostnames | ||
340 | 1663 | ) | ||
341 | 1664 | return filtered_nodes | ||
342 | 1665 | |||
343 | 1666 | def clean_mac_address(self): | ||
344 | 1667 | value = self.cleaned_data.get(self.get_field_name("mac_address")) | ||
345 | 1668 | if value: | ||
346 | 1669 | interfaces = self._substring_filter( | ||
347 | 1670 | Interface.objects, "mac_address", value | ||
348 | 1671 | ) | ||
349 | 1672 | if not interfaces: | ||
350 | 1673 | set_form_error( | ||
351 | 1674 | self, | ||
352 | 1675 | self.get_field_name("mac_address"), | ||
353 | 1676 | "no mac address found for %s" % value, | ||
354 | 1677 | ) | ||
355 | 1678 | return None | ||
356 | 1679 | return value | ||
357 | 1680 | |||
358 | 1681 | def filter_by_mac_addresses(self, filtered_nodes): | ||
359 | 1682 | value = self.cleaned_data.get(self.get_field_name("mac_address")) | ||
360 | 1683 | if value: | ||
361 | 1684 | interfaces = self._substring_filter( | ||
362 | 1685 | Interface.objects, "mac_address", value | ||
363 | 1686 | ) | ||
364 | 1687 | filtered_nodes = filtered_nodes.filter( | ||
365 | 1688 | current_config__interface__in=interfaces | ||
366 | 1689 | ) | ||
367 | 1690 | return filtered_nodes | ||
368 | 1691 | |||
369 | 1692 | def filter_by_agent_name(self, filtered_nodes): | ||
370 | 1693 | agent_name = self.cleaned_data[self.get_field_name("agent_name")] | ||
371 | 1694 | if agent_name: | ||
372 | 1695 | filtered_nodes = self._substring_filter( | ||
373 | 1696 | filtered_nodes, "agent_name", agent_name | ||
374 | 1697 | ) | ||
375 | 1698 | 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 | 24 | NODE_STATUS, | 24 | NODE_STATUS, |
382 | 25 | POWER_STATE, | 25 | POWER_STATE, |
383 | 26 | ) | 26 | ) |
385 | 27 | from maasserver.models import Domain, Machine, NodeDevice, Zone | 27 | from maasserver.models import Domain, Machine, NodeDevice, Tag, Zone |
386 | 28 | from maasserver.node_constraint_filter_forms import ( | 28 | from maasserver.node_constraint_filter_forms import ( |
387 | 29 | AcquireNodeForm, | 29 | AcquireNodeForm, |
388 | 30 | detect_nonexistent_names, | 30 | detect_nonexistent_names, |
389 | 31 | FilterNodeForm, | 31 | FilterNodeForm, |
390 | 32 | FreeTextFilterNodeForm, | ||
391 | 32 | generate_architecture_wildcards, | 33 | generate_architecture_wildcards, |
392 | 33 | get_architecture_wildcards, | 34 | get_architecture_wildcards, |
393 | 34 | get_storage_constraints_from_string, | 35 | get_storage_constraints_from_string, |
394 | @@ -1592,6 +1593,242 @@ class TestFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin): | |||
395 | 1592 | self.assertEqual(constraints.keys(), described_constraints) | 1593 | self.assertEqual(constraints.keys(), described_constraints) |
396 | 1593 | 1594 | ||
397 | 1594 | 1595 | ||
398 | 1596 | class TestFreeTextFilterNodeForm(MAASServerTestCase, FilterConstraintsMixin): | ||
399 | 1597 | def test_substring_filter_one_substring(self): | ||
400 | 1598 | name = factory.make_name("hostname") | ||
401 | 1599 | node1 = factory.make_Node(hostname=name) | ||
402 | 1600 | node2 = factory.make_Node() | ||
403 | 1601 | form = FreeTextFilterNodeForm(data={}) | ||
404 | 1602 | result = form._substring_filter( | ||
405 | 1603 | Machine.objects, "hostname", name[0 : len("hostname-") + 1] | ||
406 | 1604 | ) | ||
407 | 1605 | self.assertIn(node1, list(result)) | ||
408 | 1606 | self.assertNotIn(node2, list(result)) | ||
409 | 1607 | |||
410 | 1608 | def test_substring_filter_list_of_substrings(self): | ||
411 | 1609 | tags = [ | ||
412 | 1610 | factory.make_Tag(name=factory.make_name("tag")) for _ in range(3) | ||
413 | 1611 | ] | ||
414 | 1612 | form = FreeTextFilterNodeForm(data={}) | ||
415 | 1613 | # +1 to include first letter in random suffix | ||
416 | 1614 | result = form._substring_filter( | ||
417 | 1615 | Tag.objects, | ||
418 | 1616 | "name", | ||
419 | 1617 | [tag.name[0 : len("tag-") + 1] for tag in tags], | ||
420 | 1618 | ) | ||
421 | 1619 | self.assertCountEqual(tags, list(result)) | ||
422 | 1620 | |||
423 | 1621 | def test_substring_filter_exact_match(self): | ||
424 | 1622 | name = factory.make_name("hostname") | ||
425 | 1623 | node1 = factory.make_Node(hostname=name) | ||
426 | 1624 | node2 = factory.make_Node() | ||
427 | 1625 | form = FreeTextFilterNodeForm(data={}) | ||
428 | 1626 | result = form._substring_filter(Machine.objects, "hostname", name) | ||
429 | 1627 | self.assertIn(node1, list(result)) | ||
430 | 1628 | self.assertNotIn(node2, list(result)) | ||
431 | 1629 | |||
432 | 1630 | def test_substring_arch_filter(self): | ||
433 | 1631 | architecture = factory.make_name("arch") | ||
434 | 1632 | subarch = factory.make_name() | ||
435 | 1633 | arch = "/".join([architecture, subarch]) | ||
436 | 1634 | factory.make_usable_boot_resource(architecture=arch) | ||
437 | 1635 | node1 = factory.make_Node(architecture=arch) | ||
438 | 1636 | node2 = factory.make_Node() | ||
439 | 1637 | constraints = { | ||
440 | 1638 | "arch": arch[:2], | ||
441 | 1639 | } | ||
442 | 1640 | form = FreeTextFilterNodeForm(data=constraints) | ||
443 | 1641 | self.assertTrue(form.is_valid()) | ||
444 | 1642 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
445 | 1643 | self.assertIn(node1, filtered_nodes) | ||
446 | 1644 | self.assertNotIn(node2, filtered_nodes) | ||
447 | 1645 | |||
448 | 1646 | def test_substring_tag_filter(self): | ||
449 | 1647 | tags = [ | ||
450 | 1648 | factory.make_Tag(name=factory.make_name("tag")) for _ in range(3) | ||
451 | 1649 | ] | ||
452 | 1650 | node1 = factory.make_Node() | ||
453 | 1651 | node2 = factory.make_Node() | ||
454 | 1652 | [node1.tags.add(tag) for tag in tags] | ||
455 | 1653 | constraints = { | ||
456 | 1654 | "tags": [tag.name[: len("tag-") + 1] for tag in tags], | ||
457 | 1655 | } | ||
458 | 1656 | form = FreeTextFilterNodeForm(data=constraints) | ||
459 | 1657 | self.assertTrue(form.is_valid()) | ||
460 | 1658 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
461 | 1659 | self.assertIn(node1, filtered_nodes) | ||
462 | 1660 | self.assertNotIn(node2, filtered_nodes) | ||
463 | 1661 | |||
464 | 1662 | def test_substring_zone_filter(self): | ||
465 | 1663 | zone = factory.make_Zone() | ||
466 | 1664 | node1 = factory.make_Node(zone=zone) | ||
467 | 1665 | node2 = factory.make_Node() | ||
468 | 1666 | constraints = { | ||
469 | 1667 | "zone": zone.name[:2], | ||
470 | 1668 | } | ||
471 | 1669 | form = FreeTextFilterNodeForm(data=constraints) | ||
472 | 1670 | self.assertTrue(form.is_valid()) | ||
473 | 1671 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
474 | 1672 | self.assertIn(node1, filtered_nodes) | ||
475 | 1673 | self.assertNotIn(node2, filtered_nodes) | ||
476 | 1674 | |||
477 | 1675 | def test_substring_not_in_zone_filter(self): | ||
478 | 1676 | zone = factory.make_Zone() | ||
479 | 1677 | node1 = factory.make_Node(zone=zone) | ||
480 | 1678 | node2 = factory.make_Node() | ||
481 | 1679 | constraints = { | ||
482 | 1680 | "not_in_zone": [zone.name[:2]], | ||
483 | 1681 | } | ||
484 | 1682 | form = FreeTextFilterNodeForm(data=constraints) | ||
485 | 1683 | self.assertTrue(form.is_valid()) | ||
486 | 1684 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
487 | 1685 | self.assertNotIn(node1, filtered_nodes) | ||
488 | 1686 | self.assertIn(node2, filtered_nodes) | ||
489 | 1687 | |||
490 | 1688 | def test_substring_pool_filter(self): | ||
491 | 1689 | pool = factory.make_ResourcePool() | ||
492 | 1690 | node1 = factory.make_Node(pool=pool) | ||
493 | 1691 | node2 = factory.make_Node() | ||
494 | 1692 | constraints = { | ||
495 | 1693 | "pool": pool.name[: len("resourcepool-") + 1], | ||
496 | 1694 | } | ||
497 | 1695 | form = FreeTextFilterNodeForm(data=constraints) | ||
498 | 1696 | self.assertTrue(form.is_valid()) | ||
499 | 1697 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
500 | 1698 | self.assertIn(node1, filtered_nodes) | ||
501 | 1699 | self.assertNotIn(node2, filtered_nodes) | ||
502 | 1700 | |||
503 | 1701 | def test_substring_not_in_pool_filter(self): | ||
504 | 1702 | pool = factory.make_ResourcePool() | ||
505 | 1703 | node1 = factory.make_Node(pool=pool) | ||
506 | 1704 | node2 = factory.make_Node() | ||
507 | 1705 | constraints = { | ||
508 | 1706 | "not_in_pool": [pool.name[: len("resourcepool-") + 1]], | ||
509 | 1707 | } | ||
510 | 1708 | form = FreeTextFilterNodeForm(data=constraints) | ||
511 | 1709 | self.assertTrue(form.is_valid()) | ||
512 | 1710 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
513 | 1711 | self.assertNotIn(node1, filtered_nodes) | ||
514 | 1712 | self.assertIn(node2, filtered_nodes) | ||
515 | 1713 | |||
516 | 1714 | def test_substring_pod_filter(self): | ||
517 | 1715 | pod = factory.make_Pod() | ||
518 | 1716 | node1 = factory.make_Node(bmc=pod.as_bmc()) | ||
519 | 1717 | node2 = factory.make_Node() | ||
520 | 1718 | constraints = {"pod": pod.name[: len("pod-") + 1]} | ||
521 | 1719 | form = FreeTextFilterNodeForm(data=constraints) | ||
522 | 1720 | self.assertTrue(form.is_valid()) | ||
523 | 1721 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
524 | 1722 | self.assertIn(node1, filtered_nodes) | ||
525 | 1723 | self.assertNotIn(node2, filtered_nodes) | ||
526 | 1724 | |||
527 | 1725 | def test_substring_fabrics_filter(self): | ||
528 | 1726 | fabric = factory.make_Fabric() | ||
529 | 1727 | vlan = factory.make_VLAN(fabric=fabric) | ||
530 | 1728 | interface = factory.make_Interface(vlan=vlan) | ||
531 | 1729 | node1 = factory.make_Node() | ||
532 | 1730 | node1.current_config.interface_set.add(interface) | ||
533 | 1731 | node2 = factory.make_Node() | ||
534 | 1732 | constraints = { | ||
535 | 1733 | "fabrics": [fabric.name[:2]], | ||
536 | 1734 | } | ||
537 | 1735 | form = FreeTextFilterNodeForm(data=constraints) | ||
538 | 1736 | self.assertTrue(form.is_valid()) | ||
539 | 1737 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
540 | 1738 | self.assertIn(node1, filtered_nodes) | ||
541 | 1739 | self.assertNotIn(node2, filtered_nodes) | ||
542 | 1740 | |||
543 | 1741 | def test_substring_fabric_classes_filter(self): | ||
544 | 1742 | fabric_class = factory.make_name() | ||
545 | 1743 | fabric = factory.make_Fabric(class_type=fabric_class) | ||
546 | 1744 | vlan = factory.make_VLAN(fabric=fabric) | ||
547 | 1745 | interface = factory.make_Interface(vlan=vlan) | ||
548 | 1746 | node1 = factory.make_Node() | ||
549 | 1747 | node1.current_config.interface_set.add(interface) | ||
550 | 1748 | node2 = factory.make_Node() | ||
551 | 1749 | constraints = { | ||
552 | 1750 | "fabric_classes": [fabric_class[:2]], | ||
553 | 1751 | } | ||
554 | 1752 | form = FreeTextFilterNodeForm(data=constraints) | ||
555 | 1753 | form.is_valid() | ||
556 | 1754 | print(form.errors) | ||
557 | 1755 | self.assertTrue(form.is_valid()) | ||
558 | 1756 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
559 | 1757 | self.assertIn(node1, filtered_nodes) | ||
560 | 1758 | self.assertNotIn(node2, filtered_nodes) | ||
561 | 1759 | |||
562 | 1760 | def test_substring_vlans_filter(self): | ||
563 | 1761 | vlan = factory.make_VLAN(name=factory.make_name()) | ||
564 | 1762 | interface = factory.make_Interface(vlan=vlan) | ||
565 | 1763 | node1 = factory.make_Node() | ||
566 | 1764 | node1.current_config.interface_set.add(interface) | ||
567 | 1765 | node2 = factory.make_Node() | ||
568 | 1766 | constraints = { | ||
569 | 1767 | "vlans": [vlan.name[:2]], | ||
570 | 1768 | } | ||
571 | 1769 | form = FreeTextFilterNodeForm(data=constraints) | ||
572 | 1770 | self.assertTrue(form.is_valid()) | ||
573 | 1771 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
574 | 1772 | self.assertIn(node1, filtered_nodes) | ||
575 | 1773 | self.assertNotIn(node2, filtered_nodes) | ||
576 | 1774 | |||
577 | 1775 | def test_substring_subnet_filter(self): | ||
578 | 1776 | subnet = factory.make_Subnet() | ||
579 | 1777 | interface = factory.make_Interface(subnet=subnet) | ||
580 | 1778 | node1 = factory.make_Node() | ||
581 | 1779 | node1.current_config.interface_set.add(interface) | ||
582 | 1780 | node2 = factory.make_Node() | ||
583 | 1781 | constraints = { | ||
584 | 1782 | "subnets": [str(subnet.cidr)[:3]], | ||
585 | 1783 | } | ||
586 | 1784 | form = FreeTextFilterNodeForm(data=constraints) | ||
587 | 1785 | self.assertTrue(form.is_valid()) | ||
588 | 1786 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
589 | 1787 | self.assertIn(node1, filtered_nodes) | ||
590 | 1788 | self.assertNotIn(node2, filtered_nodes) | ||
591 | 1789 | |||
592 | 1790 | def test_substring_hostnames_filter(self): | ||
593 | 1791 | hostname = factory.make_name() | ||
594 | 1792 | node1 = factory.make_Node(hostname=hostname) | ||
595 | 1793 | node2 = factory.make_Node() | ||
596 | 1794 | constraints = { | ||
597 | 1795 | "hostname": [hostname[:2]], | ||
598 | 1796 | } | ||
599 | 1797 | form = FreeTextFilterNodeForm(data=constraints) | ||
600 | 1798 | self.assertTrue(form.is_valid()) | ||
601 | 1799 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
602 | 1800 | self.assertIn(node1, filtered_nodes) | ||
603 | 1801 | self.assertNotIn(node2, filtered_nodes) | ||
604 | 1802 | |||
605 | 1803 | def test_substring_mac_addresses_filter(self): | ||
606 | 1804 | mac_address = factory.make_mac_address() | ||
607 | 1805 | interface = factory.make_Interface(mac_address=mac_address) | ||
608 | 1806 | node1 = factory.make_Node() | ||
609 | 1807 | node1.current_config.interface_set.add(interface) | ||
610 | 1808 | node2 = factory.make_Node() | ||
611 | 1809 | constraints = { | ||
612 | 1810 | "mac_address": [str(mac_address)[:4]], | ||
613 | 1811 | } | ||
614 | 1812 | form = FreeTextFilterNodeForm(data=constraints) | ||
615 | 1813 | self.assertTrue(form.is_valid()) | ||
616 | 1814 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
617 | 1815 | self.assertIn(node1, filtered_nodes) | ||
618 | 1816 | self.assertNotIn(node2, filtered_nodes) | ||
619 | 1817 | |||
620 | 1818 | def test_substring_agent_name_filter(self): | ||
621 | 1819 | name = factory.make_name() | ||
622 | 1820 | node1 = factory.make_Node(agent_name=name) | ||
623 | 1821 | node2 = factory.make_Node() | ||
624 | 1822 | constraints = { | ||
625 | 1823 | "agent_name": name, | ||
626 | 1824 | } | ||
627 | 1825 | form = FreeTextFilterNodeForm(data=constraints) | ||
628 | 1826 | self.assertTrue(form.is_valid()) | ||
629 | 1827 | filtered_nodes = form.filter_nodes(Machine.objects.all())[0] | ||
630 | 1828 | self.assertIn(node1, filtered_nodes) | ||
631 | 1829 | self.assertNotIn(node2, filtered_nodes) | ||
632 | 1830 | |||
633 | 1831 | |||
634 | 1595 | class TestAcquireNodeForm(MAASServerTestCase, FilterConstraintsMixin): | 1832 | class TestAcquireNodeForm(MAASServerTestCase, FilterConstraintsMixin): |
635 | 1596 | 1833 | ||
636 | 1597 | form_class = AcquireNodeForm | 1834 | 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 | 45 | from maasserver.models.nodeprobeddetails import script_output_nsmap | 45 | from maasserver.models.nodeprobeddetails import script_output_nsmap |
643 | 46 | from maasserver.node_action import compile_node_actions | 46 | from maasserver.node_action import compile_node_actions |
644 | 47 | from maasserver.node_constraint_filter_forms import ( | 47 | from maasserver.node_constraint_filter_forms import ( |
645 | 48 | FreeTextFilterNodeForm, | ||
646 | 48 | GROUPABLE_FIELDS, | 49 | GROUPABLE_FIELDS, |
647 | 49 | ReadNodesForm, | ||
648 | 50 | STATIC_FILTER_FIELDS, | 50 | STATIC_FILTER_FIELDS, |
649 | 51 | ) | 51 | ) |
650 | 52 | from maasserver.permissions import NodePermission | 52 | from maasserver.permissions import NodePermission |
651 | @@ -1200,7 +1200,7 @@ class NodeHandler(TimestampedModelHandler): | |||
652 | 1200 | 1200 | ||
653 | 1201 | def _filter(self, qs, action, params): | 1201 | def _filter(self, qs, action, params): |
654 | 1202 | qs = super()._filter(qs, action, params) | 1202 | qs = super()._filter(qs, action, params) |
656 | 1203 | form = ReadNodesForm(data=params) | 1203 | form = FreeTextFilterNodeForm(data=params) |
657 | 1204 | if not form.is_valid(): | 1204 | if not form.is_valid(): |
658 | 1205 | raise HandlerValidationError(form.errors) | 1205 | raise HandlerValidationError(form.errors) |
659 | 1206 | qs, _, _ = form.filter_nodes(qs) | 1206 | qs, _, _ = form.filter_nodes(qs) |
660 | @@ -1215,7 +1215,7 @@ class NodeHandler(TimestampedModelHandler): | |||
661 | 1215 | "dynamic": name not in STATIC_FILTER_FIELDS, | 1215 | "dynamic": name not in STATIC_FILTER_FIELDS, |
662 | 1216 | "for_grouping": name in GROUPABLE_FIELDS, | 1216 | "for_grouping": name in GROUPABLE_FIELDS, |
663 | 1217 | } | 1217 | } |
665 | 1218 | for name, field in ReadNodesForm.declared_fields.items() | 1218 | for name, field in FreeTextFilterNodeForm.declared_fields.items() |
666 | 1219 | ] | 1219 | ] |
667 | 1220 | 1220 | ||
668 | 1221 | def _get_dynamic_filter_options(self, key): | 1221 | def _get_dynamic_filter_options(self, key): |
669 | @@ -1316,7 +1316,7 @@ class NodeHandler(TimestampedModelHandler): | |||
670 | 1316 | "a 'group_key' param must be provided for filter_options" | 1316 | "a 'group_key' param must be provided for filter_options" |
671 | 1317 | ) | 1317 | ) |
672 | 1318 | else: | 1318 | else: |
674 | 1319 | if key not in ReadNodesForm.declared_fields.keys(): | 1319 | if key not in FreeTextFilterNodeForm.declared_fields.keys(): |
675 | 1320 | raise HandlerValidationError( | 1320 | raise HandlerValidationError( |
676 | 1321 | f"{key} is not a valid 'group_key' for filter_options" | 1321 | f"{key} is not a valid 'group_key' for filter_options" |
677 | 1322 | ) | 1322 | ) |
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