Merge lp:~blake-rouse/maas/fix-1522898 into lp:~maas-committers/maas/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
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.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :
review: Approve

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,