Merge lp:~newell-jensen/maas/fix-bug-1378936 into lp:~maas-committers/maas/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
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.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

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.

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