Merge lp:~rvb/maas/backport-fixes into lp:maas/1.2
- backport-fixes
- Merge into 1.2
Proposed by
Raphaël Badin
Status: | Merged |
---|---|
Approved by: | Raphaël Badin |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1375 |
Proposed branch: | lp:~rvb/maas/backport-fixes |
Merge into: | lp:maas/1.2 |
Diff against target: |
307 lines (+116/-29) 6 files modified
src/maasserver/api.py (+29/-3) src/maasserver/models/node.py (+2/-0) src/maasserver/testing/factory.py (+19/-0) src/maasserver/tests/test_api.py (+46/-0) src/maasserver/tests/test_forms.py (+7/-26) src/maasserver/tests/test_node.py (+13/-0) |
To merge this branch: | bzr merge lp:~rvb/maas/backport-fixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Review via email: mp+160809@code.launchpad.net |
Commit message
Backport revisions 1468 and 1469.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api.py' | |||
2 | --- src/maasserver/api.py 2013-03-12 16:28:26 +0000 | |||
3 | +++ src/maasserver/api.py 2013-04-25 07:58:26 +0000 | |||
4 | @@ -65,6 +65,7 @@ | |||
5 | 65 | "FilesHandler", | 65 | "FilesHandler", |
6 | 66 | "get_oauth_token", | 66 | "get_oauth_token", |
7 | 67 | "MaasHandler", | 67 | "MaasHandler", |
8 | 68 | "NodeGroupHandler", | ||
9 | 68 | "NodeGroupsHandler", | 69 | "NodeGroupsHandler", |
10 | 69 | "NodeGroupInterfaceHandler", | 70 | "NodeGroupInterfaceHandler", |
11 | 70 | "NodeGroupInterfacesHandler", | 71 | "NodeGroupInterfacesHandler", |
12 | @@ -149,6 +150,7 @@ | |||
13 | 149 | from maasserver.forms import ( | 150 | from maasserver.forms import ( |
14 | 150 | get_node_create_form, | 151 | get_node_create_form, |
15 | 151 | get_node_edit_form, | 152 | get_node_edit_form, |
16 | 153 | NodeGroupEdit, | ||
17 | 152 | NodeGroupInterfaceForm, | 154 | NodeGroupInterfaceForm, |
18 | 153 | NodeGroupWithInterfacesForm, | 155 | NodeGroupWithInterfacesForm, |
19 | 154 | TagForm, | 156 | TagForm, |
20 | @@ -457,6 +459,9 @@ | |||
21 | 457 | ) | 459 | ) |
22 | 458 | 460 | ||
23 | 459 | 461 | ||
24 | 462 | METHOD_RESERVED_ADMIN = "That method is reserved for admin users." | ||
25 | 463 | |||
26 | 464 | |||
27 | 460 | def store_node_power_parameters(node, request): | 465 | def store_node_power_parameters(node, request): |
28 | 461 | """Store power parameters in request. | 466 | """Store power parameters in request. |
29 | 462 | 467 | ||
30 | @@ -1363,7 +1368,7 @@ | |||
31 | 1363 | nodegroup.accept() | 1368 | nodegroup.accept() |
32 | 1364 | return HttpResponse("Nodegroup(s) accepted.", status=httplib.OK) | 1369 | return HttpResponse("Nodegroup(s) accepted.", status=httplib.OK) |
33 | 1365 | else: | 1370 | else: |
35 | 1366 | raise PermissionDenied("That method is reserved to admin users.") | 1371 | raise PermissionDenied(METHOD_RESERVED_ADMIN) |
36 | 1367 | 1372 | ||
37 | 1368 | @operation(idempotent=False) | 1373 | @operation(idempotent=False) |
38 | 1369 | def reject(self, request): | 1374 | def reject(self, request): |
39 | @@ -1381,7 +1386,7 @@ | |||
40 | 1381 | nodegroup.reject() | 1386 | nodegroup.reject() |
41 | 1382 | return HttpResponse("Nodegroup(s) rejected.", status=httplib.OK) | 1387 | return HttpResponse("Nodegroup(s) rejected.", status=httplib.OK) |
42 | 1383 | else: | 1388 | else: |
44 | 1384 | raise PermissionDenied("That method is reserved to admin users.") | 1389 | raise PermissionDenied(METHOD_RESERVED_ADMIN) |
45 | 1385 | 1390 | ||
46 | 1386 | @classmethod | 1391 | @classmethod |
47 | 1387 | def resource_uri(cls): | 1392 | def resource_uri(cls): |
48 | @@ -1418,7 +1423,7 @@ | |||
49 | 1418 | Each NodeGroup has its own uuid. | 1423 | Each NodeGroup has its own uuid. |
50 | 1419 | """ | 1424 | """ |
51 | 1420 | 1425 | ||
53 | 1421 | create = update = delete = None | 1426 | create = delete = None |
54 | 1422 | fields = DISPLAYED_NODEGROUP_FIELDS | 1427 | fields = DISPLAYED_NODEGROUP_FIELDS |
55 | 1423 | 1428 | ||
56 | 1424 | def read(self, request, uuid): | 1429 | def read(self, request, uuid): |
57 | @@ -1433,6 +1438,27 @@ | |||
58 | 1433 | uuid = nodegroup.uuid | 1438 | uuid = nodegroup.uuid |
59 | 1434 | return ('nodegroup_handler', [uuid]) | 1439 | return ('nodegroup_handler', [uuid]) |
60 | 1435 | 1440 | ||
61 | 1441 | def update(self, request, uuid): | ||
62 | 1442 | """Update a specific cluster. | ||
63 | 1443 | |||
64 | 1444 | :param name: The new DNS name for this cluster. | ||
65 | 1445 | :type name: basestring | ||
66 | 1446 | :param cluster_name: The new name for this cluster. | ||
67 | 1447 | :type cluster_name: basestring | ||
68 | 1448 | :param status: The new status for this cluster (see | ||
69 | 1449 | vocabulary `NODEGROUP_STATUS`). | ||
70 | 1450 | :type status: int | ||
71 | 1451 | """ | ||
72 | 1452 | if not request.user.is_superuser: | ||
73 | 1453 | raise PermissionDenied(METHOD_RESERVED_ADMIN) | ||
74 | 1454 | nodegroup = get_object_or_404(NodeGroup, uuid=uuid) | ||
75 | 1455 | data = get_overrided_query_dict(model_to_dict(nodegroup), request.data) | ||
76 | 1456 | form = NodeGroupEdit(instance=nodegroup, data=data) | ||
77 | 1457 | if form.is_valid(): | ||
78 | 1458 | return form.save() | ||
79 | 1459 | else: | ||
80 | 1460 | raise ValidationError(form.errors) | ||
81 | 1461 | |||
82 | 1436 | @operation(idempotent=False) | 1462 | @operation(idempotent=False) |
83 | 1437 | def update_leases(self, request, uuid): | 1463 | def update_leases(self, request, uuid): |
84 | 1438 | """Submit latest state of DHCP leases within the cluster. | 1464 | """Submit latest state of DHCP leases within the cluster. |
85 | 1439 | 1465 | ||
86 | === modified file 'src/maasserver/models/node.py' | |||
87 | --- src/maasserver/models/node.py 2013-01-14 16:16:57 +0000 | |||
88 | +++ src/maasserver/models/node.py 2013-04-25 07:58:26 +0000 | |||
89 | @@ -395,6 +395,8 @@ | |||
90 | 395 | node.cpu_count = cpu_count or 0 | 395 | node.cpu_count = cpu_count or 0 |
91 | 396 | node.memory = memory | 396 | node.memory = memory |
92 | 397 | for tag in tag_manager.all(): | 397 | for tag in tag_manager.all(): |
93 | 398 | if not tag.definition: | ||
94 | 399 | continue | ||
95 | 398 | has_tag = evaluator(tag.definition) | 400 | has_tag = evaluator(tag.definition) |
96 | 399 | if has_tag: | 401 | if has_tag: |
97 | 400 | node.tags.add(tag) | 402 | node.tags.add(tag) |
98 | 401 | 403 | ||
99 | === modified file 'src/maasserver/testing/factory.py' | |||
100 | --- src/maasserver/testing/factory.py 2013-03-07 16:26:00 +0000 | |||
101 | +++ src/maasserver/testing/factory.py 2013-04-25 07:58:26 +0000 | |||
102 | @@ -210,6 +210,25 @@ | |||
103 | 210 | ng.save() | 210 | ng.save() |
104 | 211 | return ng | 211 | return ng |
105 | 212 | 212 | ||
106 | 213 | def make_unrenamable_nodegroup_with_node(self): | ||
107 | 214 | """Create a `NodeGroup` that can't be renamed, and `Node`. | ||
108 | 215 | |||
109 | 216 | Node groups can't be renamed while they are in an accepted state, have | ||
110 | 217 | DHCP and DNS management enabled, and have a node that is in allocated | ||
111 | 218 | state. | ||
112 | 219 | |||
113 | 220 | :return: tuple: (`NodeGroup`, `Node`). | ||
114 | 221 | """ | ||
115 | 222 | name = self.make_name('original-name') | ||
116 | 223 | nodegroup = self.make_node_group( | ||
117 | 224 | name=name, status=NODEGROUP_STATUS.ACCEPTED) | ||
118 | 225 | interface = nodegroup.get_managed_interface() | ||
119 | 226 | interface.management = NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS | ||
120 | 227 | interface.save() | ||
121 | 228 | node = self.make_node( | ||
122 | 229 | nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED) | ||
123 | 230 | return nodegroup, node | ||
124 | 231 | |||
125 | 213 | def make_node_group_interface(self, nodegroup, ip=None, | 232 | def make_node_group_interface(self, nodegroup, ip=None, |
126 | 214 | router_ip=None, network=None, | 233 | router_ip=None, network=None, |
127 | 215 | subnet_mask=None, broadcast_ip=None, | 234 | subnet_mask=None, broadcast_ip=None, |
128 | 216 | 235 | ||
129 | === modified file 'src/maasserver/tests/test_api.py' | |||
130 | --- src/maasserver/tests/test_api.py 2013-03-12 16:28:26 +0000 | |||
131 | +++ src/maasserver/tests/test_api.py 2013-04-25 07:58:26 +0000 | |||
132 | @@ -77,6 +77,7 @@ | |||
133 | 77 | NODE_STATUS, | 77 | NODE_STATUS, |
134 | 78 | NODE_STATUS_CHOICES_DICT, | 78 | NODE_STATUS_CHOICES_DICT, |
135 | 79 | NODEGROUP_STATUS, | 79 | NODEGROUP_STATUS, |
136 | 80 | NODEGROUP_STATUS_CHOICES, | ||
137 | 80 | NODEGROUPINTERFACE_MANAGEMENT, | 81 | NODEGROUPINTERFACE_MANAGEMENT, |
138 | 81 | ) | 82 | ) |
139 | 82 | from maasserver.exceptions import ( | 83 | from maasserver.exceptions import ( |
140 | @@ -4394,6 +4395,51 @@ | |||
141 | 4394 | self.get_uri('nodegroups/%s/' % factory.make_name('nodegroup'))) | 4395 | self.get_uri('nodegroups/%s/' % factory.make_name('nodegroup'))) |
142 | 4395 | self.assertEqual(httplib.NOT_FOUND, response.status_code) | 4396 | self.assertEqual(httplib.NOT_FOUND, response.status_code) |
143 | 4396 | 4397 | ||
144 | 4398 | def test_PUT_reserved_to_admin_users(self): | ||
145 | 4399 | nodegroup = factory.make_node_group() | ||
146 | 4400 | response = self.client.put( | ||
147 | 4401 | reverse('nodegroup_handler', args=[nodegroup.uuid]), | ||
148 | 4402 | {'name': factory.make_name("new-name")}) | ||
149 | 4403 | |||
150 | 4404 | self.assertEqual(httplib.FORBIDDEN, response.status_code) | ||
151 | 4405 | |||
152 | 4406 | def test_PUT_updates_nodegroup(self): | ||
153 | 4407 | # The api allows the updating of a NodeGroup. | ||
154 | 4408 | nodegroup = factory.make_node_group() | ||
155 | 4409 | self.become_admin() | ||
156 | 4410 | new_name = factory.make_name("new-name") | ||
157 | 4411 | new_cluster_name = factory.make_name("new-cluster-name") | ||
158 | 4412 | new_status = factory.getRandomChoice( | ||
159 | 4413 | NODEGROUP_STATUS_CHOICES, but_not=[nodegroup.status]) | ||
160 | 4414 | response = self.client.put( | ||
161 | 4415 | reverse('nodegroup_handler', args=[nodegroup.uuid]), | ||
162 | 4416 | { | ||
163 | 4417 | 'name': new_name, | ||
164 | 4418 | 'cluster_name': new_cluster_name, | ||
165 | 4419 | 'status': new_status, | ||
166 | 4420 | }) | ||
167 | 4421 | |||
168 | 4422 | self.assertEqual(httplib.OK, response.status_code, response.content) | ||
169 | 4423 | nodegroup = reload_object(nodegroup) | ||
170 | 4424 | self.assertEqual( | ||
171 | 4425 | (new_name, new_cluster_name, new_status), | ||
172 | 4426 | (nodegroup.name, nodegroup.cluster_name, nodegroup.status)) | ||
173 | 4427 | |||
174 | 4428 | def test_PUT_updates_nodegroup_validates_data(self): | ||
175 | 4429 | nodegroup, _ = factory.make_unrenamable_nodegroup_with_node() | ||
176 | 4430 | self.become_admin() | ||
177 | 4431 | new_name = factory.make_name("new-name") | ||
178 | 4432 | response = self.client.put( | ||
179 | 4433 | reverse('nodegroup_handler', args=[nodegroup.uuid]), | ||
180 | 4434 | {'name': new_name}) | ||
181 | 4435 | |||
182 | 4436 | parsed_result = json.loads(response.content) | ||
183 | 4437 | |||
184 | 4438 | self.assertEqual(httplib.BAD_REQUEST, response.status_code) | ||
185 | 4439 | self.assertIn( | ||
186 | 4440 | "Can't rename DNS zone", | ||
187 | 4441 | parsed_result['name'][0]) | ||
188 | 4442 | |||
189 | 4397 | def test_update_leases_processes_empty_leases_dict(self): | 4443 | def test_update_leases_processes_empty_leases_dict(self): |
190 | 4398 | nodegroup = factory.make_node_group() | 4444 | nodegroup = factory.make_node_group() |
191 | 4399 | factory.make_dhcp_lease(nodegroup=nodegroup) | 4445 | factory.make_dhcp_lease(nodegroup=nodegroup) |
192 | 4400 | 4446 | ||
193 | === modified file 'src/maasserver/tests/test_forms.py' | |||
194 | --- src/maasserver/tests/test_forms.py 2012-12-04 02:31:07 +0000 | |||
195 | +++ src/maasserver/tests/test_forms.py 2013-04-25 07:58:26 +0000 | |||
196 | @@ -878,25 +878,6 @@ | |||
197 | 878 | ]) | 878 | ]) |
198 | 879 | 879 | ||
199 | 880 | 880 | ||
200 | 881 | def make_unrenamable_nodegroup_with_node(): | ||
201 | 882 | """Create a `NodeGroup` that can't be renamed, and `Node`. | ||
202 | 883 | |||
203 | 884 | Node groups can't be renamed while they are in an accepted state, have | ||
204 | 885 | DHCP and DNS management enabled, and have a node that is in allocated | ||
205 | 886 | state. | ||
206 | 887 | |||
207 | 888 | :return: tuple: (`NodeGroup`, `Node`). | ||
208 | 889 | """ | ||
209 | 890 | name = factory.make_name('original-name') | ||
210 | 891 | nodegroup = factory.make_node_group( | ||
211 | 892 | name=name, status=NODEGROUP_STATUS.ACCEPTED) | ||
212 | 893 | interface = nodegroup.get_managed_interface() | ||
213 | 894 | interface.management = NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS | ||
214 | 895 | interface.save() | ||
215 | 896 | node = factory.make_node(nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED) | ||
216 | 897 | return nodegroup, node | ||
217 | 898 | |||
218 | 899 | |||
219 | 900 | class TestNodeGroupEdit(TestCase): | 881 | class TestNodeGroupEdit(TestCase): |
220 | 901 | 882 | ||
221 | 902 | def make_form_data(self, nodegroup): | 883 | def make_form_data(self, nodegroup): |
222 | @@ -918,14 +899,14 @@ | |||
223 | 918 | self.assertEqual(new_name, reload_object(nodegroup).name) | 899 | self.assertEqual(new_name, reload_object(nodegroup).name) |
224 | 919 | 900 | ||
225 | 920 | def test_refuses_name_change_if_dns_managed_and_nodes_in_use(self): | 901 | def test_refuses_name_change_if_dns_managed_and_nodes_in_use(self): |
227 | 921 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 902 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
228 | 922 | data = self.make_form_data(nodegroup) | 903 | data = self.make_form_data(nodegroup) |
229 | 923 | data['name'] = factory.make_name('new-name') | 904 | data['name'] = factory.make_name('new-name') |
230 | 924 | form = NodeGroupEdit(instance=nodegroup, data=data) | 905 | form = NodeGroupEdit(instance=nodegroup, data=data) |
231 | 925 | self.assertFalse(form.is_valid()) | 906 | self.assertFalse(form.is_valid()) |
232 | 926 | 907 | ||
233 | 927 | def test_accepts_unchanged_name(self): | 908 | def test_accepts_unchanged_name(self): |
235 | 928 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 909 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
236 | 929 | original_name = nodegroup.name | 910 | original_name = nodegroup.name |
237 | 930 | form = NodeGroupEdit( | 911 | form = NodeGroupEdit( |
238 | 931 | instance=nodegroup, data=self.make_form_data(nodegroup)) | 912 | instance=nodegroup, data=self.make_form_data(nodegroup)) |
239 | @@ -934,7 +915,7 @@ | |||
240 | 934 | self.assertEqual(original_name, reload_object(nodegroup).name) | 915 | self.assertEqual(original_name, reload_object(nodegroup).name) |
241 | 935 | 916 | ||
242 | 936 | def test_accepts_omitted_name(self): | 917 | def test_accepts_omitted_name(self): |
244 | 937 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 918 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
245 | 938 | original_name = nodegroup.name | 919 | original_name = nodegroup.name |
246 | 939 | data = self.make_form_data(nodegroup) | 920 | data = self.make_form_data(nodegroup) |
247 | 940 | del data['name'] | 921 | del data['name'] |
248 | @@ -944,7 +925,7 @@ | |||
249 | 944 | self.assertEqual(original_name, reload_object(nodegroup).name) | 925 | self.assertEqual(original_name, reload_object(nodegroup).name) |
250 | 945 | 926 | ||
251 | 946 | def test_accepts_name_change_if_nodegroup_not_accepted(self): | 927 | def test_accepts_name_change_if_nodegroup_not_accepted(self): |
253 | 947 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 928 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
254 | 948 | nodegroup.status = NODEGROUP_STATUS.PENDING | 929 | nodegroup.status = NODEGROUP_STATUS.PENDING |
255 | 949 | data = self.make_form_data(nodegroup) | 930 | data = self.make_form_data(nodegroup) |
256 | 950 | data['name'] = factory.make_name('new-name') | 931 | data['name'] = factory.make_name('new-name') |
257 | @@ -952,7 +933,7 @@ | |||
258 | 952 | self.assertTrue(form.is_valid()) | 933 | self.assertTrue(form.is_valid()) |
259 | 953 | 934 | ||
260 | 954 | def test_accepts_name_change_if_dns_managed_but_no_nodes_in_use(self): | 935 | def test_accepts_name_change_if_dns_managed_but_no_nodes_in_use(self): |
262 | 955 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 936 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
263 | 956 | node.status = NODE_STATUS.READY | 937 | node.status = NODE_STATUS.READY |
264 | 957 | node.save() | 938 | node.save() |
265 | 958 | data = self.make_form_data(nodegroup) | 939 | data = self.make_form_data(nodegroup) |
266 | @@ -963,7 +944,7 @@ | |||
267 | 963 | self.assertEqual(data['name'], reload_object(nodegroup).name) | 944 | self.assertEqual(data['name'], reload_object(nodegroup).name) |
268 | 964 | 945 | ||
269 | 965 | def test_accepts_name_change_if_nodes_in_use_but_dns_not_managed(self): | 946 | def test_accepts_name_change_if_nodes_in_use_but_dns_not_managed(self): |
271 | 966 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 947 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
272 | 967 | interface = nodegroup.get_managed_interface() | 948 | interface = nodegroup.get_managed_interface() |
273 | 968 | interface.management = NODEGROUPINTERFACE_MANAGEMENT.DHCP | 949 | interface.management = NODEGROUPINTERFACE_MANAGEMENT.DHCP |
274 | 969 | interface.save() | 950 | interface.save() |
275 | @@ -975,7 +956,7 @@ | |||
276 | 975 | self.assertEqual(data['name'], reload_object(nodegroup).name) | 956 | self.assertEqual(data['name'], reload_object(nodegroup).name) |
277 | 976 | 957 | ||
278 | 977 | def test_accepts_name_change_if_nodegroup_has_no_interface(self): | 958 | def test_accepts_name_change_if_nodegroup_has_no_interface(self): |
280 | 978 | nodegroup, node = make_unrenamable_nodegroup_with_node() | 959 | nodegroup, node = factory.make_unrenamable_nodegroup_with_node() |
281 | 979 | NodeGroupInterface.objects.filter(nodegroup=nodegroup).delete() | 960 | NodeGroupInterface.objects.filter(nodegroup=nodegroup).delete() |
282 | 980 | data = self.make_form_data(nodegroup) | 961 | data = self.make_form_data(nodegroup) |
283 | 981 | data['name'] = factory.make_name('new-name') | 962 | data['name'] = factory.make_name('new-name') |
284 | 982 | 963 | ||
285 | === modified file 'src/maasserver/tests/test_node.py' | |||
286 | --- src/maasserver/tests/test_node.py 2012-11-16 13:50:43 +0000 | |||
287 | +++ src/maasserver/tests/test_node.py 2013-04-25 07:58:26 +0000 | |||
288 | @@ -586,6 +586,19 @@ | |||
289 | 586 | node = reload_object(node) | 586 | node = reload_object(node) |
290 | 587 | self.assertEqual([], list(node.tags.all())) | 587 | self.assertEqual([], list(node.tags.all())) |
291 | 588 | 588 | ||
292 | 589 | def test_hardware_updates_ignores_empty_tags(self): | ||
293 | 590 | # Tags with empty definitions are ignored when | ||
294 | 591 | # node.set_hardware_details gets called. | ||
295 | 592 | factory.make_tag(definition='') | ||
296 | 593 | node = factory.make_node() | ||
297 | 594 | node.save() | ||
298 | 595 | xmlbytes = '<node/>' | ||
299 | 596 | node.set_hardware_details(xmlbytes) | ||
300 | 597 | node = reload_object(node) | ||
301 | 598 | # The real test is that node.set_hardware_details does not blow | ||
302 | 599 | # up, see bug 1131418. | ||
303 | 600 | self.assertEqual([], list(node.tags.all())) | ||
304 | 601 | |||
305 | 589 | def test_fqdn_returns_hostname_if_dns_not_managed(self): | 602 | def test_fqdn_returns_hostname_if_dns_not_managed(self): |
306 | 590 | nodegroup = factory.make_node_group( | 603 | nodegroup = factory.make_node_group( |
307 | 591 | name=factory.getRandomString(), | 604 | name=factory.getRandomString(), |
Simple backport, self-approving.