Merge lp:~newell-jensen/maas/fix-bug-1378936 into lp:~maas-committers/maas/trunk
- fix-bug-1378936
- Merge into trunk
Proposed by
Newell Jensen
Status: | Merged |
---|---|
Approved by: | Newell Jensen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3270 |
Proposed branch: | lp:~newell-jensen/maas/fix-bug-1378936 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
378 lines (+109/-69) 4 files modified
src/maasserver/forms.py (+41/-21) src/maasserver/templates/maasserver/node_edit.html (+16/-14) src/maasserver/tests/test_forms_node.py (+43/-34) src/maasserver/views/nodes.py (+9/-0) |
To merge this branch: | bzr merge lp:~newell-jensen/maas/fix-bug-1378936 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+238375@code.launchpad.net |
Commit message
This branch implements the behavior that if the MAAS user wants to change the OS or Release of a node, it first must be acquired.
Description of the change
To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Revision history for this message
Newell Jensen (newell-jensen) wrote : | # |
Tested with self-packages and currently queued in CI.
Revision history for this message
Newell Jensen (newell-jensen) wrote : | # |
Merging with trunk, this passes CI.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : | # |
Sensible change. It may be worth mentioning that this should only affect administrators, because they're the only users who can edit nodes that have no owners.
I have some nitpicks inline, and one broader design question, with a suggested alternate implementation if it turns out to touch a weak spot.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/forms.py' |
2 | --- src/maasserver/forms.py 2014-10-14 11:22:28 +0000 |
3 | +++ src/maasserver/forms.py 2014-10-19 22:10:43 +0000 |
4 | @@ -327,8 +327,14 @@ |
5 | # a parameter named 'request' because it is used interchangingly |
6 | # with NodeAdminForm which actually uses this parameter. |
7 | |
8 | + instance = kwargs.get('instance') |
9 | + if instance is None or instance.owner is None: |
10 | + self.has_owner = False |
11 | + else: |
12 | + self.has_owner = True |
13 | + |
14 | # Are we creating a new node object? |
15 | - self.new_node = (kwargs.get('instance') is None) |
16 | + self.new_node = (instance is None) |
17 | if self.new_node: |
18 | # Offer choice of nodegroup. |
19 | self.fields['nodegroup'] = NodeGroupFormField( |
20 | @@ -349,7 +355,7 @@ |
21 | self.instance.nodegroup.nodegroupinterface_set.all()) |
22 | |
23 | self.set_up_architecture_field() |
24 | - self.set_up_osystem_and_distro_series_fields(kwargs.get('instance')) |
25 | + self.set_up_osystem_and_distro_series_fields(instance) |
26 | |
27 | if not allow_disable_ipv4: |
28 | # Hide the disable_ipv4 field until support works properly. The |
29 | @@ -361,6 +367,17 @@ |
30 | self.fields['disable_ipv4'] = forms.BooleanField( |
31 | label="", required=False, widget=forms.HiddenInput()) |
32 | |
33 | + # We only want the license key field to render in the UI if the `OS` |
34 | + # and `Release` fields are also present. |
35 | + if self.has_owner: |
36 | + self.fields['license_key'] = forms.CharField( |
37 | + label="License Key", required=False, help_text=( |
38 | + "License key for operating system"), |
39 | + max_length=30) |
40 | + else: |
41 | + self.fields['license_key'] = forms.CharField( |
42 | + label="", required=False, widget=forms.HiddenInput()) |
43 | + |
44 | def set_up_architecture_field(self): |
45 | """Create the `architecture` field. |
46 | |
47 | @@ -387,19 +404,26 @@ |
48 | """ |
49 | osystems = list_all_usable_osystems() |
50 | releases = list_all_usable_releases(osystems) |
51 | - os_choices = list_osystem_choices(osystems) |
52 | - distro_choices = list_release_choices(releases) |
53 | - invalid_osystem_message = compose_invalid_choice_text( |
54 | - 'osystem', os_choices) |
55 | - invalid_distro_series_message = compose_invalid_choice_text( |
56 | - 'distro_series', distro_choices) |
57 | - self.fields['osystem'] = forms.ChoiceField( |
58 | - label="OS", choices=os_choices, required=False, initial='', |
59 | - error_messages={'invalid_choice': invalid_osystem_message}) |
60 | - self.fields['distro_series'] = forms.ChoiceField( |
61 | - label="Release", choices=distro_choices, |
62 | - required=False, initial='', |
63 | - error_messages={'invalid_choice': invalid_distro_series_message}) |
64 | + if self.has_owner: |
65 | + os_choices = list_osystem_choices(osystems) |
66 | + distro_choices = list_release_choices(releases) |
67 | + invalid_osystem_message = compose_invalid_choice_text( |
68 | + 'osystem', os_choices) |
69 | + invalid_distro_series_message = compose_invalid_choice_text( |
70 | + 'distro_series', distro_choices) |
71 | + self.fields['osystem'] = forms.ChoiceField( |
72 | + label="OS", choices=os_choices, required=False, initial='', |
73 | + error_messages={'invalid_choice': invalid_osystem_message}) |
74 | + self.fields['distro_series'] = forms.ChoiceField( |
75 | + label="Release", choices=distro_choices, |
76 | + required=False, initial='', |
77 | + error_messages={ |
78 | + 'invalid_choice': invalid_distro_series_message}) |
79 | + else: |
80 | + self.fields['osystem'] = forms.ChoiceField( |
81 | + label="", required=False, widget=forms.HiddenInput()) |
82 | + self.fields['distro_series'] = forms.ChoiceField( |
83 | + label="", required=False, widget=forms.HiddenInput()) |
84 | if instance is not None: |
85 | initial_value = get_distro_series_initial(osystems, instance) |
86 | if instance is not None: |
87 | @@ -546,11 +570,6 @@ |
88 | "does not manage DNS, then the host name as entered will be the " |
89 | "FQDN.")) |
90 | |
91 | - license_key = forms.CharField( |
92 | - label="License Key", required=False, help_text=( |
93 | - "License key for operating system"), |
94 | - max_length=30) |
95 | - |
96 | class Meta: |
97 | model = Node |
98 | |
99 | @@ -2342,7 +2361,8 @@ |
100 | error_messages={'invalid_choice': invalid_osystem_message}) |
101 | self.fields['distro_series'] = forms.ChoiceField( |
102 | label="Release", choices=distro_choices, required=True, |
103 | - error_messages={'invalid_choice': invalid_distro_series_message}) |
104 | + error_messages={ |
105 | + 'invalid_choice': invalid_distro_series_message}) |
106 | if instance is not None: |
107 | initial_value = get_distro_series_initial( |
108 | osystems, instance, with_key_required=False) |
109 | |
110 | === modified file 'src/maasserver/templates/maasserver/node_edit.html' |
111 | --- src/maasserver/templates/maasserver/node_edit.html 2014-09-29 13:21:42 +0000 |
112 | +++ src/maasserver/templates/maasserver/node_edit.html 2014-10-19 22:10:43 +0000 |
113 | @@ -16,20 +16,22 @@ |
114 | Y.on('load', function() { |
115 | // Create OSDistroWidget so that the release field will be |
116 | // updated each time the value of the os field changes. |
117 | - var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({ |
118 | - srcNode: '#id_distro_series' |
119 | - }); |
120 | - releaseWidget.bindTo(Y.one('.osystem').one('select'), 'change'); |
121 | - releaseWidget.render(); |
122 | - |
123 | - // Create LicenseKeyWidget so that the license key field will be |
124 | - // show when a release is selected that requires it. |
125 | - var licenseKeyWidget = new Y.maas.license_key.LicenseKeyWidget({ |
126 | - srcNode: '.license_key' |
127 | - }); |
128 | - licenseKeyWidget.bindTo(Y.one('.distro_series').one('select'), 'change'); |
129 | - licenseKeyWidget.render(); |
130 | - |
131 | + var os_release = {{ os_release }}; |
132 | + if (os_release) { |
133 | + var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({ |
134 | + srcNode: '#id_distro_series' |
135 | + }); |
136 | + releaseWidget.bindTo(Y.one('.osystem').one('select'), 'change'); |
137 | + releaseWidget.render(); |
138 | + |
139 | + // Create LicenseKeyWidget so that the license key field will be |
140 | + // show when a release is selected that requires it. |
141 | + var licenseKeyWidget = new Y.maas.license_key.LicenseKeyWidget({ |
142 | + srcNode: '.license_key' |
143 | + }); |
144 | + licenseKeyWidget.bindTo(Y.one('.distro_series').one('select'), 'change'); |
145 | + licenseKeyWidget.render(); |
146 | + } |
147 | // Create LinkedContentWidget widget so that the power_parameter field |
148 | // will be updated each time the value of the power_type field changes. |
149 | var power_types = {{ power_types }}; |
150 | |
151 | === modified file 'src/maasserver/tests/test_forms_node.py' |
152 | --- src/maasserver/tests/test_forms_node.py 2014-10-03 04:53:51 +0000 |
153 | +++ src/maasserver/tests/test_forms_node.py 2014-10-19 22:10:43 +0000 |
154 | @@ -69,7 +69,7 @@ |
155 | 'distro_series', |
156 | 'license_key', |
157 | 'disable_ipv4', |
158 | - 'nodegroup', |
159 | + 'nodegroup' |
160 | ], list(form.fields)) |
161 | |
162 | def test_changes_node(self): |
163 | @@ -128,33 +128,43 @@ |
164 | form.errors['architecture']) |
165 | |
166 | def test_accepts_osystem(self): |
167 | + self.client_log_in() |
168 | + node = factory.make_Node(owner=self.logged_in_user) |
169 | osystem = make_usable_osystem(self) |
170 | form = NodeForm(data={ |
171 | 'hostname': factory.make_name('host'), |
172 | 'architecture': make_usable_architecture(self), |
173 | 'osystem': osystem['name'], |
174 | - }) |
175 | + }, |
176 | + instance=node) |
177 | self.assertTrue(form.is_valid(), form._errors) |
178 | |
179 | def test_rejects_invalid_osystem(self): |
180 | + self.client_log_in() |
181 | + node = factory.make_Node(owner=self.logged_in_user) |
182 | patch_usable_osystems(self) |
183 | form = NodeForm(data={ |
184 | 'hostname': factory.make_name('host'), |
185 | 'architecture': make_usable_architecture(self), |
186 | 'osystem': factory.make_name('os'), |
187 | - }) |
188 | + }, |
189 | + instance=node) |
190 | self.assertFalse(form.is_valid()) |
191 | self.assertItemsEqual(['osystem'], form._errors.keys()) |
192 | |
193 | def test_starts_with_default_osystem(self): |
194 | + self.client_log_in() |
195 | + node = factory.make_Node(owner=self.logged_in_user) |
196 | osystems = [make_osystem_with_releases(self) for _ in range(5)] |
197 | patch_usable_osystems(self, osystems) |
198 | - form = NodeForm() |
199 | + form = NodeForm(instance=node) |
200 | self.assertEqual( |
201 | '', |
202 | form.fields['osystem'].initial) |
203 | |
204 | def test_accepts_osystem_distro_series(self): |
205 | + self.client_log_in() |
206 | + node = factory.make_Node(owner=self.logged_in_user) |
207 | osystem = make_usable_osystem(self) |
208 | release = osystem['default_release'] |
209 | form = NodeForm(data={ |
210 | @@ -162,10 +172,13 @@ |
211 | 'architecture': make_usable_architecture(self), |
212 | 'osystem': osystem['name'], |
213 | 'distro_series': '%s/%s' % (osystem['name'], release), |
214 | - }) |
215 | + }, |
216 | + instance=node) |
217 | self.assertTrue(form.is_valid(), form._errors) |
218 | |
219 | def test_rejects_invalid_osystem_distro_series(self): |
220 | + self.client_log_in() |
221 | + node = factory.make_Node(owner=self.logged_in_user) |
222 | osystem = make_usable_osystem(self) |
223 | release = factory.make_name('release') |
224 | form = NodeForm(data={ |
225 | @@ -173,19 +186,24 @@ |
226 | 'architecture': make_usable_architecture(self), |
227 | 'osystem': osystem['name'], |
228 | 'distro_series': '%s/%s' % (osystem['name'], release), |
229 | - }) |
230 | + }, |
231 | + instance=node) |
232 | self.assertFalse(form.is_valid()) |
233 | self.assertItemsEqual(['distro_series'], form._errors.keys()) |
234 | |
235 | def test_starts_with_default_distro_series(self): |
236 | + self.client_log_in() |
237 | + node = factory.make_Node(owner=self.logged_in_user) |
238 | osystems = [make_osystem_with_releases(self) for _ in range(5)] |
239 | patch_usable_osystems(self, osystems) |
240 | - form = NodeForm() |
241 | + form = NodeForm(instance=node) |
242 | self.assertEqual( |
243 | '', |
244 | form.fields['distro_series'].initial) |
245 | |
246 | def test_rejects_mismatch_osystem_distro_series(self): |
247 | + self.client_log_in() |
248 | + node = factory.make_Node(owner=self.logged_in_user) |
249 | osystem = make_usable_osystem(self) |
250 | release = osystem['default_release'] |
251 | invalid = factory.make_name('invalid_os') |
252 | @@ -194,30 +212,14 @@ |
253 | 'architecture': make_usable_architecture(self), |
254 | 'osystem': osystem['name'], |
255 | 'distro_series': '%s/%s' % (invalid, release), |
256 | - }) |
257 | + }, |
258 | + instance=node) |
259 | self.assertFalse(form.is_valid()) |
260 | self.assertItemsEqual(['distro_series'], form._errors.keys()) |
261 | |
262 | - def test_calls_validate_license_key_without_nodegroup(self): |
263 | - release = make_rpc_release(requires_license_key=True) |
264 | - osystem = make_rpc_osystem(releases=[release]) |
265 | - patch_usable_osystems(self, osystems=[osystem]) |
266 | - license_key = factory.make_name('key') |
267 | - mock_validate = self.patch(forms, 'validate_license_key') |
268 | - mock_validate.return_value = True |
269 | - form = NodeForm(data={ |
270 | - 'hostname': factory.make_name('host'), |
271 | - 'architecture': make_usable_architecture(self), |
272 | - 'osystem': osystem['name'], |
273 | - 'distro_series': '%s/%s*' % (osystem['name'], release['name']), |
274 | - 'license_key': license_key, |
275 | - }) |
276 | - self.assertTrue(form.is_valid()) |
277 | - self.assertThat( |
278 | - mock_validate, |
279 | - MockCalledOnceWith(osystem['name'], release['name'], license_key)) |
280 | - |
281 | def test_rejects_when_validate_license_key_returns_False(self): |
282 | + self.client_log_in() |
283 | + node = factory.make_Node(owner=self.logged_in_user) |
284 | release = make_rpc_release(requires_license_key=True) |
285 | osystem = make_rpc_osystem(releases=[release]) |
286 | patch_usable_osystems(self, osystems=[osystem]) |
287 | @@ -230,12 +232,14 @@ |
288 | 'osystem': osystem['name'], |
289 | 'distro_series': '%s/%s*' % (osystem['name'], release['name']), |
290 | 'license_key': license_key, |
291 | - }) |
292 | + }, |
293 | + instance=node) |
294 | self.assertFalse(form.is_valid()) |
295 | self.assertItemsEqual(['license_key'], form._errors.keys()) |
296 | |
297 | def test_calls_validate_license_key_for_with_nodegroup(self): |
298 | - node = factory.make_Node() |
299 | + self.client_log_in() |
300 | + node = factory.make_Node(owner=self.logged_in_user) |
301 | release = make_rpc_release(requires_license_key=True) |
302 | osystem = make_rpc_osystem(releases=[release]) |
303 | patch_usable_osystems(self, osystems=[osystem]) |
304 | @@ -256,7 +260,8 @@ |
305 | node.nodegroup, osystem['name'], release['name'], license_key)) |
306 | |
307 | def test_rejects_when_validate_license_key_for_returns_False(self): |
308 | - node = factory.make_Node() |
309 | + self.client_log_in() |
310 | + node = factory.make_Node(owner=self.logged_in_user) |
311 | release = make_rpc_release(requires_license_key=True) |
312 | osystem = make_rpc_osystem(releases=[release]) |
313 | patch_usable_osystems(self, osystems=[osystem]) |
314 | @@ -274,7 +279,8 @@ |
315 | self.assertItemsEqual(['license_key'], form._errors.keys()) |
316 | |
317 | def test_rejects_when_validate_license_key_for_raise_no_connection(self): |
318 | - node = factory.make_Node() |
319 | + self.client_log_in() |
320 | + node = factory.make_Node(owner=self.logged_in_user) |
321 | release = make_rpc_release(requires_license_key=True) |
322 | osystem = make_rpc_osystem(releases=[release]) |
323 | patch_usable_osystems(self, osystems=[osystem]) |
324 | @@ -292,7 +298,8 @@ |
325 | self.assertItemsEqual(['license_key'], form._errors.keys()) |
326 | |
327 | def test_rejects_when_validate_license_key_for_raise_timeout(self): |
328 | - node = factory.make_Node() |
329 | + self.client_log_in() |
330 | + node = factory.make_Node(owner=self.logged_in_user) |
331 | release = make_rpc_release(requires_license_key=True) |
332 | osystem = make_rpc_osystem(releases=[release]) |
333 | patch_usable_osystems(self, osystems=[osystem]) |
334 | @@ -310,7 +317,8 @@ |
335 | self.assertItemsEqual(['license_key'], form._errors.keys()) |
336 | |
337 | def test_rejects_when_validate_license_key_for_raise_no_os(self): |
338 | - node = factory.make_Node() |
339 | + self.client_log_in() |
340 | + node = factory.make_Node(owner=self.logged_in_user) |
341 | release = make_rpc_release(requires_license_key=True) |
342 | osystem = make_rpc_osystem(releases=[release]) |
343 | patch_usable_osystems(self, osystems=[osystem]) |
344 | @@ -472,7 +480,8 @@ |
345 | class TestAdminNodeForm(MAASServerTestCase): |
346 | |
347 | def test_AdminNodeForm_contains_limited_set_of_fields(self): |
348 | - node = factory.make_Node() |
349 | + self.client_log_in() |
350 | + node = factory.make_Node(owner=self.logged_in_user) |
351 | form = AdminNodeForm(instance=node) |
352 | |
353 | self.assertEqual( |
354 | |
355 | === modified file 'src/maasserver/views/nodes.py' |
356 | --- src/maasserver/views/nodes.py 2014-10-10 17:15:45 +0000 |
357 | +++ src/maasserver/views/nodes.py 2014-10-19 22:10:43 +0000 |
358 | @@ -658,6 +658,12 @@ |
359 | def get_form_class(self): |
360 | return get_node_edit_form(self.request.user) |
361 | |
362 | + def get_has_owner(self): |
363 | + node = self.get_object() |
364 | + if node is None or node.owner is None: |
365 | + return mark_safe("false") |
366 | + return mark_safe("true") |
367 | + |
368 | def get_form_kwargs(self): |
369 | # This is here so the request can be passed to the form. The |
370 | # form needs it because it sets error messages for the UI. |
371 | @@ -673,6 +679,9 @@ |
372 | context = super(NodeEdit, self).get_context_data(**kwargs) |
373 | context['power_types'] = generate_js_power_types( |
374 | self.get_object().nodegroup) |
375 | + # 'os_release' lets us know if we should render the `OS` |
376 | + # and `Release` choice fields in the UI. |
377 | + context['os_release'] = self.get_has_owner() |
378 | return context |
379 | |
380 |
I know this is WIP and you said you are having API issues, but just wanted to give the code a look over. Looks good, lets talk to day about the API.