Merge ~ltrager/maas:lp1835289 into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 324b243fe4ee59d66f451fd5a473187e6917e1ae
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1835289
Merge into: maas:master
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
Newell Jensen (community) Approve
Review via email: mp+369863@code.launchpad.net

Commit message

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
Newell Jensen (newell-jensen) wrote :

Since this endpoint is deprecated, why are we not raising a message that says this when an admin tries using it?

review: Needs Information
Revision history for this message
Lee Trager (ltrager) wrote :

When I added the node-scripts API end point we decided that the commissioning-scripts end point had to be kept around for API compatibility. If we ever bump the API version we'd remove it.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

I think simply raising a message letting the user know they are using a deprecated endpoint and pointing them to its successor is a better overall user experience and gets them using all the bells and whistles of supported endpoint. Having a message doesn't break API compatibility as those endpoints are still reachable.

Not going to block you on this though but would be good to get another team members opinion on this. As we will be bumping up the API version at some point in the future and cleaning all of this up, seems like that would have been the easier coarse of action.

review: Approve
~ltrager/maas:lp1835289 updated
c939af6... by Lee Trager

Merge branch 'master' into lp1835289

324b243... by Lee Trager

Add deprecation warning

Revision history for this message
Lee Trager (ltrager) wrote :

As discussed in the SU I added a warning that the commissioning-scripts endpoint is deprecated. Unfortunatly I can only do this with the output when a commissioning script is created. reading or updating a script does not allow me to add the message without breaking API compatibility.

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 da95d3c..e32e6fc 100644
100--- a/src/maasserver/forms/script.py
101+++ b/src/maasserver/forms/script.py
102@@ -173,6 +173,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@@ -228,52 +295,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 b67a568..859d802 100644
232--- a/src/maasserver/forms/tests/test_script.py
233+++ b/src/maasserver/forms/tests/test_script.py
234@@ -622,6 +622,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