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
1=== modified file 'src/maasserver/enum.py'
2--- src/maasserver/enum.py 2017-02-17 21:27:46 +0000
3+++ src/maasserver/enum.py 2017-02-28 04:16:58 +0000
4@@ -7,6 +7,8 @@
5 'CACHE_MODE_TYPE',
6 'CACHE_MODE_TYPE_CHOICES',
7 'COMPONENT',
8+ 'DEVICE_IP_ASSIGNMENT_TYPE',
9+ 'DEVICE_IP_ASSIGNMENT_TYPE_CHOICES',
10 'FILESYSTEM_GROUP_TYPE',
11 'FILESYSTEM_GROUP_TYPE_CHOICES',
12 'FILESYSTEM_TYPE',
13@@ -206,6 +208,27 @@
14 ADMIN = 'admin_node'
15
16
17+class DEVICE_IP_ASSIGNMENT_TYPE:
18+ """The vocabulary of a `Device`'s possible IP assignment type. This value
19+ is calculated by looking at the overall model for a `Device`. This is not
20+ set directly on the model."""
21+ #: Device is outside of MAAS control.
22+ EXTERNAL = "external"
23+
24+ #: Device receives ip address from `NodeGroupInterface` dynamic range.
25+ DYNAMIC = "dynamic"
26+
27+ #: Device has ip address assigned from `NodeGroupInterface` static range.
28+ STATIC = "static"
29+
30+
31+DEVICE_IP_ASSIGNMENT_TYPE_CHOICES = (
32+ (DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC, "Dynamic"),
33+ (DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL, "External"),
34+ (DEVICE_IP_ASSIGNMENT_TYPE.STATIC, "Static"),
35+)
36+
37+
38 class PRESEED_TYPE:
39 """Types of preseed documents that can be generated."""
40 COMMISSIONING = 'commissioning'
41
42=== modified file 'src/maasserver/forms/__init__.py'
43--- src/maasserver/forms/__init__.py 2017-02-17 14:23:04 +0000
44+++ src/maasserver/forms/__init__.py 2017-02-28 04:16:58 +0000
45@@ -803,27 +803,42 @@
46 required=False, initial=None,
47 queryset=Node.objects.all(), to_field_name='system_id')
48
49+ zone = forms.ModelChoiceField(
50+ label="Physical zone", required=False,
51+ initial=Zone.objects.get_default_zone,
52+ queryset=Zone.objects.all(), to_field_name='name')
53+
54 class Meta:
55 model = Device
56
57 fields = NodeForm.Meta.fields + (
58 'parent',
59+ 'zone',
60 )
61
62+ zone = forms.ModelChoiceField(
63+ label="Physical zone", required=False,
64+ initial=Zone.objects.get_default_zone,
65+ queryset=Zone.objects.all(), to_field_name='name')
66+
67 def __init__(self, request=None, *args, **kwargs):
68 super(DeviceForm, self).__init__(*args, **kwargs)
69 self.request = request
70
71 instance = kwargs.get('instance')
72 self.set_up_initial_device(instance)
73+ if instance is not None:
74+ self.initial['zone'] = instance.zone.name
75
76 def set_up_initial_device(self, instance):
77 """Initialize the 'parent' field if a device instance was given.
78
79 This is a workaround for Django bug #17657.
80 """
81- if instance is not None and instance.parent is not None:
82- self.initial['parent'] = instance.parent.system_id
83+ if instance is not None:
84+ if instance.parent is not None:
85+ self.initial['parent'] = instance.parent.system_id
86+ self.initial['zone'] = instance.zone.name
87
88 def save(self, commit=True):
89 device = super(DeviceForm, self).save(commit=False)
90@@ -832,6 +847,10 @@
91 # Set the owner: devices are owned by their creator.
92 device.owner = self.request.user
93
94+ zone = self.cleaned_data.get('zone')
95+ if zone:
96+ device.zone = zone
97+
98 # If the device has a parent and no domain was provided,
99 # inherit the parent's domain.
100 if device.parent:
101@@ -839,6 +858,9 @@
102 device.parent.domain):
103 device.domain = device.parent.domain
104
105+ zone = self.cleaned_data.get('zone')
106+ if zone:
107+ device.zone = zone
108 device.save()
109 return device
110
111
112=== modified file 'src/maasserver/forms/interface.py'
113--- src/maasserver/forms/interface.py 2017-02-17 14:23:04 +0000
114+++ src/maasserver/forms/interface.py 2017-02-28 04:16:58 +0000
115@@ -16,6 +16,7 @@
116 BOND_LACP_RATE_CHOICES,
117 BOND_MODE_CHOICES,
118 BOND_XMIT_HASH_POLICY_CHOICES,
119+ DEVICE_IP_ASSIGNMENT_TYPE,
120 INTERFACE_TYPE,
121 IPADDRESS_TYPE,
122 NODE_TYPE,
123@@ -56,6 +57,16 @@
124 accept_ra = forms.NullBooleanField(required=False)
125 autoconf = forms.NullBooleanField(required=False)
126
127+ # Device parameters
128+ ip_assignment = forms.MultipleChoiceField(
129+ choices=(
130+ ('static', DEVICE_IP_ASSIGNMENT_TYPE.STATIC),
131+ ('dynamic', DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC),
132+ ('external', DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL)),
133+ required=False)
134+ ip_address = forms.GenericIPAddressField(
135+ unpack_ipv4=True, required=False)
136+
137 @staticmethod
138 def get_interface_form(type):
139 try:
140@@ -148,9 +159,18 @@
141 msg = "Parents are related to different nodes."
142 set_form_error(self, 'name', msg)
143
144+ def clean_device(self, cleaned_data):
145+ ip_assignment = cleaned_data.get('ip_assignment')
146+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
147+ # Dynamic means that there is no IP address stored.
148+ cleaned_data['ip_address'] = None
149+ return cleaned_data
150+
151 def clean(self):
152 cleaned_data = super(InterfaceForm, self).clean()
153 self.clean_parents_all_same_node(cleaned_data.get('parents'))
154+ if self.node.node_type == NODE_TYPE.DEVICE:
155+ cleaned_data = self.clean_device(cleaned_data)
156 return cleaned_data
157
158 def _set_param(self, interface, key):
159
160=== modified file 'src/maasserver/forms/tests/test_device.py'
161--- src/maasserver/forms/tests/test_device.py 2017-02-17 14:23:04 +0000
162+++ src/maasserver/forms/tests/test_device.py 2017-02-28 04:16:58 +0000
163@@ -40,6 +40,7 @@
164 'parent',
165 'disable_ipv4',
166 'swap_size',
167+ 'zone',
168 ], list(form.fields))
169
170 def test_changes_device_parent(self):
171
172=== modified file 'src/maasserver/static/js/angular/controllers/node_details.js'
173--- src/maasserver/static/js/angular/controllers/node_details.js 2017-01-28 00:51:47 +0000
174+++ src/maasserver/static/js/angular/controllers/node_details.js 2017-02-28 04:16:58 +0000
175@@ -6,14 +6,22 @@
176
177 angular.module('MAAS').controller('NodeDetailsController', [
178 '$scope', '$rootScope', '$routeParams', '$location', '$interval',
179+ 'DevicesManager',
180 'MachinesManager', 'ControllersManager', 'ZonesManager', 'GeneralManager',
181 'UsersManager', 'TagsManager', 'DomainsManager', 'ManagerHelperService',
182 'ServicesManager', 'ErrorService', 'ValidationService', function(
183- $scope, $rootScope, $routeParams, $location, $interval,
184+ $scope, $rootScope, $routeParams, $location, $interval, DevicesManager,
185 MachinesManager, ControllersManager, ZonesManager, GeneralManager,
186 UsersManager, TagsManager, DomainsManager, ManagerHelperService,
187 ServicesManager, ErrorService, ValidationService) {
188
189+ // Mapping of device.ip_assignment to viewable text.
190+ var DEVICE_IP_ASSIGNMENT = {
191+ external: "External",
192+ dynamic: "Dynamic",
193+ "static": "Static"
194+ };
195+
196 // Set title and page.
197 $rootScope.title = "Loading...";
198 $rootScope.page = "nodes";
199@@ -84,6 +92,11 @@
200 parameters: {}
201 };
202
203+ // Get the display text for device ip assignment type.
204+ $scope.getDeviceIPAssignment = function(ipAssignment) {
205+ return DEVICE_IP_ASSIGNMENT[ipAssignment];
206+ };
207+
208 // Events section.
209 $scope.events = {
210 limit: 10
211@@ -674,9 +687,10 @@
212 $scope.hasInvalidArchitecture = function() {
213 if(angular.isObject($scope.node)) {
214 return (
215- $scope.node.architecture === "" ||
216- $scope.summary.architecture.options.indexOf(
217- $scope.node.architecture) === -1);
218+ !$scope.isDevice && (
219+ $scope.node.architecture === "" ||
220+ $scope.summary.architecture.options.indexOf(
221+ $scope.node.architecture) === -1));
222 } else {
223 return false;
224 }
225@@ -685,9 +699,10 @@
226 // Return true if the current architecture selection is invalid.
227 $scope.invalidArchitecture = function() {
228 return (
229- $scope.summary.architecture.selected === "" ||
230- $scope.summary.architecture.options.indexOf(
231- $scope.summary.architecture.selected) === -1);
232+ !$scope.isDevice && (
233+ $scope.summary.architecture.selected === "" ||
234+ $scope.summary.architecture.options.indexOf(
235+ $scope.summary.architecture.selected) === -1));
236 };
237
238 // Return true if at least a rack controller is connected to the
239@@ -702,7 +717,7 @@
240 return (
241 $scope.isRackControllerConnected() &&
242 $scope.isSuperUser() &&
243- !$scope.isController);
244+ !$scope.isController && !$scope.isDevice);
245 };
246
247 // Called to edit the node name.
248@@ -1029,6 +1044,7 @@
249 // Load all the required managers.
250 ManagerHelperService.loadManagers($scope, [
251 MachinesManager,
252+ DevicesManager,
253 ControllersManager,
254 ZonesManager,
255 GeneralManager,
256@@ -1040,11 +1056,19 @@
257 if('controller' === $routeParams.type) {
258 $scope.nodesManager = ControllersManager;
259 $scope.isController = true;
260+ $scope.isDevice = false;
261 $scope.type_name = 'controller';
262 $scope.type_name_title = 'Controller';
263+ }else if('device' === $routeParams.type) {
264+ $scope.nodesManager = DevicesManager;
265+ $scope.isController = false;
266+ $scope.isDevice = true;
267+ $scope.type_name = 'device';
268+ $scope.type_name_title = 'Device';
269 }else{
270 $scope.nodesManager = MachinesManager;
271 $scope.isController = false;
272+ $scope.isDevice = false;
273 $scope.type_name = 'machine';
274 $scope.type_name_title = 'Machine';
275 }
276@@ -1062,6 +1086,10 @@
277 }, function(error) {
278 ErrorService.raiseError(error);
279 });
280+ activeNode = $scope.nodesManager.getActiveItem();
281+ }
282+ if($scope.isDevice) {
283+ $scope.ip_assignment = activeNode.ip_assignment;
284 }
285
286 // Poll for architectures, hwe_kernels, and osinfo the whole
287
288=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
289--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-10-26 19:05:43 +0000
290+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2017-02-28 04:16:58 +0000
291@@ -171,6 +171,28 @@
292 EDIT: "edit"
293 };
294
295+ var IP_ASSIGNMENT = {
296+ DYNAMIC: "dynamic",
297+ EXTERNAL: "external",
298+ STATIC: "static"
299+ };
300+
301+ // Device ip assignment options.
302+ $scope.ipAssignments = [
303+ {
304+ name: IP_ASSIGNMENT.EXTERNAL,
305+ text: "External"
306+ },
307+ {
308+ name: IP_ASSIGNMENT.DYNAMIC,
309+ text: "Dynamic"
310+ },
311+ {
312+ name: IP_ASSIGNMENT.STATIC,
313+ text: "Static"
314+ }
315+ ];
316+
317 // Set the initial values for this scope.
318 $scope.loaded = false;
319 $scope.nodeHasLoaded = false;
320@@ -542,6 +564,10 @@
321 // If the user is not the superuser, pretend it's not Ready.
322 return false;
323 }
324+ if ($scope.$parent.isDevice) {
325+ // Devices are never Ready, for our purposes, for now.
326+ return true;
327+ }
328 if ($scope.$parent.isController) {
329 // Controllers are always Ready, for our purposes.
330 return true;
331@@ -563,8 +589,8 @@
332 // If the user is not the superuser, pretend it's not Ready.
333 return false;
334 }
335- if ($scope.$parent.isController) {
336- // Controllers never in limited mode.
337+ if ($scope.$parent.isController || $scope.$parent.isDevice) {
338+ // Controllers and Devices are never in limited mode.
339 return false;
340 }
341 return (
342@@ -581,9 +607,9 @@
343 // If the user is not a superuser, disable the networking panel.
344 return true;
345 }
346- if ($scope.$parent.isController) {
347+ if ($scope.$parent.isController || $scope.$parent.isDevice) {
348 // Never disable the full networking panel when its a
349- // controller.
350+ // Controller or Device.
351 return false;
352 }
353 if (angular.isObject($scope.node) &&
354@@ -772,18 +798,29 @@
355 $scope.edit = function(nic) {
356 $scope.selectedInterfaces = [$scope.getUniqueKey(nic)];
357 $scope.selectedMode = SELECTION_MODE.EDIT;
358- $scope.editInterface = {
359- id: nic.id,
360- name: nic.name,
361- mac_address: nic.mac_address,
362- tags: angular.copy(nic.tags),
363- fabric: nic.fabric,
364- vlan: nic.vlan,
365- subnet: nic.subnet,
366- mode: nic.mode,
367- ip_address: nic.ip_address,
368- link_id: nic.link_id
369- };
370+ if($scope.$parent.isDevice) {
371+ $scope.editInterface = {
372+ id: nic.id,
373+ name: nic.name,
374+ mac_address: nic.mac_address,
375+ subnet: nic.subnet,
376+ ip_address: nic.ip_address,
377+ ip_assignment: nic.ip_assignment
378+ };
379+ }else{
380+ $scope.editInterface = {
381+ id: nic.id,
382+ name: nic.name,
383+ mac_address: nic.mac_address,
384+ tags: angular.copy(nic.tags),
385+ fabric: nic.fabric,
386+ vlan: nic.vlan,
387+ subnet: nic.subnet,
388+ mode: nic.mode,
389+ ip_address: nic.ip_address,
390+ link_id: nic.link_id
391+ };
392+ }
393 };
394
395 // Called when the fabric is changed.
396@@ -828,6 +865,10 @@
397 }
398 };
399
400+ $scope.ipAssignementChanged = function(nic) {
401+ return;
402+ };
403+
404 // Called to cancel edit mode.
405 $scope.editCancel = function(nic) {
406 $scope.selectedInterfaces = [];
407@@ -837,13 +878,24 @@
408
409 // Save the following interface on the node.
410 $scope.saveInterface = function(nic) {
411- var params = {
412- "name": nic.name,
413- "mac_address": nic.mac_address,
414- "tags": nic.tags.map(
415- function(tag) { return tag.text; })
416- };
417- if(nic.vlan !== null) {
418+ var params;
419+ if($scope.$parent.isDevice) {
420+ params = {
421+ "name": nic.name,
422+ "mac_address": nic.mac_address,
423+ "ip_assignment": nic.ip_assignment,
424+ "ip_address": nic.ip_address,
425+ "subnet": nic.subnet
426+ };
427+ }else{
428+ params = {
429+ "name": nic.name,
430+ "mac_address": nic.mac_address,
431+ "tags": nic.tags.map(
432+ function(tag) { return tag.text; })
433+ };
434+ }
435+ if(nic.vlan !== undefined && nic.vlan !== null) {
436 params.vlan = nic.vlan.id;
437 } else {
438 params.vlan = null;
439@@ -866,6 +918,9 @@
440 var params = {
441 "mode": nic.mode
442 };
443+ if($scope.$parent.isDevice) {
444+ params.ip_assignment=nic.ip_assignment;
445+ }
446 if(angular.isObject(nic.subnet)) {
447 params.subnet = nic.subnet.id;
448 }
449@@ -1110,9 +1165,18 @@
450
451 // Perform the add action over the websocket.
452 $scope.addInterface = function(type) {
453- if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {
454+ var nic;
455+ if($scope.$parent.isDevice) {
456+ nic = {
457+ id: $scope.newInterface.parent.id,
458+ ip_assignment: $scope.newInterface.ip_assignment,
459+ subnet: $scope.newInterface.subnet,
460+ ip_address: $scope.newInterface.ip_address
461+ };
462+ $scope.saveInterfaceLink(nic);
463+ } else if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {
464 // Add a link to the current interface.
465- var nic = {
466+ nic = {
467 id: $scope.newInterface.parent.id,
468 mode: $scope.newInterface.mode,
469 subnet: $scope.newInterface.subnet,
470@@ -1416,17 +1480,29 @@
471 $scope.showCreatePhysical = function() {
472 if($scope.selectedMode === SELECTION_MODE.NONE) {
473 $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL;
474- $scope.newInterface = {
475- name: getNextName("eth"),
476- macAddress: "",
477- macError: false,
478- tags: [],
479- errorMsg: null,
480- fabric: $scope.fabrics[0],
481- vlan: getDefaultVLAN($scope.fabrics[0]),
482- subnet: null,
483- mode: LINK_MODE.LINK_UP
484- };
485+ if($scope.$parent.isDevice) {
486+ $scope.newInterface = {
487+ name: getNextName("eth"),
488+ macAddress: "",
489+ macError: false,
490+ tags: [],
491+ errorMsg: null,
492+ subnet: null,
493+ ip_assignment: IP_ASSIGNMENT.DYNAMIC
494+ };
495+ }else{
496+ $scope.newInterface = {
497+ name: getNextName("eth"),
498+ macAddress: "",
499+ macError: false,
500+ tags: [],
501+ errorMsg: null,
502+ fabric: $scope.fabrics[0],
503+ vlan: getDefaultVLAN($scope.fabrics[0]),
504+ subnet: null,
505+ mode: LINK_MODE.LINK_UP
506+ };
507+ }
508 }
509 };
510
511@@ -1444,14 +1520,24 @@
512 return;
513 }
514
515- var params = {
516- name: $scope.newInterface.name,
517- tags: $scope.newInterface.tags.map(
518- function(tag) { return tag.text; }),
519- mac_address: $scope.newInterface.macAddress,
520- vlan: $scope.newInterface.vlan.id,
521- mode: $scope.newInterface.mode
522- };
523+ var params;
524+ if($scope.$parent.isDevice) {
525+ params = {
526+ name: $scope.newInterface.name,
527+ mac_address: $scope.newInterface.macAddress,
528+ ip_assignment: $scope.newInterface.ip_assignment,
529+ ip_address: $scope.newInterface.ip_address
530+ };
531+ }else{
532+ params = {
533+ name: $scope.newInterface.name,
534+ tags: $scope.newInterface.tags.map(
535+ function(tag) { return tag.text; }),
536+ mac_address: $scope.newInterface.macAddress,
537+ vlan: $scope.newInterface.vlan.id,
538+ mode: $scope.newInterface.mode
539+ };
540+ }
541 if(angular.isObject($scope.newInterface.subnet)) {
542 params.subnet = $scope.newInterface.subnet.id;
543 }
544
545=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details.js'
546--- src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-01-28 00:51:47 +0000
547+++ src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-02-28 04:16:58 +0000
548@@ -42,6 +42,7 @@
549 var webSocket;
550 beforeEach(inject(function($injector) {
551 MachinesManager = $injector.get("MachinesManager");
552+ DevicesManager = $injector.get("DevicesManager");
553 ControllersManager = $injector.get("ControllersManager");
554 ZonesManager = $injector.get("ZonesManager");
555 GeneralManager = $injector.get("GeneralManager");
556@@ -136,6 +137,7 @@
557 $routeParams: $routeParams,
558 $location: $location,
559 MachinesManager: MachinesManager,
560+ DevicesManager: DevicesManager,
561 ControllersManager: ControllersManager,
562 ZonesManager: ZonesManager,
563 GeneralManager: GeneralManager,
564@@ -260,9 +262,9 @@
565 var controller = makeController();
566 expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith(
567 $scope, [
568- MachinesManager, ControllersManager, ZonesManager,
569- GeneralManager, UsersManager, TagsManager, DomainsManager,
570- ServicesManager]);
571+ MachinesManager, DevicesManager, ControllersManager,
572+ ZonesManager, GeneralManager, UsersManager, TagsManager,
573+ DomainsManager, ServicesManager]);
574 });
575
576 it("doesnt call setActiveItem if node is loaded", function() {
577
578=== modified file 'src/maasserver/static/js/angular/factories/devices.js'
579--- src/maasserver/static/js/angular/factories/devices.js 2016-09-27 00:47:54 +0000
580+++ src/maasserver/static/js/angular/factories/devices.js 2017-02-28 04:16:58 +0000
581@@ -11,16 +11,19 @@
582
583 angular.module('MAAS').factory(
584 'DevicesManager',
585- ['$q', '$rootScope', 'RegionConnection', 'Manager', function(
586- $q, $rootScope, RegionConnection, Manager) {
587+ ['$q', '$rootScope', 'RegionConnection', 'NodesManager', function(
588+ $q, $rootScope, RegionConnection, NodesManager) {
589
590 function DevicesManager() {
591- Manager.call(this);
592+ NodesManager.call(this);
593
594 this._pk = "system_id";
595 this._handler = "device";
596 this._metadataAttributes = {
597 "owner": null,
598+ "subnets": null,
599+ "fabrics": null,
600+ "spaces": null,
601 "tags": null,
602 "zone": function(device) {
603 return device.zone.name;
604@@ -34,7 +37,7 @@
605 });
606 }
607
608- DevicesManager.prototype = new Manager();
609+ DevicesManager.prototype = new NodesManager();
610
611 // Create a device.
612 DevicesManager.prototype.create = function(node) {
613
614=== modified file 'src/maasserver/static/js/angular/factories/tests/test_devices.js'
615--- src/maasserver/static/js/angular/factories/tests/test_devices.js 2016-09-27 14:24:19 +0000
616+++ src/maasserver/static/js/angular/factories/tests/test_devices.js 2017-02-28 04:16:58 +0000
617@@ -46,7 +46,7 @@
618 expect(DevicesManager._pk).toBe("system_id");
619 expect(DevicesManager._handler).toBe("device");
620 expect(Object.keys(DevicesManager._metadataAttributes)).toEqual(
621- ["owner", "tags", "zone"]);
622+ ["owner", "subnets", "fabrics", "spaces", "tags", "zone"]);
623 });
624
625 describe("createInferface", function() {
626
627=== modified file 'src/maasserver/static/partials/node-details.html'
628--- src/maasserver/static/partials/node-details.html 2017-02-17 14:23:04 +0000
629+++ src/maasserver/static/partials/node-details.html 2017-02-28 04:16:58 +0000
630@@ -30,7 +30,7 @@
631 data-ng-click="saveEditHeader()">Save</a>
632
633 <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
634- <p class="page-header__status" data-ng-hide="isController || header.editing">
635+ <p class="page-header__status" data-ng-if="!isController && !isDevice && !header.editing">
636 {$ node.status $}
637 <span class="u-text--{$ getPowerStateClass() $} u-margin--left-small"><i class="icon icon--power-{$ getPowerStateClass() $} u-margin--right-tiny"></i>{$ getPowerStateText() $}</span>
638 <a href="" class="page-header__status-check" data-ng-show="canCheckPowerState()" data-ng-click="checkPowerState()">check now</a>
639@@ -47,7 +47,7 @@
640 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
641 <div class="page-header__dropdown" data-ng-class="{ 'is-open': actionOption }">
642
643- <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="!node.dhcp_on">
644+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-if="!isDevice && !node.dhcp_on">
645 <p class="page-header__message page-header__message--warning">
646 MAAS is not providing DHCP.
647 </p>
648@@ -89,58 +89,20 @@
649 <div class="page-header__controls">
650 <a href="" class="button--base button--inline" data-ng-click="actionCancel()">Cancel</a>
651 <button class="button--inline" data-ng-class="actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">
652- <span data-ng-if="actionOption.name === 'commission'">Commission
653- <span data-ng-if="!isController">machine</span>
654- <span data-ng-if="isController">controller</span>
655- </span>
656- <span data-ng-if="actionOption.name === 'acquire'">Acquire
657- <span data-ng-if="!isController">machine</span>
658- <span data-ng-if="isController">controller</span>
659- </span>
660- <span data-ng-if="actionOption.name === 'deploy'">Deploy
661- <span data-ng-if="!isController">machine</span>
662- <span data-ng-if="isController">controller</span>
663- </span>
664- <span data-ng-if="actionOption.name === 'release'">Release
665- <span data-ng-if="!isController">machine</span>
666- <span data-ng-if="isController">controller</span>
667- </span>
668- <span data-ng-if="actionOption.name === 'set-zone'">Set zone for
669- <span data-ng-if="!isController">machine</span>
670- <span data-ng-if="isController">controller</span>
671- </span>
672- <span data-ng-if="actionOption.name === 'on'">Power on
673- <span data-ng-if="!isController">machine</span>
674- <span data-ng-if="isController">controller</span>
675- </span>
676- <span data-ng-if="actionOption.name === 'off'">Power off
677- <span data-ng-if="!isController">machine</span>
678- <span data-ng-if="isController">controller</span>
679- </span>
680- <span data-ng-if="actionOption.name === 'abort'">Abort action on
681- <span data-ng-if="!isController">machine</span>
682- <span data-ng-if="isController">controller</span>
683- </span>
684- <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue
685- <span data-ng-if="!isController">machine</span>
686- <span data-ng-if="isController">controller</span>
687- </span>
688- <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode
689- </span>
690- <span data-ng-if="actionOption.name === 'mark-broken'">Mark
691- <span data-ng-if="!isController">machine</span>
692- <span data-ng-if="isController">controller</span> as broken
693- </span>
694- <span data-ng-if="actionOption.name === 'mark-fixed'">Mark
695- <span data-ng-if="!isController">machine</span>
696- <span data-ng-if="isController">controller</span> as fixed
697- </span>
698- <span data-ng-if="actionOption.name === 'delete'">Delete
699- <span data-ng-if="!isController">machine</span>
700- <span data-ng-if="isController">controller</span>
701- </span>
702- <span data-ng-if="actionOption.name === 'import-images'">Import images
703- </span>
704+ <span data-ng-if="actionOption.name === 'commission'">Commission {$ type_name $}</span>
705+ <span data-ng-if="actionOption.name === 'acquire'">Acquire {$ type_name $}</span>
706+ <span data-ng-if="actionOption.name === 'deploy'">Deploy {$ type_name $}</span>
707+ <span data-ng-if="actionOption.name === 'release'">Release {$ type_name $}</span>
708+ <span data-ng-if="actionOption.name === 'set-zone'">Set zone for {$ type_name $}</span>
709+ <span data-ng-if="actionOption.name === 'on'">Power on {$ type_name $}</span>
710+ <span data-ng-if="actionOption.name === 'off'">Power off {$ type_name $}</span>
711+ <span data-ng-if="actionOption.name === 'abort'">Abort action on {$ type_name $}</span>
712+ <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue {$ type_name $}</span>
713+ <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode</span>
714+ <span data-ng-if="actionOption.name === 'mark-broken'">Mark {$ type_name $}</span>
715+ <span data-ng-if="actionOption.name === 'mark-fixed'">Mark {$ type_name $}</span>
716+ <span data-ng-if="actionOption.name === 'delete'">Delete {$ type_name $}</span>
717+ <span data-ng-if="actionOption.name === 'import-images'">Import images</span>
718 </button>
719 </div>
720 </form>
721@@ -201,12 +163,13 @@
722 </nav> -->
723 <div class="row" id="summary">
724 <div class="wrapper--inner">
725- <h2 data-ng-hide="isController" class="eight-col">Machine summary</h2>
726- <h2 data-ng-show="isController" class="eight-col">Controller summary</h2>
727- <div data-ng-hide="isController">
728+ <h2 data-ng-if="!isController && !isDevice" class="eight-col">Machine summary</h2>
729+ <h2 data-ng-if="isController" class="eight-col">Controller summary</h2>
730+ <h2 data-ng-if="isDevice" class="eight-col">Device summary</h2>
731+ <div data-ng-if="!isController">
732 <div class="four-col last-col u-align--right" data-ng-hide="summary.editing">
733 <a href="" class="button--secondary button--inline"
734- data-ng-show="canEdit()"
735+ data-ng-if="canEdit()"
736 data-ng-click="editSummary()">Edit</a>
737 </div>
738 <div class="four-col last-col u-align--right ng-hide" data-ng-show="summary.editing">
739@@ -219,17 +182,17 @@
740 </div>
741 <div class="twelve-col" data-ng-if="isSuperUser()">
742 <div class="p-notification--error"
743- data-ng-if="!isController && !isRackControllerConnected()">
744+ data-ng-if="!isController && !isDevice && !isRackControllerConnected()">
745 <p class="p-notification__response">
746 <span class="p-notification__status">Error:</span>Editing is currently disabled because no rack controller is currently connected to the region.</p>
747 </div>
748 <div class="p-notification--error"
749- data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">
750+ data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">
751 <p class="p-notification__response">
752 <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>
753 </div>
754 <div class="p-notification--error"
755- data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">
756+ data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">
757 <p class="p-notification__response">
758 <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>
759 </div>
760@@ -247,7 +210,7 @@
761 </select>
762 </div>
763 </div>
764- <div class="form__group">
765+ <div class="form__group" data-ng-if="!isDevice">
766 <label for="architecture" class="form__group-label two-col">Architecture</label>
767 <div class="form__group-input three-col">
768 <select name="architecture" id="architecture"
769@@ -259,7 +222,7 @@
770 </select>
771 </div>
772 </div>
773- <div class="form__group">
774+ <div class="form__group" data-ng-if="!isDevice">
775 <label for="min_hwe_kernel" class="form__group-label two-col">Minimum Kernel</label>
776 <div class="form__group-input three-col">
777 <select name="min_hwe_kernel" id="min_hwe_kernel" placeholder="No minimum kernel"
778@@ -293,7 +256,7 @@
779 <dd class="four-col last-col">
780 {$ node.zone.name $}
781 </dd>
782- <dt class="two-col">Architecture</dt>
783+ <dt class="two-col" data-ng-if="!isDevice">Architecture</dt>
784 <dd class="four-col last-col">
785 {$ node.architecture || "Missing" $}
786 </dd>
787@@ -336,7 +299,7 @@
788 </dd>
789 </dl>
790 </div>
791- <div class="six-col last-col">
792+ <div class="six-col last-col" data-ng-if="!isDevice">
793 <dl>
794 <dt class="two-col">CPU</dt>
795 <dd class="four-col last-col">
796@@ -388,7 +351,7 @@
797 </div>
798 </div>
799 </div>
800- <div class="row" id="power" data-ng-show="!isController && isSuperUser()">
801+ <div class="row" id="power" data-ng-if="!isController && !isDevice && isSuperUser()">
802 <div class="wrapper--inner">
803 <h2 class="eight-col">Power</h2>
804 <div class="four-col last-col u-align--right" data-ng-hide="power.editing">
805@@ -613,8 +576,8 @@
806 <h2 class="title">Interfaces</h2>
807 </div>
808 <div class="twelve-col" data-ng-if="
809- (!isController && isAllNetworkingDisabled() && isSuperUser()) ||
810- !node.on_network || (!isController && !isUbuntuOS())">
811+ !isDevice && ((!isController && isAllNetworkingDisabled() && isSuperUser()) ||
812+ !node.on_network || (!isController && !isUbuntuOS()))">
813 <div class="p-notification--error" data-ng-if="!node.on_network">
814 <p class="p-notification__response">
815 <span class="p-notification__status">Error:</span>Node must be connected to a network.</p>
816@@ -635,6 +598,7 @@
817 <div class="table__row">
818 <div class="table__header table-col--3"></div>
819 <div class="table__header table-col--12">
820+ <span data-ng-if="!isDevice">
821 <a class="table__header-link is-active"
822 data-ng-class="{ 'is-active': column == 'name' }"
823 data-ng-click="column = 'name'">
824@@ -645,11 +609,16 @@
825 data-ng-click="column = 'mac'">
826 MAC
827 </a>
828- </div>
829- <div class="table__header table-col--3"><span data-ng-if="!isController">PXE</span></div>
830- <div class="table__header table-col--9">Type</div>
831- <div class="table__header table-col--14">Fabric</div>
832- <div class="table__header table-col--14">VLAN</div>
833+ </span>
834+ <span data-ng-if="isDevice">MAC</span>
835+ </div>
836+ <div class="table__header table-col--3"><span data-ng-if="!isController && !isDevice">PXE</span></div>
837+ <div class="table__header table-col--9"><span data-ng-if="!isDevice">Type</span></div>
838+ <div class="table__header table-col--14">
839+ <span data-ng-if="!isDevice">Fabric</span>
840+ <span data-ng-if="isDevice">IP Assignment</span>
841+ </div>
842+ <div class="table__header table-col--14"><span data-ng-if="!isDevice">VLAN</span></div>
843 <div class="table__header table-col--18">Subnet</div>
844 <div class="table__header table-col--27">IP Address</div>
845 </div>
846@@ -667,43 +636,48 @@
847 data-ng-if="!isController && isNodeEditingAllowed()">
848 <label for="{$ getUniqueKey(interface) $}"></label>
849 </div>
850- <div class="table__data table-col--12" aria-label="Name" data-ng-show="column == 'name'">
851+ <div class="table__data table-col--12" aria-label="Name" data-ng-if="!isDevice" data-ng-show="column == 'name'">
852 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>
853 <input type="text" class="table__input"
854 data-ng-if="isEditing(interface) && interface.type != 'vlan'"
855 data-ng-model="editInterface.name"
856 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">
857 </div>
858- <div class="table__data table-col--12 ng-hide" data-ng-show="column == 'mac'">
859+ <div class="table__data table-col--12 ng-hide" data-ng-if="!isDevice" data-ng-show="column == 'mac'">
860 {$ interface.mac_address $}
861 </div>
862+ <div class="table__data table-col--12" data-ng-if="isDevice">{$ interface.mac_address $}</div>
863 <div class="table__data table-col--3 u-align--center" aria-label="Boot interface">
864 <input type="radio" name="bootInterface" id="{$ interface.name $}" checked
865- data-ng-if="!isController && isBootInterface(interface)" class="u-display--desktop">
866+ data-ng-if="!isController && !isDevice && isBootInterface(interface)" class="u-display--desktop">
867 <label for="{$ interface.name $}" class="u-display--desktop"></label>
868- <span class="u-display--mobile" data-ng-if="!isController && isBootInterface(interface)">Yes</span>
869- <span class="u-display--mobile" data-ng-if="!isController && !isBootInterface(interface)">No</span>
870+ <span class="u-display--mobile" data-ng-if="!isController && !isDevice && isBootInterface(interface)">Yes</span>
871+ <span class="u-display--mobile" data-ng-if="!isController && !isDevice && !isBootInterface(interface)">No</span>
872 </div>
873 <div class="table__data table-col--9" aria-label="Type">
874- <span data-ng-if="!isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>
875+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>
876 </div>
877 <div class="table__data table-col--14" aria-label="Fabric">
878- <span data-ng-if="!isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>
879+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>
880+ <span data-ng-if="isDevice && !isEditing(interface)">{$ getDeviceIPAssignment(interface.ip_assignment) $}</span>
881 </div>
882 <div class="table__data table-col--14" aria-label="VLAN">
883- <span data-ng-if="!isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>
884+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>
885 </div>
886 <div class="table__data table-col--18" aria-label="Subnet">
887- <span data-ng-if="!isEditing(interface) && interface.fabric">{$ getSubnetText(interface.subnet) $}</span>
888+ <span data-ng-if="!isEditing(interface) && ((isDevice && interface.subnet) || interface.fabric)">{$ getSubnetText(interface.subnet) $}</span>
889 <span data-ng-if="isAllNetworkingDisabled() && interface.discovered[0].subnet_id">
890 {$ getSubnetText(getSubnet(interface.discovered[0].subnet_id)) $}
891 </span>
892 </div>
893 <div class="table__data table-col--19" aria-label="IP Address">
894- <span data-ng-if="!isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric">
895+ <span data-ng-if="isDevice">
896+ {$ interface.ip_address $}
897+ </span>
898+ <span data-ng-if="!isDevice && !isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric" >
899 {$ interface.ip_address $} ({$ getLinkModeText(interface) $})
900 </span>
901- <span data-ng-if="!isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">
902+ <span data-ng-if="!isDevice && !isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">
903 {$ interface.discovered[0].ip_address $} (DHCP)
904 </span>
905 </div>
906@@ -711,7 +685,7 @@
907 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">
908 <a class="u-display--desktop icon icon--add tooltip"
909 aria-label="Add Alias or VLAN"
910- data-ng-if="canAddAliasOrVLAN(interface)"
911+ data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
912 data-ng-click="quickAdd(interface)"></a>
913 <a class="u-display--desktop icon icon--edit tooltip"
914 data-ng-if="!cannotEditInterface(interface)"
915@@ -723,7 +697,7 @@
916 data-ng-click="quickRemove(interface)"></a>
917 <a class="button--secondary u-display--mobile"
918 aria-label="Add Alias or VLAN"
919- data-ng-if="canAddAliasOrVLAN(interface)"
920+ data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
921 data-ng-click="quickAdd(interface)">Add alias or VLAN</a>
922 <a class="button--secondary u-display--mobile"
923 data-ng-if="!cannotEditInterface(interface)"
924@@ -837,8 +811,10 @@
925 <div class="table__data table-col--100" data-ng-if="isEditing(interface)">
926 <fieldset class="form__fieldset six-col">
927 <dl>
928+ <span data-ng-if="!isDevice">
929 <dt class="two-col">Type</dt>
930 <dd class="four-col last-col">{$ getInterfaceTypeText(interface) $}</dd>
931+ </span>
932 </dl>
933 <div class="form__group" data-ng-if="interface.type != 'alias' && interface.type != 'vlan'">
934 <label for="mac" class="two-col">MAC address</label>
935@@ -847,14 +823,14 @@
936 data-ng-model="editInterface.mac_address"
937 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">
938 </div>
939- <div class="form__group" data-ng-if="!isLimitedEditingAllowed(interface)">
940+ <div class="form__group" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
941 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>
942 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">
943 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>
944 </div>
945 </div>
946 </fieldset>
947- <fieldset class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)">
948+ <fieldset class="form__fieldset six-col last-col" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
949 <div class="form__group">
950 <label for="fabric" class="two-col">Fabric</label>
951 <select name="fabric" class="three-col"
952@@ -904,6 +880,37 @@
953 </div>
954 </div>
955 </fieldset>
956+ <fieldset class="form__fieldset six-col last-col" data-ng-if="isDevice && !isLimitedEditingAllowed(interface)">
957+ <div class="form__group">
958+ <label for="ip-assignment" class="two-col">IP Assignment</label>
959+ <select name="ip-assignment" class="three-col"
960+ ng-model="editInterface.ip_assignment"
961+ data-ng-change="ipAssignementChanged(editInterface)"
962+ data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
963+ </select>
964+ </div>
965+ <div class="form__group" data-ng-if="editInterface.ip_assignment === 'static'">
966+ <label for="subnet" class="two-col">Subnet</label>
967+ <select name="subnet" class="three-col"
968+ ng-model="editInterface.subnet"
969+ data-ng-change="subnetChanged(newInterface)"
970+ data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
971+ <option value="" data-ng-hide="interface.links.length > 1">Unconfigured</option>
972+ </select>
973+ </div>
974+ <div class="form__group" data-ng-if="editInterface.ip_assignment !== 'dynamic'">
975+ <label for="ip-address" class="two-col">IP address</label>
976+ <div class="form__group-input three-col last-col">
977+ <input name="ip-address" type="text"
978+ placeholder="IP address (optional)"
979+ ng-model="editInterface.ip_address"
980+ data-ng-class="{ 'has-error': isIPAddressInvalid(editInterface) }">
981+ <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(editInterface)">
982+ <li>{$ getInterfaceError(editInterface) $}</li>
983+ </ul>
984+ </div>
985+ </div>
986+ </fieldset>
987 </div>
988 </div>
989 <div class="table__row is-active" data-ng-if="isShowingDeleteConfirm()">
990@@ -1124,7 +1131,7 @@
991 <input type="checkbox" class="checkbox" id="interface-create" disabled="disabled" checked />
992 <label for="interface-create"></label>
993 </div>
994- <div class="table__data table-col--12">
995+ <div class="table__data table-col--12" data-ng-if="!isDevice">
996 <input type="text" class="table__input"
997 data-ng-class="{ 'has-error': isInterfaceNameInvalid(newInterface) }"
998 data-ng-model="newInterface.name">
999@@ -1138,7 +1145,15 @@
1000 </div>
1001 <div class="table__row form form--stack is-active">
1002 <div class="table__data table-col--100">
1003- <div class="form__fieldset six-col">
1004+ <div class="table__data table-col--12" data-ng-if="isDevice">
1005+ <div class="form__group">
1006+ <label for="mac" class="three-col">MAC address</label>
1007+ <input type="text" name="mac" class="table__input"
1008+ data-ng-model="newInterface.macAddress"
1009+ data-ng-class="{ 'has-error': isMACAddressInvalid(newInterface.macAddress, false) || newInterface.macError }">
1010+ </div>
1011+ </div>
1012+ <div class="form__fieldset six-col" data-ng-if="!isDevice">
1013 <div class="form__group u-display--mobile">
1014 <label for="new-interface-name" class="two-col">Name</label>
1015 <input type="text" class="three-col" id="new-interface-name"
1016@@ -1162,7 +1177,8 @@
1017 </div>
1018 </div>
1019 </div>
1020- <div class="form__fieldset six-col last-col">
1021+ <span data-ng-if="!isDevice">
1022+ <div class="form__fieldset six-col last-col">
1023 <div class="form__group">
1024 <label for="fabric" class="two-col">Fabric</label>
1025 <select name="fabric" class="three-col"
1026@@ -1203,7 +1219,41 @@
1027 ng-model="newInterface.ip_address"
1028 data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
1029 </div>
1030- </div>
1031+ </div>
1032+ </span>
1033+ <span data-ng-if="isDevice">
1034+ <div class="form__fieldset six-col last-col">
1035+ <div class="form__group">
1036+ <label for="ip-assignment" class="two-col">IP Assignment</label>
1037+ <select name="ip-assignment" class="three-col"
1038+ ng-model="newInterface.ip_assignment"
1039+ data-ng-change="ipAssignementChanged(newInterface)"
1040+ data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
1041+ </select>
1042+ </div>
1043+ <div class="form__group" data-ng-if="newInterface.ip_assignment === 'static'">
1044+ <label for="subnet" class="two-col">Subnet</label>
1045+ <select name="subnet" class="three-col"
1046+ ng-model="newInterface.subnet"
1047+ data-ng-change="subnetChanged(newInterface)"
1048+ data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
1049+ <option value="" data-ng-hide="interface.links.length > 1">Unconfigured</option>
1050+ </select>
1051+ </div>
1052+ <div class="form__group" data-ng-if="newInterface.ip_assignment !== 'dynamic'">
1053+ <label for="ip-address" class="two-col">IP address</label>
1054+ <div class="form__group-input three-col last-col">
1055+ <input name="ip-address" type="text"
1056+ placeholder="IP address (optional)"
1057+ ng-model="newInterface.ip_address"
1058+ data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
1059+ <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(newInterface)">
1060+ <li>{$ getInterfaceError(newInterface) $}</li>
1061+ </ul>
1062+ </div>
1063+ </div>
1064+ </div>
1065+ </span>
1066 </div>
1067 </div>
1068 <div class="table__row is-active">
1069@@ -1221,7 +1271,7 @@
1070 </div>
1071 </main>
1072 </div>
1073- <div data-ng-if="!isController" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
1074+ <div data-ng-if="!isController && !isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
1075 <a class="button--secondary button--inline"
1076 data-ng-disabled="selectedMode !== null"
1077 data-ng-click="showCreatePhysical()">Add interface</a>
1078@@ -1232,12 +1282,17 @@
1079 data-ng-disabled="!canCreateBridge()"
1080 data-ng-click="showCreateBridge()">Create bridge</a>
1081 </div>
1082+ <div data-ng-if="isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
1083+ <a class="button--secondary button--inline"
1084+ data-ng-disabled="selectedMode !== null"
1085+ data-ng-click="showCreatePhysical()">Add interface</a>
1086+ </div>
1087 </div>
1088 </form>
1089 </div>
1090 </div>
1091 </div>
1092- <div class="row" id="storage" data-ng-hide="isController">
1093+ <div class="row" id="storage" data-ng-if="!isController && !isDevice">
1094 <div class="wrapper--inner" ng-controller="NodeStorageController">
1095 <form>
1096 <div class="twelve-col">
1097@@ -2223,7 +2278,7 @@
1098 </form>
1099 </div>
1100 </div>
1101- <div class="row" id="events" data-ng-hide="isController">
1102+ <div class="row" id="events" data-ng-if="!isController && !isDevice">
1103 <div class="wrapper--inner">
1104 <div class="eight-col">
1105 <h2 class="title">Latest {$ type_name $} events</h2>
1106
1107=== modified file 'src/maasserver/static/partials/nodes-list.html'
1108--- src/maasserver/static/partials/nodes-list.html 2017-02-17 14:23:04 +0000
1109+++ src/maasserver/static/partials/nodes-list.html 2017-02-28 04:16:58 +0000
1110@@ -902,7 +902,9 @@
1111 <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(device, 'devices')" data-ng-checked="device.$selected" id="{$ device.fqdn $}" data-ng-disabled="hasActionsInProgress('devices')"/>
1112 <label for="{$ device.fqdn $}" class="checkbox-label"></label>
1113 </td>
1114- <td class="table-col--24" aria-label="FQDN">{$ device.fqdn $}</td>
1115+ <td class="table-col--24" aria-label="FQDN">
1116+ <a href="#/node/device/{$ device.system_id $}">{$ device.fqdn $}</a>
1117+ </td>
1118 <td class="table-col--25" aria-label="MAC">
1119 {$ device.primary_mac $}
1120 <span class="extra-macs" data-ng-show="device.extra_macs.length">(+{$ device.extra_macs.length $})</span>
1121
1122=== modified file 'src/maasserver/websockets/handlers/device.py'
1123--- src/maasserver/websockets/handlers/device.py 2017-02-17 21:27:46 +0000
1124+++ src/maasserver/websockets/handlers/device.py 2017-02-28 04:16:58 +0000
1125@@ -7,7 +7,9 @@
1126 "DeviceHandler",
1127 ]
1128
1129+from django.core.exceptions import ValidationError
1130 from maasserver.enum import (
1131+ DEVICE_IP_ASSIGNMENT_TYPE,
1132 INTERFACE_LINK_TYPE,
1133 IPADDRESS_TYPE,
1134 NODE_PERMISSION,
1135@@ -17,7 +19,11 @@
1136 DeviceForm,
1137 DeviceWithMACsForm,
1138 )
1139-from maasserver.forms.interface import PhysicalInterfaceForm
1140+from maasserver.forms.interface import (
1141+ InterfaceForm,
1142+ PhysicalInterfaceForm,
1143+)
1144+from maasserver.models.interface import Interface
1145 from maasserver.models.node import Device
1146 from maasserver.models.staticipaddress import StaticIPAddress
1147 from maasserver.models.subnet import Subnet
1148@@ -26,6 +32,7 @@
1149 from maasserver.websockets.base import (
1150 HandlerDoesNotExistError,
1151 HandlerError,
1152+ HandlerPermissionError,
1153 HandlerValidationError,
1154 )
1155 from maasserver.websockets.handlers.node import (
1156@@ -39,20 +46,6 @@
1157 maaslog = get_maas_logger("websockets.device")
1158
1159
1160-class DEVICE_IP_ASSIGNMENT:
1161- """The vocabulary of a `Device`'s possible IP assignment type. This value
1162- is calculated by looking at the overall model for a `Device`. This is not
1163- set directly on the model."""
1164- #: Device is outside of MAAS control.
1165- EXTERNAL = "external"
1166-
1167- #: Device receives ip address from `NodeGroupInterface` dynamic range.
1168- DYNAMIC = "dynamic"
1169-
1170- #: Device has ip address assigned from `NodeGroupInterface` static range.
1171- STATIC = "static"
1172-
1173-
1174 def get_Interface_from_list(interfaces, mac):
1175 """Return the `Interface` object with the given MAC address."""
1176 # Compare using EUI instances so that we're not concerned with a MAC's
1177@@ -77,6 +70,12 @@
1178 'set_active',
1179 'create',
1180 'create_interface',
1181+ 'create_physical',
1182+ 'update_interface',
1183+ 'delete_interface',
1184+ 'link_subnet',
1185+ 'unlink_subnet',
1186+ 'update',
1187 'action']
1188 exclude = [
1189 "creation_type",
1190@@ -149,32 +148,39 @@
1191
1192 def dehydrate(self, obj, data, for_list=False):
1193 """Add extra fields to `data`."""
1194- data["fqdn"] = obj.fqdn
1195- data["actions"] = list(compile_node_actions(obj, self.user).keys())
1196- data["node_type_display"] = obj.get_node_type_display()
1197+ data = super().dehydrate(obj, data, for_list=for_list)
1198
1199+ # We handle interfaces ourselves, because of ip_assignment.
1200 boot_interface = obj.get_boot_interface()
1201 data["primary_mac"] = (
1202 "%s" % boot_interface.mac_address
1203 if boot_interface is not None else "")
1204- data["extra_macs"] = [
1205- "%s" % interface.mac_address
1206- for interface in obj.interface_set.all()
1207- if interface != boot_interface
1208- ]
1209-
1210- data["ip_assignment"] = self.dehydrate_ip_assignment(
1211- obj, boot_interface)
1212- data["ip_address"] = self.dehydrate_ip_address(
1213- obj, boot_interface)
1214-
1215- data["tags"] = [
1216- tag.name
1217- for tag in obj.tags.all()
1218- ]
1219- return data
1220-
1221- def _get_first_none_discovered_ip(self, ip_addresses):
1222+
1223+ if for_list:
1224+ data["ip_assignment"] = self.dehydrate_ip_assignment(
1225+ obj, boot_interface)
1226+ data["ip_address"] = self.dehydrate_ip_address(obj, boot_interface)
1227+ else:
1228+ data["interfaces"] = [
1229+ self.dehydrate_interface(interface, obj)
1230+ for interface in obj.interface_set.all().order_by('name')
1231+ ]
1232+ # Propogate the boot interface ip assignment/address to the device.
1233+ for iface_data in data["interfaces"]:
1234+ if iface_data["name"] == boot_interface.name:
1235+ data["ip_assignment"] = iface_data["ip_assignment"]
1236+ data["ip_address"] = iface_data["ip_address"]
1237+
1238+ return data
1239+
1240+ def dehydrate_interface(self, interface, obj):
1241+ """Add extra fields to interface data."""
1242+ data = super().dehydrate_interface(interface, obj)
1243+ data["ip_assignment"] = self.dehydrate_ip_assignment(obj, interface)
1244+ data["ip_address"] = self.dehydrate_ip_address(obj, interface)
1245+ return data
1246+
1247+ def _get_first_non_discovered_ip(self, ip_addresses):
1248 for ip in ip_addresses:
1249 if ip.alloc_type != IPADDRESS_TYPE.DISCOVERED:
1250 return ip
1251@@ -190,15 +196,15 @@
1252 return ""
1253 # We get the IP address from the all() so the cache is used.
1254 ip_addresses = list(interface.ip_addresses.all())
1255- first_ip = self._get_first_none_discovered_ip(ip_addresses)
1256+ first_ip = self._get_first_non_discovered_ip(ip_addresses)
1257 if first_ip is not None:
1258 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:
1259- return DEVICE_IP_ASSIGNMENT.DYNAMIC
1260+ return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
1261 elif first_ip.subnet is None:
1262- return DEVICE_IP_ASSIGNMENT.EXTERNAL
1263+ return DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL
1264 else:
1265- return DEVICE_IP_ASSIGNMENT.STATIC
1266- return DEVICE_IP_ASSIGNMENT.DYNAMIC
1267+ return DEVICE_IP_ASSIGNMENT_TYPE.STATIC
1268+ return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
1269
1270 def dehydrate_ip_address(self, obj, interface):
1271 """Return the IP address for the device."""
1272@@ -207,7 +213,7 @@
1273
1274 # Get ip address from StaticIPAddress if available.
1275 ip_addresses = list(interface.ip_addresses.all())
1276- first_ip = self._get_first_none_discovered_ip(ip_addresses)
1277+ first_ip = self._get_first_non_discovered_ip(ip_addresses)
1278 if first_ip is not None:
1279 if first_ip.alloc_type == IPADDRESS_TYPE.DHCP:
1280 discovered_ip = self._get_first_discovered_ip_with_ip(
1281@@ -254,6 +260,8 @@
1282 "parent": params.get("parent"),
1283 }
1284
1285+ if "zone" in params:
1286+ new_params["zone"] = params["zone"]["name"]
1287 if "domain" in params:
1288 new_params["domain"] = params["domain"]["name"]
1289
1290@@ -268,16 +276,17 @@
1291 def _configure_interface(self, interface, params):
1292 """Configure the interface based on the selection."""
1293 ip_assignment = params["ip_assignment"]
1294- if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:
1295+ interface.ip_addresses.all().delete()
1296+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
1297 subnet = Subnet.objects.get_best_subnet_for_ip(
1298 params["ip_address"])
1299 sticky_ip = StaticIPAddress.objects.create(
1300 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
1301 ip=params["ip_address"], subnet=subnet, user=self.user)
1302 interface.ip_addresses.add(sticky_ip)
1303- elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:
1304+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
1305 interface.link_subnet(INTERFACE_LINK_TYPE.DHCP, None)
1306- elif ip_assignment == DEVICE_IP_ASSIGNMENT.STATIC:
1307+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
1308 # Convert an empty string to None.
1309 ip_address = params.get("ip_address")
1310 if not ip_address:
1311@@ -316,6 +325,76 @@
1312 else:
1313 raise HandlerValidationError(form.errors)
1314
1315+ def create_physical(self, params):
1316+ """Create a physical interface, an alias for create_interface."""
1317+ return self.create_interface(params)
1318+
1319+ def update_interface(self, params):
1320+ """Update the interface."""
1321+ # Only admin users can perform update.
1322+ if not reload_object(self.user).is_superuser:
1323+ raise HandlerPermissionError()
1324+
1325+ device = self.get_object(params)
1326+ interface = Interface.objects.get(
1327+ node=device, id=params["interface_id"])
1328+ interface_form = InterfaceForm.get_interface_form(interface.type)
1329+ form = interface_form(instance=interface, data=params)
1330+ if form.is_valid():
1331+ interface = form.save()
1332+ self._configure_interface(interface, params)
1333+ return self.full_dehydrate(reload_object(device))
1334+ else:
1335+ raise ValidationError(form.errors)
1336+
1337+ def delete_interface(self, params):
1338+ """Delete the interface."""
1339+ # Only admin users can perform delete.
1340+ if not reload_object(self.user).is_superuser:
1341+ raise HandlerPermissionError()
1342+
1343+ node = self.get_object(params)
1344+ interface = Interface.objects.get(node=node, id=params["interface_id"])
1345+ interface.delete()
1346+
1347+ def link_subnet(self, params):
1348+ """Create or update the link."""
1349+ # Only admin users can perform update.
1350+ if not reload_object(self.user).is_superuser:
1351+ raise HandlerPermissionError()
1352+
1353+ node = self.get_object(params)
1354+ interface = Interface.objects.get(node=node, id=params["interface_id"])
1355+ if params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
1356+ mode = INTERFACE_LINK_TYPE.STATIC
1357+ elif params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
1358+ mode = INTERFACE_LINK_TYPE.DHCP
1359+ else:
1360+ mode = INTERFACE_LINK_TYPE.LINK_UP
1361+ subnet = None
1362+ if "subnet" in params:
1363+ subnet = Subnet.objects.get(id=params["subnet"])
1364+ if "link_id" in params:
1365+ # We are updating an already existing link.
1366+ interface.update_link_by_id(
1367+ params["link_id"], mode, subnet,
1368+ ip_address=params.get("ip_address", None))
1369+ elif params['ip_assignment'] == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
1370+ # We are creating a new link.
1371+ interface.link_subnet(
1372+ INTERFACE_LINK_TYPE.STATIC, subnet,
1373+ ip_address=params.get("ip_address", None))
1374+
1375+ def unlink_subnet(self, params):
1376+ """Delete the link."""
1377+ # Only admin users can perform unlink.
1378+ if not reload_object(self.user).is_superuser:
1379+ raise HandlerPermissionError()
1380+
1381+ node = self.get_object(params)
1382+ interface = Interface.objects.get(node=node, id=params["interface_id"])
1383+ interface.unlink_subnet_by_id(params["link_id"])
1384+
1385 def action(self, params):
1386 """Perform the action on the object."""
1387 obj = self.get_object(params)
1388@@ -327,3 +406,11 @@
1389 "%s action is not available for this device." % action_name)
1390 extra_params = params.get("extra", {})
1391 return action.execute(**extra_params)
1392+
1393+ def update(self, params):
1394+ """Update the object from params."""
1395+ # Only admin users can perform update.
1396+ if not reload_object(self.user).is_superuser:
1397+ raise HandlerPermissionError()
1398+
1399+ return super().update(params)
1400
1401=== modified file 'src/maasserver/websockets/handlers/node.py'
1402--- src/maasserver/websockets/handlers/node.py 2017-02-17 14:23:04 +0000
1403+++ src/maasserver/websockets/handlers/node.py 2017-02-28 04:16:58 +0000
1404@@ -16,6 +16,7 @@
1405 FILESYSTEM_FORMAT_TYPE_CHOICES,
1406 FILESYSTEM_FORMAT_TYPE_CHOICES_DICT,
1407 NODE_STATUS,
1408+ NODE_TYPE,
1409 )
1410 from maasserver.models.cacheset import CacheSet
1411 from maasserver.models.config import Config
1412@@ -99,36 +100,13 @@
1413 def dehydrate(self, obj, data, for_list=False):
1414 """Add extra fields to `data`."""
1415 data["fqdn"] = obj.fqdn
1416- data["status"] = obj.display_status()
1417- data["status_code"] = obj.status
1418 data["actions"] = list(compile_node_actions(obj, self.user).keys())
1419- data["memory"] = obj.display_memory()
1420 data["node_type_display"] = obj.get_node_type_display()
1421
1422 data["extra_macs"] = [
1423 "%s" % mac_address
1424 for mac_address in obj.get_extra_macs()
1425 ]
1426- boot_interface = obj.get_boot_interface()
1427- if boot_interface is not None:
1428- data["pxe_mac"] = "%s" % boot_interface.mac_address
1429- data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
1430- else:
1431- data["pxe_mac"] = data["pxe_mac_vendor"] = ""
1432-
1433- blockdevices = self.get_blockdevices_for(obj)
1434- physical_blockdevices = [
1435- blockdevice for blockdevice in blockdevices
1436- if isinstance(blockdevice, PhysicalBlockDevice)
1437- ]
1438- data["physical_disk_count"] = len(physical_blockdevices)
1439- data["storage"] = "%3.1f" % (
1440- sum([
1441- blockdevice.size
1442- for blockdevice in physical_blockdevices
1443- ]) / (1000 ** 3))
1444- data["storage_tags"] = self.get_all_storage_tags(blockdevices)
1445-
1446 subnets = self.get_all_subnets(obj)
1447 data["subnets"] = [subnet.cidr for subnet in subnets]
1448 data["fabrics"] = self.get_all_fabric_names(obj, subnets)
1449@@ -138,73 +116,100 @@
1450 tag.name
1451 for tag in obj.tags.all()
1452 ]
1453- data["osystem"] = obj.get_osystem(
1454- default=self.default_osystem)
1455- data["distro_series"] = obj.get_distro_series(
1456- default=self.default_distro_series)
1457- data["dhcp_on"] = self.get_providing_dhcp(obj)
1458+ if obj.node_type != NODE_TYPE.DEVICE:
1459+ data["memory"] = obj.display_memory()
1460+ data["status"] = obj.display_status()
1461+ data["status_code"] = obj.status
1462+ boot_interface = obj.get_boot_interface()
1463+ if boot_interface is not None:
1464+ data["pxe_mac"] = "%s" % boot_interface.mac_address
1465+ data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
1466+ else:
1467+ data["pxe_mac"] = data["pxe_mac_vendor"] = ""
1468+
1469+ blockdevices = self.get_blockdevices_for(obj)
1470+ physical_blockdevices = [
1471+ blockdevice for blockdevice in blockdevices
1472+ if isinstance(blockdevice, PhysicalBlockDevice)
1473+ ]
1474+ data["physical_disk_count"] = len(physical_blockdevices)
1475+ data["storage"] = "%3.1f" % (
1476+ sum(
1477+ blockdevice.size
1478+ for blockdevice in physical_blockdevices
1479+ ) / (1000 ** 3))
1480+ data["storage_tags"] = self.get_all_storage_tags(blockdevices)
1481+
1482+ data["osystem"] = obj.get_osystem(
1483+ default=self.default_osystem)
1484+ data["distro_series"] = obj.get_distro_series(
1485+ default=self.default_distro_series)
1486+ data["dhcp_on"] = self.get_providing_dhcp(obj)
1487 if not for_list:
1488- data["hwe_kernel"] = make_hwe_kernel_ui_text(obj.hwe_kernel)
1489-
1490- data["power_type"] = obj.power_type
1491- data["power_parameters"] = self.dehydrate_power_parameters(
1492- obj.power_parameters)
1493- data["power_bmc_node_count"] = obj.bmc.node_set.count() if (
1494- obj.bmc is not None) else 0
1495-
1496- # Network
1497- data["interfaces"] = [
1498- self.dehydrate_interface(interface, obj)
1499- for interface in obj.interface_set.all().order_by('name')
1500- ]
1501 data["on_network"] = obj.on_network()
1502-
1503- # Storage
1504- data["disks"] = sorted(chain(
1505- (self.dehydrate_blockdevice(blockdevice, obj)
1506- for blockdevice in blockdevices),
1507- (self.dehydrate_volume_group(volume_group) for volume_group
1508- in VolumeGroup.objects.filter_by_node(obj)),
1509- (self.dehydrate_cache_set(cache_set) for cache_set
1510- in CacheSet.objects.get_cache_sets_for_node(obj)),
1511- ), key=itemgetter("name"))
1512- data["supported_filesystems"] = [
1513- {'key': key, 'ui': ui}
1514- for key, ui in FILESYSTEM_FORMAT_TYPE_CHOICES
1515- ]
1516- data["storage_layout_issues"] = obj.storage_layout_issues()
1517- data["special_filesystems"] = [
1518- self.dehydrate_filesystem(filesystem)
1519- for filesystem in obj.special_filesystems.order_by("id")
1520- ]
1521-
1522- # Events
1523- data["events"] = self.dehydrate_events(obj)
1524-
1525- # Machine output
1526- data = self.dehydrate_summary_output(obj, data)
1527- # XXX ltrager 2017-01-27 - Show the testing results in the
1528- # commissioning table until we get the testing UI done.
1529- if obj.current_testing_script_set is not None:
1530- data["commissioning_results"] = self.dehydrate_script_set(
1531- chain(
1532- obj.current_commissioning_script_set,
1533- obj.current_testing_script_set,
1534- ))
1535- else:
1536- data["commissioning_results"] = self.dehydrate_script_set(
1537- obj.current_commissioning_script_set)
1538- data["installation_results"] = self.dehydrate_script_set(
1539- obj.current_installation_script_set)
1540-
1541- # Third party drivers
1542- if Config.objects.get_config('enable_third_party_drivers'):
1543- driver = get_third_party_driver(obj)
1544- if "module" in driver and "comment" in driver:
1545- data["third_party_driver"] = {
1546- "module": driver["module"],
1547- "comment": driver["comment"],
1548- }
1549+ if obj.node_type != NODE_TYPE.DEVICE:
1550+ # XXX lamont 2017-02-15 Much of this should be split out into
1551+ # individual methods, rather than having this huge block of
1552+ # dense code here.
1553+ # Network
1554+ data["interfaces"] = [
1555+ self.dehydrate_interface(interface, obj)
1556+ for interface in obj.interface_set.all().order_by('name')
1557+ ]
1558+
1559+ data["hwe_kernel"] = make_hwe_kernel_ui_text(obj.hwe_kernel)
1560+
1561+ data["power_type"] = obj.power_type
1562+ data["power_parameters"] = self.dehydrate_power_parameters(
1563+ obj.power_parameters)
1564+ data["power_bmc_node_count"] = obj.bmc.node_set.count() if (
1565+ obj.bmc is not None) else 0
1566+
1567+ # Storage
1568+ data["disks"] = sorted(chain(
1569+ (self.dehydrate_blockdevice(blockdevice, obj)
1570+ for blockdevice in blockdevices),
1571+ (self.dehydrate_volume_group(volume_group) for volume_group
1572+ in VolumeGroup.objects.filter_by_node(obj)),
1573+ (self.dehydrate_cache_set(cache_set) for cache_set
1574+ in CacheSet.objects.get_cache_sets_for_node(obj)),
1575+ ), key=itemgetter("name"))
1576+ data["supported_filesystems"] = [
1577+ {'key': key, 'ui': ui}
1578+ for key, ui in FILESYSTEM_FORMAT_TYPE_CHOICES
1579+ ]
1580+ data["storage_layout_issues"] = obj.storage_layout_issues()
1581+ data["special_filesystems"] = [
1582+ self.dehydrate_filesystem(filesystem)
1583+ for filesystem in obj.special_filesystems.order_by("id")
1584+ ]
1585+
1586+ # Events
1587+ data["events"] = self.dehydrate_events(obj)
1588+
1589+ # Machine output
1590+ data = self.dehydrate_summary_output(obj, data)
1591+ # XXX ltrager 2017-01-27 - Show the testing results in the
1592+ # commissioning table until we get the testing UI done.
1593+ if obj.current_testing_script_set is not None:
1594+ data["commissioning_results"] = self.dehydrate_script_set(
1595+ chain(
1596+ obj.current_commissioning_script_set,
1597+ obj.current_testing_script_set,
1598+ ))
1599+ else:
1600+ data["commissioning_results"] = self.dehydrate_script_set(
1601+ obj.current_commissioning_script_set)
1602+ data["installation_results"] = self.dehydrate_script_set(
1603+ obj.current_installation_script_set)
1604+ # Third party drivers
1605+ if Config.objects.get_config('enable_third_party_drivers'):
1606+ driver = get_third_party_driver(obj)
1607+ if "module" in driver and "comment" in driver:
1608+ data["third_party_driver"] = {
1609+ "module": driver["module"],
1610+ "comment": driver["comment"],
1611+ }
1612
1613 return data
1614
1615@@ -495,8 +500,8 @@
1616 def get_all_space_names(self, subnets):
1617 space_names = set()
1618 for subnet in subnets:
1619- if subnet.space is not None:
1620- space_names.add(subnet.space.name)
1621+ if subnet.vlan.space is not None:
1622+ space_names.add(subnet.vlan.space.name)
1623 return list(space_names)
1624
1625 def get_blockdevices_for(self, obj):
1626
1627=== modified file 'src/maasserver/websockets/handlers/tests/test_device.py'
1628--- src/maasserver/websockets/handlers/tests/test_device.py 2017-01-28 00:51:47 +0000
1629+++ src/maasserver/websockets/handlers/tests/test_device.py 2017-02-28 04:16:58 +0000
1630@@ -5,7 +5,15 @@
1631
1632 __all__ = []
1633
1634+from operator import itemgetter
1635+import random
1636+from unittest.mock import ANY
1637+
1638 from maasserver.enum import (
1639+ DEVICE_IP_ASSIGNMENT_TYPE,
1640+ DEVICE_IP_ASSIGNMENT_TYPE_CHOICES,
1641+ INTERFACE_LINK_TYPE,
1642+ INTERFACE_TYPE,
1643 IPADDRESS_TYPE,
1644 NODE_TYPE,
1645 )
1646@@ -28,13 +36,17 @@
1647 dehydrate_datetime,
1648 HandlerDoesNotExistError,
1649 HandlerError,
1650+ HandlerPermissionError,
1651 HandlerValidationError,
1652 )
1653-from maasserver.websockets.handlers.device import (
1654- DEVICE_IP_ASSIGNMENT,
1655- DeviceHandler,
1656-)
1657+from maasserver.websockets.handlers.device import DeviceHandler
1658 from maastesting.djangotestcase import count_queries
1659+from maastesting.matchers import MockCalledOnceWith
1660+from maastesting.matchers import MockNotCalled
1661+from netaddr import (
1662+ IPAddress,
1663+ IPNetwork,
1664+)
1665 from testtools import ExpectedException
1666 from testtools.matchers import (
1667 Equals,
1668@@ -44,27 +56,25 @@
1669
1670 class TestDeviceHandler(MAASTransactionServerTestCase):
1671
1672- def dehydrate_ip_assignment(self, device):
1673- boot_interface = device.get_boot_interface()
1674- if boot_interface is None:
1675+ def dehydrate_ip_assignment(self, device, interface):
1676+ if interface is None:
1677 return ""
1678- ip_address = boot_interface.ip_addresses.exclude(
1679+ ip_address = interface.ip_addresses.exclude(
1680 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()
1681 if ip_address is not None:
1682 if ip_address.alloc_type == IPADDRESS_TYPE.DHCP:
1683- return DEVICE_IP_ASSIGNMENT.DYNAMIC
1684+ return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
1685 elif ip_address.subnet is None:
1686- return DEVICE_IP_ASSIGNMENT.EXTERNAL
1687+ return DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL
1688 else:
1689- return DEVICE_IP_ASSIGNMENT.STATIC
1690- return DEVICE_IP_ASSIGNMENT.DYNAMIC
1691+ return DEVICE_IP_ASSIGNMENT_TYPE.STATIC
1692+ return DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC
1693
1694- def dehydrate_ip_address(self, device):
1695+ def dehydrate_ip_address(self, device, interface):
1696 """Return the IP address for the device."""
1697- boot_interface = device.get_boot_interface()
1698- if boot_interface is None:
1699+ if interface is None:
1700 return None
1701- static_ip = boot_interface.ip_addresses.exclude(
1702+ static_ip = interface.ip_addresses.exclude(
1703 alloc_type=IPADDRESS_TYPE.DISCOVERED).first()
1704 if static_ip is not None:
1705 ip = static_ip.get_ip()
1706@@ -72,8 +82,58 @@
1707 return "%s" % ip
1708 return None
1709
1710+ def dehydrate_interface(self, interface, obj):
1711+ """Dehydrate a `interface` into a interface definition."""
1712+ # Sort the links by ID that way they show up in the same order in
1713+ # the UI.
1714+ links = sorted(interface.get_links(), key=itemgetter("id"))
1715+ for link in links:
1716+ # Replace the subnet object with the subnet_id. The client will
1717+ # use this information to pull the subnet information from the
1718+ # websocket.
1719+ subnet = link.pop("subnet", None)
1720+ if subnet is not None:
1721+ link["subnet_id"] = subnet.id
1722+ data = {
1723+ "id": interface.id,
1724+ "type": interface.type,
1725+ "name": interface.get_name(),
1726+ "enabled": interface.is_enabled(),
1727+ "tags": interface.tags,
1728+ "is_boot": interface == obj.get_boot_interface(),
1729+ "mac_address": "%s" % interface.mac_address,
1730+ "vlan_id": interface.vlan_id,
1731+ "parents": [
1732+ nic.id
1733+ for nic in interface.parents.all()
1734+ ],
1735+ "children": [
1736+ nic.child.id
1737+ for nic in interface.children_relationships.all()
1738+ ],
1739+ "links": links,
1740+ "ip_assignment": self.dehydrate_ip_assignment(obj, interface),
1741+ "ip_address": self.dehydrate_ip_address(obj, interface),
1742+ }
1743+ return data
1744+
1745 def dehydrate_device(self, node, user, for_list=False):
1746 boot_interface = node.get_boot_interface()
1747+ subnets = set(
1748+ ip_address.subnet
1749+ for interface in node.interface_set.all()
1750+ for ip_address in interface.ip_addresses.all()
1751+ if ip_address.subnet is not None)
1752+ space_names = set(
1753+ subnet.space.name
1754+ for subnet in subnets
1755+ if subnet.space is not None)
1756+ fabric_names = set(
1757+ iface.vlan.fabric.name
1758+ for iface in node.interface_set.all()
1759+ if iface.vlan is not None)
1760+ fabric_names.update({subnet.vlan.fabric.name for subnet in subnets})
1761+ boot_interface = node.get_boot_interface()
1762 data = {
1763 "actions": list(compile_node_actions(node, user).keys()),
1764 "bmc": node.bmc_id,
1765@@ -95,8 +155,17 @@
1766 "%s" % boot_interface.mac_address),
1767 "parent": (
1768 node.parent.system_id if node.parent is not None else None),
1769- "ip_address": self.dehydrate_ip_address(node),
1770- "ip_assignment": self.dehydrate_ip_assignment(node),
1771+ "ip_address": self.dehydrate_ip_address(node, boot_interface),
1772+ "ip_assignment": self.dehydrate_ip_assignment(
1773+ node, boot_interface),
1774+ "interfaces": [
1775+ self.dehydrate_interface(interface, node)
1776+ for interface in node.interface_set.all().order_by('name')
1777+ ],
1778+ "subnets": [subnet.cidr for subnet in subnets],
1779+ "fabrics": list(fabric_names),
1780+ "spaces": list(space_names),
1781+ "on_network": node.on_network(),
1782 "owner": "" if node.owner is None else node.owner.username,
1783 "swap_size": node.swap_size,
1784 "system_id": node.system_id,
1785@@ -121,6 +190,9 @@
1786 "ip_address",
1787 "ip_assignment",
1788 "node_type_display",
1789+ "subnets",
1790+ "spaces",
1791+ "fabrics",
1792 ]
1793 for key in list(data):
1794 if key not in allowed_fields:
1795@@ -133,20 +205,20 @@
1796 for a device. This will setup the model to make sure the device will
1797 match `ip_assignment`."""
1798 if ip_assignment is None:
1799- ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT)
1800+ ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
1801 if owner is None:
1802 owner = factory.make_User()
1803 device = factory.make_Node(
1804 node_type=NODE_TYPE.DEVICE,
1805 interface=True, owner=owner)
1806 interface = device.get_boot_interface()
1807- if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:
1808+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
1809 subnet = factory.make_Subnet()
1810 factory.make_StaticIPAddress(
1811 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
1812 ip=factory.pick_ip_in_network(subnet.get_ipnetwork()),
1813 subnet=subnet, user=owner)
1814- elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:
1815+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
1816 factory.make_StaticIPAddress(
1817 alloc_type=IPADDRESS_TYPE.DHCP, ip="", interface=interface)
1818 else:
1819@@ -206,7 +278,7 @@
1820 handler.list({}))
1821
1822 @transactional
1823- def test_list_num_queries_is_independent_of_num_devices(self):
1824+ def test_list_num_queries_is_the_expected_number(self):
1825 owner = factory.make_User()
1826 handler = DeviceHandler(owner, {})
1827 self.make_devices(10, owner=owner)
1828@@ -222,6 +294,21 @@
1829 self.assertEqual(
1830 query_10_count, 12,
1831 "Number of queries has changed; make sure this is expected.")
1832+
1833+ @transactional
1834+ def test_list_num_queries_is_independent_of_num_devices(self):
1835+ owner = factory.make_User()
1836+ handler = DeviceHandler(owner, {})
1837+ self.make_devices(10, owner=owner)
1838+ query_10_count, _ = count_queries(handler.list, {})
1839+ self.make_devices(10, owner=owner)
1840+ query_20_count, _ = count_queries(handler.list, {})
1841+
1842+ # This check is to notify the developer that a change was made that
1843+ # affects the number of queries performed when doing a node listing.
1844+ # It is important to keep this number as low as possible. A larger
1845+ # number means regiond has to do more work slowing down its process
1846+ # and slowing down the client waiting for the response.
1847 self.assertEqual(
1848 query_10_count, query_20_count,
1849 "Number of queries is not independent to the number of nodes.")
1850@@ -308,7 +395,7 @@
1851 "primary_mac": mac,
1852 "interfaces": [{
1853 "mac": mac,
1854- "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,
1855+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
1856 }],
1857 })
1858 self.expectThat(created_device["hostname"], Equals(hostname))
1859@@ -316,7 +403,7 @@
1860 self.expectThat(created_device["extra_macs"], Equals([]))
1861 self.expectThat(
1862 created_device["ip_assignment"],
1863- Equals(DEVICE_IP_ASSIGNMENT.DYNAMIC))
1864+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC))
1865 self.expectThat(created_device["ip_address"], Is(None))
1866 self.expectThat(created_device["owner"], Equals(user.username))
1867
1868@@ -332,13 +419,13 @@
1869 "primary_mac": mac,
1870 "interfaces": [{
1871 "mac": mac,
1872- "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,
1873+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
1874 "ip_address": ip_address,
1875 }],
1876 })
1877 self.expectThat(
1878 created_device["ip_assignment"],
1879- Equals(DEVICE_IP_ASSIGNMENT.EXTERNAL))
1880+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL))
1881 self.expectThat(created_device["ip_address"], Equals(ip_address))
1882 self.expectThat(
1883 StaticIPAddress.objects.filter(ip=ip_address).count(),
1884@@ -356,13 +443,13 @@
1885 "primary_mac": mac,
1886 "interfaces": [{
1887 "mac": mac,
1888- "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,
1889+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
1890 "subnet": subnet.id,
1891 }],
1892 })
1893 self.expectThat(
1894 created_device["ip_assignment"],
1895- Equals(DEVICE_IP_ASSIGNMENT.STATIC))
1896+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
1897 static_interface = Interface.objects.get(mac_address=MAC(mac))
1898 observed_subnet = static_interface.ip_addresses.first().subnet
1899 self.expectThat(
1900@@ -386,14 +473,14 @@
1901 "primary_mac": mac,
1902 "interfaces": [{
1903 "mac": mac,
1904- "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,
1905+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
1906 "subnet": subnet.id,
1907 "ip_address": ip_address,
1908 }],
1909 })
1910 self.expectThat(
1911 created_device["ip_assignment"],
1912- Equals(DEVICE_IP_ASSIGNMENT.STATIC))
1913+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
1914 self.expectThat(created_device["ip_address"], Equals(ip_address))
1915 static_interface = Interface.objects.get(mac_address=MAC(mac))
1916 observed_subnet = static_interface.ip_addresses.first().subnet
1917@@ -423,13 +510,13 @@
1918 "interfaces": [
1919 {
1920 "mac": mac_static,
1921- "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,
1922+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
1923 "subnet": subnet.id,
1924 "ip_address": static_ip_address,
1925 },
1926 {
1927 "mac": mac_external,
1928- "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,
1929+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
1930 "ip_address": external_ip_address,
1931 },
1932 ],
1933@@ -442,7 +529,7 @@
1934 Equals([mac_external]))
1935 self.expectThat(
1936 created_device["ip_assignment"],
1937- Equals(DEVICE_IP_ASSIGNMENT.STATIC))
1938+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
1939 self.expectThat(
1940 created_device["ip_address"], Equals(static_ip_address))
1941 static_interface = Interface.objects.get(mac_address=MAC(mac_static))
1942@@ -467,7 +554,7 @@
1943 "primary_mac": mac.lower(), # Lowercase.
1944 "interfaces": [{
1945 "mac": mac.upper(), # Uppercase.
1946- "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,
1947+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
1948 }],
1949 })
1950 self.assertThat(created_device["primary_mac"], Equals(mac))
1951@@ -482,7 +569,7 @@
1952 "primary_mac": mac, # Colons.
1953 "interfaces": [{
1954 "mac": mac.replace(":", "-"), # Hyphens.
1955- "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,
1956+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
1957 }],
1958 })
1959 self.assertThat(created_device["primary_mac"], Equals(mac))
1960@@ -511,12 +598,12 @@
1961 updated_device = handler.create_interface({
1962 "system_id": device.system_id,
1963 "mac_address": mac,
1964- "ip_assignment": DEVICE_IP_ASSIGNMENT.DYNAMIC,
1965+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
1966 })
1967 self.expectThat(updated_device["primary_mac"], Equals(mac))
1968 self.expectThat(
1969 updated_device["ip_assignment"],
1970- Equals(DEVICE_IP_ASSIGNMENT.DYNAMIC))
1971+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC))
1972
1973 @transactional
1974 def test_create_interface_creates_with_external_ip_assignment(self):
1975@@ -528,13 +615,13 @@
1976 updated_device = handler.create_interface({
1977 "system_id": device.system_id,
1978 "mac_address": mac,
1979- "ip_assignment": DEVICE_IP_ASSIGNMENT.EXTERNAL,
1980+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
1981 "ip_address": ip_address,
1982 })
1983 self.expectThat(updated_device["primary_mac"], Equals(mac))
1984 self.expectThat(
1985 updated_device["ip_assignment"],
1986- Equals(DEVICE_IP_ASSIGNMENT.EXTERNAL))
1987+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL))
1988 self.expectThat(
1989 StaticIPAddress.objects.filter(ip=ip_address).count(),
1990 Equals(1), "StaticIPAddress was not created.")
1991@@ -549,13 +636,13 @@
1992 updated_device = handler.create_interface({
1993 "system_id": device.system_id,
1994 "mac_address": mac,
1995- "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,
1996+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
1997 "subnet": subnet.id,
1998 })
1999 self.expectThat(updated_device["primary_mac"], Equals(mac))
2000 self.expectThat(
2001 updated_device["ip_assignment"],
2002- Equals(DEVICE_IP_ASSIGNMENT.STATIC))
2003+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
2004 static_interface = Interface.objects.get(mac_address=MAC(mac))
2005 observed_subnet = static_interface.ip_addresses.first().subnet
2006 self.expectThat(
2007@@ -573,14 +660,14 @@
2008 updated_device = handler.create_interface({
2009 "system_id": device.system_id,
2010 "mac_address": mac,
2011- "ip_assignment": DEVICE_IP_ASSIGNMENT.STATIC,
2012+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
2013 "subnet": subnet.id,
2014 "ip_address": ip_address,
2015 })
2016 self.expectThat(updated_device["primary_mac"], Equals(mac))
2017 self.expectThat(
2018 updated_device["ip_assignment"],
2019- Equals(DEVICE_IP_ASSIGNMENT.STATIC))
2020+ Equals(DEVICE_IP_ASSIGNMENT_TYPE.STATIC))
2021 self.expectThat(updated_device["ip_address"], Equals(ip_address))
2022 static_interface = Interface.objects.get(mac_address=MAC(mac))
2023 observed_subnet = static_interface.ip_addresses.first().subnet
2024@@ -641,3 +728,238 @@
2025 }})
2026 device = reload_object(device)
2027 self.expectThat(device.zone, Equals(zone))
2028+
2029+ @transactional
2030+ def test_create_interface_creates_interface(self):
2031+ user = factory.make_admin()
2032+ node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
2033+ handler = DeviceHandler(user, {})
2034+ name = factory.make_name("eth")
2035+ mac_address = factory.make_mac_address()
2036+ handler.create_interface({
2037+ "system_id": node.system_id,
2038+ "name": name,
2039+ "mac_address": mac_address,
2040+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC,
2041+ })
2042+ self.assertEqual(
2043+ 1, node.interface_set.count(),
2044+ "Should have one interface on the node.")
2045+
2046+ @transactional
2047+ def test_create_interface_creates_static(self):
2048+ user = factory.make_admin()
2049+ node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
2050+ handler = DeviceHandler(user, {})
2051+ name = factory.make_name("eth")
2052+ mac_address = factory.make_mac_address()
2053+ fabric = factory.make_Fabric()
2054+ vlan = fabric.get_default_vlan()
2055+ subnet = factory.make_Subnet(vlan=vlan)
2056+ handler.create_interface({
2057+ "system_id": node.system_id,
2058+ "name": name,
2059+ "mac_address": mac_address,
2060+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.STATIC,
2061+ "subnet": subnet.id,
2062+ })
2063+ new_interface = node.interface_set.first()
2064+ self.assertIsNotNone(new_interface)
2065+ auto_ip = new_interface.ip_addresses.filter(
2066+ alloc_type=IPADDRESS_TYPE.STICKY, subnet=subnet)
2067+ self.assertIsNotNone(auto_ip)
2068+ self.assertEqual(1, len(auto_ip))
2069+
2070+ @transactional
2071+ def test_create_interface_creates_external(self):
2072+ user = factory.make_admin()
2073+ node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
2074+ handler = DeviceHandler(user, {})
2075+ name = factory.make_name("eth")
2076+ mac_address = factory.make_mac_address()
2077+ ip_address = factory.make_ip_address()
2078+ handler.create_interface({
2079+ "system_id": node.system_id,
2080+ "name": name,
2081+ "mac_address": mac_address,
2082+ "ip_assignment": DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL,
2083+ "ip_address": ip_address,
2084+ })
2085+ new_interface = node.interface_set.first()
2086+ self.assertIsNotNone(new_interface)
2087+ auto_ip = new_interface.ip_addresses.filter(
2088+ alloc_type=IPADDRESS_TYPE.USER_RESERVED)
2089+ self.assertIsNotNone(auto_ip)
2090+ self.assertEqual(1, len(auto_ip))
2091+
2092+ @transactional
2093+ def test_update_interface_updates(self):
2094+ user = factory.make_admin()
2095+ node = factory.make_Node(interface=False, node_type=NODE_TYPE.DEVICE)
2096+ handler = DeviceHandler(user, {})
2097+ name = factory.make_name("eth")
2098+ mac_address = factory.make_mac_address()
2099+ ip_assignment = random.choice(DEVICE_IP_ASSIGNMENT_TYPE_CHOICES)[0]
2100+ params = {
2101+ "system_id": node.system_id,
2102+ "name": name,
2103+ "mac_address": mac_address,
2104+ "ip_assignment": ip_assignment,
2105+ }
2106+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
2107+ subnet = factory.make_Subnet()
2108+ params['subnet'] = subnet.id
2109+ ip_address = str(IPAddress(IPNetwork(subnet.cidr).first))
2110+ params['ip_address'] = ip_address
2111+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
2112+ ip_address = factory.make_ip_address()
2113+ params['ip_address'] = ip_address
2114+ handler.create_interface(params)
2115+ interface = node.interface_set.first()
2116+ self.assertIsNotNone(interface)
2117+ new_name = factory.make_name("eth")
2118+ new_ip_assignment = random.choice(DEVICE_IP_ASSIGNMENT_TYPE_CHOICES)[0]
2119+ new_params = {
2120+ "system_id": node.system_id,
2121+ "interface_id": interface.id,
2122+ "name": new_name,
2123+ "mac_address": mac_address,
2124+ "ip_assignment": new_ip_assignment,
2125+ }
2126+ if new_ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
2127+ new_subnet = factory.make_Subnet()
2128+ new_params['subnet'] = new_subnet.id
2129+ new_ip_address = str(IPAddress(IPNetwork(new_subnet.cidr).first))
2130+ new_params['ip_address'] = new_ip_address
2131+ elif new_ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.EXTERNAL:
2132+ new_ip_address = factory.make_ip_address()
2133+ new_params['ip_address'] = new_ip_address
2134+ handler.update_interface(new_params)
2135+ data = self.dehydrate_device(node, user)['interfaces']
2136+ self.assertEqual(1, len(data))
2137+ self.assertEqual(data[0]['ip_assignment'], new_ip_assignment)
2138+ if new_ip_assignment != DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
2139+ self.assertEqual(data[0]['ip_address'], new_ip_address)
2140+
2141+ @transactional
2142+ def test_update_raise_permissions_error_for_non_admin(self):
2143+ user = factory.make_User()
2144+ handler = DeviceHandler(user, {})
2145+ self.assertRaises(
2146+ HandlerPermissionError,
2147+ handler.update, {})
2148+
2149+ @transactional
2150+ def test_update_does_not_raise_validation_error_for_invalid_arch(self):
2151+ user = factory.make_admin()
2152+ handler = DeviceHandler(user, {})
2153+ node = factory.make_Node(interface=True, node_type=NODE_TYPE.DEVICE)
2154+ node_data = self.dehydrate_device(node, user)
2155+ arch = factory.make_name("arch")
2156+ node_data["architecture"] = arch
2157+ handler.update(node_data)
2158+ # succeeds, because Devices don't care about architecture.
2159+
2160+ @transactional
2161+ def test_update_updates_node(self):
2162+ user = factory.make_admin()
2163+ handler = DeviceHandler(user, {})
2164+ node = factory.make_Node(interface=True, node_type=NODE_TYPE.DEVICE)
2165+ node_data = self.dehydrate_device(node, user)
2166+ new_zone = factory.make_Zone()
2167+ new_hostname = factory.make_name("hostname")
2168+ node_data["hostname"] = new_hostname
2169+ node_data["zone"] = {
2170+ "name": new_zone.name,
2171+ }
2172+ updated_node = handler.update(node_data)
2173+ self.assertEqual(updated_node["hostname"], new_hostname)
2174+ self.assertEqual(updated_node["zone"]["id"], new_zone.id)
2175+
2176+ @transactional
2177+ def test_delete_interface(self):
2178+ user = factory.make_admin()
2179+ node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
2180+ handler = DeviceHandler(user, {})
2181+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2182+ handler.delete_interface({
2183+ "system_id": node.system_id,
2184+ "interface_id": interface.id,
2185+ })
2186+ self.assertIsNone(reload_object(interface))
2187+
2188+ @transactional
2189+ def test_link_subnet_calls_update_link_by_id_if_link_id(self):
2190+ user = factory.make_admin()
2191+ node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
2192+ handler = DeviceHandler(user, {})
2193+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2194+ subnet = factory.make_Subnet()
2195+ link_id = random.randint(0, 100)
2196+ ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
2197+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
2198+ mode = INTERFACE_LINK_TYPE.STATIC
2199+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
2200+ mode = INTERFACE_LINK_TYPE.DHCP
2201+ else:
2202+ mode = INTERFACE_LINK_TYPE.LINK_UP
2203+ ip_address = factory.make_ip_address()
2204+ self.patch_autospec(Interface, "update_link_by_id")
2205+ handler.link_subnet({
2206+ "system_id": node.system_id,
2207+ "interface_id": interface.id,
2208+ "link_id": link_id,
2209+ "subnet": subnet.id,
2210+ "ip_assignment": ip_assignment,
2211+ "ip_address": ip_address,
2212+ })
2213+ self.assertThat(
2214+ Interface.update_link_by_id,
2215+ MockCalledOnceWith(
2216+ ANY, link_id, mode, subnet, ip_address=ip_address))
2217+
2218+ @transactional
2219+ def test_link_subnet_calls_link_subnet_if_not_link_id(self):
2220+ user = factory.make_admin()
2221+ node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
2222+ handler = DeviceHandler(user, {})
2223+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2224+ subnet = factory.make_Subnet()
2225+ ip_assignment = factory.pick_enum(DEVICE_IP_ASSIGNMENT_TYPE)
2226+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
2227+ mode = INTERFACE_LINK_TYPE.STATIC
2228+ elif ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.DYNAMIC:
2229+ mode = INTERFACE_LINK_TYPE.DHCP
2230+ else:
2231+ mode = INTERFACE_LINK_TYPE.LINK_UP
2232+ ip_address = factory.make_ip_address()
2233+ self.patch_autospec(Interface, "link_subnet")
2234+ handler.link_subnet({
2235+ "system_id": node.system_id,
2236+ "interface_id": interface.id,
2237+ "subnet": subnet.id,
2238+ "ip_assignment": ip_assignment,
2239+ "ip_address": ip_address,
2240+ })
2241+ if ip_assignment == DEVICE_IP_ASSIGNMENT_TYPE.STATIC:
2242+ self.assertThat(
2243+ Interface.link_subnet,
2244+ MockCalledOnceWith(
2245+ ANY, mode, subnet, ip_address=ip_address))
2246+ else:
2247+ self.assertThat(Interface.link_subnet, MockNotCalled())
2248+
2249+ @transactional
2250+ def test_unlink_subnet(self):
2251+ user = factory.make_admin()
2252+ node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
2253+ handler = DeviceHandler(user, {})
2254+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2255+ link_ip = factory.make_StaticIPAddress(
2256+ alloc_type=IPADDRESS_TYPE.AUTO, ip="", interface=interface)
2257+ handler.delete_interface({
2258+ "system_id": node.system_id,
2259+ "interface_id": interface.id,
2260+ "link_id": link_ip.id,
2261+ })
2262+ self.assertIsNone(reload_object(link_ip))