Merge lp:~blake-rouse/maas/fix-1598175 into lp:~maas-committers/maas/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
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.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

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.

review: Needs Fixing
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()