Merge lp:~ltrager/maas/lp1593991 into lp:maas/trunk

Proposed by Lee Trager on 2016-10-20
Status: Merged
Approved by: Lee Trager on 2016-10-20
Approved revision: 5493
Merged at revision: 5499
Proposed branch: lp:~ltrager/maas/lp1593991
Merge into: lp: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
Reviewer Review Type Date Requested Status
Blake Rouse 2016-10-20 Approve on 2016-10-20
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.

To post a comment you must log in.
lp:~ltrager/maas/lp1593991 updated on 2016-10-20
5491. By Lee Trager on 2016-10-20

Require the power_id

5492. By Lee Trager on 2016-10-20

Merge trunk

Blake Rouse (blake-rouse) wrote :

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.

review: Approve
lp:~ltrager/maas/lp1593991 updated on 2016-10-20
5493. By Lee Trager on 2016-10-20

Nova requires all fields

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 ]