Merge lp:~lamont/maas/bug-1660188 into lp:~maas-committers/maas/trunk

Proposed by LaMont Jones
Status: Rejected
Rejected by: LaMont Jones
Proposed branch: lp:~lamont/maas/bug-1660188
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 2262 lines (+992/-336)
14 files modified
src/maasserver/enum.py (+23/-0)
src/maasserver/forms/__init__.py (+24/-2)
src/maasserver/forms/interface.py (+20/-0)
src/maasserver/forms/tests/test_device.py (+1/-0)
src/maasserver/static/js/angular/controllers/node_details.js (+36/-8)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+130/-44)
src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+5/-3)
src/maasserver/static/js/angular/factories/devices.js (+7/-4)
src/maasserver/static/js/angular/factories/tests/test_devices.js (+1/-1)
src/maasserver/static/partials/node-details.html (+150/-95)
src/maasserver/static/partials/nodes-list.html (+3/-1)
src/maasserver/websockets/handlers/device.py (+132/-45)
src/maasserver/websockets/handlers/node.py (+95/-90)
src/maasserver/websockets/handlers/tests/test_device.py (+365/-43)
To merge this branch: bzr merge lp:~lamont/maas/bug-1660188
Reviewer Review Type Date Requested Status
Blake Rouse (community) Needs Fixing
Review via email: mp+317147@code.launchpad.net

Commit message

Create Device Details page.

Description of the change

Create Device Details page.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Just a few comments from quickly going over this code.

You were worried about query counts going up. That's a good thing to have noticed and to worry about, but it's nothing unusual: it's the curse of ORMs. The dehydrate_* method pattern is nice to work with but it exacerbates this problem.

The first tool I'd reach for is tests, specifically using CountQueries / count_queries. Typically I'd do something like "render n things" and then "render n+1 things" and assert that the query count is the same for both. Then I'd add pre-fetching in the "right" places until the test passes.

Overall, there's a lot of stuff in here. It's already at ~1000 lines of diff, but it's densely populated. Splitting it into 2 or more pieces will help a lot at review time.

Revision history for this message
LaMont Jones (lamont) wrote :

Replies.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Overall almost there. Couple of issues with the directives, just always use ng-if instead of ng-show or ng-hide. ng-if is better performance as it actually removes the dom elements from the page. There has been some effort to transition all to ng-if but that has not happened 100% yet.

I did run into this issue on the devices details page. The DHCP warning shouldn't be there and I think editing should be possible for a device if a rack controller if no rack controllers. I really want to remove that limitation for machines as well, but not yet, sigh...

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Forgot the link to the screenshot:

