Merge lp:~rackspace-titan/nova/openstack-api-versioned-controllers into lp:~hudson-openstack/nova/trunk

Proposed by Naveed Massjouni
Status: Superseded
Proposed branch: lp:~rackspace-titan/nova/openstack-api-versioned-controllers
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~rackspace-titan/nova/openstack-api-version-split
Diff against target: 704 lines (+216/-138)
12 files modified
etc/api-paste.ini (+13/-7)
nova/api/openstack/__init__.py (+28/-5)
nova/api/openstack/auth.py (+0/-2)
nova/api/openstack/common.py (+14/-3)
nova/api/openstack/servers.py (+55/-10)
nova/api/openstack/views/addresses.py (+2/-14)
nova/api/openstack/views/flavors.py (+1/-18)
nova/api/openstack/views/images.py (+1/-18)
nova/api/openstack/views/servers.py (+26/-41)
nova/tests/api/openstack/fakes.py (+11/-7)
nova/tests/api/openstack/test_auth.py (+2/-4)
nova/tests/api/openstack/test_servers.py (+63/-9)
To merge this branch: bzr merge lp:~rackspace-titan/nova/openstack-api-versioned-controllers
Reviewer Review Type Date Requested Status
Sandy Walsh (community) Approve
Todd Willey (community) Approve
Eric Day (community) Needs Fixing
Joshua McKenty (community) Needs Information
Brian Waldon (community) Approve
Review via email: mp+53748@code.launchpad.net

This proposal has been superseded by a proposal from 2011-03-23.

Description of the change

Added a mechanism for versioned controllers for openstack api versions 1.0/1.1.
Create servers in the 1.1 api now supports imageRef/flavorRef instead of imageId/flavorId.

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Right now your imageRef parsing for v1.1 requests is very simple. All it does is use everything after the last slash as the image id. What will happen if you get an unexpected imageRef, such as this:

    http://servers.api.openstack.org/ v1.1/1234/images?limit=1&marker=743

I ask, because this is the format of the "next" link returned in a request to /images. I understand this shouldn't be valid, but it appears it may have unintended consequences.

I like what you did with the versioned APIRouters and Controllers.

review: Needs Fixing
Revision history for this message
Naveed Massjouni (ironcamel) wrote :

> Right now your imageRef parsing for v1.1 requests is very simple. All it does
> is use everything after the last slash as the image id. What will happen if
> you get an unexpected imageRef, such as this:
>
> http://servers.api.openstack.org/ v1.1/1234/images?limit=1&marker=743
>
> I ask, because this is the format of the "next" link returned in a request to
> /images. I understand this shouldn't be valid, but it appears it may have
> unintended consequences.
>

Commit 806 addresses this. Thanks.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Looks good! Thanks for taking a look at that.

review: Approve
Revision history for this message
Joshua McKenty (joshua-mckenty) wrote :

Overall thoughts:

 - Using an Integer for the API version number - convenient, but needs to be well documented (docstrings, etc), so that we don't end up with collisions between 1.0 and 10.0, for instance.

 - Using a general assumption that <object_id> is the last portion of the URL, and an integer, seems really dangerous. If I understand correctly, the point of using ref for image is that it could potentially refer to a remote glance server, is that right? Even if this is reliably on the same domain, can we use a proper url parsing library (wsgilib from paste, perhaps) for grabbing portions of the path? What happens if the image ref is sftp:// or git:// schema instead of http?

Why the change to isDetail=True?
Missing docstrings on classes and private methods.
430, instead of the if, use:
    for item in inst.get('metadata', []):

449 and 457 - we don't want to guarantee that those keys are set? Otherwise use .get() again.

468 and 474 - do we really want == None checks here? E.g., are the calls valid if image id and flavour aren't set? Would an exception thrown here be appropriate?

597 Content-Type header should, I believe, be "Accept" header on request. Also, it's case-insensitive according to the RFC (see http://www.ietf.org/rfc/rfc2616.txt section 4.2), so we should probably support it that way.

622 - Why the mix of dict() and {} ?

review: Needs Information
Revision history for this message
Brian Waldon (bcwaldon) wrote :

I addressed some of your concerns, as Naveed is out today:

> - Using an Integer for the API version number - convenient, but needs to be well documented
> (docstrings, etc), so that we don't end up with collisions between 1.0 and 10.0, for instance.

I'll let Naveed address this.

> - Using a general assumption that <object_id> is the last portion of the URL,
> and an integer, seems really dangerous. If I understand correctly, the point
> of using ref for image is that it could potentially refer to a remote glance
> server, is that right? Even if this is reliably on the same domain, can we use
> a proper url parsing library (wsgilib from paste, perhaps) for grabbing
> portions of the path? What happens if the image ref is sftp:// or git://
> schema instead of http?

Our main goal was to get as much of the openstack api v1.1 interface defined in code. We know we don't handle the intended functionality, but we also haven't officially announced it. We want to get the interface defined as well as possible for cactus with the intention of filling in the holes in future releases (maybe even as bugs before we release).

