Merge lp:~ltrager/maas/lp1593991 into lp:~maas-committers/maas/trunk
- lp1593991
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Lee Trager |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5499 |
Proposed branch: | lp:~ltrager/maas/lp1593991 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1216 lines (+296/-142) 21 files modified
src/maasserver/api/machines.py (+0/-3) src/maasserver/api/tests/test_enlistment.py (+21/-12) src/maasserver/api/tests/test_machine.py (+33/-22) src/maasserver/api/tests/test_node.py (+1/-1) src/maasserver/api/tests/test_rackcontroller.py (+2/-1) src/maasserver/api/tests/test_utils.py (+20/-0) src/maasserver/api/utils.py (+14/-1) src/maasserver/clusterrpc/power_parameters.py (+24/-5) src/maasserver/clusterrpc/tests/test_power_parameters.py (+48/-0) src/maasserver/config_forms.py (+3/-0) src/maasserver/forms.py (+37/-6) src/maasserver/rpc/nodes.py (+0/-4) src/maasserver/rpc/tests/test_nodes.py (+18/-32) src/maasserver/testing/factory.py (+2/-3) src/maasserver/tests/test_config_forms.py (+9/-0) src/maasserver/tests/test_forms_controller.py (+4/-2) src/maasserver/tests/test_forms_machine.py (+4/-1) src/maasserver/websockets/handlers/machine.py (+2/-9) src/maasserver/websockets/handlers/tests/test_machine.py (+7/-4) src/maasserver/websockets/tests/test_base.py (+2/-2) src/provisioningserver/power/schema.py (+45/-34) |
To merge this branch: | bzr merge lp:~ltrager/maas/lp1593991 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+308893@code.launchpad.net |
Commit message
Validate power parameters in the node form.
Description of the change
Previously power parameters were hacked on after form validation and the node object was saved. This was done wherever a node form was used(API, websockets, RPC) Most likely this was done because the form was not picking up the power parameters. The form wasn't picking up power paramters because of three issues, we filter all fields before going into the form and were not accepting power parameters, django requires a prefix when dealing with subfields, and we weren't properly setting the initial fields for subitems.
The power parameters are now validated based on the fields given by each power driver. This not only solves lp:1593991 by ensuring a power_parameter is set but also gives more specific validation errors. For exammple, if a field is missing or if a given field is invalid for the power type.
I noticed that the power drivers were not setting any field to be required. I set the power address for each power type as required and kept all other fields as is. Please let me know if other fields should be listed as required.
Preview Diff
1 | === modified file 'src/maasserver/api/machines.py' |
2 | --- src/maasserver/api/machines.py 2016-10-12 02:26:47 +0000 |
3 | +++ src/maasserver/api/machines.py 2016-10-20 20:55:55 +0000 |
4 | @@ -34,7 +34,6 @@ |
5 | OwnerDataMixin, |
6 | PowerMixin, |
7 | PowersMixin, |
8 | - store_node_power_parameters, |
9 | ) |
10 | from maasserver.api.support import ( |
11 | admin_method, |
12 | @@ -796,8 +795,6 @@ |
13 | form = Form(data=altered_query_data, request=request) |
14 | if form.is_valid(): |
15 | machine = form.save() |
16 | - # Hack in the power parameters here. |
17 | - store_node_power_parameters(machine, request) |
18 | maaslog.info("%s: Enlisted new machine", machine.hostname) |
19 | return machine |
20 | else: |
21 | |
22 | === modified file 'src/maasserver/api/tests/test_enlistment.py' |
23 | --- src/maasserver/api/tests/test_enlistment.py 2016-09-22 02:53:33 +0000 |
24 | +++ src/maasserver/api/tests/test_enlistment.py 2016-10-20 20:55:55 +0000 |
25 | @@ -87,6 +87,7 @@ |
26 | architecture = make_usable_architecture(self) |
27 | power_type = 'ipmi' |
28 | power_parameters = { |
29 | + "power_address": factory.make_ip_address(), |
30 | "power_user": factory.make_name("power-user"), |
31 | "power_pass": factory.make_name("power-pass"), |
32 | } |
33 | @@ -95,14 +96,16 @@ |
34 | { |
35 | 'hostname': hostname, |
36 | 'architecture': architecture, |
37 | - 'power_type': 'manual', |
38 | + 'power_type': power_type, |
39 | 'mac_addresses': factory.make_mac_address(), |
40 | 'power_parameters': json.dumps(power_parameters), |
41 | - 'power_type': power_type, |
42 | }) |
43 | + # Add the default values. |
44 | + power_parameters['power_driver'] = 'LAN_2_0' |
45 | + power_parameters['mac_address'] = '' |
46 | self.assertEqual(http.client.OK, response.status_code) |
47 | [machine] = Machine.objects.filter(hostname=hostname) |
48 | - self.assertEqual(power_parameters, machine.power_parameters) |
49 | + self.assertItemsEqual(power_parameters, machine.power_parameters) |
50 | self.assertEqual(power_type, machine.power_type) |
51 | |
52 | def test_POST_create_creates_machine_with_arch_only(self): |
53 | @@ -454,23 +457,29 @@ |
54 | self.assertEqual([], machines_returned) |
55 | |
56 | def test_POST_simple_user_can_set_power_type_and_parameters(self): |
57 | - new_power_address = factory.make_string() |
58 | + new_power_address = factory.make_url() |
59 | + new_power_id = factory.make_name('power_id') |
60 | response = self.client.post( |
61 | reverse('machines_handler'), { |
62 | 'architecture': make_usable_architecture(self), |
63 | - 'power_type': 'manual', |
64 | + 'power_type': 'virsh', |
65 | 'power_parameters': json.dumps( |
66 | - {"power_address": new_power_address}), |
67 | + { |
68 | + "power_address": new_power_address, |
69 | + "power_id": new_power_id, |
70 | + }), |
71 | 'mac_addresses': ['AA:BB:CC:DD:EE:FF'], |
72 | }) |
73 | - |
74 | machine = Machine.objects.get( |
75 | system_id=json_load_bytes(response.content)['system_id']) |
76 | - self.assertEqual( |
77 | - (http.client.OK, {"power_address": new_power_address}, |
78 | - 'manual'), |
79 | - (response.status_code, machine.power_parameters, |
80 | - machine.power_type)) |
81 | + self.assertEqual(http.client.OK, response.status_code) |
82 | + self.assertEqual('virsh', machine.power_type) |
83 | + self.assertItemsEqual( |
84 | + { |
85 | + 'power_pass': '', |
86 | + 'power_id': new_power_id, |
87 | + 'power_address': new_power_address, |
88 | + }, machine.power_parameters) |
89 | |
90 | def test_POST_returns_limited_fields(self): |
91 | response = self.client.post( |
92 | |
93 | === modified file 'src/maasserver/api/tests/test_machine.py' |
94 | --- src/maasserver/api/tests/test_machine.py 2016-10-18 08:00:37 +0000 |
95 | +++ src/maasserver/api/tests/test_machine.py 2016-10-20 20:55:55 +0000 |
96 | @@ -850,7 +850,7 @@ |
97 | # The api allows the updating of a Machine. |
98 | machine = factory.make_Node( |
99 | hostname='diane', owner=self.user, |
100 | - architecture=make_usable_architecture(self)) |
101 | + architecture=make_usable_architecture(self), power_type='manual') |
102 | response = self.client.put( |
103 | self.get_machine_uri(machine), {'hostname': 'francis'}) |
104 | parsed_result = json_load_bytes(response.content) |
105 | @@ -867,7 +867,8 @@ |
106 | hostname = factory.make_name('hostname') |
107 | arch = make_usable_architecture(self) |
108 | machine = factory.make_Node( |
109 | - hostname=hostname, owner=self.user, architecture=arch) |
110 | + hostname=hostname, owner=self.user, architecture=arch, |
111 | + power_type='manual') |
112 | response = self.client.put( |
113 | self.get_machine_uri(machine), |
114 | {'architecture': arch}) |
115 | @@ -890,7 +891,8 @@ |
116 | self.become_admin() |
117 | machine = factory.make_Node( |
118 | owner=self.user, |
119 | - architecture=make_usable_architecture(self)) |
120 | + architecture=make_usable_architecture(self), |
121 | + power_type='manual') |
122 | field = factory.make_string() |
123 | response = self.client.put( |
124 | self.get_machine_uri(machine), |
125 | @@ -908,7 +910,11 @@ |
126 | power_type=original_power_type, |
127 | architecture=make_usable_architecture(self)) |
128 | response = self.client.put( |
129 | - self.get_machine_uri(machine), {'power_type': new_power_type}) |
130 | + self.get_machine_uri(machine), |
131 | + { |
132 | + 'power_type': new_power_type, |
133 | + 'power_parameters_skip_check': 'true', |
134 | + }) |
135 | |
136 | self.assertEqual(http.client.OK, response.status_code) |
137 | self.assertEqual( |
138 | @@ -932,7 +938,8 @@ |
139 | # provides the URI for this Machine. |
140 | machine = factory.make_Node( |
141 | hostname='diane', owner=self.user, |
142 | - architecture=make_usable_architecture(self)) |
143 | + architecture=make_usable_architecture(self), |
144 | + power_type='manual') |
145 | response = self.client.put( |
146 | self.get_machine_uri(machine), {'hostname': 'francis'}) |
147 | parsed_result = json_load_bytes(response.content) |
148 | @@ -948,7 +955,8 @@ |
149 | self.become_admin() |
150 | machine = factory.make_Node( |
151 | hostname='diane', owner=self.user, |
152 | - architecture=make_usable_architecture(self)) |
153 | + architecture=make_usable_architecture(self), |
154 | + power_type='manual') |
155 | response = self.client.put( |
156 | self.get_machine_uri(machine), {'hostname': '.'}) |
157 | parsed_result = json_load_bytes(response.content) |
158 | @@ -1001,8 +1009,8 @@ |
159 | self.become_admin() |
160 | machine = factory.make_Node( |
161 | owner=self.user, |
162 | - power_type=factory.pick_power_type(), |
163 | - architecture=make_usable_architecture(self)) |
164 | + architecture=make_usable_architecture(self), |
165 | + power_type='manual') |
166 | response = self.client.put( |
167 | self.get_machine_uri(machine), |
168 | {'cpu_count': 1, 'memory': 1024}) |
169 | @@ -1109,7 +1117,8 @@ |
170 | self.become_admin() |
171 | machine = factory.make_Node( |
172 | owner=self.user, |
173 | - architecture=make_usable_architecture(self)) |
174 | + architecture=make_usable_architecture(self), |
175 | + power_parameters={}) |
176 | new_param = factory.make_string() |
177 | new_value = factory.make_string() |
178 | response = self.client.put( |
179 | @@ -1125,7 +1134,11 @@ |
180 | |
181 | def test_PUT_updates_power_parameters_empty_string(self): |
182 | self.become_admin() |
183 | - power_parameters = {factory.make_string(): factory.make_string()} |
184 | + power_parameters = { |
185 | + 'power_address': factory.make_url(), |
186 | + 'power_id': factory.make_name('power_id'), |
187 | + 'power_pass': factory.make_name('power_pass'), |
188 | + } |
189 | machine = factory.make_Node( |
190 | owner=self.user, |
191 | power_type='virsh', |
192 | @@ -1133,22 +1146,17 @@ |
193 | architecture=make_usable_architecture(self)) |
194 | response = self.client.put( |
195 | self.get_machine_uri(machine), |
196 | - {'power_parameters_power_id': ''}) |
197 | + {'power_parameters_power_pass': ''}) |
198 | |
199 | self.assertEqual(http.client.OK, response.status_code) |
200 | self.assertEqual( |
201 | - { |
202 | - 'power_id': '', |
203 | - 'power_pass': '', |
204 | - 'power_address': '', |
205 | - }, |
206 | - reload_object(machine).power_parameters) |
207 | + '', reload_object(machine).power_parameters['power_pass']) |
208 | |
209 | def test_PUT_sets_zone(self): |
210 | self.become_admin() |
211 | new_zone = factory.make_Zone() |
212 | machine = factory.make_Node( |
213 | - architecture=make_usable_architecture(self)) |
214 | + architecture=make_usable_architecture(self), power_type='manual') |
215 | |
216 | response = self.client.put( |
217 | self.get_machine_uri(machine), {'zone': new_zone.name}) |
218 | @@ -1161,7 +1169,7 @@ |
219 | self.become_admin() |
220 | new_name = factory.make_name() |
221 | machine = factory.make_Node( |
222 | - architecture=make_usable_architecture(self)) |
223 | + architecture=make_usable_architecture(self), power_type='manual') |
224 | old_zone = machine.zone |
225 | |
226 | response = self.client.put( |
227 | @@ -1190,7 +1198,8 @@ |
228 | self.become_admin() |
229 | zone = factory.make_Zone() |
230 | machine = factory.make_Node( |
231 | - zone=zone, architecture=make_usable_architecture(self)) |
232 | + zone=zone, architecture=make_usable_architecture(self), |
233 | + power_type='manual') |
234 | |
235 | response = self.client.put(self.get_machine_uri(machine), {}) |
236 | |
237 | @@ -1226,7 +1235,8 @@ |
238 | self.become_admin() |
239 | machine = factory.make_Node( |
240 | owner=self.user, |
241 | - architecture=make_usable_architecture(self)) |
242 | + architecture=make_usable_architecture(self), |
243 | + power_type='manual') |
244 | response = self.client.put( |
245 | reverse('machine_handler', args=[machine.system_id]), |
246 | {'swap_size': 5 * 1000 ** 3}) # Making sure we overflow 32 bits |
247 | @@ -1239,7 +1249,8 @@ |
248 | self.become_admin() |
249 | machine = factory.make_Node( |
250 | owner=self.user, |
251 | - architecture=make_usable_architecture(self)) |
252 | + architecture=make_usable_architecture(self), |
253 | + power_type='manual') |
254 | |
255 | response = self.client.put( |
256 | reverse('machine_handler', args=[machine.system_id]), |
257 | |
258 | === modified file 'src/maasserver/api/tests/test_node.py' |
259 | --- src/maasserver/api/tests/test_node.py 2016-08-31 13:52:59 +0000 |
260 | +++ src/maasserver/api/tests/test_node.py 2016-10-20 20:55:55 +0000 |
261 | @@ -303,7 +303,7 @@ |
262 | self.assertEqual( |
263 | http.client.OK, response.status_code, response.content) |
264 | parsed_params = json_load_bytes(response.content) |
265 | - self.assertEqual({}, parsed_params) |
266 | + self.assertEqual(node.power_parameters, parsed_params) |
267 | |
268 | def test_power_parameters_requires_admin(self): |
269 | node = factory.make_Node() |
270 | |
271 | === modified file 'src/maasserver/api/tests/test_rackcontroller.py' |
272 | --- src/maasserver/api/tests/test_rackcontroller.py 2016-06-08 20:20:40 +0000 |
273 | +++ src/maasserver/api/tests/test_rackcontroller.py 2016-10-20 20:55:55 +0000 |
274 | @@ -36,7 +36,8 @@ |
275 | |
276 | def test_PUT_updates_rack_controller(self): |
277 | self.become_admin() |
278 | - rack = factory.make_RackController(owner=self.user) |
279 | + rack = factory.make_RackController( |
280 | + owner=self.user, power_type='manual') |
281 | zone = factory.make_zone() |
282 | response = self.client.put( |
283 | self.get_rack_uri(rack), {'zone': zone.name}) |
284 | |
285 | === modified file 'src/maasserver/api/tests/test_utils.py' |
286 | --- src/maasserver/api/tests/test_utils.py 2016-05-24 13:51:36 +0000 |
287 | +++ src/maasserver/api/tests/test_utils.py 2016-10-20 20:55:55 +0000 |
288 | @@ -7,6 +7,7 @@ |
289 | |
290 | from collections import namedtuple |
291 | |
292 | +from django.forms import CharField |
293 | from django.http import QueryDict |
294 | from maasserver.api.utils import ( |
295 | extract_bool, |
296 | @@ -15,6 +16,7 @@ |
297 | get_oauth_token, |
298 | get_overridden_query_dict, |
299 | ) |
300 | +from maasserver.config_forms import DictCharField |
301 | from maasserver.exceptions import Unauthorized |
302 | from maasserver.testing.factory import factory |
303 | from maasserver.testing.testcase import MAASServerTestCase |
304 | @@ -94,6 +96,24 @@ |
305 | results = get_overridden_query_dict(defaults, data, [key1]) |
306 | self.assertEqual([data_value2], results.getlist(key2)) |
307 | |
308 | + def test_expands_dict_fields(self): |
309 | + field_name = factory.make_name('field_name') |
310 | + sub_fields = { |
311 | + factory.make_name('sub_field'): CharField() for _ in range(3) |
312 | + } |
313 | + fields = { |
314 | + field_name: DictCharField(sub_fields) |
315 | + } |
316 | + defaults = { |
317 | + "%s_%s" % (field_name, field): factory.make_name('subfield') |
318 | + for field in sub_fields.keys() |
319 | + } |
320 | + data = {field_name: DictCharField(fields)} |
321 | + results = get_overridden_query_dict(defaults, data, fields) |
322 | + expected = {key: [value] for key, value in defaults.items()} |
323 | + expected.update(fields) |
324 | + self.assertItemsEqual(expected, results) |
325 | + |
326 | |
327 | def make_fake_request(auth_header): |
328 | """Create a very simple fake request, with just an auth header.""" |
329 | |
330 | === modified file 'src/maasserver/api/utils.py' |
331 | --- src/maasserver/api/utils.py 2016-09-21 23:23:54 +0000 |
332 | +++ src/maasserver/api/utils.py 2016-10-20 20:55:55 +0000 |
333 | @@ -16,6 +16,7 @@ |
334 | |
335 | from django.http import QueryDict |
336 | from formencode.validators import Invalid |
337 | +from maasserver.config_forms import DictCharField |
338 | from maasserver.exceptions import ( |
339 | MAASAPIValidationError, |
340 | Unauthorized, |
341 | @@ -158,11 +159,23 @@ |
342 | """ |
343 | # Create a writable query dict. |
344 | new_data = QueryDict('').copy() |
345 | + # If the fields are a dict of django Fields see if one is a DictCharField. |
346 | + # DictCharField must have their values prefixed with the DictField name in |
347 | + # the returned data or defaults don't get carried. |
348 | + if isinstance(fields, dict): |
349 | + acceptable_fields = [] |
350 | + for field_name, field in fields.items(): |
351 | + acceptable_fields.append(field_name) |
352 | + if isinstance(field, DictCharField): |
353 | + for sub_field in field.names: |
354 | + acceptable_fields.append("%s_%s" % (field_name, sub_field)) |
355 | + else: |
356 | + acceptable_fields = fields |
357 | # Missing fields will be taken from the node's current values. This |
358 | # is to circumvent Django's ModelForm (form created from a model) |
359 | # default behaviour that requires all the fields to be defined. |
360 | for k, v in defaults.items(): |
361 | - if k in fields: |
362 | + if k in acceptable_fields: |
363 | new_data.setlist(k, listify(v)) |
364 | # We can't use update here because data is a QueryDict and 'update' |
365 | # does not replaces the old values with the new as one would expect. |
366 | |
367 | === modified file 'src/maasserver/clusterrpc/power_parameters.py' |
368 | --- src/maasserver/clusterrpc/power_parameters.py 2016-03-28 13:54:47 +0000 |
369 | +++ src/maasserver/clusterrpc/power_parameters.py 2016-10-20 20:55:55 +0000 |
370 | @@ -65,12 +65,16 @@ |
371 | json_field['name'], json_field['choices']) |
372 | extra_parameters = { |
373 | 'choices': json_field['choices'], |
374 | - 'initial': json_field['default'], |
375 | 'error_messages': { |
376 | 'invalid_choice': invalid_choice_message}, |
377 | } |
378 | else: |
379 | extra_parameters = {} |
380 | + |
381 | + default = json_field.get('default') |
382 | + if default is not None: |
383 | + extra_parameters['initial'] = default |
384 | + |
385 | form_field = field_class( |
386 | label=json_field['label'], required=json_field['required'], |
387 | **extra_parameters) |
388 | @@ -111,13 +115,20 @@ |
389 | 'missing_packages': missing_packages}) |
390 | |
391 | |
392 | -def get_power_type_parameters_from_json(json_power_type_parameters): |
393 | +def get_power_type_parameters_from_json( |
394 | + json_power_type_parameters, initial_power_params=None, |
395 | + skip_check=False): |
396 | """Return power type parameters. |
397 | |
398 | :param json_power_type_parameters: Power type parameters expressed |
399 | as a JSON string or as set of JSONSchema-verifiable objects. |
400 | Will be validated using jsonschema.validate(). |
401 | :type json_power_type_parameters: JSON string or iterable. |
402 | + :param initial_power_params: Power paramaters that were already set, any |
403 | + field which matches will have its initial value set. |
404 | + :type initial_power_params: dict |
405 | + :param skip_check: Whether the field should be checked or not. |
406 | + :type skip_check: bool |
407 | :return: A dict of power parameters for all power types, indexed by |
408 | power type name. |
409 | """ |
410 | @@ -127,19 +138,27 @@ |
411 | '': DictCharField( |
412 | [], required=False, skip_check=True), |
413 | } |
414 | + if initial_power_params is None: |
415 | + initial_power_params = [] |
416 | for power_type in json_power_type_parameters: |
417 | fields = [] |
418 | + has_required_field = False |
419 | for json_field in power_type['fields']: |
420 | + field_name = json_field['name'] |
421 | + if field_name in initial_power_params: |
422 | + json_field['default'] = initial_power_params[field_name] |
423 | + has_required_field = has_required_field or json_field['required'] |
424 | fields.append(( |
425 | json_field['name'], make_form_field(json_field))) |
426 | - params = DictCharField(fields, required=False, skip_check=True) |
427 | + params = DictCharField( |
428 | + fields, required=has_required_field, skip_check=skip_check) |
429 | power_parameters[power_type['name']] = params |
430 | return power_parameters |
431 | |
432 | |
433 | -def get_power_type_parameters(): |
434 | +def get_power_type_parameters(initial_power_params=None, skip_check=False): |
435 | return get_power_type_parameters_from_json( |
436 | - get_all_power_types_from_clusters()) |
437 | + get_all_power_types_from_clusters(), initial_power_params, skip_check) |
438 | |
439 | |
440 | def get_power_type_choices(): |
441 | |
442 | === modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py' |
443 | --- src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-05-12 19:07:37 +0000 |
444 | +++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-10-20 20:55:55 +0000 |
445 | @@ -20,6 +20,7 @@ |
446 | ) |
447 | from maasserver.config_forms import DictCharField |
448 | from maasserver.fields import MACAddressFormField |
449 | +from maasserver.testing.factory import factory |
450 | from maasserver.testing.testcase import MAASServerTestCase |
451 | from maasserver.utils.forms import compose_invalid_choice_text |
452 | from maastesting.matchers import MockCalledOnceWith |
453 | @@ -70,6 +71,42 @@ |
454 | for name, field in power_type_parameters.items(): |
455 | self.assertIsInstance(field, DictCharField) |
456 | |
457 | + def test__overrides_defaults(self): |
458 | + name = factory.make_name('name') |
459 | + field_name = factory.make_name('field_name') |
460 | + new_default = factory.make_name('new default') |
461 | + json_parameters = [{ |
462 | + 'name': name, |
463 | + 'description': factory.make_name('description'), |
464 | + 'fields': [{ |
465 | + 'name': field_name, |
466 | + 'label': factory.make_name('field label'), |
467 | + 'field_type': factory.make_name('field type'), |
468 | + 'default': factory.make_name('field default'), |
469 | + 'required': False, |
470 | + }], |
471 | + }] |
472 | + power_type_parameters = get_power_type_parameters_from_json( |
473 | + json_parameters, {field_name: new_default}) |
474 | + self.assertEqual( |
475 | + new_default, power_type_parameters[name].fields[0].initial) |
476 | + |
477 | + def test__manual_does_not_require_power_params(self): |
478 | + json_parameters = [{ |
479 | + 'name': 'manual', |
480 | + 'description': factory.make_name('description'), |
481 | + 'fields': [{ |
482 | + 'name': factory.make_name('field name'), |
483 | + 'label': factory.make_name('field label'), |
484 | + 'field_type': factory.make_name('field type'), |
485 | + 'default': factory.make_name('field default'), |
486 | + 'required': False, |
487 | + }], |
488 | + }] |
489 | + power_type_parameters = get_power_type_parameters_from_json( |
490 | + json_parameters) |
491 | + self.assertFalse(power_type_parameters['manual'].required) |
492 | + |
493 | |
494 | class TestMakeFormField(MAASServerTestCase): |
495 | """Test that make_form_field() converts JSON fields to Django.""" |
496 | @@ -137,6 +174,17 @@ |
497 | (json_field['label'], json_field['required']), |
498 | (django_field.label, django_field.required)) |
499 | |
500 | + def test__sets_initial_to_default(self): |
501 | + json_field = { |
502 | + 'name': 'some_field', |
503 | + 'label': 'Some Field', |
504 | + 'field_type': 'string', |
505 | + 'required': False, |
506 | + 'default': 'some default', |
507 | + } |
508 | + django_field = make_form_field(json_field) |
509 | + self.assertEquals(json_field['default'], django_field.initial) |
510 | + |
511 | |
512 | class TestMakeJSONField(MAASServerTestCase): |
513 | """Test that make_json_field() creates JSON-verifiable fields.""" |
514 | |
515 | === modified file 'src/maasserver/config_forms.py' |
516 | --- src/maasserver/config_forms.py 2015-12-01 18:12:59 +0000 |
517 | +++ src/maasserver/config_forms.py 2016-10-20 20:55:55 +0000 |
518 | @@ -186,6 +186,9 @@ |
519 | field_value = value[index] |
520 | except IndexError: |
521 | field_value = None |
522 | + # Set the field_value to the default value if not set. |
523 | + if field_value is None and field.initial not in (None, ''): |
524 | + field_value = field.initial |
525 | # Check the field's 'required' field instead of the global |
526 | # 'required' field to allow subfields to be required or not. |
527 | if field.required and field_value in validators.EMPTY_VALUES: |
528 | |
529 | === modified file 'src/maasserver/forms.py' |
530 | --- src/maasserver/forms.py 2016-10-17 20:09:56 +0000 |
531 | +++ src/maasserver/forms.py 2016-10-20 20:55:55 +0000 |
532 | @@ -36,6 +36,7 @@ |
533 | |
534 | from collections import Counter |
535 | from functools import partial |
536 | +import json |
537 | import pipes |
538 | import re |
539 | |
540 | @@ -241,6 +242,27 @@ |
541 | return power_type |
542 | |
543 | @staticmethod |
544 | + def _get_power_parameters(form, data, machine): |
545 | + if data is None: |
546 | + data = {} |
547 | + |
548 | + power_parameters = data.get( |
549 | + 'power_parameters', form.initial.get('power_parameters', {})) |
550 | + |
551 | + if isinstance(power_parameters, str): |
552 | + try: |
553 | + power_parameters = json.loads(power_parameters) |
554 | + except json.JSONDecodeError: |
555 | + raise ValidationError("Failed to parse JSON power_parameters") |
556 | + |
557 | + # Integrate the machines existing power_parameters if unset by form. |
558 | + if machine: |
559 | + for key, value in machine.power_parameters.items(): |
560 | + if power_parameters.get(key) is None: |
561 | + power_parameters[key] = value |
562 | + return power_parameters |
563 | + |
564 | + @staticmethod |
565 | def set_up_power_type(form, data, machine=None): |
566 | """Set up the 'power_type' and 'power_parameters' fields. |
567 | |
568 | @@ -251,10 +273,18 @@ |
569 | choices = [BLANK_CHOICE] + get_power_type_choices() |
570 | form.fields['power_type'] = forms.ChoiceField( |
571 | required=False, choices=choices, initial=power_type) |
572 | - form.fields['power_parameters'] = get_power_type_parameters()[ |
573 | - power_type] |
574 | - if form.instance is not None and form.instance.power_type != '': |
575 | - form.initial['power_type'] = form.instance.power_type |
576 | + power_parameters = WithPowerMixin._get_power_parameters( |
577 | + form, data, machine) |
578 | + skip_check = ( |
579 | + form.data.get('power_parameters_%s' % SKIP_CHECK_NAME) == 'true') |
580 | + form.fields['power_parameters'] = get_power_type_parameters( |
581 | + power_parameters, skip_check=skip_check)[power_type] |
582 | + if form.instance is not None: |
583 | + if form.instance.power_type != '': |
584 | + form.initial['power_type'] = form.instance.power_type |
585 | + if form.instance.power_parameters != '': |
586 | + for key, value in power_parameters.items(): |
587 | + form.initial['power_parameters_%s' % key] = value |
588 | |
589 | @staticmethod |
590 | def check_power_type(form, cleaned_data): |
591 | @@ -277,8 +307,9 @@ |
592 | |
593 | # If power_type is not set and power_parameters_skip_check is not |
594 | # on, reset power_parameters (set it to the empty string). |
595 | - if cleaned_data.get('power_type', '') == '': |
596 | - cleaned_data['power_parameters'] = '' |
597 | + power_type = cleaned_data.get('power_type', '') |
598 | + if power_type == '': |
599 | + cleaned_data['power_parameters'] = {} |
600 | return cleaned_data |
601 | |
602 | @staticmethod |
603 | |
604 | === modified file 'src/maasserver/rpc/nodes.py' |
605 | --- src/maasserver/rpc/nodes.py 2016-10-03 20:36:58 +0000 |
606 | +++ src/maasserver/rpc/nodes.py 2016-10-20 20:55:55 +0000 |
607 | @@ -225,10 +225,6 @@ |
608 | form = AdminMachineWithMACAddressesForm(data_query_dict) |
609 | if form.is_valid(): |
610 | node = form.save() |
611 | - # We have to explicitly save the power parameters; the form |
612 | - # won't do it for us. |
613 | - node.power_parameters = json.loads(power_parameters) |
614 | - node.save() |
615 | return node |
616 | else: |
617 | raise ValidationError(form.errors) |
618 | |
619 | === modified file 'src/maasserver/rpc/tests/test_nodes.py' |
620 | --- src/maasserver/rpc/tests/test_nodes.py 2016-10-03 20:36:58 +0000 |
621 | +++ src/maasserver/rpc/tests/test_nodes.py 2016-10-20 20:55:55 +0000 |
622 | @@ -7,7 +7,6 @@ |
623 | |
624 | from datetime import timedelta |
625 | import json |
626 | -from json import dumps |
627 | from operator import attrgetter |
628 | import random |
629 | from random import randint |
630 | @@ -88,17 +87,13 @@ |
631 | mac_addresses = [ |
632 | factory.make_mac_address() for _ in range(3)] |
633 | architecture = make_usable_architecture(self) |
634 | - power_type = random.choice(self.power_types)['name'] |
635 | - power_parameters = dumps({}) |
636 | |
637 | - node = create_node( |
638 | - architecture, power_type, power_parameters, |
639 | - mac_addresses) |
640 | + node = create_node(architecture, 'manual', {}, mac_addresses) |
641 | |
642 | self.assertEqual( |
643 | ( |
644 | architecture, |
645 | - power_type, |
646 | + 'manual', |
647 | {}, |
648 | ), |
649 | ( |
650 | @@ -122,17 +117,14 @@ |
651 | factory.make_mac_address() for _ in range(3)] |
652 | architecture = make_usable_architecture(self) |
653 | hostname = factory.make_hostname() |
654 | - power_type = random.choice(self.power_types)['name'] |
655 | - power_parameters = dumps({}) |
656 | |
657 | node = create_node( |
658 | - architecture, power_type, power_parameters, |
659 | - mac_addresses, hostname=hostname) |
660 | + architecture, 'manual', {}, mac_addresses, hostname=hostname) |
661 | |
662 | self.assertEqual( |
663 | ( |
664 | architecture, |
665 | - power_type, |
666 | + 'manual', |
667 | {}, |
668 | hostname |
669 | ), |
670 | @@ -158,7 +150,7 @@ |
671 | "Microsoft Windows", |
672 | ]) |
673 | power_type = random.choice(self.power_types)['name'] |
674 | - power_parameters = dumps({}) |
675 | + power_parameters = {} |
676 | |
677 | with ExpectedException(ValidationError): |
678 | create_node( |
679 | @@ -173,17 +165,15 @@ |
680 | architecture = make_usable_architecture(self) |
681 | hostname = factory.make_hostname() |
682 | domain = factory.make_Domain() |
683 | - power_type = random.choice(self.power_types)['name'] |
684 | - power_parameters = dumps({}) |
685 | |
686 | node = create_node( |
687 | - architecture, power_type, power_parameters, |
688 | - mac_addresses, domain=domain.name, hostname=hostname) |
689 | + architecture, 'manual', {}, mac_addresses, domain=domain.name, |
690 | + hostname=hostname) |
691 | |
692 | self.assertEqual( |
693 | ( |
694 | architecture, |
695 | - power_type, |
696 | + 'manual', |
697 | {}, |
698 | domain.id, |
699 | hostname, |
700 | @@ -207,7 +197,7 @@ |
701 | factory.make_mac_address() for _ in range(3)] |
702 | architecture = make_usable_architecture(self) |
703 | power_type = random.choice(self.power_types)['name'] |
704 | - power_parameters = dumps({}) |
705 | + power_parameters = {} |
706 | |
707 | with ExpectedException(ValidationError): |
708 | create_node( |
709 | @@ -220,7 +210,7 @@ |
710 | self.assertRaises( |
711 | ValidationError, create_node, |
712 | architecture="spam/eggs", power_type="scrambled", |
713 | - power_parameters=dumps({}), |
714 | + power_parameters={}, |
715 | mac_addresses=[factory.make_mac_address()]) |
716 | |
717 | def test__raises_error_if_node_already_exists(self): |
718 | @@ -229,8 +219,8 @@ |
719 | mac_addresses = [ |
720 | factory.make_mac_address() for _ in range(3)] |
721 | architecture = make_usable_architecture(self) |
722 | - power_type = random.choice(self.power_types)['name'] |
723 | - power_parameters = dumps({}) |
724 | + power_type = 'manual' |
725 | + power_parameters = {} |
726 | |
727 | create_node( |
728 | architecture, power_type, power_parameters, |
729 | @@ -245,20 +235,20 @@ |
730 | mac_addresses = [ |
731 | factory.make_mac_address() for _ in range(3)] |
732 | architecture = make_usable_architecture(self) |
733 | - power_type = random.choice(self.power_types)['name'] |
734 | power_parameters = { |
735 | - factory.make_name('key'): factory.make_name('value') |
736 | - for _ in range(3) |
737 | + 'power_address': factory.make_url(), |
738 | + 'power_pass': factory.make_name('power_pass'), |
739 | + 'power_id': factory.make_name('power_id'), |
740 | } |
741 | |
742 | node = create_node( |
743 | - architecture, power_type, dumps(power_parameters), |
744 | + architecture, 'virsh', power_parameters, |
745 | mac_addresses) |
746 | |
747 | # Reload the object from the DB so that we're sure its power |
748 | # parameters are being persisted. |
749 | node = reload_object(node) |
750 | - self.assertEqual(power_parameters, node.power_parameters) |
751 | + self.assertItemsEqual(power_parameters, node.power_parameters) |
752 | |
753 | def test__forces_generic_subarchitecture_if_missing(self): |
754 | self.prepare_rack_rpc() |
755 | @@ -266,13 +256,9 @@ |
756 | mac_addresses = [ |
757 | factory.make_mac_address() for _ in range(3)] |
758 | architecture = make_usable_architecture(self, subarch_name='generic') |
759 | - power_type = random.choice(self.power_types)['name'] |
760 | - power_parameters = dumps({}) |
761 | |
762 | arch, subarch = architecture.split('/') |
763 | - node = create_node( |
764 | - arch, power_type, power_parameters, |
765 | - mac_addresses) |
766 | + node = create_node(arch, 'manual', {}, mac_addresses) |
767 | |
768 | self.assertEqual(architecture, node.architecture) |
769 | |
770 | |
771 | === modified file 'src/maasserver/testing/factory.py' |
772 | --- src/maasserver/testing/factory.py 2016-10-18 08:00:37 +0000 |
773 | +++ src/maasserver/testing/factory.py 2016-10-20 20:55:55 +0000 |
774 | @@ -381,8 +381,6 @@ |
775 | zone = self.make_Zone() |
776 | if power_type is None: |
777 | power_type = 'virsh' |
778 | - if power_parameters is None: |
779 | - power_parameters = {} |
780 | if power_state is None: |
781 | power_state = self.pick_enum(POWER_STATE) |
782 | if power_state_updated is undefined: |
783 | @@ -443,7 +441,8 @@ |
784 | bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet) |
785 | node.power_parameters = { |
786 | "power_address": "qemu+ssh://user@%s/system" % ( |
787 | - factory.ip_to_url_format(bmc_ip_address)) |
788 | + factory.ip_to_url_format(bmc_ip_address)), |
789 | + "power_id": factory.make_name("power_id"), |
790 | } |
791 | node.save() |
792 | |
793 | |
794 | === modified file 'src/maasserver/tests/test_config_forms.py' |
795 | --- src/maasserver/tests/test_config_forms.py 2016-06-22 17:03:02 +0000 |
796 | +++ src/maasserver/tests/test_config_forms.py 2016-10-20 20:55:55 +0000 |
797 | @@ -165,6 +165,15 @@ |
798 | {'char_field': char_value, 'multi_field': None}, |
799 | form.cleaned_data) |
800 | |
801 | + def test_DictCharField_sets_default_value_for_subfields(self): |
802 | + default_value = factory.make_name('default_value') |
803 | + multi_field = DictCharField( |
804 | + [('field_a', forms.CharField( |
805 | + label='Field a', initial=default_value))], |
806 | + required=False) |
807 | + self.assertEquals( |
808 | + default_value, multi_field.clean_sub_fields('')['field_a']) |
809 | + |
810 | |
811 | class TestUtilities(MAASServerTestCase): |
812 | |
813 | |
814 | === modified file 'src/maasserver/tests/test_forms_controller.py' |
815 | --- src/maasserver/tests/test_forms_controller.py 2016-04-13 02:20:16 +0000 |
816 | +++ src/maasserver/tests/test_forms_controller.py 2016-10-20 20:55:55 +0000 |
817 | @@ -40,6 +40,7 @@ |
818 | form = ControllerForm( |
819 | data={ |
820 | 'power_type': power_type, |
821 | + 'power_parameters_skip_check': 'true', |
822 | }, |
823 | instance=rack) |
824 | rack = form.save() |
825 | @@ -51,12 +52,12 @@ |
826 | form = ControllerForm( |
827 | data={ |
828 | 'power_parameters_field': power_parameters_field, |
829 | - 'power_parameters_skip_check': True, |
830 | + 'power_parameters_skip_check': 'true', |
831 | }, |
832 | instance=rack) |
833 | rack = form.save() |
834 | self.assertEqual( |
835 | - {'field': power_parameters_field}, rack.power_parameters) |
836 | + power_parameters_field, rack.power_parameters['field']) |
837 | |
838 | def test__sets_zone(self): |
839 | rack = factory.make_RackController() |
840 | @@ -64,6 +65,7 @@ |
841 | form = ControllerForm( |
842 | data={ |
843 | 'zone': zone.name, |
844 | + 'power_parameters_skip_check': 'true', |
845 | }, |
846 | instance=rack) |
847 | rack = form.save() |
848 | |
849 | === modified file 'src/maasserver/tests/test_forms_machine.py' |
850 | --- src/maasserver/tests/test_forms_machine.py 2016-09-22 02:53:33 +0000 |
851 | +++ src/maasserver/tests/test_forms_machine.py 2016-10-20 20:55:55 +0000 |
852 | @@ -373,7 +373,7 @@ |
853 | 'architecture': arch, |
854 | 'power_type': power_type, |
855 | 'power_parameters_field': power_parameters_field, |
856 | - 'power_parameters_skip_check': True, |
857 | + 'power_parameters_skip_check': 'true', |
858 | }, |
859 | instance=node) |
860 | form.save() |
861 | @@ -393,6 +393,7 @@ |
862 | data={ |
863 | 'hostname': hostname, |
864 | 'architecture': arch, |
865 | + 'power_parameters_skip_check': 'true', |
866 | }, |
867 | instance=node) |
868 | node = form.save() |
869 | @@ -407,6 +408,7 @@ |
870 | data={ |
871 | 'hostname': hostname, |
872 | 'architecture': arch, |
873 | + 'power_parameters_skip_check': 'true', |
874 | }, |
875 | instance=node) |
876 | node = form.save() |
877 | @@ -422,6 +424,7 @@ |
878 | 'hostname': hostname, |
879 | 'architecture': arch, |
880 | 'power_type': power_type, |
881 | + 'power_parameters_skip_check': 'true', |
882 | }, |
883 | instance=node) |
884 | node = form.save() |
885 | |
886 | === modified file 'src/maasserver/websockets/handlers/machine.py' |
887 | --- src/maasserver/websockets/handlers/machine.py 2016-10-17 20:09:56 +0000 |
888 | +++ src/maasserver/websockets/handlers/machine.py 2016-10-20 20:55:55 +0000 |
889 | @@ -237,6 +237,7 @@ |
890 | new_params["hostname"] = params.get("hostname") |
891 | new_params["architecture"] = params.get("architecture") |
892 | new_params["power_type"] = params.get("power_type") |
893 | + new_params["power_parameters"] = params.get("power_parameters") |
894 | if "zone" in params: |
895 | new_params["zone"] = params["zone"]["name"] |
896 | if "domain" in params: |
897 | @@ -259,13 +260,8 @@ |
898 | if not reload_object(self.user).is_superuser: |
899 | raise HandlerPermissionError() |
900 | |
901 | - # Create the object, then save the power parameters because the |
902 | - # form will not save this information. |
903 | data = super(NodeHandler, self).create(params) |
904 | node_obj = Node.objects.get(system_id=data['system_id']) |
905 | - node_obj.power_type = params.get("power_type", '') |
906 | - node_obj.power_parameters = params.get("power_parameters", {}) |
907 | - node_obj.save() |
908 | |
909 | # Start the commissioning process right away, which has the |
910 | # desired side effect of initializing the node's power state. |
911 | @@ -279,16 +275,13 @@ |
912 | if not reload_object(self.user).is_superuser: |
913 | raise HandlerPermissionError() |
914 | |
915 | - # Update the node with the form. The form will not update the |
916 | - # power_type or power_parameters, so we perform that here. |
917 | data = super(NodeHandler, self).update(params) |
918 | node_obj = Node.objects.get(system_id=data['system_id']) |
919 | - node_obj.power_type = params.get("power_type", '') |
920 | - node_obj.power_parameters = params.get("power_parameters", {}) |
921 | |
922 | # Update the tags for the node and disks. |
923 | self.update_tags(node_obj, params['tags']) |
924 | node_obj.save() |
925 | + |
926 | return self.full_dehydrate(node_obj) |
927 | |
928 | def mount_special(self, params): |
929 | |
930 | === modified file 'src/maasserver/websockets/handlers/tests/test_machine.py' |
931 | --- src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-17 20:09:56 +0000 |
932 | +++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-20 20:55:55 +0000 |
933 | @@ -1145,7 +1145,7 @@ |
934 | def test_update_raises_validation_error_for_invalid_architecture(self): |
935 | user = factory.make_admin() |
936 | handler = MachineHandler(user, {}) |
937 | - node = factory.make_Node(interface=True) |
938 | + node = factory.make_Node(interface=True, power_type='manual') |
939 | node_data = self.dehydrate_node(node, handler) |
940 | arch = factory.make_name("arch") |
941 | node_data["architecture"] = arch |
942 | @@ -1195,7 +1195,8 @@ |
943 | user = factory.make_admin() |
944 | handler = MachineHandler(user, {}) |
945 | architecture = make_usable_architecture(self) |
946 | - node = factory.make_Node(interface=True, architecture=architecture) |
947 | + node = factory.make_Node( |
948 | + interface=True, architecture=architecture, power_type='manual') |
949 | tags = [ |
950 | factory.make_Tag(definition='').name |
951 | for _ in range(3) |
952 | @@ -1209,7 +1210,8 @@ |
953 | user = factory.make_admin() |
954 | handler = MachineHandler(user, {}) |
955 | architecture = make_usable_architecture(self) |
956 | - node = factory.make_Node(interface=True, architecture=architecture) |
957 | + node = factory.make_Node( |
958 | + interface=True, architecture=architecture, power_type='manual') |
959 | tags = [] |
960 | for _ in range(3): |
961 | tag = factory.make_Tag(definition='') |
962 | @@ -1226,7 +1228,8 @@ |
963 | user = factory.make_admin() |
964 | handler = MachineHandler(user, {}) |
965 | architecture = make_usable_architecture(self) |
966 | - node = factory.make_Node(interface=True, architecture=architecture) |
967 | + node = factory.make_Node( |
968 | + interface=True, architecture=architecture, power_type='manual') |
969 | tag_name = factory.make_name("tag") |
970 | node_data = self.dehydrate_node(node, handler) |
971 | node_data["tags"].append(tag_name) |
972 | |
973 | === modified file 'src/maasserver/websockets/tests/test_base.py' |
974 | --- src/maasserver/websockets/tests/test_base.py 2016-09-27 14:24:19 +0000 |
975 | +++ src/maasserver/websockets/tests/test_base.py 2016-10-20 20:55:55 +0000 |
976 | @@ -523,7 +523,7 @@ |
977 | |
978 | def test_update_with_form_updates_node(self): |
979 | arch = make_usable_architecture(self) |
980 | - node = factory.make_Node(architecture=arch) |
981 | + node = factory.make_Node(architecture=arch, power_type='manual') |
982 | hostname = factory.make_name("hostname") |
983 | handler = self.make_nodes_handler( |
984 | fields=['hostname'], form=AdminMachineForm) |
985 | @@ -539,7 +539,7 @@ |
986 | |
987 | def test_update_with_form_uses_form_from_get_form_class(self): |
988 | arch = make_usable_architecture(self) |
989 | - node = factory.make_Node(architecture=arch) |
990 | + node = factory.make_Node(architecture=arch, power_type='manual') |
991 | hostname = factory.make_name("hostname") |
992 | handler = self.make_nodes_handler(fields=['hostname']) |
993 | self.patch( |
994 | |
995 | === modified file 'src/provisioningserver/power/schema.py' |
996 | --- src/provisioningserver/power/schema.py 2016-09-27 18:38:53 +0000 |
997 | +++ src/provisioningserver/power/schema.py 2016-10-20 20:55:55 +0000 |
998 | @@ -238,9 +238,10 @@ |
999 | 'name': 'virsh', |
1000 | 'description': 'Virsh (virtual systems)', |
1001 | 'fields': [ |
1002 | - make_json_field('power_address', "Power address"), |
1003 | + make_json_field('power_address', "Power address", required=True), |
1004 | make_json_field( |
1005 | - 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE), |
1006 | + 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE, |
1007 | + required=True), |
1008 | make_json_field( |
1009 | 'power_pass', "Power password (optional)", |
1010 | required=False, field_type='password'), |
1011 | @@ -258,10 +259,11 @@ |
1012 | make_json_field( |
1013 | 'power_uuid', "VM UUID (if known)", required=False, |
1014 | scope=POWER_PARAMETER_SCOPE.NODE), |
1015 | - make_json_field('power_address', "VMware hostname"), |
1016 | - make_json_field('power_user', "VMware username"), |
1017 | + make_json_field('power_address', "VMware hostname", required=True), |
1018 | + make_json_field('power_user', "VMware username", required=True), |
1019 | make_json_field( |
1020 | - 'power_pass', "VMware password", field_type='password'), |
1021 | + 'power_pass', "VMware password", field_type='password', |
1022 | + required=True), |
1023 | make_json_field( |
1024 | 'power_port', "VMware API port (optional)", required=False), |
1025 | make_json_field( |
1026 | @@ -273,9 +275,10 @@ |
1027 | 'name': 'fence_cdu', |
1028 | 'description': 'Sentry Switch CDU', |
1029 | 'fields': [ |
1030 | - make_json_field('power_address', "Power address"), |
1031 | + make_json_field('power_address', "Power address", required=True), |
1032 | make_json_field( |
1033 | - 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE), |
1034 | + 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE, |
1035 | + required=True), |
1036 | make_json_field('power_user', "Power user"), |
1037 | make_json_field( |
1038 | 'power_pass', "Power password", field_type='password'), |
1039 | @@ -288,8 +291,9 @@ |
1040 | 'fields': [ |
1041 | make_json_field( |
1042 | 'power_driver', "Power driver", field_type='choice', |
1043 | - choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0), |
1044 | - make_json_field('power_address', "IP address"), |
1045 | + choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0, |
1046 | + required=True), |
1047 | + make_json_field('power_address', "IP address", required=True), |
1048 | make_json_field('power_user', "Power user"), |
1049 | make_json_field( |
1050 | 'power_pass', "Power password", field_type='password'), |
1051 | @@ -302,13 +306,13 @@ |
1052 | 'name': 'moonshot', |
1053 | 'description': 'HP Moonshot - iLO4 (IPMI)', |
1054 | 'fields': [ |
1055 | - make_json_field('power_address', "Power address"), |
1056 | + make_json_field('power_address', "Power address", required=True), |
1057 | make_json_field('power_user', "Power user"), |
1058 | make_json_field( |
1059 | 'power_pass', "Power password", field_type='password'), |
1060 | make_json_field( |
1061 | 'power_hwaddress', "Power hardware address", |
1062 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1063 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1064 | ], |
1065 | 'ip_extractor': make_ip_extractor('power_address'), |
1066 | }, |
1067 | @@ -317,14 +321,16 @@ |
1068 | 'description': 'SeaMicro 15000', |
1069 | 'fields': [ |
1070 | make_json_field( |
1071 | - 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE), |
1072 | - make_json_field('power_address', "Power address"), |
1073 | + 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE, |
1074 | + required=True), |
1075 | + make_json_field('power_address', "Power address", required=True), |
1076 | make_json_field('power_user', "Power user"), |
1077 | make_json_field( |
1078 | 'power_pass', "Power password", field_type='password'), |
1079 | make_json_field( |
1080 | 'power_control', "Power control type", field_type='choice', |
1081 | - choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi'), |
1082 | + choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi', |
1083 | + required=True), |
1084 | ], |
1085 | 'ip_extractor': make_ip_extractor('power_address'), |
1086 | }, |
1087 | @@ -334,7 +340,7 @@ |
1088 | 'fields': [ |
1089 | make_json_field( |
1090 | 'power_pass', "Power password", field_type='password'), |
1091 | - make_json_field('power_address', "Power address") |
1092 | + make_json_field('power_address', "Power address", required=True) |
1093 | ], |
1094 | 'ip_extractor': make_ip_extractor('power_address'), |
1095 | }, |
1096 | @@ -343,8 +349,9 @@ |
1097 | 'description': 'Digital Loggers, Inc. PDU', |
1098 | 'fields': [ |
1099 | make_json_field( |
1100 | - 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE), |
1101 | - make_json_field('power_address', "Power address"), |
1102 | + 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE, |
1103 | + required=True), |
1104 | + make_json_field('power_address', "Power address", required=True), |
1105 | make_json_field('power_user', "Power user"), |
1106 | make_json_field( |
1107 | 'power_pass', "Power password", field_type='password'), |
1108 | @@ -355,7 +362,7 @@ |
1109 | 'name': 'wedge', |
1110 | 'description': "Facebook's Wedge", |
1111 | 'fields': [ |
1112 | - make_json_field('power_address', "IP address"), |
1113 | + make_json_field('power_address', "IP address", required=True), |
1114 | make_json_field('power_user', "Power user"), |
1115 | make_json_field( |
1116 | 'power_pass', "Power password", field_type='password'), |
1117 | @@ -367,8 +374,9 @@ |
1118 | 'description': "Cisco UCS Manager", |
1119 | 'fields': [ |
1120 | make_json_field( |
1121 | - 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE), |
1122 | - make_json_field('power_address', "URL for XML API"), |
1123 | + 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE, |
1124 | + required=True), |
1125 | + make_json_field('power_address', "URL for XML API", required=True), |
1126 | make_json_field('power_user', "API user"), |
1127 | make_json_field( |
1128 | 'power_pass', "API password", field_type='password'), |
1129 | @@ -380,7 +388,8 @@ |
1130 | 'name': 'mscm', |
1131 | 'description': "HP Moonshot - iLO Chassis Manager", |
1132 | 'fields': [ |
1133 | - make_json_field('power_address', "IP for MSCM CLI API"), |
1134 | + make_json_field( |
1135 | + 'power_address', "IP for MSCM CLI API", required=True), |
1136 | make_json_field('power_user', "MSCM CLI API user"), |
1137 | make_json_field( |
1138 | 'power_pass', "MSCM CLI API password", field_type='password'), |
1139 | @@ -388,7 +397,7 @@ |
1140 | 'node_id', |
1141 | "Node ID - Must adhere to cXnY format " |
1142 | "(X=cartridge number, Y=node number).", |
1143 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1144 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1145 | ], |
1146 | 'ip_extractor': make_ip_extractor('power_address'), |
1147 | }, |
1148 | @@ -396,14 +405,14 @@ |
1149 | 'name': 'msftocs', |
1150 | 'description': "Microsoft OCS - Chassis Manager", |
1151 | 'fields': [ |
1152 | - make_json_field('power_address', "Power address"), |
1153 | + make_json_field('power_address', "Power address", required=True), |
1154 | make_json_field('power_port', "Power port"), |
1155 | make_json_field('power_user', "Power user"), |
1156 | make_json_field( |
1157 | 'power_pass', "Power password", field_type='password'), |
1158 | make_json_field( |
1159 | 'blade_id', "Blade ID (Typically 1-24)", |
1160 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1161 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1162 | ], |
1163 | 'ip_extractor': make_ip_extractor('power_address'), |
1164 | }, |
1165 | @@ -411,10 +420,10 @@ |
1166 | 'name': 'apc', |
1167 | 'description': "American Power Conversion (APC) PDU", |
1168 | 'fields': [ |
1169 | - make_json_field('power_address', "IP for APC PDU"), |
1170 | + make_json_field('power_address', "IP for APC PDU", required=True), |
1171 | make_json_field( |
1172 | 'node_outlet', "APC PDU node outlet number (1-16)", |
1173 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1174 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1175 | make_json_field( |
1176 | 'power_on_delay', "Power ON outlet delay (seconds)", |
1177 | default='5'), |
1178 | @@ -425,16 +434,16 @@ |
1179 | 'name': 'hmc', |
1180 | 'description': "IBM Hardware Management Console (HMC)", |
1181 | 'fields': [ |
1182 | - make_json_field('power_address', "IP for HMC"), |
1183 | + make_json_field('power_address', "IP for HMC", required=True), |
1184 | make_json_field('power_user', "HMC username"), |
1185 | make_json_field( |
1186 | 'power_pass', "HMC password", field_type='password'), |
1187 | make_json_field( |
1188 | 'server_name', "HMC Managed System server name", |
1189 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1190 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1191 | make_json_field( |
1192 | 'lpar', "HMC logical partition", |
1193 | - scope=POWER_PARAMETER_SCOPE.NODE), |
1194 | + scope=POWER_PARAMETER_SCOPE.NODE, required=True), |
1195 | ], |
1196 | 'ip_extractor': make_ip_extractor('power_address'), |
1197 | }, |
1198 | @@ -442,11 +451,13 @@ |
1199 | 'name': 'nova', |
1200 | 'description': 'OpenStack Nova', |
1201 | 'fields': [ |
1202 | - make_json_field('nova_id', "Host UUID"), |
1203 | - make_json_field('os_tenantname', "Tenant name"), |
1204 | - make_json_field('os_username', "Username"), |
1205 | - make_json_field('os_password', "Password", field_type='password'), |
1206 | - make_json_field('os_authurl', "Auth URL"), |
1207 | + make_json_field('nova_id', "Host UUID", required=True), |
1208 | + make_json_field('os_tenantname', "Tenant name", required=True), |
1209 | + make_json_field('os_username', "Username", required=True), |
1210 | + make_json_field( |
1211 | + 'os_password', "Password", field_type='password', |
1212 | + required=True), |
1213 | + make_json_field('os_authurl', "Auth URL", required=True), |
1214 | ], |
1215 | }, |
1216 | ] |
Much cleaner thanks for taking this on. I do feel like Django forms just get dirty as so many different conditions can occur. Just on comment that should be addresses before landing, but not going to block you.