Merge lp:~jtv/maas/api-list-networks 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: 1917
Proposed branch: lp:~jtv/maas/api-list-networks
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 157 lines (+89/-6)
3 files modified
src/maasserver/api.py (+15/-5)
src/maasserver/tests/test_api_networks.py (+70/-0)
src/maasserver/urls_api.py (+4/-1)
To merge this branch: bzr merge lp:~jtv/maas/api-list-networks
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+205525@code.launchpad.net

Commit message

"List networks" API call.

Description of the change

In doing this I noticed a change in our API style: where we used to write "list" operations for the "plural" resources, unless we need to parameterise the listing, we now just implement GET on the resource. So to list all nodes, you call the nodes handler's list() method, but to list all zones, you just GET ".../zones/". I followed the new style here, but I'm not sure the CLI can deal with this. Maybe we ought to have both for all resources.

Jeroen

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

[0]

50 === added file 'src/maasserver/tests/test_api_networks.py'
[...]
83 + def test_list_returns_network(self):

The name of this test is strange. One would expect test_list_returns_network*s*

[1]

50 === added file 'src/maasserver/tests/test_api_networks.py'
[...]
83 + def test_list_returns_network(self):

I put a break point in this test and look:

(Pdb) parsed_result
[{u'description': u'tjBXqFQ7vK', u'ip': u'217.187.192.0', u'_state': u'<django.db.models.base.ModelState object at 0x962acd0>', u'netmask': u'255.255.192.0', u'vlan_tag': None, u'id': 1, u'name': u'6THuCI'}]

Django adds clutter to the objects (I'm talking about the '_state' field) so we need to explicitly list the fields that we want to return.

[2]

50 === added file 'src/maasserver/tests/test_api_networks.py'
[...]
93 + (
94 + original_network.name,
95 + original_network.ip,
96 + original_network.netmask,
97 + original_network.vlan_tag,
98 + original_network.description,
99 + ),
100 + (
101 + returned_network['name'],
102 + returned_network['ip'],
103 + returned_network['netmask'],
104 + returned_network['vlan_tag'],
105 + returned_network['description'],
106 + ))

Couldn't this benefit from self.assertAttributes?

[3]

50 === added file 'src/maasserver/tests/test_api_networks.py'
[...]
93 + (
94 + original_network.name,
95 + original_network.ip,
96 + original_network.netmask,
97 + original_network.vlan_tag,
98 + original_network.description,
99 + ),
100 + (
101 + returned_network['name'],
102 + returned_network['ip'],
103 + returned_network['netmask'],
104 + returned_network['vlan_tag'],
105 + returned_network['description'],
106 + ))

There is a bug: we should also have a 'resource_uri' field.

Revision history for this message
Raphaël Badin (rvb) wrote :

No further remarks :).

[1] and [3] are, I think, blocker but feel free to fix that in follow-up branches.

I see you've added the 'resource_uri' field in https://code.launchpad.net/~jtv/maas/api-add-edit-network/+merge/205539 so it's just a matter of adding a test to fix [3].

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

> [0]
>
> 50 === added file 'src/maasserver/tests/test_api_networks.py'
> [...]
> 83 + def test_list_returns_network(self):
>
> The name of this test is strange. One would expect
> test_list_returns_network*s*

Except in this case it's only one.

> [1]
>
> 50 === added file 'src/maasserver/tests/test_api_networks.py'
> [...]
> 83 + def test_list_returns_network(self):
>
> I put a break point in this test and look:
>
> (Pdb) parsed_result
> [{u'description': u'tjBXqFQ7vK', u'ip': u'217.187.192.0', u'_state':
> u'<django.db.models.base.ModelState object at 0x962acd0>', u'netmask':
> u'255.255.192.0', u'vlan_tag': None, u'id': 1, u'name': u'6THuCI'}]
>
> Django adds clutter to the objects (I'm talking about the '_state' field) so
> we need to explicitly list the fields that we want to return.

That goes in NetworkHandler.fields I believe, which is in one of the later branches in this series. I'll add it there.

> [2]
>
> 50 === added file 'src/maasserver/tests/test_api_networks.py'
> [...]
> 93 + (
> 94 + original_network.name,
> 95 + original_network.ip,
> 96 + original_network.netmask,
> 97 + original_network.vlan_tag,
> 98 + original_network.description,
> 99 + ),
> 100 + (
> 101 + returned_network['name'],
> 102 + returned_network['ip'],
> 103 + returned_network['netmask'],
> 104 + returned_network['vlan_tag'],
> 105 + returned_network['description'],
> 106 + ))
>
> Couldn't this benefit from self.assertAttributes?

