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

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 5499
Proposed branch: lp:~ltrager/maas/lp1593991
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 1216 lines (+296/-142)
21 files modified
src/maasserver/api/machines.py (+0/-3)
src/maasserver/api/tests/test_enlistment.py (+21/-12)
src/maasserver/api/tests/test_machine.py (+33/-22)
src/maasserver/api/tests/test_node.py (+1/-1)
src/maasserver/api/tests/test_rackcontroller.py (+2/-1)
src/maasserver/api/tests/test_utils.py (+20/-0)
src/maasserver/api/utils.py (+14/-1)
src/maasserver/clusterrpc/power_parameters.py (+24/-5)
src/maasserver/clusterrpc/tests/test_power_parameters.py (+48/-0)
src/maasserver/config_forms.py (+3/-0)
src/maasserver/forms.py (+37/-6)
src/maasserver/rpc/nodes.py (+0/-4)
src/maasserver/rpc/tests/test_nodes.py (+18/-32)
src/maasserver/testing/factory.py (+2/-3)
src/maasserver/tests/test_config_forms.py (+9/-0)
src/maasserver/tests/test_forms_controller.py (+4/-2)
src/maasserver/tests/test_forms_machine.py (+4/-1)
src/maasserver/websockets/handlers/machine.py (+2/-9)
src/maasserver/websockets/handlers/tests/test_machine.py (+7/-4)
src/maasserver/websockets/tests/test_base.py (+2/-2)
src/provisioningserver/power/schema.py (+45/-34)
To merge this branch: bzr merge lp:~ltrager/maas/lp1593991
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+308893@code.launchpad.net

Commit message

Validate power parameters in the node form.

Description of the change

Previously power parameters were hacked on after form validation and the node object was saved. This was done wherever a node form was used(API, websockets, RPC) Most likely this was done because the form was not picking up the power parameters. The form wasn't picking up power paramters because of three issues, we filter all fields before going into the form and were not accepting power parameters, django requires a prefix when dealing with subfields, and we weren't properly setting the initial fields for subitems.

The power parameters are now validated based on the fields given by each power driver. This not only solves lp:1593991 by ensuring a power_parameter is set but also gives more specific validation errors. For exammple, if a field is missing or if a given field is invalid for the power type.

I noticed that the power drivers were not setting any field to be required. I set the power address for each power type as required and kept all other fields as is. Please let me know if other fields should be listed as required.

