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
=== modified file 'src/maasserver/api/blockdevices.py'
--- src/maasserver/api/blockdevices.py 2016-08-18 17:31:05 +0000
+++ src/maasserver/api/blockdevices.py 2016-10-19 18:09:00 +0000
@@ -22,6 +22,7 @@
22from maasserver.forms import (22from maasserver.forms import (
23 CreatePhysicalBlockDeviceForm,23 CreatePhysicalBlockDeviceForm,
24 FormatBlockDeviceForm,24 FormatBlockDeviceForm,
25 UpdateDeployedPhysicalBlockDeviceForm,
25 UpdatePhysicalBlockDeviceForm,26 UpdatePhysicalBlockDeviceForm,
26 UpdateVirtualBlockDeviceForm,27 UpdateVirtualBlockDeviceForm,
27)28)
@@ -227,6 +228,11 @@
227 def update(self, request, system_id, device_id):228 def update(self, request, system_id, device_id):
228 """Update block device on a machine.229 """Update block device on a machine.
229230
231 Machines must have a status of Ready to have access to all options.
232 Machines with Deployed status can only have the name, model, serial,
233 and/or id_path updated for a block device. This is intented to allow a
234 bad block device to be replaced while the machine remains deployed.
235
230 Fields for physical block device:236 Fields for physical block device:
231237
232 :param name: Name of the block device.238 :param name: Name of the block device.
@@ -252,18 +258,27 @@
252 device = BlockDevice.objects.get_block_device_or_404(258 device = BlockDevice.objects.get_block_device_or_404(
253 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)259 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
254 node = device.get_node()260 node = device.get_node()
255 if node.status != NODE_STATUS.READY:261 if node.status not in [NODE_STATUS.READY, NODE_STATUS.DEPLOYED]:
256 raise NodeStateViolation(262 raise NodeStateViolation(
257 "Cannot update block device because the machine is not Ready.")263 "Cannot update block device because the machine is not Ready.")
258 if device.type == 'physical':264 if node.status == NODE_STATUS.DEPLOYED:
259 form = UpdatePhysicalBlockDeviceForm(265 if device.type == 'physical':
260 instance=device, data=request.data)266 form = UpdateDeployedPhysicalBlockDeviceForm(
261 elif device.type == 'virtual':267 instance=device, data=request.data)
262 form = UpdateVirtualBlockDeviceForm(268 else:
263 instance=device, data=request.data)269 raise NodeStateViolation(
270 "Cannot update virtual block device because the machine "
271 "is Deployed.")
264 else:272 else:
265 raise ValueError(273 if device.type == 'physical':
266 'Cannot update block device of type %s' % device.type)274 form = UpdatePhysicalBlockDeviceForm(
275 instance=device, data=request.data)
276 elif device.type == 'virtual':
277 form = UpdateVirtualBlockDeviceForm(
278 instance=device, data=request.data)
279 else:
280 raise ValueError(
281 'Cannot update block device of type %s' % device.type)
267 if form.is_valid():282 if form.is_valid():
268 return form.save()283 return form.save()
269 else:284 else:
270285
=== modified file 'src/maasserver/api/interfaces.py'
--- src/maasserver/api/interfaces.py 2016-08-18 17:31:05 +0000
+++ src/maasserver/api/interfaces.py 2016-10-19 18:09:00 +0000
@@ -26,6 +26,7 @@
26 BondInterfaceForm,26 BondInterfaceForm,
27 BridgeInterfaceForm,27 BridgeInterfaceForm,
28 ControllerInterfaceForm,28 ControllerInterfaceForm,
29 DeployedInterfaceForm,
29 InterfaceForm,30 InterfaceForm,
30 PhysicalInterfaceForm,31 PhysicalInterfaceForm,
31 VLANInterfaceForm,32 VLANInterfaceForm,
@@ -73,8 +74,21 @@
7374
7475
75def raise_error_for_invalid_state_on_allocated_operations(76def raise_error_for_invalid_state_on_allocated_operations(
76 node, user, operation):77 node, user, operation, extra_states=None):
77 if node.status not in ALLOWED_STATES:78 """Raises `NodeStateViolation` when the status of the node is not
79 READY or BROKEN.
80
81 :param node: Node to check status.
82 :param user: User performing the operation.
83 :param operation: Nice name of the operation.
84 :param extra_states: Extra states that the node can be in when checking
85 the status of the node.
86 :type extra_states: `Iterable`.
87 """
88 allowed = list(ALLOWED_STATES)
89 if extra_states is not None:
90 allowed.extend(extra_states)
91 if node.status not in allowed:
78 raise NodeStateViolation(92 raise NodeStateViolation(
79 "Cannot %s interface because the machine is not Ready or "93 "Cannot %s interface because the machine is not Ready or "
80 "Broken." % operation)94 "Broken." % operation)
@@ -378,6 +392,11 @@
378 def update(self, request, system_id, interface_id):392 def update(self, request, system_id, interface_id):
379 """Update interface on node.393 """Update interface on node.
380394
395 Machines must has status of Ready or Broken to have access to all
396 options. Machines with Deployed status can only have the name and/or
397 mac_address updated for an interface. This is intented to allow a bad
398 interface to be replaced while the machine remains deployed.
399
381 Fields for physical interface:400 Fields for physical interface:
382401
383 :param name: Name of the interface.402 :param name: Name of the interface.
@@ -477,12 +496,15 @@
477 # This node needs to be in the correct state to modify496 # This node needs to be in the correct state to modify
478 # the interface.497 # the interface.
479 raise_error_for_invalid_state_on_allocated_operations(498 raise_error_for_invalid_state_on_allocated_operations(
480 node, request.user, "update interface")499 node, request.user, "update interface",
500 extra_states=[NODE_STATUS.DEPLOYED])
481 if node.is_controller:501 if node.is_controller:
482 if interface.type == INTERFACE_TYPE.VLAN:502 if interface.type == INTERFACE_TYPE.VLAN:
483 raise MAASAPIForbidden(503 raise MAASAPIForbidden(
484 "Cannot modify VLAN interface on controller.")504 "Cannot modify VLAN interface on controller.")
485 interface_form = ControllerInterfaceForm505 interface_form = ControllerInterfaceForm
506 elif node.status == NODE_STATUS.DEPLOYED:
507 interface_form = DeployedInterfaceForm
486 else:508 else:
487 interface_form = InterfaceForm.get_interface_form(interface.type)509 interface_form = InterfaceForm.get_interface_form(interface.type)
488 # For VLAN interface we cast parents to parent. As a VLAN can only510 # For VLAN interface we cast parents to parent. As a VLAN can only
489511
=== modified file 'src/maasserver/api/tests/test_blockdevice.py'
--- src/maasserver/api/tests/test_blockdevice.py 2016-08-14 17:36:30 +0000
+++ src/maasserver/api/tests/test_blockdevice.py 2016-10-19 18:09:00 +0000
@@ -908,6 +908,32 @@
908 self.assertEqual('mynewname', parsed_device['name'])908 self.assertEqual('mynewname', parsed_device['name'])
909 self.assertEqual(4096, parsed_device['block_size'])909 self.assertEqual(4096, parsed_device['block_size'])
910910
911 def test_update_deployed_physical_block_device_as_admin(self):
912 """Update deployed physical block device.
913
914 PUT /api/2.0/nodes/{system_id}/blockdevice/{id}
915 """
916 self.become_admin()
917 node = factory.make_Node(
918 status=NODE_STATUS.DEPLOYED, owner=self.user)
919 block_device = factory.make_PhysicalBlockDevice(
920 node=node,
921 name='myblockdevice',
922 size=MIN_BLOCK_DEVICE_SIZE,
923 block_size=1024)
924 uri = get_blockdevice_uri(block_device)
925 response = self.client.put(uri, {
926 'name': 'mynewname',
927 'block_size': 4096
928 })
929 block_device = reload_object(block_device)
930 self.assertEqual(
931 http.client.OK, response.status_code, response.content)
932 parsed_device = json_load_bytes(response.content)
933 self.assertEqual(parsed_device['id'], block_device.id)
934 self.assertEqual('mynewname', parsed_device['name'])
935 self.assertEqual(1024, parsed_device['block_size'])
936
911 def test_update_virtual_block_device_as_admin(self):937 def test_update_virtual_block_device_as_admin(self):
912 """Check update block device with a virtual one.938 """Check update block device with a virtual one.
913939
914940
=== modified file 'src/maasserver/api/tests/test_interfaces.py'
--- src/maasserver/api/tests/test_interfaces.py 2016-10-18 08:00:37 +0000
+++ src/maasserver/api/tests/test_interfaces.py 2016-10-19 18:09:00 +0000
@@ -877,6 +877,24 @@
877 self.assertEqual(877 self.assertEqual(
878 http.client.NOT_FOUND, response.status_code, response.content)878 http.client.NOT_FOUND, response.status_code, response.content)
879879
880 def test_update_deployed_machine_interface(self):
881 self.become_admin()
882 node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
883 interface = factory.make_Interface(
884 INTERFACE_TYPE.PHYSICAL, node=node)
885 new_name = factory.make_name("name")
886 new_mac = factory.make_mac_address()
887 uri = get_interface_uri(interface)
888 response = self.client.put(uri, {
889 "name": new_name,
890 "mac_address": new_mac,
891 })
892 self.assertEqual(
893 http.client.OK, response.status_code, response.content)
894 parsed_interface = json_load_bytes(response.content)
895 self.assertEqual(new_name, parsed_interface["name"])
896 self.assertEqual(new_mac, parsed_interface["mac_address"])
897
880 def test_update_physical_interface(self):898 def test_update_physical_interface(self):
881 self.become_admin()899 self.become_admin()
882 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):900 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
@@ -963,9 +981,13 @@
963 self.assertEqual(981 self.assertEqual(
964 http.client.FORBIDDEN, response.status_code, response.content)982 http.client.FORBIDDEN, response.status_code, response.content)
965983
966 def test_read_409_when_not_ready_or_broken(self):984 def test_update_409_when_not_ready_broken_or_deployed(self):
967 self.become_admin()985 self.become_admin()
968 for status in STATUSES:986 statuses = list(STATUSES)
987 # Update is the only call that a deployed node can have called on
988 # its interface.
989 statuses.remove(NODE_STATUS.DEPLOYED)
990 for status in statuses:
969 node = factory.make_Node(interface=True, status=status)991 node = factory.make_Node(interface=True, status=status)
970 interface = factory.make_Interface(992 interface = factory.make_Interface(
971 INTERFACE_TYPE.PHYSICAL, node=node)993 INTERFACE_TYPE.PHYSICAL, node=node)
972994
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2016-09-25 03:40:16 +0000
+++ src/maasserver/forms.py 2016-10-19 18:09:00 +0000
@@ -2537,6 +2537,22 @@
2537 ]2537 ]
25382538
25392539
2540class UpdateDeployedPhysicalBlockDeviceForm(MAASModelForm):
2541 """For updating physical block device on deployed machine."""
2542
2543 name = forms.CharField(required=False)
2544 id_path = AbsolutePathField(required=False)
2545
2546 class Meta:
2547 model = PhysicalBlockDevice
2548 fields = [
2549 "name",
2550 "model",
2551 "serial",
2552 "id_path",
2553 ]
2554
2555
2540class UpdateVirtualBlockDeviceForm(MAASModelForm):2556class UpdateVirtualBlockDeviceForm(MAASModelForm):
2541 """For updating virtual block device."""2557 """For updating virtual block device."""
25422558
25432559
=== modified file 'src/maasserver/forms_interface.py'
--- src/maasserver/forms_interface.py 2016-09-27 00:47:54 +0000
+++ src/maasserver/forms_interface.py 2016-10-19 18:09:00 +0000
@@ -187,6 +187,17 @@
187 return interface187 return interface
188188
189189
190class DeployedInterfaceForm(MAASModelForm):
191 """Interface update form for machines when deployed."""
192
193 class Meta:
194 model = Interface
195 fields = (
196 'name',
197 'mac_address',
198 )
199
200
190class PhysicalInterfaceForm(InterfaceForm):201class PhysicalInterfaceForm(InterfaceForm):
191 """Form used to create/edit a physical interface."""202 """Form used to create/edit a physical interface."""
192203
193204
=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-09-21 14:49:23 +0000
+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-10-19 18:09:00 +0000
@@ -556,6 +556,23 @@
556 return true;556 return true;
557 };557 };
558558
559 // Return true if only the name or mac address of an interface can
560 // be edited.
561 $scope.isLimitedEditingAllowed = function(nic) {
562 if (!$scope.isSuperUser()) {
563 // If the user is not the superuser, pretend it's not Ready.
564 return false;
565 }
566 if ($scope.$parent.isController) {
567 // Controllers never in limited mode.
568 return false;
569 }
570 return (
571 angular.isObject($scope.node) &&
572 $scope.node.status === "Deployed" &&
573 nic.type !== "vlan");
574 };
575
559 // Return true if the networking information cannot be edited.576 // Return true if the networking information cannot be edited.
560 // (it can't be changed when the node is in any state other577 // (it can't be changed when the node is in any state other
561 // than Ready or Broken and the user is not a superuser)578 // than Ready or Broken and the user is not a superuser)
@@ -903,6 +920,17 @@
903 return $scope.selectedMode === SELECTION_MODE.ADD;920 return $scope.selectedMode === SELECTION_MODE.ADD;
904 };921 };
905922
923 // Return true if either an alias or VLAN can be added.
924 $scope.canAddAliasOrVLAN = function(nic) {
925 if($scope.$parent.isController) {
926 return false;
927 } else if (!$scope.isNodeEditingAllowed()) {
928 return false;
929 } else {
930 return $scope.canAddAlias(nic) || $scope.canAddVLAN(nic);
931 }
932 };
933
906 // Return true if the alias can be added to interface.934 // Return true if the alias can be added to interface.
907 $scope.canAddAlias = function(nic) {935 $scope.canAddAlias = function(nic) {
908 if(!angular.isObject(nic)) {936 if(!angular.isObject(nic)) {
@@ -950,6 +978,13 @@
950 }978 }
951 };979 };
952980
981 // Return true if the interface can be removed.
982 $scope.canBeRemoved = function() {
983 return (
984 !$scope.$parent.isController &&
985 $scope.isNodeEditingAllowed());
986 };
987
953 // Enter remove mode.988 // Enter remove mode.
954 $scope.remove = function() {989 $scope.remove = function() {
955 $scope.selectedMode = SELECTION_MODE.DELETE;990 $scope.selectedMode = SELECTION_MODE.DELETE;
956991
=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js'
--- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-08-18 17:31:05 +0000
+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2016-10-19 18:09:00 +0000
@@ -2027,6 +2027,45 @@
2027 });2027 });
2028 });2028 });
20292029
2030 describe("canAddAliasOrVLAN", function() {
2031
2032 it("returns false if isController", function() {
2033 var controller = makeController();
2034 $parentScope.isController = true;
2035 spyOn($scope, "isNodeEditingAllowed").and.returnValue(true);
2036 spyOn($scope, "canAddAlias").and.returnValue(true);
2037 spyOn($scope, "canAddVLAN").and.returnValue(true);
2038 expect($scope.canAddAliasOrVLAN({})).toBe(false);
2039 });
2040
2041 it("returns false if no node editing", function() {
2042 var controller = makeController();
2043 $parentScope.isController = false;
2044 spyOn($scope, "isNodeEditingAllowed").and.returnValue(false);
2045 spyOn($scope, "canAddAlias").and.returnValue(true);
2046 spyOn($scope, "canAddVLAN").and.returnValue(true);
2047 expect($scope.canAddAliasOrVLAN({})).toBe(false);
2048 });
2049
2050 it("returns true if can edit alias", function() {
2051 var controller = makeController();
2052 $parentScope.isController = false;
2053 spyOn($scope, "isNodeEditingAllowed").and.returnValue(true);
2054 spyOn($scope, "canAddAlias").and.returnValue(true);
2055 spyOn($scope, "canAddVLAN").and.returnValue(false);
2056 expect($scope.canAddAliasOrVLAN({})).toBe(true);
2057 });
2058
2059 it("returns true if can edit VLAN", function() {
2060 var controller = makeController();
2061 $parentScope.isController = false;
2062 spyOn($scope, "isNodeEditingAllowed").and.returnValue(true);
2063 spyOn($scope, "canAddAlias").and.returnValue(false);
2064 spyOn($scope, "canAddVLAN").and.returnValue(true);
2065 expect($scope.canAddAliasOrVLAN({})).toBe(true);
2066 });
2067 });
2068
2030 describe("canAddAlias", function() {2069 describe("canAddAlias", function() {
20312070
2032 it("returns false if nic undefined", function() {2071 it("returns false if nic undefined", function() {
@@ -2365,6 +2404,30 @@
2365 });2404 });
2366 });2405 });
23672406
2407 describe("canBeRemoved", function() {
2408
2409 it("false if isController", function() {
2410 var controller = makeController();
2411 $parentScope.isController = true;
2412 spyOn($scope, "isNodeEditingAllowed").and.returnValue(true);
2413 expect($scope.canBeRemoved()).toBe(false);
2414 });
2415
2416 it("false if no node editing", function() {
2417 var controller = makeController();
2418 $parentScope.isController = false;
2419 spyOn($scope, "isNodeEditingAllowed").and.returnValue(false);
2420 expect($scope.canBeRemoved()).toBe(false);
2421 });
2422
2423 it("true if node can be edited", function() {
2424 var controller = makeController();
2425 $parentScope.isController = false;
2426 spyOn($scope, "isNodeEditingAllowed").and.returnValue(true);
2427 expect($scope.canBeRemoved()).toBe(true);
2428 });
2429 });
2430
2368 describe("remove", function() {2431 describe("remove", function() {
23692432
2370 it("sets selectedMode to delete", function() {2433 it("sets selectedMode to delete", function() {
@@ -2935,6 +2998,35 @@
2935 });2998 });
2936 });2999 });
29373000
3001 describe("isLimitedEditingAllowed", function() {
3002
3003 it("returns false when not superuser", function() {
3004 var controller = makeController();
3005 $scope.isSuperUser = function() { return false; };
3006 expect($scope.isLimitedEditingAllowed()).toBe(false);
3007 });
3008
3009 it("returns false when isController", function() {
3010 var controller = makeController();
3011 $scope.isSuperUser = function() { return true; };
3012 $parentScope.isController = true;
3013 expect($scope.isLimitedEditingAllowed()).toBe(false);
3014 });
3015
3016 it("returns true when deployed and not vlan", function() {
3017 var controller = makeController();
3018 $scope.isSuperUser = function() { return true; };
3019 $parentScope.isController = false;
3020 $scope.node = {
3021 status: "Deployed"
3022 };
3023 var nic = {
3024 type: "physical"
3025 };
3026 expect($scope.isLimitedEditingAllowed(nic)).toBe(true);
3027 });
3028 });
3029
2938 describe("isAllNetworkingDisabled", function() {3030 describe("isAllNetworkingDisabled", function() {
29393031
2940 it("returns true if the user is not a superuser " +3032 it("returns true if the user is not a superuser " +
29413033
=== modified file 'src/maasserver/static/partials/node-details.html'
--- src/maasserver/static/partials/node-details.html 2016-09-30 12:50:08 +0000
+++ src/maasserver/static/partials/node-details.html 2016-10-19 18:09:00 +0000
@@ -574,7 +574,7 @@
574 </header>574 </header>
575 <main class="table__body" data-selected-rows>575 <main class="table__body" data-selected-rows>
576 <div class="table__row"576 <div class="table__row"
577 data-ng-class="{ disabled: isDisabled(), 'is-active': isInterfaceSelected(interface) && isNodeEditingAllowed(), noEdit: cannotEditInterface(interface) }"577 data-ng-class="{ disabled: isDisabled(), 'is-active': isInterfaceSelected(interface) && (isNodeEditingAllowed() || isLimitedEditingAllowed(interface)), noEdit: cannotEditInterface(interface) }"
578 data-ng-repeat="interface in interfaces | removeInterfaceParents:newBondInterface:!isNodeEditingAllowed() | removeInterfaceParents:newBridgeInterface:!isNodeEditingAllowed()">578 data-ng-repeat="interface in interfaces | removeInterfaceParents:newBondInterface:!isNodeEditingAllowed() | removeInterfaceParents:newBridgeInterface:!isNodeEditingAllowed()">
579 <div class="table__data table-col--3">579 <div class="table__data table-col--3">
580 <input type="checkbox" class="checkbox" id="{$ getUniqueKey(interface) $}"580 <input type="checkbox" class="checkbox" id="{$ getUniqueKey(interface) $}"
@@ -588,7 +588,7 @@
588 <div class="table__data table-col--12" data-ng-show="column == 'name'">588 <div class="table__data table-col--12" data-ng-show="column == 'name'">
589 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>589 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>
590 <input type="text" class="table__input"590 <input type="text" class="table__input"
591 data-ng-if="isEditing(interface)"591 data-ng-if="isEditing(interface) && interface.type != 'vlan'"
592 data-ng-model="editInterface.name"592 data-ng-model="editInterface.name"
593 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">593 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">
594 </div>594 </div>
@@ -624,17 +624,17 @@
624 </span>624 </span>
625 </div>625 </div>
626 <div class="table__data table-col--8">626 <div class="table__data table-col--8">
627 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed()">627 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">
628 <a class="icon icon--add tooltip"628 <a class="icon icon--add tooltip"
629 aria-label="Add Alias or VLAN"629 aria-label="Add Alias or VLAN"
630 data-ng-if="!isController && (canAddAlias(interface) || canAddVLAN(interface))"630 data-ng-if="canAddAliasOrVLAN(interface)"
631 data-ng-click="quickAdd(interface)"></a>631 data-ng-click="quickAdd(interface)"></a>
632 <a class="icon icon--edit tooltip"632 <a class="icon icon--edit tooltip"
633 data-ng-if="!cannotEditInterface(interface)"633 data-ng-if="!cannotEditInterface(interface)"
634 aria-label="Edit"634 aria-label="Edit"
635 data-ng-click="edit(interface)"></a>635 data-ng-click="edit(interface)"></a>
636 <a class="icon icon--delete tooltip u-margin--left"636 <a class="icon icon--delete tooltip u-margin--left"
637 data-ng-if="!isController"637 data-ng-if="canBeRemoved()"
638 aria-label="Remove"638 aria-label="Remove"
639 data-ng-click="quickRemove(interface)"></a>639 data-ng-click="quickRemove(interface)"></a>
640 </div>640 </div>
@@ -653,7 +653,7 @@
653 </div>653 </div>
654 </div>654 </div>
655 </div>655 </div>
656 <div data-ng-show="isNodeEditingAllowed()">656 <div data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">
657 <div class="table__dropdown">657 <div class="table__dropdown">
658 <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() }">658 <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() }">
659 <div data-ng-if="isShowingAdd()" class="table__row--indent">659 <div data-ng-if="isShowingAdd()" class="table__row--indent">
@@ -742,14 +742,14 @@
742 data-ng-model="editInterface.mac_address"742 data-ng-model="editInterface.mac_address"
743 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">743 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">
744 </div>744 </div>
745 <div class="form__group">745 <div class="form__group" data-ng-if="!isLimitedEditingAllowed(interface)">
746 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>746 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>
747 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">747 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">
748 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>748 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>
749 </div>749 </div>
750 </div>750 </div>
751 </div>751 </div>
752 <div class="form__fieldset six-col last-col">752 <div class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)">
753 <div class="form__group">753 <div class="form__group">
754 <label for="fabric" class="two-col">Fabric</label>754 <label for="fabric" class="two-col">Fabric</label>
755 <select name="fabric" class="three-col"755 <select name="fabric" class="three-col"
756756
=== modified file 'src/maasserver/tests/test_forms_blockdevice.py'
--- src/maasserver/tests/test_forms_blockdevice.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/tests/test_forms_blockdevice.py 2016-10-19 18:09:00 +0000
@@ -12,6 +12,7 @@
12from maasserver.forms import (12from maasserver.forms import (
13 CreatePhysicalBlockDeviceForm,13 CreatePhysicalBlockDeviceForm,
14 FormatBlockDeviceForm,14 FormatBlockDeviceForm,
15 UpdateDeployedPhysicalBlockDeviceForm,
15 UpdatePhysicalBlockDeviceForm,16 UpdatePhysicalBlockDeviceForm,
16 UpdateVirtualBlockDeviceForm,17 UpdateVirtualBlockDeviceForm,
17)18)
@@ -245,6 +246,40 @@
245 ))246 ))
246247
247248
249class TestUpdateDeployedPhysicalBlockDeviceForm(MAASServerTestCase):
250
251 def test_requires_no_fields(self):
252 block_device = factory.make_PhysicalBlockDevice()
253 form = UpdateDeployedPhysicalBlockDeviceForm(
254 instance=block_device, data={})
255 self.assertTrue(form.is_valid(), form.errors)
256 self.assertItemsEqual([], form.errors.keys())
257
258 def test_updates_deployed_physical_block_device(self):
259 block_device = factory.make_PhysicalBlockDevice()
260 name = factory.make_name("sd")
261 model = factory.make_name("model")
262 serial = factory.make_name("serial")
263 id_path = factory.make_absolute_path()
264 form = UpdateDeployedPhysicalBlockDeviceForm(
265 instance=block_device, data={
266 'name': name,
267 'model': model,
268 'serial': serial,
269 'id_path': id_path,
270 })
271 self.assertTrue(form.is_valid(), form.errors)
272 block_device = form.save()
273 self.assertThat(block_device, MatchesStructure.byEquality(
274 name=name,
275 model=model,
276 serial=serial,
277 id_path=id_path,
278 size=block_device.size,
279 block_size=block_device.block_size,
280 ))
281
282
248class TestUpdateVirtualBlockDeviceForm(MAASServerTestCase):283class TestUpdateVirtualBlockDeviceForm(MAASServerTestCase):
249284
250 def test_requires_no_fields(self):285 def test_requires_no_fields(self):
251286
=== modified file 'src/maasserver/tests/test_forms_interface.py'
--- src/maasserver/tests/test_forms_interface.py 2016-09-27 14:24:19 +0000
+++ src/maasserver/tests/test_forms_interface.py 2016-10-19 18:09:00 +0000
@@ -20,6 +20,7 @@
20 BondInterfaceForm,20 BondInterfaceForm,
21 BridgeInterfaceForm,21 BridgeInterfaceForm,
22 ControllerInterfaceForm,22 ControllerInterfaceForm,
23 DeployedInterfaceForm,
23 InterfaceForm,24 InterfaceForm,
24 PhysicalInterfaceForm,25 PhysicalInterfaceForm,
25 VLANInterfaceForm,26 VLANInterfaceForm,
@@ -104,6 +105,27 @@
104 name=interface.name, vlan=None, enabled=interface.enabled))105 name=interface.name, vlan=None, enabled=interface.enabled))
105106
106107
108class DeployedInterfaceFormTest(MAASServerTestCase):
109
110 def test__updates_interface(self):
111 interface = factory.make_Interface(
112 INTERFACE_TYPE.PHYSICAL, name='eth0')
113 new_name = 'eth1'
114 new_mac = factory.make_mac_address()
115 form = DeployedInterfaceForm(
116 instance=interface,
117 data={
118 'name': new_name,
119 'mac_address': new_mac,
120 })
121 self.assertTrue(form.is_valid(), form.errors)
122 interface = form.save()
123 self.assertThat(
124 interface,
125 MatchesStructure.byEquality(
126 name=new_name, mac_address=new_mac))
127
128
107class PhysicalInterfaceFormTest(MAASServerTestCase):129class PhysicalInterfaceFormTest(MAASServerTestCase):
108130
109 def test__creates_physical_interface(self):131 def test__creates_physical_interface(self):
110132
=== modified file 'src/maasserver/websockets/handlers/machine.py'
--- src/maasserver/websockets/handlers/machine.py 2016-10-13 01:35:42 +0000
+++ src/maasserver/websockets/handlers/machine.py 2016-10-19 18:09:00 +0000
@@ -45,6 +45,7 @@
45 AcquiredBridgeInterfaceForm,45 AcquiredBridgeInterfaceForm,
46 BondInterfaceForm,46 BondInterfaceForm,
47 BridgeInterfaceForm,47 BridgeInterfaceForm,
48 DeployedInterfaceForm,
48 InterfaceForm,49 InterfaceForm,
49 PhysicalInterfaceForm,50 PhysicalInterfaceForm,
50 VLANInterfaceForm,51 VLANInterfaceForm,
@@ -811,7 +812,10 @@
811812
812 node = self.get_object(params)813 node = self.get_object(params)
813 interface = Interface.objects.get(node=node, id=params["interface_id"])814 interface = Interface.objects.get(node=node, id=params["interface_id"])
814 interface_form = InterfaceForm.get_interface_form(interface.type)815 if node.status == NODE_STATUS.DEPLOYED:
816 interface_form = DeployedInterfaceForm
817 else:
818 interface_form = InterfaceForm.get_interface_form(interface.type)
815 form = interface_form(instance=interface, data=params)819 form = interface_form(instance=interface, data=params)
816 if form.is_valid():820 if form.is_valid():
817 interface = form.save()821 interface = form.save()
818822
=== modified file 'src/maasserver/websockets/handlers/tests/test_machine.py'
--- src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-12 16:34:23 +0000
+++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-10-19 18:09:00 +0000
@@ -2326,6 +2326,20 @@
2326 self.assertEqual(new_name, interface.name)2326 self.assertEqual(new_name, interface.name)
2327 self.assertEqual(new_vlan, interface.vlan)2327 self.assertEqual(new_vlan, interface.vlan)
23282328
2329 def test_update_interface_for_deployed_node(self):
2330 user = factory.make_admin()
2331 node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
2332 handler = MachineHandler(user, {})
2333 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2334 new_name = factory.make_name("name")
2335 handler.update_interface({
2336 "system_id": node.system_id,
2337 "interface_id": interface.id,
2338 "name": new_name,
2339 })
2340 interface = reload_object(interface)
2341 self.assertEqual(new_name, interface.name)
2342
2329 def test_update_interface_raises_ValidationError(self):2343 def test_update_interface_raises_ValidationError(self):
2330 user = factory.make_admin()2344 user = factory.make_admin()
2331 node = factory.make_Node()2345 node = factory.make_Node()