Merge lp:~chemikadze/nova/contrib-extention-networks into lp:~hudson-openstack/nova/trunk

Proposed by Nikolay Sokolov
Status: Needs review
Proposed branch: lp:~chemikadze/nova/contrib-extention-networks
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 398 lines (+378/-0)
3 files modified
nova/api/openstack/contrib/networks.py (+148/-0)
nova/tests/api/openstack/contrib/test_networks.py (+229/-0)
nova/tests/api/openstack/test_extensions.py (+1/-0)
To merge this branch: bzr merge lp:~chemikadze/nova/contrib-extention-networks
Reviewer Review Type Date Requested Status
Brian Lamar (community) Needs Fixing
Thierry Carrez (community) ffe Abstain
Matt Dietz (community) Needs Fixing
Review via email: mp+72204@code.launchpad.net

Description of the change

This contrib extention adds some admin-level API into Openstack Nova.

In that extention implemented non-single-use methods of nova-manage-network: disassociating, deleting, listing.

Delete networks:
DELETE /admin/networks/{id}
DELETE /admin/networks/delete?cidr={cidr}

Disassociate networks:
DELETE /admin/networks/{id}/disassociate
DELETE /admin/networks/disassociate?cidr={cidr}

List all networks:
GET /admin/networks/

Get specifiq network:
GET /admin/networks/{id}
GET /admin/networks?cidr={cidr}

To post a comment you must log in.
Revision history for this message
Nachi Ueno (nati-ueno) wrote :

 I got errors when I run unittest.
Plz check it
http://paste.openstack.org/show/2228/

1456. By Nikolay Sokolov

Fixed log formatting and extention tests.

Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

I didn't expect that adding an extention may cause other test failures, I'll be more careful in future. Fixed that problems.

Revision history for this message
Brian Lamar (blamar) wrote :

I haven't done a full review yet, but I was curious how/if this is currently work for you. It seems like you're setting up self.network_api, but never using it?

69 + self.network_api = network.API()

Does this not need to be used anywhere?

review: Needs Information
1457. By Nikolay Sokolov

Removed dead code

Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

Thanks again, Brian. Yes, you are right, I mistakely got this code from FloatingIPs extention, what I have used as pattern during porting my code from openstackx to nova. Now I understand extention mechanics in OS a little bit more, so that can be deleted.

1458. By Nikolay Sokolov

Consistent get_updated

Revision history for this message
Thierry Carrez (ttx) wrote :

This should wait for Essex.

review: Disapprove
Revision history for this message
Thierry Carrez (ttx) :
review: Needs Information (ffe)
Revision history for this message
Matt Dietz (cerberus) wrote :

I'm not really a fan of this being an admin route, exactly. Currently there are no other extensions that use the "admin" route or any standard spec controllers that do. I would actually suggest we simply remove "admin" from the route, because I think it could be confusing to the end user trying to discover functional
ity about the API. Specifically, they might wonder why other "admin" looking actions are routed under existing controllers, and what about "networks" makes it specifically admin related.

7 +# Copyright 2011 OpenStack LLC.
8 +# Copyright 2011 Grid Dynamics
9 +# Copyright 2011 Nikolay Sokolov

If you're adding a new file, you only need whichever copyright is most relevant to the work you're doing. I'm guessing this would primarily be Grid Dynamics in this case?

I'm concerned about the unit tests in this patch, too. While I endorse the idea that we don't use the database for unit tests, I'm afraid that, given your overriding every single database API method, if the networks tables were to change in any way, your tests would continue to pass. What do you think?

review: Needs Fixing
1459. By Nikolay Sokolov

Changed endpoint to os-networks

1460. By Nikolay Sokolov

Updated copyright

Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

Matt, thanks for your comments.

I checked admin api bluprints and now see that unlike openstackx, core nova extentions doesn't use special admin route. So I changed it to common-style 'os-networks'.

