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

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 5199
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
=== modified file 'src/maasserver/api/machines.py'
--- src/maasserver/api/machines.py 2016-08-18 12:30:42 +0000
+++ src/maasserver/api/machines.py 2016-10-24 18:44:21 +0000
@@ -30,7 +30,6 @@
30 OwnerDataMixin,30 OwnerDataMixin,
31 PowerMixin,31 PowerMixin,
32 PowersMixin,32 PowersMixin,
33 store_node_power_parameters,
34)33)
35from maasserver.api.support import (34from maasserver.api.support import (
36 admin_method,35 admin_method,
@@ -737,8 +736,6 @@
737 form = Form(data=altered_query_data, request=request)736 form = Form(data=altered_query_data, request=request)
738 if form.is_valid():737 if form.is_valid():
739 machine = form.save()738 machine = form.save()
740 # Hack in the power parameters here.
741 store_node_power_parameters(machine, request)
742 maaslog.info("%s: Enlisted new machine", machine.hostname)739 maaslog.info("%s: Enlisted new machine", machine.hostname)
743 return machine740 return machine
744 else:741 else:
745742
=== modified file 'src/maasserver/api/tests/test_enlistment.py'
--- src/maasserver/api/tests/test_enlistment.py 2016-06-14 18:31:07 +0000
+++ src/maasserver/api/tests/test_enlistment.py 2016-10-24 18:44:21 +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-05-24 22:05:45 +0000
+++ src/maasserver/api/tests/test_machine.py 2016-10-24 18:44:21 +0000
@@ -808,7 +808,7 @@
808 # The api allows the updating of a Machine.808 # The api allows the updating of a Machine.
809 machine = factory.make_Node(809 machine = factory.make_Node(
810 hostname='diane', owner=self.user,810 hostname='diane', owner=self.user,
811 architecture=make_usable_architecture(self))811 architecture=make_usable_architecture(self), power_type='manual')
812 response = self.client.put(812 response = self.client.put(
813 self.get_machine_uri(machine), {'hostname': 'francis'})813 self.get_machine_uri(machine), {'hostname': 'francis'})
814 parsed_result = json_load_bytes(response.content)814 parsed_result = json_load_bytes(response.content)
@@ -825,7 +825,8 @@
825 hostname = factory.make_name('hostname')825 hostname = factory.make_name('hostname')
826 arch = make_usable_architecture(self)826 arch = make_usable_architecture(self)
827 machine = factory.make_Node(827 machine = factory.make_Node(
828 hostname=hostname, owner=self.user, architecture=arch)828 hostname=hostname, owner=self.user, architecture=arch,
829 power_type='manual')
829 response = self.client.put(830 response = self.client.put(
830 self.get_machine_uri(machine),831 self.get_machine_uri(machine),
831 {'architecture': arch})832 {'architecture': arch})
@@ -848,7 +849,8 @@
848 self.become_admin()849 self.become_admin()
849 machine = factory.make_Node(850 machine = factory.make_Node(
850 owner=self.user,851 owner=self.user,
851 architecture=make_usable_architecture(self))852 architecture=make_usable_architecture(self),
853 power_type='manual')
852 field = factory.make_string()854 field = factory.make_string()
853 response = self.client.put(855 response = self.client.put(
854 self.get_machine_uri(machine),856 self.get_machine_uri(machine),
@@ -866,7 +868,11 @@
866 power_type=original_power_type,868 power_type=original_power_type,
867 architecture=make_usable_architecture(self))869 architecture=make_usable_architecture(self))
868 response = self.client.put(870 response = self.client.put(
869 self.get_machine_uri(machine), {'power_type': new_power_type})871 self.get_machine_uri(machine),
872 {
873 'power_type': new_power_type,
874 'power_parameters_skip_check': 'true',
875 })
870876
871 self.assertEqual(http.client.OK, response.status_code)877 self.assertEqual(http.client.OK, response.status_code)
872 self.assertEqual(878 self.assertEqual(
@@ -890,7 +896,8 @@
890 # provides the URI for this Machine.896 # provides the URI for this Machine.
891 machine = factory.make_Node(897 machine = factory.make_Node(
892 hostname='diane', owner=self.user,898 hostname='diane', owner=self.user,
893 architecture=make_usable_architecture(self))899 architecture=make_usable_architecture(self),
900 power_type='manual')
894 response = self.client.put(901 response = self.client.put(
895 self.get_machine_uri(machine), {'hostname': 'francis'})902 self.get_machine_uri(machine), {'hostname': 'francis'})
896 parsed_result = json_load_bytes(response.content)903 parsed_result = json_load_bytes(response.content)
@@ -906,7 +913,8 @@
906 self.become_admin()913 self.become_admin()
907 machine = factory.make_Node(914 machine = factory.make_Node(
908 hostname='diane', owner=self.user,915 hostname='diane', owner=self.user,
909 architecture=make_usable_architecture(self))916 architecture=make_usable_architecture(self),
917 power_type='manual')
910 response = self.client.put(918 response = self.client.put(
911 self.get_machine_uri(machine), {'hostname': '.'})919 self.get_machine_uri(machine), {'hostname': '.'})
912 parsed_result = json_load_bytes(response.content)920 parsed_result = json_load_bytes(response.content)
@@ -959,8 +967,8 @@
959 self.become_admin()967 self.become_admin()
960 machine = factory.make_Node(968 machine = factory.make_Node(
961 owner=self.user,969 owner=self.user,
962 power_type=factory.pick_power_type(),970 architecture=make_usable_architecture(self),
963 architecture=make_usable_architecture(self))971 power_type='manual')
964 response = self.client.put(972 response = self.client.put(
965 self.get_machine_uri(machine),973 self.get_machine_uri(machine),
966 {'cpu_count': 1, 'memory': 1024})974 {'cpu_count': 1, 'memory': 1024})
@@ -1067,7 +1075,8 @@
1067 self.become_admin()1075 self.become_admin()
1068 machine = factory.make_Node(1076 machine = factory.make_Node(
1069 owner=self.user,1077 owner=self.user,
1070 architecture=make_usable_architecture(self))1078 architecture=make_usable_architecture(self),
1079 power_parameters={})
1071 new_param = factory.make_string()1080 new_param = factory.make_string()
1072 new_value = factory.make_string()1081 new_value = factory.make_string()
1073 response = self.client.put(1082 response = self.client.put(
@@ -1083,7 +1092,11 @@
10831092
1084 def test_PUT_updates_power_parameters_empty_string(self):1093 def test_PUT_updates_power_parameters_empty_string(self):
1085 self.become_admin()1094 self.become_admin()
1086 power_parameters = {factory.make_string(): factory.make_string()}1095 power_parameters = {
1096 'power_address': factory.make_url(),
1097 'power_id': factory.make_name('power_id'),
1098 'power_pass': factory.make_name('power_pass'),
1099 }
1087 machine = factory.make_Node(1100 machine = factory.make_Node(
1088 owner=self.user,1101 owner=self.user,
1089 power_type='virsh',1102 power_type='virsh',
@@ -1091,22 +1104,17 @@
1091 architecture=make_usable_architecture(self))1104 architecture=make_usable_architecture(self))
1092 response = self.client.put(1105 response = self.client.put(
1093 self.get_machine_uri(machine),1106 self.get_machine_uri(machine),
1094 {'power_parameters_power_id': ''})1107 {'power_parameters_power_pass': ''})
10951108
1096 self.assertEqual(http.client.OK, response.status_code)1109 self.assertEqual(http.client.OK, response.status_code)
1097 self.assertEqual(1110 self.assertEqual(
1098 {1111 '', reload_object(machine).power_parameters['power_pass'])
1099 'power_id': '',
1100 'power_pass': '',
1101 'power_address': '',
1102 },
1103 reload_object(machine).power_parameters)
11041112
1105 def test_PUT_sets_zone(self):1113 def test_PUT_sets_zone(self):
1106 self.become_admin()1114 self.become_admin()
1107 new_zone = factory.make_Zone()1115 new_zone = factory.make_Zone()
1108 machine = factory.make_Node(1116 machine = factory.make_Node(
1109 architecture=make_usable_architecture(self))1117 architecture=make_usable_architecture(self), power_type='manual')
11101118
1111 response = self.client.put(1119 response = self.client.put(
1112 self.get_machine_uri(machine), {'zone': new_zone.name})1120 self.get_machine_uri(machine), {'zone': new_zone.name})
@@ -1119,7 +1127,7 @@
1119 self.become_admin()1127 self.become_admin()
1120 new_name = factory.make_name()1128 new_name = factory.make_name()
1121 machine = factory.make_Node(1129 machine = factory.make_Node(
1122 architecture=make_usable_architecture(self))1130 architecture=make_usable_architecture(self), power_type='manual')
1123 old_zone = machine.zone1131 old_zone = machine.zone
11241132
1125 response = self.client.put(1133 response = self.client.put(
@@ -1148,7 +1156,8 @@
1148 self.become_admin()1156 self.become_admin()
1149 zone = factory.make_Zone()1157 zone = factory.make_Zone()
1150 machine = factory.make_Node(1158 machine = factory.make_Node(
1151 zone=zone, architecture=make_usable_architecture(self))1159 zone=zone, architecture=make_usable_architecture(self),
1160 power_type='manual')
11521161
1153 response = self.client.put(self.get_machine_uri(machine), {})1162 response = self.client.put(self.get_machine_uri(machine), {})
11541163
@@ -1186,6 +1195,7 @@
1186 machine = factory.make_Node(1195 machine = factory.make_Node(
1187 owner=self.user,1196 owner=self.user,
1188 architecture=make_usable_architecture(self),1197 architecture=make_usable_architecture(self),
1198 power_type='manual',
1189 disable_ipv4=original_setting)1199 disable_ipv4=original_setting)
1190 new_setting = not original_setting1200 new_setting = not original_setting
11911201
@@ -1202,6 +1212,7 @@
1202 machine = factory.make_Node(1212 machine = factory.make_Node(
1203 owner=self.user,1213 owner=self.user,
1204 architecture=make_usable_architecture(self),1214 architecture=make_usable_architecture(self),
1215 power_type='manual',
1205 disable_ipv4=original_setting)1216 disable_ipv4=original_setting)
1206 self.assertEqual(original_setting, machine.disable_ipv4)1217 self.assertEqual(original_setting, machine.disable_ipv4)
12071218
@@ -1216,7 +1227,8 @@
1216 self.become_admin()1227 self.become_admin()
1217 machine = factory.make_Node(1228 machine = factory.make_Node(
1218 owner=self.user,1229 owner=self.user,
1219 architecture=make_usable_architecture(self))1230 architecture=make_usable_architecture(self),
1231 power_type='manual')
1220 response = self.client.put(1232 response = self.client.put(
1221 reverse('machine_handler', args=[machine.system_id]),1233 reverse('machine_handler', args=[machine.system_id]),
1222 {'swap_size': 5 * 1000 ** 3}) # Making sure we overflow 32 bits1234 {'swap_size': 5 * 1000 ** 3}) # Making sure we overflow 32 bits
@@ -1229,7 +1241,8 @@
1229 self.become_admin()1241 self.become_admin()
1230 machine = factory.make_Node(1242 machine = factory.make_Node(
1231 owner=self.user,1243 owner=self.user,
1232 architecture=make_usable_architecture(self))1244 architecture=make_usable_architecture(self),
1245 power_type='manual')
12331246
1234 response = self.client.put(1247 response = self.client.put(
1235 reverse('machine_handler', args=[machine.system_id]),1248 reverse('machine_handler', args=[machine.system_id]),
12361249
=== modified file 'src/maasserver/api/tests/test_node.py'
--- src/maasserver/api/tests/test_node.py 2016-06-17 07:16:39 +0000
+++ src/maasserver/api/tests/test_node.py 2016-10-24 18:44:21 +0000
@@ -302,7 +302,7 @@
302 self.assertEqual(302 self.assertEqual(
303 http.client.OK, response.status_code, response.content)303 http.client.OK, response.status_code, response.content)
304 parsed_params = json_load_bytes(response.content)304 parsed_params = json_load_bytes(response.content)
305 self.assertEqual({}, parsed_params)305 self.assertEqual(node.power_parameters, parsed_params)
306306
307 def test_power_parameters_requires_admin(self):307 def test_power_parameters_requires_admin(self):
308 node = factory.make_Node()308 node = factory.make_Node()
309309
=== 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-24 18:44:21 +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-24 18:44:21 +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-03-28 13:54:47 +0000
+++ src/maasserver/api/utils.py 2016-10-24 18:44:21 +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,
@@ -149,11 +150,23 @@
149 """150 """
150 # Create a writable query dict.151 # Create a writable query dict.
151 new_data = QueryDict('').copy()152 new_data = QueryDict('').copy()
153 # If the fields are a dict of django Fields see if one is a DictCharField.
154 # DictCharField must have their values prefixed with the DictField name in
155 # the returned data or defaults don't get carried.
156 if isinstance(fields, dict):
157 acceptable_fields = []
158 for field_name, field in fields.items():
159 acceptable_fields.append(field_name)
160 if isinstance(field, DictCharField):
161 for sub_field in field.names:
162 acceptable_fields.append("%s_%s" % (field_name, sub_field))
163 else:
164 acceptable_fields = fields
152 # Missing fields will be taken from the node's current values. This165 # Missing fields will be taken from the node's current values. This
153 # is to circumvent Django's ModelForm (form created from a model)166 # is to circumvent Django's ModelForm (form created from a model)
154 # default behaviour that requires all the fields to be defined.167 # default behaviour that requires all the fields to be defined.
155 for k, v in defaults.items():168 for k, v in defaults.items():
156 if k in fields:169 if k in acceptable_fields:
157 new_data.setlist(k, listify(v))170 new_data.setlist(k, listify(v))
158 # We can't use update here because data is a QueryDict and 'update'171 # We can't use update here because data is a QueryDict and 'update'
159 # does not replaces the old values with the new as one would expect.172 # does not replaces the old values with the new as one would expect.
160173
=== 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-24 18:44:21 +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-24 18:44:21 +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-24 18:44:21 +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-08-09 17:19:45 +0000
+++ src/maasserver/forms.py 2016-10-24 18:44:21 +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
@@ -244,6 +245,27 @@
244 return power_type245 return power_type
245246
246 @staticmethod247 @staticmethod
248 def _get_power_parameters(form, data, machine):
249 if data is None:
250 data = {}
251
252 power_parameters = data.get(
253 'power_parameters', form.initial.get('power_parameters', {}))
254
255 if isinstance(power_parameters, str):
256 try:
257 power_parameters = json.loads(power_parameters)
258 except json.JSONDecodeError:
259 raise ValidationError("Failed to parse JSON power_parameters")
260
261 # Integrate the machines existing power_parameters if unset by form.
262 if machine:
263 for key, value in machine.power_parameters.items():
264 if power_parameters.get(key) is None:
265 power_parameters[key] = value
266 return power_parameters
267
268 @staticmethod
247 def set_up_power_type(form, data, machine=None):269 def set_up_power_type(form, data, machine=None):
248 """Set up the 'power_type' and 'power_parameters' fields.270 """Set up the 'power_type' and 'power_parameters' fields.
249271
@@ -254,10 +276,18 @@
254 choices = [BLANK_CHOICE] + get_power_type_choices()276 choices = [BLANK_CHOICE] + get_power_type_choices()
255 form.fields['power_type'] = forms.ChoiceField(277 form.fields['power_type'] = forms.ChoiceField(
256 required=False, choices=choices, initial=power_type)278 required=False, choices=choices, initial=power_type)
257 form.fields['power_parameters'] = get_power_type_parameters()[279 power_parameters = WithPowerMixin._get_power_parameters(
258 power_type]280 form, data, machine)
259 if form.instance is not None and form.instance.power_type != '':281 skip_check = (
260 form.initial['power_type'] = form.instance.power_type282 form.data.get('power_parameters_%s' % SKIP_CHECK_NAME) == 'true')
283 form.fields['power_parameters'] = get_power_type_parameters(
284 power_parameters, skip_check=skip_check)[power_type]
285 if form.instance is not None:
286 if form.instance.power_type != '':
287 form.initial['power_type'] = form.instance.power_type
288 if form.instance.power_parameters != '':
289 for key, value in power_parameters.items():
290 form.initial['power_parameters_%s' % key] = value
261291
262 @staticmethod292 @staticmethod
263 def check_power_type(form, cleaned_data):293 def check_power_type(form, cleaned_data):
@@ -280,8 +310,9 @@
280310
281 # If power_type is not set and power_parameters_skip_check is not311 # If power_type is not set and power_parameters_skip_check is not
282 # on, reset power_parameters (set it to the empty string).312 # on, reset power_parameters (set it to the empty string).
283 if cleaned_data.get('power_type', '') == '':313 power_type = cleaned_data.get('power_type', '')
284 cleaned_data['power_parameters'] = ''314 if power_type == '':
315 cleaned_data['power_parameters'] = {}
285 return cleaned_data316 return cleaned_data
286317
287 @staticmethod318 @staticmethod
288319
=== modified file 'src/maasserver/rpc/nodes.py'
--- src/maasserver/rpc/nodes.py 2016-07-25 23:25:53 +0000
+++ src/maasserver/rpc/nodes.py 2016-10-24 18:44:21 +0000
@@ -222,10 +222,6 @@
222 form = AdminMachineWithMACAddressesForm(data_query_dict)222 form = AdminMachineWithMACAddressesForm(data_query_dict)
223 if form.is_valid():223 if form.is_valid():
224 node = form.save()224 node = form.save()
225 # We have to explicitly save the power parameters; the form
226 # won't do it for us.
227 node.power_parameters = json.loads(power_parameters)
228 node.save()
229 return node225 return node
230 else:226 else:
231 raise ValidationError(form.errors)227 raise ValidationError(form.errors)
232228
=== modified file 'src/maasserver/rpc/tests/test_nodes.py'
--- src/maasserver/rpc/tests/test_nodes.py 2016-05-11 00:25:14 +0000
+++ src/maasserver/rpc/tests/test_nodes.py 2016-10-24 18:44:21 +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
@@ -79,17 +78,13 @@
79 mac_addresses = [78 mac_addresses = [
80 factory.make_mac_address() for _ in range(3)]79 factory.make_mac_address() for _ in range(3)]
81 architecture = make_usable_architecture(self)80 architecture = make_usable_architecture(self)
82 power_type = random.choice(self.power_types)['name']
83 power_parameters = dumps({})
8481
85 node = create_node(82 node = create_node(architecture, 'manual', {}, mac_addresses)
86 architecture, power_type, power_parameters,
87 mac_addresses)
8883
89 self.assertEqual(84 self.assertEqual(
90 (85 (
91 architecture,86 architecture,
92 power_type,87 'manual',
93 {},88 {},
94 ),89 ),
95 (90 (
@@ -113,17 +108,14 @@
113 factory.make_mac_address() for _ in range(3)]108 factory.make_mac_address() for _ in range(3)]
114 architecture = make_usable_architecture(self)109 architecture = make_usable_architecture(self)
115 hostname = factory.make_hostname()110 hostname = factory.make_hostname()
116 power_type = random.choice(self.power_types)['name']
117 power_parameters = dumps({})
118111
119 node = create_node(112 node = create_node(
120 architecture, power_type, power_parameters,113 architecture, 'manual', {}, mac_addresses, hostname=hostname)
121 mac_addresses, hostname=hostname)
122114
123 self.assertEqual(115 self.assertEqual(
124 (116 (
125 architecture,117 architecture,
126 power_type,118 'manual',
127 {},119 {},
128 hostname120 hostname
129 ),121 ),
@@ -149,7 +141,7 @@
149 "Microsoft Windows",141 "Microsoft Windows",
150 ])142 ])
151 power_type = random.choice(self.power_types)['name']143 power_type = random.choice(self.power_types)['name']
152 power_parameters = dumps({})144 power_parameters = {}
153145
154 with ExpectedException(ValidationError):146 with ExpectedException(ValidationError):
155 create_node(147 create_node(
@@ -164,17 +156,15 @@
164 architecture = make_usable_architecture(self)156 architecture = make_usable_architecture(self)
165 hostname = factory.make_hostname()157 hostname = factory.make_hostname()
166 domain = factory.make_Domain()158 domain = factory.make_Domain()
167 power_type = random.choice(self.power_types)['name']
168 power_parameters = dumps({})
169159
170 node = create_node(160 node = create_node(
171 architecture, power_type, power_parameters,161 architecture, 'manual', {}, mac_addresses, domain=domain.name,
172 mac_addresses, domain=domain.name, hostname=hostname)162 hostname=hostname)
173163
174 self.assertEqual(164 self.assertEqual(
175 (165 (
176 architecture,166 architecture,
177 power_type,167 'manual',
178 {},168 {},
179 domain.id,169 domain.id,
180 hostname,170 hostname,
@@ -198,7 +188,7 @@
198 factory.make_mac_address() for _ in range(3)]188 factory.make_mac_address() for _ in range(3)]
199 architecture = make_usable_architecture(self)189 architecture = make_usable_architecture(self)
200 power_type = random.choice(self.power_types)['name']190 power_type = random.choice(self.power_types)['name']
201 power_parameters = dumps({})191 power_parameters = {}
202192
203 with ExpectedException(ValidationError):193 with ExpectedException(ValidationError):
204 create_node(194 create_node(
@@ -211,7 +201,7 @@
211 self.assertRaises(201 self.assertRaises(
212 ValidationError, create_node,202 ValidationError, create_node,
213 architecture="spam/eggs", power_type="scrambled",203 architecture="spam/eggs", power_type="scrambled",
214 power_parameters=dumps({}),204 power_parameters={},
215 mac_addresses=[factory.make_mac_address()])205 mac_addresses=[factory.make_mac_address()])
216206
217 def test__raises_error_if_node_already_exists(self):207 def test__raises_error_if_node_already_exists(self):
@@ -220,8 +210,8 @@
220 mac_addresses = [210 mac_addresses = [
221 factory.make_mac_address() for _ in range(3)]211 factory.make_mac_address() for _ in range(3)]
222 architecture = make_usable_architecture(self)212 architecture = make_usable_architecture(self)
223 power_type = random.choice(self.power_types)['name']213 power_type = 'manual'
224 power_parameters = dumps({})214 power_parameters = {}
225215
226 create_node(216 create_node(
227 architecture, power_type, power_parameters,217 architecture, power_type, power_parameters,
@@ -236,20 +226,20 @@
236 mac_addresses = [226 mac_addresses = [
237 factory.make_mac_address() for _ in range(3)]227 factory.make_mac_address() for _ in range(3)]
238 architecture = make_usable_architecture(self)228 architecture = make_usable_architecture(self)
239 power_type = random.choice(self.power_types)['name']
240 power_parameters = {229 power_parameters = {
241 factory.make_name('key'): factory.make_name('value')230 'power_address': factory.make_url(),
242 for _ in range(3)231 'power_pass': factory.make_name('power_pass'),
232 'power_id': factory.make_name('power_id'),
243 }233 }
244234
245 node = create_node(235 node = create_node(
246 architecture, power_type, dumps(power_parameters),236 architecture, 'virsh', power_parameters,
247 mac_addresses)237 mac_addresses)
248238
249 # Reload the object from the DB so that we're sure its power239 # Reload the object from the DB so that we're sure its power
250 # parameters are being persisted.240 # parameters are being persisted.
251 node = reload_object(node)241 node = reload_object(node)
252 self.assertEqual(power_parameters, node.power_parameters)242 self.assertItemsEqual(power_parameters, node.power_parameters)
253243
254 def test__forces_generic_subarchitecture_if_missing(self):244 def test__forces_generic_subarchitecture_if_missing(self):
255 self.prepare_rack_rpc()245 self.prepare_rack_rpc()
@@ -257,13 +247,9 @@
257 mac_addresses = [247 mac_addresses = [
258 factory.make_mac_address() for _ in range(3)]248 factory.make_mac_address() for _ in range(3)]
259 architecture = make_usable_architecture(self, subarch_name='generic')249 architecture = make_usable_architecture(self, subarch_name='generic')
260 power_type = random.choice(self.power_types)['name']
261 power_parameters = dumps({})
262250
263 arch, subarch = architecture.split('/')251 arch, subarch = architecture.split('/')
264 node = create_node(252 node = create_node(arch, 'manual', {}, mac_addresses)
265 arch, power_type, power_parameters,
266 mac_addresses)
267253
268 self.assertEqual(architecture, node.architecture)254 self.assertEqual(architecture, node.architecture)
269255
270256
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-07-21 16:43:37 +0000
+++ src/maasserver/testing/factory.py 2016-10-24 18:44:21 +0000
@@ -367,8 +367,6 @@
367 zone = self.make_Zone()367 zone = self.make_Zone()
368 if power_type is None:368 if power_type is None:
369 power_type = 'virsh'369 power_type = 'virsh'
370 if power_parameters is None:
371 power_parameters = {}
372 if power_state is None:370 if power_state is None:
373 power_state = self.pick_enum(POWER_STATE)371 power_state = self.pick_enum(POWER_STATE)
374 if power_state_updated is undefined:372 if power_state_updated is undefined:
@@ -430,7 +428,8 @@
430 ip_address = existing_static_ips[0]428 ip_address = existing_static_ips[0]
431 bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet)429 bmc_ip_address = self.pick_ip_in_Subnet(ip_address.subnet)
432 node.power_parameters = {430 node.power_parameters = {
433 "power_address": "qemu+ssh://user@%s/system" % bmc_ip_address431 "power_address": "qemu+ssh://user@%s/system" % bmc_ip_address,
432 "power_id": factory.make_name("power_id"),
434 }433 }
435 node.save()434 node.save()
436435
437436
=== modified file 'src/maasserver/tests/test_config_forms.py'
--- src/maasserver/tests/test_config_forms.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/tests/test_config_forms.py 2016-10-24 18:44:21 +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-24 18:44:21 +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-04-12 22:25:44 +0000
+++ src/maasserver/tests/test_forms_machine.py 2016-10-24 18:44:21 +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-04-22 21:02:20 +0000
+++ src/maasserver/websockets/handlers/machine.py 2016-10-24 18:44:21 +0000
@@ -214,6 +214,7 @@
214 new_params["hostname"] = params.get("hostname")214 new_params["hostname"] = params.get("hostname")
215 new_params["architecture"] = params.get("architecture")215 new_params["architecture"] = params.get("architecture")
216 new_params["power_type"] = params.get("power_type")216 new_params["power_type"] = params.get("power_type")
217 new_params["power_parameters"] = params.get("power_parameters")
217 if "zone" in params:218 if "zone" in params:
218 new_params["zone"] = params["zone"]["name"]219 new_params["zone"] = params["zone"]["name"]
219 if "domain" in params:220 if "domain" in params:
@@ -236,13 +237,8 @@
236 if not self.user.is_superuser:237 if not self.user.is_superuser:
237 raise HandlerPermissionError()238 raise HandlerPermissionError()
238239
239 # Create the object, then save the power parameters because the
240 # form will not save this information.
241 data = super(NodeHandler, self).create(params)240 data = super(NodeHandler, self).create(params)
242 node_obj = Node.objects.get(system_id=data['system_id'])241 node_obj = Node.objects.get(system_id=data['system_id'])
243 node_obj.power_type = params.get("power_type", '')
244 node_obj.power_parameters = params.get("power_parameters", {})
245 node_obj.save()
246242
247 # Start the commissioning process right away, which has the243 # Start the commissioning process right away, which has the
248 # desired side effect of initializing the node's power state.244 # desired side effect of initializing the node's power state.
@@ -256,16 +252,13 @@
256 if not self.user.is_superuser:252 if not self.user.is_superuser:
257 raise HandlerPermissionError()253 raise HandlerPermissionError()
258254
259 # Update the node with the form. The form will not update the
260 # power_type or power_parameters, so we perform that here.
261 data = super(NodeHandler, self).update(params)255 data = super(NodeHandler, self).update(params)
262 node_obj = Node.objects.get(system_id=data['system_id'])256 node_obj = Node.objects.get(system_id=data['system_id'])
263 node_obj.power_type = params.get("power_type", '')
264 node_obj.power_parameters = params.get("power_parameters", {})
265257
266 # Update the tags for the node and disks.258 # Update the tags for the node and disks.
267 self.update_tags(node_obj, params['tags'])259 self.update_tags(node_obj, params['tags'])
268 node_obj.save()260 node_obj.save()
261
269 return self.full_dehydrate(node_obj)262 return self.full_dehydrate(node_obj)
270263
271 def mount_special(self, params):264 def mount_special(self, params):
272265
=== modified file 'src/maasserver/websockets/handlers/tests/test_machine.py'
--- src/maasserver/websockets/handlers/tests/test_machine.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-24 18:44:21 +0000
@@ -1094,7 +1094,7 @@
1094 def test_update_raises_validation_error_for_invalid_architecture(self):1094 def test_update_raises_validation_error_for_invalid_architecture(self):
1095 user = factory.make_admin()1095 user = factory.make_admin()
1096 handler = MachineHandler(user, {})1096 handler = MachineHandler(user, {})
1097 node = factory.make_Node(interface=True)1097 node = factory.make_Node(interface=True, power_type='manual')
1098 node_data = self.dehydrate_node(node, handler)1098 node_data = self.dehydrate_node(node, handler)
1099 arch = factory.make_name("arch")1099 arch = factory.make_name("arch")
1100 node_data["architecture"] = arch1100 node_data["architecture"] = arch
@@ -1144,7 +1144,8 @@
1144 user = factory.make_admin()1144 user = factory.make_admin()
1145 handler = MachineHandler(user, {})1145 handler = MachineHandler(user, {})
1146 architecture = make_usable_architecture(self)1146 architecture = make_usable_architecture(self)
1147 node = factory.make_Node(interface=True, architecture=architecture)1147 node = factory.make_Node(
1148 interface=True, architecture=architecture, power_type='manual')
1148 tags = [1149 tags = [
1149 factory.make_Tag(definition='').name1150 factory.make_Tag(definition='').name
1150 for _ in range(3)1151 for _ in range(3)
@@ -1158,7 +1159,8 @@
1158 user = factory.make_admin()1159 user = factory.make_admin()
1159 handler = MachineHandler(user, {})1160 handler = MachineHandler(user, {})
1160 architecture = make_usable_architecture(self)1161 architecture = make_usable_architecture(self)
1161 node = factory.make_Node(interface=True, architecture=architecture)1162 node = factory.make_Node(
1163 interface=True, architecture=architecture, power_type='manual')
1162 tags = []1164 tags = []
1163 for _ in range(3):1165 for _ in range(3):
1164 tag = factory.make_Tag(definition='')1166 tag = factory.make_Tag(definition='')
@@ -1175,7 +1177,8 @@
1175 user = factory.make_admin()1177 user = factory.make_admin()
1176 handler = MachineHandler(user, {})1178 handler = MachineHandler(user, {})
1177 architecture = make_usable_architecture(self)1179 architecture = make_usable_architecture(self)
1178 node = factory.make_Node(interface=True, architecture=architecture)1180 node = factory.make_Node(
1181 interface=True, architecture=architecture, power_type='manual')
1179 tag_name = factory.make_name("tag")1182 tag_name = factory.make_name("tag")
1180 node_data = self.dehydrate_node(node, handler)1183 node_data = self.dehydrate_node(node, handler)
1181 node_data["tags"].append(tag_name)1184 node_data["tags"].append(tag_name)
11821185
=== modified file 'src/maasserver/websockets/tests/test_base.py'
--- src/maasserver/websockets/tests/test_base.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/websockets/tests/test_base.py 2016-10-24 18:44:21 +0000
@@ -536,7 +536,7 @@
536536
537 def test_update_with_form_updates_node(self):537 def test_update_with_form_updates_node(self):
538 arch = make_usable_architecture(self)538 arch = make_usable_architecture(self)
539 node = factory.make_Node(architecture=arch)539 node = factory.make_Node(architecture=arch, power_type='manual')
540 hostname = factory.make_name("hostname")540 hostname = factory.make_name("hostname")
541 handler = self.make_nodes_handler(541 handler = self.make_nodes_handler(
542 fields=['hostname'], form=AdminMachineForm)542 fields=['hostname'], form=AdminMachineForm)
@@ -552,7 +552,7 @@
552552
553 def test_update_with_form_uses_form_from_get_form_class(self):553 def test_update_with_form_uses_form_from_get_form_class(self):
554 arch = make_usable_architecture(self)554 arch = make_usable_architecture(self)
555 node = factory.make_Node(architecture=arch)555 node = factory.make_Node(architecture=arch, power_type='manual')
556 hostname = factory.make_name("hostname")556 hostname = factory.make_name("hostname")
557 handler = self.make_nodes_handler(fields=['hostname'])557 handler = self.make_nodes_handler(fields=['hostname'])
558 self.patch(558 self.patch(
559559
=== modified file 'src/provisioningserver/power/schema.py'
--- src/provisioningserver/power/schema.py 2016-05-20 19:58:33 +0000
+++ src/provisioningserver/power/schema.py 2016-10-24 18:44:21 +0000
@@ -224,9 +224,10 @@
224 'name': 'virsh',224 'name': 'virsh',
225 'description': 'Virsh (virtual systems)',225 'description': 'Virsh (virtual systems)',
226 'fields': [226 'fields': [
227 make_json_field('power_address', "Power address"),227 make_json_field('power_address', "Power address", required=True),
228 make_json_field(228 make_json_field(
229 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE),229 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE,
230 required=True),
230 make_json_field(231 make_json_field(
231 'power_pass', "Power password (optional)",232 'power_pass', "Power password (optional)",
232 required=False, field_type='password'),233 required=False, field_type='password'),
@@ -244,10 +245,11 @@
244 make_json_field(245 make_json_field(
245 'power_uuid', "VM UUID (if known)", required=False,246 'power_uuid', "VM UUID (if known)", required=False,
246 scope=POWER_PARAMETER_SCOPE.NODE),247 scope=POWER_PARAMETER_SCOPE.NODE),
247 make_json_field('power_address', "VMware hostname"),248 make_json_field('power_address', "VMware hostname", required=True),
248 make_json_field('power_user', "VMware username"),249 make_json_field('power_user', "VMware username", required=True),
249 make_json_field(250 make_json_field(
250 'power_pass', "VMware password", field_type='password'),251 'power_pass', "VMware password", field_type='password',
252 required=True),
251 make_json_field(253 make_json_field(
252 'power_port', "VMware API port (optional)", required=False),254 'power_port', "VMware API port (optional)", required=False),
253 make_json_field(255 make_json_field(
@@ -259,9 +261,10 @@
259 'name': 'fence_cdu',261 'name': 'fence_cdu',
260 'description': 'Sentry Switch CDU',262 'description': 'Sentry Switch CDU',
261 'fields': [263 'fields': [
262 make_json_field('power_address', "Power address"),264 make_json_field('power_address', "Power address", required=True),
263 make_json_field(265 make_json_field(
264 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE),266 'power_id', "Power ID", scope=POWER_PARAMETER_SCOPE.NODE,
267 required=True),
265 make_json_field('power_user', "Power user"),268 make_json_field('power_user', "Power user"),
266 make_json_field(269 make_json_field(
267 'power_pass', "Power password", field_type='password'),270 'power_pass', "Power password", field_type='password'),
@@ -274,8 +277,9 @@
274 'fields': [277 'fields': [
275 make_json_field(278 make_json_field(
276 'power_driver', "Power driver", field_type='choice',279 'power_driver', "Power driver", field_type='choice',
277 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0),280 choices=IPMI_DRIVER_CHOICES, default=IPMI_DRIVER.LAN_2_0,
278 make_json_field('power_address', "IP address"),281 required=True),
282 make_json_field('power_address', "IP address", required=True),
279 make_json_field('power_user', "Power user"),283 make_json_field('power_user', "Power user"),
280 make_json_field(284 make_json_field(
281 'power_pass', "Power password", field_type='password'),285 'power_pass', "Power password", field_type='password'),
@@ -288,13 +292,13 @@
288 'name': 'moonshot',292 'name': 'moonshot',
289 'description': 'HP Moonshot - iLO4 (IPMI)',293 'description': 'HP Moonshot - iLO4 (IPMI)',
290 'fields': [294 'fields': [
291 make_json_field('power_address', "Power address"),295 make_json_field('power_address', "Power address", required=True),
292 make_json_field('power_user', "Power user"),296 make_json_field('power_user', "Power user"),
293 make_json_field(297 make_json_field(
294 'power_pass', "Power password", field_type='password'),298 'power_pass', "Power password", field_type='password'),
295 make_json_field(299 make_json_field(
296 'power_hwaddress', "Power hardware address",300 'power_hwaddress', "Power hardware address",
297 scope=POWER_PARAMETER_SCOPE.NODE),301 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
298 ],302 ],
299 'ip_extractor': make_ip_extractor('power_address'),303 'ip_extractor': make_ip_extractor('power_address'),
300 },304 },
@@ -303,14 +307,16 @@
303 'description': 'SeaMicro 15000',307 'description': 'SeaMicro 15000',
304 'fields': [308 'fields': [
305 make_json_field(309 make_json_field(
306 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE),310 'system_id', "System ID", scope=POWER_PARAMETER_SCOPE.NODE,
307 make_json_field('power_address', "Power address"),311 required=True),
312 make_json_field('power_address', "Power address", required=True),
308 make_json_field('power_user', "Power user"),313 make_json_field('power_user', "Power user"),
309 make_json_field(314 make_json_field(
310 'power_pass', "Power password", field_type='password'),315 'power_pass', "Power password", field_type='password'),
311 make_json_field(316 make_json_field(
312 'power_control', "Power control type", field_type='choice',317 'power_control', "Power control type", field_type='choice',
313 choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi'),318 choices=SM15K_POWER_CONTROL_CHOICES, default='ipmi',
319 required=True),
314 ],320 ],
315 'ip_extractor': make_ip_extractor('power_address'),321 'ip_extractor': make_ip_extractor('power_address'),
316 },322 },
@@ -320,7 +326,7 @@
320 'fields': [326 'fields': [
321 make_json_field(327 make_json_field(
322 'power_pass', "Power password", field_type='password'),328 'power_pass', "Power password", field_type='password'),
323 make_json_field('power_address', "Power address")329 make_json_field('power_address', "Power address", required=True)
324 ],330 ],
325 'ip_extractor': make_ip_extractor('power_address'),331 'ip_extractor': make_ip_extractor('power_address'),
326 },332 },
@@ -329,8 +335,9 @@
329 'description': 'Digital Loggers, Inc. PDU',335 'description': 'Digital Loggers, Inc. PDU',
330 'fields': [336 'fields': [
331 make_json_field(337 make_json_field(
332 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE),338 'outlet_id', "Outlet ID", scope=POWER_PARAMETER_SCOPE.NODE,
333 make_json_field('power_address', "Power address"),339 required=True),
340 make_json_field('power_address', "Power address", required=True),
334 make_json_field('power_user', "Power user"),341 make_json_field('power_user', "Power user"),
335 make_json_field(342 make_json_field(
336 'power_pass', "Power password", field_type='password'),343 'power_pass', "Power password", field_type='password'),
@@ -342,8 +349,9 @@
342 'description': "Cisco UCS Manager",349 'description': "Cisco UCS Manager",
343 'fields': [350 'fields': [
344 make_json_field(351 make_json_field(
345 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE),352 'uuid', "Server UUID", scope=POWER_PARAMETER_SCOPE.NODE,
346 make_json_field('power_address', "URL for XML API"),353 required=True),
354 make_json_field('power_address', "URL for XML API", required=True),
347 make_json_field('power_user', "API user"),355 make_json_field('power_user', "API user"),
348 make_json_field(356 make_json_field(
349 'power_pass', "API password", field_type='password'),357 'power_pass', "API password", field_type='password'),
@@ -355,7 +363,8 @@
355 'name': 'mscm',363 'name': 'mscm',
356 'description': "HP Moonshot - iLO Chassis Manager",364 'description': "HP Moonshot - iLO Chassis Manager",
357 'fields': [365 'fields': [
358 make_json_field('power_address', "IP for MSCM CLI API"),366 make_json_field(
367 'power_address', "IP for MSCM CLI API", required=True),
359 make_json_field('power_user', "MSCM CLI API user"),368 make_json_field('power_user', "MSCM CLI API user"),
360 make_json_field(369 make_json_field(
361 'power_pass', "MSCM CLI API password", field_type='password'),370 'power_pass', "MSCM CLI API password", field_type='password'),
@@ -363,7 +372,7 @@
363 'node_id',372 'node_id',
364 "Node ID - Must adhere to cXnY format "373 "Node ID - Must adhere to cXnY format "
365 "(X=cartridge number, Y=node number).",374 "(X=cartridge number, Y=node number).",
366 scope=POWER_PARAMETER_SCOPE.NODE),375 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
367 ],376 ],
368 'ip_extractor': make_ip_extractor('power_address'),377 'ip_extractor': make_ip_extractor('power_address'),
369 },378 },
@@ -371,14 +380,14 @@
371 'name': 'msftocs',380 'name': 'msftocs',
372 'description': "Microsoft OCS - Chassis Manager",381 'description': "Microsoft OCS - Chassis Manager",
373 'fields': [382 'fields': [
374 make_json_field('power_address', "Power address"),383 make_json_field('power_address', "Power address", required=True),
375 make_json_field('power_port', "Power port"),384 make_json_field('power_port', "Power port"),
376 make_json_field('power_user', "Power user"),385 make_json_field('power_user', "Power user"),
377 make_json_field(386 make_json_field(
378 'power_pass', "Power password", field_type='password'),387 'power_pass', "Power password", field_type='password'),
379 make_json_field(388 make_json_field(
380 'blade_id', "Blade ID (Typically 1-24)",389 'blade_id', "Blade ID (Typically 1-24)",
381 scope=POWER_PARAMETER_SCOPE.NODE),390 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
382 ],391 ],
383 'ip_extractor': make_ip_extractor('power_address'),392 'ip_extractor': make_ip_extractor('power_address'),
384 },393 },
@@ -386,10 +395,10 @@
386 'name': 'apc',395 'name': 'apc',
387 'description': "American Power Conversion (APC) PDU",396 'description': "American Power Conversion (APC) PDU",
388 'fields': [397 'fields': [
389 make_json_field('power_address', "IP for APC PDU"),398 make_json_field('power_address', "IP for APC PDU", required=True),
390 make_json_field(399 make_json_field(
391 'node_outlet', "APC PDU node outlet number (1-16)",400 'node_outlet', "APC PDU node outlet number (1-16)",
392 scope=POWER_PARAMETER_SCOPE.NODE),401 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
393 make_json_field(402 make_json_field(
394 'power_on_delay', "Power ON outlet delay (seconds)",403 'power_on_delay', "Power ON outlet delay (seconds)",
395 default='5'),404 default='5'),
@@ -400,16 +409,16 @@
400 'name': 'hmc',409 'name': 'hmc',
401 'description': "IBM Hardware Management Console (HMC)",410 'description': "IBM Hardware Management Console (HMC)",
402 'fields': [411 'fields': [
403 make_json_field('power_address', "IP for HMC"),412 make_json_field('power_address', "IP for HMC", required=True),
404 make_json_field('power_user', "HMC username"),413 make_json_field('power_user', "HMC username"),
405 make_json_field(414 make_json_field(
406 'power_pass', "HMC password", field_type='password'),415 'power_pass', "HMC password", field_type='password'),
407 make_json_field(416 make_json_field(
408 'server_name', "HMC Managed System server name",417 'server_name', "HMC Managed System server name",
409 scope=POWER_PARAMETER_SCOPE.NODE),418 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
410 make_json_field(419 make_json_field(
411 'lpar', "HMC logical partition",420 'lpar', "HMC logical partition",
412 scope=POWER_PARAMETER_SCOPE.NODE),421 scope=POWER_PARAMETER_SCOPE.NODE, required=True),
413 ],422 ],
414 'ip_extractor': make_ip_extractor('power_address'),423 'ip_extractor': make_ip_extractor('power_address'),
415 },424 },
@@ -417,11 +426,13 @@
417 'name': 'nova',426 'name': 'nova',
418 'description': 'OpenStack Nova',427 'description': 'OpenStack Nova',
419 'fields': [428 'fields': [
420 make_json_field('nova_id', "Host UUID"),429 make_json_field('nova_id', "Host UUID", required=True),
421 make_json_field('os_tenantname', "Tenant name"),430 make_json_field('os_tenantname', "Tenant name", required=True),
422 make_json_field('os_username', "Username"),431 make_json_field('os_username', "Username", required=True),
423 make_json_field('os_password', "Password", field_type='password'),432 make_json_field(
424 make_json_field('os_authurl', "Auth URL"),433 'os_password', "Password", field_type='password',
434 required=True),
435 make_json_field('os_authurl', "Auth URL", required=True),
425 ],436 ],
426 },437 },
427]438]

Subscribers

People subscribed via source and target branches

to all changes: