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
1=== added file 'nova/api/openstack/contrib/networks.py'
2--- nova/api/openstack/contrib/networks.py 1970-01-01 00:00:00 +0000
3+++ nova/api/openstack/contrib/networks.py 2011-09-20 09:08:24 +0000
4@@ -0,0 +1,148 @@
5+# vim: tabstop=4 shiftwidth=4 softtabstop=4
6+
7+# Copyright 2011 Grid Dynamics
8+# All Rights Reserved.
9+#
10+# Licensed under the Apache License, Version 2.0 (the "License"); you may
11+# not use this file except in compliance with the License. You may obtain
12+# a copy of the License at
13+#
14+# http://www.apache.org/licenses/LICENSE-2.0
15+#
16+# Unless required by applicable law or agreed to in writing, software
17+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
18+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
19+# License for the specific language governing permissions and limitations
20+# under the License.
21+
22+import urlparse
23+
24+from webob import exc
25+
26+from nova import db
27+from nova import exception
28+from nova import flags
29+from nova import log as logging
30+
31+from nova.api.openstack import extensions
32+from nova.api.openstack.contrib.admin_only import admin_only
33+
34+FLAGS = flags.FLAGS
35+
36+LOG = logging.getLogger('nova.api.contrib.networks')
37+
38+
39+def network_dict(network):
40+ if network:
41+ fields = (
42+ 'bridge', 'vpn_public_port', 'dhcp_start', 'bridge_interface',
43+ 'updated_at', 'id', 'cidr_v6', 'deleted_at',
44+ 'gateway', 'label', 'project_id', 'vpn_private_address',
45+ 'deleted',
46+ 'vlan', 'broadcast', 'netmask', 'injected',
47+ 'cidr', 'vpn_public_address', 'multi_host', 'dns1', 'host',
48+ 'gateway_v6', 'netmask_v6', 'created_at')
49+ return dict((field, getattr(network, field)) for field in fields)
50+ else:
51+ return {}
52+
53+
54+def require_admin(f):
55+ def wraps(self, req, *args, **kwargs):
56+ if 'nova.context' in req.environ and\
57+ req.environ['nova.context'].is_admin:
58+ return f(self, req, *args, **kwargs)
59+ else:
60+ raise exception.AdminRequired()
61+ return wraps
62+
63+
64+class NetworkController(object):
65+
66+ @require_admin
67+ def action(self, req, id, body):
68+ actions = {'disassociate': self._disassociate}
69+ for action, data in body.iteritems():
70+ try:
71+ return actions[action](req, id, body)
72+ except KeyError:
73+ msg = _("Network does not have %s action") % action
74+ raise exc.HTTPBadRequest(explanation=msg)
75+ msg = _("Invalid request body")
76+ raise exc.HTTPBadRequest(explanation=msg)
77+
78+ def _disassociate(self, req, id, body):
79+ context = req.environ['nova.context']
80+ LOG.debug(_("Disassociating network with id %{query}s, "
81+ "context %{context}s"),
82+ {"query": id, "context": context})
83+ try:
84+ net = db.network_get(context, int(id))
85+ except exception.NetworkNotFound:
86+ raise exc.HTTPNotFound()
87+ db.network_disassociate(context, net.id)
88+ return {'disassociated': int(id)}
89+
90+ @require_admin
91+ def index(self, req):
92+ """Can filter projects """
93+ context = req.environ['nova.context']
94+ LOG.info(_("Getting networks with context %{ctxt}s"),
95+ {"ctxt": context})
96+ networks = db.network_get_all(context)
97+ result = [network_dict(net_ref) for net_ref in networks]
98+ return {'networks': result}
99+
100+ @require_admin
101+ def show(self, req, id):
102+ context = req.environ['nova.context']
103+ LOG.info(_("Showing network with id %{query}s, context %{ctxt}s"),
104+ {"query": id, "context": context})
105+ try:
106+ net = db.network_get(context, int(id))
107+ except exception.NetworkNotFound:
108+ raise exc.HTTPNotFound()
109+ return {'network': network_dict(net)}
110+
111+ @require_admin
112+ def delete(self, req, id):
113+ context = req.environ['nova.context']
114+ LOG.audit(_("Deleting network with id %{query}s, context %{ctxt}s"),
115+ {"query": context, "ctxt": context})
116+ try:
117+ net = db.network_get(context, int(id))
118+ except exception.NetworkNotFound:
119+ raise exc.HTTPNotFound()
120+ db.network_delete_safe(context, net.id)
121+ return exc.HTTPAccepted()
122+
123+ # TODO(nsokolov): implement full CRUD, not done right in nova too
124+
125+
126+class Networks(extensions.ExtensionDescriptor):
127+ def __init__(self):
128+ pass
129+
130+ def get_name(self):
131+ return "NetworkAdmin"
132+
133+ def get_alias(self):
134+ return "NETWORK"
135+
136+ def get_description(self):
137+ return "The Network API Extension"
138+
139+ def get_namespace(self):
140+ return "http://docs.openstack.org/ext/os-networks/api/v1.1"
141+
142+ def get_updated(self):
143+ return "2011-08-23 07:44:50.888131"
144+
145+ @admin_only
146+ def get_resources(self):
147+ resources = []
148+ resources.append(extensions.ResourceExtension('os-networks',
149+ NetworkController(),
150+ member_actions={
151+ 'action': 'POST'}))
152+ return resources
153
154=== added file 'nova/tests/api/openstack/contrib/test_networks.py'
155--- nova/tests/api/openstack/contrib/test_networks.py 1970-01-01 00:00:00 +0000
156+++ nova/tests/api/openstack/contrib/test_networks.py 2011-09-20 09:08:24 +0000
157@@ -0,0 +1,229 @@
158+# Copyright 2011 Grid Dynamics
159+# All Rights Reserved.
160+#
161+# Licensed under the Apache License, Version 2.0 (the "License"); you may
162+# not use this file except in compliance with the License. You may obtain
163+# a copy of the License at
164+#
165+# http://www.apache.org/licenses/LICENSE-2.0
166+#
167+# Unless required by applicable law or agreed to in writing, software
168+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
169+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
170+# License for the specific language governing permissions and limitations
171+# under the License.
172+
173+import json
174+from urllib import urlencode
175+import webob
176+
177+from nova import context
178+from nova import db
179+from nova.db.sqlalchemy.api import require_admin_context, require_context
180+from nova import exception
181+from nova import test
182+from nova.tests.api.openstack import fakes
183+
184+from nova.flags import FLAGS
185+
186+from nova.api.openstack.contrib.networks import NetworkController
187+
188+
189+class fake_ref(dict):
190+ def __getattr__(self, item):
191+ return self[item]
192+
193+
194+def init_fake_nets():
195+ return [fake_ref({'bridge': 'br100', 'vpn_public_port': 1000,
196+ 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0',
197+ 'updated_at': '2011-08-16 09:26:13.048257', 'id': 1,
198+ 'cidr_v6': None, 'deleted_at': None,
199+ 'gateway': '10.0.0.1', 'label': 'mynet_0',
200+ 'project_id': 'admin',
201+ 'vpn_private_address': '10.0.0.2', 'deleted': False,
202+ 'vlan': 100, 'broadcast': '10.0.0.7',
203+ 'netmask': '255.255.255.248', 'injected': False,
204+ 'cidr': '10.0.0.0/29',
205+ 'vpn_public_address': '127.0.0.1', 'multi_host': False,
206+ 'dns1': None, 'host': 'nsokolov-desktop',
207+ 'gateway_v6': None, 'netmask_v6': None,
208+ 'created_at': '2011-08-15 06:19:19.387525'}),
209+ fake_ref({'bridge': 'br101', 'vpn_public_port': 1001,
210+ 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0',
211+ 'updated_at': None, 'id': 2, 'cidr_v6': None,
212+ 'deleted_at': None, 'gateway': '10.0.0.9',
213+ 'label': 'mynet_1', 'project_id': None,
214+ 'vpn_private_address': '10.0.0.10', 'deleted': False,
215+ 'vlan': 101, 'broadcast': '10.0.0.15',
216+ 'netmask': '255.255.255.248', 'injected': False,
217+ 'cidr': '10.0.0.10/29', 'vpn_public_address': None,
218+ 'multi_host': False, 'dns1': None, 'host': None,
219+ 'gateway_v6': None, 'netmask_v6': None,
220+ 'created_at': '2011-08-15 06:19:19.885495'})]
221+
222+fake_nets = init_fake_nets()
223+
224+
225+@require_admin_context
226+def db_network_disassociate(context, id):
227+ global fake_nets
228+ for net in fake_nets:
229+ if net['id'] == id:
230+ net['project_id'] = None
231+ return net
232+ raise exception.NetworkNotFound()
233+
234+
235+@require_admin_context
236+def db_network_get_all(context):
237+ global fake_nets
238+ return fake_nets
239+
240+
241+@require_context
242+def db_project_get_networks(context, tenant_id, associate=False):
243+ global fake_nets
244+ tenant_nets = []
245+ for net in fake_nets:
246+ if net['project_id'] == tenant_id:
247+ tenant_nets.append(net)
248+ return tenant_nets
249+
250+
251+@require_context
252+def db_network_get(context, network_id):
253+ global fake_nets
254+ for net in fake_nets:
255+ if net['id'] == network_id:
256+ return net
257+ raise exception.NetworkNotFound()
258+
259+
260+@require_context
261+def db_network_get_by_cidr(context, cidr):
262+ global fake_nets
263+ for net in fake_nets:
264+ if net['cidr'] == cidr:
265+ return net
266+ raise exception.NetworkNotFound()
267+
268+
269+@require_admin_context
270+def db_network_delete(context, network_id):
271+ global fake_nets
272+ fake_nets.remove(db.network_get(context, network_id))
273+
274+
275+class NetworkExtentionTest(test.TestCase):
276+ def setUp(self):
277+ super(NetworkExtentionTest, self).setUp()
278+ FLAGS.allow_admin_api = True
279+ self.controller = NetworkController()
280+ fakes.stub_out_networking(self.stubs)
281+ fakes.stub_out_rate_limiting(self.stubs)
282+ self.stubs.Set(db, "network_disassociate",
283+ db_network_disassociate)
284+ self.stubs.Set(db, "network_get_all",
285+ db_network_get_all)
286+ self.stubs.Set(db, "project_get_networks",
287+ db_project_get_networks)
288+ self.stubs.Set(db, "network_get",
289+ db_network_get)
290+ self.stubs.Set(db, "network_get_by_cidr",
291+ db_network_get_by_cidr)
292+ self.stubs.Set(db, "network_delete_safe",
293+ db_network_delete)
294+ self.user = 'user'
295+ self.project = 'project'
296+ self.user_context = context.RequestContext(self.user, self.project,
297+ is_admin=False)
298+ self.admin_context = context.RequestContext(self.user, self.project,
299+ is_admin=True)
300+ global fake_nets
301+ fake_nets = init_fake_nets()
302+
303+ def test_network_list_all(self):
304+ req = webob.Request.blank('/v1.1/os-networks')
305+ req.method = 'GET'
306+ req.headers['Content-Type'] = 'application/json'
307+
308+ res = req.get_response(fakes.wsgi_app(
309+ fake_auth_context=self.admin_context))
310+ self.assertEqual(res.status_int, 200)
311+ res_dict = json.loads(res.body)
312+ self.assertEquals(res_dict, {'networks': fake_nets})
313+
314+ req = webob.Request.blank('/v1.1/os-networks')
315+ req.method = 'GET'
316+ req.headers['Content-Type'] = 'application/json'
317+
318+ with self.assertRaises(exception.AdminRequired):
319+ res = req.get_response(fakes.wsgi_app(
320+ fake_auth_context=self.user_context))
321+ self.assertEqual(res.status_int, 500) # admin required
322+ res_dict = json.loads(res.body)
323+
324+ right_ans = {'networks': db_network_get_all(self.user_context)}
325+
326+ def test_network_disassociate(self):
327+ req = webob.Request.blank('/v1.1/os-networks/1/action')
328+ req.method = 'POST'
329+ req.body = json.dumps({'disassociate': None})
330+ req.headers['Content-Type'] = 'application/json'
331+
332+ res = req.get_response(fakes.wsgi_app(
333+ fake_auth_context=self.admin_context))
334+ self.assertEqual(res.status_int, 200)
335+ self.assertEqual(json.loads(res.body), {'disassociated': 1})
336+ self.assertEqual(db.network_get(self.admin_context, 1)['project_id'],
337+ None)
338+
339+ req = webob.Request.blank(
340+ '/v1.1/os-networks/12345/action') # not present
341+ req.method = 'POST'
342+ req.body = json.dumps({'disassociate': None})
343+ req.headers['Content-Type'] = 'application/json'
344+
345+ res = req.get_response(fakes.wsgi_app(
346+ fake_auth_context=self.admin_context))
347+ self.assertEqual(res.status_int, 404)
348+
349+ def test_network_get(self):
350+ req = webob.Request.blank('/v1.1/os-networks/1')
351+ req.method = 'GET'
352+ req.headers['Content-Type'] = 'application/json'
353+ res = req.get_response(fakes.wsgi_app(
354+ fake_auth_context=self.admin_context))
355+ self.assertEqual(res.status_int, 200)
356+ res_dict = json.loads(res.body)
357+ waited = {'network': db_network_get(self.admin_context, 1)}
358+ self.assertEquals(res_dict, waited)
359+
360+ req = webob.Request.blank('/v1.1/os-networks/1')
361+ req.headers['Content-Type'] = 'application/json'
362+ res = req.get_response(fakes.wsgi_app(
363+ fake_auth_context=self.user_context))
364+ self.assertEqual(res.status_int, 500)
365+
366+ def test_network_delete(self):
367+ req = webob.Request.blank('/v1.1/os-networks/1')
368+ req.method = 'DELETE'
369+ req.headers['Content-Type'] = 'application/json'
370+ res = req.get_response(fakes.wsgi_app(
371+ fake_auth_context=self.admin_context))
372+ self.assertEqual(res.status_int, 202)
373+ # check it was really deleted
374+ req = webob.Request.blank('/v1.1/os-networks/1')
375+ req.method = 'GET'
376+ req.headers['Content-Type'] = 'application/json'
377+ res = req.get_response(fakes.wsgi_app(
378+ fake_auth_context=self.admin_context))
379+ self.assertEqual(res.status_int, 404)
380+
381+ req = webob.Request.blank('/v1.1/os-networks/12345') # not present
382+ req.method = 'DELETE'
383+ req.headers['Content-Type'] = 'application/json'
384+ res = req.get_response(fakes.wsgi_app(
385+ fake_auth_context=self.admin_context))
386+ self.assertEqual(res.status_int, 404)
387
388=== modified file 'nova/tests/api/openstack/test_extensions.py'
389--- nova/tests/api/openstack/test_extensions.py 2011-09-14 15:54:56 +0000
390+++ nova/tests/api/openstack/test_extensions.py 2011-09-20 09:08:24 +0000
391@@ -93,6 +93,7 @@
392 "Hosts",
393 "Keypairs",
394 "Multinic",
395+ "NetworkAdmin",
396 "Quotas",
397 "Rescue",
398 "SecurityGroups",