Merge lp:~jtv/maas/validate-network-nodes-filter into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1941
Proposed branch: lp:~jtv/maas/validate-network-nodes-filter
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 129 lines (+68/-8)
3 files modified
src/maasserver/api.py (+6/-7)
src/maasserver/forms.py (+41/-1)
src/maasserver/tests/test_api_networks.py (+21/-0)
To merge this branch: bzr merge lp:~jtv/maas/validate-network-nodes-filter
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+205972@code.launchpad.net

Commit message

Handle the API GET request to list networks through a form. This fixes the lack of validation: passing multiple nodes means "networks connected to *all* of these nodes," so quietly ignoring unknown nodes could lead to unwanted results.

Description of the change

Raphaël suggested that more API calls ought to do this, so see it as a prototype for possible repeated use.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Very nice!

[0]

24 + if nodes is not None:
25 + for node in nodes:
26 networks = networks.filter(node=node)
27 return networks.order_by('name')

Ideally, I think this logic (the filtering and the order_by) belongs to the form (same as the usual call to form.save() when the form is a modification/creation form).

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

OK, I've moved it. I'm tempted to write unit tests for the form at this point, but without any reuse and the API such a thin wrapper around it, it's really not worth the extra weight.

Thanks for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api.py'
2--- src/maasserver/api.py 2014-02-12 04:33:17 +0000
3+++ src/maasserver/api.py 2014-02-12 15:40:09 +0000
4@@ -173,13 +173,14 @@
5 from maasserver.forms import (
6 BulkNodeActionForm,
7 DownloadProgressForm,
8- NodeGroupInterfaceForeignDHCPForm,
9 get_action_form,
10 get_node_create_form,
11 get_node_edit_form,
12 NetworkForm,
13+ NetworksListingForm,
14 NodeActionForm,
15 NodeGroupEdit,
16+ NodeGroupInterfaceForeignDHCPForm,
17 NodeGroupInterfaceForm,
18 NodeGroupWithInterfacesForm,
19 SSHKeyForm,
20@@ -2767,12 +2768,10 @@
21 networks. If more than one node is given, the result will be
22 restricted to networks that these nodes have in common.
23 """
24- networks = Network.objects.all()
25- node_ids = get_optional_list(request.GET, 'node')
26- if node_ids is not None:
27- for node in Node.objects.filter(system_id__in=node_ids):
28- networks = networks.filter(node=node)
29- return networks.order_by('name')
30+ form = NetworksListingForm(data=request.GET)
31+ if not form.is_valid():
32+ raise ValidationError(form.errors)
33+ return form.filter_networks(Network.objects.all())
34
35 @admin_method
36 def create(self, request):
37
38=== modified file 'src/maasserver/forms.py'
39--- src/maasserver/forms.py 2014-02-12 13:13:51 +0000
40+++ src/maasserver/forms.py 2014-02-12 15:40:09 +0000
41@@ -26,6 +26,7 @@
42 "MAASAndNetworkForm",
43 "MACAddressForm",
44 "NetworkForm",
45+ "NetworksListingForm",
46 "NodeGroupEdit",
47 "NodeGroupInterfaceForm",
48 "NodeGroupWithInterfacesForm",
49@@ -856,7 +857,6 @@
50 % (networks[index - 1]['name'], networks[index]['name']))
51
52
53-# XXX JeroenVermeulen 2014-01-29 bug=1052339: This restriction is going away.
54 class NodeGroupWithInterfacesForm(ModelForm):
55 """Create a NodeGroup with unmanaged interfaces."""
56
57@@ -1223,3 +1223,43 @@
58 'netmask',
59 'vlan_tag',
60 )
61+
62+
63+class NetworksListingForm(forms.Form):
64+ """Form for the networks listing API."""
65+
66+ # Multi-value parameter, but with a name in the singular. This is going
67+ # to be passed as a GET-style parameter in the URL, so repeated as "node="
68+ # for every node.
69+ node = UnconstrainedMultipleChoiceField(
70+ label="Show only networks that are attached to all of these nodes.",
71+ required=False, error_messages={
72+ 'invalid_list':
73+ "Invalid parameter: list of node system IDs required.",
74+ })
75+
76+ def clean_node(self):
77+ system_ids = self.cleaned_data['node']
78+ if system_ids is None:
79+ return None
80+ system_ids = set(system_ids)
81+ nodes = Node.objects.filter(system_id__in=system_ids)
82+ if len(nodes) != len(system_ids):
83+ unknown = system_ids.difference({node.system_id for node in nodes})
84+ raise forms.ValidationError(
85+ "Unknown node(s): %s."
86+ % ', '.join(sorted(unknown)))
87+ return nodes
88+
89+ def filter_networks(self, networks):
90+ """Filter (and order) the given networks by the form's criteria.
91+
92+ :param networks: A query set of :class:`Network`.
93+ :return: A version of `networks` restricted and ordered according to
94+ the criteria passed to the form.
95+ """
96+ nodes = self.cleaned_data.get('node')
97+ if nodes is not None:
98+ for node in nodes:
99+ networks = networks.filter(node=node)
100+ return networks.order_by('name')
101
102=== modified file 'src/maasserver/tests/test_api_networks.py'
103--- src/maasserver/tests/test_api_networks.py 2014-02-12 04:26:59 +0000
104+++ src/maasserver/tests/test_api_networks.py 2014-02-12 15:40:09 +0000
105@@ -117,3 +117,24 @@
106 self.assertEqual(
107 {networks[2].name},
108 {network['name'] for network in json.loads(response.content)})
109+
110+ def test_GET_fails_if_filtering_by_nonexistent_node(self):
111+ bad_system_id = factory.make_name('no_node')
112+ response = self.client.get(
113+ reverse('networks_handler'),
114+ {'node': [bad_system_id]})
115+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
116+ self.assertEqual(
117+ {'node': ["Unknown node(s): %s." % bad_system_id]},
118+ json.loads(response.content))
119+
120+ def test_GET_ignores_duplicates(self):
121+ factory.make_network()
122+ node = factory.make_node(networks=[factory.make_network()])
123+ response = self.client.get(
124+ reverse('networks_handler'),
125+ {'node': [node.system_id, node.system_id]})
126+ self.assertEqual(httplib.OK, response.status_code, response.content)
127+ self.assertEqual(
128+ {network.name for network in node.networks.all()},
129+ {network['name'] for network in json.loads(response.content)})