Merge lp:~blake-rouse/maas/fix-1598175 into lp:~maas-committers/maas/trunk
- fix-1598175
- Merge into trunk
Proposed by
Blake Rouse
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5494 |
Proposed branch: | lp:~blake-rouse/maas/fix-1598175 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
644 lines (+337/-23) 13 files modified
src/maasserver/api/blockdevices.py (+24/-9) src/maasserver/api/interfaces.py (+25/-3) src/maasserver/api/tests/test_blockdevice.py (+26/-0) src/maasserver/api/tests/test_interfaces.py (+24/-2) src/maasserver/forms.py (+16/-0) src/maasserver/forms_interface.py (+11/-0) src/maasserver/static/js/angular/controllers/node_details_networking.js (+35/-0) src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+92/-0) src/maasserver/static/partials/node-details.html (+8/-8) src/maasserver/tests/test_forms_blockdevice.py (+35/-0) src/maasserver/tests/test_forms_interface.py (+22/-0) src/maasserver/websockets/handlers/machine.py (+5/-1) src/maasserver/websockets/handlers/tests/test_machine.py (+14/-0) |
To merge this branch: | bzr merge lp:~blake-rouse/maas/fix-1598175 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+308666@code.launchpad.net |
Commit message
Allow changing the name and MAC address of an interface on a deployed machine. Allow changing the name, model, serial, and id_path of a physical block device on a deployed machine.
Description of the change
To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) : | # |
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Fixed issues and updated the docstrings. This is ready for another review.
Revision history for this message
Mike Pontillo (mpontillo) wrote : | # |
Looks good. Thanks for refactoring the ng-if statements; it's much clearer now.
Just one comment about the docstring below.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api/blockdevices.py' |
2 | --- src/maasserver/api/blockdevices.py 2016-08-18 17:31:05 +0000 |
3 | +++ src/maasserver/api/blockdevices.py 2016-10-19 18:09:00 +0000 |
4 | @@ -22,6 +22,7 @@ |
5 | from maasserver.forms import ( |
6 | CreatePhysicalBlockDeviceForm, |
7 | FormatBlockDeviceForm, |
8 | + UpdateDeployedPhysicalBlockDeviceForm, |
9 | UpdatePhysicalBlockDeviceForm, |
10 | UpdateVirtualBlockDeviceForm, |
11 | ) |
12 | @@ -227,6 +228,11 @@ |
13 | def update(self, request, system_id, device_id): |
14 | """Update block device on a machine. |
15 | |
16 | + Machines must have a status of Ready to have access to all options. |
17 | + Machines with Deployed status can only have the name, model, serial, |
18 | + and/or id_path updated for a block device. This is intented to allow a |
19 | + bad block device to be replaced while the machine remains deployed. |
20 | + |
21 | Fields for physical block device: |
22 | |
23 | :param name: Name of the block device. |
24 | @@ -252,18 +258,27 @@ |
25 | device = BlockDevice.objects.get_block_device_or_404( |
26 | system_id, device_id, request.user, NODE_PERMISSION.ADMIN) |
27 | node = device.get_node() |
28 | - if node.status != NODE_STATUS.READY: |
29 | + if node.status not in [NODE_STATUS.READY, NODE_STATUS.DEPLOYED]: |
30 | raise NodeStateViolation( |
31 | "Cannot update block device because the machine is not Ready.") |
32 | - if device.type == 'physical': |
33 | - form = UpdatePhysicalBlockDeviceForm( |
34 | - instance=device, data=request.data) |
35 | - elif device.type == 'virtual': |
36 | - form = UpdateVirtualBlockDeviceForm( |
37 | - instance=device, data=request.data) |
38 | + if node.status == NODE_STATUS.DEPLOYED: |
39 | + if device.type == 'physical': |
40 | + form = UpdateDeployedPhysicalBlockDeviceForm( |
41 | + instance=device, data=request.data) |
42 | + else: |
43 | + raise NodeStateViolation( |
44 | + "Cannot update virtual block device because the machine " |
45 | + "is Deployed.") |
46 | else: |
47 | - raise ValueError( |
48 | - 'Cannot update block device of type %s' % device.type) |
49 | + if device.type == 'physical': |
50 | + form = UpdatePhysicalBlockDeviceForm( |
51 | + instance=device, data=request.data) |
52 | + elif device.type == 'virtual': |
53 | + form = UpdateVirtualBlockDeviceForm( |
54 | + instance=device, data=request.data) |
55 | + else: |
56 | + raise ValueError( |
57 | + 'Cannot update block device of type %s' % device.type) |
58 | if form.is_valid(): |
59 | return form.save() |
60 | else: |
61 | |
62 | === modified file 'src/maasserver/api/interfaces.py' |
63 | --- src/maasserver/api/interfaces.py 2016-08-18 17:31:05 +0000 |
64 | +++ src/maasserver/api/interfaces.py 2016-10-19 18:09:00 +0000 |
65 | @@ -26,6 +26,7 @@ |
66 | BondInterfaceForm, |
67 | BridgeInterfaceForm, |
68 | ControllerInterfaceForm, |
69 | + DeployedInterfaceForm, |
70 | InterfaceForm, |
71 | PhysicalInterfaceForm, |
72 | VLANInterfaceForm, |
73 | @@ -73,8 +74,21 @@ |
74 | |
75 | |
76 | def raise_error_for_invalid_state_on_allocated_operations( |
77 | - node, user, operation): |
78 | - if node.status not in ALLOWED_STATES: |
79 | + node, user, operation, extra_states=None): |
80 | + """Raises `NodeStateViolation` when the status of the node is not |
81 | + READY or BROKEN. |
82 | + |
83 | + :param node: Node to check status. |
84 | + :param user: User performing the operation. |
85 | + :param operation: Nice name of the operation. |
86 | + :param extra_states: Extra states that the node can be in when checking |
87 | + the status of the node. |
88 | + :type extra_states: `Iterable`. |
89 | + """ |
90 | + allowed = list(ALLOWED_STATES) |
91 | + if extra_states is not None: |
92 | + allowed.extend(extra_states) |
93 | + if node.status not in allowed: |
94 | raise NodeStateViolation( |
95 | "Cannot %s interface because the machine is not Ready or " |
96 | "Broken." % operation) |
97 | @@ -378,6 +392,11 @@ |
98 | def update(self, request, system_id, interface_id): |
99 | """Update interface on node. |
100 | |
101 | + Machines must has status of Ready or Broken to have access to all |
102 | + options. Machines with Deployed status can only have the name and/or |
103 | + mac_address updated for an interface. This is intented to allow a bad |
104 | + interface to be replaced while the machine remains deployed. |
105 | + |
106 | Fields for physical interface: |
107 | |
108 | :param name: Name of the interface. |
109 | @@ -477,12 +496,15 @@ |
110 | # This node needs to be in the correct state to modify |
111 | # the interface. |
112 | raise_error_for_invalid_state_on_allocated_operations( |
113 | - node, request.user, "update interface") |
114 | + node, request.user, "update interface", |
115 | + extra_states=[NODE_STATUS.DEPLOYED]) |
116 | if node.is_controller: |
117 | if interface.type == INTERFACE_TYPE.VLAN: |
118 | raise MAASAPIForbidden( |
119 | "Cannot modify VLAN interface on controller.") |
120 | interface_form = ControllerInterfaceForm |
121 | + elif node.status == NODE_STATUS.DEPLOYED: |
122 | + interface_form = DeployedInterfaceForm |
123 | else: |
124 | interface_form = InterfaceForm.get_interface_form(interface.type) |
125 | # For VLAN interface we cast parents to parent. As a VLAN can only |
126 | |
127 | === modified file 'src/maasserver/api/tests/test_blockdevice.py' |
128 | --- src/maasserver/api/tests/test_blockdevice.py 2016-08-14 17:36:30 +0000 |
129 | +++ src/maasserver/api/tests/test_blockdevice.py 2016-10-19 18:09:00 +0000 |
130 | @@ -908,6 +908,32 @@ |
131 | self.assertEqual('mynewname', parsed_device['name']) |
132 | self.assertEqual(4096, parsed_device['block_size']) |
133 | |
134 | + def test_update_deployed_physical_block_device_as_admin(self): |
135 | + """Update deployed physical block device. |
136 | + |
137 | + PUT /api/2.0/nodes/{system_id}/blockdevice/{id} |
138 | + """ |
139 | + self.become_admin() |
140 | + node = factory.make_Node( |
141 | + status=NODE_STATUS.DEPLOYED, owner=self.user) |
142 | + block_device = factory.make_PhysicalBlockDevice( |
143 | + node=node, |
144 | + name='myblockdevice', |
145 | + size=MIN_BLOCK_DEVICE_SIZE, |
146 | + block_size=1024) |
147 | + uri = get_blockdevice_uri(block_device) |
148 | + response = self.client.put(uri, { |
149 | + 'name': 'mynewname', |
150 | + 'block_size': 4096 |
151 | + }) |
152 | + block_device = reload_object(block_device) |
153 | + self.assertEqual( |
154 | + http.client.OK, response.status_code, response.content) |
155 | + parsed_device = json_load_bytes(response.content) |
156 | + self.assertEqual(parsed_device['id'], block_device.id) |
157 | + self.assertEqual('mynewname', parsed_device['name']) |
158 | + self.assertEqual(1024, parsed_device['block_size']) |
159 | + |
160 | def test_update_virtual_block_device_as_admin(self): |
161 | """Check update block device with a virtual one. |
162 | |
163 | |
164 | === modified file 'src/maasserver/api/tests/test_interfaces.py' |
165 | --- src/maasserver/api/tests/test_interfaces.py 2016-10-18 08:00:37 +0000 |
166 | +++ src/maasserver/api/tests/test_interfaces.py 2016-10-19 18:09:00 +0000 |
167 | @@ -877,6 +877,24 @@ |
168 | self.assertEqual( |
169 | http.client.NOT_FOUND, response.status_code, response.content) |
170 | |
171 | + def test_update_deployed_machine_interface(self): |
172 | + self.become_admin() |
173 | + node = factory.make_Node(status=NODE_STATUS.DEPLOYED) |
174 | + interface = factory.make_Interface( |
175 | + INTERFACE_TYPE.PHYSICAL, node=node) |
176 | + new_name = factory.make_name("name") |
177 | + new_mac = factory.make_mac_address() |
178 | + uri = get_interface_uri(interface) |
179 | + response = self.client.put(uri, { |
180 | + "name": new_name, |
181 | + "mac_address": new_mac, |
182 | + }) |
183 | + self.assertEqual( |
184 | + http.client.OK, response.status_code, response.content) |
185 | + parsed_interface = json_load_bytes(response.content) |
186 | + self.assertEqual(new_name, parsed_interface["name"]) |
187 | + self.assertEqual(new_mac, parsed_interface["mac_address"]) |
188 | + |
189 | def test_update_physical_interface(self): |
190 | self.become_admin() |
191 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
192 | @@ -963,9 +981,13 @@ |
193 | self.assertEqual( |
194 | http.client.FORBIDDEN, response.status_code, response.content) |
195 | |
196 | - def test_read_409_when_not_ready_or_broken(self): |
197 | + def test_update_409_when_not_ready_broken_or_deployed(self): |
198 | self.become_admin() |
199 | - for status in STATUSES: |
200 | + statuses = list(STATUSES) |
201 | + # Update is the only call that a deployed node can have called on |
202 | + # its interface. |
203 | + statuses.remove(NODE_STATUS.DEPLOYED) |
204 | + for status in statuses: |
205 | node = factory.make_Node(interface=True, status=status) |
206 | interface = factory.make_Interface( |
207 | INTERFACE_TYPE.PHYSICAL, node=node) |
208 | |
209 | === modified file 'src/maasserver/forms.py' |
210 | --- src/maasserver/forms.py 2016-09-25 03:40:16 +0000 |
211 | +++ src/maasserver/forms.py 2016-10-19 18:09:00 +0000 |
212 | @@ -2537,6 +2537,22 @@ |
213 | ] |
214 | |
215 | |
216 | +class UpdateDeployedPhysicalBlockDeviceForm(MAASModelForm): |
217 | + """For updating physical block device on deployed machine.""" |
218 | + |
219 | + name = forms.CharField(required=False) |
220 | + id_path = AbsolutePathField(required=False) |
221 | + |
222 | + class Meta: |
223 | + model = PhysicalBlockDevice |
224 | + fields = [ |
225 | + "name", |
226 | + "model", |
227 | + "serial", |
228 | + "id_path", |
229 | + ] |
230 | + |
231 | + |
232 | class UpdateVirtualBlockDeviceForm(MAASModelForm): |
233 | """For updating virtual block device.""" |
234 | |
235 | |
236 | === modified file 'src/maasserver/forms_interface.py' |
237 | --- src/maasserver/forms_interface.py 2016-09-27 00:47:54 +0000 |
238 | +++ src/maasserver/forms_interface.py 2016-10-19 18:09:00 +0000 |
239 | @@ -187,6 +187,17 @@ |
240 | return interface |
241 | |
242 | |
243 | +class DeployedInterfaceForm(MAASModelForm): |
244 | + """Interface update form for machines when deployed.""" |
245 | + |
246 | + class Meta: |
247 | + model = Interface |
248 | + fields = ( |
249 | + 'name', |
250 | + 'mac_address', |
251 | + ) |
252 | + |
253 | + |
254 | class PhysicalInterfaceForm(InterfaceForm): |
255 | """Form used to create/edit a physical interface.""" |
256 | |
257 | |
258 | === modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js' |
259 | --- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-09-21 14:49:23 +0000 |
260 | +++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-10-19 18:09:00 +0000 |
261 | @@ -556,6 +556,23 @@ |
262 | return true; |
263 | }; |
264 | |
265 | + // Return true if only the name or mac address of an interface can |
266 | + // be edited. |
267 | + $scope.isLimitedEditingAllowed = function(nic) { |
268 | + if (!$scope.isSuperUser()) { |
269 | + // If the user is not the superuser, pretend it's not Ready. |
270 | + return false; |
271 | + } |
272 | + if ($scope.$parent.isController) { |
273 | + // Controllers never in limited mode. |
274 | + return false; |
275 | + } |
276 | + return ( |
277 | + angular.isObject($scope.node) && |
278 | + $scope.node.status === "Deployed" && |
279 | + nic.type !== "vlan"); |
280 | + }; |
281 | + |
282 | // Return true if the networking information cannot be edited. |
283 | // (it can't be changed when the node is in any state other |
284 | // than Ready or Broken and the user is not a superuser) |
285 | @@ -903,6 +920,17 @@ |
286 | return $scope.selectedMode === SELECTION_MODE.ADD; |
287 | }; |
288 | |
289 | + // Return true if either an alias or VLAN can be added. |
290 | + $scope.canAddAliasOrVLAN = function(nic) { |
291 | + if($scope.$parent.isController) { |
292 | + return false; |
293 | + } else if (!$scope.isNodeEditingAllowed()) { |
294 | + return false; |
295 | + } else { |
296 | + return $scope.canAddAlias(nic) || $scope.canAddVLAN(nic); |
297 | + } |
298 | + }; |
299 | + |
300 | // Return true if the alias can be added to interface. |
301 | $scope.canAddAlias = function(nic) { |
302 | if(!angular.isObject(nic)) { |
303 | @@ -950,6 +978,13 @@ |
304 | } |
305 | }; |
306 | |
307 | + // Return true if the interface can be removed. |
308 | + $scope.canBeRemoved = function() { |
309 | + return ( |
310 | + !$scope.$parent.isController && |
311 | + $scope.isNodeEditingAllowed()); |
312 | + }; |
313 | + |
314 | // Enter remove mode. |
315 | $scope.remove = function() { |
316 | $scope.selectedMode = SELECTION_MODE.DELETE; |
317 | |
318 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js' |
319 | --- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-08-18 17:31:05 +0000 |
320 | +++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-10-19 18:09:00 +0000 |
321 | @@ -2027,6 +2027,45 @@ |
322 | }); |
323 | }); |
324 | |
325 | + describe("canAddAliasOrVLAN", function() { |
326 | + |
327 | + it("returns false if isController", function() { |
328 | + var controller = makeController(); |
329 | + $parentScope.isController = true; |
330 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(true); |
331 | + spyOn($scope, "canAddAlias").and.returnValue(true); |
332 | + spyOn($scope, "canAddVLAN").and.returnValue(true); |
333 | + expect($scope.canAddAliasOrVLAN({})).toBe(false); |
334 | + }); |
335 | + |
336 | + it("returns false if no node editing", function() { |
337 | + var controller = makeController(); |
338 | + $parentScope.isController = false; |
339 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(false); |
340 | + spyOn($scope, "canAddAlias").and.returnValue(true); |
341 | + spyOn($scope, "canAddVLAN").and.returnValue(true); |
342 | + expect($scope.canAddAliasOrVLAN({})).toBe(false); |
343 | + }); |
344 | + |
345 | + it("returns true if can edit alias", function() { |
346 | + var controller = makeController(); |
347 | + $parentScope.isController = false; |
348 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(true); |
349 | + spyOn($scope, "canAddAlias").and.returnValue(true); |
350 | + spyOn($scope, "canAddVLAN").and.returnValue(false); |
351 | + expect($scope.canAddAliasOrVLAN({})).toBe(true); |
352 | + }); |
353 | + |
354 | + it("returns true if can edit VLAN", function() { |
355 | + var controller = makeController(); |
356 | + $parentScope.isController = false; |
357 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(true); |
358 | + spyOn($scope, "canAddAlias").and.returnValue(false); |
359 | + spyOn($scope, "canAddVLAN").and.returnValue(true); |
360 | + expect($scope.canAddAliasOrVLAN({})).toBe(true); |
361 | + }); |
362 | + }); |
363 | + |
364 | describe("canAddAlias", function() { |
365 | |
366 | it("returns false if nic undefined", function() { |
367 | @@ -2365,6 +2404,30 @@ |
368 | }); |
369 | }); |
370 | |
371 | + describe("canBeRemoved", function() { |
372 | + |
373 | + it("false if isController", function() { |
374 | + var controller = makeController(); |
375 | + $parentScope.isController = true; |
376 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(true); |
377 | + expect($scope.canBeRemoved()).toBe(false); |
378 | + }); |
379 | + |
380 | + it("false if no node editing", function() { |
381 | + var controller = makeController(); |
382 | + $parentScope.isController = false; |
383 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(false); |
384 | + expect($scope.canBeRemoved()).toBe(false); |
385 | + }); |
386 | + |
387 | + it("true if node can be edited", function() { |
388 | + var controller = makeController(); |
389 | + $parentScope.isController = false; |
390 | + spyOn($scope, "isNodeEditingAllowed").and.returnValue(true); |
391 | + expect($scope.canBeRemoved()).toBe(true); |
392 | + }); |
393 | + }); |
394 | + |
395 | describe("remove", function() { |
396 | |
397 | it("sets selectedMode to delete", function() { |
398 | @@ -2935,6 +2998,35 @@ |
399 | }); |
400 | }); |
401 | |
402 | + describe("isLimitedEditingAllowed", function() { |
403 | + |
404 | + it("returns false when not superuser", function() { |
405 | + var controller = makeController(); |
406 | + $scope.isSuperUser = function() { return false; }; |
407 | + expect($scope.isLimitedEditingAllowed()).toBe(false); |
408 | + }); |
409 | + |
410 | + it("returns false when isController", function() { |
411 | + var controller = makeController(); |
412 | + $scope.isSuperUser = function() { return true; }; |
413 | + $parentScope.isController = true; |
414 | + expect($scope.isLimitedEditingAllowed()).toBe(false); |
415 | + }); |
416 | + |
417 | + it("returns true when deployed and not vlan", function() { |
418 | + var controller = makeController(); |
419 | + $scope.isSuperUser = function() { return true; }; |
420 | + $parentScope.isController = false; |
421 | + $scope.node = { |
422 | + status: "Deployed" |
423 | + }; |
424 | + var nic = { |
425 | + type: "physical" |
426 | + }; |
427 | + expect($scope.isLimitedEditingAllowed(nic)).toBe(true); |
428 | + }); |
429 | + }); |
430 | + |
431 | describe("isAllNetworkingDisabled", function() { |
432 | |
433 | it("returns true if the user is not a superuser " + |
434 | |
435 | === modified file 'src/maasserver/static/partials/node-details.html' |
436 | --- src/maasserver/static/partials/node-details.html 2016-09-30 12:50:08 +0000 |
437 | +++ src/maasserver/static/partials/node-details.html 2016-10-19 18:09:00 +0000 |
438 | @@ -574,7 +574,7 @@ |
439 | </header> |
440 | <main class="table__body" data-selected-rows> |
441 | <div class="table__row" |
442 | - data-ng-class="{ disabled: isDisabled(), 'is-active': isInterfaceSelected(interface) && isNodeEditingAllowed(), noEdit: cannotEditInterface(interface) }" |
443 | + data-ng-class="{ disabled: isDisabled(), 'is-active': isInterfaceSelected(interface) && (isNodeEditingAllowed() || isLimitedEditingAllowed(interface)), noEdit: cannotEditInterface(interface) }" |
444 | data-ng-repeat="interface in interfaces | removeInterfaceParents:newBondInterface:!isNodeEditingAllowed() | removeInterfaceParents:newBridgeInterface:!isNodeEditingAllowed()"> |
445 | <div class="table__data table-col--3"> |
446 | <input type="checkbox" class="checkbox" id="{$ getUniqueKey(interface) $}" |
447 | @@ -588,7 +588,7 @@ |
448 | <div class="table__data table-col--12" data-ng-show="column == 'name'"> |
449 | <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span> |
450 | <input type="text" class="table__input" |
451 | - data-ng-if="isEditing(interface)" |
452 | + data-ng-if="isEditing(interface) && interface.type != 'vlan'" |
453 | data-ng-model="editInterface.name" |
454 | data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }"> |
455 | </div> |
456 | @@ -624,17 +624,17 @@ |
457 | </span> |
458 | </div> |
459 | <div class="table__data table-col--8"> |
460 | - <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed()"> |
461 | + <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)"> |
462 | <a class="icon icon--add tooltip" |
463 | aria-label="Add Alias or VLAN" |
464 | - data-ng-if="!isController && (canAddAlias(interface) || canAddVLAN(interface))" |
465 | + data-ng-if="canAddAliasOrVLAN(interface)" |
466 | data-ng-click="quickAdd(interface)"></a> |
467 | <a class="icon icon--edit tooltip" |
468 | data-ng-if="!cannotEditInterface(interface)" |
469 | aria-label="Edit" |
470 | data-ng-click="edit(interface)"></a> |
471 | <a class="icon icon--delete tooltip u-margin--left" |
472 | - data-ng-if="!isController" |
473 | + data-ng-if="canBeRemoved()" |
474 | aria-label="Remove" |
475 | data-ng-click="quickRemove(interface)"></a> |
476 | </div> |
477 | @@ -653,7 +653,7 @@ |
478 | </div> |
479 | </div> |
480 | </div> |
481 | - <div data-ng-show="isNodeEditingAllowed()"> |
482 | + <div data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)"> |
483 | <div class="table__dropdown"> |
484 | <div class="table__row form form--stack u-padding--top u-padding--right u-padding--left box-sizing" data-ng-class="{ 'is-active': isEditing(interface) || isShowingAdd() }"> |
485 | <div data-ng-if="isShowingAdd()" class="table__row--indent"> |
486 | @@ -742,14 +742,14 @@ |
487 | data-ng-model="editInterface.mac_address" |
488 | data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }"> |
489 | </div> |
490 | - <div class="form__group"> |
491 | + <div class="form__group" data-ng-if="!isLimitedEditingAllowed(interface)"> |
492 | <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label> |
493 | <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'"> |
494 | <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input> |
495 | </div> |
496 | </div> |
497 | </div> |
498 | - <div class="form__fieldset six-col last-col"> |
499 | + <div class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)"> |
500 | <div class="form__group"> |
501 | <label for="fabric" class="two-col">Fabric</label> |
502 | <select name="fabric" class="three-col" |
503 | |
504 | === modified file 'src/maasserver/tests/test_forms_blockdevice.py' |
505 | --- src/maasserver/tests/test_forms_blockdevice.py 2016-03-28 13:54:47 +0000 |
506 | +++ src/maasserver/tests/test_forms_blockdevice.py 2016-10-19 18:09:00 +0000 |
507 | @@ -12,6 +12,7 @@ |
508 | from maasserver.forms import ( |
509 | CreatePhysicalBlockDeviceForm, |
510 | FormatBlockDeviceForm, |
511 | + UpdateDeployedPhysicalBlockDeviceForm, |
512 | UpdatePhysicalBlockDeviceForm, |
513 | UpdateVirtualBlockDeviceForm, |
514 | ) |
515 | @@ -245,6 +246,40 @@ |
516 | )) |
517 | |
518 | |
519 | +class TestUpdateDeployedPhysicalBlockDeviceForm(MAASServerTestCase): |
520 | + |
521 | + def test_requires_no_fields(self): |
522 | + block_device = factory.make_PhysicalBlockDevice() |
523 | + form = UpdateDeployedPhysicalBlockDeviceForm( |
524 | + instance=block_device, data={}) |
525 | + self.assertTrue(form.is_valid(), form.errors) |
526 | + self.assertItemsEqual([], form.errors.keys()) |
527 | + |
528 | + def test_updates_deployed_physical_block_device(self): |
529 | + block_device = factory.make_PhysicalBlockDevice() |
530 | + name = factory.make_name("sd") |
531 | + model = factory.make_name("model") |
532 | + serial = factory.make_name("serial") |
533 | + id_path = factory.make_absolute_path() |
534 | + form = UpdateDeployedPhysicalBlockDeviceForm( |
535 | + instance=block_device, data={ |
536 | + 'name': name, |
537 | + 'model': model, |
538 | + 'serial': serial, |
539 | + 'id_path': id_path, |
540 | + }) |
541 | + self.assertTrue(form.is_valid(), form.errors) |
542 | + block_device = form.save() |
543 | + self.assertThat(block_device, MatchesStructure.byEquality( |
544 | + name=name, |
545 | + model=model, |
546 | + serial=serial, |
547 | + id_path=id_path, |
548 | + size=block_device.size, |
549 | + block_size=block_device.block_size, |
550 | + )) |
551 | + |
552 | + |
553 | class TestUpdateVirtualBlockDeviceForm(MAASServerTestCase): |
554 | |
555 | def test_requires_no_fields(self): |
556 | |
557 | === modified file 'src/maasserver/tests/test_forms_interface.py' |
558 | --- src/maasserver/tests/test_forms_interface.py 2016-09-27 14:24:19 +0000 |
559 | +++ src/maasserver/tests/test_forms_interface.py 2016-10-19 18:09:00 +0000 |
560 | @@ -20,6 +20,7 @@ |
561 | BondInterfaceForm, |
562 | BridgeInterfaceForm, |
563 | ControllerInterfaceForm, |
564 | + DeployedInterfaceForm, |
565 | InterfaceForm, |
566 | PhysicalInterfaceForm, |
567 | VLANInterfaceForm, |
568 | @@ -104,6 +105,27 @@ |
569 | name=interface.name, vlan=None, enabled=interface.enabled)) |
570 | |
571 | |
572 | +class DeployedInterfaceFormTest(MAASServerTestCase): |
573 | + |
574 | + def test__updates_interface(self): |
575 | + interface = factory.make_Interface( |
576 | + INTERFACE_TYPE.PHYSICAL, name='eth0') |
577 | + new_name = 'eth1' |
578 | + new_mac = factory.make_mac_address() |
579 | + form = DeployedInterfaceForm( |
580 | + instance=interface, |
581 | + data={ |
582 | + 'name': new_name, |
583 | + 'mac_address': new_mac, |
584 | + }) |
585 | + self.assertTrue(form.is_valid(), form.errors) |
586 | + interface = form.save() |
587 | + self.assertThat( |
588 | + interface, |
589 | + MatchesStructure.byEquality( |
590 | + name=new_name, mac_address=new_mac)) |
591 | + |
592 | + |
593 | class PhysicalInterfaceFormTest(MAASServerTestCase): |
594 | |
595 | def test__creates_physical_interface(self): |
596 | |
597 | === modified file 'src/maasserver/websockets/handlers/machine.py' |
598 | --- src/maasserver/websockets/handlers/machine.py 2016-10-13 01:35:42 +0000 |
599 | +++ src/maasserver/websockets/handlers/machine.py 2016-10-19 18:09:00 +0000 |
600 | @@ -45,6 +45,7 @@ |
601 | AcquiredBridgeInterfaceForm, |
602 | BondInterfaceForm, |
603 | BridgeInterfaceForm, |
604 | + DeployedInterfaceForm, |
605 | InterfaceForm, |
606 | PhysicalInterfaceForm, |
607 | VLANInterfaceForm, |
608 | @@ -811,7 +812,10 @@ |
609 | |
610 | node = self.get_object(params) |
611 | interface = Interface.objects.get(node=node, id=params["interface_id"]) |
612 | - interface_form = InterfaceForm.get_interface_form(interface.type) |
613 | + if node.status == NODE_STATUS.DEPLOYED: |
614 | + interface_form = DeployedInterfaceForm |
615 | + else: |
616 | + interface_form = InterfaceForm.get_interface_form(interface.type) |
617 | form = interface_form(instance=interface, data=params) |
618 | if form.is_valid(): |
619 | interface = form.save() |
620 | |
621 | === modified file 'src/maasserver/websockets/handlers/tests/test_machine.py' |
622 | --- src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-12 16:34:23 +0000 |
623 | +++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-19 18:09:00 +0000 |
624 | @@ -2326,6 +2326,20 @@ |
625 | self.assertEqual(new_name, interface.name) |
626 | self.assertEqual(new_vlan, interface.vlan) |
627 | |
628 | + def test_update_interface_for_deployed_node(self): |
629 | + user = factory.make_admin() |
630 | + node = factory.make_Node(status=NODE_STATUS.DEPLOYED) |
631 | + handler = MachineHandler(user, {}) |
632 | + interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
633 | + new_name = factory.make_name("name") |
634 | + handler.update_interface({ |
635 | + "system_id": node.system_id, |
636 | + "interface_id": interface.id, |
637 | + "name": new_name, |
638 | + }) |
639 | + interface = reload_object(interface) |
640 | + self.assertEqual(new_name, interface.name) |
641 | + |
642 | def test_update_interface_raises_ValidationError(self): |
643 | user = factory.make_admin() |
644 | node = factory.make_Node() |
Code looks pretty good (see a few comments below, though).
"Needs fixing", though, because I don't see any docstring updates. We need to explain to API users what [new] states editing is allowed in, and which values can be updated.