Merge lp:~jtv/maas/extract-formtests-node into lp:~maas-committers/maas/trunk
- extract-formtests-node
- Merge into trunk
Proposed by
Jeroen T. Vermeulen
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2816 |
Proposed branch: | lp:~jtv/maas/extract-formtests-node |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
722 lines (+353/-324) 2 files modified
src/maasserver/tests/test_forms.py (+1/-324) src/maasserver/tests/test_forms_node.py (+352/-0) |
To merge this branch: | bzr merge lp:~jtv/maas/extract-formtests-node |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+232170@code.launchpad.net |
Commit message
Extract Node form tests into their own test module.
Description of the change
For self-approval.
Jeroen
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/tests/test_forms.py' | |||
2 | --- src/maasserver/tests/test_forms.py 2014-08-26 05:43:19 +0000 | |||
3 | +++ src/maasserver/tests/test_forms.py 2014-08-26 07:57:29 +0000 | |||
4 | @@ -18,7 +18,6 @@ | |||
5 | 18 | 18 | ||
6 | 19 | from django.conf import settings | 19 | from django.conf import settings |
7 | 20 | from django.core.exceptions import ValidationError | 20 | from django.core.exceptions import ValidationError |
8 | 21 | from maasserver.clusterrpc.power_parameters import get_power_type_choices | ||
9 | 22 | from maasserver.enum import ( | 21 | from maasserver.enum import ( |
10 | 23 | NODE_STATUS, | 22 | NODE_STATUS, |
11 | 24 | NODEGROUP_STATUS, | 23 | NODEGROUP_STATUS, |
12 | @@ -27,7 +26,6 @@ | |||
13 | 27 | from maasserver.forms import ( | 26 | from maasserver.forms import ( |
14 | 28 | AdminNodeForm, | 27 | AdminNodeForm, |
15 | 29 | AdminNodeWithMACAddressesForm, | 28 | AdminNodeWithMACAddressesForm, |
16 | 30 | BLANK_CHOICE, | ||
17 | 31 | ERROR_MESSAGE_STATIC_IPS_OUTSIDE_RANGE, | 29 | ERROR_MESSAGE_STATIC_IPS_OUTSIDE_RANGE, |
18 | 32 | ERROR_MESSAGE_STATIC_RANGE_IN_USE, | 30 | ERROR_MESSAGE_STATIC_RANGE_IN_USE, |
19 | 33 | get_node_create_form, | 31 | get_node_create_form, |
20 | @@ -37,7 +35,6 @@ | |||
21 | 37 | MACAddressForm, | 35 | MACAddressForm, |
22 | 38 | MAX_MESSAGES, | 36 | MAX_MESSAGES, |
23 | 39 | merge_error_messages, | 37 | merge_error_messages, |
24 | 40 | NO_ARCHITECTURES_AVAILABLE, | ||
25 | 41 | NodeForm, | 38 | NodeForm, |
26 | 42 | NodeGroupInterfaceForeignDHCPForm, | 39 | NodeGroupInterfaceForeignDHCPForm, |
27 | 43 | NodeGroupInterfaceForm, | 40 | NodeGroupInterfaceForm, |
28 | @@ -56,17 +53,9 @@ | |||
29 | 56 | ) | 53 | ) |
30 | 57 | from maasserver.models.network import get_name_and_vlan_from_cluster_interface | 54 | from maasserver.models.network import get_name_and_vlan_from_cluster_interface |
31 | 58 | from maasserver.models.staticipaddress import StaticIPAddress | 55 | from maasserver.models.staticipaddress import StaticIPAddress |
36 | 59 | from maasserver.testing.architecture import ( | 56 | from maasserver.testing.architecture import make_usable_architecture |
33 | 60 | make_usable_architecture, | ||
34 | 61 | patch_usable_architectures, | ||
35 | 62 | ) | ||
37 | 63 | from maasserver.testing.factory import factory | 57 | from maasserver.testing.factory import factory |
38 | 64 | from maasserver.testing.orm import reload_object | 58 | from maasserver.testing.orm import reload_object |
39 | 65 | from maasserver.testing.osystems import ( | ||
40 | 66 | make_osystem_with_releases, | ||
41 | 67 | make_usable_osystem, | ||
42 | 68 | patch_usable_osystems, | ||
43 | 69 | ) | ||
44 | 70 | from maasserver.testing.testcase import MAASServerTestCase | 59 | from maasserver.testing.testcase import MAASServerTestCase |
45 | 71 | from maastesting.matchers import MockCalledOnceWith | 60 | from maastesting.matchers import MockCalledOnceWith |
46 | 72 | from netaddr import IPNetwork | 61 | from netaddr import IPNetwork |
47 | @@ -202,318 +191,6 @@ | |||
48 | 202 | AdminNodeWithMACAddressesForm, get_node_create_form(admin)) | 191 | AdminNodeWithMACAddressesForm, get_node_create_form(admin)) |
49 | 203 | 192 | ||
50 | 204 | 193 | ||
51 | 205 | class TestNodeForm(MAASServerTestCase): | ||
52 | 206 | |||
53 | 207 | def test_contains_limited_set_of_fields(self): | ||
54 | 208 | form = NodeForm() | ||
55 | 209 | |||
56 | 210 | self.assertEqual( | ||
57 | 211 | [ | ||
58 | 212 | 'hostname', | ||
59 | 213 | 'architecture', | ||
60 | 214 | 'osystem', | ||
61 | 215 | 'distro_series', | ||
62 | 216 | 'license_key', | ||
63 | 217 | 'disable_ipv4', | ||
64 | 218 | 'nodegroup', | ||
65 | 219 | ], list(form.fields)) | ||
66 | 220 | |||
67 | 221 | def test_changes_node(self): | ||
68 | 222 | node = factory.make_node() | ||
69 | 223 | hostname = factory.make_string() | ||
70 | 224 | patch_usable_architectures(self, [node.architecture]) | ||
71 | 225 | |||
72 | 226 | form = NodeForm( | ||
73 | 227 | data={ | ||
74 | 228 | 'hostname': hostname, | ||
75 | 229 | 'architecture': make_usable_architecture(self), | ||
76 | 230 | }, | ||
77 | 231 | instance=node) | ||
78 | 232 | form.save() | ||
79 | 233 | |||
80 | 234 | self.assertEqual(hostname, node.hostname) | ||
81 | 235 | |||
82 | 236 | def test_accepts_usable_architecture(self): | ||
83 | 237 | arch = make_usable_architecture(self) | ||
84 | 238 | form = NodeForm(data={ | ||
85 | 239 | 'hostname': factory.make_name('host'), | ||
86 | 240 | 'architecture': arch, | ||
87 | 241 | }) | ||
88 | 242 | self.assertTrue(form.is_valid(), form._errors) | ||
89 | 243 | |||
90 | 244 | def test_rejects_unusable_architecture(self): | ||
91 | 245 | patch_usable_architectures(self) | ||
92 | 246 | form = NodeForm(data={ | ||
93 | 247 | 'hostname': factory.make_name('host'), | ||
94 | 248 | 'architecture': factory.make_name('arch'), | ||
95 | 249 | }) | ||
96 | 250 | self.assertFalse(form.is_valid()) | ||
97 | 251 | self.assertItemsEqual(['architecture'], form._errors.keys()) | ||
98 | 252 | |||
99 | 253 | def test_starts_with_default_architecture(self): | ||
100 | 254 | arches = sorted([factory.make_name('arch') for _ in range(5)]) | ||
101 | 255 | patch_usable_architectures(self, arches) | ||
102 | 256 | form = NodeForm() | ||
103 | 257 | self.assertEqual( | ||
104 | 258 | pick_default_architecture(arches), | ||
105 | 259 | form.fields['architecture'].initial) | ||
106 | 260 | |||
107 | 261 | def test_adds_blank_default_when_no_arches_available(self): | ||
108 | 262 | patch_usable_architectures(self, []) | ||
109 | 263 | form = NodeForm() | ||
110 | 264 | self.assertEqual( | ||
111 | 265 | [BLANK_CHOICE], | ||
112 | 266 | form.fields['architecture'].choices) | ||
113 | 267 | |||
114 | 268 | def test_adds_error_when_no_arches_available(self): | ||
115 | 269 | patch_usable_architectures(self, []) | ||
116 | 270 | form = NodeForm() | ||
117 | 271 | self.assertFalse(form.is_valid()) | ||
118 | 272 | self.assertEqual( | ||
119 | 273 | [NO_ARCHITECTURES_AVAILABLE], | ||
120 | 274 | form.errors['architecture']) | ||
121 | 275 | |||
122 | 276 | def test_accepts_osystem(self): | ||
123 | 277 | osystem = make_usable_osystem(self) | ||
124 | 278 | form = NodeForm(data={ | ||
125 | 279 | 'hostname': factory.make_name('host'), | ||
126 | 280 | 'architecture': make_usable_architecture(self), | ||
127 | 281 | 'osystem': osystem.name, | ||
128 | 282 | }) | ||
129 | 283 | self.assertTrue(form.is_valid(), form._errors) | ||
130 | 284 | |||
131 | 285 | def test_rejects_invalid_osystem(self): | ||
132 | 286 | patch_usable_osystems(self) | ||
133 | 287 | form = NodeForm(data={ | ||
134 | 288 | 'hostname': factory.make_name('host'), | ||
135 | 289 | 'architecture': make_usable_architecture(self), | ||
136 | 290 | 'osystem': factory.make_name('os'), | ||
137 | 291 | }) | ||
138 | 292 | self.assertFalse(form.is_valid()) | ||
139 | 293 | self.assertItemsEqual(['osystem'], form._errors.keys()) | ||
140 | 294 | |||
141 | 295 | def test_starts_with_default_osystem(self): | ||
142 | 296 | osystems = [make_osystem_with_releases(self) for _ in range(5)] | ||
143 | 297 | patch_usable_osystems(self, osystems) | ||
144 | 298 | form = NodeForm() | ||
145 | 299 | self.assertEqual( | ||
146 | 300 | '', | ||
147 | 301 | form.fields['osystem'].initial) | ||
148 | 302 | |||
149 | 303 | def test_accepts_osystem_distro_series(self): | ||
150 | 304 | osystem = make_usable_osystem(self) | ||
151 | 305 | release = osystem.get_default_release() | ||
152 | 306 | form = NodeForm(data={ | ||
153 | 307 | 'hostname': factory.make_name('host'), | ||
154 | 308 | 'architecture': make_usable_architecture(self), | ||
155 | 309 | 'osystem': osystem.name, | ||
156 | 310 | 'distro_series': '%s/%s' % (osystem.name, release), | ||
157 | 311 | }) | ||
158 | 312 | self.assertTrue(form.is_valid(), form._errors) | ||
159 | 313 | |||
160 | 314 | def test_rejects_invalid_osystem_distro_series(self): | ||
161 | 315 | osystem = make_usable_osystem(self) | ||
162 | 316 | release = factory.make_name('release') | ||
163 | 317 | form = NodeForm(data={ | ||
164 | 318 | 'hostname': factory.make_name('host'), | ||
165 | 319 | 'architecture': make_usable_architecture(self), | ||
166 | 320 | 'osystem': osystem.name, | ||
167 | 321 | 'distro_series': '%s/%s' % (osystem.name, release), | ||
168 | 322 | }) | ||
169 | 323 | self.assertFalse(form.is_valid()) | ||
170 | 324 | self.assertItemsEqual(['distro_series'], form._errors.keys()) | ||
171 | 325 | |||
172 | 326 | def test_starts_with_default_distro_series(self): | ||
173 | 327 | osystems = [make_osystem_with_releases(self) for _ in range(5)] | ||
174 | 328 | patch_usable_osystems(self, osystems) | ||
175 | 329 | form = NodeForm() | ||
176 | 330 | self.assertEqual( | ||
177 | 331 | '', | ||
178 | 332 | form.fields['distro_series'].initial) | ||
179 | 333 | |||
180 | 334 | def test_rejects_mismatch_osystem_distro_series(self): | ||
181 | 335 | osystem = make_usable_osystem(self) | ||
182 | 336 | release = osystem.get_default_release() | ||
183 | 337 | invalid = factory.make_name('invalid_os') | ||
184 | 338 | form = NodeForm(data={ | ||
185 | 339 | 'hostname': factory.make_name('host'), | ||
186 | 340 | 'architecture': make_usable_architecture(self), | ||
187 | 341 | 'osystem': osystem.name, | ||
188 | 342 | 'distro_series': '%s/%s' % (invalid, release), | ||
189 | 343 | }) | ||
190 | 344 | self.assertFalse(form.is_valid()) | ||
191 | 345 | self.assertItemsEqual(['distro_series'], form._errors.keys()) | ||
192 | 346 | |||
193 | 347 | def test_rejects_missing_license_key(self): | ||
194 | 348 | osystem = make_usable_osystem(self) | ||
195 | 349 | release = osystem.get_default_release() | ||
196 | 350 | self.patch(osystem, 'requires_license_key').return_value = True | ||
197 | 351 | mock_validate = self.patch(osystem, 'validate_license_key') | ||
198 | 352 | mock_validate.return_value = True | ||
199 | 353 | form = NodeForm(data={ | ||
200 | 354 | 'hostname': factory.make_name('host'), | ||
201 | 355 | 'architecture': make_usable_architecture(self), | ||
202 | 356 | 'osystem': osystem.name, | ||
203 | 357 | 'distro_series': '%s/%s*' % (osystem.name, release), | ||
204 | 358 | }) | ||
205 | 359 | self.assertFalse(form.is_valid()) | ||
206 | 360 | self.assertItemsEqual(['license_key'], form._errors.keys()) | ||
207 | 361 | |||
208 | 362 | def test_calls_validate_license_key(self): | ||
209 | 363 | osystem = make_usable_osystem(self) | ||
210 | 364 | release = osystem.get_default_release() | ||
211 | 365 | self.patch(osystem, 'requires_license_key').return_value = True | ||
212 | 366 | mock_validate = self.patch(osystem, 'validate_license_key') | ||
213 | 367 | mock_validate.return_value = True | ||
214 | 368 | form = NodeForm(data={ | ||
215 | 369 | 'hostname': factory.make_name('host'), | ||
216 | 370 | 'architecture': make_usable_architecture(self), | ||
217 | 371 | 'osystem': osystem.name, | ||
218 | 372 | 'distro_series': '%s/%s*' % (osystem.name, release), | ||
219 | 373 | 'license_key': factory.make_string(), | ||
220 | 374 | }) | ||
221 | 375 | self.assertTrue(form.is_valid()) | ||
222 | 376 | mock_validate.assert_called_once() | ||
223 | 377 | |||
224 | 378 | def test_rejects_duplicate_fqdn_with_unmanaged_dns_on_one_nodegroup(self): | ||
225 | 379 | # If a host with a given hostname exists on a managed nodegroup, | ||
226 | 380 | # new nodes on unmanaged nodegroups with hostnames that match | ||
227 | 381 | # that FQDN will be rejected. | ||
228 | 382 | nodegroup = factory.make_node_group( | ||
229 | 383 | status=NODEGROUP_STATUS.ACCEPTED, | ||
230 | 384 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) | ||
231 | 385 | node = factory.make_node( | ||
232 | 386 | hostname=factory.make_name("hostname"), nodegroup=nodegroup) | ||
233 | 387 | other_nodegroup = factory.make_node_group() | ||
234 | 388 | form = NodeForm(data={ | ||
235 | 389 | 'nodegroup': other_nodegroup, | ||
236 | 390 | 'hostname': node.fqdn, | ||
237 | 391 | 'architecture': make_usable_architecture(self), | ||
238 | 392 | }) | ||
239 | 393 | form.instance.nodegroup = other_nodegroup | ||
240 | 394 | self.assertFalse(form.is_valid()) | ||
241 | 395 | |||
242 | 396 | def test_rejects_duplicate_fqdn_on_same_nodegroup(self): | ||
243 | 397 | # If a node with a given FQDN exists on a managed nodegroup, new | ||
244 | 398 | # nodes on that nodegroup with duplicate FQDNs will be rejected. | ||
245 | 399 | nodegroup = factory.make_node_group( | ||
246 | 400 | status=NODEGROUP_STATUS.ACCEPTED, | ||
247 | 401 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) | ||
248 | 402 | node = factory.make_node( | ||
249 | 403 | hostname=factory.make_name("hostname"), nodegroup=nodegroup) | ||
250 | 404 | form = NodeForm(data={ | ||
251 | 405 | 'nodegroup': nodegroup, | ||
252 | 406 | 'hostname': node.fqdn, | ||
253 | 407 | 'architecture': make_usable_architecture(self), | ||
254 | 408 | }) | ||
255 | 409 | form.instance.nodegroup = nodegroup | ||
256 | 410 | self.assertFalse(form.is_valid()) | ||
257 | 411 | |||
258 | 412 | |||
259 | 413 | class TestAdminNodeForm(MAASServerTestCase): | ||
260 | 414 | |||
261 | 415 | def test_AdminNodeForm_contains_limited_set_of_fields(self): | ||
262 | 416 | node = factory.make_node() | ||
263 | 417 | form = AdminNodeForm(instance=node) | ||
264 | 418 | |||
265 | 419 | self.assertEqual( | ||
266 | 420 | [ | ||
267 | 421 | 'hostname', | ||
268 | 422 | 'architecture', | ||
269 | 423 | 'osystem', | ||
270 | 424 | 'distro_series', | ||
271 | 425 | 'license_key', | ||
272 | 426 | 'disable_ipv4', | ||
273 | 427 | 'power_type', | ||
274 | 428 | 'power_parameters', | ||
275 | 429 | 'cpu_count', | ||
276 | 430 | 'memory', | ||
277 | 431 | 'storage', | ||
278 | 432 | 'zone', | ||
279 | 433 | ], | ||
280 | 434 | list(form.fields)) | ||
281 | 435 | |||
282 | 436 | def test_AdminNodeForm_initialises_zone(self): | ||
283 | 437 | # The zone field uses "to_field_name", so that it can refer to a zone | ||
284 | 438 | # by name instead of by ID. A bug in Django breaks initialisation | ||
285 | 439 | # from an instance: the field tries to initialise the field using a | ||
286 | 440 | # zone's ID instead of its name, and ends up reverting to the default. | ||
287 | 441 | # The code must work around this bug. | ||
288 | 442 | zone = factory.make_zone() | ||
289 | 443 | node = factory.make_node(zone=zone) | ||
290 | 444 | # We'll create a form that makes a change, but not to the zone. | ||
291 | 445 | data = {'hostname': factory.make_name('host')} | ||
292 | 446 | form = AdminNodeForm(instance=node, data=data) | ||
293 | 447 | # The Django bug would stop the initial field value from being set, | ||
294 | 448 | # but the workaround ensures that it is initialised. | ||
295 | 449 | self.assertEqual(zone.name, form.initial['zone']) | ||
296 | 450 | |||
297 | 451 | def test_AdminNodeForm_changes_node(self): | ||
298 | 452 | node = factory.make_node() | ||
299 | 453 | zone = factory.make_zone() | ||
300 | 454 | hostname = factory.make_string() | ||
301 | 455 | power_type = factory.pick_power_type() | ||
302 | 456 | form = AdminNodeForm( | ||
303 | 457 | data={ | ||
304 | 458 | 'hostname': hostname, | ||
305 | 459 | 'power_type': power_type, | ||
306 | 460 | 'architecture': make_usable_architecture(self), | ||
307 | 461 | 'zone': zone.name, | ||
308 | 462 | }, | ||
309 | 463 | instance=node) | ||
310 | 464 | form.save() | ||
311 | 465 | |||
312 | 466 | node = reload_object(node) | ||
313 | 467 | self.assertEqual( | ||
314 | 468 | (node.hostname, node.power_type, node.zone), | ||
315 | 469 | (hostname, power_type, zone)) | ||
316 | 470 | |||
317 | 471 | def test_AdminNodeForm_populates_power_type_choices(self): | ||
318 | 472 | form = AdminNodeForm() | ||
319 | 473 | self.assertEqual( | ||
320 | 474 | [''] + [choice[0] for choice in get_power_type_choices()], | ||
321 | 475 | [choice[0] for choice in form.fields['power_type'].choices]) | ||
322 | 476 | |||
323 | 477 | def test_AdminNodeForm_populates_power_type_initial(self): | ||
324 | 478 | node = factory.make_node() | ||
325 | 479 | form = AdminNodeForm(instance=node) | ||
326 | 480 | self.assertEqual(node.power_type, form.fields['power_type'].initial) | ||
327 | 481 | |||
328 | 482 | def test_AdminNodeForm_changes_node_with_skip_check(self): | ||
329 | 483 | node = factory.make_node() | ||
330 | 484 | hostname = factory.make_string() | ||
331 | 485 | power_type = factory.pick_power_type() | ||
332 | 486 | power_parameters_field = factory.make_string() | ||
333 | 487 | arch = make_usable_architecture(self) | ||
334 | 488 | form = AdminNodeForm( | ||
335 | 489 | data={ | ||
336 | 490 | 'hostname': hostname, | ||
337 | 491 | 'architecture': arch, | ||
338 | 492 | 'power_type': power_type, | ||
339 | 493 | 'power_parameters_field': power_parameters_field, | ||
340 | 494 | 'power_parameters_skip_check': True, | ||
341 | 495 | }, | ||
342 | 496 | instance=node) | ||
343 | 497 | form.save() | ||
344 | 498 | |||
345 | 499 | self.assertEqual( | ||
346 | 500 | (hostname, power_type, {'field': power_parameters_field}), | ||
347 | 501 | (node.hostname, node.power_type, node.power_parameters)) | ||
348 | 502 | |||
349 | 503 | def test_AdminForm_does_not_permit_nodegroup_change(self): | ||
350 | 504 | # We had to make Node.nodegroup editable to get Django to | ||
351 | 505 | # validate it as non-blankable, but that doesn't mean that we | ||
352 | 506 | # actually want to allow people to edit it through API or UI. | ||
353 | 507 | old_nodegroup = factory.make_node_group() | ||
354 | 508 | node = factory.make_node( | ||
355 | 509 | nodegroup=old_nodegroup, | ||
356 | 510 | architecture=make_usable_architecture(self)) | ||
357 | 511 | new_nodegroup = factory.make_node_group() | ||
358 | 512 | AdminNodeForm(data={'nodegroup': new_nodegroup}, instance=node).save() | ||
359 | 513 | # The form saved without error, but the nodegroup change was ignored. | ||
360 | 514 | self.assertEqual(old_nodegroup, node.nodegroup) | ||
361 | 515 | |||
362 | 516 | |||
363 | 517 | class TestMergeErrorMessages(MAASServerTestCase): | 194 | class TestMergeErrorMessages(MAASServerTestCase): |
364 | 518 | 195 | ||
365 | 519 | def test_merge_error_messages_returns_summary_message(self): | 196 | def test_merge_error_messages_returns_summary_message(self): |
366 | 520 | 197 | ||
367 | === added file 'src/maasserver/tests/test_forms_node.py' | |||
368 | --- src/maasserver/tests/test_forms_node.py 1970-01-01 00:00:00 +0000 | |||
369 | +++ src/maasserver/tests/test_forms_node.py 2014-08-26 07:57:29 +0000 | |||
370 | @@ -0,0 +1,352 @@ | |||
371 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
372 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
373 | 3 | |||
374 | 4 | """Tests for node forms.""" | ||
375 | 5 | |||
376 | 6 | from __future__ import ( | ||
377 | 7 | absolute_import, | ||
378 | 8 | print_function, | ||
379 | 9 | unicode_literals, | ||
380 | 10 | ) | ||
381 | 11 | |||
382 | 12 | str = None | ||
383 | 13 | |||
384 | 14 | __metaclass__ = type | ||
385 | 15 | __all__ = [] | ||
386 | 16 | |||
387 | 17 | from maasserver.clusterrpc.power_parameters import get_power_type_choices | ||
388 | 18 | from maasserver.enum import ( | ||
389 | 19 | NODEGROUP_STATUS, | ||
390 | 20 | NODEGROUPINTERFACE_MANAGEMENT, | ||
391 | 21 | ) | ||
392 | 22 | from maasserver.forms import ( | ||
393 | 23 | AdminNodeForm, | ||
394 | 24 | BLANK_CHOICE, | ||
395 | 25 | NO_ARCHITECTURES_AVAILABLE, | ||
396 | 26 | NodeForm, | ||
397 | 27 | pick_default_architecture, | ||
398 | 28 | ) | ||
399 | 29 | from maasserver.testing.architecture import ( | ||
400 | 30 | make_usable_architecture, | ||
401 | 31 | patch_usable_architectures, | ||
402 | 32 | ) | ||
403 | 33 | from maasserver.testing.factory import factory | ||
404 | 34 | from maasserver.testing.orm import reload_object | ||
405 | 35 | from maasserver.testing.osystems import ( | ||
406 | 36 | make_osystem_with_releases, | ||
407 | 37 | make_usable_osystem, | ||
408 | 38 | patch_usable_osystems, | ||
409 | 39 | ) | ||
410 | 40 | from maasserver.testing.testcase import MAASServerTestCase | ||
411 | 41 | |||
412 | 42 | |||
413 | 43 | class TestNodeForm(MAASServerTestCase): | ||
414 | 44 | |||
415 | 45 | def test_contains_limited_set_of_fields(self): | ||
416 | 46 | form = NodeForm() | ||
417 | 47 | |||
418 | 48 | self.assertEqual( | ||
419 | 49 | [ | ||
420 | 50 | 'hostname', | ||
421 | 51 | 'architecture', | ||
422 | 52 | 'osystem', | ||
423 | 53 | 'distro_series', | ||
424 | 54 | 'license_key', | ||
425 | 55 | 'disable_ipv4', | ||
426 | 56 | 'nodegroup', | ||
427 | 57 | ], list(form.fields)) | ||
428 | 58 | |||
429 | 59 | def test_changes_node(self): | ||
430 | 60 | node = factory.make_node() | ||
431 | 61 | hostname = factory.make_string() | ||
432 | 62 | patch_usable_architectures(self, [node.architecture]) | ||
433 | 63 | |||
434 | 64 | form = NodeForm( | ||
435 | 65 | data={ | ||
436 | 66 | 'hostname': hostname, | ||
437 | 67 | 'architecture': make_usable_architecture(self), | ||
438 | 68 | }, | ||
439 | 69 | instance=node) | ||
440 | 70 | form.save() | ||
441 | 71 | |||
442 | 72 | self.assertEqual(hostname, node.hostname) | ||
443 | 73 | |||
444 | 74 | def test_accepts_usable_architecture(self): | ||
445 | 75 | arch = make_usable_architecture(self) | ||
446 | 76 | form = NodeForm(data={ | ||
447 | 77 | 'hostname': factory.make_name('host'), | ||
448 | 78 | 'architecture': arch, | ||
449 | 79 | }) | ||
450 | 80 | self.assertTrue(form.is_valid(), form._errors) | ||
451 | 81 | |||
452 | 82 | def test_rejects_unusable_architecture(self): | ||
453 | 83 | patch_usable_architectures(self) | ||
454 | 84 | form = NodeForm(data={ | ||
455 | 85 | 'hostname': factory.make_name('host'), | ||
456 | 86 | 'architecture': factory.make_name('arch'), | ||
457 | 87 | }) | ||
458 | 88 | self.assertFalse(form.is_valid()) | ||
459 | 89 | self.assertItemsEqual(['architecture'], form._errors.keys()) | ||
460 | 90 | |||
461 | 91 | def test_starts_with_default_architecture(self): | ||
462 | 92 | arches = sorted([factory.make_name('arch') for _ in range(5)]) | ||
463 | 93 | patch_usable_architectures(self, arches) | ||
464 | 94 | form = NodeForm() | ||
465 | 95 | self.assertEqual( | ||
466 | 96 | pick_default_architecture(arches), | ||
467 | 97 | form.fields['architecture'].initial) | ||
468 | 98 | |||
469 | 99 | def test_adds_blank_default_when_no_arches_available(self): | ||
470 | 100 | patch_usable_architectures(self, []) | ||
471 | 101 | form = NodeForm() | ||
472 | 102 | self.assertEqual( | ||
473 | 103 | [BLANK_CHOICE], | ||
474 | 104 | form.fields['architecture'].choices) | ||
475 | 105 | |||
476 | 106 | def test_adds_error_when_no_arches_available(self): | ||
477 | 107 | patch_usable_architectures(self, []) | ||
478 | 108 | form = NodeForm() | ||
479 | 109 | self.assertFalse(form.is_valid()) | ||
480 | 110 | self.assertEqual( | ||
481 | 111 | [NO_ARCHITECTURES_AVAILABLE], | ||
482 | 112 | form.errors['architecture']) | ||
483 | 113 | |||
484 | 114 | def test_accepts_osystem(self): | ||
485 | 115 | osystem = make_usable_osystem(self) | ||
486 | 116 | form = NodeForm(data={ | ||
487 | 117 | 'hostname': factory.make_name('host'), | ||
488 | 118 | 'architecture': make_usable_architecture(self), | ||
489 | 119 | 'osystem': osystem.name, | ||
490 | 120 | }) | ||
491 | 121 | self.assertTrue(form.is_valid(), form._errors) | ||
492 | 122 | |||
493 | 123 | def test_rejects_invalid_osystem(self): | ||
494 | 124 | patch_usable_osystems(self) | ||
495 | 125 | form = NodeForm(data={ | ||
496 | 126 | 'hostname': factory.make_name('host'), | ||
497 | 127 | 'architecture': make_usable_architecture(self), | ||
498 | 128 | 'osystem': factory.make_name('os'), | ||
499 | 129 | }) | ||
500 | 130 | self.assertFalse(form.is_valid()) | ||
501 | 131 | self.assertItemsEqual(['osystem'], form._errors.keys()) | ||
502 | 132 | |||
503 | 133 | def test_starts_with_default_osystem(self): | ||
504 | 134 | osystems = [make_osystem_with_releases(self) for _ in range(5)] | ||
505 | 135 | patch_usable_osystems(self, osystems) | ||
506 | 136 | form = NodeForm() | ||
507 | 137 | self.assertEqual( | ||
508 | 138 | '', | ||
509 | 139 | form.fields['osystem'].initial) | ||
510 | 140 | |||
511 | 141 | def test_accepts_osystem_distro_series(self): | ||
512 | 142 | osystem = make_usable_osystem(self) | ||
513 | 143 | release = osystem.get_default_release() | ||
514 | 144 | form = NodeForm(data={ | ||
515 | 145 | 'hostname': factory.make_name('host'), | ||
516 | 146 | 'architecture': make_usable_architecture(self), | ||
517 | 147 | 'osystem': osystem.name, | ||
518 | 148 | 'distro_series': '%s/%s' % (osystem.name, release), | ||
519 | 149 | }) | ||
520 | 150 | self.assertTrue(form.is_valid(), form._errors) | ||
521 | 151 | |||
522 | 152 | def test_rejects_invalid_osystem_distro_series(self): | ||
523 | 153 | osystem = make_usable_osystem(self) | ||
524 | 154 | release = factory.make_name('release') | ||
525 | 155 | form = NodeForm(data={ | ||
526 | 156 | 'hostname': factory.make_name('host'), | ||
527 | 157 | 'architecture': make_usable_architecture(self), | ||
528 | 158 | 'osystem': osystem.name, | ||
529 | 159 | 'distro_series': '%s/%s' % (osystem.name, release), | ||
530 | 160 | }) | ||
531 | 161 | self.assertFalse(form.is_valid()) | ||
532 | 162 | self.assertItemsEqual(['distro_series'], form._errors.keys()) | ||
533 | 163 | |||
534 | 164 | def test_starts_with_default_distro_series(self): | ||
535 | 165 | osystems = [make_osystem_with_releases(self) for _ in range(5)] | ||
536 | 166 | patch_usable_osystems(self, osystems) | ||
537 | 167 | form = NodeForm() | ||
538 | 168 | self.assertEqual( | ||
539 | 169 | '', | ||
540 | 170 | form.fields['distro_series'].initial) | ||
541 | 171 | |||
542 | 172 | def test_rejects_mismatch_osystem_distro_series(self): | ||
543 | 173 | osystem = make_usable_osystem(self) | ||
544 | 174 | release = osystem.get_default_release() | ||
545 | 175 | invalid = factory.make_name('invalid_os') | ||
546 | 176 | form = NodeForm(data={ | ||
547 | 177 | 'hostname': factory.make_name('host'), | ||
548 | 178 | 'architecture': make_usable_architecture(self), | ||
549 | 179 | 'osystem': osystem.name, | ||
550 | 180 | 'distro_series': '%s/%s' % (invalid, release), | ||
551 | 181 | }) | ||
552 | 182 | self.assertFalse(form.is_valid()) | ||
553 | 183 | self.assertItemsEqual(['distro_series'], form._errors.keys()) | ||
554 | 184 | |||
555 | 185 | def test_rejects_missing_license_key(self): | ||
556 | 186 | osystem = make_usable_osystem(self) | ||
557 | 187 | release = osystem.get_default_release() | ||
558 | 188 | self.patch(osystem, 'requires_license_key').return_value = True | ||
559 | 189 | mock_validate = self.patch(osystem, 'validate_license_key') | ||
560 | 190 | mock_validate.return_value = True | ||
561 | 191 | form = NodeForm(data={ | ||
562 | 192 | 'hostname': factory.make_name('host'), | ||
563 | 193 | 'architecture': make_usable_architecture(self), | ||
564 | 194 | 'osystem': osystem.name, | ||
565 | 195 | 'distro_series': '%s/%s*' % (osystem.name, release), | ||
566 | 196 | }) | ||
567 | 197 | self.assertFalse(form.is_valid()) | ||
568 | 198 | self.assertItemsEqual(['license_key'], form._errors.keys()) | ||
569 | 199 | |||
570 | 200 | def test_calls_validate_license_key(self): | ||
571 | 201 | osystem = make_usable_osystem(self) | ||
572 | 202 | release = osystem.get_default_release() | ||
573 | 203 | self.patch(osystem, 'requires_license_key').return_value = True | ||
574 | 204 | mock_validate = self.patch(osystem, 'validate_license_key') | ||
575 | 205 | mock_validate.return_value = True | ||
576 | 206 | form = NodeForm(data={ | ||
577 | 207 | 'hostname': factory.make_name('host'), | ||
578 | 208 | 'architecture': make_usable_architecture(self), | ||
579 | 209 | 'osystem': osystem.name, | ||
580 | 210 | 'distro_series': '%s/%s*' % (osystem.name, release), | ||
581 | 211 | 'license_key': factory.make_string(), | ||
582 | 212 | }) | ||
583 | 213 | self.assertTrue(form.is_valid()) | ||
584 | 214 | mock_validate.assert_called_once() | ||
585 | 215 | |||
586 | 216 | def test_rejects_duplicate_fqdn_with_unmanaged_dns_on_one_nodegroup(self): | ||
587 | 217 | # If a host with a given hostname exists on a managed nodegroup, | ||
588 | 218 | # new nodes on unmanaged nodegroups with hostnames that match | ||
589 | 219 | # that FQDN will be rejected. | ||
590 | 220 | nodegroup = factory.make_node_group( | ||
591 | 221 | status=NODEGROUP_STATUS.ACCEPTED, | ||
592 | 222 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) | ||
593 | 223 | node = factory.make_node( | ||
594 | 224 | hostname=factory.make_name("hostname"), nodegroup=nodegroup) | ||
595 | 225 | other_nodegroup = factory.make_node_group() | ||
596 | 226 | form = NodeForm(data={ | ||
597 | 227 | 'nodegroup': other_nodegroup, | ||
598 | 228 | 'hostname': node.fqdn, | ||
599 | 229 | 'architecture': make_usable_architecture(self), | ||
600 | 230 | }) | ||
601 | 231 | form.instance.nodegroup = other_nodegroup | ||
602 | 232 | self.assertFalse(form.is_valid()) | ||
603 | 233 | |||
604 | 234 | def test_rejects_duplicate_fqdn_on_same_nodegroup(self): | ||
605 | 235 | # If a node with a given FQDN exists on a managed nodegroup, new | ||
606 | 236 | # nodes on that nodegroup with duplicate FQDNs will be rejected. | ||
607 | 237 | nodegroup = factory.make_node_group( | ||
608 | 238 | status=NODEGROUP_STATUS.ACCEPTED, | ||
609 | 239 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) | ||
610 | 240 | node = factory.make_node( | ||
611 | 241 | hostname=factory.make_name("hostname"), nodegroup=nodegroup) | ||
612 | 242 | form = NodeForm(data={ | ||
613 | 243 | 'nodegroup': nodegroup, | ||
614 | 244 | 'hostname': node.fqdn, | ||
615 | 245 | 'architecture': make_usable_architecture(self), | ||
616 | 246 | }) | ||
617 | 247 | form.instance.nodegroup = nodegroup | ||
618 | 248 | self.assertFalse(form.is_valid()) | ||
619 | 249 | |||
620 | 250 | |||
621 | 251 | class TestAdminNodeForm(MAASServerTestCase): | ||
622 | 252 | |||
623 | 253 | def test_AdminNodeForm_contains_limited_set_of_fields(self): | ||
624 | 254 | node = factory.make_node() | ||
625 | 255 | form = AdminNodeForm(instance=node) | ||
626 | 256 | |||
627 | 257 | self.assertEqual( | ||
628 | 258 | [ | ||
629 | 259 | 'hostname', | ||
630 | 260 | 'architecture', | ||
631 | 261 | 'osystem', | ||
632 | 262 | 'distro_series', | ||
633 | 263 | 'license_key', | ||
634 | 264 | 'disable_ipv4', | ||
635 | 265 | 'power_type', | ||
636 | 266 | 'power_parameters', | ||
637 | 267 | 'cpu_count', | ||
638 | 268 | 'memory', | ||
639 | 269 | 'storage', | ||
640 | 270 | 'zone', | ||
641 | 271 | ], | ||
642 | 272 | list(form.fields)) | ||
643 | 273 | |||
644 | 274 | def test_AdminNodeForm_initialises_zone(self): | ||
645 | 275 | # The zone field uses "to_field_name", so that it can refer to a zone | ||
646 | 276 | # by name instead of by ID. A bug in Django breaks initialisation | ||
647 | 277 | # from an instance: the field tries to initialise the field using a | ||
648 | 278 | # zone's ID instead of its name, and ends up reverting to the default. | ||
649 | 279 | # The code must work around this bug. | ||
650 | 280 | zone = factory.make_zone() | ||
651 | 281 | node = factory.make_node(zone=zone) | ||
652 | 282 | # We'll create a form that makes a change, but not to the zone. | ||
653 | 283 | data = {'hostname': factory.make_name('host')} | ||
654 | 284 | form = AdminNodeForm(instance=node, data=data) | ||
655 | 285 | # The Django bug would stop the initial field value from being set, | ||
656 | 286 | # but the workaround ensures that it is initialised. | ||
657 | 287 | self.assertEqual(zone.name, form.initial['zone']) | ||
658 | 288 | |||
659 | 289 | def test_AdminNodeForm_changes_node(self): | ||
660 | 290 | node = factory.make_node() | ||
661 | 291 | zone = factory.make_zone() | ||
662 | 292 | hostname = factory.make_string() | ||
663 | 293 | power_type = factory.pick_power_type() | ||
664 | 294 | form = AdminNodeForm( | ||
665 | 295 | data={ | ||
666 | 296 | 'hostname': hostname, | ||
667 | 297 | 'power_type': power_type, | ||
668 | 298 | 'architecture': make_usable_architecture(self), | ||
669 | 299 | 'zone': zone.name, | ||
670 | 300 | }, | ||
671 | 301 | instance=node) | ||
672 | 302 | form.save() | ||
673 | 303 | |||
674 | 304 | node = reload_object(node) | ||
675 | 305 | self.assertEqual( | ||
676 | 306 | (node.hostname, node.power_type, node.zone), | ||
677 | 307 | (hostname, power_type, zone)) | ||
678 | 308 | |||
679 | 309 | def test_AdminNodeForm_populates_power_type_choices(self): | ||
680 | 310 | form = AdminNodeForm() | ||
681 | 311 | self.assertEqual( | ||
682 | 312 | [''] + [choice[0] for choice in get_power_type_choices()], | ||
683 | 313 | [choice[0] for choice in form.fields['power_type'].choices]) | ||
684 | 314 | |||
685 | 315 | def test_AdminNodeForm_populates_power_type_initial(self): | ||
686 | 316 | node = factory.make_node() | ||
687 | 317 | form = AdminNodeForm(instance=node) | ||
688 | 318 | self.assertEqual(node.power_type, form.fields['power_type'].initial) | ||
689 | 319 | |||
690 | 320 | def test_AdminNodeForm_changes_node_with_skip_check(self): | ||
691 | 321 | node = factory.make_node() | ||
692 | 322 | hostname = factory.make_string() | ||
693 | 323 | power_type = factory.pick_power_type() | ||
694 | 324 | power_parameters_field = factory.make_string() | ||
695 | 325 | arch = make_usable_architecture(self) | ||
696 | 326 | form = AdminNodeForm( | ||
697 | 327 | data={ | ||
698 | 328 | 'hostname': hostname, | ||
699 | 329 | 'architecture': arch, | ||
700 | 330 | 'power_type': power_type, | ||
701 | 331 | 'power_parameters_field': power_parameters_field, | ||
702 | 332 | 'power_parameters_skip_check': True, | ||
703 | 333 | }, | ||
704 | 334 | instance=node) | ||
705 | 335 | form.save() | ||
706 | 336 | |||
707 | 337 | self.assertEqual( | ||
708 | 338 | (hostname, power_type, {'field': power_parameters_field}), | ||
709 | 339 | (node.hostname, node.power_type, node.power_parameters)) | ||
710 | 340 | |||
711 | 341 | def test_AdminForm_does_not_permit_nodegroup_change(self): | ||
712 | 342 | # We had to make Node.nodegroup editable to get Django to | ||
713 | 343 | # validate it as non-blankable, but that doesn't mean that we | ||
714 | 344 | # actually want to allow people to edit it through API or UI. | ||
715 | 345 | old_nodegroup = factory.make_node_group() | ||
716 | 346 | node = factory.make_node( | ||
717 | 347 | nodegroup=old_nodegroup, | ||
718 | 348 | architecture=make_usable_architecture(self)) | ||
719 | 349 | new_nodegroup = factory.make_node_group() | ||
720 | 350 | AdminNodeForm(data={'nodegroup': new_nodegroup}, instance=node).save() | ||
721 | 351 | # The form saved without error, but the nodegroup change was ignored. | ||
722 | 352 | self.assertEqual(old_nodegroup, node.nodegroup) |
Looks OK. Self-approving.