> Why the change to isDetail=True?

Assuming you mean line 214: when creating a server you are supposed to get a "detail" response back. It was a bug to use is_detail=False. Check the spec if you want to see more info.

> Missing docstrings on classes and private methods.

I'll let Naveed address this.

> 430, instead of the if, use:
> for item in inst.get('metadata', []):

Good call. Fixed.

> 449 and 457 - we don't want to guarantee that those keys are set? Otherwise
> use .get() again.

The absence of those values from "inst" could indicate an error, as a server should always have an image id and instance type. I don't want to communicate those as NULL or an empty string if that should never actually happen. How would you suggest handling that?

> 468 and 474 - do we really want == None checks here? E.g., are the calls valid
> if image id and flavour aren't set? Would an exception thrown here be
> appropriate?

This goes along with the above statement.

> 597 Content-Type header should, I believe, be "Accept" header on request.
> Also, it's case-insensitive according to the RFC (see
> http://www.ietf.org/rfc/rfc2616.txt section 4.2), so we should probably
> support it that way.

I think you might have Accept and Content-Type a little confused. With a request to a server, you use Content-Type to describe the mimetype of the data you are providing. You use Accept to ask the server for a certain mimetype in the response. When the server responds, it uses Content-Type to describe the mimetype of the response entity. In this case, the test is setting the Content-Type of the request body.

> 622 - Why the mix of dict() and {} ?

Looks like it was copy/pasted in without thinking about it. Fixed.

Revision history for this message
Todd Willey (xtoddx) wrote :
Download full text (3.4 KiB)

> I addressed some of your concerns, as Naveed is out today:
>
> > - Using an Integer for the API version number - convenient, but needs to be
> well documented
> > (docstrings, etc), so that we don't end up with collisions between 1.0 and
> 10.0, for instance.
>
> I'll let Naveed address this.
>

Josh, do you mean in naming the classes? Paste.deploy maps urls into the classes, so the actual names are not very consequential.

>
> > - Using a general assumption that <object_id> is the last portion of the
> URL,
> > and an integer, seems really dangerous. If I understand correctly, the point
> > of using ref for image is that it could potentially refer to a remote glance
> > server, is that right? Even if this is reliably on the same domain, can we
> use
> > a proper url parsing library (wsgilib from paste, perhaps) for grabbing
> > portions of the path? What happens if the image ref is sftp:// or git://
> > schema instead of http?
>
> Our main goal was to get as much of the openstack api v1.1 interface defined
> in code. We know we don't handle the intended functionality, but we also
> haven't officially announced it. We want to get the interface defined as well
> as possible for cactus with the intention of filling in the holes in future
> releases (maybe even as bugs before we release).
>

I'd also like to use PATH_INFO or SCRIPT_NAME or the like (set by wsgi) at the very least.

>
> > Why the change to isDetail=True?
>
> Assuming you mean line 214: when creating a server you are supposed to get a
> "detail" response back. It was a bug to use is_detail=False. Check the spec if
> you want to see more info.
>
> > Missing docstrings on classes and private methods.
>
> I'll let Naveed address this.
>
>
> > 430, instead of the if, use:
> > for item in inst.get('metadata', []):
>
> Good call. Fixed.
>
>
> > 449 and 457 - we don't want to guarantee that those keys are set? Otherwise
> > use .get() again.
>
> The absence of those values from "inst" could indicate an error, as a server
> should always have an image id and instance type. I don't want to communicate
> those as NULL or an empty string if that should never actually happen. How
> would you suggest handling that?
>

But inst comes back from compute_api, so it should be a reliable source of data. If for whatever reason it isn't, I think the place to perform this check is in the controller, not in the view builder. You can raise whatever fault is appropriate there.

>
> > 468 and 474 - do we really want == None checks here? E.g., are the calls
> valid
> > if image id and flavour aren't set? Would an exception thrown here be
> > appropriate?
>
> This goes along with the above statement.
>
>
> > 597 Content-Type header should, I believe, be "Accept" header on request.
> > Also, it's case-insensitive according to the RFC (see
> > http://www.ietf.org/rfc/rfc2616.txt section 4.2), so we should probably
> > support it that way.
>
> I think you might have Accept and Content-Type a little confused. With a
> request to a server, you use Content-Type to describe the mimetype of the data
> you are providing. You use Accept to ask the server for a certain mimetype in
> th...

Read more...

Revision history for this message
Naveed Massjouni (ironcamel) wrote :

> Overall thoughts:
>
> - Using an Integer for the API version number - convenient, but needs to be
> well documented (docstrings, etc), so that we don't end up with collisions
> between 1.0 and 10.0, for instance.

Added docstrings. Also, by the time we get to 10.0, the code will probably be totally different anyway.

>
> - Using a general assumption that <object_id> is the last portion of the URL,
> and an integer, seems really dangerous. If I understand correctly, the point
> of using ref for image is that it could potentially refer to a remote glance
> server, is that right? Even if this is reliably on the same domain, can we use
> a proper url parsing library (wsgilib from paste, perhaps) for grabbing
> portions of the path? What happens if the image ref is sftp:// or git://
> schema instead of http?
>

Good point. I am now using the urlparse module to parse the id out of the url.

>
> Why the change to isDetail=True?
> Missing docstrings on classes and private methods.
> 430, instead of the if, use:
> for item in inst.get('metadata', []):

Nice, thanks for show me this idiom.

>
> 449 and 457 - we don't want to guarantee that those keys are set? Otherwise
> use .get() again.
>
> 468 and 474 - do we really want == None checks here? E.g., are the calls valid
> if image id and flavour aren't set? Would an exception thrown here be
> appropriate?

You are right. The image_id and instance_type should always exist in an instance entity. Fixed.

Revision history for this message
Todd Willey (xtoddx) wrote :

Can you please read over Pep-257 for docstring conventions? http://www.python.org/dev/peps/pep-0257/

review: Needs Fixing
Revision history for this message
Eric Day (eday) wrote :

Looks good! I definitely think the view building is a lot cleaner now. One thing:

119-122: See the HACKING file for more, but import order should be core-modules, 3rd party modules, and then local modules with each section separated by a line. Also, I don't think you are using re?

review: Needs Fixing
Revision history for this message
Naveed Massjouni (ironcamel) wrote :

> Looks good! I definitely think the view building is a lot cleaner now. One
> thing:
>
> 119-122: See the HACKING file for more, but import order should be core-
> modules, 3rd party modules, and then local modules with each section separated
> by a line. Also, I don't think you are using re?

Fixed.

Revision history for this message
Naveed Massjouni (ironcamel) wrote :

> Can you please read over Pep-257 for docstring conventions?
> http://www.python.org/dev/peps/pep-0257/

Thanks for pointing me to PEP 257. Fixed.

Revision history for this message
Todd Willey (xtoddx) :
review: Approve
Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

137, 148 - use super (new style classes)
APIRouter._setup_routes(self, mapper) should be
super(APIRouterV10, self)._setup_routes(mapper) / super(APIRouterV11, self)._setup_routes(mapper)

Otherwise, looks good!

review: Needs Fixing
Revision history for this message
Naveed Massjouni (ironcamel) wrote :

> 137, 148 - use super (new style classes)
> APIRouter._setup_routes(self, mapper) should be
> super(APIRouterV10, self)._setup_routes(mapper) / super(APIRouterV11,
> self)._setup_routes(mapper)

Fixed.

>
> Otherwise, looks good!

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

lgtm

review: Approve
815. By Naveed Massjouni

Should not call super __init__ twice in APIRouter

816. By Naveed Massjouni

Merge from trunk

817. By Naveed Massjouni

Merge from trunk

818. By Naveed Massjouni

