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
1diff --git a/src/maasserver/api/commissioning_scripts.py b/src/maasserver/api/commissioning_scripts.py
2index 16791fc..906e3ee 100644
3--- a/src/maasserver/api/commissioning_scripts.py
4+++ b/src/maasserver/api/commissioning_scripts.py
5@@ -1,4 +1,4 @@
6-# Copyright 2014-2018 Canonical Ltd. This software is licensed under the
7+# Copyright 2014-2019 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """API handlers: `CommissioningScript`."""
11@@ -92,11 +92,14 @@ class CommissioningScriptsHandler(OperationsHandler):
12 return {
13 'name': script.name,
14 'content': b64encode(script.script.data.encode()),
15+ 'deprecated': (
16+ 'The commissioning-scripts endpoint is deprecated. '
17+ 'Please use the node-scripts endpoint.'),
18 'resource_uri': reverse(
19 'commissioning_script_handler', args=[script.name]),
20 }
21 else:
22- return MAASAPIValidationError(form.errors)
23+ raise MAASAPIValidationError(form.errors)
24
25 @classmethod
26 def resource_uri(cls):
27@@ -142,7 +145,7 @@ class CommissioningScriptHandler(OperationsHandler):
28 form.save(request)
29 return rc.ALL_OK
30 else:
31- return MAASAPIValidationError(form.errors)
32+ raise MAASAPIValidationError(form.errors)
33
34 @classmethod
35 def resource_uri(cls, script=None):
36diff --git a/src/maasserver/api/tests/test_commissioning.py b/src/maasserver/api/tests/test_commissioning.py
37index 1cfa07b..49dcce2 100644
38--- a/src/maasserver/api/tests/test_commissioning.py
39+++ b/src/maasserver/api/tests/test_commissioning.py
40@@ -1,4 +1,4 @@
41-# Copyright 2013-2018 Canonical Ltd. This software is licensed under the
42+# Copyright 2013-2019 Canonical Ltd. This software is licensed under the
43 # GNU Affero General Public License version 3 (see the file LICENSE).
44
45 """Tests for the commissioning-related portions of the MAAS API."""
46@@ -74,6 +74,22 @@ class AdminCommissioningScriptsAPITest(APITestCase.ForAdmin):
47 self.assertEqual(content, stored_script.script.data)
48 self.assertEqual(SCRIPT_TYPE.COMMISSIONING, stored_script.script_type)
49
50+ def test_POST_creates_errors(self):
51+ content = factory.make_script_content(
52+ yaml_content={'name': factory.make_name('name')})
53+ response = self.client.post(
54+ self.get_url(),
55+ {
56+ 'name': factory.make_name('name'),
57+ 'content': factory.make_file_upload(content=content.encode()),
58+ })
59+ self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST))
60+
61+ ret = json_load_bytes(response.content)
62+ self.assertDictEqual(
63+ {'name': ['May not override values defined in embedded YAML.']},
64+ ret)
65+
66
67 class CommissioningScriptsAPITest(APITestCase.ForUser):
68
69@@ -118,6 +134,28 @@ class AdminCommissioningScriptAPITest(APITestCase.ForAdmin):
70 self.assertEqual(
71 new_content.decode('utf-8'), reload_object(script).script.data)
72
73+ def test_PUT_errors(self):
74+ old_content = factory.make_script_content(
75+ yaml_content={'name': factory.make_name('name')})
76+ old_content = old_content.encode('ascii')
77+ script = factory.make_Script(
78+ script_type=SCRIPT_TYPE.COMMISSIONING, script=old_content)
79+ new_content = factory.make_script_content(
80+ yaml_content={'name': factory.make_name('name')})
81+ new_content = new_content.encode('ascii')
82+
83+ response = self.client.put(
84+ self.get_url(script.name),
85+ {
86+ 'name': factory.make_name('name'),
87+ 'content': factory.make_file_upload(content=new_content)})
88+ self.assertThat(response, HasStatusCode(http.client.BAD_REQUEST))
89+
90+ ret = json_load_bytes(response.content)
91+ self.assertDictEqual(
92+ {'name': ['May not override values defined in embedded YAML.']},
93+ ret)
94+
95 def test_DELETE_deletes_script(self):
96 script = factory.make_Script(script_type=SCRIPT_TYPE.COMMISSIONING)
97 self.client.delete(self.get_url(script.name))
98diff --git a/src/maasserver/forms/script.py b/src/maasserver/forms/script.py
99index 4555831..ab03f36 100644
100--- a/src/maasserver/forms/script.py
101+++ b/src/maasserver/forms/script.py
102@@ -169,6 +169,73 @@ class ScriptForm(ModelForm):
103 valid = False
104 return valid
105
106+ def _clean_script(self, parsed_yaml):
107+ """Clean script data and validate input."""
108+ # Tags and timeout may not be updated from new embedded YAML. This
109+ # allows users to receive updated scripts from an upstream maintainer,
110+ # such as Canonical, while maintaining user defined tags and timeout.
111+
112+ # Tags must be a comma seperated string for the form.
113+ tags = parsed_yaml.pop('tags', None)
114+ if (tags is not None and self.instance.id is None and
115+ 'tags' not in self.data):
116+ tags_valid = True
117+ if isinstance(tags, str):
118+ self.data['tags'] = tags
119+ elif isinstance(tags, list):
120+ for tag in tags:
121+ if not isinstance(tag, str):
122+ tags_valid = False
123+ continue
124+ if tags_valid:
125+ self.data['tags'] = ','.join(tags)
126+ else:
127+ tags_valid = False
128+ if not tags_valid:
129+ set_form_error(
130+ self, 'tags',
131+ 'Embedded tags must be a string of comma seperated '
132+ 'values, or a list of strings.')
133+
134+ # Timeout must be a string for the form.
135+ timeout = parsed_yaml.pop('timeout', None)
136+ if (timeout is not None and self.instance.id is None and
137+ 'timeout' not in self.data):
138+ self.data['timeout'] = str(timeout)
139+
140+ # Packages and for_hardware must be a JSON string for the form.
141+ for key in ['packages', 'for_hardware']:
142+ value = parsed_yaml.pop(key, None)
143+ if value is not None and key not in self.data:
144+ self.data[key] = json.dumps(value)
145+
146+ for key, value in parsed_yaml.items():
147+ if key in self.fields:
148+ error = False
149+ if key not in self.data:
150+ self.data[key] = value
151+ elif key == 'script_type':
152+ # The deprecated Commissioning API always sets the
153+ # script_type to commissioning as it has always only
154+ # accepted commissioning scripts while the form sets
155+ # the default type to testing. If the YAML matches the
156+ # type allow it.
157+ try:
158+ if (translate_script_type(value) !=
159+ translate_script_type(self.data[key])):
160+ error = True
161+ except ValidationError:
162+ error = True
163+ elif value != self.data[key]:
164+ # Only allow form data for fields defined in the YAML if
165+ # the data matches.
166+ error = True
167+
168+ if error:
169+ set_form_error(
170+ self, key,
171+ 'May not override values defined in embedded YAML.')
172+
173 def _read_script(self):
174 """Read embedded YAML configuration in a script.
175
176@@ -224,52 +291,7 @@ class ScriptForm(ModelForm):
177 self.instance.results = parsed_yaml.pop('results', {})
178 self.instance.parameters = parsed_yaml.pop('parameters', {})
179
180- # Tags and timeout may not be updated from new embedded YAML. This
181- # allows users to receive updated scripts from an upstream maintainer,
182- # such as Canonical, while maintaining user defined tags and timeout.
183-
184- # Tags must be a comma seperated string for the form.
185- tags = parsed_yaml.pop('tags', None)
186- if (tags is not None and self.instance.id is None and
187- 'tags' not in self.data):
188- tags_valid = True
189- if isinstance(tags, str):
190- self.data['tags'] = tags
191- elif isinstance(tags, list):
192- for tag in tags:
193- if not isinstance(tag, str):
194- tags_valid = False
195- continue
196- if tags_valid:
197- self.data['tags'] = ','.join(tags)
198- else:
199- tags_valid = False
200- if not tags_valid:
201- set_form_error(
202- self, 'tags',
203- 'Embedded tags must be a string of comma seperated '
204- 'values, or a list of strings.')
205-
206- # Timeout must be a string for the form.
207- timeout = parsed_yaml.pop('timeout', None)
208- if (timeout is not None and self.instance.id is None and
209- 'timeout' not in self.data):
210- self.data['timeout'] = str(timeout)
211-
212- # Packages and for_hardware must be a JSON string for the form.
213- for key in ['packages', 'for_hardware']:
214- value = parsed_yaml.pop(key, None)
215- if value is not None and key not in self.data:
216- self.data[key] = json.dumps(value)
217-
218- for key, value in parsed_yaml.items():
219- if key in self.fields:
220- if key not in self.data:
221- self.data[key] = value
222- else:
223- set_form_error(
224- self, key,
225- 'May not override values defined in embedded YAML.')
226+ self._clean_script(parsed_yaml)
227
228 def clean_packages(self):
229 if self.cleaned_data['packages'] == '':
230diff --git a/src/maasserver/forms/tests/test_script.py b/src/maasserver/forms/tests/test_script.py
231index b7df349..23f8c69 100644
232--- a/src/maasserver/forms/tests/test_script.py
233+++ b/src/maasserver/forms/tests/test_script.py
234@@ -618,6 +618,19 @@ class TestScriptForm(MAASServerTestCase):
235 })
236 self.assertFalse(form.is_valid())
237
238+ def test__user_option_can_match_yaml_value(self):
239+ name = factory.make_name('name')
240+ script_type = factory.pick_choice(SCRIPT_TYPE_CHOICES)
241+ form = ScriptForm(data={
242+ 'name': name,
243+ 'script_type': script_type,
244+ 'script': factory.make_script_content({
245+ 'name': name,
246+ 'script_type': script_type,
247+ })
248+ })
249+ self.assertTrue(form.is_valid())
250+
251 def test__errors_on_bad_yaml(self):
252 form = ScriptForm(data={
253 'name': factory.make_name('name'),

Subscribers

People subscribed via source and target branches