Merge ~ltrager/maas:lp1835289_2.6 into maas:2.6

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: e0be9ea77aa3da21e6d794a40d28ed29514a43b2
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1835289_2.6
Merge into: maas:2.6
Diff against target: 253 lines (+126/-50)
4 files modified
src/maasserver/api/commissioning_scripts.py (+6/-3)
src/maasserver/api/tests/test_commissioning.py (+39/-1)
src/maasserver/forms/script.py (+68/-46)
src/maasserver/forms/tests/test_script.py (+13/-0)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+369978@code.launchpad.net

Commit message

Backport of 36d5dd0 - LP: #1835289 - Fix error handling on the commissioning API.

The commissioning API was returning an exception instead or raising it. This
caused a 500 internal error with no readable error message. Fixing this
revealed the commissioning API cannot be used with embedded script YAML as
the commissioning API sets the script_type to commissioning. The form now
allows fields to be set as long as they match what is in the script YAML.

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/api/commissioning_scripts.py b/src/maasserver/api/commissioning_scripts.py
index 16791fc..906e3ee 100644
--- a/src/maasserver/api/commissioning_scripts.py
+++ b/src/maasserver/api/commissioning_scripts.py
@@ -1,4 +1,4 @@
1# Copyright 2014-2018 Canonical Ltd. This software is licensed under the1# Copyright 2014-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""API handlers: `CommissioningScript`."""4"""API handlers: `CommissioningScript`."""
@@ -92,11 +92,14 @@ class CommissioningScriptsHandler(OperationsHandler):
92 return {92 return {
93 'name': script.name,93 'name': script.name,
94 'content': b64encode(script.script.data.encode()),94 'content': b64encode(script.script.data.encode()),
95 'deprecated': (
96 'The commissioning-scripts endpoint is deprecated. '
97 'Please use the node-scripts endpoint.'),
95 'resource_uri': reverse(98 'resource_uri': reverse(
96 'commissioning_script_handler', args=[script.name]),99 'commissioning_script_handler', args=[script.name]),
97 }100 }
98 else:101 else:
99 return MAASAPIValidationError(form.errors)102 raise MAASAPIValidationError(form.errors)
100103
101 @classmethod104 @classmethod
102 def resource_uri(cls):105 def resource_uri(cls):
@@ -142,7 +145,7 @@ class CommissioningScriptHandler(OperationsHandler):
142 form.save(request)145 form.save(request)
143 return rc.ALL_OK146 return rc.ALL_OK
144 else:147 else:
145 return MAASAPIValidationError(form.errors)148 raise MAASAPIValidationError(form.errors)
146149
147 @classmethod150 @classmethod
148 def resource_uri(cls, script=None):151 def resource_uri(cls, script=None):
diff --git a/src/maasserver/api/tests/test_commissioning.py b/src/maasserver/api/tests/test_commissioning.py
index 1cfa07b..49dcce2 100644
--- a/src/maasserver/api/tests/test_commissioning.py
+++ b/src/maasserver/api/tests/test_commissioning.py
@@ -1,4 +1,4 @@
1# Copyright 2013-2018 Canonical Ltd. This software is licensed under the1# Copyright 2013-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the commissioning-related portions of the MAAS API."""4"""Tests for the commissioning-related portions of the MAAS API."""
@@ -74,6 +74,22 @@ class AdminCommissioningScriptsAPITest(APITestCase.ForAdmin):
74 self.assertEqual(content, stored_script.script.data)74 self.assertEqual(content, stored_script.script.data)
75 self.assertEqual(SCRIPT_TYPE.COMMISSIONING, stored_script.script_type)75 self.assertEqual(SCRIPT_TYPE.COMMISSIONING, stored_script.script_type)
7676
77 def test_POST_creates_errors(self):
78 content = factory.make_script_content(
79 yaml_content={'name': factory.make_name('name')})
80 response = self.client.post(
81 self.get_url(),
82 {
83 'name': factory.make_name('name'),
84 'content': factory.make_file_upload(content=content.encode()),
85 })
86 self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST))
87
88 ret = json_load_bytes(response.content)
89 self.assertDictEqual(
90 {'name': ['May not override values defined in embedded YAML.']},
91 ret)
92
7793
78class CommissioningScriptsAPITest(APITestCase.ForUser):94class CommissioningScriptsAPITest(APITestCase.ForUser):
7995
@@ -118,6 +134,28 @@ class AdminCommissioningScriptAPITest(APITestCase.ForAdmin):
118 self.assertEqual(134 self.assertEqual(
119 new_content.decode('utf-8'), reload_object(script).script.data)135 new_content.decode('utf-8'), reload_object(script).script.data)
120136
137 def test_PUT_errors(self):
138 old_content = factory.make_script_content(
139 yaml_content={'name': factory.make_name('name')})
140 old_content = old_content.encode('ascii')
141 script = factory.make_Script(
142 script_type=SCRIPT_TYPE.COMMISSIONING, script=old_content)
143 new_content = factory.make_script_content(
144 yaml_content={'name': factory.make_name('name')})
145 new_content = new_content.encode('ascii')
146
147 response = self.client.put(
148 self.get_url(script.name),
149 {
150 'name': factory.make_name('name'),
151 'content': factory.make_file_upload(content=new_content)})
152 self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST))
153
154 ret = json_load_bytes(response.content)
155 self.assertDictEqual(
156 {'name': ['May not override values defined in embedded YAML.']},
157 ret)
158
121 def test_DELETE_deletes_script(self):159 def test_DELETE_deletes_script(self):
122 script = factory.make_Script(script_type=SCRIPT_TYPE.COMMISSIONING)160 script = factory.make_Script(script_type=SCRIPT_TYPE.COMMISSIONING)
123 self.client.delete(self.get_url(script.name))161 self.client.delete(self.get_url(script.name))
diff --git a/src/maasserver/forms/script.py b/src/maasserver/forms/script.py
index 4555831..ab03f36 100644
--- a/src/maasserver/forms/script.py
+++ b/src/maasserver/forms/script.py
@@ -169,6 +169,73 @@ class ScriptForm(ModelForm):
169 valid = False169 valid = False
170 return valid170 return valid
171171
172 def _clean_script(self, parsed_yaml):
173 """Clean script data and validate input."""
174 # Tags and timeout may not be updated from new embedded YAML. This
175 # allows users to receive updated scripts from an upstream maintainer,
176 # such as Canonical, while maintaining user defined tags and timeout.
177
178 # Tags must be a comma seperated string for the form.
179 tags = parsed_yaml.pop('tags', None)
180 if (tags is not None and self.instance.id is None and
181 'tags' not in self.data):
182 tags_valid = True
183 if isinstance(tags, str):
184 self.data['tags'] = tags
185 elif isinstance(tags, list):
186 for tag in tags:
187 if not isinstance(tag, str):
188 tags_valid = False
189 continue
190 if tags_valid:
191 self.data['tags'] = ','.join(tags)
192 else:
193 tags_valid = False
194 if not tags_valid:
195 set_form_error(
196 self, 'tags',
197 'Embedded tags must be a string of comma seperated '
198 'values, or a list of strings.')
199
200 # Timeout must be a string for the form.
201 timeout = parsed_yaml.pop('timeout', None)
202 if (timeout is not None and self.instance.id is None and
203 'timeout' not in self.data):
204 self.data['timeout'] = str(timeout)
205
206 # Packages and for_hardware must be a JSON string for the form.
207 for key in ['packages', 'for_hardware']:
208 value = parsed_yaml.pop(key, None)
209 if value is not None and key not in self.data:
210 self.data[key] = json.dumps(value)
211
212 for key, value in parsed_yaml.items():
213 if key in self.fields:
214 error = False
215 if key not in self.data:
216 self.data[key] = value
217 elif key == 'script_type':
218 # The deprecated Commissioning API always sets the
219 # script_type to commissioning as it has always only
220 # accepted commissioning scripts while the form sets
221 # the default type to testing. If the YAML matches the
222 # type allow it.
223 try:
224 if (translate_script_type(value) !=
225 translate_script_type(self.data[key])):
226 error = True
227 except ValidationError:
228 error = True
229 elif value != self.data[key]:
230 # Only allow form data for fields defined in the YAML if
231 # the data matches.
232 error = True
233
234 if error:
235 set_form_error(
236 self, key,
237 'May not override values defined in embedded YAML.')
238
172 def _read_script(self):239 def _read_script(self):
173 """Read embedded YAML configuration in a script.240 """Read embedded YAML configuration in a script.
174241
@@ -224,52 +291,7 @@ class ScriptForm(ModelForm):
224 self.instance.results = parsed_yaml.pop('results', {})291 self.instance.results = parsed_yaml.pop('results', {})
225 self.instance.parameters = parsed_yaml.pop('parameters', {})292 self.instance.parameters = parsed_yaml.pop('parameters', {})
226293
227 # Tags and timeout may not be updated from new embedded YAML. This294 self._clean_script(parsed_yaml)
228 # allows users to receive updated scripts from an upstream maintainer,
229 # such as Canonical, while maintaining user defined tags and timeout.
230
231 # Tags must be a comma seperated string for the form.
232 tags = parsed_yaml.pop('tags', None)
233 if (tags is not None and self.instance.id is None and
234 'tags' not in self.data):
235 tags_valid = True
236 if isinstance(tags, str):
237 self.data['tags'] = tags
238 elif isinstance(tags, list):
239 for tag in tags:
240 if not isinstance(tag, str):
241 tags_valid = False
242 continue
243 if tags_valid:
244 self.data['tags'] = ','.join(tags)
245 else:
246 tags_valid = False
247 if not tags_valid:
248 set_form_error(
249 self, 'tags',
250 'Embedded tags must be a string of comma seperated '
251 'values, or a list of strings.')
252
253 # Timeout must be a string for the form.
254 timeout = parsed_yaml.pop('timeout', None)
255 if (timeout is not None and self.instance.id is None and
256 'timeout' not in self.data):
257 self.data['timeout'] = str(timeout)
258
259 # Packages and for_hardware must be a JSON string for the form.
260 for key in ['packages', 'for_hardware']:
261 value = parsed_yaml.pop(key, None)
262 if value is not None and key not in self.data:
263 self.data[key] = json.dumps(value)
264
265 for key, value in parsed_yaml.items():
266 if key in self.fields:
267 if key not in self.data:
268 self.data[key] = value
269 else:
270 set_form_error(
271 self, key,
272 'May not override values defined in embedded YAML.')
273295
274 def clean_packages(self):296 def clean_packages(self):
275 if self.cleaned_data['packages'] == '':297 if self.cleaned_data['packages'] == '':
diff --git a/src/maasserver/forms/tests/test_script.py b/src/maasserver/forms/tests/test_script.py
index b7df349..23f8c69 100644
--- a/src/maasserver/forms/tests/test_script.py
+++ b/src/maasserver/forms/tests/test_script.py
@@ -618,6 +618,19 @@ class TestScriptForm(MAASServerTestCase):
618 })618 })
619 self.assertFalse(form.is_valid())619 self.assertFalse(form.is_valid())
620620
621 def test__user_option_can_match_yaml_value(self):
622 name = factory.make_name('name')
623 script_type = factory.pick_choice(SCRIPT_TYPE_CHOICES)
624 form = ScriptForm(data={
625 'name': name,
626 'script_type': script_type,
627 'script': factory.make_script_content({
628 'name': name,
629 'script_type': script_type,
630 })
631 })
632 self.assertTrue(form.is_valid())
633
621 def test__errors_on_bad_yaml(self):634 def test__errors_on_bad_yaml(self):
622 form = ScriptForm(data={635 form = ScriptForm(data={
623 'name': factory.make_name('name'),636 'name': factory.make_name('name'),

Subscribers

People subscribed via source and target branches