The same words went through my head, believe me! The problem is that in this case, the object has the expected values and the dict has the tested values.

> [3]
>
> 50 === added file 'src/maasserver/tests/test_api_networks.py'
> [...]
> 93 + (
> 94 + original_network.name,
> 95 + original_network.ip,
> 96 + original_network.netmask,
> 97 + original_network.vlan_tag,
> 98 + original_network.description,
> 99 + ),
> 100 + (
> 101 + returned_network['name'],
> 102 + returned_network['ip'],
> 103 + returned_network['netmask'],
> 104 + returned_network['vlan_tag'],
> 105 + returned_network['description'],
> 106 + ))
>
> There is a bug: we should also have a 'resource_uri' field.

Yeah, I added that in one of the other branches.

Revision history for this message
Raphaël Badin (rvb) wrote :

> > [0]
> >
> > 50 === added file 'src/maasserver/tests/test_api_networks.py'
> > [...]
> > 83 + def test_list_returns_network(self):
> >
> > The name of this test is strange. One would expect
> > test_list_returns_network*s*
>
> Except in this case it's only one.

Well, I'd say it's a list of networks… with only one network. For the casual reader, test_list_returns_network looks weird.

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

OK, I renamed the test.

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-07 17:14:08 +0000
3+++ src/maasserver/api.py 2014-02-10 11:22:04 +0000
4@@ -69,6 +69,7 @@
5 "FilesHandler",
6 "get_oauth_token",
7 "MaasHandler",
8+ "NetworksHandler",
9 "NodeGroupHandler",
10 "NodeGroupsHandler",
11 "NodeGroupInterfaceHandler",
12@@ -193,6 +194,7 @@
13 DHCPLease,
14 FileStorage,
15 MACAddress,
16+ Network,
17 Node,
18 NodeGroup,
19 NodeGroupInterface,
20@@ -2590,11 +2592,10 @@
21 class ZoneHandler(OperationsHandler):
22 """Manage a physical zone.
23
24- Any node may optionally be assigned to a physical zone, or "zone" for
25- short. The meaning of a physical zone is up to you: it could identify
26- e.g. a server rack, a network, or a data centre. Users can then allocate
27- nodes from specific physical zones, to suit their redundancy or
28- performance requirements.
29+ Any node is in a physical zone, or "zone" for short. The meaning of a
30+ physical zone is up to you: it could identify e.g. a server rack, a
31+ network, or a data centre. Users can then allocate nodes from specific
32+ physical zones, to suit their redundancy or performance requirements.
33
34 This functionality is only available to administrators. Other users can
35 view physical zones, but not modify them.
36@@ -2665,3 +2666,12 @@
37 Get a listing of all the physical zones.
38 """
39 return Zone.objects.all().order_by('name')
40+
41+
42+class NetworksHandler(OperationsHandler):
43+ """API for networks."""
44+ create = update = delete = None
45+
46+ def read(self, request):
47+ """List networks."""
48+ return Network.objects.all().order_by('name')
49
50=== added file 'src/maasserver/tests/test_api_networks.py'
51--- src/maasserver/tests/test_api_networks.py 1970-01-01 00:00:00 +0000
52+++ src/maasserver/tests/test_api_networks.py 2014-02-10 11:22:04 +0000
53@@ -0,0 +1,70 @@
54+# Copyright 2014 Canonical Ltd. This software is licensed under the
55+# GNU Affero General Public License version 3 (see the file LICENSE).
56+
57+"""Tests for networks API."""
58+
59+from __future__ import (
60+ absolute_import,
61+ print_function,
62+ unicode_literals,
63+ )
64+
65+str = None
66+
67+__metaclass__ = type
68+__all__ = []
69+
70+import httplib
71+import json
72+
73+from django.core.urlresolvers import reverse
74+from maasserver.testing.api import APITestCase
75+from maasserver.testing.factory import factory
76+
77+
78+class TestNetworksAPI(APITestCase):
79+
80+ def test_handler_path(self):
81+ self.assertEqual('/api/1.0/networks/', reverse('networks_handler'))
82+
83+ def test_list_returns_networks(self):
84+ original_network = factory.make_network()
85+
86+ response = self.client.get(reverse('networks_handler'))
87+ self.assertEqual(httplib.OK, response.status_code, response.content)
88+
89+ parsed_result = json.loads(response.content)
90+ self.assertEqual(1, len(parsed_result))
91+ [returned_network] = parsed_result
92+ self.assertEqual(
93+ (
94+ original_network.name,
95+ original_network.ip,
96+ original_network.netmask,
97+ original_network.vlan_tag,
98+ original_network.description,
99+ ),
100+ (
101+ returned_network['name'],
102+ returned_network['ip'],
103+ returned_network['netmask'],
104+ returned_network['vlan_tag'],
105+ returned_network['description'],
106+ ))
107+
108+ def test_list_returns_empty_if_no_networks(self):
109+ response = self.client.get(reverse('networks_handler'))
110+ self.assertEqual(httplib.OK, response.status_code, response.content)
111+ self.assertEqual([], json.loads(response.content))
112+
113+ def test_list_sorts_by_name(self):
114+ original_names = [factory.make_name('net').lower() for _ in range(3)]
115+ for name in original_names:
116+ factory.make_network(name=name)
117+
118+ response = self.client.get(reverse('networks_handler'))
119+ self.assertEqual(httplib.OK, response.status_code, response.content)
120+
121+ self.assertEqual(
122+ sorted(original_names),
123+ [network['name'] for network in json.loads(response.content)])
124
125=== modified file 'src/maasserver/urls_api.py'
126--- src/maasserver/urls_api.py 2013-12-13 11:43:26 +0000
127+++ src/maasserver/urls_api.py 2014-02-10 11:22:04 +0000
128@@ -1,4 +1,4 @@
129-# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
130+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
131 # GNU Affero General Public License version 3 (see the file LICENSE).
132
133 """URL API routing configuration."""
134@@ -29,6 +29,7 @@
135 FileHandler,
136 FilesHandler,
137 MaasHandler,
138+ NetworksHandler,
139 NodeGroupHandler,
140 NodeGroupInterfaceHandler,
141 NodeGroupInterfacesHandler,
142@@ -57,6 +58,7 @@
143 account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
144 files_handler = RestrictedResource(FilesHandler, authentication=api_auth)
145 file_handler = RestrictedResource(FileHandler, authentication=api_auth)
146+networks_handler = RestrictedResource(NetworksHandler, authentication=api_auth)
147 node_handler = RestrictedResource(NodeHandler, authentication=api_auth)
148 nodes_handler = RestrictedResource(NodesHandler, authentication=api_auth)
149 node_mac_handler = RestrictedResource(NodeMacHandler, authentication=api_auth)
150@@ -126,6 +128,7 @@
151 nodegroupinterfaces_handler, name='nodegroupinterfaces_handler'),
152 url(r'^nodegroups/(?P<uuid>[^/]+)/interfaces/(?P<interface>[^/]+)/$',
153 nodegroupinterface_handler, name='nodegroupinterface_handler'),
154+ url(r'^networks/$', networks_handler, name='networks_handler'),
155 url(r'^files/$', files_handler, name='files_handler'),
156 url(r'^files/(?P<filename>.+)/$', file_handler, name='file_handler'),
157 url(r'^account/$', account_handler, name='account_handler'),