To post a comment you must log in.
Revision history for this message
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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/machines.py'
--- src/maasserver/api/machines.py 2016-10-12 02:26:47 +0000
+++ src/maasserver/api/machines.py 2016-10-20 20:55:55 +0000
@@ -34,7 +34,6 @@
34 OwnerDataMixin,34 OwnerDataMixin,
35 PowerMixin,35 PowerMixin,
36 PowersMixin,36 PowersMixin,
37 store_node_power_parameters,
38)37)
39from maasserver.api.support import (38from maasserver.api.support import (
40 admin_method,39 admin_method,
@@ -796,8 +795,6 @@
796 form = Form(data=altered_query_data, request=request)795 form = Form(data=altered_query_data, request=request)
797 if form.is_valid():796 if form.is_valid():
798 machine = form.save()797 machine = form.save()
799 # Hack in the power parameters here.
800 store_node_power_parameters(machine, request)
801 maaslog.info("%s: Enlisted new machine", machine.hostname)798 maaslog.info("%s: Enlisted new machine", machine.hostname)
802 return machine799 return machine
803 else:800 else:
804801
=== modified file 'src/maasserver/api/tests/test_enlistment.py'
--- src/maasserver/api/tests/test_enlistment.py 2016-09-22 02:53:33 +0000
+++ src/maasserver/api/tests/test_enlistment.py 2016-10-20 20:55:55 +0000
@@ -87,6 +87,7 @@
87 architecture = make_usable_architecture(self)87 architecture = make_usable_architecture(self)
88 power_type = 'ipmi'88 power_type = 'ipmi'
89 power_parameters = {89 power_parameters = {
90 "power_address": factory.make_ip_address(),
90 "power_user": factory.make_name("power-user"),91 "power_user": factory.make_name("power-user"),
91 "power_pass": factory.make_name("power-pass"),92 "power_pass": factory.make_name("power-pass"),
92 }93 }
@@ -95,14 +96,16 @@
95 {96 {
96 'hostname': hostname,97 'hostname': hostname,
97 'architecture': architecture,98 'architecture': architecture,
98 'power_type': 'manual',99 'power_type': power_type,
99 'mac_addresses': factory.make_mac_address(),100 'mac_addresses': factory.make_mac_address(),
100 'power_parameters': json.dumps(power_parameters),101 'power_parameters': json.dumps(power_parameters),
101 'power_type': power_type,
102 })102 })
103 # Add the default values.
104 power_parameters['power_driver'] = 'LAN_2_0'
105 power_parameters['mac_address'] = ''
103 self.assertEqual(http.client.OK, response.status_code)106 self.assertEqual(http.client.OK, response.status_code)
104 [machine] = Machine.objects.filter(hostname=hostname)107 [machine] = Machine.objects.filter(hostname=hostname)
105 self.assertEqual(power_parameters, machine.power_parameters)108 self.assertItemsEqual(power_parameters, machine.power_parameters)
106 self.assertEqual(power_type, machine.power_type)109 self.assertEqual(power_type, machine.power_type)
107110
108 def test_POST_create_creates_machine_with_arch_only(self):111 def test_POST_create_creates_machine_with_arch_only(self):
@@ -454,23 +457,29 @@
454 self.assertEqual([], machines_returned)457 self.assertEqual([], machines_returned)
455458
456 def test_POST_simple_user_can_set_power_type_and_parameters(self):459 def test_POST_simple_user_can_set_power_type_and_parameters(self):
457 new_power_address = factory.make_string()460 new_power_address = factory.make_url()
461 new_power_id = factory.make_name('power_id')
458 response = self.client.post(462 response = self.client.post(
459 reverse('machines_handler'), {463 reverse('machines_handler'), {
460 'architecture': make_usable_architecture(self),464 'architecture': make_usable_architecture(self),
461 'power_type': 'manual',465 'power_type': 'virsh',
462 'power_parameters': json.dumps(466 'power_parameters': json.dumps(
463 {"power_address": new_power_address}),467 {
468 "power_address": new_power_address,
469 "power_id": new_power_id,
470 }),
464 'mac_addresses': ['AA:BB:CC:DD:EE:FF'],471 'mac_addresses': ['AA:BB:CC:DD:EE:FF'],
465 })472 })
466
467 machine = Machine.objects.get(473 machine = Machine.objects.get(
468 system_id=json_load_bytes(response.content)['system_id'])474 system_id=json_load_bytes(response.content)['system_id'])
469 self.assertEqual(475 self.assertEqual(http.client.OK, response.status_code)
470 (http.client.OK, {"power_address": new_power_address},476 self.assertEqual('virsh', machine.power_type)
471 'manual'),477 self.assertItemsEqual(
472 (response.status_code, machine.power_parameters,478 {
473 machine.power_type))479 'power_pass': '',
480 'power_id': new_power_id,
481 'power_address': new_power_address,
482 }, machine.power_parameters)
474483
475 def test_POST_returns_limited_fields(self):484 def test_POST_returns_limited_fields(self):
476 response = self.client.post(485 response = self.client.post(
477486
=== modified file 'src/maasserver/api/tests/test_machine.py'
--- src/maasserver/api/tests/test_machine.py 2016-10-18 08:00:37 +0000
+++ src/maasserver/api/tests/test_machine.py 2016-10-20 20:55:55 +0000
@@ -850,7 +850,7 @@
850 # The api allows the updating of a Machine.850 # The api allows the updating of a Machine.
851 machine = factory.make_Node(851 machine = factory.make_Node(
852 hostname='diane', owner=self.user,852 hostname='diane', owner=self.user,
853 architecture=make_usable_architecture(self))853 architecture=make_usable_architecture(self), power_type='manual')
854 response = self.client.put(854 response = self.client.put(
855 self.get_machine_uri(machine), {'hostname': 'francis'})855 self.get_machine_uri(machine), {'hostname': 'francis'})
856 parsed_result = json_load_bytes(response.content)856 parsed_result = json_load_bytes(response.content)
@@ -867,7 +867,8 @@
867 hostname = factory.make_name('hostname')867 hostname = factory.make_name('hostname')
868 arch = make_usable_architecture(self)868 arch = make_usable_architecture(self)
869 machine = factory.make_Node(869 machine = factory.make_Node(
870 hostname=hostname, owner=self.user, architecture=arch)870 hostname=hostname, owner=self.user, architecture=arch,
871 power_type='manual')
871 response = self.client.put(872 response = self.client.put(
872 self.get_machine_uri(machine),873 self.get_machine_uri(machine),
873 {'architecture': arch})874 {'architecture': arch})
@@ -890,7 +891,8 @@
890 self.become_admin()891 self.become_admin()
891 machine = factory.make_Node(892 machine = factory.make_Node(
892 owner=self.user,893 owner=self.user,
893 architecture=make_usable_architecture(self))894 architecture=make_usable_architecture(self),
895 power_type='manual')
894 field = factory.make_string()896 field = factory.make_string()
895 response = self.client.put(897 response = self.client.put(
896 self.get_machine_uri(machine),898 self.get_machine_uri(machine),
@@ -908,7 +910,11 @@
908 power_type=original_power_type,910 power_type=original_power_type,
909 architecture=make_usable_architecture(self))911 architecture=make_usable_architecture(self))
910 response = self.client.put(912 response = self.client.put(
911 self.get_machine_uri(machine), {'power_type': new_power_type})913 self.get_machine_uri(machine),
914 {
915 'power_type': new_power_type,
916 'power_parameters_skip_check': 'true',
917 })
912918
913 self.assertEqual(http.client.OK, response.status_code)919 self.assertEqual(http.client.OK, response.status_code)
914 self.assertEqual(920 self.assertEqual(
@@ -932,7 +938,8 @@
932 # provides the URI for this Machine.938 # provides the URI for this Machine.
933 machine = factory.make_Node(939 machine = factory.make_Node(
934 hostname='diane', owner=self.user,940 hostname='diane', owner=self.user,
935 architecture=make_usable_architecture(self))941 architecture=make_usable_architecture(self),
942 power_type='manual')
936 response = self.client.put(943 response = self.client.put(
937 self.get_machine_uri(machine), {'hostname': 'francis'})944 self.get_machine_uri(machine), {'hostname': 'francis'})
938 parsed_result = json_load_bytes(response.content)945 parsed_result = json_load_bytes(response.content)
@@ -948,7 +955,8 @@
948 self.become_admin()955 self.become_admin()
949 machine = factory.make_Node(956 machine = factory.make_Node(
950 hostname='diane', owner=self.user,957 hostname='diane', owner=self.user,
951 architecture=make_usable_architecture(self))958 architecture=make_usable_architecture(self),
959 power_type='manual')
952 response = self.client.put(960 response = self.client.put(
953 self.get_machine_uri(machine), {'hostname': '.'})961 self.get_machine_uri(machine), {'hostname': '.'})
954 parsed_result = json_load_bytes(response.content)962 parsed_result = json_load_bytes(response.content)
@@ -1001,8 +1009,8 @@
1001 self.become_admin()1009 self.become_admin()
1002 machine = factory.make_Node(1010 machine = factory.make_Node(
1003 owner=self.user,1011 owner=self.user,
1004 power_type=factory.pick_power_type(),1012 architecture=make_usable_architecture(self),
1005 architecture=make_usable_architecture(self))1013 power_type='manual')
1006 response = self.client.put(1014 response = self.client.put(
1007 self.get_machine_uri(machine),1015 self.get_machine_uri(machine),
1008 {'cpu_count': 1, 'memory': 1024})1016 {'cpu_count': 1, 'memory': 1024})
@@ -1109,7 +1117,8 @@
1109 self.become_admin()1117 self.become_admin()
1110 machine = factory.make_Node(1118 machine = factory.make_Node(
1111 owner=self.user,1119 owner=self.user,
1112 architecture=make_usable_architecture(self))1120 architecture=make_usable_architecture(self),
1121 power_parameters={})
1113 new_param = factory.make_string()1122 new_param = factory.make_string()
1114 new_value = factory.make_string()1123 new_value = factory.make_string()
1115 response = self.client.put(1124 response = self.client.put(
@@ -1125,7 +1134,11 @@
11251134
1126 def test_PUT_updates_power_parameters_empty_string(self):1135 def test_PUT_updates_power_parameters_empty_string(self):
1127 self.become_admin()1136 self.become_admin()
1128 power_parameters = {factory.make_string(): factory.make_string()}1137 power_parameters = {
1138 'power_address': factory.make_url(),
1139 'power_id': factory.make_name('power_id'),
1140 'power_pass': factory.make_name('power_pass'),
1141 }
1129 machine = factory.make_Node(1142 machine = factory.make_Node(
1130 owner=self.user,1143 owner=self.user,
1131 power_type='virsh',1144 power_type='virsh',
@@ -1133,22 +1146,17 @@
1133 architecture=make_usable_architecture(self))1146 architecture=make_usable_architecture(self))
1134 response = self.client.put(1147 response = self.client.put(
1135 self.get_machine_uri(machine),1148 self.get_machine_uri(machine),
1136 {'power_parameters_power_id': ''})1149 {'power_parameters_power_pass': ''})
11371150
1138 self.assertEqual(http.client.OK, response.status_code)1151 self.assertEqual(http.client.OK, response.status_code)
1139 self.assertEqual(1152 self.assertEqual(
1140 {1153 '', reload_object(machine).power_parameters['power_pass'])
1141 'power_id': '',
1142 'power_pass': '',
1143 'power_address': '',
1144 },
1145 reload_object(machine).power_parameters)
11461154
1147 def test_PUT_sets_zone(self):1155 def test_PUT_sets_zone(self):
1148 self.become_admin()1156 self.become_admin()
1149 new_zone = factory.make_Zone()1157 new_zone = factory.make_Zone()
1150 machine = factory.make_Node(1158 machine = factory.make_Node(
1151 architecture=make_usable_architecture(self))1159 architecture=make_usable_architecture(self), power_type='manual')
11521160
1153 response = self.client.put(1161 response = self.client.put(
1154 self.get_machine_uri(machine), {'zone': new_zone.name})1162 self.get_machine_uri(machine), {'zone': new_zone.name})
@@ -1161,7 +1169,7 @@
1161 self.become_admin()1169 self.become_admin()
1162 new_name = factory.make_name()1170 new_name = factory.make_name()
1163 machine = factory.make_Node(1171 machine = factory.make_Node(
1164 architecture=make_usable_architecture(self))1172 architecture=make_usable_architecture(self), power_type='manual')
1165 old_zone = machine.zone1173 old_zone = machine.zone
11661174
1167 response = self.client.put(1175 response = self.client.put(
@@ -1190,7 +1198,8 @@
1190 self.become_admin()1198 self.become_admin()
1191 zone = factory.make_Zone()1199 zone = factory.make_Zone()
1192 machine = factory.make_Node(1200 machine = factory.make_Node(
1193 zone=zone, architecture=make_usable_architecture(self))1201 zone=zone, architecture=make_usable_architecture(self),
1202 power_type='manual')
11941203
1195 response = self.client.put(self.get_machine_uri(machine), {})1204 response = self.client.put(self.get_machine_uri(machine), {})
11961205
@@ -1226,7 +1235,8 @@
1226 self.become_admin()1235 self.become_admin()
1227 machine = factory.make_Node(1236 machine = factory.make_Node(
1228 owner=self.user,1237 owner=self.user,
1229 architecture=make_usable_architecture(self))1238 architecture=make_usable_architecture(self),
1239 power_type='manual')
1230 response = self.client.put(1240 response = self.client.put(
1231 reverse('machine_handler', args=[machine.system_id]),1241 reverse('machine_handler', args=[machine.system_id]),
1232 {'swap_size': 5 * 1000 ** 3}) # Making sure we overflow 32 bits1242 {'swap_size': 5 * 1000 ** 3}) # Making sure we overflow 32 bits
@@ -1239,7 +1249,8 @@
1239 self.become_admin()1249 self.become_admin()
1240 machine = factory.make_Node(1250 machine = factory.make_Node(
1241 owner=self.user,1251 owner=self.user,
1242 architecture=make_usable_architecture(self))1252 architecture=make_usable_architecture(self),
1253 power_type='manual')
12431254
1244 response = self.client.put(1255 response = self.client.put(
1245 reverse('machine_handler', args=[machine.system_id]),1256 reverse('machine_handler', args=[machine.system_id]),
12461257
=== modified file 'src/maasserver/api/tests/test_node.py'
--- src/maasserver/api/tests/test_node.py 2016-08-31 13:52:59 +0000
+++ src/maasserver/api/tests/test_node.py 2016-10-20 20:55:55 +0000
@@ -303,7 +303,7 @@
303 self.assertEqual(303 self.assertEqual(
304 http.client.OK, response.status_code, response.content)304 http.client.OK, response.status_code, response.content)
305 parsed_params = json_load_bytes(response.content)305 parsed_params = json_load_bytes(response.content)
306 self.assertEqual({}, parsed_params)306 self.assertEqual(node.power_parameters, parsed_params)
307307
308 def test_power_parameters_requires_admin(self):308 def test_power_parameters_requires_admin(self):
309 node = factory.make_Node()309 node = factory.make_Node()
310310
=== modified file 'src/maasserver/api/tests/test_rackcontroller.py'
--- src/maasserver/api/tests/test_rackcontroller.py 2016-06-08 20:20:40 +0000
+++ src/maasserver/api/tests/test_rackcontroller.py 2016-10-20 20:55:55 +0000
@@ -36,7 +36,8 @@
3636
37 def test_PUT_updates_rack_controller(self):37 def test_PUT_updates_rack_controller(self):
38 self.become_admin()38 self.become_admin()
39 rack = factory.make_RackController(owner=self.user)39 rack = factory.make_RackController(
40 owner=self.user, power_type='manual')
40 zone = factory.make_zone()41 zone = factory.make_zone()
41 response = self.client.put(42 response = self.client.put(
42 self.get_rack_uri(rack), {'zone': zone.name})43 self.get_rack_uri(rack), {'zone': zone.name})
4344
=== modified file 'src/maasserver/api/tests/test_utils.py'
--- src/maasserver/api/tests/test_utils.py 2016-05-24 13:51:36 +0000
+++ src/maasserver/api/tests/test_utils.py 2016-10-20 20:55:55 +0000
@@ -7,6 +7,7 @@
77
8from collections import namedtuple8from collections import namedtuple
99
10from django.forms import CharField
10from django.http import QueryDict11from django.http import QueryDict
11from maasserver.api.utils import (12from maasserver.api.utils import (
12 extract_bool,13 extract_bool,
@@ -15,6 +16,7 @@
15 get_oauth_token,16 get_oauth_token,
16 get_overridden_query_dict,17 get_overridden_query_dict,
17)18)
19from maasserver.config_forms import DictCharField
18from maasserver.exceptions import Unauthorized20from maasserver.exceptions import Unauthorized
19from maasserver.testing.factory import factory21from maasserver.testing.factory import factory
20from maasserver.testing.testcase import MAASServerTestCase22from maasserver.testing.testcase import MAASServerTestCase
@@ -94,6 +96,24 @@
94 results = get_overridden_query_dict(defaults, data, [key1])96 results = get_overridden_query_dict(defaults, data, [key1])
95 self.assertEqual([data_value2], results.getlist(key2))97 self.assertEqual([data_value2], results.getlist(key2))
9698
99 def test_expands_dict_fields(self):
100 field_name = factory.make_name('field_name')
101 sub_fields = {
102 factory.make_name('sub_field'): CharField() for _ in range(3)
103 }
104 fields = {
105 field_name: DictCharField(sub_fields)
106 }
107 defaults = {
108 "%s_%s" % (field_name, field): factory.make_name('subfield')
109 for field in sub_fields.keys()
110 }
111 data = {field_name: DictCharField(fields)}
112 results = get_overridden_query_dict(defaults, data, fields)
113 expected = {key: [value] for key, value in defaults.items()}
114 expected.update(fields)
115 self.assertItemsEqual(expected, results)
116
97117
98def make_fake_request(auth_header):118def make_fake_request(auth_header):
99 """Create a very simple fake request, with just an auth header."""119 """Create a very simple fake request, with just an auth header."""
100120
=== modified file 'src/maasserver/api/utils.py'
--- src/maasserver/api/utils.py 2016-09-21 23:23:54 +0000
+++ src/maasserver/api/utils.py 2016-10-20 20:55:55 +0000
@@ -16,6 +16,7 @@
1616
17from django.http import QueryDict17from django.http import QueryDict
18from formencode.validators import Invalid18from formencode.validators import Invalid
19from maasserver.config_forms import DictCharField
19from maasserver.exceptions import (20from maasserver.exceptions import (
20 MAASAPIValidationError,21 MAASAPIValidationError,
21 Unauthorized,22 Unauthorized,
@@ -158,11 +159,23 @@
158 """159 """
159 # Create a writable query dict.160 # Create a writable query dict.
160 new_data = QueryDict('').copy()161 new_data = QueryDict('').copy()
162 # If the fields are a dict of django Fields see if one is a DictCharField.
163 # DictCharField must have their values prefixed with the DictField name in
164 # the returned data or defaults don't get carried.
165 if isinstance(fields, dict):
166 acceptable_fields = []
167 for field_name, field in fields.items():
168 acceptable_fields.append(field_name)
169 if isinstance(field, DictCharField):
170 for sub_field in field.names:
171 acceptable_fields.append("%s_%s" % (field_name, sub_field))
172 else:
173 acceptable_fields = fields
161 # Missing fields will be taken from the node's current values. This174 # Missing fields will be taken from the node's current values. This
162 # is to circumvent Django's ModelForm (form created from a model)175 # is to circumvent Django's ModelForm (form created from a model)
163 # default behaviour that requires all the fields to be defined.176 # default behaviour that requires all the fields to be defined.
164 for k, v in defaults.items():177 for k, v in defaults.items():
165 if k in fields:178 if k in acceptable_fields:
166 new_data.setlist(k, listify(v))179 new_data.setlist(k, listify(v))
167 # We can't use update here because data is a QueryDict and 'update'180 # We can't use update here because data is a QueryDict and 'update'
168 # does not replaces the old values with the new as one would expect.181 # does not replaces the old values with the new as one would expect.
169182
=== modified file 'src/maasserver/clusterrpc/power_parameters.py'
--- src/maasserver/clusterrpc/power_parameters.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/clusterrpc/power_parameters.py 2016-10-20 20:55:55 +0000
@@ -65,12 +65,16 @@
65 json_field['name'], json_field['choices'])65 json_field['name'], json_field['choices'])
66 extra_parameters = {66 extra_parameters = {
67 'choices': json_field['choices'],67 'choices': json_field['choices'],
68 'initial': json_field['default'],
69 'error_messages': {68 'error_messages': {
70 'invalid_choice': invalid_choice_message},69 'invalid_choice': invalid_choice_message},
71 }70 }
72 else:71 else:
73 extra_parameters = {}72 extra_parameters = {}
73
74 default = json_field.get('default')
75 if default is not None:
76 extra_parameters['initial'] = default
77
74 form_field = field_class(78 form_field = field_class(
75 label=json_field['label'], required=json_field['required'],79 label=json_field['label'], required=json_field['required'],
76 **extra_parameters)80 **extra_parameters)
@@ -111,13 +115,20 @@
111 'missing_packages': missing_packages})115 'missing_packages': missing_packages})
112116
113117
114def get_power_type_parameters_from_json(json_power_type_parameters):118def get_power_type_parameters_from_json(
119 json_power_type_parameters, initial_power_params=None,
120 skip_check=False):
115 """Return power type parameters.121 """Return power type parameters.
116122
117 :param json_power_type_parameters: Power type parameters expressed123 :param json_power_type_parameters: Power type parameters expressed
118 as a JSON string or as set of JSONSchema-verifiable objects.124 as a JSON string or as set of JSONSchema-verifiable objects.
119 Will be validated using jsonschema.validate().125 Will be validated using jsonschema.validate().
120 :type json_power_type_parameters: JSON string or iterable.126 :type json_power_type_parameters: JSON string or iterable.
127 :param initial_power_params: Power paramaters that were already set, any
128 field which matches will have its initial value set.
129 :type initial_power_params: dict
130 :param skip_check: Whether the field should be checked or not.
131 :type skip_check: bool
121 :return: A dict of power parameters for all power types, indexed by132 :return: A dict of power parameters for all power types, indexed by
122 power type name.133 power type name.
123 """134 """
@@ -127,19 +138,27 @@
127 '': DictCharField(138 '': DictCharField(
128 [], required=False, skip_check=True),139 [], required=False, skip_check=True),
129 }140 }
141 if initial_power_params is None:
142 initial_power_params = []
130 for power_type in json_power_type_parameters:143 for power_type in json_power_type_parameters:
131 fields = []144 fields = []
145 has_required_field = False
132 for json_field in power_type['fields']:146 for json_field in power_type['fields']:
147 field_name = json_field['name']
148 if field_name in initial_power_params:
149 json_field['default'] = initial_power_params[field_name]
150 has_required_field = has_required_field or json_field['required']
133 fields.append((151 fields.append((
134 json_field['name'], make_form_field(json_field)))152 json_field['name'], make_form_field(json_field)))
135 params = DictCharField(fields, required=False, skip_check=True)153 params = DictCharField(
154 fields, required=has_required_field, skip_check=skip_check)
136 power_parameters[power_type['name']] = params155 power_parameters[power_type['name']] = params
137 return power_parameters156 return power_parameters
138157
139158
140def get_power_type_parameters():159def get_power_type_parameters(initial_power_params=None, skip_check=False):
141 return get_power_type_parameters_from_json(160 return get_power_type_parameters_from_json(
142 get_all_power_types_from_clusters())161 get_all_power_types_from_clusters(), initial_power_params, skip_check)
143162
144163
145def get_power_type_choices():164def get_power_type_choices():
146165
=== modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py'
--- src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-10-20 20:55:55 +0000
@@ -20,6 +20,7 @@
20)20)
21from maasserver.config_forms import DictCharField21from maasserver.config_forms import DictCharField
22from maasserver.fields import MACAddressFormField22from maasserver.fields import MACAddressFormField
23from maasserver.testing.factory import factory
23from maasserver.testing.testcase import MAASServerTestCase24from maasserver.testing.testcase import MAASServerTestCase
24from maasserver.utils.forms import compose_invalid_choice_text25from maasserver.utils.forms import compose_invalid_choice_text
25from maastesting.matchers import MockCalledOnceWith26from maastesting.matchers import MockCalledOnceWith
@@ -70,6 +71,42 @@
70 for name, field in power_type_parameters.items():71 for name, field in power_type_parameters.items():
71 self.assertIsInstance(field, DictCharField)72 self.assertIsInstance(field, DictCharField)
7273
74 def test__overrides_defaults(self):
75 name = factory.make_name('name')
76 field_name = factory.make_name('field_name')
77 new_default = factory.make_name('new default')
78 json_parameters = [{
79 'name': name,
80 'description': factory.make_name('description'),
81 'fields': [{
82 'name': field_name,
83 'label': factory.make_name('field label'),
84 'field_type': factory.make_name('field type'),
85 'default': factory.make_name('field default'),
86 'required': False,
87 }],
88 }]
89 power_type_parameters = get_power_type_parameters_from_json(
90 json_parameters, {field_name: new_default})
91 self.assertEqual(
92 new_default, power_type_parameters[name].fields[0].initial)
93
94 def test__manual_does_not_require_power_params(self):
95 json_parameters = [{
96 'name': 'manual',
97 'description': factory.make_name('description'),
98 'fields': [{
99 'name': factory.make_name('field name'),
100 'label': factory.make_name('field label'),
101 'field_type': factory.make_name('field type'),
102 'default': factory.make_name('field default'),
103 'required': False,
104 }],
105 }]
106 power_type_parameters = get_power_type_parameters_from_json(
107 json_parameters)
108 self.assertFalse(power_type_parameters['manual'].required)
109
73110
74class TestMakeFormField(MAASServerTestCase):111class TestMakeFormField(MAASServerTestCase):
75 """Test that make_form_field() converts JSON fields to Django."""112 """Test that make_form_field() converts JSON fields to Django."""
@@ -137,6 +174,17 @@
137 (json_field['label'], json_field['required']),174 (json_field['label'], json_field['required']),
138 (django_field.label, django_field.required))175 (django_field.label, django_field.required))
139176
177 def test__sets_initial_to_default(self):
178 json_field = {
179 'name': 'some_field',
180 'label': 'Some Field',
181 'field_type': 'string',
182 'required': False,
183 'default': 'some default',
184 }
185 django_field = make_form_field(json_field)
186 self.assertEquals(json_field['default'], django_field.initial)
187
140188
141class TestMakeJSONField(MAASServerTestCase):189class TestMakeJSONField(MAASServerTestCase):
142 """Test that make_json_field() creates JSON-verifiable fields."""190 """Test that make_json_field() creates JSON-verifiable fields."""
143191
=== modified file 'src/maasserver/config_forms.py'
--- src/maasserver/config_forms.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/config_forms.py 2016-10-20 20:55:55 +0000
@@ -186,6 +186,9 @@
186 field_value = value[index]186 field_value = value[index]
187 except IndexError:187 except IndexError:
188 field_value = None188 field_value = None
189 # Set the field_value to the default value if not set.
190 if field_value is None and field.initial not in (None, ''):
191 field_value = field.initial
189 # Check the field's 'required' field instead of the global192 # Check the field's 'required' field instead of the global
190 # 'required' field to allow subfields to be required or not.193 # 'required' field to allow subfields to be required or not.
191 if field.required and field_value in validators.EMPTY_VALUES:194 if field.required and field_value in validators.EMPTY_VALUES:
192195
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2016-10-17 20:09:56 +0000
+++ src/maasserver/forms.py 2016-10-20 20:55:55 +0000
@@ -36,6 +36,7 @@
3636
37from collections import Counter37from collections import Counter
38from functools import partial38from functools import partial
39import json
39import pipes40import pipes
40import re41import re
4142
@@ -241,6 +242,27 @@
241 return power_type242 return power_type
242243
243 @staticmethod244 @staticmethod
245 def _get_power_parameters(form, data, machine):
246 if data is None:
247 data = {}
248
249 power_parameters = data.get(
250 'power_parameters', form.initial.get('power_parameters', {}))
251
252 if isinstance(power_parameters, str):
253 try:
254 power_parameters = json.loads(power_parameters)
255 except json.JSONDecodeError:
256 raise ValidationError("Failed to parse JSON power_parameters")
257
258 # Integrate the machines existing power_parameters if unset by form.
259 if machine:
260 for key, value in machine.power_parameters.items():
261 if power_parameters.get(key) is None:
262 power_parameters[key] = value
263 return power_parameters
264
265 @staticmethod
244 def set_up_power_type(form, data, machine=None):266 def set_up_power_type(form, data, machine=None):
245 """Set up the 'power_type' and 'power_parameters' fields.267 """Set up the 'power_type' and 'power_parameters' fields.
246268
@@ -251,10 +273,18 @@
251 choices = [BLANK_CHOICE] + get_power_type_choices()273 choices = [BLANK_CHOICE] + get_power_type_choices()
252 form.fields['power_type'] = forms.ChoiceField(274 form.fields['power_type'] = forms.ChoiceField(
253 required=False, choices=choices, initial=power_type)275 required=False, choices=choices, initial=power_type)
254 form.fields['power_parameters'] = get_power_type_parameters()[276 power_parameters = WithPowerMixin._get_power_parameters(
255 power_type]277 form, data, machine)
256 if form.instance is not None and form.instance.power_type != '':278 skip_check = (
257 form.initial['power_type'] = form.instance.power_type279 form.data.get('power_parameters_%s' % SKIP_CHECK_NAME) == 'true')
280 form.fields['power_parameters'] = get_power_type_parameters(
281 power_parameters, skip_check=skip_check)[power_type]
282 if form.instance is not None:
283 if form.instance.power_type != '':
284 form.initial['power_type'] = form.instance.power_type
285 if form.instance.power_parameters != '':
286 for key, value in power_parameters.items():
287 form.initial['power_parameters_%s' % key] = value
258288
259 @staticmethod289 @staticmethod
260 def check_power_type(form, cleaned_data):290 def check_power_type(form, cleaned_data):
@@ -277,8 +307,9 @@
277307
278 # If power_type is not set and power_parameters_skip_check is not308 # If power_type is not set and power_parameters_skip_check is not
279 # on, reset power_parameters (set it to the empty string).309 # on, reset power_parameters (set it to the empty string).
280 if cleaned_data.get('power_type', '') == '':310 power_type = cleaned_data.get('power_type', '')
281 cleaned_data['power_parameters'] = ''311 if power_type == '':
312 cleaned_data['power_parameters'] = {}
282 return cleaned_data313 return cleaned_data
283314
284 @staticmethod315 @staticmethod
285316
=== modified file 'src/maasserver/rpc/nodes.py'
--- src/maasserver/rpc/nodes.py 2016-10-03 20:36:58 +0000
+++ src/maasserver/rpc/nodes.py 2016-10-20 20:55:55 +0000
@@ -225,10 +225,6 @@
225 form = AdminMachineWithMACAddressesForm(data_query_dict)225 form = AdminMachineWithMACAddressesForm(data_query_dict)
226 if form.is_valid():226 if form.is_valid():
227 node = form.save()227 node = form.save()
228 # We have to explicitly save the power parameters; the form
229 # won't do it for us.
230 node.power_parameters = json.loads(power_parameters)
231 node.save()
232 return node228 return node
233 else:229 else:
234 raise ValidationError(form.errors)230 raise ValidationError(form.errors)
235231
=== modified file 'src/maasserver/rpc/tests/test_nodes.py'
--- src/maasserver/rpc/tests/test_nodes.py 2016-10-03 20:36:58 +0000
+++ src/maasserver/rpc/tests/test_nodes.py 2016-10-20 20:55:55 +0000
@@ -7,7 +7,6 @@
77
8from datetime import timedelta8from datetime import timedelta
9import json9import json
10from json import dumps
11from operator import attrgetter10from operator import attrgetter
12import random11import random
13from random import randint12from random import randint
@@ -88,17 +87,13 @@
88 mac_addresses = [87 mac_addresses = [
89 factory.make_mac_address() for _ in range(3)]88 factory.make_mac_address() for _ in range(3)]
90 architecture = make_usable_architecture(self)89 architecture = make_usable_architecture(self)
91 power_type = random.choice(self.power_types)['name']
92 power_parameters = dumps({})
9390
94 node = create_node(91 node = create_node(architecture, 'manual', {}, mac_addresses)
95 architecture, power_type, power_parameters,
96 mac_addresses)
9792
98 self.assertEqual(93 self.assertEqual(
99 (94 (
100 architecture,95 architecture,
101 power_type,96 'manual',
102 {},97 {},
103 ),98 ),
104 (99 (
@@ -122,17 +117,14 @@
122 factory.make_mac_address() for _ in range(3)]117 factory.make_mac_address() for _ in range(3)]
123 architecture = make_usable_architecture(self)118 architecture = make_usable_architecture(self)
124 hostname = factory.make_hostname()119 hostname = factory.make_hostname()
125 power_type = random.choice(self.power_types)['name']
126 power_parameters = dumps({})
127120
128 node = create_node(121 node = create_node(
129 architecture, power_type, power_parameters,122 architecture, 'manual', {}, mac_addresses, hostname=hostname)
130 mac_addresses, hostname=hostname)
131123
132 self.assertEqual(124 self.assertEqual(
133 (125 (
134 architecture,126 architecture,
135 power_type,127 'manual',
136 {},128 {},
137 hostname129 hostname
138 ),130 ),
@@ -158,7 +150,7 @@
158 "Microsoft Windows",150 "Microsoft Windows",
159 ])151 ])
160 power_type = random.choice(self.power_types)['name']152 power_type = random.choice(self.power_types)['name']
161 power_parameters = dumps({})153 power_parameters = {}
162154
163 with ExpectedException(ValidationError):155 with ExpectedException(ValidationError):
164 create_node(156 create_node(
@@ -173,17 +165,15 @@
173 architecture = make_usable_architecture(self)165 architecture = make_usable_architecture(self)
174 hostname = factory.make_hostname()166 hostname = factory.make_hostname()
175 domain = factory.make_Domain()167 domain = factory.make_Domain()
176 power_type = random.choice(self.power_types)['name']
177 power_parameters = dumps({})
178168
179 node = create_node(169 node = create_node(
180 architecture, power_type, power_parameters,170 architecture, 'manual', {}, mac_addresses, domain=domain.name,
181 mac_addresses, domain=domain.name, hostname=hostname)171 hostname=hostname)
182172
183 self.assertEqual(173 self.assertEqual(
184 (174 (
185 architecture,175 architecture,
186 power_type,176 'manual',
187 {},177 {},
188 domain.id,178 domain.id,
189 hostname,179 hostname,
@@ -207,7 +197,7 @@
207 factory.make_mac_address() for _ in range(3)]197 factory.make_mac_address() for _ in range(3)]
208 architecture = make_usable_architecture(self)198 architecture = make_usable_architecture(self)
209 power_type = random.choice(self.power_types)['name']199 power_type = random.choice(self.power_types)['name']
210 power_parameters = dumps({})200 power_parameters = {}
211201
212 with ExpectedException(ValidationError):202 with ExpectedException(ValidationError):
213 create_node(203 create_node(
@@ -220,7 +210,7 @@
220 self.assertRaises(210 self.assertRaises(
221 ValidationError, create_node,211 ValidationError, create_node,
222 architecture="spam/eggs", power_type="scrambled",212 architecture="spam/eggs", power_type="scrambled",
223 power_parameters=dumps({}),213 power_parameters={},
224 mac_addresses=[factory.make_mac_address()])214 mac_addresses=[factory.make_mac_address()])
225215
226 def test__raises_error_if_node_already_exists(self):216 def test__raises_error_if_node_already_exists(self):
@@ -229,8 +219,8 @@
229 mac_addresses = [219 mac_addresses = [
230 factory.make_mac_address() for _ in range(3)]220 factory.make_mac_address() for _ in range(3)]
231 architecture = make_usable_architecture(self)221 architecture = make_usable_architecture(self)
232 power_type = random.choice(self.power_types)['name']222 power_type = 'manual'
233 power_parameters = dumps({})223 power_parameters = {}
234224
235 create_node(225 create_node(
236 architecture, power_type, power_parameters,226 architecture, power_type, power_parameters,
@@ -245,20 +235,20 @@
245 mac_addresses = [235 mac_addresses = [
246 factory.make_mac_address() for _ in range(3)]236 factory.make_mac_address() for _ in range(3)]
247 architecture = make_usable_architecture(self)237 architecture = make_usable_architecture(self)
248 power_type = random.choice(self.power_types)['name']
249 power_parameters = {238 power_parameters = {
250 factory.make_name('key'): factory.make_name('value')239 'power_address': factory.make_url(),
251 for _ in range(3)240 'power_pass': factory.make_name('power_pass'),
241 'power_id': factory.make_name('power_id'),
252 }242 }
253243
254 node = create_node(244 node = create_node(
255 architecture, power_type, dumps(power_parameters),245 architecture, 'virsh', power_parameters,
256 mac_addresses)246 mac_addresses)
257247
258 # Reload the object from the DB so that we're sure its power248 # Reload the object from the DB so that we're sure its power
259 # parameters are being persisted.249 # parameters are being persisted.
260 node = reload_object(node)250 node = reload_object(node)
261 self.assertEqual(power_parameters, node.power_parameters)251 self.assertItemsEqual(power_parameters, node.power_parameters)
262252
263 def test__forces_generic_subarchitecture_if_missing(self):253 def test__forces_generic_subarchitecture_if_missing(self):
264 self.prepare_rack_rpc()254 self.prepare_rack_rpc()
@@ -266,13 +256,9 @@
266 mac_addresses = [256 mac_addresses = [
267 factory.make_mac_address() for _ in range(3)]257 factory.make_mac_address() for _ in range(3)]
268 architecture = make_usable_architecture(self, subarch_name='generic')258 architecture = make_usable_architecture(self, subarch_name='generic')
269 power_type = random.choice(self.power_types)['name']
270 power_parameters = dumps({})
271259
272 arch, subarch = architecture.split('/')260 arch, subarch = architecture.split('/')
273 node = create_node(261 node = create_node(arch, 'manual', {}, mac_addresses)
274 arch, power_type, power_parameters,
275 mac_addresses)
276262
277 self.assertEqual(architecture, node.architecture)263 self.assertEqual(architecture, node.architecture)
278264
279265
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-10-18 08:00:37 +0000
+++ src/maasserver/testing/factory.py 2016-10-20 20:55:55 +0000
@@ -381,8 +381,6 @@
381 zone = self.make_Zone()381 zone = self.make_Zone()
382 if power_type is None:382 if power_type is None:
383 power_type = 'virsh'383 power_type = 'virsh'
384 if power_parameters is None:
385 power_parameters = {}
386 if power_state is None:384 if power_state is None:
387 power_state = self.pick_enum(POWER_STATE)385 power_state = self.pick_enum(POWER_STATE)
388 if power_state_updated is undefined:386 if power_state_updated is undefined:
@@ -443,7 +441,8 @@
443 bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet)441 bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet)
444 node.power_parameters = {442 node.power_parameters = {
445 "power_address": "qemu+ssh://user@%s/system" % (443 "power_address": "qemu+ssh://user@%s/system" % (
446 factory.ip_to_url_format(bmc_ip_address))444 factory.ip_to_url_format(bmc_ip_address)),
445 "power_id": factory.make_name("power_id"),
447 }446 }
448 node.save()447 node.save()
449448
450449
=== modified file 'src/maasserver/tests/test_config_forms.py'
--- src/maasserver/tests/test_config_forms.py 2016-06-22 17:03:02 +0000
+++ src/maasserver/tests/test_config_forms.py 2016-10-20 20:55:55 +0000
@@ -165,6 +165,15 @@
165 {'char_field': char_value, 'multi_field': None},165 {'char_field': char_value, 'multi_field': None},
166 form.cleaned_data)166 form.cleaned_data)
167167
168 def test_DictCharField_sets_default_value_for_subfields(self):
169 default_value = factory.make_name('default_value')
170 multi_field = DictCharField(
171 [('field_a', forms.CharField(
172 label='Field a', initial=default_value))],
173 required=False)
174 self.assertEquals(
175 default_value, multi_field.clean_sub_fields('')['field_a'])
176
168177
169class TestUtilities(MAASServerTestCase):178class TestUtilities(MAASServerTestCase):
170179
171180
=== modified file 'src/maasserver/tests/test_forms_controller.py'
--- src/maasserver/tests/test_forms_controller.py 2016-04-13 02:20:16 +0000
+++ src/maasserver/tests/test_forms_controller.py 2016-10-20 20:55:55 +0000
@@ -40,6 +40,7 @@
40 form = ControllerForm(40 form = ControllerForm(
41 data={41 data={
42 'power_type': power_type,42 'power_type': power_type,
43 'power_parameters_skip_check': 'true',
43 },44 },
44 instance=rack)45 instance=rack)
45 rack = form.save()46 rack = form.save()
@@ -51,12 +52,12 @@
51 form = ControllerForm(52 form = ControllerForm(
52 data={53 data={
53 'power_parameters_field': power_parameters_field,54 'power_parameters_field': power_parameters_field,
54 'power_parameters_skip_check': True,55 'power_parameters_skip_check': 'true',
55 },56 },
56 instance=rack)57 instance=rack)
57 rack = form.save()58 rack = form.save()
58 self.assertEqual(59 self.assertEqual(
59 {'field': power_parameters_field}, rack.power_parameters)60 power_parameters_field, rack.power_parameters['field'])
6061
61 def test__sets_zone(self):62 def test__sets_zone(self):
62 rack = factory.make_RackController()63 rack = factory.make_RackController()
@@ -64,6 +65,7 @@
64 form = ControllerForm(65 form = ControllerForm(
65 data={66 data={
66 'zone': zone.name,67 'zone': zone.name,
68 'power_parameters_skip_check': 'true',
67 },69 },
68 instance=rack)70 instance=rack)
69 rack = form.save()71 rack = form.save()
7072
=== modified file 'src/maasserver/tests/test_forms_machine.py'
--- src/maasserver/tests/test_forms_machine.py 2016-09-22 02:53:33 +0000
+++ src/maasserver/tests/test_forms_machine.py 2016-10-20 20:55:55 +0000
@@ -373,7 +373,7 @@
373 'architecture': arch,373 'architecture': arch,
374 'power_type': power_type,374 'power_type': power_type,
375 'power_parameters_field': power_parameters_field,375 'power_parameters_field': power_parameters_field,
376 'power_parameters_skip_check': True,376 'power_parameters_skip_check': 'true',
377 },377 },
378 instance=node)378 instance=node)
379 form.save()379 form.save()
@@ -393,6 +393,7 @@
393 data={393 data={
394 'hostname': hostname,394 'hostname': hostname,
395 'architecture': arch,395 'architecture': arch,
396 'power_parameters_skip_check': 'true',
396 },397 },
397 instance=node)398 instance=node)
398 node = form.save()399 node = form.save()
@@ -407,6 +408,7 @@
407 data={408 data={
408 'hostname': hostname,409 'hostname': hostname,
409 'architecture': arch,410 'architecture': arch,
411 'power_parameters_skip_check': 'true',
410 },412 },
411 instance=node)413 instance=node)
412 node = form.save()414 node = form.save()
@@ -422,6 +424,7 @@
422 'hostname': hostname,424 'hostname': hostname,
423 'architecture': arch,425 'architecture': arch,
424 'power_type': power_type,426 'power_type': power_type,
427 'power_parameters_skip_check': 'true',
425 },428 },
426 instance=node)429 instance=node)
427 node = form.save()430 node = form.save()
428431
=== modified file 'src/maasserver/websockets/handlers/machine.py'
--- src/maasserver/websockets/handlers/machine.py 2016-10-17 20:09:56 +0000
+++ src/maasserver/websockets/handlers/machine.py 2016-10-20 20:55:55 +0000
@@ -237,6 +237,7 @@
237 new_params["hostname"] = params.get("hostname")237 new_params["hostname"] = params.get("hostname")
238 new_params["architecture"] = params.get("architecture")238 new_params["architecture"] = params.get("architecture")
239 new_params["power_type"] = params.get("power_type")239 new_params["power_type"] = params.get("power_type")
240 new_params["power_parameters"] = params.get("power_parameters")
240 if "zone" in params:241 if "zone" in params:
241 new_params["zone"] = params["zone"]["name"]242 new_params["zone"] = params["zone"]["name"]
242 if "domain" in params:243 if "domain" in params:
@@ -259,13 +260,8 @@
259 if not reload_object(self.user).is_superuser:260 if not reload_object(self.user).is_superuser:
260 raise HandlerPermissionError()261 raise HandlerPermissionError()
261262
262 # Create the object, then save the power parameters because the
263 # form will not save this information.
264 data = super(NodeHandler, self).create(params)263 data = super(NodeHandler, self).create(params)
265 node_obj = Node.objects.get(system_id=data['system_id'])264 node_obj = Node.objects.get(system_id=data['system_id'])
266 node_obj.power_type = params.get("power_type", '')
267 node_obj.power_parameters = params.get("power_parameters", {})
268 node_obj.save()
269265
270 # Start the commissioning process right away, which has the266 # Start the commissioning process right away, which has the
271 # desired side effect of initializing the node's power state.267 # desired side effect of initializing the node's power state.
@@ -279,16 +275,13 @@
279 if not reload_object(self.user).is_superuser:275 if not reload_object(self.user).is_superuser:
280 raise HandlerPermissionError()276 raise HandlerPermissionError()
281277
282 # Update the node with the form. The form will not update the
283 # power_type or power_parameters, so we perform that here.
284 data = super(NodeHandler, self).update(params)278 data = super(NodeHandler, self).update(params)
285 node_obj = Node.objects.get(system_id=data['system_id'])279 node_obj = Node.objects.get(system_id=data['system_id'])
286 node_obj.power_type = params.get("power_type", '')
287 node_obj.power_parameters = params.get("power_parameters", {})
288280
289 # Update the tags for the node and disks.281 # Update the tags for the node and disks.
290 self.update_tags(node_obj, params['tags'])282 self.update_tags(node_obj, params['tags'])
291 node_obj.save()283 node_obj.save()
284
292 return self.full_dehydrate(node_obj)285 return self.full_dehydrate(node_obj)
293286
294 def mount_special(self, params):287 def mount_special(self, params):
295288
=== modified file 'src/maasserver/websockets/handlers/tests/test_machine.py'
--- src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-17 20:09:56 +0000
+++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-20 20:55:55 +0000
@@ -1145,7 +1145,7 @@
1145 def test_update_raises_validation_error_for_invalid_architecture(self):1145 def test_update_raises_validation_error_for_invalid_architecture(self):
1146 user = factory.make_admin()1146 user = factory.make_admin()
1147 handler = MachineHandler(user, {})1147 handler = MachineHandler(user, {})
1148 node = factory.make_Node(interface=True)1148 node = factory.make_Node(interface=True, power_type='manual')
1149 node_data = self.dehydrate_node(node, handler)1149 node_data = self.dehydrate_node(node, handler)
1150 arch = factory.make_name("arch")1150 arch = factory.make_name("arch")
1151 node_data["architecture"] = arch1151 node_data["architecture"] = arch
@@ -1195,7 +1195,8 @@
1195 user = factory.make_admin()1195 user = factory.make_admin()
1196 handler = MachineHandler(user, {})1196 handler = MachineHandler(user, {})
1197 architecture = make_usable_architecture(self)1197 architecture = make_usable_architecture(self)
1198 node = factory.make_Node(interface=True, architecture=architecture)1198 node = factory.make_Node(
1199 interface=True, architecture=architecture, power_type='manual')
1199 tags = [1200 tags = [
1200 factory.make_Tag(definition='').name1201 factory.make_Tag(definition='').name
1201 for _ in range(3)1202 for _ in range(3)
@@ -1209,7 +1210,8 @@
1209 user = factory.make_admin()1210 user = factory.make_admin()
1210 handler = MachineHandler(user, {})1211 handler = MachineHandler(user, {})
1211 architecture = make_usable_architecture(self)1212 architecture = make_usable_architecture(self)
1212 node = factory.make_Node(interface=True, architecture=architecture)1213 node = factory.make_Node(
1214 interface=True, architecture=architecture, power_type='manual')
1213 tags = []1215 tags = []
1214 for _ in range(3):1216 for _ in range(3):
1215 tag = factory.make_Tag(definition='')1217 tag = factory.make_Tag(definition='')
@@ -1226,7 +1228,8 @@
1226 user = factory.make_admin()1228 user = factory.make_admin()
1227 handler = MachineHandler(user, {})1229 handler = MachineHandler(user, {})
1228 architecture = make_usable_architecture(self)1230 architecture = make_usable_architecture(self)
1229 node = factory.make_Node(interface=True, architecture=architecture)1231 node = factory.make_Node(
1232 interface=True, architecture=architecture, power_type='manual')
1230 tag_name = factory.make_name("tag")1233 tag_name = factory.make_name("tag")
1231 node_data = self.dehydrate_node(node, handler)1234 node_data = self.dehydrate_node(node, handler)
1232 node_data["tags"].append(tag_name)1235 node_data["tags"].append(tag_name)
12331236
=== modified file 'src/maasserver/websockets/tests/test_base.py'
--- src/maasserver/websockets/tests/test_base.py 2016-09-27 14:24:19 +0000
+++ src/maasserver/websockets/tests/test_base.py 2016-10-20 20:55:55 +0000
@@ -523,7 +523,7 @@
523523
524 def test_update_with_form_updates_node(self):524 def test_update_with_form_updates_node(self):
525 arch = make_usable_architecture(self)525 arch = make_usable_architecture(self)
526 node = factory.make_Node(architecture=arch)526 node = factory.make_Node(architecture=arch, power_type='manual')
527 hostname = factory.make_name("hostname")527 hostname = factory.make_name("hostname")
528 handler = self.make_nodes_handler(528 handler = self.make_nodes_handler(
529 fields=['hostname'], form=AdminMachineForm)529 fields=['hostname'], form=AdminMachineForm)
@@ -539,7 +539,7 @@
539539
540 def test_update_with_form_uses_form_from_get_form_class(self):540 def test_update_with_form_uses_form_from_get_form_class(self):
541 arch = make_usable_architecture(self)541 arch = make_usable_architecture(self)
542 node = factory.make_Node(architecture=arch)542 node = factory.make_Node(architecture=arch, power_type='manual')
543 hostname = factory.make_name("hostname")543 hostname = factory.make_name("hostname")
544 handler = self.make_nodes_handler(fields=['hostname'])544 handler = self.make_nodes_handler(fields=['hostname'])
545 self.patch(545 self.patch(
546546
=== modified file 'src/provisioningserver/power/schema.py'
--- src/provisioningserver/power/schema.py 2016-09-27 18:38:53 +0000
+++ src/provisioningserver/power/schema.py 2016-10-20 20:55:55 +0000
@@ -238,9 +238,10 @@
238 'name': 'virsh',238 'name': 'virsh',
239 'description': 'Virsh (virtual systems)',239 'description': 'Virsh (virtual systems)',
240 'fields': [240 'fields': [
241 make_json_field('power_address', "Power address"),241 make_json_field('power_address', "Power address", required=True),
242 make_json_field(242 make_json_field(
243 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE),243 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE,
244 required=True),
244 make_json_field(245 make_json_field(
245 'power_pass', "Power password (optional)",246 'power_pass', "Power password (optional)",
246 required=False, field_type='password'),247 required=False, field_type='password'),
@@ -258,10 +259,11 @@
258 make_json_field(259 make_json_field(
259 'power_uuid', "VM UUID (if known)", required=False,260 'power_uuid', "VM UUID (if known)", required=False,
260 scope=POWER_PARAMETER_SCOPE.NODE),261 scope=POWER_PARAMETER_SCOPE.NODE),
261 make_json_field('power_address', "VMware hostname"),262 make_json_field('power_address', "VMware hostname", required=True),
262 make_json_field('power_user', "VMware username"),263 make_json_field('power_user', "VMware username", required=True),
263 make_json_field(264 make_json_field(
264 'power_pass', "VMware password", field_type='password'),265 'power_pass', "VMware password", field_type='password',
266 required=True),
265 make_json_field(267 make_json_field(
266 'power_port', "VMware API port (optional)", required=False),268 'power_port', "VMware API port (optional)", required=False),
267 make_json_field(269 make_json_field(
@@ -273,9 +275,10 @@
273 'name': 'fence_cdu',275 'name': 'fence_cdu',
274 'description': 'Sentry Switch CDU',276 'description': 'Sentry Switch CDU',
275 'fields': [277 'fields': [
276 make_json_field('power_address', "Power address"),278 make_json_field('power_address', "Power address", required=True),
277 make_json_field(279 make_json_field(
278 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE),280 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE,
281 required=True),
279 make_json_field('power_user', "Power user"),282 make_json_field('power_user', "Power user"),
280 make_json_field(283 make_json_field(
281 'power_pass', "Power password", field_type='password'),284 'power_pass', "Power password", field_type='password'),
@@ -288,8 +291,9 @@
288 'fields': [291 'fields': [
289 make_json_field(292 make_json_field(
290 'power_driver', "Power driver", field_type='choice',293 'power_driver', "Power driver", field_type='choice',
291 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0),294 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0,
292 make_json_field('power_address', "IP address"),295 required=True),
296 make_json_field('power_address', "IP address", required=True),
293 make_json_field('power_user', "Power user"),297 make_json_field('power_user', "Power user"),
294 make_json_field(298 make_json_field(
295 'power_pass', "Power password", field_type='password'),299 'power_pass', "Power password", field_type='password'),
@@ -302,13 +306,13 @@
302 'name': 'moonshot',306 'name': 'moonshot',
303 'description': 'HP Moonshot - iLO4 (IPMI)',307 'description': 'HP Moonshot - iLO4 (IPMI)',
304 'fields': [308 'fields': [
305 make_json_field('power_address', "Power address"),309 make_json_field('power_address', "Power address", required=True),
306 make_json_field('power_user', "Power user"),310 make_json_field('power_user', "Power user"),
307 make_json_field(311 make_json_field(
308 'power_pass', "Power password", field_type='password'),312 'power_pass', "Power password", field_type='password'),
309 make_json_field(313 make_json_field(
310 'power_hwaddress', "Power hardware address",314 'power_hwaddress', "Power hardware address",
311 scope=POWER_PARAMETER_SCOPE.NODE),315 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
312 ],316 ],
313 'ip_extractor': make_ip_extractor('power_address'),317 'ip_extractor': make_ip_extractor('power_address'),
314 },318 },
@@ -317,14 +321,16 @@
317 'description': 'SeaMicro 15000',321 'description': 'SeaMicro 15000',
318 'fields': [322 'fields': [
319 make_json_field(323 make_json_field(
320 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE),324 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE,
321 make_json_field('power_address', "Power address"),325 required=True),
326 make_json_field('power_address', "Power address", required=True),
322 make_json_field('power_user', "Power user"),327 make_json_field('power_user', "Power user"),
323 make_json_field(328 make_json_field(
324 'power_pass', "Power password", field_type='password'),329 'power_pass', "Power password", field_type='password'),
325 make_json_field(330 make_json_field(
326 'power_control', "Power control type", field_type='choice',331 'power_control', "Power control type", field_type='choice',
327 choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi'),332 choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi',
333 required=True),
328 ],334 ],
329 'ip_extractor': make_ip_extractor('power_address'),335 'ip_extractor': make_ip_extractor('power_address'),
330 },336 },
@@ -334,7 +340,7 @@
334 'fields': [340 'fields': [
335 make_json_field(341 make_json_field(
336 'power_pass', "Power password", field_type='password'),342 'power_pass', "Power password", field_type='password'),
337 make_json_field('power_address', "Power address")343 make_json_field('power_address', "Power address", required=True)
338 ],344 ],
339 'ip_extractor': make_ip_extractor('power_address'),345 'ip_extractor': make_ip_extractor('power_address'),
340 },346 },
@@ -343,8 +349,9 @@
343 'description': 'Digital Loggers, Inc. PDU',349 'description': 'Digital Loggers, Inc. PDU',
344 'fields': [350 'fields': [
345 make_json_field(351 make_json_field(
346 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE),352 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE,
347 make_json_field('power_address', "Power address"),353 required=True),
354 make_json_field('power_address', "Power address", required=True),
348 make_json_field('power_user', "Power user"),355 make_json_field('power_user', "Power user"),
349 make_json_field(356 make_json_field(
350 'power_pass', "Power password", field_type='password'),357 'power_pass', "Power password", field_type='password'),
@@ -355,7 +362,7 @@
355 'name': 'wedge',362 'name': 'wedge',
356 'description': "Facebook's Wedge",363 'description': "Facebook's Wedge",
357 'fields': [364 'fields': [
358 make_json_field('power_address', "IP address"),365 make_json_field('power_address', "IP address", required=True),
359 make_json_field('power_user', "Power user"),366 make_json_field('power_user', "Power user"),
360 make_json_field(367 make_json_field(
361 'power_pass', "Power password", field_type='password'),368 'power_pass', "Power password", field_type='password'),
@@ -367,8 +374,9 @@
367 'description': "Cisco UCS Manager",374 'description': "Cisco UCS Manager",
368 'fields': [375 'fields': [
369 make_json_field(376 make_json_field(
370 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE),377 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE,
371 make_json_field('power_address', "URL for XML API"),378 required=True),
379 make_json_field('power_address', "URL for XML API", required=True),
372 make_json_field('power_user', "API user"),380 make_json_field('power_user', "API user"),
373 make_json_field(381 make_json_field(
374 'power_pass', "API password", field_type='password'),382 'power_pass', "API password", field_type='password'),
@@ -380,7 +388,8 @@
380 'name': 'mscm',388 'name': 'mscm',
381 'description': "HP Moonshot - iLO Chassis Manager",389 'description': "HP Moonshot - iLO Chassis Manager",
382 'fields': [390 'fields': [
383 make_json_field('power_address', "IP for MSCM CLI API"),391 make_json_field(
392 'power_address', "IP for MSCM CLI API", required=True),
384 make_json_field('power_user', "MSCM CLI API user"),393 make_json_field('power_user', "MSCM CLI API user"),
385 make_json_field(394 make_json_field(
386 'power_pass', "MSCM CLI API password", field_type='password'),395 'power_pass', "MSCM CLI API password", field_type='password'),
@@ -388,7 +397,7 @@
388 'node_id',397 'node_id',
389 "Node ID - Must adhere to cXnY format "398 "Node ID - Must adhere to cXnY format "
390 "(X=cartridge number, Y=node number).",399 "(X=cartridge number, Y=node number).",
391 scope=POWER_PARAMETER_SCOPE.NODE),400 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
392 ],401 ],
393 'ip_extractor': make_ip_extractor('power_address'),402 'ip_extractor': make_ip_extractor('power_address'),
394 },403 },
@@ -396,14 +405,14 @@
396 'name': 'msftocs',405 'name': 'msftocs',
397 'description': "Microsoft OCS - Chassis Manager",406 'description': "Microsoft OCS - Chassis Manager",
398 'fields': [407 'fields': [
399 make_json_field('power_address', "Power address"),408 make_json_field('power_address', "Power address", required=True),
400 make_json_field('power_port', "Power port"),409 make_json_field('power_port', "Power port"),
401 make_json_field('power_user', "Power user"),410 make_json_field('power_user', "Power user"),
402 make_json_field(411 make_json_field(
403 'power_pass', "Power password", field_type='password'),412 'power_pass', "Power password", field_type='password'),
404 make_json_field(413 make_json_field(
405 'blade_id', "Blade ID (Typically 1-24)",414 'blade_id', "Blade ID (Typically 1-24)",
406 scope=POWER_PARAMETER_SCOPE.NODE),415 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
407 ],416 ],
408 'ip_extractor': make_ip_extractor('power_address'),417 'ip_extractor': make_ip_extractor('power_address'),
409 },418 },
@@ -411,10 +420,10 @@
411 'name': 'apc',420 'name': 'apc',
412 'description': "American Power Conversion (APC) PDU",421 'description': "American Power Conversion (APC) PDU",
413 'fields': [422 'fields': [
414 make_json_field('power_address', "IP for APC PDU"),423 make_json_field('power_address', "IP for APC PDU", required=True),
415 make_json_field(424 make_json_field(
416 'node_outlet', "APC PDU node outlet number (1-16)",425 'node_outlet', "APC PDU node outlet number (1-16)",
417 scope=POWER_PARAMETER_SCOPE.NODE),426 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
418 make_json_field(427 make_json_field(
419 'power_on_delay', "Power ON outlet delay (seconds)",428 'power_on_delay', "Power ON outlet delay (seconds)",
420 default='5'),429 default='5'),
@@ -425,16 +434,16 @@
425 'name': 'hmc',434 'name': 'hmc',
426 'description': "IBM Hardware Management Console (HMC)",435 'description': "IBM Hardware Management Console (HMC)",
427 'fields': [436 'fields': [
428 make_json_field('power_address', "IP for HMC"),437 make_json_field('power_address', "IP for HMC", required=True),
429 make_json_field('power_user', "HMC username"),438 make_json_field('power_user', "HMC username"),
430 make_json_field(439 make_json_field(
431 'power_pass', "HMC password", field_type='password'),440 'power_pass', "HMC password", field_type='password'),
432 make_json_field(441 make_json_field(
433 'server_name', "HMC Managed System server name",442 'server_name', "HMC Managed System server name",
434 scope=POWER_PARAMETER_SCOPE.NODE),443 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
435 make_json_field(444 make_json_field(
436 'lpar', "HMC logical partition",445 'lpar', "HMC logical partition",
437 scope=POWER_PARAMETER_SCOPE.NODE),446 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
438 ],447 ],
439 'ip_extractor': make_ip_extractor('power_address'),448 'ip_extractor': make_ip_extractor('power_address'),
440 },449 },
@@ -442,11 +451,13 @@
442 'name': 'nova',451 'name': 'nova',
443 'description': 'OpenStack Nova',452 'description': 'OpenStack Nova',
444 'fields': [453 'fields': [
445 make_json_field('nova_id', "Host UUID"),454 make_json_field('nova_id', "Host UUID", required=True),
446 make_json_field('os_tenantname', "Tenant name"),455 make_json_field('os_tenantname', "Tenant name", required=True),
447 make_json_field('os_username', "Username"),456 make_json_field('os_username', "Username", required=True),
448 make_json_field('os_password', "Password", field_type='password'),457 make_json_field(
449 make_json_field('os_authurl', "Auth URL"),458 'os_password', "Password", field_type='password',
459 required=True),
460 make_json_field('os_authurl', "Auth URL", required=True),
450 ],461 ],
451 },462 },
452]463]