Copyrights were made like in floating ips extention, and I thought that if it is in trunk, then it is ok. I'm master in legal issues, but reviewing other sources that my patch is outstanding in that case too, and I fixed that.

But the last one is not clear to me: I agree with thought, that tests passing on mock underlaying code and broken with real code is not good (by the way, just today i've read Sandy's article about that). I tried to find sample of how to deal with that, but I didn't find it neither in extention tests, nor in core OS API tests. In that particular case, I think there is possible to write separate test case, just checking that "fake net" object fits to database. But in general, I'm confused that other tests doesn't fit that requirement. From the other side, isn't verifying backward compatability a problem of database tests?

Revision history for this message
Thierry Carrez (ttx) wrote :

Essex is open !

review: Abstain (ffe)
Revision history for this message
Brian Lamar (blamar) wrote :

Sorry for the delay in review,

66 + def _network_get_autodetect(self, context, id):

I don't love the need for this function, but I suppose it's a design decision to allow for multiple types of network identifiers to be passed to show/delete/index/etc. It's probably not something I'd hold the merge prop up on, but just stating that I'd rather only have 1 type of input instead of 2 or 3.

122 + except exception.NetworkNotFound:
123 + query_params = urlparse.parse_qs(req.environ['QUERY_STRING'])
124 + cidrs = query_params.get('cidr')
125 + if cidrs:
126 + for cidr in cidrs:
127 + if id == 'disassociate':
128 + self.disassociate(req, cidr)
129 + elif id == 'delete':
130 + self.delete(req, cidr)
131 + return exc.HTTPAccepted()

At first this exception handling was a bit confusing to me, then I realized it was because this method is handling multiple cases:

DELETE /v1.1/os-networks/networks/{id}
DELETE /v1.1/os-networks/networks/delete?cidr={cidr}
DELETE /v1.1/os-networks/networks/disassociate?cidr={cidr}

You're using special identifiers 'delete' and 'disassociate' to allow for the deletion or disassociation of multiple networks at the same time. IMO this is something we should avoid and I'd rather have one way to do things instead of 2. Also there aren't any prior API calls that I'm aware of that do this (multiple deletions at the same time) so it would be more 'standard' to remove this feature and only allow deletions and disassociations by integer identifier. It's quite confusing to call DELETE on /v1.1/os-networks/networks/disassociate, it's double negative-ish.

447 + "NetworkAdmin",

Can you replace these tabs with spaces?

review: Needs Fixing
Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

When I wrote that extention, I was confused about API too: from one side, it is nice to have possibility to manipulate networks by CIDR, like using nova-manage, from another -- resulting API was conceptually unclear. "Feature creature" won at that time, but now
after serveral weeks passed I totally agree with you (especially after viewing OS API -- it seemed fairy nice and simple to me). What do you think about changing resulting IP to something like that:

Delete networks:
DELETE /os-networks/{id}

Disassociate networks:
DELETE /os-networks/{id}/association

List all networks:
GET /os-networks/

Get specific network:
GET /os-networks/{id}

1461. By Nikolay Sokolov

Fixed issue with tabs

Revision history for this message
Brian Lamar (blamar) wrote :

Not sure if you're in IRC, but I'm curious what:

DELETE /os-networks/{id}/association

does still. It looks like it just calls DB's network_disassociate() method which updates the project_id and host of the network to None. If we're following other OpenStack APIs I think a:

POST /os-networks/{id}/actions

with a body of:

{
    "disassociate": null
}

