Merge lp:~ltrager/maas/lp1593991_2.0 into lp:maas/2.0

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 5199
Proposed branch: lp:~ltrager/maas/lp1593991_2.0
Merge into: lp:maas/2.0
Diff against target: 1223 lines (+297/-141)
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 (+35/-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 (+44/-33)
To merge this branch: bzr merge lp:~ltrager/maas/lp1593991_2.0
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+309157@code.launchpad.net

Commit message

Backport from 2.1.1 - Validate power parameters in the node form.

Description of the change

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

You can self-approve and immediately land backports, unless there was a merge conflict and you'd like to get another pair of eyes. I'm assuming that is the case here.

review: Approve

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

Subscribers

People subscribed via source and target branches

to all changes: