Merge lp:~blake-rouse/maas/fix-1522898-1.9 into lp:maas/1.9

Proposed by Blake Rouse
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
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.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) 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://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.

review: Needs Information
Revision history for this message
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.)

Revision history for this message
Mike Pontillo (mpontillo) wrote :

And I meant "migrate plain MAC addresses from 1.8 to 1.9".

/me needs to learn to proofread; sorry.

Revision history for this message
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://paste.ubuntu.com/13684305/

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.

Revision history for this message
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.

review: Approve
Revision history for this message
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

[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-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,

Subscribers

People subscribed via source and target branches