might be the most compliant. It seems like this lines up with our server "actions" (http://docs.openstack.org/cactus/openstack-compute/developer/openstack-compute-api-1.1/content/Server_Actions-d1e3229.html)

That being said this is a pretty big switch from what you have so I'm open for talking more about the best approach! Other than that I think:

Delete networks:
DELETE /os-networks/{id}

List all networks:
GET /os-networks/

Get specific network:
GET /os-networks/{id}

Looks great.

1462. By Nikolay Sokolov

More openstack-style IP

1463. By Nikolay Sokolov

pep8 fix

Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

It is not hard to me make this extention more close to nova api. You can check it out just now.

Revision history for this message
Matt Dietz (cerberus) wrote :

Thanks for the fixes, but noticed something else

In the test methods where you use the global keyword, you actually don't need it. Python is a little opaque on how it handles scoping, but in the methods where you don't actually reassign fake_nets, you don't need global.

See http://paste.openstack.org/show/2487/

300 + global fake_nets
301 + fake_nets = init_fake_nets()

You *would* need it here, because you're assigning it.

Revision history for this message
Nikolay Sokolov (chemikadze) wrote :

Yes, I know. Wrote those extra lines just to emphasize with what these functions actually work. If you don't like such redurancy, I can fix but don't see real reason for that.

Revision history for this message
Matt Dietz (cerberus) wrote :

I'd argue the code speaks for itself, and doesn't need global constructs
to highlight it. I'd prefer you fix it for brevity's sake.

On 9/22/11 1:21 AM, "Nikolay Sokolov" <email address hidden> wrote:

>Yes, I know. Wrote those extra lines just to emphasize with what these
>functions actually work. If you don't like such redurancy, I can fix but
>don't see real reason for that.
>--
>https://code.launchpad.net/~chemikadze/nova/contrib-extention-networks/+me
>rge/72204
>You are reviewing the proposed merge of
>lp:~chemikadze/nova/contrib-extention-networks into lp:nova.

This email may include confidential information. If you received it in error, please delete it.

Unmerged revisions

1463. By Nikolay Sokolov

pep8 fix

1462. By Nikolay Sokolov

More openstack-style IP

1461. By Nikolay Sokolov

Fixed issue with tabs

1460. By Nikolay Sokolov

Updated copyright

1459. By Nikolay Sokolov

Changed endpoint to os-networks

1458. By Nikolay Sokolov

Consistent get_updated

1457. By Nikolay Sokolov

Removed dead code

1456. By Nikolay Sokolov

Fixed log formatting and extention tests.

1455. By Nikolay Sokolov

pep8

1454. By Nikolay Sokolov

Deleting by CIDR

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'nova/api/openstack/contrib/networks.py'
--- nova/api/openstack/contrib/networks.py 1970-01-01 00:00:00 +0000
+++ nova/api/openstack/contrib/networks.py 2011-09-20 09:08:24 +0000
@@ -0,0 +1,148 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 Grid Dynamics
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import urlparse
19
20from webob import exc
21
22from nova import db
23from nova import exception
24from nova import flags
25from nova import log as logging
26
27from nova.api.openstack import extensions
28from nova.api.openstack.contrib.admin_only import admin_only
29
30FLAGS = flags.FLAGS
31
32LOG = logging.getLogger('nova.api.contrib.networks')
33
34
35def network_dict(network):
36 if network:
37 fields = (
38 'bridge', 'vpn_public_port', 'dhcp_start', 'bridge_interface',
39 'updated_at', 'id', 'cidr_v6', 'deleted_at',
40 'gateway', 'label', 'project_id', 'vpn_private_address',
41 'deleted',
42 'vlan', 'broadcast', 'netmask', 'injected',
43 'cidr', 'vpn_public_address', 'multi_host', 'dns1', 'host',
44 'gateway_v6', 'netmask_v6', 'created_at')
45 return dict((field, getattr(network, field)) for field in fields)
46 else:
47 return {}
48
49
50def require_admin(f):
51 def wraps(self, req, *args, **kwargs):
52 if 'nova.context' in req.environ and\
53 req.environ['nova.context'].is_admin:
54 return f(self, req, *args, **kwargs)
55 else:
56 raise exception.AdminRequired()
57 return wraps
58
59
60class NetworkController(object):
61
62 @require_admin
63 def action(self, req, id, body):
64 actions = {'disassociate': self._disassociate}
65 for action, data in body.iteritems():
66 try:
67 return actions[action](req, id, body)
68 except KeyError:
69 msg = _("Network does not have %s action") % action
70 raise exc.HTTPBadRequest(explanation=msg)
71 msg = _("Invalid request body")
72 raise exc.HTTPBadRequest(explanation=msg)
73
74 def _disassociate(self, req, id, body):
75 context = req.environ['nova.context']
76 LOG.debug(_("Disassociating network with id %{query}s, "
77 "context %{context}s"),
78 {"query": id, "context": context})
79 try:
80 net = db.network_get(context, int(id))
81 except exception.NetworkNotFound:
82 raise exc.HTTPNotFound()
83 db.network_disassociate(context, net.id)
84 return {'disassociated': int(id)}
85
86 @require_admin
87 def index(self, req):
88 """Can filter projects """
89 context = req.environ['nova.context']
90 LOG.info(_("Getting networks with context %{ctxt}s"),
91 {"ctxt": context})
92 networks = db.network_get_all(context)
93 result = [network_dict(net_ref) for net_ref in networks]
94 return {'networks': result}
95
96 @require_admin
97 def show(self, req, id):
98 context = req.environ['nova.context']
99 LOG.info(_("Showing network with id %{query}s, context %{ctxt}s"),
100 {"query": id, "context": context})
101 try:
102 net = db.network_get(context, int(id))
103 except exception.NetworkNotFound:
104 raise exc.HTTPNotFound()
105 return {'network': network_dict(net)}
106
107 @require_admin
108 def delete(self, req, id):
109 context = req.environ['nova.context']
110 LOG.audit(_("Deleting network with id %{query}s, context %{ctxt}s"),
111 {"query": context, "ctxt": context})
112 try:
113 net = db.network_get(context, int(id))
114 except exception.NetworkNotFound:
115 raise exc.HTTPNotFound()
116 db.network_delete_safe(context, net.id)
117 return exc.HTTPAccepted()
118
119 # TODO(nsokolov): implement full CRUD, not done right in nova too
120
121
122class Networks(extensions.ExtensionDescriptor):
123 def __init__(self):
124 pass
125
126 def get_name(self):
127 return "NetworkAdmin"
128
129 def get_alias(self):
130 return "NETWORK"
131
132 def get_description(self):
133 return "The Network API Extension"
134
135 def get_namespace(self):
136 return "http://docs.openstack.org/ext/os-networks/api/v1.1"
137
138 def get_updated(self):
139 return "2011-08-23 07:44:50.888131"
140
141 @admin_only
142 def get_resources(self):
143 resources = []
144 resources.append(extensions.ResourceExtension('os-networks',
145 NetworkController(),
146 member_actions={
147 'action': 'POST'}))
148 return resources
0149
=== added file 'nova/tests/api/openstack/contrib/test_networks.py'
--- nova/tests/api/openstack/contrib/test_networks.py 1970-01-01 00:00:00 +0000
+++ nova/tests/api/openstack/contrib/test_networks.py 2011-09-20 09:08:24 +0000
@@ -0,0 +1,229 @@
1# Copyright 2011 Grid Dynamics
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import json
17from urllib import urlencode
18import webob
19
20from nova import context
21from nova import db
22from nova.db.sqlalchemy.api import require_admin_context, require_context
23from nova import exception
24from nova import test
25from nova.tests.api.openstack import fakes
26
27from nova.flags import FLAGS
28
29from nova.api.openstack.contrib.networks import NetworkController
30
31
32class fake_ref(dict):
33 def __getattr__(self, item):
34 return self[item]
35
36
37def init_fake_nets():
38 return [fake_ref({'bridge': 'br100', 'vpn_public_port': 1000,
39 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0',
40 'updated_at': '2011-08-16 09:26:13.048257', 'id': 1,
41 'cidr_v6': None, 'deleted_at': None,
42 'gateway': '10.0.0.1', 'label': 'mynet_0',
43 'project_id': 'admin',
44 'vpn_private_address': '10.0.0.2', 'deleted': False,
45 'vlan': 100, 'broadcast': '10.0.0.7',
46 'netmask': '255.255.255.248', 'injected': False,
47 'cidr': '10.0.0.0/29',
48 'vpn_public_address': '127.0.0.1', 'multi_host': False,
49 'dns1': None, 'host': 'nsokolov-desktop',
50 'gateway_v6': None, 'netmask_v6': None,
51 'created_at': '2011-08-15 06:19:19.387525'}),
52 fake_ref({'bridge': 'br101', 'vpn_public_port': 1001,
53 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0',
54 'updated_at': None, 'id': 2, 'cidr_v6': None,
55 'deleted_at': None, 'gateway': '10.0.0.9',
56 'label': 'mynet_1', 'project_id': None,
57 'vpn_private_address': '10.0.0.10', 'deleted': False,
58 'vlan': 101, 'broadcast': '10.0.0.15',
59 'netmask': '255.255.255.248', 'injected': False,
60 'cidr': '10.0.0.10/29', 'vpn_public_address': None,
61 'multi_host': False, 'dns1': None, 'host': None,
62 'gateway_v6': None, 'netmask_v6': None,
63 'created_at': '2011-08-15 06:19:19.885495'})]
64
65fake_nets = init_fake_nets()
66
67
68@require_admin_context
69def db_network_disassociate(context, id):
70 global fake_nets
71 for net in fake_nets:
72 if net['id'] == id:
73 net['project_id'] = None
74 return net
75 raise exception.NetworkNotFound()
76
77
78@require_admin_context
79def db_network_get_all(context):
80 global fake_nets
81 return fake_nets
82
83
84@require_context
85def db_project_get_networks(context, tenant_id, associate=False):
86 global fake_nets
87 tenant_nets = []
88 for net in fake_nets:
89 if net['project_id'] == tenant_id:
90 tenant_nets.append(net)
91 return tenant_nets
92
93
94@require_context
95def db_network_get(context, network_id):
96 global fake_nets
97 for net in fake_nets:
98 if net['id'] == network_id:
99 return net
100 raise exception.NetworkNotFound()
101
102
103@require_context
104def db_network_get_by_cidr(context, cidr):
105 global fake_nets
106 for net in fake_nets:
107 if net['cidr'] == cidr:
108 return net
109 raise exception.NetworkNotFound()
110
111
112@require_admin_context
113def db_network_delete(context, network_id):
114 global fake_nets
115 fake_nets.remove(db.network_get(context, network_id))
116
117
118class NetworkExtentionTest(test.TestCase):
119 def setUp(self):
120 super(NetworkExtentionTest, self).setUp()
121 FLAGS.allow_admin_api = True
122 self.controller = NetworkController()
123 fakes.stub_out_networking(self.stubs)
124 fakes.stub_out_rate_limiting(self.stubs)
125 self.stubs.Set(db, "network_disassociate",
126 db_network_disassociate)
127 self.stubs.Set(db, "network_get_all",
128 db_network_get_all)
129 self.stubs.Set(db, "project_get_networks",
130 db_project_get_networks)
131 self.stubs.Set(db, "network_get",
132 db_network_get)
133 self.stubs.Set(db, "network_get_by_cidr",
134 db_network_get_by_cidr)
135 self.stubs.Set(db, "network_delete_safe",
136 db_network_delete)
137 self.user = 'user'
138 self.project = 'project'
139 self.user_context = context.RequestContext(self.user, self.project,
140 is_admin=False)
141 self.admin_context = context.RequestContext(self.user, self.project,
142 is_admin=True)
143 global fake_nets
144 fake_nets = init_fake_nets()
145
146 def test_network_list_all(self):
147 req = webob.Request.blank('/v1.1/os-networks')
148 req.method = 'GET'
149 req.headers['Content-Type'] = 'application/json'
150
151 res = req.get_response(fakes.wsgi_app(
152 fake_auth_context=self.admin_context))
153 self.assertEqual(res.status_int, 200)
154 res_dict = json.loads(res.body)
155 self.assertEquals(res_dict, {'networks': fake_nets})
156
157 req = webob.Request.blank('/v1.1/os-networks')
158 req.method = 'GET'
159 req.headers['Content-Type'] = 'application/json'
160
161 with self.assertRaises(exception.AdminRequired):
162 res = req.get_response(fakes.wsgi_app(
163 fake_auth_context=self.user_context))
164 self.assertEqual(res.status_int, 500) # admin required
165 res_dict = json.loads(res.body)
166
167 right_ans = {'networks': db_network_get_all(self.user_context)}
168
169 def test_network_disassociate(self):
170 req = webob.Request.blank('/v1.1/os-networks/1/action')
171 req.method = 'POST'
172 req.body = json.dumps({'disassociate': None})
173 req.headers['Content-Type'] = 'application/json'
174
175 res = req.get_response(fakes.wsgi_app(
176 fake_auth_context=self.admin_context))
177 self.assertEqual(res.status_int, 200)
178 self.assertEqual(json.loads(res.body), {'disassociated': 1})
179 self.assertEqual(db.network_get(self.admin_context, 1)['project_id'],
180 None)
181
182 req = webob.Request.blank(
183 '/v1.1/os-networks/12345/action') # not present
184 req.method = 'POST'
185 req.body = json.dumps({'disassociate': None})
186 req.headers['Content-Type'] = 'application/json'
187
188 res = req.get_response(fakes.wsgi_app(
189 fake_auth_context=self.admin_context))
190 self.assertEqual(res.status_int, 404)
191
192 def test_network_get(self):
193 req = webob.Request.blank('/v1.1/os-networks/1')
194 req.method = 'GET'
195 req.headers['Content-Type'] = 'application/json'
196 res = req.get_response(fakes.wsgi_app(
197 fake_auth_context=self.admin_context))
198 self.assertEqual(res.status_int, 200)
199 res_dict = json.loads(res.body)
200 waited = {'network': db_network_get(self.admin_context, 1)}
201 self.assertEquals(res_dict, waited)
202
203 req = webob.Request.blank('/v1.1/os-networks/1')
204 req.headers['Content-Type'] = 'application/json'
205 res = req.get_response(fakes.wsgi_app(
206 fake_auth_context=self.user_context))
207 self.assertEqual(res.status_int, 500)
208
209 def test_network_delete(self):
210 req = webob.Request.blank('/v1.1/os-networks/1')
211 req.method = 'DELETE'
212 req.headers['Content-Type'] = 'application/json'
213 res = req.get_response(fakes.wsgi_app(
214 fake_auth_context=self.admin_context))
215 self.assertEqual(res.status_int, 202)
216 # check it was really deleted
217 req = webob.Request.blank('/v1.1/os-networks/1')
218 req.method = 'GET'
219 req.headers['Content-Type'] = 'application/json'
220 res = req.get_response(fakes.wsgi_app(
221 fake_auth_context=self.admin_context))
222 self.assertEqual(res.status_int, 404)
223
224 req = webob.Request.blank('/v1.1/os-networks/12345') # not present
225 req.method = 'DELETE'
226 req.headers['Content-Type'] = 'application/json'
227 res = req.get_response(fakes.wsgi_app(
228 fake_auth_context=self.admin_context))
229 self.assertEqual(res.status_int, 404)
0230
=== modified file 'nova/tests/api/openstack/test_extensions.py'
--- nova/tests/api/openstack/test_extensions.py 2011-09-14 15:54:56 +0000
+++ nova/tests/api/openstack/test_extensions.py 2011-09-20 09:08:24 +0000
@@ -93,6 +93,7 @@
93 "Hosts",93 "Hosts",
94 "Keypairs",94 "Keypairs",
95 "Multinic",95 "Multinic",
96 "NetworkAdmin",
96 "Quotas",97 "Quotas",
97 "Rescue",98 "Rescue",
98 "SecurityGroups",99 "SecurityGroups",