Merge lp:~blake-rouse/maas/fix-1522898 into lp:~maas-committers/maas/trunk
- fix-1522898
- Merge into trunk
Proposed by
Blake Rouse
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4539 |
Proposed branch: | lp:~blake-rouse/maas/fix-1522898 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1057 lines (+408/-94) 7 files modified
docs/changelog.rst (+31/-1) src/maasserver/api/interfaces.py (+73/-34) src/maasserver/api/tests/test_interfaces.py (+223/-51) src/maasserver/forms_interface_link.py (+21/-7) src/maasserver/models/__init__.py (+20/-1) src/maasserver/tests/test_auth.py (+28/-0) src/maasserver/urls_api.py (+12/-0) |
To merge this branch: | bzr merge lp:~blake-rouse/maas/fix-1522898 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+279798@code.launchpad.net |
Commit message
Fix the node interfaces API to work for devices as well. Keep node-interface(s) on the cli for compatability. Update changelog to match 1.9 branch.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'docs/changelog.rst' |
2 | --- docs/changelog.rst 2015-11-23 15:18:53 +0000 |
3 | +++ docs/changelog.rst 2015-12-07 16:38:37 +0000 |
4 | @@ -3,6 +3,37 @@ |
5 | ========= |
6 | |
7 | |
8 | +1.9.0 (RC3) |
9 | +============= |
10 | + |
11 | +Major bugs fixed in this release |
12 | +-------------------------------- |
13 | + |
14 | +LP: #1522898 "node-interface" API should just be "interface" - to allow devices to use it |
15 | + |
16 | +LP: #1519527 Juju 1.25.1 proposed: lxc units all have the same IP address after upgrade from 1.7/1.8. |
17 | + |
18 | +LP: #1522294 MAAS fails to parse some DHCP leases. |
19 | + |
20 | +LP: #1519090 DHCP interface automatically obtains an IP even when the subnet is unmanaged. |
21 | + |
22 | +LP: #1519077 MAAS assigns IP addresses on unmanaged subnets without consideration for some addresses known to be in use. |
23 | + |
24 | +LP: #1519396 MTU field is not exposed over the API for VLAN. |
25 | + |
26 | +LP: #1521833 Updating subnet name removes dns_server. |
27 | + |
28 | +LP: #1519919 CC looks for NICs with kernel module loaded and fall back doesn't check persistent device names. |
29 | + |
30 | +LP: #1522225 Migration 0181 can fail on upgrade if disks across nodes have duplicate serial numbers. |
31 | + |
32 | +LP: #1519247 Migration 0146 can fail on upgrade when migrating unmanaged subnets. |
33 | + |
34 | +LP: #1519397 [UI] Once a cache_set is created the UI fails with ERROR. |
35 | + |
36 | +LP: #1519918 [UI] "failed to detect a valid IP address" when trying to view node details. |
37 | + |
38 | + |
39 | 1.9.0 (RC2) |
40 | ============= |
41 | |
42 | @@ -2369,4 +2400,3 @@ |
43 | #1185897 expose ability to re-commission node in api and cli |
44 | |
45 | #997092 Can't delete allocated node even if owned by self |
46 | - |
47 | |
48 | === modified file 'src/maasserver/api/interfaces.py' |
49 | --- src/maasserver/api/interfaces.py 2015-12-01 18:12:59 +0000 |
50 | +++ src/maasserver/api/interfaces.py 2015-12-07 16:38:37 +0000 |
51 | @@ -9,6 +9,7 @@ |
52 | OperationsHandler, |
53 | ) |
54 | from maasserver.enum import ( |
55 | + INTERFACE_LINK_TYPE, |
56 | INTERFACE_TYPE, |
57 | NODE_PERMISSION, |
58 | NODE_STATUS, |
59 | @@ -66,29 +67,29 @@ |
60 | "Cannot %s interface because the node is not Ready." % operation) |
61 | |
62 | |
63 | -class NodeInterfacesHandler(OperationsHandler): |
64 | - """Manage interfaces on a node.""" |
65 | - api_doc_section_name = "Node Interfaces" |
66 | +class InterfacesHandler(OperationsHandler): |
67 | + """Manage interfaces on a node or device.""" |
68 | + api_doc_section_name = "Interfaces" |
69 | create = update = delete = None |
70 | fields = DISPLAYED_INTERFACE_FIELDS |
71 | |
72 | @classmethod |
73 | def resource_uri(cls, *args, **kwargs): |
74 | # See the comment in NodeHandler.resource_uri. |
75 | - return ('node_interfaces_handler', ["system_id"]) |
76 | + return ('interfaces_handler', ["system_id"]) |
77 | |
78 | def read(self, request, system_id): |
79 | - """List all interfaces belonging to node. |
80 | + """List all interfaces belonging to a node or device. |
81 | |
82 | Returns 404 if the node is not found. |
83 | """ |
84 | - node = Node.nodes.get_node_or_404( |
85 | + node = Node.objects.get_node_or_404( |
86 | system_id, request.user, NODE_PERMISSION.VIEW) |
87 | return node.interface_set.all() |
88 | |
89 | @operation(idempotent=False) |
90 | def create_physical(self, request, system_id): |
91 | - """Create a physical interface on node. |
92 | + """Create a physical interface on a node or device. |
93 | |
94 | :param name: Name of the interface. |
95 | :param mac_address: MAC address of the interface. |
96 | @@ -103,10 +104,12 @@ |
97 | |
98 | Returns 404 if the node is not found. |
99 | """ |
100 | - node = Node.nodes.get_node_or_404( |
101 | - system_id, request.user, NODE_PERMISSION.ADMIN) |
102 | - raise_error_for_invalid_state_on_allocated_operations( |
103 | - node, request.user, "create") |
104 | + node = Node.objects.get_node_or_404( |
105 | + system_id, request.user, NODE_PERMISSION.EDIT) |
106 | + # Installable nodes require the node needs to be in the correct state. |
107 | + if node.installable: |
108 | + raise_error_for_invalid_state_on_allocated_operations( |
109 | + node, request.user, "create") |
110 | form = PhysicalInterfaceForm(node=node, data=request.data) |
111 | if form.is_valid(): |
112 | return form.save() |
113 | @@ -232,9 +235,9 @@ |
114 | raise MAASAPIValidationError(form.errors) |
115 | |
116 | |
117 | -class NodeInterfaceHandler(OperationsHandler): |
118 | - """Manage a node's interface.""" |
119 | - api_doc_section_name = "Node Interface" |
120 | +class InterfaceHandler(OperationsHandler): |
121 | + """Manage a node's or device's interface.""" |
122 | + api_doc_section_name = "Interface" |
123 | create = None |
124 | model = Interface |
125 | fields = DISPLAYED_INTERFACE_FIELDS |
126 | @@ -249,7 +252,7 @@ |
127 | node = interface.get_node() |
128 | if node is not None: |
129 | system_id = node.system_id |
130 | - return ('node_interface_handler', (system_id, interface_id)) |
131 | + return ('interface_handler', (system_id, interface_id)) |
132 | |
133 | @classmethod |
134 | def mac_address(cls, interface): |
135 | @@ -367,9 +370,12 @@ |
136 | Returns 404 if the node or interface is not found. |
137 | """ |
138 | interface = Interface.objects.get_interface_or_404( |
139 | - system_id, interface_id, request.user, NODE_PERMISSION.ADMIN) |
140 | - raise_error_for_invalid_state_on_allocated_operations( |
141 | - interface.node, request.user, "update interface") |
142 | + system_id, interface_id, request.user, NODE_PERMISSION.EDIT) |
143 | + if interface.get_node().installable: |
144 | + # This node needs to be in the correct state to modify |
145 | + # the interface. |
146 | + raise_error_for_invalid_state_on_allocated_operations( |
147 | + interface.node, request.user, "update interface") |
148 | interface_form = InterfaceForm.get_interface_form(interface.type) |
149 | # For VLAN interface we cast parents to parent. As a VLAN can only |
150 | # have one parent. |
151 | @@ -394,9 +400,12 @@ |
152 | Returns 404 if the node or interface is not found. |
153 | """ |
154 | interface = Interface.objects.get_interface_or_404( |
155 | - system_id, interface_id, request.user, NODE_PERMISSION.ADMIN) |
156 | - raise_error_for_invalid_state_on_allocated_operations( |
157 | - interface.node, request.user, "delete interface") |
158 | + system_id, interface_id, request.user, NODE_PERMISSION.EDIT) |
159 | + if interface.get_node().installable: |
160 | + # This node needs to be in the correct state to modify |
161 | + # the interface. |
162 | + raise_error_for_invalid_state_on_allocated_operations( |
163 | + interface.node, request.user, "delete interface") |
164 | interface.delete() |
165 | return rc.DELETED |
166 | |
167 | @@ -432,10 +441,24 @@ |
168 | Returns 404 if the node or interface is not found. |
169 | """ |
170 | interface = Interface.objects.get_interface_or_404( |
171 | - system_id, interface_id, request.user, NODE_PERMISSION.ADMIN) |
172 | - raise_error_for_invalid_state_on_allocated_operations( |
173 | - interface.node, request.user, "link subnet") |
174 | - form = InterfaceLinkForm(instance=interface, data=request.data) |
175 | + system_id, interface_id, request.user, NODE_PERMISSION.EDIT) |
176 | + node = interface.get_node() |
177 | + if node.installable: |
178 | + # This node needs to be in the correct state to modify |
179 | + # the interface. |
180 | + raise_error_for_invalid_state_on_allocated_operations( |
181 | + interface.node, request.user, "link subnet") |
182 | + allowed_modes = [ |
183 | + INTERFACE_LINK_TYPE.AUTO, |
184 | + INTERFACE_LINK_TYPE.DHCP, |
185 | + INTERFACE_LINK_TYPE.STATIC, |
186 | + INTERFACE_LINK_TYPE.LINK_UP, |
187 | + ] |
188 | + else: |
189 | + # Devices can only be set in static IP mode. |
190 | + allowed_modes = [INTERFACE_LINK_TYPE.STATIC] |
191 | + form = InterfaceLinkForm( |
192 | + instance=interface, data=request.data, allowed_modes=allowed_modes) |
193 | if form.is_valid(): |
194 | return form.save() |
195 | else: |
196 | @@ -450,9 +473,12 @@ |
197 | Returns 404 if the node or interface is not found. |
198 | """ |
199 | interface = Interface.objects.get_interface_or_404( |
200 | - system_id, interface_id, request.user, NODE_PERMISSION.ADMIN) |
201 | - raise_error_for_invalid_state_on_allocated_operations( |
202 | - interface.node, request.user, "unlink subnet") |
203 | + system_id, interface_id, request.user, NODE_PERMISSION.EDIT) |
204 | + if interface.get_node().installable: |
205 | + # This node needs to be in the correct state to modify |
206 | + # the interface. |
207 | + raise_error_for_invalid_state_on_allocated_operations( |
208 | + interface.node, request.user, "unlink subnet") |
209 | form = InterfaceUnlinkForm(instance=interface, data=request.data) |
210 | if form.is_valid(): |
211 | return form.save() |
212 | @@ -474,9 +500,12 @@ |
213 | Returns 404 if the node or interface is not found. |
214 | """ |
215 | interface = Interface.objects.get_interface_or_404( |
216 | - system_id, interface_id, request.user, NODE_PERMISSION.ADMIN) |
217 | - raise_error_for_invalid_state_on_allocated_operations( |
218 | - interface.node, request.user, "set default gateway") |
219 | + system_id, interface_id, request.user, NODE_PERMISSION.EDIT) |
220 | + if interface.get_node().installable: |
221 | + # This node needs to be in the correct state to modify |
222 | + # the interface. |
223 | + raise_error_for_invalid_state_on_allocated_operations( |
224 | + interface.node, request.user, "set default gateway") |
225 | form = InterfaceSetDefaultGatwayForm( |
226 | instance=interface, data=request.data) |
227 | if form.is_valid(): |
228 | @@ -485,7 +514,17 @@ |
229 | raise MAASAPIValidationError(form.errors) |
230 | |
231 | |
232 | -class PhysicalInterfaceHandler(NodeInterfaceHandler): |
233 | +class NodeInterfacesHandler(InterfacesHandler): |
234 | + """Manage interfaces on a node. (Deprecated)""" |
235 | + api_doc_section_name = "Node Interfaces" |
236 | + |
237 | + |
238 | +class NodeInterfaceHandler(InterfaceHandler): |
239 | + """Manage a node's interface. (Deprecated)""" |
240 | + api_doc_section_name = "Node Interface" |
241 | + |
242 | + |
243 | +class PhysicaInterfaceHandler(InterfaceHandler): |
244 | """ |
245 | This handler only exists because piston requires a unique handler per |
246 | class type. Without this class the resource_uri will not be added to any |
247 | @@ -499,7 +538,7 @@ |
248 | model = PhysicalInterface |
249 | |
250 | |
251 | -class BondInterfaceHandler(NodeInterfaceHandler): |
252 | +class BondInterfaceHandler(InterfaceHandler): |
253 | """ |
254 | This handler only exists because piston requires a unique handler per |
255 | class type. Without this class the resource_uri will not be added to any |
256 | @@ -513,7 +552,7 @@ |
257 | model = BondInterface |
258 | |
259 | |
260 | -class VLANInterfaceHandler(NodeInterfaceHandler): |
261 | +class VLANInterfaceHandler(InterfaceHandler): |
262 | """ |
263 | This handler only exists because piston requires a unique handler per |
264 | class type. Without this class the resource_uri will not be added to any |
265 | |
266 | === modified file 'src/maasserver/api/tests/test_interfaces.py' |
267 | --- src/maasserver/api/tests/test_interfaces.py 2015-12-04 08:25:27 +0000 |
268 | +++ src/maasserver/api/tests/test_interfaces.py 2015-12-07 16:38:37 +0000 |
269 | @@ -28,20 +28,20 @@ |
270 | ) |
271 | |
272 | |
273 | -def get_node_interfaces_uri(node): |
274 | - """Return a Node's interfaces URI on the API.""" |
275 | +def get_interfaces_uri(node): |
276 | + """Return a interfaces URI on the API.""" |
277 | return reverse( |
278 | - 'node_interfaces_handler', args=[node.system_id]) |
279 | - |
280 | - |
281 | -def get_node_interface_uri(interface, node=None): |
282 | - """Return a Node's interface URI on the API.""" |
283 | + 'interfaces_handler', args=[node.system_id]) |
284 | + |
285 | + |
286 | +def get_interface_uri(interface, node=None): |
287 | + """Return a interface URI on the API.""" |
288 | if isinstance(interface, Interface): |
289 | if node is None: |
290 | node = interface.get_node() |
291 | interface = interface.id |
292 | return reverse( |
293 | - 'node_interface_handler', args=[node.system_id, interface]) |
294 | + 'interface_handler', args=[node.system_id, interface]) |
295 | |
296 | |
297 | def make_complex_interface(node, name=None): |
298 | @@ -65,18 +65,18 @@ |
299 | return bond_interface, parents, [vlan_nic_10, vlan_nic_11] |
300 | |
301 | |
302 | -class TestNodeInterfacesAPI(APITestCase): |
303 | +class TestInterfacesAPI(APITestCase): |
304 | |
305 | def test_handler_path(self): |
306 | node = factory.make_Node() |
307 | self.assertEqual( |
308 | '/api/1.0/nodes/%s/interfaces/' % (node.system_id), |
309 | - get_node_interfaces_uri(node)) |
310 | + get_interfaces_uri(node)) |
311 | |
312 | def test_read(self): |
313 | node = factory.make_Node() |
314 | bond, parents, children = make_complex_interface(node) |
315 | - uri = get_node_interfaces_uri(node) |
316 | + uri = get_interfaces_uri(node) |
317 | response = self.client.get(uri) |
318 | |
319 | self.assertEqual( |
320 | @@ -91,6 +91,20 @@ |
321 | ] |
322 | self.assertItemsEqual(expected_ids, result_ids) |
323 | |
324 | + def test_read_on_device(self): |
325 | + parent = factory.make_Node() |
326 | + device = factory.make_Node( |
327 | + owner=self.logged_in_user, installable=False, parent=parent) |
328 | + interface = factory.make_Interface( |
329 | + INTERFACE_TYPE.PHYSICAL, node=device) |
330 | + uri = get_interfaces_uri(device) |
331 | + response = self.client.get(uri) |
332 | + |
333 | + self.assertEqual( |
334 | + http.client.OK, response.status_code, response.content) |
335 | + self.assertEqual( |
336 | + interface.id, json_load_bytes(response.content)[0]['id']) |
337 | + |
338 | def test_create_physical(self): |
339 | self.become_admin() |
340 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
341 | @@ -102,7 +116,7 @@ |
342 | factory.make_name("tag") |
343 | for _ in range(3) |
344 | ] |
345 | - uri = get_node_interfaces_uri(node) |
346 | + uri = get_interfaces_uri(node) |
347 | response = self.client.post(uri, { |
348 | "op": "create_physical", |
349 | "mac_address": mac, |
350 | @@ -124,6 +138,39 @@ |
351 | "enabled": Equals(True), |
352 | })) |
353 | |
354 | + def test_create_physical_on_device(self): |
355 | + parent = factory.make_Node() |
356 | + device = factory.make_Node( |
357 | + owner=self.logged_in_user, installable=False, parent=parent) |
358 | + mac = factory.make_mac_address() |
359 | + name = factory.make_name("eth") |
360 | + vlan = factory.make_VLAN() |
361 | + tags = [ |
362 | + factory.make_name("tag") |
363 | + for _ in range(3) |
364 | + ] |
365 | + uri = get_interfaces_uri(device) |
366 | + response = self.client.post(uri, { |
367 | + "op": "create_physical", |
368 | + "mac_address": mac, |
369 | + "name": name, |
370 | + "vlan": vlan.id, |
371 | + "tags": ",".join(tags), |
372 | + }) |
373 | + |
374 | + self.assertEqual( |
375 | + http.client.OK, response.status_code, response.content) |
376 | + self.assertThat(json_load_bytes(response.content), ContainsDict({ |
377 | + "mac_address": Equals(mac), |
378 | + "name": Equals(name), |
379 | + "vlan": ContainsDict({ |
380 | + "id": Equals(vlan.id), |
381 | + }), |
382 | + "type": Equals("physical"), |
383 | + "tags": Equals(tags), |
384 | + "enabled": Equals(True), |
385 | + })) |
386 | + |
387 | def test_create_physical_disabled(self): |
388 | self.become_admin() |
389 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
390 | @@ -135,7 +182,7 @@ |
391 | factory.make_name("tag") |
392 | for _ in range(3) |
393 | ] |
394 | - uri = get_node_interfaces_uri(node) |
395 | + uri = get_interfaces_uri(node) |
396 | response = self.client.post(uri, { |
397 | "op": "create_physical", |
398 | "mac_address": mac, |
399 | @@ -163,7 +210,7 @@ |
400 | mac = factory.make_mac_address() |
401 | name = factory.make_name("eth") |
402 | vlan = factory.make_VLAN() |
403 | - uri = get_node_interfaces_uri(node) |
404 | + uri = get_interfaces_uri(node) |
405 | response = self.client.post(uri, { |
406 | "op": "create_physical", |
407 | "mac_address": mac, |
408 | @@ -195,7 +242,7 @@ |
409 | mac = factory.make_mac_address() |
410 | name = factory.make_name("eth") |
411 | vlan = factory.make_VLAN() |
412 | - uri = get_node_interfaces_uri(node) |
413 | + uri = get_interfaces_uri(node) |
414 | response = self.client.post(uri, { |
415 | "op": "create_physical", |
416 | "mac_address": mac, |
417 | @@ -208,7 +255,7 @@ |
418 | def test_create_physical_requires_mac_name_and_vlan(self): |
419 | self.become_admin() |
420 | node = factory.make_Node(status=NODE_STATUS.READY) |
421 | - uri = get_node_interfaces_uri(node) |
422 | + uri = get_interfaces_uri(node) |
423 | response = self.client.post(uri, { |
424 | "op": "create_physical", |
425 | }) |
426 | @@ -227,7 +274,7 @@ |
427 | INTERFACE_TYPE.PHYSICAL) |
428 | name = factory.make_name("eth") |
429 | vlan = factory.make_VLAN() |
430 | - uri = get_node_interfaces_uri(node) |
431 | + uri = get_interfaces_uri(node) |
432 | response = self.client.post(uri, { |
433 | "op": "create_physical", |
434 | "mac_address": "%s" % interface_on_other_node.mac_address, |
435 | @@ -256,7 +303,7 @@ |
436 | factory.make_name("tag") |
437 | for _ in range(3) |
438 | ] |
439 | - uri = get_node_interfaces_uri(node) |
440 | + uri = get_interfaces_uri(node) |
441 | response = self.client.post(uri, { |
442 | "op": "create_bond", |
443 | "mac_address": "%s" % parent_1_iface.mac_address, |
444 | @@ -283,6 +330,17 @@ |
445 | parent_2_iface.name, |
446 | ], parsed_interface['parents']) |
447 | |
448 | + def test_create_bond_404_on_device(self): |
449 | + parent = factory.make_Node() |
450 | + device = factory.make_Node( |
451 | + owner=self.logged_in_user, installable=False, parent=parent) |
452 | + uri = get_interfaces_uri(device) |
453 | + response = self.client.post(uri, { |
454 | + "op": "create_bond", |
455 | + }) |
456 | + self.assertEqual( |
457 | + http.client.NOT_FOUND, response.status_code, response.content) |
458 | + |
459 | def test_create_bond_requires_admin(self): |
460 | node = factory.make_Node() |
461 | vlan = factory.make_VLAN() |
462 | @@ -291,7 +349,7 @@ |
463 | parent_2_iface = factory.make_Interface( |
464 | INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node) |
465 | name = factory.make_name("bond") |
466 | - uri = get_node_interfaces_uri(node) |
467 | + uri = get_interfaces_uri(node) |
468 | response = self.client.post(uri, { |
469 | "op": "create_bond", |
470 | "mac": "%s" % parent_1_iface.mac_address, |
471 | @@ -327,7 +385,7 @@ |
472 | parent_2_iface = factory.make_Interface( |
473 | INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node) |
474 | name = factory.make_name("bond") |
475 | - uri = get_node_interfaces_uri(node) |
476 | + uri = get_interfaces_uri(node) |
477 | response = self.client.post(uri, { |
478 | "op": "create_bond", |
479 | "mac": "%s" % parent_1_iface.mac_address, |
480 | @@ -341,7 +399,7 @@ |
481 | def test_create_bond_requires_name_vlan_and_parents(self): |
482 | self.become_admin() |
483 | node = factory.make_Node(status=NODE_STATUS.READY) |
484 | - uri = get_node_interfaces_uri(node) |
485 | + uri = get_interfaces_uri(node) |
486 | response = self.client.post(uri, { |
487 | "op": "create_bond", |
488 | }) |
489 | @@ -366,7 +424,7 @@ |
490 | factory.make_name("tag") |
491 | for _ in range(3) |
492 | ] |
493 | - uri = get_node_interfaces_uri(node) |
494 | + uri = get_interfaces_uri(node) |
495 | response = self.client.post(uri, { |
496 | "op": "create_vlan", |
497 | "vlan": tagged_vlan.id, |
498 | @@ -387,13 +445,24 @@ |
499 | "tags": Equals(tags), |
500 | })) |
501 | |
502 | + def test_create_vlan_404_on_device(self): |
503 | + parent = factory.make_Node() |
504 | + device = factory.make_Node( |
505 | + owner=self.logged_in_user, installable=False, parent=parent) |
506 | + uri = get_interfaces_uri(device) |
507 | + response = self.client.post(uri, { |
508 | + "op": "create_vlan", |
509 | + }) |
510 | + self.assertEqual( |
511 | + http.client.NOT_FOUND, response.status_code, response.content) |
512 | + |
513 | def test_create_vlan_requires_admin(self): |
514 | node = factory.make_Node() |
515 | untagged_vlan = factory.make_VLAN() |
516 | parent_iface = factory.make_Interface( |
517 | INTERFACE_TYPE.PHYSICAL, vlan=untagged_vlan, node=node) |
518 | tagged_vlan = factory.make_VLAN() |
519 | - uri = get_node_interfaces_uri(node) |
520 | + uri = get_interfaces_uri(node) |
521 | response = self.client.post(uri, { |
522 | "op": "create_vlan", |
523 | "vlan": tagged_vlan.vid, |
524 | @@ -405,7 +474,7 @@ |
525 | def test_create_vlan_requires_vlan_and_parent(self): |
526 | self.become_admin() |
527 | node = factory.make_Node() |
528 | - uri = get_node_interfaces_uri(node) |
529 | + uri = get_interfaces_uri(node) |
530 | response = self.client.post(uri, { |
531 | "op": "create_vlan", |
532 | }) |
533 | @@ -426,7 +495,7 @@ |
534 | self.assertEqual( |
535 | '/api/1.0/nodes/%s/interfaces/%s/' % ( |
536 | node.system_id, interface.id), |
537 | - get_node_interface_uri(interface, node=node)) |
538 | + get_interface_uri(interface, node=node)) |
539 | |
540 | def test_read(self): |
541 | node = factory.make_Node() |
542 | @@ -493,7 +562,7 @@ |
543 | } |
544 | bond.save() |
545 | |
546 | - uri = get_node_interface_uri(bond) |
547 | + uri = get_interface_uri(bond) |
548 | response = self.client.get(uri) |
549 | self.assertEqual( |
550 | http.client.OK, response.status_code, response.content) |
551 | @@ -507,7 +576,7 @@ |
552 | }), |
553 | "mac_address": Equals("%s" % bond.mac_address), |
554 | "tags": Equals(bond.tags), |
555 | - "resource_uri": Equals(get_node_interface_uri(bond)), |
556 | + "resource_uri": Equals(get_interface_uri(bond)), |
557 | "params": Equals(bond.params), |
558 | "effective_mtu": Equals(bond.get_effective_mtu()), |
559 | })) |
560 | @@ -527,7 +596,7 @@ |
561 | def test_read_by_specifier(self): |
562 | node = factory.make_Node(hostname="tasty-biscuits") |
563 | bond0, _, _ = make_complex_interface(node, name="bond0") |
564 | - uri = get_node_interface_uri( |
565 | + uri = get_interface_uri( |
566 | "hostname:tasty-biscuits,name:bond0", node=node) |
567 | response = self.client.get(uri) |
568 | self.assertEqual( |
569 | @@ -535,10 +604,22 @@ |
570 | parsed_interface = json_load_bytes(response.content) |
571 | self.assertEqual(bond0.id, parsed_interface['id']) |
572 | |
573 | + def test_read_device_interface(self): |
574 | + parent = factory.make_Node() |
575 | + device = factory.make_Node(installable=False, parent=parent) |
576 | + interface = factory.make_Interface( |
577 | + INTERFACE_TYPE.PHYSICAL, node=device) |
578 | + uri = get_interface_uri(interface) |
579 | + response = self.client.get(uri) |
580 | + self.assertEqual( |
581 | + http.client.OK, response.status_code, response.content) |
582 | + parsed_interface = json_load_bytes(response.content) |
583 | + self.assertEqual(interface.id, parsed_interface['id']) |
584 | + |
585 | def test_read_404_when_invalid_id(self): |
586 | node = factory.make_Node() |
587 | uri = reverse( |
588 | - 'node_interface_handler', |
589 | + 'interface_handler', |
590 | args=[node.system_id, random.randint(100, 1000)]) |
591 | response = self.client.get(uri) |
592 | self.assertEqual( |
593 | @@ -552,7 +633,7 @@ |
594 | INTERFACE_TYPE.PHYSICAL, node=node) |
595 | new_name = factory.make_name("name") |
596 | new_vlan = factory.make_VLAN() |
597 | - uri = get_node_interface_uri(interface) |
598 | + uri = get_interface_uri(interface) |
599 | response = self.client.put(uri, { |
600 | "name": new_name, |
601 | "vlan": new_vlan.id, |
602 | @@ -563,13 +644,32 @@ |
603 | self.assertEqual(new_name, parsed_interface["name"]) |
604 | self.assertEqual(new_vlan.vid, parsed_interface["vlan"]["vid"]) |
605 | |
606 | + def test_update_device_physical_interface(self): |
607 | + node = factory.make_Node() |
608 | + device = factory.make_Node( |
609 | + owner=self.logged_in_user, installable=False, parent=node) |
610 | + interface = factory.make_Interface( |
611 | + INTERFACE_TYPE.PHYSICAL, node=device) |
612 | + new_name = factory.make_name("name") |
613 | + new_vlan = factory.make_VLAN() |
614 | + uri = get_interface_uri(interface) |
615 | + response = self.client.put(uri, { |
616 | + "name": new_name, |
617 | + "vlan": new_vlan.id, |
618 | + }) |
619 | + self.assertEqual( |
620 | + http.client.OK, response.status_code, response.content) |
621 | + parsed_interface = json_load_bytes(response.content) |
622 | + self.assertEquals(new_name, parsed_interface["name"]) |
623 | + self.assertEquals(new_vlan.vid, parsed_interface["vlan"]["vid"]) |
624 | + |
625 | def test_update_bond_interface(self): |
626 | self.become_admin() |
627 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
628 | node = factory.make_Node(status=status) |
629 | bond, [nic_0, nic_1], [vlan_10, vlan_11] = make_complex_interface( |
630 | node) |
631 | - uri = get_node_interface_uri(bond) |
632 | + uri = get_interface_uri(bond) |
633 | response = self.client.put(uri, { |
634 | "parents": [nic_0.id], |
635 | }) |
636 | @@ -586,7 +686,7 @@ |
637 | node) |
638 | physical_interface = factory.make_Interface( |
639 | INTERFACE_TYPE.PHYSICAL, node=node) |
640 | - uri = get_node_interface_uri(vlan_10) |
641 | + uri = get_interface_uri(vlan_10) |
642 | response = self.client.put(uri, { |
643 | "parent": physical_interface.id, |
644 | }) |
645 | @@ -600,7 +700,7 @@ |
646 | node = factory.make_Node() |
647 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node) |
648 | new_name = factory.make_name("name") |
649 | - uri = get_node_interface_uri(interface) |
650 | + uri = get_interface_uri(interface) |
651 | response = self.client.put(uri, { |
652 | "name": new_name, |
653 | }) |
654 | @@ -629,7 +729,7 @@ |
655 | interface = factory.make_Interface( |
656 | INTERFACE_TYPE.PHYSICAL, node=node) |
657 | new_name = factory.make_name("name") |
658 | - uri = get_node_interface_uri(interface) |
659 | + uri = get_interface_uri(interface) |
660 | response = self.client.put(uri, { |
661 | "name": new_name, |
662 | }) |
663 | @@ -641,25 +741,38 @@ |
664 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
665 | node = factory.make_Node(interface=True, status=status) |
666 | interface = node.get_boot_interface() |
667 | - uri = get_node_interface_uri(interface) |
668 | + uri = get_interface_uri(interface) |
669 | response = self.client.delete(uri) |
670 | self.assertEqual( |
671 | http.client.NO_CONTENT, response.status_code, response.content) |
672 | self.assertIsNone(reload_object(interface)) |
673 | |
674 | + def test_delete_deletes_device_interface(self): |
675 | + parent = factory.make_Node() |
676 | + device = factory.make_Node( |
677 | + owner=self.logged_in_user, installable=False, parent=parent) |
678 | + interface = factory.make_Interface( |
679 | + INTERFACE_TYPE.PHYSICAL, node=device) |
680 | + uri = get_interface_uri(interface) |
681 | + response = self.client.delete(uri) |
682 | + self.assertEqual( |
683 | + http.client.NO_CONTENT, response.status_code, response.content) |
684 | + self.assertIsNone(reload_object(interface)) |
685 | + |
686 | def test_delete_403_when_not_admin(self): |
687 | node = factory.make_Node(interface=True) |
688 | interface = node.get_boot_interface() |
689 | - uri = get_node_interface_uri(interface) |
690 | + uri = get_interface_uri(interface) |
691 | response = self.client.delete(uri) |
692 | self.assertEqual( |
693 | http.client.FORBIDDEN, response.status_code, response.content) |
694 | self.assertIsNotNone(reload_object(interface)) |
695 | |
696 | def test_delete_404_when_invalid_id(self): |
697 | + self.become_admin() |
698 | node = factory.make_Node() |
699 | uri = reverse( |
700 | - 'node_interface_handler', |
701 | + 'interface_handler', |
702 | args=[node.system_id, random.randint(100, 1000)]) |
703 | response = self.client.delete(uri) |
704 | self.assertEqual( |
705 | @@ -685,7 +798,7 @@ |
706 | ): |
707 | node = factory.make_Node(interface=True, status=status) |
708 | interface = node.get_boot_interface() |
709 | - uri = get_node_interface_uri(interface) |
710 | + uri = get_interface_uri(interface) |
711 | response = self.client.delete(uri) |
712 | self.assertEqual( |
713 | http.client.CONFLICT, response.status_code, response.content) |
714 | @@ -698,7 +811,7 @@ |
715 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
716 | node = factory.make_Node(interface=True, status=status) |
717 | interface = node.get_boot_interface() |
718 | - uri = get_node_interface_uri(interface) |
719 | + uri = get_interface_uri(interface) |
720 | response = self.client.post(uri, { |
721 | "op": "link_subnet", |
722 | "mode": INTERFACE_LINK_TYPE.DHCP, |
723 | @@ -711,12 +824,52 @@ |
724 | "mode": Equals(INTERFACE_LINK_TYPE.DHCP), |
725 | })) |
726 | |
727 | + def test_link_subnet_creates_link_on_device(self): |
728 | + parent = factory.make_Node() |
729 | + device = factory.make_Node( |
730 | + owner=self.logged_in_user, installable=False, parent=parent) |
731 | + interface = factory.make_Interface( |
732 | + INTERFACE_TYPE.PHYSICAL, node=device) |
733 | + subnet = factory.make_Subnet(vlan=interface.vlan) |
734 | + uri = get_interface_uri(interface) |
735 | + response = self.client.post(uri, { |
736 | + "op": "link_subnet", |
737 | + "mode": INTERFACE_LINK_TYPE.STATIC, |
738 | + "subnet": subnet.id, |
739 | + }) |
740 | + self.assertEqual( |
741 | + http.client.OK, response.status_code, response.content) |
742 | + parsed_response = json_load_bytes(response.content) |
743 | + self.assertThat( |
744 | + parsed_response["links"][0], ContainsDict({ |
745 | + "mode": Equals(INTERFACE_LINK_TYPE.STATIC), |
746 | + })) |
747 | + |
748 | + def test_link_subnet_on_device_only_allows_static(self): |
749 | + parent = factory.make_Node() |
750 | + device = factory.make_Node( |
751 | + owner=self.logged_in_user, installable=False, parent=parent) |
752 | + interface = factory.make_Interface( |
753 | + INTERFACE_TYPE.PHYSICAL, node=device) |
754 | + for link_type in [ |
755 | + INTERFACE_LINK_TYPE.AUTO, |
756 | + INTERFACE_LINK_TYPE.DHCP, |
757 | + INTERFACE_LINK_TYPE.LINK_UP]: |
758 | + uri = get_interface_uri(interface) |
759 | + response = self.client.post(uri, { |
760 | + "op": "link_subnet", |
761 | + "mode": link_type, |
762 | + }) |
763 | + self.assertEqual( |
764 | + http.client.BAD_REQUEST, response.status_code, |
765 | + response.content) |
766 | + |
767 | def test_link_subnet_raises_error(self): |
768 | self.become_admin() |
769 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
770 | node = factory.make_Node(interface=True, status=status) |
771 | interface = node.get_boot_interface() |
772 | - uri = get_node_interface_uri(interface) |
773 | + uri = get_interface_uri(interface) |
774 | response = self.client.post(uri, { |
775 | "op": "link_subnet", |
776 | }) |
777 | @@ -730,7 +883,7 @@ |
778 | def test_link_subnet_requries_admin(self): |
779 | node = factory.make_Node(interface=True) |
780 | interface = node.get_boot_interface() |
781 | - uri = get_node_interface_uri(interface) |
782 | + uri = get_interface_uri(interface) |
783 | response = self.client.post(uri, { |
784 | "op": "link_subnet", |
785 | }) |
786 | @@ -757,7 +910,7 @@ |
787 | ): |
788 | node = factory.make_Node(interface=True, status=status) |
789 | interface = node.get_boot_interface() |
790 | - uri = get_node_interface_uri(interface) |
791 | + uri = get_interface_uri(interface) |
792 | response = self.client.post(uri, { |
793 | "op": "link_subnet", |
794 | "mode": INTERFACE_LINK_TYPE.DHCP, |
795 | @@ -777,7 +930,7 @@ |
796 | dhcp_ip = factory.make_StaticIPAddress( |
797 | alloc_type=IPADDRESS_TYPE.DHCP, ip="", |
798 | subnet=subnet, interface=interface) |
799 | - uri = get_node_interface_uri(interface) |
800 | + uri = get_interface_uri(interface) |
801 | response = self.client.post(uri, { |
802 | "op": "unlink_subnet", |
803 | "id": dhcp_ip.id, |
804 | @@ -786,12 +939,31 @@ |
805 | http.client.OK, response.status_code, response.content) |
806 | self.assertIsNone(reload_object(dhcp_ip)) |
807 | |
808 | + def test_unlink_subnet_deletes_link_on_device(self): |
809 | + parent = factory.make_Node() |
810 | + device = factory.make_Node( |
811 | + owner=self.logged_in_user, installable=False, parent=parent) |
812 | + interface = factory.make_Interface( |
813 | + INTERFACE_TYPE.PHYSICAL, node=device) |
814 | + subnet = factory.make_Subnet() |
815 | + static_ip = factory.make_StaticIPAddress( |
816 | + alloc_type=IPADDRESS_TYPE.STICKY, |
817 | + subnet=subnet, interface=interface) |
818 | + uri = get_interface_uri(interface) |
819 | + response = self.client.post(uri, { |
820 | + "op": "unlink_subnet", |
821 | + "id": static_ip.id, |
822 | + }) |
823 | + self.assertEqual( |
824 | + http.client.OK, response.status_code, response.content) |
825 | + self.assertIsNone(reload_object(static_ip)) |
826 | + |
827 | def test_unlink_subnet_raises_error(self): |
828 | self.become_admin() |
829 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
830 | node = factory.make_Node(interface=True, status=status) |
831 | interface = node.get_boot_interface() |
832 | - uri = get_node_interface_uri(interface) |
833 | + uri = get_interface_uri(interface) |
834 | response = self.client.post(uri, { |
835 | "op": "unlink_subnet", |
836 | }) |
837 | @@ -805,7 +977,7 @@ |
838 | def test_unlink_subnet_requries_admin(self): |
839 | node = factory.make_Node(interface=True) |
840 | interface = node.get_boot_interface() |
841 | - uri = get_node_interface_uri(interface) |
842 | + uri = get_interface_uri(interface) |
843 | response = self.client.post(uri, { |
844 | "op": "unlink_subnet", |
845 | }) |
846 | @@ -832,7 +1004,7 @@ |
847 | ): |
848 | node = factory.make_Node(interface=True, status=status) |
849 | interface = node.get_boot_interface() |
850 | - uri = get_node_interface_uri(interface) |
851 | + uri = get_interface_uri(interface) |
852 | response = self.client.post(uri, { |
853 | "op": "unlink_subnet", |
854 | }) |
855 | @@ -852,7 +1024,7 @@ |
856 | link_ip = factory.make_StaticIPAddress( |
857 | alloc_type=IPADDRESS_TYPE.AUTO, ip="", |
858 | subnet=subnet, interface=interface) |
859 | - uri = get_node_interface_uri(interface) |
860 | + uri = get_interface_uri(interface) |
861 | response = self.client.post(uri, { |
862 | "op": "set_default_gateway", |
863 | "link_id": link_ip.id |
864 | @@ -874,7 +1046,7 @@ |
865 | link_ip = factory.make_StaticIPAddress( |
866 | alloc_type=IPADDRESS_TYPE.AUTO, ip="", |
867 | subnet=subnet, interface=interface) |
868 | - uri = get_node_interface_uri(interface) |
869 | + uri = get_interface_uri(interface) |
870 | response = self.client.post(uri, { |
871 | "op": "set_default_gateway", |
872 | "link_id": link_ip.id |
873 | @@ -888,7 +1060,7 @@ |
874 | for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN): |
875 | node = factory.make_Node(interface=True, status=status) |
876 | interface = node.get_boot_interface() |
877 | - uri = get_node_interface_uri(interface) |
878 | + uri = get_interface_uri(interface) |
879 | response = self.client.post(uri, { |
880 | "op": "set_default_gateway", |
881 | }) |
882 | @@ -902,7 +1074,7 @@ |
883 | def test_set_default_gateway_requries_admin(self): |
884 | node = factory.make_Node(interface=True) |
885 | interface = node.get_boot_interface() |
886 | - uri = get_node_interface_uri(interface) |
887 | + uri = get_interface_uri(interface) |
888 | response = self.client.post(uri, { |
889 | "op": "set_default_gateway", |
890 | }) |
891 | @@ -929,7 +1101,7 @@ |
892 | ): |
893 | node = factory.make_Node(interface=True, status=status) |
894 | interface = node.get_boot_interface() |
895 | - uri = get_node_interface_uri(interface) |
896 | + uri = get_interface_uri(interface) |
897 | response = self.client.post(uri, { |
898 | "op": "set_default_gateway", |
899 | }) |
900 | |
901 | === modified file 'src/maasserver/forms_interface_link.py' |
902 | --- src/maasserver/forms_interface_link.py 2015-12-01 18:12:59 +0000 |
903 | +++ src/maasserver/forms_interface_link.py 2015-12-07 16:38:37 +0000 |
904 | @@ -43,13 +43,6 @@ |
905 | class InterfaceLinkForm(forms.Form): |
906 | """Interface link form.""" |
907 | |
908 | - mode = CaseInsensitiveChoiceField( |
909 | - choices=INTERFACE_LINK_TYPE_CHOICES, required=True, |
910 | - error_messages={ |
911 | - 'invalid_choice': compose_invalid_choice_text( |
912 | - 'mode', INTERFACE_LINK_TYPE_CHOICES), |
913 | - }) |
914 | - |
915 | subnet = SpecifierOrModelChoiceField(queryset=None, required=False) |
916 | |
917 | ip_address = forms.GenericIPAddressField(required=False) |
918 | @@ -57,8 +50,29 @@ |
919 | default_gateway = forms.BooleanField(initial=False, required=False) |
920 | |
921 | def __init__(self, *args, **kwargs): |
922 | + # Get list of allowed modes for this interface. |
923 | + allowed_modes = kwargs.pop("allowed_modes", [ |
924 | + INTERFACE_LINK_TYPE.AUTO, |
925 | + INTERFACE_LINK_TYPE.DHCP, |
926 | + INTERFACE_LINK_TYPE.STATIC, |
927 | + INTERFACE_LINK_TYPE.LINK_UP, |
928 | + ]) |
929 | + mode_choices = [ |
930 | + (key, value) |
931 | + for key, value in INTERFACE_LINK_TYPE_CHOICES |
932 | + if key in allowed_modes |
933 | + ] |
934 | + |
935 | self.instance = kwargs.pop("instance") |
936 | super(InterfaceLinkForm, self).__init__(*args, **kwargs) |
937 | + |
938 | + # Create the mode field and setup the queryset on the subnet. |
939 | + self.fields['mode'] = CaseInsensitiveChoiceField( |
940 | + choices=mode_choices, required=True, |
941 | + error_messages={ |
942 | + 'invalid_choice': compose_invalid_choice_text( |
943 | + 'mode', mode_choices), |
944 | + }) |
945 | self.fields['subnet'].queryset = self.instance.vlan.subnet_set.all() |
946 | |
947 | def clean(self): |
948 | |
949 | === modified file 'src/maasserver/models/__init__.py' |
950 | --- src/maasserver/models/__init__.py 2015-12-02 20:43:30 +0000 |
951 | +++ src/maasserver/models/__init__.py 2015-12-07 16:38:37 +0000 |
952 | @@ -252,7 +252,26 @@ |
953 | raise NotImplementedError( |
954 | 'Invalid permission check (invalid permission name: %s).' % |
955 | perm) |
956 | - elif isinstance(obj, (Fabric, FanNetwork, Interface, Subnet, Space)): |
957 | + elif isinstance(obj, Interface): |
958 | + if perm == NODE_PERMISSION.VIEW: |
959 | + # Any registered user can view a interface regardless |
960 | + # of its state. |
961 | + return True |
962 | + elif perm in NODE_PERMISSION.EDIT: |
963 | + # A device can be editted by its owner a node must be admin. |
964 | + node = obj.get_node() |
965 | + if node is None or node.installable: |
966 | + return user.is_superuser |
967 | + else: |
968 | + return node.owner == user |
969 | + elif perm in NODE_PERMISSION.ADMIN: |
970 | + # Admin permission is solely granted to superusers. |
971 | + return user.is_superuser |
972 | + else: |
973 | + raise NotImplementedError( |
974 | + 'Invalid permission check (invalid permission name: %s).' % |
975 | + perm) |
976 | + elif isinstance(obj, (Fabric, FanNetwork, Subnet, Space)): |
977 | if perm == NODE_PERMISSION.VIEW: |
978 | # Any registered user can view a fabric or interface regardless |
979 | # of its state. |
980 | |
981 | === modified file 'src/maasserver/tests/test_auth.py' |
982 | --- src/maasserver/tests/test_auth.py 2015-12-01 18:12:59 +0000 |
983 | +++ src/maasserver/tests/test_auth.py 2015-12-07 16:38:37 +0000 |
984 | @@ -223,6 +223,34 @@ |
985 | user, NODE_PERMISSION.ADMIN, factory.make_FilesystemGroup())) |
986 | |
987 | |
988 | +class TestMAASAuthorizationBackendForDeviceInterface(MAASServerTestCase): |
989 | + |
990 | + def test_owner_can_edit_device_interface(self): |
991 | + backend = MAASAuthorizationBackend() |
992 | + user = factory.make_User() |
993 | + parent = factory.make_Node() |
994 | + device = factory.make_Node( |
995 | + owner=user, installable=False, parent=parent) |
996 | + interface = factory.make_Interface( |
997 | + INTERFACE_TYPE.PHYSICAL, node=device) |
998 | + self.assertTrue( |
999 | + backend.has_perm( |
1000 | + user, NODE_PERMISSION.EDIT, interface)) |
1001 | + |
1002 | + def test_non_owner_cannot_edit_device_interface(self): |
1003 | + backend = MAASAuthorizationBackend() |
1004 | + user = factory.make_User() |
1005 | + owner = factory.make_User() |
1006 | + parent = factory.make_Node() |
1007 | + device = factory.make_Node( |
1008 | + owner=owner, installable=False, parent=parent) |
1009 | + interface = factory.make_Interface( |
1010 | + INTERFACE_TYPE.PHYSICAL, node=device) |
1011 | + self.assertFalse( |
1012 | + backend.has_perm( |
1013 | + user, NODE_PERMISSION.EDIT, interface)) |
1014 | + |
1015 | + |
1016 | class TestMAASAuthorizationBackendForNetworking(MAASServerTestCase): |
1017 | |
1018 | scenarios = ( |
1019 | |
1020 | === modified file 'src/maasserver/urls_api.py' |
1021 | --- src/maasserver/urls_api.py 2015-12-01 18:12:59 +0000 |
1022 | +++ src/maasserver/urls_api.py 2015-12-07 16:38:37 +0000 |
1023 | @@ -67,6 +67,8 @@ |
1024 | FilesHandler, |
1025 | ) |
1026 | from maasserver.api.interfaces import ( |
1027 | + InterfaceHandler, |
1028 | + InterfacesHandler, |
1029 | NodeInterfaceHandler, |
1030 | NodeInterfacesHandler, |
1031 | ) |
1032 | @@ -188,6 +190,10 @@ |
1033 | BcacheCacheSetHandler, authentication=api_auth) |
1034 | bcache_cache_sets_handler = RestrictedResource( |
1035 | BcacheCacheSetsHandler, authentication=api_auth) |
1036 | +interface_handler = RestrictedResource( |
1037 | + InterfaceHandler, authentication=api_auth) |
1038 | +interfaces_handler = RestrictedResource( |
1039 | + InterfacesHandler, authentication=api_auth) |
1040 | node_interface_handler = RestrictedResource( |
1041 | NodeInterfaceHandler, authentication=api_auth) |
1042 | node_interfaces_handler = RestrictedResource( |
1043 | @@ -299,8 +305,14 @@ |
1044 | '(?P<cache_set_id>[^/]+)/$', |
1045 | bcache_cache_set_handler, name='bcache_cache_set_handler'), |
1046 | url(r'^nodes/(?P<system_id>[^/]+)/interfaces/(?P<interface_id>[^/]+)/$', |
1047 | + interface_handler, name='interface_handler'), |
1048 | + url( |
1049 | + r'^nodes/(?P<system_id>[^/]+)/node-interfaces/' |
1050 | + '(?P<interface_id>[^/]+)/$', |
1051 | node_interface_handler, name='node_interface_handler'), |
1052 | url(r'^nodes/(?P<system_id>[^/]+)/interfaces/$', |
1053 | + interfaces_handler, name='interfaces_handler'), |
1054 | + url(r'^nodes/(?P<system_id>[^/]+)/node-interfaces/$', |
1055 | node_interfaces_handler, name='node_interfaces_handler'), |
1056 | url( |
1057 | r'^nodes/(?P<system_id>[^/]+)/$', node_handler, |
Forward port with some python3 fixes: https:/ /code.launchpad .net/~blake- rouse/maas/ fix-1522898- 1.9/+merge/ 279660