Merge lp:~lamont/maas/bug-1660188 into lp:~maas-committers/maas/trunk
- bug-1660188
- Merge into 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 |
Related bugs: |
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 : | # |
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:
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)) |
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.