Fixed the docstring for common.get_id_from_href

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/api-paste.ini'
2--- etc/api-paste.ini 2011-03-18 17:16:14 +0000
3+++ etc/api-paste.ini 2011-03-23 18:10:37 +0000
4@@ -67,11 +67,14 @@
5 [composite:osapi]
6 use = egg:Paste#urlmap
7 /: osversions
8-/v1.0: openstackapi
9-/v1.1: openstackapi
10-
11-[pipeline:openstackapi]
12-pipeline = faultwrap auth ratelimit osapiapp
13+/v1.0: openstackapi10
14+/v1.1: openstackapi11
15+
16+[pipeline:openstackapi10]
17+pipeline = faultwrap auth ratelimit osapiapp10
18+
19+[pipeline:openstackapi11]
20+pipeline = faultwrap auth ratelimit osapiapp11
21
22 [filter:faultwrap]
23 paste.filter_factory = nova.api.openstack:FaultWrapper.factory
24@@ -82,8 +85,11 @@
25 [filter:ratelimit]
26 paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory
27
28-[app:osapiapp]
29-paste.app_factory = nova.api.openstack:APIRouter.factory
30+[app:osapiapp10]
31+paste.app_factory = nova.api.openstack:APIRouterV10.factory
32+
33+[app:osapiapp11]
34+paste.app_factory = nova.api.openstack:APIRouterV11.factory
35
36 [pipeline:osversions]
37 pipeline = faultwrap osversionapp
38
39=== modified file 'nova/api/openstack/__init__.py'
40--- nova/api/openstack/__init__.py 2011-03-18 17:16:14 +0000
41+++ nova/api/openstack/__init__.py 2011-03-23 18:10:37 +0000
42@@ -72,9 +72,14 @@
43 return cls()
44
45 def __init__(self):
46+ self.server_members = {}
47 mapper = routes.Mapper()
48+ self._setup_routes(mapper)
49+ super(APIRouter, self).__init__(mapper)
50
51- server_members = {'action': 'POST'}
52+ def _setup_routes(self, mapper):
53+ server_members = self.server_members
54+ server_members['action'] = 'POST'
55 if FLAGS.allow_admin_api:
56 LOG.debug(_("Including admin operations in API."))
57
58@@ -99,10 +104,6 @@
59 controller=accounts.Controller(),
60 collection={'detail': 'GET'})
61
62- mapper.resource("server", "servers", controller=servers.Controller(),
63- collection={'detail': 'GET'},
64- member=server_members)
65-
66 mapper.resource("backup_schedule", "backup_schedule",
67 controller=backup_schedules.Controller(),
68 parent_resource=dict(member_name='server',
69@@ -129,6 +130,28 @@
70 super(APIRouter, self).__init__(mapper)
71
72
73+class APIRouterV10(APIRouter):
74+ """Define routes specific to OpenStack API V1.0."""
75+
76+ def _setup_routes(self, mapper):
77+ super(APIRouterV10, self)._setup_routes(mapper)
78+ mapper.resource("server", "servers",
79+ controller=servers.ControllerV10(),
80+ collection={'detail': 'GET'},
81+ member=self.server_members)
82+
83+
84+class APIRouterV11(APIRouter):
85+ """Define routes specific to OpenStack API V1.1."""
86+
87+ def _setup_routes(self, mapper):
88+ super(APIRouterV11, self)._setup_routes(mapper)
89+ mapper.resource("server", "servers",
90+ controller=servers.ControllerV11(),
91+ collection={'detail': 'GET'},
92+ member=self.server_members)
93+
94+
95 class Versions(wsgi.Application):
96 @webob.dec.wsgify(RequestClass=wsgi.Request)
97 def __call__(self, req):
98
99=== modified file 'nova/api/openstack/auth.py'
100--- nova/api/openstack/auth.py 2011-03-17 17:42:42 +0000
101+++ nova/api/openstack/auth.py 2011-03-23 18:10:37 +0000
102@@ -69,8 +69,6 @@
103 return faults.Fault(webob.exc.HTTPUnauthorized())
104
105 req.environ['nova.context'] = context.RequestContext(user, account)
106- version = req.path.split('/')[1].replace('v', '')
107- req.environ['api.version'] = version
108 return self.application
109
110 def has_authentication(self, req):
111
112=== modified file 'nova/api/openstack/common.py'
113--- nova/api/openstack/common.py 2011-03-16 01:26:45 +0000
114+++ nova/api/openstack/common.py 2011-03-23 18:10:37 +0000
115@@ -15,7 +15,9 @@
116 # License for the specific language governing permissions and limitations
117 # under the License.
118
119-import webob.exc
120+from urlparse import urlparse
121+
122+import webob
123
124 from nova import exception
125
126@@ -76,5 +78,14 @@
127 raise exception.NotFound(image_hash)
128
129
130-def get_api_version(req):
131- return req.environ.get('api.version')
132+def get_id_from_href(href):
133+ """Return the id portion of a url.
134+
135+ Given: http://www.foo.com/bar/123?q=4
136+ Returns: 4
137+
138+ """
139+ try:
140+ return int(urlparse(href).path.split('/')[-1])
141+ except:
142+ raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
143
144=== modified file 'nova/api/openstack/servers.py'
145--- nova/api/openstack/servers.py 2011-03-17 22:24:08 +0000
146+++ nova/api/openstack/servers.py 2011-03-23 18:10:37 +0000
147@@ -29,8 +29,9 @@
148 from nova import utils
149 from nova.api.openstack import common
150 from nova.api.openstack import faults
151-from nova.api.openstack.views import servers as servers_views
152-from nova.api.openstack.views import addresses as addresses_views
153+import nova.api.openstack.views.addresses
154+import nova.api.openstack.views.flavors
155+import nova.api.openstack.views.servers
156 from nova.auth import manager as auth_manager
157 from nova.compute import instance_types
158 from nova.compute import power_state
159@@ -63,7 +64,7 @@
160 except exception.NotFound:
161 return faults.Fault(exc.HTTPNotFound())
162
163- builder = addresses_views.get_view_builder(req)
164+ builder = self._get_addresses_view_builder(req)
165 return builder.build(instance)
166
167 def index(self, req):
168@@ -81,7 +82,7 @@
169 """
170 instance_list = self.compute_api.get_all(req.environ['nova.context'])
171 limited_list = common.limited(instance_list, req)
172- builder = servers_views.get_view_builder(req)
173+ builder = self._get_view_builder(req)
174 servers = [builder.build(inst, is_detail)['server']
175 for inst in limited_list]
176 return dict(servers=servers)
177@@ -90,7 +91,7 @@
178 """ Returns server details by server id """
179 try:
180 instance = self.compute_api.get(req.environ['nova.context'], id)
181- builder = servers_views.get_view_builder(req)
182+ builder = self._get_view_builder(req)
183 return builder.build(instance, is_detail=True)
184 except exception.NotFound:
185 return faults.Fault(exc.HTTPNotFound())
186@@ -119,8 +120,9 @@
187 key_name = key_pair['name']
188 key_data = key_pair['public_key']
189
190+ requested_image_id = self._image_id_from_req_data(env)
191 image_id = common.get_image_id_from_image_hash(self._image_service,
192- context, env['server']['imageId'])
193+ context, requested_image_id)
194 kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
195 req, image_id)
196
197@@ -139,10 +141,11 @@
198 if personality:
199 injected_files = self._get_injected_files(personality)
200
201+ flavor_id = self._flavor_id_from_req_data(env)
202 try:
203- instances = self.compute_api.create(
204+ (inst,) = self.compute_api.create(
205 context,
206- instance_types.get_by_flavor_id(env['server']['flavorId']),
207+ instance_types.get_by_flavor_id(flavor_id),
208 image_id,
209 kernel_id=kernel_id,
210 ramdisk_id=ramdisk_id,
211@@ -155,8 +158,11 @@
212 except QuotaError as error:
213 self._handle_quota_errors(error)
214
215- builder = servers_views.get_view_builder(req)
216- server = builder.build(instances[0], is_detail=False)
217+ inst['instance_type'] = flavor_id
218+ inst['image_id'] = requested_image_id
219+
220+ builder = self._get_view_builder(req)
221+ server = builder.build(inst, is_detail=True)
222 password = "%s%s" % (server['server']['name'][:4],
223 utils.generate_password(12))
224 server['server']['adminPass'] = password
225@@ -511,6 +517,45 @@
226 return kernel_id, ramdisk_id
227
228
229+class ControllerV10(Controller):
230+ def _image_id_from_req_data(self, data):
231+ return data['server']['imageId']
232+
233+ def _flavor_id_from_req_data(self, data):
234+ return data['server']['flavorId']
235+
236+ def _get_view_builder(self, req):
237+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
238+ return nova.api.openstack.views.servers.ViewBuilderV10(
239+ addresses_builder)
240+
241+ def _get_addresses_view_builder(self, req):
242+ return nova.api.openstack.views.addresses.ViewBuilderV10(req)
243+
244+
245+class ControllerV11(Controller):
246+ def _image_id_from_req_data(self, data):
247+ href = data['server']['imageRef']
248+ return common.get_id_from_href(href)
249+
250+ def _flavor_id_from_req_data(self, data):
251+ href = data['server']['flavorRef']
252+ return common.get_id_from_href(href)
253+
254+ def _get_view_builder(self, req):
255+ base_url = req.application_url
256+ flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
257+ base_url)
258+ image_builder = nova.api.openstack.views.images.ViewBuilderV11(
259+ base_url)
260+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
261+ return nova.api.openstack.views.servers.ViewBuilderV11(
262+ addresses_builder, flavor_builder, image_builder)
263+
264+ def _get_addresses_view_builder(self, req):
265+ return nova.api.openstack.views.addresses.ViewBuilderV11(req)
266+
267+
268 class ServerCreateRequestXMLDeserializer(object):
269 """
270 Deserializer to handle xml-formatted server create requests.
271
272=== modified file 'nova/api/openstack/views/addresses.py'
273--- nova/api/openstack/views/addresses.py 2011-03-15 23:55:13 +0000
274+++ nova/api/openstack/views/addresses.py 2011-03-23 18:10:37 +0000
275@@ -19,18 +19,6 @@
276 from nova.api.openstack import common
277
278
279-def get_view_builder(req):
280- '''
281- A factory method that returns the correct builder based on the version of
282- the api requested.
283- '''
284- version = common.get_api_version(req)
285- if version == '1.1':
286- return ViewBuilder_1_1()
287- else:
288- return ViewBuilder_1_0()
289-
290-
291 class ViewBuilder(object):
292 ''' Models a server addresses response as a python dictionary.'''
293
294@@ -38,14 +26,14 @@
295 raise NotImplementedError()
296
297
298-class ViewBuilder_1_0(ViewBuilder):
299+class ViewBuilderV10(ViewBuilder):
300 def build(self, inst):
301 private_ips = utils.get_from_path(inst, 'fixed_ip/address')
302 public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
303 return dict(public=public_ips, private=private_ips)
304
305
306-class ViewBuilder_1_1(ViewBuilder):
307+class ViewBuilderV11(ViewBuilder):
308 def build(self, inst):
309 private_ips = utils.get_from_path(inst, 'fixed_ip/address')
310 private_ips = [dict(version=4, addr=a) for a in private_ips]
311
312=== modified file 'nova/api/openstack/views/flavors.py'
313--- nova/api/openstack/views/flavors.py 2011-03-16 01:26:45 +0000
314+++ nova/api/openstack/views/flavors.py 2011-03-23 18:10:37 +0000
315@@ -18,19 +18,6 @@
316 from nova.api.openstack import common
317
318
319-def get_view_builder(req):
320- '''
321- A factory method that returns the correct builder based on the version of
322- the api requested.
323- '''
324- version = common.get_api_version(req)
325- base_url = req.application_url
326- if version == '1.1':
327- return ViewBuilder_1_1(base_url)
328- else:
329- return ViewBuilder_1_0()
330-
331-
332 class ViewBuilder(object):
333 def __init__(self):
334 pass
335@@ -39,13 +26,9 @@
336 raise NotImplementedError()
337
338
339-class ViewBuilder_1_1(ViewBuilder):
340+class ViewBuilderV11(ViewBuilder):
341 def __init__(self, base_url):
342 self.base_url = base_url
343
344 def generate_href(self, flavor_id):
345 return "%s/flavors/%s" % (self.base_url, flavor_id)
346-
347-
348-class ViewBuilder_1_0(ViewBuilder):
349- pass
350
351=== modified file 'nova/api/openstack/views/images.py'
352--- nova/api/openstack/views/images.py 2011-03-16 01:26:45 +0000
353+++ nova/api/openstack/views/images.py 2011-03-23 18:10:37 +0000
354@@ -18,19 +18,6 @@
355 from nova.api.openstack import common
356
357
358-def get_view_builder(req):
359- '''
360- A factory method that returns the correct builder based on the version of
361- the api requested.
362- '''
363- version = common.get_api_version(req)
364- base_url = req.application_url
365- if version == '1.1':
366- return ViewBuilder_1_1(base_url)
367- else:
368- return ViewBuilder_1_0()
369-
370-
371 class ViewBuilder(object):
372 def __init__(self):
373 pass
374@@ -39,13 +26,9 @@
375 raise NotImplementedError()
376
377
378-class ViewBuilder_1_1(ViewBuilder):
379+class ViewBuilderV11(ViewBuilder):
380 def __init__(self, base_url):
381 self.base_url = base_url
382
383 def generate_href(self, image_id):
384 return "%s/images/%s" % (self.base_url, image_id)
385-
386-
387-class ViewBuilder_1_0(ViewBuilder):
388- pass
389
390=== modified file 'nova/api/openstack/views/servers.py'
391--- nova/api/openstack/views/servers.py 2011-03-15 23:55:13 +0000
392+++ nova/api/openstack/views/servers.py 2011-03-23 18:10:37 +0000
393@@ -24,45 +24,30 @@
394 from nova import utils
395
396
397-def get_view_builder(req):
398- '''
399- A factory method that returns the correct builder based on the version of
400- the api requested.
401- '''
402- version = common.get_api_version(req)
403- addresses_builder = addresses_view.get_view_builder(req)
404- if version == '1.1':
405- flavor_builder = flavors_view.get_view_builder(req)
406- image_builder = images_view.get_view_builder(req)
407- return ViewBuilder_1_1(addresses_builder, flavor_builder,
408- image_builder)
409- else:
410- return ViewBuilder_1_0(addresses_builder)
411-
412-
413 class ViewBuilder(object):
414- '''
415- Models a server response as a python dictionary.
416+ """Model a server response as a python dictionary.
417+
418+ Public methods: build
419 Abstract methods: _build_image, _build_flavor
420- '''
421+
422+ """
423
424 def __init__(self, addresses_builder):
425 self.addresses_builder = addresses_builder
426
427 def build(self, inst, is_detail):
428- """
429- Coerces into dictionary format, mapping everything to
430- Rackspace-like attributes for return
431- """
432+ """Return a dict that represenst a server."""
433 if is_detail:
434 return self._build_detail(inst)
435 else:
436 return self._build_simple(inst)
437
438 def _build_simple(self, inst):
439- return dict(server=dict(id=inst['id'], name=inst['display_name']))
440+ """Return a simple model of a server."""
441+ return dict(server=dict(id=inst['id'], name=inst['display_name']))
442
443 def _build_detail(self, inst):
444+ """Returns a detailed model of a server."""
445 power_mapping = {
446 None: 'build',
447 power_state.NOSTATE: 'build',
448@@ -76,25 +61,19 @@
449 power_state.FAILED: 'error'}
450 inst_dict = {}
451
452- #mapped_keys = dict(status='state', imageId='image_id',
453- # flavorId='instance_type', name='display_name', id='id')
454-
455- mapped_keys = dict(status='state', name='display_name', id='id')
456-
457- for k, v in mapped_keys.iteritems():
458- inst_dict[k] = inst[v]
459-
460- inst_dict['status'] = power_mapping[inst_dict['status']]
461+ inst_dict['id'] = int(inst['id'])
462+ inst_dict['name'] = inst['display_name']
463+ inst_dict['status'] = power_mapping[inst.get('state')]
464 inst_dict['addresses'] = self.addresses_builder.build(inst)
465
466 # Return the metadata as a dictionary
467 metadata = {}
468- for item in inst['metadata']:
469+ for item in inst.get('metadata', []):
470 metadata[item['key']] = item['value']
471 inst_dict['metadata'] = metadata
472
473 inst_dict['hostId'] = ''
474- if inst['host']:
475+ if inst.get('host'):
476 inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest()
477
478 self._build_image(inst_dict, inst)
479@@ -103,21 +82,27 @@
480 return dict(server=inst_dict)
481
482 def _build_image(self, response, inst):
483+ """Return the image sub-resource of a server."""
484 raise NotImplementedError()
485
486 def _build_flavor(self, response, inst):
487+ """Return the flavor sub-resource of a server."""
488 raise NotImplementedError()
489
490
491-class ViewBuilder_1_0(ViewBuilder):
492+class ViewBuilderV10(ViewBuilder):
493+ """Model an Openstack API V1.0 server response."""
494+
495 def _build_image(self, response, inst):
496- response["imageId"] = inst["image_id"]
497+ response['imageId'] = inst['image_id']
498
499 def _build_flavor(self, response, inst):
500- response["flavorId"] = inst["instance_type"]
501-
502-
503-class ViewBuilder_1_1(ViewBuilder):
504+ response['flavorId'] = inst['instance_type']
505+
506+
507+class ViewBuilderV11(ViewBuilder):
508+ """Model an Openstack API V1.0 server response."""
509+
510 def __init__(self, addresses_builder, flavor_builder, image_builder):
511 ViewBuilder.__init__(self, addresses_builder)
512 self.flavor_builder = flavor_builder
513
514=== modified file 'nova/tests/api/openstack/fakes.py'
515--- nova/tests/api/openstack/fakes.py 2011-03-18 18:07:24 +0000
516+++ nova/tests/api/openstack/fakes.py 2011-03-23 18:10:37 +0000
517@@ -72,14 +72,18 @@
518 return self.application
519
520
521-def wsgi_app(inner_application=None):
522- if not inner_application:
523- inner_application = openstack.APIRouter()
524+def wsgi_app(inner_app10=None, inner_app11=None):
525+ if not inner_app10:
526+ inner_app10 = openstack.APIRouterV10()
527+ if not inner_app11:
528+ inner_app11 = openstack.APIRouterV11()
529 mapper = urlmap.URLMap()
530- api = openstack.FaultWrapper(auth.AuthMiddleware(
531- limits.RateLimitingMiddleware(inner_application)))
532- mapper['/v1.0'] = api
533- mapper['/v1.1'] = api
534+ api10 = openstack.FaultWrapper(auth.AuthMiddleware(
535+ limits.RateLimitingMiddleware(inner_app10)))
536+ api11 = openstack.FaultWrapper(auth.AuthMiddleware(
537+ limits.RateLimitingMiddleware(inner_app11)))
538+ mapper['/v1.0'] = api10
539+ mapper['/v1.1'] = api11
540 mapper['/'] = openstack.FaultWrapper(openstack.Versions())
541 return mapper
542
543
544=== modified file 'nova/tests/api/openstack/test_auth.py'
545--- nova/tests/api/openstack/test_auth.py 2011-03-20 19:04:51 +0000
546+++ nova/tests/api/openstack/test_auth.py 2011-03-23 18:10:37 +0000
547@@ -83,8 +83,7 @@
548 self.assertEqual(result.headers['X-Storage-Url'], "")
549
550 token = result.headers['X-Auth-Token']
551- self.stubs.Set(nova.api.openstack, 'APIRouter',
552- fakes.FakeRouter)
553+ self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
554 req = webob.Request.blank('/v1.0/fake')
555 req.headers['X-Auth-Token'] = token
556 result = req.get_response(fakes.wsgi_app())
557@@ -201,8 +200,7 @@
558 self.assertEqual(len(result.headers['X-Auth-Token']), 40)
559
560 token = result.headers['X-Auth-Token']
561- self.stubs.Set(nova.api.openstack, 'APIRouter',
562- fakes.FakeRouter)
563+ self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
564 req = webob.Request.blank('/v1.0/fake')
565 req.method = 'POST'
566 req.headers['X-Auth-Token'] = token
567
568=== modified file 'nova/tests/api/openstack/test_servers.py'
569--- nova/tests/api/openstack/test_servers.py 2011-03-20 19:06:22 +0000
570+++ nova/tests/api/openstack/test_servers.py 2011-03-23 18:10:37 +0000
571@@ -161,7 +161,7 @@
572 req = webob.Request.blank('/v1.0/servers/1')
573 res = req.get_response(fakes.wsgi_app())
574 res_dict = json.loads(res.body)
575- self.assertEqual(res_dict['server']['id'], '1')
576+ self.assertEqual(res_dict['server']['id'], 1)
577 self.assertEqual(res_dict['server']['name'], 'server1')
578
579 def test_get_server_by_id_with_addresses(self):
580@@ -172,7 +172,7 @@
581 req = webob.Request.blank('/v1.0/servers/1')
582 res = req.get_response(fakes.wsgi_app())
583 res_dict = json.loads(res.body)
584- self.assertEqual(res_dict['server']['id'], '1')
585+ self.assertEqual(res_dict['server']['id'], 1)
586 self.assertEqual(res_dict['server']['name'], 'server1')
587 addresses = res_dict['server']['addresses']
588 self.assertEqual(len(addresses["public"]), len(public))
589@@ -180,7 +180,7 @@
590 self.assertEqual(len(addresses["private"]), 1)
591 self.assertEqual(addresses["private"][0], private)
592
593- def test_get_server_by_id_with_addresses_v1_1(self):
594+ def test_get_server_by_id_with_addresses_v11(self):
595 private = "192.168.0.3"
596 public = ["1.2.3.4"]
597 new_return_server = return_server_with_addresses(private, public)
598@@ -189,7 +189,7 @@
599 req.environ['api.version'] = '1.1'
600 res = req.get_response(fakes.wsgi_app())
601 res_dict = json.loads(res.body)
602- self.assertEqual(res_dict['server']['id'], '1')
603+ self.assertEqual(res_dict['server']['id'], 1)
604 self.assertEqual(res_dict['server']['name'], 'server1')
605 addresses = res_dict['server']['addresses']
606 self.assertEqual(len(addresses["public"]), len(public))
607@@ -239,7 +239,7 @@
608 servers = json.loads(res.body)['servers']
609 self.assertEqual([s['id'] for s in servers], [1, 2])
610
611- def _test_create_instance_helper(self):
612+ def _setup_for_create_instance(self):
613 """Shared implementation for tests below that create instance"""
614 def instance_create(context, inst):
615 return {'id': '1', 'display_name': 'server_test'}
616@@ -276,14 +276,17 @@
617 self.stubs.Set(nova.api.openstack.common,
618 "get_image_id_from_image_hash", image_id_from_hash)
619
620+ def _test_create_instance_helper(self):
621+ self._setup_for_create_instance()
622+
623 body = dict(server=dict(
624- name='server_test', imageId=2, flavorId=2,
625+ name='server_test', imageId=3, flavorId=2,
626 metadata={'hello': 'world', 'open': 'stack'},
627 personality={}))
628 req = webob.Request.blank('/v1.0/servers')
629 req.method = 'POST'
630 req.body = json.dumps(body)
631- req.headers["Content-Type"] = "application/json"
632+ req.headers["content-type"] = "application/json"
633
634 res = req.get_response(fakes.wsgi_app())
635
636@@ -291,8 +294,9 @@
637 self.assertEqual('serv', server['adminPass'][:4])
638 self.assertEqual(16, len(server['adminPass']))
639 self.assertEqual('server_test', server['name'])
640- self.assertEqual('1', server['id'])
641-
642+ self.assertEqual(1, server['id'])
643+ self.assertEqual(2, server['flavorId'])
644+ self.assertEqual(3, server['imageId'])
645 self.assertEqual(res.status_int, 200)
646
647 def test_create_instance(self):
648@@ -302,6 +306,56 @@
649 fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False)
650 self._test_create_instance_helper()
651
652+ def test_create_instance_v11(self):
653+ self._setup_for_create_instance()
654+
655+ imageRef = 'http://localhost/v1.1/images/2'
656+ flavorRef = 'http://localhost/v1.1/flavors/3'
657+ body = {
658+ 'server': {
659+ 'name': 'server_test',
660+ 'imageRef': imageRef,
661+ 'flavorRef': flavorRef,
662+ 'metadata': {
663+ 'hello': 'world',
664+ 'open': 'stack',
665+ },
666+ 'personality': {},
667+ },
668+ }
669+
670+ req = webob.Request.blank('/v1.1/servers')
671+ req.method = 'POST'
672+ req.body = json.dumps(body)
673+ req.headers["content-type"] = "application/json"
674+
675+ res = req.get_response(fakes.wsgi_app())
676+
677+ server = json.loads(res.body)['server']
678+ self.assertEqual('serv', server['adminPass'][:4])
679+ self.assertEqual(16, len(server['adminPass']))
680+ self.assertEqual('server_test', server['name'])
681+ self.assertEqual(1, server['id'])
682+ self.assertEqual(flavorRef, server['flavorRef'])
683+ self.assertEqual(imageRef, server['imageRef'])
684+ self.assertEqual(res.status_int, 200)
685+
686+ def test_create_instance_v11_bad_href(self):
687+ self._setup_for_create_instance()
688+
689+ imageRef = 'http://localhost/v1.1/images/asdf'
690+ flavorRef = 'http://localhost/v1.1/flavors/3'
691+ body = dict(server=dict(
692+ name='server_test', imageRef=imageRef, flavorRef=flavorRef,
693+ metadata={'hello': 'world', 'open': 'stack'},
694+ personality={}))
695+ req = webob.Request.blank('/v1.1/servers')
696+ req.method = 'POST'
697+ req.body = json.dumps(body)
698+ req.headers["content-type"] = "application/json"
699+ res = req.get_response(fakes.wsgi_app())
700+ self.assertEqual(res.status_int, 400)
701+
702 def test_update_no_body(self):
703 req = webob.Request.blank('/v1.0/servers/1')
704 req.method = 'PUT'