http://imgur.com/a/SiPpq

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2017-02-17 21:27:46 +0000
+++ src/maasserver/enum.py 2017-02-28 04:16:58 +0000
@@ -7,6 +7,8 @@
7 'CACHE_MODE_TYPE',7 'CACHE_MODE_TYPE',
8 'CACHE_MODE_TYPE_CHOICES',8 'CACHE_MODE_TYPE_CHOICES',
9 'COMPONENT',9 'COMPONENT',
10 'DEVICE_IP_ASSIGNMENT_TYPE',
11 'DEVICE_IP_ASSIGNMENT_TYPE_CHOICES',
10 'FILESYSTEM_GROUP_TYPE',12 'FILESYSTEM_GROUP_TYPE',
11 'FILESYSTEM_GROUP_TYPE_CHOICES',13 'FILESYSTEM_GROUP_TYPE_CHOICES',
12 'FILESYSTEM_TYPE',14 'FILESYSTEM_TYPE',
@@ -206,6 +208,27 @@
206 ADMIN = 'admin_node'208 ADMIN = 'admin_node'
207209
208210
211class DEVICE_IP_ASSIGNMENT_TYPE:
212 """The vocabulary of a `Device`'s possible IP assignment type. This value
213 is calculated by looking at the overall model for a `Device`. This is not
214 set directly on the model."""
215 #: Device is outside of MAAS control.
216 EXTERNAL = "external"
217
218 #: Device receives ip address from `NodeGroupInterface` dynamic range.
219 DYNAMIC = "dynamic"
220
221 #: Device has ip address assigned from `NodeGroupInterface` static range.
222 STATIC = "static"
223
224
225DEVICE_IP_ASSIGNMENT_TYPE_CHOICES = (
226 (DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC, "Dynamic"),
227 (DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL, "External"),
228 (DEVICE_IP_ASSIGNMENT_TYPE.STATIC, "Static"),
229)
230
231
209class PRESEED_TYPE:232class PRESEED_TYPE:
210 """Types of preseed documents that can be generated."""233 """Types of preseed documents that can be generated."""
211 COMMISSIONING = 'commissioning'234 COMMISSIONING = 'commissioning'
212235
=== modified file 'src/maasserver/forms/__init__.py'
--- src/maasserver/forms/__init__.py 2017-02-17 14:23:04 +0000
+++ src/maasserver/forms/__init__.py 2017-02-28 04:16:58 +0000
@@ -803,27 +803,42 @@
803 required=False, initial=None,803 required=False, initial=None,
804 queryset=Node.objects.all(), to_field_name='system_id')804 queryset=Node.objects.all(), to_field_name='system_id')
805805
806 zone = forms.ModelChoiceField(
807 label="Physical zone", required=False,
808 initial=Zone.objects.get_default_zone,
809 queryset=Zone.objects.all(), to_field_name='name')
810
806 class Meta:811 class Meta:
807 model = Device812 model = Device
808813
809 fields = NodeForm.Meta.fields + (814 fields = NodeForm.Meta.fields + (
810 'parent',815 'parent',
816 'zone',
811 )817 )
812818
819 zone = forms.ModelChoiceField(
820 label="Physical zone", required=False,
821 initial=Zone.objects.get_default_zone,
822 queryset=Zone.objects.all(), to_field_name='name')
823
813 def __init__(self, request=None, *args, **kwargs):824 def __init__(self, request=None, *args, **kwargs):
814 super(DeviceForm, self).__init__(*args, **kwargs)825 super(DeviceForm, self).__init__(*args, **kwargs)
815 self.request = request826 self.request = request
816827
817 instance = kwargs.get('instance')828 instance = kwargs.get('instance')
818 self.set_up_initial_device(instance)829 self.set_up_initial_device(instance)
830 if instance is not None:
831 self.initial['zone'] = instance.zone.name
819832
820 def set_up_initial_device(self, instance):833 def set_up_initial_device(self, instance):
821 """Initialize the 'parent' field if a device instance was given.834 """Initialize the 'parent' field if a device instance was given.
822835
823 This is a workaround for Django bug #17657.836 This is a workaround for Django bug #17657.
824 """837 """
825 if instance is not None and instance.parent is not None:838 if instance is not None:
826 self.initial['parent'] = instance.parent.system_id839 if instance.parent is not None:
840 self.initial['parent'] = instance.parent.system_id
841 self.initial['zone'] = instance.zone.name
827842
828 def save(self, commit=True):843 def save(self, commit=True):
829 device = super(DeviceForm, self).save(commit=False)844 device = super(DeviceForm, self).save(commit=False)
@@ -832,6 +847,10 @@
832 # Set the owner: devices are owned by their creator.847 # Set the owner: devices are owned by their creator.
833 device.owner = self.request.user848 device.owner = self.request.user
834849
850 zone = self.cleaned_data.get('zone')
851 if zone:
852 device.zone = zone
853
835 # If the device has a parent and no domain was provided,854 # If the device has a parent and no domain was provided,
836 # inherit the parent's domain.855 # inherit the parent's domain.
837 if device.parent:856 if device.parent:
@@ -839,6 +858,9 @@
839 device.parent.domain):858 device.parent.domain):
840 device.domain = device.parent.domain859 device.domain = device.parent.domain
841860
861 zone = self.cleaned_data.get('zone')
862 if zone:
863 device.zone = zone
842 device.save()864 device.save()
843 return device865 return device
844866
845867
=== modified file 'src/maasserver/forms/interface.py'
--- src/maasserver/forms/interface.py 2017-02-17 14:23:04 +0000
+++ src/maasserver/forms/interface.py 2017-02-28 04:16:58 +0000
@@ -16,6 +16,7 @@
16 BOND_LACP_RATE_CHOICES,16 BOND_LACP_RATE_CHOICES,
17 BOND_MODE_CHOICES,17 BOND_MODE_CHOICES,
18 BOND_XMIT_HASH_POLICY_CHOICES,18 BOND_XMIT_HASH_POLICY_CHOICES,
19 DEVICE_IP_ASSIGNMENT_TYPE,
19 INTERFACE_TYPE,20 INTERFACE_TYPE,
20 IPADDRESS_TYPE,21 IPADDRESS_TYPE,
21 NODE_TYPE,22 NODE_TYPE,
@@ -56,6 +57,16 @@
56 accept_ra = forms.NullBooleanField(required=False)57 accept_ra = forms.NullBooleanField(required=False)
57 autoconf = forms.NullBooleanField(required=False)58 autoconf = forms.NullBooleanField(required=False)
5859
60 # Device parameters
61 ip_assignment = forms.MultipleChoiceField(
62 choices=(
63 ('static', DEVICE_IP_ASSIGNMENT_TYPE.STATIC),
64 ('dynamic', DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC),
65 ('external', DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL)),
66 required=False)
67 ip_address = forms.GenericIPAddressField(
68 unpack_ipv4=True, required=False)
69
59 @staticmethod70 @staticmethod
60 def get_interface_form(type):71 def get_interface_form(type):
61 try:72 try:
@@ -148,9 +159,18 @@
148 msg = "Parents are related to different nodes."159 msg = "Parents are related to different nodes."
149 set_form_error(self, 'name', msg)160 set_form_error(self, 'name', msg)
150161
162 def clean_device(self, cleaned_data):
163 ip_assignment = cleaned_data.get('ip_assignment')
164 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
165 # Dynamic means that there is no IP address stored.
166 cleaned_data['ip_address'] = None
167 return cleaned_data
168
151 def clean(self):169 def clean(self):
152 cleaned_data = super(InterfaceForm, self).clean()170 cleaned_data = super(InterfaceForm, self).clean()
153 self.clean_parents_all_same_node(cleaned_data.get('parents'))171 self.clean_parents_all_same_node(cleaned_data.get('parents'))
172 if self.node.node_type == NODE_TYPE.DEVICE:
173 cleaned_data = self.clean_device(cleaned_data)
154 return cleaned_data174 return cleaned_data
155175
156 def _set_param(self, interface, key):176 def _set_param(self, interface, key):
157177
=== modified file 'src/maasserver/forms/tests/test_device.py'
--- src/maasserver/forms/tests/test_device.py 2017-02-17 14:23:04 +0000
+++ src/maasserver/forms/tests/test_device.py 2017-02-28 04:16:58 +0000
@@ -40,6 +40,7 @@
40 'parent',40 'parent',
41 'disable_ipv4',41 'disable_ipv4',
42 'swap_size',42 'swap_size',
43 'zone',
43 ], list(form.fields))44 ], list(form.fields))
4445
45 def test_changes_device_parent(self):46 def test_changes_device_parent(self):
4647
=== modified file 'src/maasserver/static/js/angular/controllers/node_details.js'
--- src/maasserver/static/js/angular/controllers/node_details.js 2017-01-28 00:51:47 +0000
+++ src/maasserver/static/js/angular/controllers/node_details.js 2017-02-28 04:16:58 +0000
@@ -6,14 +6,22 @@
66
7angular.module('MAAS').controller('NodeDetailsController', [7angular.module('MAAS').controller('NodeDetailsController', [
8 '$scope', '$rootScope', '$routeParams', '$location', '$interval',8 '$scope', '$rootScope', '$routeParams', '$location', '$interval',
9 'DevicesManager',
9 'MachinesManager', 'ControllersManager', 'ZonesManager', 'GeneralManager',10 'MachinesManager', 'ControllersManager', 'ZonesManager', 'GeneralManager',
10 'UsersManager', 'TagsManager', 'DomainsManager', 'ManagerHelperService',11 'UsersManager', 'TagsManager', 'DomainsManager', 'ManagerHelperService',
11 'ServicesManager', 'ErrorService', 'ValidationService', function(12 'ServicesManager', 'ErrorService', 'ValidationService', function(
12 $scope, $rootScope, $routeParams, $location, $interval,13 $scope, $rootScope, $routeParams, $location, $interval, DevicesManager,
13 MachinesManager, ControllersManager, ZonesManager, GeneralManager,14 MachinesManager, ControllersManager, ZonesManager, GeneralManager,
14 UsersManager, TagsManager, DomainsManager, ManagerHelperService,15 UsersManager, TagsManager, DomainsManager, ManagerHelperService,
15 ServicesManager, ErrorService, ValidationService) {16 ServicesManager, ErrorService, ValidationService) {
1617
18 // Mapping of device.ip_assignment to viewable text.
19 var DEVICE_IP_ASSIGNMENT = {
20 external: "External",
21 dynamic: "Dynamic",
22 "static": "Static"
23 };
24
17 // Set title and page.25 // Set title and page.
18 $rootScope.title = "Loading...";26 $rootScope.title = "Loading...";
19 $rootScope.page = "nodes";27 $rootScope.page = "nodes";
@@ -84,6 +92,11 @@
84 parameters: {}92 parameters: {}
85 };93 };
8694
95 // Get the display text for device ip assignment type.
96 $scope.getDeviceIPAssignment = function(ipAssignment) {
97 return DEVICE_IP_ASSIGNMENT[ipAssignment];
98 };
99
87 // Events section.100 // Events section.
88 $scope.events = {101 $scope.events = {
89 limit: 10102 limit: 10
@@ -674,9 +687,10 @@
674 $scope.hasInvalidArchitecture = function() {687 $scope.hasInvalidArchitecture = function() {
675 if(angular.isObject($scope.node)) {688 if(angular.isObject($scope.node)) {
676 return (689 return (
677 $scope.node.architecture === "" ||690 !$scope.isDevice && (
678 $scope.summary.architecture.options.indexOf(691 $scope.node.architecture === "" ||
679 $scope.node.architecture) === -1);692 $scope.summary.architecture.options.indexOf(
693 $scope.node.architecture) === -1));
680 } else {694 } else {
681 return false;695 return false;
682 }696 }
@@ -685,9 +699,10 @@
685 // Return true if the current architecture selection is invalid.699 // Return true if the current architecture selection is invalid.
686 $scope.invalidArchitecture = function() {700 $scope.invalidArchitecture = function() {
687 return (701 return (
688 $scope.summary.architecture.selected === "" ||702 !$scope.isDevice && (
689 $scope.summary.architecture.options.indexOf(703 $scope.summary.architecture.selected === "" ||
690 $scope.summary.architecture.selected) === -1);704 $scope.summary.architecture.options.indexOf(
705 $scope.summary.architecture.selected) === -1));
691 };706 };
692707
693 // Return true if at least a rack controller is connected to the708 // Return true if at least a rack controller is connected to the
@@ -702,7 +717,7 @@
702 return (717 return (
703 $scope.isRackControllerConnected() &&718 $scope.isRackControllerConnected() &&
704 $scope.isSuperUser() &&719 $scope.isSuperUser() &&
705 !$scope.isController);720 !$scope.isController && !$scope.isDevice);
706 };721 };
707722
708 // Called to edit the node name.723 // Called to edit the node name.
@@ -1029,6 +1044,7 @@
1029 // Load all the required managers.1044 // Load all the required managers.
1030 ManagerHelperService.loadManagers($scope, [1045 ManagerHelperService.loadManagers($scope, [
1031 MachinesManager,1046 MachinesManager,
1047 DevicesManager,
1032 ControllersManager,1048 ControllersManager,
1033 ZonesManager,1049 ZonesManager,
1034 GeneralManager,1050 GeneralManager,
@@ -1040,11 +1056,19 @@
1040 if('controller' === $routeParams.type) {1056 if('controller' === $routeParams.type) {
1041 $scope.nodesManager = ControllersManager;1057 $scope.nodesManager = ControllersManager;
1042 $scope.isController = true;1058 $scope.isController = true;
1059 $scope.isDevice = false;
1043 $scope.type_name = 'controller';1060 $scope.type_name = 'controller';
1044 $scope.type_name_title = 'Controller';1061 $scope.type_name_title = 'Controller';
1062 }else if('device' === $routeParams.type) {
1063 $scope.nodesManager = DevicesManager;
1064 $scope.isController = false;
1065 $scope.isDevice = true;
1066 $scope.type_name = 'device';
1067 $scope.type_name_title = 'Device';
1045 }else{1068 }else{
1046 $scope.nodesManager = MachinesManager;1069 $scope.nodesManager = MachinesManager;
1047 $scope.isController = false;1070 $scope.isController = false;
1071 $scope.isDevice = false;
1048 $scope.type_name = 'machine';1072 $scope.type_name = 'machine';
1049 $scope.type_name_title = 'Machine';1073 $scope.type_name_title = 'Machine';
1050 }1074 }
@@ -1062,6 +1086,10 @@
1062 }, function(error) {1086 }, function(error) {
1063 ErrorService.raiseError(error);1087 ErrorService.raiseError(error);
1064 });1088 });
1089 activeNode = $scope.nodesManager.getActiveItem();
1090 }
1091 if($scope.isDevice) {
1092 $scope.ip_assignment = activeNode.ip_assignment;
1065 }1093 }
10661094
1067 // Poll for architectures, hwe_kernels, and osinfo the whole1095 // Poll for architectures, hwe_kernels, and osinfo the whole
10681096
=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-10-26 19:05:43 +0000
+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2017-02-28 04:16:58 +0000
@@ -171,6 +171,28 @@
171 EDIT: "edit"171 EDIT: "edit"
172 };172 };
173173
174 var IP_ASSIGNMENT = {
175 DYNAMIC: "dynamic",
176 EXTERNAL: "external",
177 STATIC: "static"
178 };
179
180 // Device ip assignment options.
181 $scope.ipAssignments = [
182 {
183 name: IP_ASSIGNMENT.EXTERNAL,
184 text: "External"
185 },
186 {
187 name: IP_ASSIGNMENT.DYNAMIC,
188 text: "Dynamic"
189 },
190 {
191 name: IP_ASSIGNMENT.STATIC,
192 text: "Static"
193 }
194 ];
195
174 // Set the initial values for this scope.196 // Set the initial values for this scope.
175 $scope.loaded = false;197 $scope.loaded = false;
176 $scope.nodeHasLoaded = false;198 $scope.nodeHasLoaded = false;
@@ -542,6 +564,10 @@
542 // If the user is not the superuser, pretend it's not Ready.564 // If the user is not the superuser, pretend it's not Ready.
543 return false;565 return false;
544 }566 }
567 if ($scope.$parent.isDevice) {
568 // Devices are never Ready, for our purposes, for now.
569 return true;
570 }
545 if ($scope.$parent.isController) {571 if ($scope.$parent.isController) {
546 // Controllers are always Ready, for our purposes.572 // Controllers are always Ready, for our purposes.
547 return true;573 return true;
@@ -563,8 +589,8 @@
563 // If the user is not the superuser, pretend it's not Ready.589 // If the user is not the superuser, pretend it's not Ready.
564 return false;590 return false;
565 }591 }
566 if ($scope.$parent.isController) {592 if ($scope.$parent.isController || $scope.$parent.isDevice) {
567 // Controllers never in limited mode.593 // Controllers and Devices are never in limited mode.
568 return false;594 return false;
569 }595 }
570 return (596 return (
@@ -581,9 +607,9 @@
581 // If the user is not a superuser, disable the networking panel.607 // If the user is not a superuser, disable the networking panel.
582 return true;608 return true;
583 }609 }
584 if ($scope.$parent.isController) {610 if ($scope.$parent.isController || $scope.$parent.isDevice) {
585 // Never disable the full networking panel when its a611 // Never disable the full networking panel when its a
586 // controller.612 // Controller or Device.
587 return false;613 return false;
588 }614 }
589 if (angular.isObject($scope.node) &&615 if (angular.isObject($scope.node) &&
@@ -772,18 +798,29 @@
772 $scope.edit = function(nic) {798 $scope.edit = function(nic) {
773 $scope.selectedInterfaces = [$scope.getUniqueKey(nic)];799 $scope.selectedInterfaces = [$scope.getUniqueKey(nic)];
774 $scope.selectedMode = SELECTION_MODE.EDIT;800 $scope.selectedMode = SELECTION_MODE.EDIT;
775 $scope.editInterface = {801 if($scope.$parent.isDevice) {
776 id: nic.id,802 $scope.editInterface = {
777 name: nic.name,803 id: nic.id,
778 mac_address: nic.mac_address,804 name: nic.name,
779 tags: angular.copy(nic.tags),805 mac_address: nic.mac_address,
780 fabric: nic.fabric,806 subnet: nic.subnet,
781 vlan: nic.vlan,807 ip_address: nic.ip_address,
782 subnet: nic.subnet,808 ip_assignment: nic.ip_assignment
783 mode: nic.mode,809 };
784 ip_address: nic.ip_address,810 }else{
785 link_id: nic.link_id811 $scope.editInterface = {
786 };812 id: nic.id,
813 name: nic.name,
814 mac_address: nic.mac_address,
815 tags: angular.copy(nic.tags),
816 fabric: nic.fabric,
817 vlan: nic.vlan,
818 subnet: nic.subnet,
819 mode: nic.mode,
820 ip_address: nic.ip_address,
821 link_id: nic.link_id
822 };
823 }
787 };824 };
788825
789 // Called when the fabric is changed.826 // Called when the fabric is changed.
@@ -828,6 +865,10 @@
828 }865 }
829 };866 };
830867
868 $scope.ipAssignementChanged = function(nic) {
869 return;
870 };
871
831 // Called to cancel edit mode.872 // Called to cancel edit mode.
832 $scope.editCancel = function(nic) {873 $scope.editCancel = function(nic) {
833 $scope.selectedInterfaces = [];874 $scope.selectedInterfaces = [];
@@ -837,13 +878,24 @@
837878
838 // Save the following interface on the node.879 // Save the following interface on the node.
839 $scope.saveInterface = function(nic) {880 $scope.saveInterface = function(nic) {
840 var params = {881 var params;
841 "name": nic.name,882 if($scope.$parent.isDevice) {
842 "mac_address": nic.mac_address,883 params = {
843 "tags": nic.tags.map(884 "name": nic.name,
844 function(tag) { return tag.text; })885 "mac_address": nic.mac_address,
845 };886 "ip_assignment": nic.ip_assignment,
846 if(nic.vlan !== null) {887 "ip_address": nic.ip_address,
888 "subnet": nic.subnet
889 };
890 }else{
891 params = {
892 "name": nic.name,
893 "mac_address": nic.mac_address,
894 "tags": nic.tags.map(
895 function(tag) { return tag.text; })
896 };
897 }
898 if(nic.vlan !== undefined && nic.vlan !== null) {
847 params.vlan = nic.vlan.id;899 params.vlan = nic.vlan.id;
848 } else {900 } else {
849 params.vlan = null;901 params.vlan = null;
@@ -866,6 +918,9 @@
866 var params = {918 var params = {
867 "mode": nic.mode919 "mode": nic.mode
868 };920 };
921 if($scope.$parent.isDevice) {
922 params.ip_assignment=nic.ip_assignment;
923 }
869 if(angular.isObject(nic.subnet)) {924 if(angular.isObject(nic.subnet)) {
870 params.subnet = nic.subnet.id;925 params.subnet = nic.subnet.id;
871 }926 }
@@ -1110,9 +1165,18 @@
11101165
1111 // Perform the add action over the websocket.1166 // Perform the add action over the websocket.
1112 $scope.addInterface = function(type) {1167 $scope.addInterface = function(type) {
1113 if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {1168 var nic;
1169 if($scope.$parent.isDevice) {
1170 nic = {
1171 id: $scope.newInterface.parent.id,
1172 ip_assignment: $scope.newInterface.ip_assignment,
1173 subnet: $scope.newInterface.subnet,
1174 ip_address: $scope.newInterface.ip_address
1175 };
1176 $scope.saveInterfaceLink(nic);
1177 } else if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {
1114 // Add a link to the current interface.1178 // Add a link to the current interface.
1115 var nic = {1179 nic = {
1116 id: $scope.newInterface.parent.id,1180 id: $scope.newInterface.parent.id,
1117 mode: $scope.newInterface.mode,1181 mode: $scope.newInterface.mode,
1118 subnet: $scope.newInterface.subnet,1182 subnet: $scope.newInterface.subnet,
@@ -1416,17 +1480,29 @@
1416 $scope.showCreatePhysical = function() {1480 $scope.showCreatePhysical = function() {
1417 if($scope.selectedMode === SELECTION_MODE.NONE) {1481 if($scope.selectedMode === SELECTION_MODE.NONE) {
1418 $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL;1482 $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL;
1419 $scope.newInterface = {1483 if($scope.$parent.isDevice) {
1420 name: getNextName("eth"),1484 $scope.newInterface = {
1421 macAddress: "",1485 name: getNextName("eth"),
1422 macError: false,1486 macAddress: "",
1423 tags: [],1487 macError: false,
1424 errorMsg: null,1488 tags: [],
1425 fabric: $scope.fabrics[0],1489 errorMsg: null,
1426 vlan: getDefaultVLAN($scope.fabrics[0]),1490 subnet: null,
1427 subnet: null,1491 ip_assignment: IP_ASSIGNMENT.DYNAMIC
1428 mode: LINK_MODE.LINK_UP1492 };
1429 };1493 }else{
1494 $scope.newInterface = {
1495 name: getNextName("eth"),
1496 macAddress: "",
1497 macError: false,
1498 tags: [],
1499 errorMsg: null,
1500 fabric: $scope.fabrics[0],
1501 vlan: getDefaultVLAN($scope.fabrics[0]),
1502 subnet: null,
1503 mode: LINK_MODE.LINK_UP
1504 };
1505 }
1430 }1506 }
1431 };1507 };
14321508
@@ -1444,14 +1520,24 @@
1444 return;1520 return;
1445 }1521 }
14461522
1447 var params = {1523 var params;
1448 name: $scope.newInterface.name,1524 if($scope.$parent.isDevice) {
1449 tags: $scope.newInterface.tags.map(1525 params = {
1450 function(tag) { return tag.text; }),1526 name: $scope.newInterface.name,
1451 mac_address: $scope.newInterface.macAddress,1527 mac_address: $scope.newInterface.macAddress,
1452 vlan: $scope.newInterface.vlan.id,1528 ip_assignment: $scope.newInterface.ip_assignment,
1453 mode: $scope.newInterface.mode1529 ip_address: $scope.newInterface.ip_address
1454 };1530 };
1531 }else{
1532 params = {
1533 name: $scope.newInterface.name,
1534 tags: $scope.newInterface.tags.map(
1535 function(tag) { return tag.text; }),
1536 mac_address: $scope.newInterface.macAddress,
1537 vlan: $scope.newInterface.vlan.id,
1538 mode: $scope.newInterface.mode
1539 };
1540 }
1455 if(angular.isObject($scope.newInterface.subnet)) {1541 if(angular.isObject($scope.newInterface.subnet)) {
1456 params.subnet = $scope.newInterface.subnet.id;1542 params.subnet = $scope.newInterface.subnet.id;
1457 }1543 }
14581544
=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details.js'
--- src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-01-28 00:51:47 +0000
+++ src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-02-28 04:16:58 +0000
@@ -42,6 +42,7 @@
42 var webSocket;42 var webSocket;
43 beforeEach(inject(function($injector) {43 beforeEach(inject(function($injector) {
44 MachinesManager = $injector.get("MachinesManager");44 MachinesManager = $injector.get("MachinesManager");
45 DevicesManager = $injector.get("DevicesManager");
45 ControllersManager = $injector.get("ControllersManager");46 ControllersManager = $injector.get("ControllersManager");
46 ZonesManager = $injector.get("ZonesManager");47 ZonesManager = $injector.get("ZonesManager");
47 GeneralManager = $injector.get("GeneralManager");48 GeneralManager = $injector.get("GeneralManager");
@@ -136,6 +137,7 @@
136 $routeParams: $routeParams,137 $routeParams: $routeParams,
137 $location: $location,138 $location: $location,
138 MachinesManager: MachinesManager,139 MachinesManager: MachinesManager,
140 DevicesManager: DevicesManager,
139 ControllersManager: ControllersManager,141 ControllersManager: ControllersManager,
140 ZonesManager: ZonesManager,142 ZonesManager: ZonesManager,
141 GeneralManager: GeneralManager,143 GeneralManager: GeneralManager,
@@ -260,9 +262,9 @@
260 var controller = makeController();262 var controller = makeController();
261 expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith(263 expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith(
262 $scope, [264 $scope, [
263 MachinesManager, ControllersManager, ZonesManager,265 MachinesManager, DevicesManager, ControllersManager,
264 GeneralManager, UsersManager, TagsManager, DomainsManager,266 ZonesManager, GeneralManager, UsersManager, TagsManager,
265 ServicesManager]);267 DomainsManager, ServicesManager]);
266 });268 });
267269
268 it("doesnt call setActiveItem if node is loaded", function() {270 it("doesnt call setActiveItem if node is loaded", function() {
269271
=== modified file 'src/maasserver/static/js/angular/factories/devices.js'
--- src/maasserver/static/js/angular/factories/devices.js 2016-09-27 00:47:54 +0000
+++ src/maasserver/static/js/angular/factories/devices.js 2017-02-28 04:16:58 +0000
@@ -11,16 +11,19 @@
1111
12angular.module('MAAS').factory(12angular.module('MAAS').factory(
13 'DevicesManager',13 'DevicesManager',
14 ['$q', '$rootScope', 'RegionConnection', 'Manager', function(14 ['$q', '$rootScope', 'RegionConnection', 'NodesManager', function(
15 $q, $rootScope, RegionConnection, Manager) {15 $q, $rootScope, RegionConnection, NodesManager) {
1616
17 function DevicesManager() {17 function DevicesManager() {
18 Manager.call(this);18 NodesManager.call(this);
1919
20 this._pk = "system_id";20 this._pk = "system_id";
21 this._handler = "device";21 this._handler = "device";
22 this._metadataAttributes = {22 this._metadataAttributes = {
23 "owner": null,23 "owner": null,
24 "subnets": null,
25 "fabrics": null,
26 "spaces": null,
24 "tags": null,27 "tags": null,
25 "zone": function(device) {28 "zone": function(device) {
26 return device.zone.name;29 return device.zone.name;
@@ -34,7 +37,7 @@
34 });37 });
35 }38 }
3639
37 DevicesManager.prototype = new Manager();40 DevicesManager.prototype = new NodesManager();
3841
39 // Create a device.42 // Create a device.
40 DevicesManager.prototype.create = function(node) {43 DevicesManager.prototype.create = function(node) {
4144
=== modified file 'src/maasserver/static/js/angular/factories/tests/test_devices.js'
--- src/maasserver/static/js/angular/factories/tests/test_devices.js 2016-09-27 14:24:19 +0000
+++ src/maasserver/static/js/angular/factories/tests/test_devices.js 2017-02-28 04:16:58 +0000
@@ -46,7 +46,7 @@
46 expect(DevicesManager._pk).toBe("system_id");46 expect(DevicesManager._pk).toBe("system_id");
47 expect(DevicesManager._handler).toBe("device");47 expect(DevicesManager._handler).toBe("device");
48 expect(Object.keys(DevicesManager._metadataAttributes)).toEqual(48 expect(Object.keys(DevicesManager._metadataAttributes)).toEqual(
49 ["owner", "tags", "zone"]);49 ["owner", "subnets", "fabrics", "spaces", "tags", "zone"]);
50 });50 });
5151
52 describe("createInferface", function() {52 describe("createInferface", function() {
5353
=== modified file 'src/maasserver/static/partials/node-details.html'
--- src/maasserver/static/partials/node-details.html 2017-02-17 14:23:04 +0000
+++ src/maasserver/static/partials/node-details.html 2017-02-28 04:16:58 +0000
@@ -30,7 +30,7 @@
30 data-ng-click="saveEditHeader()">Save</a>30 data-ng-click="saveEditHeader()">Save</a>
3131
32 <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->32 <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
33 <p class="page-header__status" data-ng-hide="isController || header.editing">33 <p class="page-header__status" data-ng-if="!isController && !isDevice && !header.editing">
34 {$ node.status $}34 {$ node.status $}
35 <span class="u-text--{$ getPowerStateClass() $} u-margin--left-small"><i class="icon icon--power-{$ getPowerStateClass() $} u-margin--right-tiny"></i>{$ getPowerStateText() $}</span>35 <span class="u-text--{$ getPowerStateClass() $} u-margin--left-small"><i class="icon icon--power-{$ getPowerStateClass() $} u-margin--right-tiny"></i>{$ getPowerStateText() $}</span>
36 <a href="" class="page-header__status-check" data-ng-show="canCheckPowerState()" data-ng-click="checkPowerState()">check now</a>36 <a href="" class="page-header__status-check" data-ng-show="canCheckPowerState()" data-ng-click="checkPowerState()">check now</a>
@@ -47,7 +47,7 @@
47 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->47 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
48 <div class="page-header__dropdown" data-ng-class="{ 'is-open': actionOption }">48 <div class="page-header__dropdown" data-ng-class="{ 'is-open': actionOption }">
4949
50 <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="!node.dhcp_on">50 <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-if="!isDevice && !node.dhcp_on">
51 <p class="page-header__message page-header__message--warning">51 <p class="page-header__message page-header__message--warning">
52 MAAS is not providing DHCP.52 MAAS is not providing DHCP.
53 </p>53 </p>
@@ -89,58 +89,20 @@
89 <div class="page-header__controls">89 <div class="page-header__controls">
90 <a href="" class="button--base button--inline" data-ng-click="actionCancel()">Cancel</a>90 <a href="" class="button--base button--inline" data-ng-click="actionCancel()">Cancel</a>
91 <button class="button--inline" data-ng-class="actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">91 <button class="button--inline" data-ng-class="actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">
92 <span data-ng-if="actionOption.name === 'commission'">Commission92 <span data-ng-if="actionOption.name === 'commission'">Commission {$ type_name $}</span>
93 <span data-ng-if="!isController">machine</span>93 <span data-ng-if="actionOption.name === 'acquire'">Acquire {$ type_name $}</span>
94 <span data-ng-if="isController">controller</span>94 <span data-ng-if="actionOption.name === 'deploy'">Deploy {$ type_name $}</span>
95 </span>95 <span data-ng-if="actionOption.name === 'release'">Release {$ type_name $}</span>
96 <span data-ng-if="actionOption.name === 'acquire'">Acquire96 <span data-ng-if="actionOption.name === 'set-zone'">Set zone for {$ type_name $}</span>
97 <span data-ng-if="!isController">machine</span>97 <span data-ng-if="actionOption.name === 'on'">Power on {$ type_name $}</span>
98 <span data-ng-if="isController">controller</span>98 <span data-ng-if="actionOption.name === 'off'">Power off {$ type_name $}</span>
99 </span>99 <span data-ng-if="actionOption.name === 'abort'">Abort action on {$ type_name $}</span>
100 <span data-ng-if="actionOption.name === 'deploy'">Deploy100 <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue {$ type_name $}</span>
101 <span data-ng-if="!isController">machine</span>101 <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode</span>
102 <span data-ng-if="isController">controller</span>102 <span data-ng-if="actionOption.name === 'mark-broken'">Mark {$ type_name $}</span>
103 </span>103 <span data-ng-if="actionOption.name === 'mark-fixed'">Mark {$ type_name $}</span>
104 <span data-ng-if="actionOption.name === 'release'">Release104 <span data-ng-if="actionOption.name === 'delete'">Delete {$ type_name $}</span>
105 <span data-ng-if="!isController">machine</span>105 <span data-ng-if="actionOption.name === 'import-images'">Import images</span>
106 <span data-ng-if="isController">controller</span>
107 </span>
108 <span data-ng-if="actionOption.name === 'set-zone'">Set zone for
109 <span data-ng-if="!isController">machine</span>
110 <span data-ng-if="isController">controller</span>
111 </span>
112 <span data-ng-if="actionOption.name === 'on'">Power on
113 <span data-ng-if="!isController">machine</span>
114 <span data-ng-if="isController">controller</span>
115 </span>
116 <span data-ng-if="actionOption.name === 'off'">Power off
117 <span data-ng-if="!isController">machine</span>
118 <span data-ng-if="isController">controller</span>
119 </span>
120 <span data-ng-if="actionOption.name === 'abort'">Abort action on
121 <span data-ng-if="!isController">machine</span>
122 <span data-ng-if="isController">controller</span>
123 </span>
124 <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue
125 <span data-ng-if="!isController">machine</span>
126 <span data-ng-if="isController">controller</span>
127 </span>
128 <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode
129 </span>
130 <span data-ng-if="actionOption.name === 'mark-broken'">Mark
131 <span data-ng-if="!isController">machine</span>
132 <span data-ng-if="isController">controller</span> as broken
133 </span>
134 <span data-ng-if="actionOption.name === 'mark-fixed'">Mark
135 <span data-ng-if="!isController">machine</span>
136 <span data-ng-if="isController">controller</span> as fixed
137 </span>
138 <span data-ng-if="actionOption.name === 'delete'">Delete
139 <span data-ng-if="!isController">machine</span>
140 <span data-ng-if="isController">controller</span>
141 </span>
142 <span data-ng-if="actionOption.name === 'import-images'">Import images
143 </span>
144 </button>106 </button>
145 </div>107 </div>
146 </form>108 </form>
@@ -201,12 +163,13 @@
201 </nav> -->163 </nav> -->
202 <div class="row" id="summary">164 <div class="row" id="summary">
203 <div class="wrapper--inner">165 <div class="wrapper--inner">
204 <h2 data-ng-hide="isController" class="eight-col">Machine summary</h2>166 <h2 data-ng-if="!isController && !isDevice" class="eight-col">Machine summary</h2>
205 <h2 data-ng-show="isController" class="eight-col">Controller summary</h2>167 <h2 data-ng-if="isController" class="eight-col">Controller summary</h2>
206 <div data-ng-hide="isController">168 <h2 data-ng-if="isDevice" class="eight-col">Device summary</h2>
169 <div data-ng-if="!isController">
207 <div class="four-col last-col u-align--right" data-ng-hide="summary.editing">170 <div class="four-col last-col u-align--right" data-ng-hide="summary.editing">
208 <a href="" class="button--secondary button--inline"171 <a href="" class="button--secondary button--inline"
209 data-ng-show="canEdit()"172 data-ng-if="canEdit()"
210 data-ng-click="editSummary()">Edit</a>173 data-ng-click="editSummary()">Edit</a>
211 </div>174 </div>
212 <div class="four-col last-col u-align--right ng-hide" data-ng-show="summary.editing">175 <div class="four-col last-col u-align--right ng-hide" data-ng-show="summary.editing">
@@ -219,17 +182,17 @@
219 </div>182 </div>
220 <div class="twelve-col" data-ng-if="isSuperUser()">183 <div class="twelve-col" data-ng-if="isSuperUser()">
221 <div class="p-notification--error"184 <div class="p-notification--error"
222 data-ng-if="!isController && !isRackControllerConnected()">185 data-ng-if="!isController && !isDevice && !isRackControllerConnected()">
223 <p class="p-notification__response">186 <p class="p-notification__response">
224 <span class="p-notification__status">Error:</span>Editing is currently disabled because no rack controller is currently connected to the region.</p>187 <span class="p-notification__status">Error:</span>Editing is currently disabled because no rack controller is currently connected to the region.</p>
225 </div>188 </div>
226 <div class="p-notification--error"189 <div class="p-notification--error"
227 data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">190 data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">
228 <p class="p-notification__response">191 <p class="p-notification__response">
229 <span class="p-notification__status">Error:</span>This machine currently has an invalid architecture. Update the architecture of this machine to make it deployable.</p>192 <span class="p-notification__status">Error:</span>This machine currently has an invalid architecture. Update the architecture of this machine to make it deployable.</p>
230 </div>193 </div>
231 <div class="p-notification--error"194 <div class="p-notification--error"
232 data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">195 data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">
233 <p class="p-notification__response">196 <p class="p-notification__response">
234 <span class="p-notification__status">Error:</span>No boot images have been imported for a valid architecture to be selected. Visit the <a href="images">images page</a> to start the import process.</p>197 <span class="p-notification__status">Error:</span>No boot images have been imported for a valid architecture to be selected. Visit the <a href="images">images page</a> to start the import process.</p>
235 </div>198 </div>
@@ -247,7 +210,7 @@
247 </select>210 </select>
248 </div>211 </div>
249 </div>212 </div>
250 <div class="form__group">213 <div class="form__group" data-ng-if="!isDevice">
251 <label for="architecture" class="form__group-label two-col">Architecture</label>214 <label for="architecture" class="form__group-label two-col">Architecture</label>
252 <div class="form__group-input three-col">215 <div class="form__group-input three-col">
253 <select name="architecture" id="architecture"216 <select name="architecture" id="architecture"
@@ -259,7 +222,7 @@
259 </select>222 </select>
260 </div>223 </div>
261 </div>224 </div>
262 <div class="form__group">225 <div class="form__group" data-ng-if="!isDevice">
263 <label for="min_hwe_kernel" class="form__group-label two-col">Minimum Kernel</label>226 <label for="min_hwe_kernel" class="form__group-label two-col">Minimum Kernel</label>
264 <div class="form__group-input three-col">227 <div class="form__group-input three-col">
265 <select name="min_hwe_kernel" id="min_hwe_kernel" placeholder="No minimum kernel"228 <select name="min_hwe_kernel" id="min_hwe_kernel" placeholder="No minimum kernel"
@@ -293,7 +256,7 @@
293 <dd class="four-col last-col">256 <dd class="four-col last-col">
294 {$ node.zone.name $}257 {$ node.zone.name $}
295 </dd>258 </dd>
296 <dt class="two-col">Architecture</dt>259 <dt class="two-col" data-ng-if="!isDevice">Architecture</dt>
297 <dd class="four-col last-col">260 <dd class="four-col last-col">
298 {$ node.architecture || "Missing" $}261 {$ node.architecture || "Missing" $}
299 </dd>262 </dd>
@@ -336,7 +299,7 @@
336 </dd>299 </dd>
337 </dl>300 </dl>
338 </div>301 </div>
339 <div class="six-col last-col">302 <div class="six-col last-col" data-ng-if="!isDevice">
340 <dl>303 <dl>
341 <dt class="two-col">CPU</dt>304 <dt class="two-col">CPU</dt>
342 <dd class="four-col last-col">305 <dd class="four-col last-col">
@@ -388,7 +351,7 @@
388 </div>351 </div>
389 </div>352 </div>
390 </div>353 </div>
391 <div class="row" id="power" data-ng-show="!isController && isSuperUser()">354 <div class="row" id="power" data-ng-if="!isController && !isDevice && isSuperUser()">
392 <div class="wrapper--inner">355 <div class="wrapper--inner">
393 <h2 class="eight-col">Power</h2>356 <h2 class="eight-col">Power</h2>
394 <div class="four-col last-col u-align--right" data-ng-hide="power.editing">357 <div class="four-col last-col u-align--right" data-ng-hide="power.editing">
@@ -613,8 +576,8 @@
613 <h2 class="title">Interfaces</h2>576 <h2 class="title">Interfaces</h2>
614 </div>577 </div>
615 <div class="twelve-col" data-ng-if="578 <div class="twelve-col" data-ng-if="
616 (!isController && isAllNetworkingDisabled() && isSuperUser()) ||579 !isDevice && ((!isController && isAllNetworkingDisabled() && isSuperUser()) ||
617 !node.on_network || (!isController && !isUbuntuOS())">580 !node.on_network || (!isController && !isUbuntuOS()))">
618 <div class="p-notification--error" data-ng-if="!node.on_network">581 <div class="p-notification--error" data-ng-if="!node.on_network">
619 <p class="p-notification__response">582 <p class="p-notification__response">
620 <span class="p-notification__status">Error:</span>Node must be connected to a network.</p>583 <span class="p-notification__status">Error:</span>Node must be connected to a network.</p>
@@ -635,6 +598,7 @@
635 <div class="table__row">598 <div class="table__row">
636 <div class="table__header table-col--3"></div>599 <div class="table__header table-col--3"></div>
637 <div class="table__header table-col--12">600 <div class="table__header table-col--12">
601 <span data-ng-if="!isDevice">
638 <a class="table__header-link is-active"602 <a class="table__header-link is-active"
639 data-ng-class="{ 'is-active': column == 'name' }"603 data-ng-class="{ 'is-active': column == 'name' }"
640 data-ng-click="column = 'name'">604 data-ng-click="column = 'name'">
@@ -645,11 +609,16 @@
645 data-ng-click="column = 'mac'">609 data-ng-click="column = 'mac'">
646 MAC610 MAC
647 </a>611 </a>
648 </div>612 </span>
649 <div class="table__header table-col--3"><span data-ng-if="!isController">PXE</span></div>613 <span data-ng-if="isDevice">MAC</span>
650 <div class="table__header table-col--9">Type</div>614 </div>
651 <div class="table__header table-col--14">Fabric</div>615 <div class="table__header table-col--3"><span data-ng-if="!isController && !isDevice">PXE</span></div>
652 <div class="table__header table-col--14">VLAN</div>616 <div class="table__header table-col--9"><span data-ng-if="!isDevice">Type</span></div>
617 <div class="table__header table-col--14">
618 <span data-ng-if="!isDevice">Fabric</span>
619 <span data-ng-if="isDevice">IP Assignment</span>
620 </div>
621 <div class="table__header table-col--14"><span data-ng-if="!isDevice">VLAN</span></div>
653 <div class="table__header table-col--18">Subnet</div>622 <div class="table__header table-col--18">Subnet</div>
654 <div class="table__header table-col--27">IP Address</div>623 <div class="table__header table-col--27">IP Address</div>
655 </div>624 </div>
@@ -667,43 +636,48 @@
667 data-ng-if="!isController && isNodeEditingAllowed()">636 data-ng-if="!isController && isNodeEditingAllowed()">
668 <label for="{$ getUniqueKey(interface) $}"></label>637 <label for="{$ getUniqueKey(interface) $}"></label>
669 </div>638 </div>
670 <div class="table__data table-col--12" aria-label="Name" data-ng-show="column == 'name'">639 <div class="table__data table-col--12" aria-label="Name" data-ng-if="!isDevice" data-ng-show="column == 'name'">
671 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>640 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>
672 <input type="text" class="table__input"641 <input type="text" class="table__input"
673 data-ng-if="isEditing(interface) && interface.type != 'vlan'"642 data-ng-if="isEditing(interface) && interface.type != 'vlan'"
674 data-ng-model="editInterface.name"643 data-ng-model="editInterface.name"
675 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">644 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">
676 </div>645 </div>
677 <div class="table__data table-col--12 ng-hide" data-ng-show="column == 'mac'">646 <div class="table__data table-col--12 ng-hide" data-ng-if="!isDevice" data-ng-show="column == 'mac'">
678 {$ interface.mac_address $}647 {$ interface.mac_address $}
679 </div>648 </div>
649 <div class="table__data table-col--12" data-ng-if="isDevice">{$ interface.mac_address $}</div>
680 <div class="table__data table-col--3 u-align--center" aria-label="Boot interface">650 <div class="table__data table-col--3 u-align--center" aria-label="Boot interface">
681 <input type="radio" name="bootInterface" id="{$ interface.name $}" checked651 <input type="radio" name="bootInterface" id="{$ interface.name $}" checked
682 data-ng-if="!isController && isBootInterface(interface)" class="u-display--desktop">652 data-ng-if="!isController && !isDevice && isBootInterface(interface)" class="u-display--desktop">
683 <label for="{$ interface.name $}" class="u-display--desktop"></label>653 <label for="{$ interface.name $}" class="u-display--desktop"></label>
684 <span class="u-display--mobile" data-ng-if="!isController && isBootInterface(interface)">Yes</span>654 <span class="u-display--mobile" data-ng-if="!isController && !isDevice && isBootInterface(interface)">Yes</span>
685 <span class="u-display--mobile" data-ng-if="!isController && !isBootInterface(interface)">No</span>655 <span class="u-display--mobile" data-ng-if="!isController && !isDevice && !isBootInterface(interface)">No</span>
686 </div>656 </div>
687 <div class="table__data table-col--9" aria-label="Type">657 <div class="table__data table-col--9" aria-label="Type">
688 <span data-ng-if="!isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>658 <span data-ng-if="!isDevice && !isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>
689 </div>659 </div>
690 <div class="table__data table-col--14" aria-label="Fabric">660 <div class="table__data table-col--14" aria-label="Fabric">
691 <span data-ng-if="!isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>661 <span data-ng-if="!isDevice && !isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>
662 <span data-ng-if="isDevice && !isEditing(interface)">{$ getDeviceIPAssignment(interface.ip_assignment) $}</span>
692 </div>663 </div>
693 <div class="table__data table-col--14" aria-label="VLAN">664 <div class="table__data table-col--14" aria-label="VLAN">
694 <span data-ng-if="!isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>665 <span data-ng-if="!isDevice && !isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>
695 </div>666 </div>
696 <div class="table__data table-col--18" aria-label="Subnet">667 <div class="table__data table-col--18" aria-label="Subnet">
697 <span data-ng-if="!isEditing(interface) && interface.fabric">{$ getSubnetText(interface.subnet) $}</span>668 <span data-ng-if="!isEditing(interface) && ((isDevice && interface.subnet) || interface.fabric)">{$ getSubnetText(interface.subnet) $}</span>
698 <span data-ng-if="isAllNetworkingDisabled() && interface.discovered[0].subnet_id">669 <span data-ng-if="isAllNetworkingDisabled() && interface.discovered[0].subnet_id">
699 {$ getSubnetText(getSubnet(interface.discovered[0].subnet_id)) $}670 {$ getSubnetText(getSubnet(interface.discovered[0].subnet_id)) $}
700 </span>671 </span>
701 </div>672 </div>
702 <div class="table__data table-col--19" aria-label="IP Address">673 <div class="table__data table-col--19" aria-label="IP Address">
703 <span data-ng-if="!isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric">674 <span data-ng-if="isDevice">
675 {$ interface.ip_address $}
676 </span>
677 <span data-ng-if="!isDevice && !isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric" >
704 {$ interface.ip_address $} ({$ getLinkModeText(interface) $})678 {$ interface.ip_address $} ({$ getLinkModeText(interface) $})
705 </span>679 </span>
706 <span data-ng-if="!isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">680 <span data-ng-if="!isDevice && !isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">
707 {$ interface.discovered[0].ip_address $} (DHCP)681 {$ interface.discovered[0].ip_address $} (DHCP)
708 </span>682 </span>
709 </div>683 </div>
@@ -711,7 +685,7 @@
711 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">685 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">
712 <a class="u-display--desktop icon icon--add tooltip"686 <a class="u-display--desktop icon icon--add tooltip"
713 aria-label="Add Alias or VLAN"687 aria-label="Add Alias or VLAN"
714 data-ng-if="canAddAliasOrVLAN(interface)"688 data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
715 data-ng-click="quickAdd(interface)"></a>689 data-ng-click="quickAdd(interface)"></a>
716 <a class="u-display--desktop icon icon--edit tooltip"690 <a class="u-display--desktop icon icon--edit tooltip"
717 data-ng-if="!cannotEditInterface(interface)"691 data-ng-if="!cannotEditInterface(interface)"
@@ -723,7 +697,7 @@
723 data-ng-click="quickRemove(interface)"></a>697 data-ng-click="quickRemove(interface)"></a>
724 <a class="button--secondary u-display--mobile"698 <a class="button--secondary u-display--mobile"
725 aria-label="Add Alias or VLAN"699 aria-label="Add Alias or VLAN"
726 data-ng-if="canAddAliasOrVLAN(interface)"700 data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
727 data-ng-click="quickAdd(interface)">Add alias or VLAN</a>701 data-ng-click="quickAdd(interface)">Add alias or VLAN</a>
728 <a class="button--secondary u-display--mobile"702 <a class="button--secondary u-display--mobile"
729 data-ng-if="!cannotEditInterface(interface)"703 data-ng-if="!cannotEditInterface(interface)"
@@ -837,8 +811,10 @@
837 <div class="table__data table-col--100" data-ng-if="isEditing(interface)">811 <div class="table__data table-col--100" data-ng-if="isEditing(interface)">
838 <fieldset class="form__fieldset six-col">812 <fieldset class="form__fieldset six-col">
839 <dl>813 <dl>
814 <span data-ng-if="!isDevice">
840 <dt class="two-col">Type</dt>815 <dt class="two-col">Type</dt>
841 <dd class="four-col last-col">{$ getInterfaceTypeText(interface) $}</dd>816 <dd class="four-col last-col">{$ getInterfaceTypeText(interface) $}</dd>
817 </span>
842 </dl>818 </dl>
843 <div class="form__group" data-ng-if="interface.type != 'alias' && interface.type != 'vlan'">819 <div class="form__group" data-ng-if="interface.type != 'alias' && interface.type != 'vlan'">
844 <label for="mac" class="two-col">MAC address</label>820 <label for="mac" class="two-col">MAC address</label>
@@ -847,14 +823,14 @@
847 data-ng-model="editInterface.mac_address"823 data-ng-model="editInterface.mac_address"
848 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">824 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">
849 </div>825 </div>
850 <div class="form__group" data-ng-if="!isLimitedEditingAllowed(interface)">826 <div class="form__group" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
851 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>827 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>
852 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">828 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">
853 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>829 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>
854 </div>830 </div>
855 </div>831 </div>
856 </fieldset>832 </fieldset>
857 <fieldset class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)">833 <fieldset class="form__fieldset six-col last-col" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
858 <div class="form__group">834 <div class="form__group">
859 <label for="fabric" class="two-col">Fabric</label>835 <label for="fabric" class="two-col">Fabric</label>
860 <select name="fabric" class="three-col"836 <select name="fabric" class="three-col"
@@ -904,6 +880,37 @@
904 </div>880 </div>
905 </div>881 </div>
906 </fieldset>882 </fieldset>
883 <fieldset class="form__fieldset six-col last-col" data-ng-if="isDevice && !isLimitedEditingAllowed(interface)">
884 <div class="form__group">
885 <label for="ip-assignment" class="two-col">IP Assignment</label>
886 <select name="ip-assignment" class="three-col"
887 ng-model="editInterface.ip_assignment"
888 data-ng-change="ipAssignementChanged(editInterface)"
889 data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
890 </select>
891 </div>
892 <div class="form__group" data-ng-if="editInterface.ip_assignment === 'static'">
893 <label for="subnet" class="two-col">Subnet</label>
894 <select name="subnet" class="three-col"
895 ng-model="editInterface.subnet"
896 data-ng-change="subnetChanged(newInterface)"
897 data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
898 <option value="" data-ng-hide="interface.links.length > 1">Unconfigured</option>
899 </select>
900 </div>
901 <div class="form__group" data-ng-if="editInterface.ip_assignment !== 'dynamic'">
902 <label for="ip-address" class="two-col">IP address</label>
903 <div class="form__group-input three-col last-col">
904 <input name="ip-address" type="text"
905 placeholder="IP address (optional)"
906 ng-model="editInterface.ip_address"
907 data-ng-class="{ 'has-error': isIPAddressInvalid(editInterface) }">
908 <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(editInterface)">
909 <li>{$ getInterfaceError(editInterface) $}</li>
910 </ul>
911 </div>
912 </div>
913 </fieldset>
907 </div>914 </div>
908 </div>915 </div>
909 <div class="table__row is-active" data-ng-if="isShowingDeleteConfirm()">916 <div class="table__row is-active" data-ng-if="isShowingDeleteConfirm()">
@@ -1124,7 +1131,7 @@
1124 <input type="checkbox" class="checkbox" id="interface-create" disabled="disabled" checked />1131 <input type="checkbox" class="checkbox" id="interface-create" disabled="disabled" checked />
1125 <label for="interface-create"></label>1132 <label for="interface-create"></label>
1126 </div>1133 </div>
1127 <div class="table__data table-col--12">1134 <div class="table__data table-col--12" data-ng-if="!isDevice">
1128 <input type="text" class="table__input"1135 <input type="text" class="table__input"
1129 data-ng-class="{ 'has-error': isInterfaceNameInvalid(newInterface) }"1136 data-ng-class="{ 'has-error': isInterfaceNameInvalid(newInterface) }"
1130 data-ng-model="newInterface.name">1137 data-ng-model="newInterface.name">
@@ -1138,7 +1145,15 @@
1138 </div>1145 </div>
1139 <div class="table__row form form--stack is-active">1146 <div class="table__row form form--stack is-active">
1140 <div class="table__data table-col--100">1147 <div class="table__data table-col--100">
1141 <div class="form__fieldset six-col">1148 <div class="table__data table-col--12" data-ng-if="isDevice">
1149 <div class="form__group">
1150 <label for="mac" class="three-col">MAC address</label>
1151 <input type="text" name="mac" class="table__input"
1152 data-ng-model="newInterface.macAddress"
1153 data-ng-class="{ 'has-error': isMACAddressInvalid(newInterface.macAddress, false) || newInterface.macError }">
1154 </div>
1155 </div>
1156 <div class="form__fieldset six-col" data-ng-if="!isDevice">
1142 <div class="form__group u-display--mobile">1157 <div class="form__group u-display--mobile">
1143 <label for="new-interface-name" class="two-col">Name</label>1158 <label for="new-interface-name" class="two-col">Name</label>
1144 <input type="text" class="three-col" id="new-interface-name"1159 <input type="text" class="three-col" id="new-interface-name"
@@ -1162,7 +1177,8 @@
1162 </div>1177 </div>
1163 </div>1178 </div>
1164 </div>1179 </div>
1165 <div class="form__fieldset six-col last-col">1180 <span data-ng-if="!isDevice">
1181 <div class="form__fieldset six-col last-col">
1166 <div class="form__group">1182 <div class="form__group">
1167 <label for="fabric" class="two-col">Fabric</label>1183 <label for="fabric" class="two-col">Fabric</label>
1168 <select name="fabric" class="three-col"1184 <select name="fabric" class="three-col"
@@ -1203,7 +1219,41 @@
1203 ng-model="newInterface.ip_address"1219 ng-model="newInterface.ip_address"
1204 data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">1220 data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
1205 </div>1221 </div>
1206 </div>1222 </div>
1223 </span>
1224 <span data-ng-if="isDevice">
1225 <div class="form__fieldset six-col last-col">
1226 <div class="form__group">
1227 <label for="ip-assignment" class="two-col">IP Assignment</label>
1228 <select name="ip-assignment" class="three-col"
1229 ng-model="newInterface.ip_assignment"
1230 data-ng-change="ipAssignementChanged(newInterface)"
1231 data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
1232 </select>
1233 </div>
1234 <div class="form__group" data-ng-if="newInterface.ip_assignment === 'static'">
1235 <label for="subnet" class="two-col">Subnet</label>
1236 <select name="subnet" class="three-col"
1237 ng-model="newInterface.subnet"
1238 data-ng-change="subnetChanged(newInterface)"
1239 data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
1240 <option value="" data-ng-hide="interface.links.length > 1">Unconfigured</option>
1241 </select>
1242 </div>
1243 <div class="form__group" data-ng-if="newInterface.ip_assignment !== 'dynamic'">
1244 <label for="ip-address" class="two-col">IP address</label>
1245 <div class="form__group-input three-col last-col">
1246 <input name="ip-address" type="text"
1247 placeholder="IP address (optional)"
1248 ng-model="newInterface.ip_address"
1249 data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
1250 <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(newInterface)">
1251 <li>{$ getInterfaceError(newInterface) $}</li>
1252 </ul>
1253 </div>
1254 </div>
1255 </div>
1256 </span>
1207 </div>1257 </div>
1208 </div>1258 </div>
1209 <div class="table__row is-active">1259 <div class="table__row is-active">
@@ -1221,7 +1271,7 @@
1221 </div>1271 </div>
1222 </main>1272 </main>
1223 </div>1273 </div>
1224 <div data-ng-if="!isController" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">1274 <div data-ng-if="!isController && !isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
1225 <a class="button--secondary button--inline"1275 <a class="button--secondary button--inline"
1226 data-ng-disabled="selectedMode !== null"1276 data-ng-disabled="selectedMode !== null"
1227 data-ng-click="showCreatePhysical()">Add interface</a>1277 data-ng-click="showCreatePhysical()">Add interface</a>
@@ -1232,12 +1282,17 @@
1232 data-ng-disabled="!canCreateBridge()"1282 data-ng-disabled="!canCreateBridge()"
1233 data-ng-click="showCreateBridge()">Create bridge</a>1283 data-ng-click="showCreateBridge()">Create bridge</a>
1234 </div>1284 </div>
1285 <div data-ng-if="isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
1286 <a class="button--secondary button--inline"
1287 data-ng-disabled="selectedMode !== null"
1288 data-ng-click="showCreatePhysical()">Add interface</a>
1289 </div>
1235 </div>1290 </div>
1236 </form>1291 </form>
1237 </div>1292 </div>
1238 </div>1293 </div>
1239 </div>1294 </div>
1240 <div class="row" id="storage" data-ng-hide="isController">1295 <div class="row" id="storage" data-ng-if="!isController && !isDevice">
1241 <div class="wrapper--inner" ng-controller="NodeStorageController">1296 <div class="wrapper--inner" ng-controller="NodeStorageController">
1242 <form>1297 <form>
1243 <div class="twelve-col">1298 <div class="twelve-col">
@@ -2223,7 +2278,7 @@
2223 </form>2278 </form>
2224 </div>2279 </div>
2225 </div>2280 </div>
2226 <div class="row" id="events" data-ng-hide="isController">2281 <div class="row" id="events" data-ng-if="!isController && !isDevice">
2227 <div class="wrapper--inner">2282 <div class="wrapper--inner">
2228 <div class="eight-col">2283 <div class="eight-col">
2229 <h2 class="title">Latest {$ type_name $} events</h2>2284 <h2 class="title">Latest {$ type_name $} events</h2>
22302285
=== modified file 'src/maasserver/static/partials/nodes-list.html'
--- src/maasserver/static/partials/nodes-list.html 2017-02-17 14:23:04 +0000
+++ src/maasserver/static/partials/nodes-list.html 2017-02-28 04:16:58 +0000
@@ -902,7 +902,9 @@
902 <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(device, 'devices')" data-ng-checked="device.$selected" id="{$ device.fqdn $}" data-ng-disabled="hasActionsInProgress('devices')"/>902 <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(device, 'devices')" data-ng-checked="device.$selected" id="{$ device.fqdn $}" data-ng-disabled="hasActionsInProgress('devices')"/>
903 <label for="{$ device.fqdn $}" class="checkbox-label"></label>903 <label for="{$ device.fqdn $}" class="checkbox-label"></label>
904 </td>904 </td>
905 <td class="table-col--24" aria-label="FQDN">{$ device.fqdn $}</td>905 <td class="table-col--24" aria-label="FQDN">
906 <a href="#/node/device/{$ device.system_id $}">{$ device.fqdn $}</a>
907 </td>
906 <td class="table-col--25" aria-label="MAC">908 <td class="table-col--25" aria-label="MAC">
907 {$ device.primary_mac $}909 {$ device.primary_mac $}
908 <span class="extra-macs" data-ng-show="device.extra_macs.length">(+{$ device.extra_macs.length $})</span>910 <span class="extra-macs" data-ng-show="device.extra_macs.length">(+{$ device.extra_macs.length $})</span>
909911
=== modified file 'src/maasserver/websockets/handlers/device.py'
--- src/maasserver/websockets/handlers/device.py 2017-02-17 21:27:46 +0000
+++ src/maasserver/websockets/handlers/device.py 2017-02-28 04:16:58 +0000
@@ -7,7 +7,9 @@
7 "DeviceHandler",7 "DeviceHandler",
8 ]8 ]
99
10from django.core.exceptions import ValidationError
10from maasserver.enum import (11from maasserver.enum import (
12 DEVICE_IP_ASSIGNMENT_TYPE,
11 INTERFACE_LINK_TYPE,13 INTERFACE_LINK_TYPE,
12 IPADDRESS_TYPE,14 IPADDRESS_TYPE,
13 NODE_PERMISSION,15 NODE_PERMISSION,
@@ -17,7 +19,11 @@
17 DeviceForm,19 DeviceForm,
18 DeviceWithMACsForm,20 DeviceWithMACsForm,
19)21)
20from maasserver.forms.interface import PhysicalInterfaceForm22from maasserver.forms.interface import (
23 InterfaceForm,
24 PhysicalInterfaceForm,
25)
26from maasserver.models.interface import Interface
21from maasserver.models.node import Device27from maasserver.models.node import Device
22from maasserver.models.staticipaddress import StaticIPAddress28from maasserver.models.staticipaddress import StaticIPAddress
23from maasserver.models.subnet import Subnet29from maasserver.models.subnet import Subnet
@@ -26,6 +32,7 @@
26from maasserver.websockets.base import (32from maasserver.websockets.base import (
27 HandlerDoesNotExistError,33 HandlerDoesNotExistError,
28 HandlerError,34 HandlerError,
35 HandlerPermissionError,
29 HandlerValidationError,36 HandlerValidationError,
30)37)
31from maasserver.websockets.handlers.node import (38from maasserver.websockets.handlers.node import (
@@ -39,20 +46,6 @@
39maaslog = get_maas_logger("websockets.device")46maaslog = get_maas_logger("websockets.device")
4047
4148
42class DEVICE_IP_ASSIGNMENT:
43 """The vocabulary of a `Device`'s possible IP assignment type. This value
44 is calculated by looking at the overall model for a `Device`. This is not
45 set directly on the model."""
46 #: Device is outside of MAAS control.
47 EXTERNAL = "external"
48
49 #: Device receives ip address from `NodeGroupInterface` dynamic range.
50 DYNAMIC = "dynamic"
51
52 #: Device has ip address assigned from `NodeGroupInterface` static range.
53 STATIC = "static"
54
55
56def get_Interface_from_list(interfaces, mac):49def get_Interface_from_list(interfaces, mac):
57 """Return the `Interface` object with the given MAC address."""50 """Return the `Interface` object with the given MAC address."""
58 # Compare using EUI instances so that we're not concerned with a MAC's51 # Compare using EUI instances so that we're not concerned with a MAC's
@@ -77,6 +70,12 @@
77 'set_active',70 'set_active',
78 'create',71 'create',
79 'create_interface',72 'create_interface',
73 'create_physical',
74 'update_interface',
75 'delete_interface',
76 'link_subnet',
77 'unlink_subnet',
78 'update',
80 'action']79 'action']
81 exclude = [80 exclude = [
82 "creation_type",81 "creation_type",
@@ -149,32 +148,39 @@
149148
150 def dehydrate(self, obj, data, for_list=False):149 def dehydrate(self, obj, data, for_list=False):
151 """Add extra fields to `data`."""150 """Add extra fields to `data`."""
152 data["fqdn"] = obj.fqdn151 data = super().dehydrate(obj, data, for_list=for_list)
153 data["actions"] = list(compile_node_actions(obj, self.user).keys())
154 data["node_type_display"] = obj.get_node_type_display()
155152
153 # We handle interfaces ourselves, because of ip_assignment.
156 boot_interface = obj.get_boot_interface()154 boot_interface = obj.get_boot_interface()
157 data["primary_mac"] = (155 data["primary_mac"] = (
158 "%s" % boot_interface.mac_address156 "%s" % boot_interface.mac_address
159 if boot_interface is not None else "")157 if boot_interface is not None else "")
160 data["extra_macs"] = [158
161 "%s" % interface.mac_address159 if for_list:
162 for interface in obj.interface_set.all()160 data["ip_assignment"] = self.dehydrate_ip_assignment(
163 if interface != boot_interface161 obj, boot_interface)
164 ]162 data["ip_address"] = self.dehydrate_ip_address(obj, boot_interface)
165163 else:
166 data["ip_assignment"] = self.dehydrate_ip_assignment(164 data["interfaces"] = [
167 obj, boot_interface)165 self.dehydrate_interface(interface, obj)
168 data["ip_address"] = self.dehydrate_ip_address(166 for interface in obj.interface_set.all().order_by('name')
169 obj, boot_interface)167 ]
170168 # Propogate the boot interface ip assignment/address to the device.
171 data["tags"] = [169 for iface_data in data["interfaces"]:
172 tag.name170 if iface_data["name"] == boot_interface.name:
173 for tag in obj.tags.all()171 data["ip_assignment"] = iface_data["ip_assignment"]
174 ]172 data["ip_address"] = iface_data["ip_address"]
175 return data173
176174 return data
177 def _get_first_none_discovered_ip(self, ip_addresses):175
176 def dehydrate_interface(self, interface, obj):
177 """Add extra fields to interface data."""
178 data = super().dehydrate_interface(interface, obj)
179 data["ip_assignment"] = self.dehydrate_ip_assignment(obj, interface)
180 data["ip_address"] = self.dehydrate_ip_address(obj, interface)
181 return data
182
183 def _get_first_non_discovered_ip(self, ip_addresses):
178 for ip in ip_addresses:184 for ip in ip_addresses:
179 if ip.alloc_type != IPADDRESS_TYPE.DISCOVERED:185 if ip.alloc_type != IPADDRESS_TYPE.DISCOVERED:
180 return ip186 return ip
@@ -190,15 +196,15 @@
190 return ""196 return ""
191 # We get the IP address from the all() so the cache is used.197 # We get the IP address from the all() so the cache is used.
192 ip_addresses = list(interface.ip_addresses.all())198 ip_addresses = list(interface.ip_addresses.all())
193 first_ip = self._get_first_none_discovered_ip(ip_addresses)199 first_ip = self._get_first_non_discovered_ip(ip_addresses)
194 if first_ip is not None:200 if first_ip is not None:
195 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:201 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:
196 return DEVICE_IP_ASSIGNMENT.DYNAMIC202 return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
197 elif first_ip.subnet is None:203 elif first_ip.subnet is None:
198 return DEVICE_IP_ASSIGNMENT.EXTERNAL204 return DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL
199 else:205 else:
200 return DEVICE_IP_ASSIGNMENT.STATIC206 return DEVICE_IP_ASSIGNMENT_TYPE.STATIC
201 return DEVICE_IP_ASSIGNMENT.DYNAMIC207 return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
202208
203 def dehydrate_ip_address(self, obj, interface):209 def dehydrate_ip_address(self, obj, interface):
204 """Return the IP address for the device."""210 """Return the IP address for the device."""
@@ -207,7 +213,7 @@
207213
208 # Get ip address from StaticIPAddress if available.214 # Get ip address from StaticIPAddress if available.
209 ip_addresses = list(interface.ip_addresses.all())215 ip_addresses = list(interface.ip_addresses.all())
210 first_ip = self._get_first_none_discovered_ip(ip_addresses)216 first_ip = self._get_first_non_discovered_ip(ip_addresses)
211 if first_ip is not None:217 if first_ip is not None:
212 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:218 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:
213 discovered_ip = self._get_first_discovered_ip_with_ip(219 discovered_ip = self._get_first_discovered_ip_with_ip(
@@ -254,6 +260,8 @@
254 "parent": params.get("parent"),260 "parent": params.get("parent"),
255 }261 }
256262
263 if "zone" in params:
264 new_params["zone"] = params["zone"]["name"]
257 if "domain" in params:265 if "domain" in params:
258 new_params["domain"] = params["domain"]["name"]266 new_params["domain"] = params["domain"]["name"]
259267
@@ -268,16 +276,17 @@
268 def _configure_interface(self, interface, params):276 def _configure_interface(self, interface, params):
269 """Configure the interface based on the selection."""277 """Configure the interface based on the selection."""
270 ip_assignment = params["ip_assignment"]278 ip_assignment = params["ip_assignment"]
271 if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:279 interface.ip_addresses.all().delete()
280 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
272 subnet = Subnet.objects.get_best_subnet_for_ip(281 subnet = Subnet.objects.get_best_subnet_for_ip(
273 params["ip_address"])282 params["ip_address"])
274 sticky_ip = StaticIPAddress.objects.create(283 sticky_ip = StaticIPAddress.objects.create(
275 alloc_type=IPADDRESS_TYPE.USER_RESERVED,284 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
276 ip=params["ip_address"], subnet=subnet, user=self.user)285 ip=params["ip_address"], subnet=subnet, user=self.user)
277 interface.ip_addresses.add(sticky_ip)286 interface.ip_addresses.add(sticky_ip)
278 elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:287 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
279 interface.link_subnet(INTERFACE_LINK_TYPE.DHCP, None)288 interface.link_subnet(INTERFACE_LINK_TYPE.DHCP, None)
280 elif ip_assignment == DEVICE_IP_ASSIGNMENT.STATIC:289 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
281 # Convert an empty string to None.290 # Convert an empty string to None.
282 ip_address = params.get("ip_address")291 ip_address = params.get("ip_address")
283 if not ip_address:292 if not ip_address:
@@ -316,6 +325,76 @@
316 else:325 else:
317 raise HandlerValidationError(form.errors)326 raise HandlerValidationError(form.errors)
318327
328 def create_physical(self, params):
329 """Create a physical interface, an alias for create_interface."""
330 return self.create_interface(params)
331
332 def update_interface(self, params):
333 """Update the interface."""
334 # Only admin users can perform update.
335 if not reload_object(self.user).is_superuser:
336 raise HandlerPermissionError()
337
338 device = self.get_object(params)
339 interface = Interface.objects.get(
340 node=device, id=params["interface_id"])
341 interface_form = InterfaceForm.get_interface_form(interface.type)
342 form = interface_form(instance=interface, data=params)
343 if form.is_valid():
344 interface = form.save()
345 self._configure_interface(interface, params)
346 return self.full_dehydrate(reload_object(device))
347 else:
348 raise ValidationError(form.errors)
349
350 def delete_interface(self, params):
351 """Delete the interface."""
352 # Only admin users can perform delete.
353 if not reload_object(self.user).is_superuser:
354 raise HandlerPermissionError()
355
356 node = self.get_object(params)
357 interface = Interface.objects.get(node=node, id=params["interface_id"])
358 interface.delete()
359
360 def link_subnet(self, params):
361 """Create or update the link."""
362 # Only admin users can perform update.
363 if not reload_object(self.user).is_superuser:
364 raise HandlerPermissionError()
365
366 node = self.get_object(params)
367 interface = Interface.objects.get(node=node, id=params["interface_id"])
368 if params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
369 mode = INTERFACE_LINK_TYPE.STATIC
370 elif params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
371 mode = INTERFACE_LINK_TYPE.DHCP
372 else:
373 mode = INTERFACE_LINK_TYPE.LINK_UP
374 subnet = None
375 if "subnet" in params:
376 subnet = Subnet.objects.get(id=params["subnet"])
377 if "link_id" in params:
378 # We are updating an already existing link.
379 interface.update_link_by_id(
380 params["link_id"], mode, subnet,
381 ip_address=params.get("ip_address", None))
382 elif params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
383 # We are creating a new link.
384 interface.link_subnet(
385 INTERFACE_LINK_TYPE.STATIC, subnet,
386 ip_address=params.get("ip_address", None))
387
388 def unlink_subnet(self, params):
389 """Delete the link."""
390 # Only admin users can perform unlink.
391 if not reload_object(self.user).is_superuser:
392 raise HandlerPermissionError()
393
394 node = self.get_object(params)
395 interface = Interface.objects.get(node=node, id=params["interface_id"])
396 interface.unlink_subnet_by_id(params["link_id"])
397
319 def action(self, params):398 def action(self, params):
320 """Perform the action on the object."""399 """Perform the action on the object."""
321 obj = self.get_object(params)400 obj = self.get_object(params)
@@ -327,3 +406,11 @@
327 "%s action is not available for this device." % action_name)406 "%s action is not available for this device." % action_name)
328 extra_params = params.get("extra", {})407 extra_params = params.get("extra", {})
329 return action.execute(**extra_params)408 return action.execute(**extra_params)
409
410 def update(self, params):
411 """Update the object from params."""
412 # Only admin users can perform update.
413 if not reload_object(self.user).is_superuser:
414 raise HandlerPermissionError()
415
416 return super().update(params)
330417
=== modified file 'src/maasserver/websockets/handlers/node.py'
--- src/maasserver/websockets/handlers/node.py 2017-02-17 14:23:04 +0000
+++ src/maasserver/websockets/handlers/node.py 2017-02-28 04:16:58 +0000
@@ -16,6 +16,7 @@
16 FILESYSTEM_FORMAT_TYPE_CHOICES,16 FILESYSTEM_FORMAT_TYPE_CHOICES,
17 FILESYSTEM_FORMAT_TYPE_CHOICES_DICT,17 FILESYSTEM_FORMAT_TYPE_CHOICES_DICT,
18 NODE_STATUS,18 NODE_STATUS,
19 NODE_TYPE,
19)20)
20from maasserver.models.cacheset import CacheSet21from maasserver.models.cacheset import CacheSet
21from maasserver.models.config import Config22from maasserver.models.config import Config
@@ -99,36 +100,13 @@
99 def dehydrate(self, obj, data, for_list=False):100 def dehydrate(self, obj, data, for_list=False):
100 """Add extra fields to `data`."""101 """Add extra fields to `data`."""
101 data["fqdn"] = obj.fqdn102 data["fqdn"] = obj.fqdn
102 data["status"] = obj.display_status()
103 data["status_code"] = obj.status
104 data["actions"] = list(compile_node_actions(obj, self.user).keys())103 data["actions"] = list(compile_node_actions(obj, self.user).keys())
105 data["memory"] = obj.display_memory()
106 data["node_type_display"] = obj.get_node_type_display()104 data["node_type_display"] = obj.get_node_type_display()
107105
108 data["extra_macs"] = [106 data["extra_macs"] = [
109 "%s" % mac_address107 "%s" % mac_address
110 for mac_address in obj.get_extra_macs()108 for mac_address in obj.get_extra_macs()
111 ]109 ]
112 boot_interface = obj.get_boot_interface()
113 if boot_interface is not None:
114 data["pxe_mac"] = "%s" % boot_interface.mac_address
115 data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
116 else:
117 data["pxe_mac"] = data["pxe_mac_vendor"] = ""
118
119 blockdevices = self.get_blockdevices_for(obj)
120 physical_blockdevices = [
121 blockdevice for blockdevice in blockdevices
122 if isinstance(blockdevice, PhysicalBlockDevice)
123 ]
124 data["physical_disk_count"] = len(physical_blockdevices)
125 data["storage"] = "%3.1f" % (
126 sum([
127 blockdevice.size
128 for blockdevice in physical_blockdevices
129 ]) / (1000 ** 3))
130 data["storage_tags"] = self.get_all_storage_tags(blockdevices)
131
132 subnets = self.get_all_subnets(obj)110 subnets = self.get_all_subnets(obj)
133 data["subnets"] = [subnet.cidr for subnet in subnets]111 data["subnets"] = [subnet.cidr for subnet in subnets]
134 data["fabrics"] = self.get_all_fabric_names(obj, subnets)112 data["fabrics"] = self.get_all_fabric_names(obj, subnets)
@@ -138,73 +116,100 @@
138 tag.name116 tag.name
139 for tag in obj.tags.all()117 for tag in obj.tags.all()
140 ]118 ]
141 data["osystem"] = obj.get_osystem(119 if obj.node_type != NODE_TYPE.DEVICE:
142 default=self.default_osystem)120 data["memory"] = obj.display_memory()
143 data["distro_series"] = obj.get_distro_series(121 data["status"] = obj.display_status()
144 default=self.default_distro_series)122 data["status_code"] = obj.status
145 data["dhcp_on"] = self.get_providing_dhcp(obj)123 boot_interface = obj.get_boot_interface()
124 if boot_interface is not None:
125 data["pxe_mac"] = "%s" % boot_interface.mac_address
126 data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
127 else:
128 data["pxe_mac"] = data["pxe_mac_vendor"] = ""
129
130 blockdevices = self.get_blockdevices_for(obj)
131 physical_blockdevices = [
132 blockdevice for blockdevice in blockdevices
133 if isinstance(blockdevice, PhysicalBlockDevice)
134 ]
135 data["physical_disk_count"] = len(physical_blockdevices)
136 data["storage"] = "%3.1f" % (
137 sum(
138 blockdevice.size
139 for blockdevice in physical_blockdevices
140 ) / (1000 ** 3))
141 data["storage_tags"] = self.get_all_storage_tags(blockdevices)
142
143 data["osystem"] = obj.get_osystem(
144 default=self.default_osystem)
145 data["distro_series"] = obj.get_distro_series(
146 default=self.default_distro_series)
147 data["dhcp_on"] = self.get_providing_dhcp(obj)
146 if not for_list:148 if not for_list:
147 data["hwe_kernel"] = make_hwe_kernel_ui_text(obj.hwe_kernel)
148
149 data["power_type"] = obj.power_type
150 data["power_parameters"] = self.dehydrate_power_parameters(
151 obj.power_parameters)
152 data["power_bmc_node_count"] = obj.bmc.node_set.count() if (
153 obj.bmc is not None) else 0
154
155 # Network
156 data["interfaces"] = [
157 self.dehydrate_interface(interface, obj)
158 for interface in obj.interface_set.all().order_by('name')
159 ]
160 data["on_network"] = obj.on_network()149 data["on_network"] = obj.on_network()
161150 if obj.node_type != NODE_TYPE.DEVICE:
162 # Storage151 # XXX lamont 2017-02-15 Much of this should be split out into
163 data["disks"] = sorted(chain(152 # individual methods, rather than having this huge block of
164 (self.dehydrate_blockdevice(blockdevice, obj)153 # dense code here.
165 for blockdevice in blockdevices),154 # Network
166 (self.dehydrate_volume_group(volume_group) for volume_group155 data["interfaces"] = [
167 in VolumeGroup.objects.filter_by_node(obj)),156 self.dehydrate_interface(interface, obj)
168 (self.dehydrate_cache_set(cache_set) for cache_set157 for interface in obj.interface_set.all().order_by('name')
169 in CacheSet.objects.get_cache_sets_for_node(obj)),158 ]
170 ), key=itemgetter("name"))159
171 data["supported_filesystems"] = [160 data["hwe_kernel"] = make_hwe_kernel_ui_text(obj.hwe_kernel)
172 {'key': key, 'ui': ui}161
173 for key, ui in FILESYSTEM_FORMAT_TYPE_CHOICES162 data["power_type"] = obj.power_type
174 ]163 data["power_parameters"] = self.dehydrate_power_parameters(
175 data["storage_layout_issues"] = obj.storage_layout_issues()164 obj.power_parameters)
176 data["special_filesystems"] = [165 data["power_bmc_node_count"] = obj.bmc.node_set.count() if (
177 self.dehydrate_filesystem(filesystem)166 obj.bmc is not None) else 0
178 for filesystem in obj.special_filesystems.order_by("id")167
179 ]168 # Storage
180169 data["disks"] = sorted(chain(
181 # Events170 (self.dehydrate_blockdevice(blockdevice, obj)
182 data["events"] = self.dehydrate_events(obj)171 for blockdevice in blockdevices),
183172 (self.dehydrate_volume_group(volume_group) for volume_group
184 # Machine output173 in VolumeGroup.objects.filter_by_node(obj)),
185 data = self.dehydrate_summary_output(obj, data)174 (self.dehydrate_cache_set(cache_set) for cache_set
186 # XXX ltrager 2017-01-27 - Show the testing results in the175 in CacheSet.objects.get_cache_sets_for_node(obj)),
187 # commissioning table until we get the testing UI done.176 ), key=itemgetter("name"))
188 if obj.current_testing_script_set is not None:177 data["supported_filesystems"] = [
189 data["commissioning_results"] = self.dehydrate_script_set(178 {'key': key, 'ui': ui}
190 chain(179 for key, ui in FILESYSTEM_FORMAT_TYPE_CHOICES
191 obj.current_commissioning_script_set,180 ]
192 obj.current_testing_script_set,181 data["storage_layout_issues"] = obj.storage_layout_issues()
193 ))182 data["special_filesystems"] = [
194 else:183 self.dehydrate_filesystem(filesystem)
195 data["commissioning_results"] = self.dehydrate_script_set(184 for filesystem in obj.special_filesystems.order_by("id")
196 obj.current_commissioning_script_set)185 ]
197 data["installation_results"] = self.dehydrate_script_set(186
198 obj.current_installation_script_set)187 # Events
199188 data["events"] = self.dehydrate_events(obj)
200 # Third party drivers189
201 if Config.objects.get_config('enable_third_party_drivers'):190 # Machine output
202 driver = get_third_party_driver(obj)191 data = self.dehydrate_summary_output(obj, data)
203 if "module" in driver and "comment" in driver:192 # XXX ltrager 2017-01-27 - Show the testing results in the
204 data["third_party_driver"] = {193 # commissioning table until we get the testing UI done.
205 "module": driver["module"],194 if obj.current_testing_script_set is not None:
206 "comment": driver["comment"],195 data["commissioning_results"] = self.dehydrate_script_set(
207 }196 chain(
197 obj.current_commissioning_script_set,
198 obj.current_testing_script_set,
199 ))
200 else:
201 data["commissioning_results"] = self.dehydrate_script_set(
202 obj.current_commissioning_script_set)
203 data["installation_results"] = self.dehydrate_script_set(
204 obj.current_installation_script_set)
205 # Third party drivers
206 if Config.objects.get_config('enable_third_party_drivers'):
207 driver = get_third_party_driver(obj)
208 if "module" in driver and "comment" in driver:
209 data["third_party_driver"] = {
210 "module": driver["module"],
211 "comment": driver["comment"],
212 }
208213
209 return data214 return data
210215
@@ -495,8 +500,8 @@
495 def get_all_space_names(self, subnets):500 def get_all_space_names(self, subnets):
496 space_names = set()501 space_names = set()
497 for subnet in subnets:502 for subnet in subnets:
498 if subnet.space is not None:503 if subnet.vlan.space is not None:
499 space_names.add(subnet.space.name)504 space_names.add(subnet.vlan.space.name)
500 return list(space_names)505 return list(space_names)
501506
502 def get_blockdevices_for(self, obj):507 def get_blockdevices_for(self, obj):
503508
=== modified file 'src/maasserver/websockets/handlers/tests/test_device.py'
--- src/maasserver/websockets/handlers/tests/test_device.py 2017-01-28 00:51:47 +0000
+++ src/maasserver/websockets/handlers/tests/test_device.py 2017-02-28 04:16:58 +0000
@@ -5,7 +5,15 @@
55
6__all__ = []6__all__ = []
77
8from operator import itemgetter
9import random
10from unittest.mock import ANY
11
8from maasserver.enum import (12from maasserver.enum import (
13 DEVICE_IP_ASSIGNMENT_TYPE,
14 DEVICE_IP_ASSIGNMENT_TYPE_CHOICES,
15 INTERFACE_LINK_TYPE,
16 INTERFACE_TYPE,
9 IPADDRESS_TYPE,17 IPADDRESS_TYPE,
10 NODE_TYPE,18 NODE_TYPE,
11)19)
@@ -28,13 +36,17 @@
28 dehydrate_datetime,36 dehydrate_datetime,
29 HandlerDoesNotExistError,37 HandlerDoesNotExistError,
30 HandlerError,38 HandlerError,
39 HandlerPermissionError,
31 HandlerValidationError,40 HandlerValidationError,
32)41)
33from maasserver.websockets.handlers.device import (42from maasserver.websockets.handlers.device import DeviceHandler
34 DEVICE_IP_ASSIGNMENT,
35 DeviceHandler,
36)
37from maastesting.djangotestcase import count_queries43from maastesting.djangotestcase import count_queries
44from maastesting.matchers import MockCalledOnceWith
45from maastesting.matchers import MockNotCalled
46from netaddr import (
47 IPAddress,
48 IPNetwork,
49)
38from testtools import ExpectedException50from testtools import ExpectedException
39from testtools.matchers import (51from testtools.matchers import (
40 Equals,52 Equals,
@@ -44,27 +56,25 @@
4456
45class TestDeviceHandler(MAASTransactionServerTestCase):57class TestDeviceHandler(MAASTransactionServerTestCase):
4658
47 def dehydrate_ip_assignment(self, device):59 def dehydrate_ip_assignment(self, device, interface):
48 boot_interface = device.get_boot_interface()60 if interface is None:
49 if boot_interface is None:
50 return ""61 return ""
51 ip_address = boot_interface.ip_addresses.exclude(62 ip_address = interface.ip_addresses.exclude(
52 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()63 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()
53 if ip_address is not None:64 if ip_address is not None:
54 if ip_address.alloc_type == IPADDRESS_TYPE.DHCP:65 if ip_address.alloc_type == IPADDRESS_TYPE.DHCP:
55 return DEVICE_IP_ASSIGNMENT.DYNAMIC66 return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
56 elif ip_address.subnet is None:67 elif ip_address.subnet is None:
57 return DEVICE_IP_ASSIGNMENT.EXTERNAL68 return DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL
58 else:69 else:
59 return DEVICE_IP_ASSIGNMENT.STATIC70 return DEVICE_IP_ASSIGNMENT_TYPE.STATIC
60 return DEVICE_IP_ASSIGNMENT.DYNAMIC71 return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
6172
62 def dehydrate_ip_address(self, device):73 def dehydrate_ip_address(self, device, interface):
63 """Return the IP address for the device."""74 """Return the IP address for the device."""
64 boot_interface = device.get_boot_interface()75 if interface is None:
65 if boot_interface is None:
66 return None76 return None
67 static_ip = boot_interface.ip_addresses.exclude(77 static_ip = interface.ip_addresses.exclude(
68 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()78 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()
69 if static_ip is not None:79 if static_ip is not None:
70 ip = static_ip.get_ip()80 ip = static_ip.get_ip()
@@ -72,8 +82,58 @@
72 return "%s" % ip82 return "%s" % ip
73 return None83 return None
7484
85 def dehydrate_interface(self, interface, obj):
86 """Dehydrate a `interface` into a interface definition."""
87 # Sort the links by ID that way they show up in the same order in
88 # the UI.
89 links = sorted(interface.get_links(), key=itemgetter("id"))
90 for link in links:
91 # Replace the subnet object with the subnet_id. The client will
92 # use this information to pull the subnet information from the
93 # websocket.
94 subnet = link.pop("subnet", None)
95 if subnet is not None:
96 link["subnet_id"] = subnet.id
97 data = {
98 "id": interface.id,
99 "type": interface.type,
100 "name": interface.get_name(),
101 "enabled": interface.is_enabled(),
102 "tags": interface.tags,
103 "is_boot": interface == obj.get_boot_interface(),
104 "mac_address": "%s" % interface.mac_address,
105 "vlan_id": interface.vlan_id,
106 "parents": [
107 nic.id
108 for nic in interface.parents.all()
109 ],
110 "children": [
111 nic.child.id
112 for nic in interface.children_relationships.all()
113 ],
114 "links": links,
115 "ip_assignment": self.dehydrate_ip_assignment(obj, interface),
116 "ip_address": self.dehydrate_ip_address(obj, interface),
117 }
118 return data
119
75 def dehydrate_device(self, node, user, for_list=False):120 def dehydrate_device(self, node, user, for_list=False):
76 boot_interface = node.get_boot_interface()121 boot_interface = node.get_boot_interface()
122 subnets = set(
123 ip_address.subnet
124 for interface in node.interface_set.all()
125 for ip_address in interface.ip_addresses.all()
126 if ip_address.subnet is not None)
127 space_names = set(
128 subnet.space.name
129 for subnet in subnets
130 if subnet.space is not None)
131 fabric_names = set(
132 iface.vlan.fabric.name
133 for iface in node.interface_set.all()
134 if iface.vlan is not None)
135 fabric_names.update({subnet.vlan.fabric.name for subnet in subnets})
136 boot_interface = node.get_boot_interface()
77 data = {137 data = {
78 "actions": list(compile_node_actions(node, user).keys()),138 "actions": list(compile_node_actions(node, user).keys()),
79 "bmc": node.bmc_id,139 "bmc": node.bmc_id,
@@ -95,8 +155,17 @@
95 "%s" % boot_interface.mac_address),155 "%s" % boot_interface.mac_address),
96 "parent": (156 "parent": (
97 node.parent.system_id if node.parent is not None else None),157 node.parent.system_id if node.parent is not None else None),
98 "ip_address": self.dehydrate_ip_address(node),158 "ip_address": self.dehydrate_ip_address(node, boot_interface),
99 "ip_assignment": self.dehydrate_ip_assignment(node),159 "ip_assignment": self.dehydrate_ip_assignment(
160 node, boot_interface),
161 "interfaces": [
162 self.dehydrate_interface(interface, node)
163 for interface in node.interface_set.all().order_by('name')
164 ],
165 "subnets": [subnet.cidr for subnet in subnets],
166 "fabrics": list(fabric_names),
167 "spaces": list(space_names),
168 "on_network": node.on_network(),
100 "owner": "" if node.owner is None else node.owner.username,169 "owner": "" if node.owner is None else node.owner.username,
101 "swap_size": node.swap_size,170 "swap_size": node.swap_size,
102 "system_id": node.system_id,171 "system_id": node.system_id,
@@ -121,6 +190,9 @@
121 "ip_address",190 "ip_address",
122 "ip_assignment",191 "ip_assignment",
123 "node_type_display",192 "node_type_display",
193 "subnets",
194 "spaces",
195 "fabrics",
124 ]196 ]
125 for key in list(data):197 for key in list(data):
126 if key not in allowed_fields:198 if key not in allowed_fields:
@@ -133,20 +205,20 @@
133 for a device. This will setup the model to make sure the device will205 for a device. This will setup the model to make sure the device will
134 match `ip_assignment`."""206 match `ip_assignment`."""
135 if ip_assignment is None:207 if ip_assignment is None:
136 ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT)208 ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
137 if owner is None:209 if owner is None:
138 owner = factory.make_User()210 owner = factory.make_User()
139 device = factory.make_Node(211 device = factory.make_Node(
140 node_type=NODE_TYPE.DEVICE,212 node_type=NODE_TYPE.DEVICE,
141 interface=True, owner=owner)213 interface=True, owner=owner)
142 interface = device.get_boot_interface()214 interface = device.get_boot_interface()
143 if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:215 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
144 subnet = factory.make_Subnet()216 subnet = factory.make_Subnet()
145 factory.make_StaticIPAddress(217 factory.make_StaticIPAddress(
146 alloc_type=IPADDRESS_TYPE.USER_RESERVED,218 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
147 ip=factory.pick_ip_in_network(subnet.get_ipnetwork()),219 ip=factory.pick_ip_in_network(subnet.get_ipnetwork()),
148 subnet=subnet, user=owner)220 subnet=subnet, user=owner)
149 elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:221 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
150 factory.make_StaticIPAddress(222 factory.make_StaticIPAddress(
151 alloc_type=IPADDRESS_TYPE.DHCP, ip="", interface=interface)223 alloc_type=IPADDRESS_TYPE.DHCP, ip="", interface=interface)
152 else:224 else:
@@ -206,7 +278,7 @@
206 handler.list({}))278 handler.list({}))
207279
208 @transactional280 @transactional
209 def test_list_num_queries_is_independent_of_num_devices(self):281 def test_list_num_queries_is_the_expected_number(self):
210 owner = factory.make_User()282 owner = factory.make_User()
211 handler = DeviceHandler(owner, {})283 handler = DeviceHandler(owner, {})
212 self.make_devices(10, owner=owner)284 self.make_devices(10, owner=owner)
@@ -222,6 +294,21 @@
222 self.assertEqual(294 self.assertEqual(
223 query_10_count, 12,295 query_10_count, 12,
224 "Number of queries has changed; make sure this is expected.")296 "Number of queries has changed; make sure this is expected.")
297
298 @transactional
299 def test_list_num_queries_is_independent_of_num_devices(self):
300 owner = factory.make_User()
301 handler = DeviceHandler(owner, {})
302 self.make_devices(10, owner=owner)
303 query_10_count, _ = count_queries(handler.list, {})
304 self.make_devices(10, owner=owner)
305 query_20_count, _ = count_queries(handler.list, {})
306
307 # This check is to notify the developer that a change was made that
308 # affects the number of queries performed when doing a node listing.
309 # It is important to keep this number as low as possible. A larger
310 # number means regiond has to do more work slowing down its process
311 # and slowing down the client waiting for the response.
225 self.assertEqual(312 self.assertEqual(
226 query_10_count, query_20_count,313 query_10_count, query_20_count,
227 "Number of queries is not independent to the number of nodes.")314 "Number of queries is not independent to the number of nodes.")
@@ -308,7 +395,7 @@
308 "primary_mac": mac,395 "primary_mac": mac,
309 "interfaces": [{396 "interfaces": [{
310 "mac": mac,397 "mac": mac,
311 "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,398 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
312 }],399 }],
313 })400 })
314 self.expectThat(created_device["hostname"], Equals(hostname))401 self.expectThat(created_device["hostname"], Equals(hostname))
@@ -316,7 +403,7 @@
316 self.expectThat(created_device["extra_macs"], Equals([]))403 self.expectThat(created_device["extra_macs"], Equals([]))
317 self.expectThat(404 self.expectThat(
318 created_device["ip_assignment"],405 created_device["ip_assignment"],
319 Equals(DEVICE_IP_ASSIGNMENT.DYNAMIC))406 Equals(DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC))
320 self.expectThat(created_device["ip_address"], Is(None))407 self.expectThat(created_device["ip_address"], Is(None))
321 self.expectThat(created_device["owner"], Equals(user.username))408 self.expectThat(created_device["owner"], Equals(user.username))
322409
@@ -332,13 +419,13 @@
332 "primary_mac": mac,419 "primary_mac": mac,
333 "interfaces": [{420 "interfaces": [{
334 "mac": mac,421 "mac": mac,
335 "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,422 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
336 "ip_address": ip_address,423 "ip_address": ip_address,
337 }],424 }],
338 })425 })
339 self.expectThat(426 self.expectThat(
340 created_device["ip_assignment"],427 created_device["ip_assignment"],
341 Equals(DEVICE_IP_ASSIGNMENT.EXTERNAL))428 Equals(DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL))
342 self.expectThat(created_device["ip_address"], Equals(ip_address))429 self.expectThat(created_device["ip_address"], Equals(ip_address))
343 self.expectThat(430 self.expectThat(
344 StaticIPAddress.objects.filter(ip=ip_address).count(),431 StaticIPAddress.objects.filter(ip=ip_address).count(),
@@ -356,13 +443,13 @@
356 "primary_mac": mac,443 "primary_mac": mac,
357 "interfaces": [{444 "interfaces": [{
358 "mac": mac,445 "mac": mac,
359 "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,446 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
360 "subnet": subnet.id,447 "subnet": subnet.id,
361 }],448 }],
362 })449 })
363 self.expectThat(450 self.expectThat(
364 created_device["ip_assignment"],451 created_device["ip_assignment"],
365 Equals(DEVICE_IP_ASSIGNMENT.STATIC))452 Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
366 static_interface = Interface.objects.get(mac_address=MAC(mac))453 static_interface = Interface.objects.get(mac_address=MAC(mac))
367 observed_subnet = static_interface.ip_addresses.first().subnet454 observed_subnet = static_interface.ip_addresses.first().subnet
368 self.expectThat(455 self.expectThat(
@@ -386,14 +473,14 @@
386 "primary_mac": mac,473 "primary_mac": mac,
387 "interfaces": [{474 "interfaces": [{
388 "mac": mac,475 "mac": mac,
389 "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,476 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
390 "subnet": subnet.id,477 "subnet": subnet.id,
391 "ip_address": ip_address,478 "ip_address": ip_address,
392 }],479 }],
393 })480 })
394 self.expectThat(481 self.expectThat(
395 created_device["ip_assignment"],482 created_device["ip_assignment"],
396 Equals(DEVICE_IP_ASSIGNMENT.STATIC))483 Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
397 self.expectThat(created_device["ip_address"], Equals(ip_address))484 self.expectThat(created_device["ip_address"], Equals(ip_address))
398 static_interface = Interface.objects.get(mac_address=MAC(mac))485 static_interface = Interface.objects.get(mac_address=MAC(mac))
399 observed_subnet = static_interface.ip_addresses.first().subnet486 observed_subnet = static_interface.ip_addresses.first().subnet
@@ -423,13 +510,13 @@
423 "interfaces": [510 "interfaces": [
424 {511 {
425 "mac": mac_static,512 "mac": mac_static,
426 "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,513 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
427 "subnet": subnet.id,514 "subnet": subnet.id,
428 "ip_address": static_ip_address,515 "ip_address": static_ip_address,
429 },516 },
430 {517 {
431 "mac": mac_external,518 "mac": mac_external,
432 "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,519 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
433 "ip_address": external_ip_address,520 "ip_address": external_ip_address,
434 },521 },
435 ],522 ],
@@ -442,7 +529,7 @@
442 Equals([mac_external]))529 Equals([mac_external]))
443 self.expectThat(530 self.expectThat(
444 created_device["ip_assignment"],531 created_device["ip_assignment"],
445 Equals(DEVICE_IP_ASSIGNMENT.STATIC))532 Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
446 self.expectThat(533 self.expectThat(
447 created_device["ip_address"], Equals(static_ip_address))534 created_device["ip_address"], Equals(static_ip_address))
448 static_interface = Interface.objects.get(mac_address=MAC(mac_static))535 static_interface = Interface.objects.get(mac_address=MAC(mac_static))
@@ -467,7 +554,7 @@
467 "primary_mac": mac.lower(), # Lowercase.554 "primary_mac": mac.lower(), # Lowercase.
468 "interfaces": [{555 "interfaces": [{
469 "mac": mac.upper(), # Uppercase.556 "mac": mac.upper(), # Uppercase.
470 "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,557 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
471 }],558 }],
472 })559 })
473 self.assertThat(created_device["primary_mac"], Equals(mac))560 self.assertThat(created_device["primary_mac"], Equals(mac))
@@ -482,7 +569,7 @@
482 "primary_mac": mac, # Colons.569 "primary_mac": mac, # Colons.
483 "interfaces": [{570 "interfaces": [{
484 "mac": mac.replace(":", "-"), # Hyphens.571 "mac": mac.replace(":", "-"), # Hyphens.
485 "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,572 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
486 }],573 }],
487 })574 })
488 self.assertThat(created_device["primary_mac"], Equals(mac))575 self.assertThat(created_device["primary_mac"], Equals(mac))
@@ -511,12 +598,12 @@
511 updated_device = handler.create_interface({598 updated_device = handler.create_interface({
512 "system_id": device.system_id,599 "system_id": device.system_id,
513 "mac_address": mac,600 "mac_address": mac,
514 "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,601 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
515 })602 })
516 self.expectThat(updated_device["primary_mac"], Equals(mac))603 self.expectThat(updated_device["primary_mac"], Equals(mac))
517 self.expectThat(604 self.expectThat(
518 updated_device["ip_assignment"],605 updated_device["ip_assignment"],
519 Equals(DEVICE_IP_ASSIGNMENT.DYNAMIC))606 Equals(DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC))
520607
521 @transactional608 @transactional
522 def test_create_interface_creates_with_external_ip_assignment(self):609 def test_create_interface_creates_with_external_ip_assignment(self):
@@ -528,13 +615,13 @@
528 updated_device = handler.create_interface({615 updated_device = handler.create_interface({
529 "system_id": device.system_id,616 "system_id": device.system_id,
530 "mac_address": mac,617 "mac_address": mac,
531 "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,618 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
532 "ip_address": ip_address,619 "ip_address": ip_address,
533 })620 })
534 self.expectThat(updated_device["primary_mac"], Equals(mac))621 self.expectThat(updated_device["primary_mac"], Equals(mac))
535 self.expectThat(622 self.expectThat(
536 updated_device["ip_assignment"],623 updated_device["ip_assignment"],
537 Equals(DEVICE_IP_ASSIGNMENT.EXTERNAL))624 Equals(DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL))
538 self.expectThat(625 self.expectThat(
539 StaticIPAddress.objects.filter(ip=ip_address).count(),626 StaticIPAddress.objects.filter(ip=ip_address).count(),
540 Equals(1), "StaticIPAddress was not created.")627 Equals(1), "StaticIPAddress was not created.")
@@ -549,13 +636,13 @@
549 updated_device = handler.create_interface({636 updated_device = handler.create_interface({
550 "system_id": device.system_id,637 "system_id": device.system_id,
551 "mac_address": mac,638 "mac_address": mac,
552 "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,639 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
553 "subnet": subnet.id,640 "subnet": subnet.id,
554 })641 })
555 self.expectThat(updated_device["primary_mac"], Equals(mac))642 self.expectThat(updated_device["primary_mac"], Equals(mac))
556 self.expectThat(643 self.expectThat(
557 updated_device["ip_assignment"],644 updated_device["ip_assignment"],
558 Equals(DEVICE_IP_ASSIGNMENT.STATIC))645 Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
559 static_interface = Interface.objects.get(mac_address=MAC(mac))646 static_interface = Interface.objects.get(mac_address=MAC(mac))
560 observed_subnet = static_interface.ip_addresses.first().subnet647 observed_subnet = static_interface.ip_addresses.first().subnet
561 self.expectThat(648 self.expectThat(
@@ -573,14 +660,14 @@
573 updated_device = handler.create_interface({660 updated_device = handler.create_interface({
574 "system_id": device.system_id,661 "system_id": device.system_id,
575 "mac_address": mac,662 "mac_address": mac,
576 "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,663 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
577 "subnet": subnet.id,664 "subnet": subnet.id,
578 "ip_address": ip_address,665 "ip_address": ip_address,
579 })666 })
580 self.expectThat(updated_device["primary_mac"], Equals(mac))667 self.expectThat(updated_device["primary_mac"], Equals(mac))
581 self.expectThat(668 self.expectThat(
582 updated_device["ip_assignment"],669 updated_device["ip_assignment"],
583 Equals(DEVICE_IP_ASSIGNMENT.STATIC))670 Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
584 self.expectThat(updated_device["ip_address"], Equals(ip_address))671 self.expectThat(updated_device["ip_address"], Equals(ip_address))
585 static_interface = Interface.objects.get(mac_address=MAC(mac))672 static_interface = Interface.objects.get(mac_address=MAC(mac))
586 observed_subnet = static_interface.ip_addresses.first().subnet673 observed_subnet = static_interface.ip_addresses.first().subnet
@@ -641,3 +728,238 @@
641 }})728 }})
642 device = reload_object(device)729 device = reload_object(device)
643 self.expectThat(device.zone, Equals(zone))730 self.expectThat(device.zone, Equals(zone))
731
732 @transactional
733 def test_create_interface_creates_interface(self):
734 user = factory.make_admin()
735 node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
736 handler = DeviceHandler(user, {})
737 name = factory.make_name("eth")
738 mac_address = factory.make_mac_address()
739 handler.create_interface({
740 "system_id": node.system_id,
741 "name": name,
742 "mac_address": mac_address,
743 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
744 })
745 self.assertEqual(
746 1, node.interface_set.count(),
747 "Should have one interface on the node.")
748
749 @transactional
750 def test_create_interface_creates_static(self):
751 user = factory.make_admin()
752 node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
753 handler = DeviceHandler(user, {})
754 name = factory.make_name("eth")
755 mac_address = factory.make_mac_address()
756 fabric = factory.make_Fabric()
757 vlan = fabric.get_default_vlan()
758 subnet = factory.make_Subnet(vlan=vlan)
759 handler.create_interface({
760 "system_id": node.system_id,
761 "name": name,
762 "mac_address": mac_address,
763 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
764 "subnet": subnet.id,
765 })
766 new_interface = node.interface_set.first()
767 self.assertIsNotNone(new_interface)
768 auto_ip = new_interface.ip_addresses.filter(
769 alloc_type=IPADDRESS_TYPE.STICKY, subnet=subnet)
770 self.assertIsNotNone(auto_ip)
771 self.assertEqual(1, len(auto_ip))
772
773 @transactional
774 def test_create_interface_creates_external(self):
775 user = factory.make_admin()
776 node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
777 handler = DeviceHandler(user, {})
778 name = factory.make_name("eth")
779 mac_address = factory.make_mac_address()
780 ip_address = factory.make_ip_address()
781 handler.create_interface({
782 "system_id": node.system_id,
783 "name": name,
784 "mac_address": mac_address,
785 "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
786 "ip_address": ip_address,
787 })
788 new_interface = node.interface_set.first()
789 self.assertIsNotNone(new_interface)
790 auto_ip = new_interface.ip_addresses.filter(
791 alloc_type=IPADDRESS_TYPE.USER_RESERVED)
792 self.assertIsNotNone(auto_ip)
793 self.assertEqual(1, len(auto_ip))
794
795 @transactional
796 def test_update_interface_updates(self):
797 user = factory.make_admin()
798 node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
799 handler = DeviceHandler(user, {})
800 name = factory.make_name("eth")
801 mac_address = factory.make_mac_address()
802 ip_assignment = random.choice(DEVICE_IP_ASSIGNMENT_TYPE_CHOICES)[0]
803 params = {
804 "system_id": node.system_id,
805 "name": name,
806 "mac_address": mac_address,
807 "ip_assignment": ip_assignment,
808 }
809 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
810 subnet = factory.make_Subnet()
811 params['subnet'] = subnet.id
812 ip_address = str(IPAddress(IPNetwork(subnet.cidr).first))
813 params['ip_address'] = ip_address
814 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
815 ip_address = factory.make_ip_address()
816 params['ip_address'] = ip_address
817 handler.create_interface(params)
818 interface = node.interface_set.first()
819 self.assertIsNotNone(interface)
820 new_name = factory.make_name("eth")
821 new_ip_assignment = random.choice(DEVICE_IP_ASSIGNMENT_TYPE_CHOICES)[0]
822 new_params = {
823 "system_id": node.system_id,
824 "interface_id": interface.id,
825 "name": new_name,
826 "mac_address": mac_address,
827 "ip_assignment": new_ip_assignment,
828 }
829 if new_ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
830 new_subnet = factory.make_Subnet()
831 new_params['subnet'] = new_subnet.id
832 new_ip_address = str(IPAddress(IPNetwork(new_subnet.cidr).first))
833 new_params['ip_address'] = new_ip_address
834 elif new_ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
835 new_ip_address = factory.make_ip_address()
836 new_params['ip_address'] = new_ip_address
837 handler.update_interface(new_params)
838 data = self.dehydrate_device(node, user)['interfaces']
839 self.assertEqual(1, len(data))
840 self.assertEqual(data[0]['ip_assignment'], new_ip_assignment)
841 if new_ip_assignment != DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
842 self.assertEqual(data[0]['ip_address'], new_ip_address)
843
844 @transactional
845 def test_update_raise_permissions_error_for_non_admin(self):
846 user = factory.make_User()
847 handler = DeviceHandler(user, {})
848 self.assertRaises(
849 HandlerPermissionError,
850 handler.update, {})
851
852 @transactional
853 def test_update_does_not_raise_validation_error_for_invalid_arch(self):
854 user = factory.make_admin()
855 handler = DeviceHandler(user, {})
856 node = factory.make_Node(interface=True, node_type=NODE_TYPE.DEVICE)
857 node_data = self.dehydrate_device(node, user)
858 arch = factory.make_name("arch")
859 node_data["architecture"] = arch
860 handler.update(node_data)
861 # succeeds, because Devices don't care about architecture.
862
863 @transactional
864 def test_update_updates_node(self):
865 user = factory.make_admin()
866 handler = DeviceHandler(user, {})
867 node = factory.make_Node(interface=True, node_type=NODE_TYPE.DEVICE)
868 node_data = self.dehydrate_device(node, user)
869 new_zone = factory.make_Zone()
870 new_hostname = factory.make_name("hostname")
871 node_data["hostname"] = new_hostname
872 node_data["zone"] = {
873 "name": new_zone.name,
874 }
875 updated_node = handler.update(node_data)
876 self.assertEqual(updated_node["hostname"], new_hostname)
877 self.assertEqual(updated_node["zone"]["id"], new_zone.id)
878
879 @transactional
880 def test_delete_interface(self):
881 user = factory.make_admin()
882 node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
883 handler = DeviceHandler(user, {})
884 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
885 handler.delete_interface({
886 "system_id": node.system_id,
887 "interface_id": interface.id,
888 })
889 self.assertIsNone(reload_object(interface))
890
891 @transactional
892 def test_link_subnet_calls_update_link_by_id_if_link_id(self):
893 user = factory.make_admin()
894 node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
895 handler = DeviceHandler(user, {})
896 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
897 subnet = factory.make_Subnet()
898 link_id = random.randint(0, 100)
899 ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
900 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
901 mode = INTERFACE_LINK_TYPE.STATIC
902 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
903 mode = INTERFACE_LINK_TYPE.DHCP
904 else:
905 mode = INTERFACE_LINK_TYPE.LINK_UP
906 ip_address = factory.make_ip_address()
907 self.patch_autospec(Interface, "update_link_by_id")
908 handler.link_subnet({
909 "system_id": node.system_id,
910 "interface_id": interface.id,
911 "link_id": link_id,
912 "subnet": subnet.id,
913 "ip_assignment": ip_assignment,
914 "ip_address": ip_address,
915 })
916 self.assertThat(
917 Interface.update_link_by_id,
918 MockCalledOnceWith(
919 ANY, link_id, mode, subnet, ip_address=ip_address))
920
921 @transactional
922 def test_link_subnet_calls_link_subnet_if_not_link_id(self):
923 user = factory.make_admin()
924 node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
925 handler = DeviceHandler(user, {})
926 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
927 subnet = factory.make_Subnet()
928 ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
929 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
930 mode = INTERFACE_LINK_TYPE.STATIC
931 elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
932 mode = INTERFACE_LINK_TYPE.DHCP
933 else:
934 mode = INTERFACE_LINK_TYPE.LINK_UP
935 ip_address = factory.make_ip_address()
936 self.patch_autospec(Interface, "link_subnet")
937 handler.link_subnet({
938 "system_id": node.system_id,
939 "interface_id": interface.id,
940 "subnet": subnet.id,
941 "ip_assignment": ip_assignment,
942 "ip_address": ip_address,
943 })
944 if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
945 self.assertThat(
946 Interface.link_subnet,
947 MockCalledOnceWith(
948 ANY, mode, subnet, ip_address=ip_address))
949 else:
950 self.assertThat(Interface.link_subnet, MockNotCalled())
951
952 @transactional
953 def test_unlink_subnet(self):
954 user = factory.make_admin()
955 node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
956 handler = DeviceHandler(user, {})
957 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
958 link_ip = factory.make_StaticIPAddress(
959 alloc_type=IPADDRESS_TYPE.AUTO, ip="", interface=interface)
960 handler.delete_interface({
961 "system_id": node.system_id,
962 "interface_id": interface.id,
963 "link_id": link_ip.id,
964 })
965 self.assertIsNone(reload_object(link_ip))