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