Merge lp:~rackspace-titan/nova/osapi-serialization into lp:~hudson-openstack/nova/trunk

Proposed by Brian Waldon
Status: Merged
Approved by: Dan Prince
Approved revision: 1088
Merged at revision: 1132
Proposed branch: lp:~rackspace-titan/nova/osapi-serialization
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 3221 lines (+1177/-827)
30 files modified
nova/api/direct.py (+8/-4)
nova/api/openstack/__init__.py (+23/-20)
nova/api/openstack/accounts.py (+21/-12)
nova/api/openstack/backup_schedules.py (+18/-9)
nova/api/openstack/common.py (+0/-7)
nova/api/openstack/consoles.py (+18/-11)
nova/api/openstack/contrib/volumes.py (+10/-13)
nova/api/openstack/extensions.py (+58/-41)
nova/api/openstack/faults.py (+23/-16)
nova/api/openstack/flavors.py (+23/-15)
nova/api/openstack/image_metadata.py (+12/-8)
nova/api/openstack/images.py (+33/-19)
nova/api/openstack/ips.py (+22/-15)
nova/api/openstack/limits.py (+34/-15)
nova/api/openstack/server_metadata.py (+15/-10)
nova/api/openstack/servers.py (+68/-64)
nova/api/openstack/shared_ip_groups.py (+10/-22)
nova/api/openstack/users.py (+26/-17)
nova/api/openstack/versions.py (+21/-26)
nova/api/openstack/wsgi.py (+380/-0)
nova/api/openstack/zones.py (+21/-12)
nova/objectstore/s3server.py (+1/-1)
nova/tests/api/openstack/extensions/foxinsocks.py (+1/-3)
nova/tests/api/openstack/test_extensions.py (+2/-2)
nova/tests/api/openstack/test_limits.py (+2/-2)
nova/tests/api/openstack/test_servers.py (+29/-23)
nova/tests/api/openstack/test_wsgi.py (+293/-0)
nova/tests/api/test_wsgi.py (+0/-189)
nova/tests/integrated/test_xml.py (+2/-2)
nova/wsgi.py (+3/-249)
To merge this branch: bzr merge lp:~rackspace-titan/nova/osapi-serialization
Reviewer Review Type Date Requested Status
Dan Prince (community) Approve
Ed Leafe (community) Approve
Mark Washenberger (community) Approve
Review via email: mp+61656@code.launchpad.net

Description of the change

- move osapi-specific wsgi code from nova/wsgi.py to nova/api/openstack/wsgi.py
- refactor wsgi modules to use more object-oriented approach to wsgi request handling:
    - Resource object steps up to original Controller position
    - Resource coordinates deserialization, dispatch to controller, serialization
    - serialization and deserialization broken down to be more testable/flexible

- this will definitely help fixing current serialization-related bugs
- this paves the way for schema validation to be implemented elegantly

To post a comment you must log in.
Revision history for this message
Mark Washenberger (markwash) wrote :

Overall, I'm quite pleased. This is very much the direction I was hoping to go with the serialization blueprint and you've done a good job of handling some of the trickier details. This approach is going to be a big help as we go move towards adding better support for xml requests in particular.

- Thanks for giving my nasty one-off deserializer for server create requests a home.

- I look forward to seeing how we can build on this with request validation.

Nits, Information, and Issues:

- I like the convenience functions for creating Resources for each entry in the mapper. But a factory is usually an object, not a function. Maybe the controller module functions should be called 'get_resource()' or 'create_resource()' ?

- I see that the approach we take with extensions leans on the serialization components you are changing. However, I think it might be better if we don't reuse Resources for extensions. Rather, we might want to reuse the serializer and deserializer directly. Does this sound feasible?

- It seems like an unnecessary duplication to pass in both the request object and the deserialized body of that request to the controller. Mostly it seems like controllers use the request object to get access to the nova.context object. Perhaps we could extract context from the request and just pass in the context and the body? If that is the case, we could also rename the body variable to "request" which would be more intuitive to me. This change might cause problems with the extensions, which seem to need access to the full request--which is partly why I propose that we pull Resource out of the extensions module.

- I think you have some conflicts to resolve from the request extensions branch that just merged into trunk.

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

 > - I like the convenience functions for creating Resources for each entry in
> the mapper. But a factory is usually an object, not a function. Maybe the
> controller module functions should be called 'get_resource()' or
> 'create_resource()' ?

Good point. I renamed resource_factory to create_resource.

> - I see that the approach we take with extensions leans on the serialization
> components you are changing. However, I think it might be better if we don't
> reuse Resources for extensions. Rather, we might want to reuse the serializer
> and deserializer directly. Does this sound feasible?
>
> - It seems like an unnecessary duplication to pass in both the request object
> and the deserialized body of that request to the controller. Mostly it seems
> like controllers use the request object to get access to the nova.context
> object. Perhaps we could extract context from the request and just pass in the
> context and the body? If that is the case, we could also rename the body
> variable to "request" which would be more intuitive to me. This change might
> cause problems with the extensions, which seem to need access to the full
> request--which is partly why I propose that we pull Resource out of the
> extensions module.

I am definitely for all this, but I feel it is out of scope. After addressing how much will have to be refactored to accomplish the goal here, I would love to do this as a separate branch. Thoughts?

> - I think you have some conflicts to resolve from the request extensions
> branch that just merged into trunk.

Took care of this.

I also removed some obsolete code in a few places.

Revision history for this message
Mark Washenberger (markwash) wrote :

> I am definitely for all this, but I feel it is out of scope. After addressing
> how much will have to be refactored to accomplish the goal here, I would love
> to do this as a separate branch. Thoughts?

I agree--it looks like the controller functions are making enough diverse usage
of the request object itself that it is hard to find a good seam for this change
right now.

Apart from all that, this looks great to me. It very much accomplishes the goal that I can add a new controller with arbitrary requests and responses without having to shoe-horn them into the existing default xml serialization code.

review: Approve
1085. By Brian Waldon

merging trunk

Revision history for this message
Ed Leafe (ed-leafe) wrote :

The try/except structure in diff lines 1865-68 is awkward: only the lines that could cause the exception should be in the block. Also, there is a mix of the try/except approach and the 'if' test approach for checking keys. I recommend sticking with try/except, using something more like:

    def get_action_args(self, request_environment):
        """Parse dictionary created by routes library."""
        args = request_environment['wsgiorg.routing_args'][1].copy()
        try:
            del args['format']
        except KeyError:
            pass
        try:
            del args['controller']
        except KeyError:
            return {}
        return args
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Line 1878: Bare excepts and/or catching the base Exception class should not be used. Catch the specific exception type, which in this case is probably an AttrbuteError. If that's correct, the whole structure could be written more simply as:
    action_method = getattr(self, action, self.default)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Diff lines 1928 & 1946 should not be comparing type(x) to a type; instead, use isinstance().
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Diff line 2003: should be catching KeyError, not a base Exception.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Diff line 2034: multiple format placeholders should use the mapping style. See https://bugs.launchpad.net/nova/+bug/703041 for explanation.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2045: use isinstance for type checking
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2994: grammar nit: change to: "well and have your controller be a controller that will route"
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

A more general comment about formatting: there are multiple places where formatting seems more apropos to Javascript or C++ than Python; one example:

    def test_get_action_args(self):
        env = {
            'wsgiorg.routing_args': [None, {
                'controller': None,
                'format': None,
                'action': 'update',
                'id': 12,
            }],
        }

Normally continued lines are to be minimized, and when necessary, should be at a consistent level of indentation. Most typical is 2 levels of indentation, but there are other 'standards', and it's most important to be consistent to make the code readable. Outdenting closing brackets/parens is discouraged in Python. You don't need to change the code, since this style is so prevalent in these scripts, but in the future we should strive for a more Pythonic and less Javascript-y style.

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :
Download full text (4.3 KiB)

> The try/except structure in diff lines 1865-68 is awkward: only the lines that
> could cause the exception should be in the block. Also, there is a mix of the
> try/except approach and the 'if' test approach for checking keys. I recommend
> sticking with try/except, using something more like:
>
> def get_action_args(self, request_environment):
> """Parse dictionary created by routes library."""
> args = request_environment['wsgiorg.routing_args'][1].copy()
> try:
> del args['format']
> except KeyError:
> pass
> try:
> del args['controller']
> except KeyError:
> return {}
> return args

Consistency is definitely a good thing to have. I updated the conditionals you mentioned with try/except blocks.

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> Line 1878: Bare excepts and/or catching the base Exception class should not be
> used. Catch the specific exception type, which in this case is probably an
> AttrbuteError. If that's correct, the whole structure could be written more
> simply as:
> action_method = getattr(self, action, self.default)

Good point. I typically use Exception when I don't know what exactly I may need to catch. Like you said, in this case I will only have to catch AttributeError. I changed the "except Exception" cases to be more specific across the module.

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> Diff lines 1928 & 1946 should not be comparing type(x) to a type; instead, use
> isinstance().

So these are "legacy" conditionals that have been working here since before I was on the project. This branch is targeted at refactoring the organization of the wsgi-related objects, not rewriting our generic xml serialization. After hearing that, are you okay with me leaving it?

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> Diff line 2003: should be catching KeyError, not a base Exception.

Fixed.

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> Diff line 2034: multiple format placeholders should use the mapping style. See
> https://bugs.launchpad.net/nova/+bug/703041 for explanation.

Well this string isn't i18n'd, but I did it for you anyways :)

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> 2045: use isinstance for type checking

This is another one of those legacy conditionals. I would love to solve this more elegantly in a future branch, possibly once we decide if we are going towards objects for our data models (instead of the dict it is checking for now).

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> 2994: grammar nit: change to: "well and have your controller be a controller
> that will route"

I should have caught that. Cleaned it up a bit.

> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
>
> A more general comment about formatting: there are multiple places where
> formatting seems more apropos to Javascript or C++ than Python; one example:
>
> def test_get_action_args(self):
> ...

Read more...

1086. By Brian Waldon

review fixups

Revision history for this message
Ed Leafe (ed-leafe) wrote :

>> Diff lines 1928 & 1946 should not be comparing type(x) to a type; instead, use
>> isinstance().

> So these are "legacy" conditionals that have been working here since before I was
> on the project. This branch is targeted at refactoring the organization of the
> wsgi-related objects, not rewriting our generic xml serialization. After hearing
> that, are you okay with me leaving it?

If you add a #TODO to those lines, sure.

>> If that's correct, the whole structure could be written more
>> simply as:
>> action_method = getattr(self, action, self.default)

> Good point. I typically use Exception when I don't know what exactly I may need to catch.

Trick: what you do in those cases is write it:
    except Exception as e:
        print "EXCEPTION", type(e)
...and then run the test for that code, passing in the bad condition that would trigger the exception. Once it's triggered, you'll know the type, and can change the code to catch the correct exception class.

I still think that you should change the lines (1787 and 1881) from:
    try:
        action_method = getattr(self, action)
    except (AttributeError, TypeError):
        action_method = self.default
to:
    action_method = getattr(self, action, self.default)
After all, that's what the default parameter for getattr() is there for!

1087. By Brian Waldon

cleaning up getattr calls with default param

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

> >> Diff lines 1928 & 1946 should not be comparing type(x) to a type; instead,
> use
> >> isinstance().
>
> > So these are "legacy" conditionals that have been working here since before
> I was
> > on the project. This branch is targeted at refactoring the organization of
> the
> > wsgi-related objects, not rewriting our generic xml serialization. After
> hearing
> > that, are you okay with me leaving it?
>
> If you add a #TODO to those lines, sure.

I'm not sure what I would say the TODO is. I don't see a clear path to "fix" this, and I think most people could argue both sides of the type vs. isinstance debate.

> >> If that's correct, the whole structure could be written more
> >> simply as:
> >> action_method = getattr(self, action, self.default)
>
> > Good point. I typically use Exception when I don't know what exactly I may
> need to catch.
>
> Trick: what you do in those cases is write it:
> except Exception as e:
> print "EXCEPTION", type(e)
> ...and then run the test for that code, passing in the bad condition that
> would trigger the exception. Once it's triggered, you'll know the type, and
> can change the code to catch the correct exception class.

By not knowing the type, I am referring more to the case where literally anything could be thrown. For example, a nova.api.openstack.wsgi.Controller object could define its create method to throw literally anything.

> I still think that you should change the lines (1787 and 1881) from:
> try:
> action_method = getattr(self, action)
> except (AttributeError, TypeError):
> action_method = self.default
> to:
> action_method = getattr(self, action, self.default)
> After all, that's what the default parameter for getattr() is there for!

Wow, I need to read over the built-ins doc. I had no idea there was a default parameter. Fixed!

Revision history for this message
Ed Leafe (ed-leafe) wrote :

> > If you add a #TODO to those lines, sure.
>
> I'm not sure what I would say the TODO is. I don't see a clear path to "fix"
> this, and I think most people could argue both sides of the type vs.
> isinstance debate.

type() has known limitations with old-style classes; isinstance() does not. Additionally, type() check does not take into account subclassing; isinstance() does. There is no clean way to check for a string object with type() due to types.StringType and types.UnicodeType; you can use isinstance(val, basestring) to check for all string types.

So who are the people who are arguing both sides? :)

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

> type() has known limitations with old-style classes; isinstance() does not.
> Additionally, type() check does not take into account subclassing;
> isinstance() does. There is no clean way to check for a string object with
> type() due to types.StringType and types.UnicodeType; you can use
> isinstance(val, basestring) to check for all string types.

So the best thing to do in this case would be to use the built-in interfaces. First assume the 'data' variable is a dict and attempt to use it as such. When that doesn't work out, assume it is a list. When that doesn't work we end up just adding it as an xml text node. I'm going to stand by my point that I don't want to touch the code you are referring to since it is serving its purpose without issue. If someone wants to refactor the actual xml-specific serializer, I welcome it :)

I went to add a TODO on the third use of type(), and realized there is already a note there mentioning a need to refactor.

Revision history for this message
Ed Leafe (ed-leafe) wrote :

> So the best thing to do in this case would be to use the built-in interfaces.

No, the best thing to do would be to change:
    if type(data) is list:
to:
    if isinstance(data, list):

> First assume the 'data' variable is a dict and attempt to use it as such. When
> that doesn't work out, assume it is a list. When that doesn't work we end up
> just adding it as an xml text node. I'm going to stand by my point that I
> don't want to touch the code you are referring to since it is serving its
> purpose without issue. If someone wants to refactor the actual xml-specific
> serializer, I welcome it :)

Since the code is suboptimal but not incorrect, it is not imperative that it be changed, which is why I suggested simply adding a #TODO comment. I'm not sure why adding a comment about a better way to write the code counts as "touching" the code; that's what I thought the whole point of #TODOs were.

review: Approve
1088. By Brian Waldon

adding TODOs per dabo's review

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

Had a discussion with Ed and resolved the discussion above. Added the TODOs.

Revision history for this message
Dan Prince (dan-prince) wrote :

Approve. Looks good to me. Well done.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/direct.py'
2--- nova/api/direct.py 2011-04-11 16:34:19 +0000
3+++ nova/api/direct.py 2011-05-26 21:32:29 +0000
4@@ -42,6 +42,7 @@
5 from nova import flags
6 from nova import utils
7 from nova import wsgi
8+import nova.api.openstack.wsgi
9
10
11 # Global storage for registering modules.
12@@ -251,7 +252,7 @@
13 return self._methods[method]
14
15
16-class ServiceWrapper(wsgi.Controller):
17+class ServiceWrapper(object):
18 """Wrapper to dynamically povide a WSGI controller for arbitrary objects.
19
20 With lightweight introspection allows public methods on the object to
21@@ -265,7 +266,7 @@
22 def __init__(self, service_handle):
23 self.service_handle = service_handle
24
25- @webob.dec.wsgify(RequestClass=wsgi.Request)
26+ @webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request)
27 def __call__(self, req):
28 arg_dict = req.environ['wsgiorg.routing_args'][1]
29 action = arg_dict['action']
30@@ -289,8 +290,11 @@
31
32 try:
33 content_type = req.best_match_content_type()
34- default_xmlns = self.get_default_xmlns(req)
35- return self._serialize(result, content_type, default_xmlns)
36+ serializer = {
37+ 'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(),
38+ 'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
39+ }[content_type]
40+ return serializer.serialize(result)
41 except:
42 raise exception.Error("returned non-serializable type: %s"
43 % result)
44
45=== modified file 'nova/api/openstack/__init__.py'
46--- nova/api/openstack/__init__.py 2011-05-13 15:17:19 +0000
47+++ nova/api/openstack/__init__.py 2011-05-26 21:32:29 +0000
48@@ -26,7 +26,7 @@
49
50 from nova import flags
51 from nova import log as logging
52-from nova import wsgi
53+from nova import wsgi as base_wsgi
54 from nova.api.openstack import accounts
55 from nova.api.openstack import faults
56 from nova.api.openstack import backup_schedules
57@@ -40,6 +40,7 @@
58 from nova.api.openstack import server_metadata
59 from nova.api.openstack import shared_ip_groups
60 from nova.api.openstack import users
61+from nova.api.openstack import wsgi
62 from nova.api.openstack import zones
63
64
65@@ -50,7 +51,7 @@
66 'When True, this API service will accept admin operations.')
67
68
69-class FaultWrapper(wsgi.Middleware):
70+class FaultWrapper(base_wsgi.Middleware):
71 """Calls down the middleware stack, making exceptions into faults."""
72
73 @webob.dec.wsgify(RequestClass=wsgi.Request)
74@@ -63,7 +64,7 @@
75 return faults.Fault(exc)
76
77
78-class APIRouter(wsgi.Router):
79+class APIRouter(base_wsgi.Router):
80 """
81 Routes requests on the OpenStack API to the appropriate controller
82 and method.
83@@ -97,19 +98,21 @@
84 server_members['reset_network'] = 'POST'
85 server_members['inject_network_info'] = 'POST'
86
87- mapper.resource("zone", "zones", controller=zones.Controller(),
88+ mapper.resource("zone", "zones",
89+ controller=zones.create_resource(),
90 collection={'detail': 'GET', 'info': 'GET',
91 'select': 'GET'})
92
93- mapper.resource("user", "users", controller=users.Controller(),
94+ mapper.resource("user", "users",
95+ controller=users.create_resource(),
96 collection={'detail': 'GET'})
97
98 mapper.resource("account", "accounts",
99- controller=accounts.Controller(),
100+ controller=accounts.create_resource(),
101 collection={'detail': 'GET'})
102
103 mapper.resource("console", "consoles",
104- controller=consoles.Controller(),
105+ controller=consoles.create_resource(),
106 parent_resource=dict(member_name='server',
107 collection_name='servers'))
108
109@@ -122,31 +125,31 @@
110 def _setup_routes(self, mapper):
111 super(APIRouterV10, self)._setup_routes(mapper)
112 mapper.resource("server", "servers",
113- controller=servers.ControllerV10(),
114+ controller=servers.create_resource('1.0'),
115 collection={'detail': 'GET'},
116 member=self.server_members)
117
118 mapper.resource("image", "images",
119- controller=images.ControllerV10(),
120+ controller=images.create_resource('1.0'),
121 collection={'detail': 'GET'})
122
123 mapper.resource("flavor", "flavors",
124- controller=flavors.ControllerV10(),
125+ controller=flavors.create_resource('1.0'),
126 collection={'detail': 'GET'})
127
128 mapper.resource("shared_ip_group", "shared_ip_groups",
129 collection={'detail': 'GET'},
130- controller=shared_ip_groups.Controller())
131+ controller=shared_ip_groups.create_resource())
132
133 mapper.resource("backup_schedule", "backup_schedule",
134- controller=backup_schedules.Controller(),
135+ controller=backup_schedules.create_resource(),
136 parent_resource=dict(member_name='server',
137 collection_name='servers'))
138
139 mapper.resource("limit", "limits",
140- controller=limits.LimitsControllerV10())
141+ controller=limits.create_resource('1.0'))
142
143- mapper.resource("ip", "ips", controller=ips.Controller(),
144+ mapper.resource("ip", "ips", controller=ips.create_resource(),
145 collection=dict(public='GET', private='GET'),
146 parent_resource=dict(member_name='server',
147 collection_name='servers'))
148@@ -158,27 +161,27 @@
149 def _setup_routes(self, mapper):
150 super(APIRouterV11, self)._setup_routes(mapper)
151 mapper.resource("server", "servers",
152- controller=servers.ControllerV11(),
153+ controller=servers.create_resource('1.1'),
154 collection={'detail': 'GET'},
155 member=self.server_members)
156
157 mapper.resource("image", "images",
158- controller=images.ControllerV11(),
159+ controller=images.create_resource('1.1'),
160 collection={'detail': 'GET'})
161
162 mapper.resource("image_meta", "meta",
163- controller=image_metadata.Controller(),
164+ controller=image_metadata.create_resource(),
165 parent_resource=dict(member_name='image',
166 collection_name='images'))
167
168 mapper.resource("server_meta", "meta",
169- controller=server_metadata.Controller(),
170+ controller=server_metadata.create_resource(),
171 parent_resource=dict(member_name='server',
172 collection_name='servers'))
173
174 mapper.resource("flavor", "flavors",
175- controller=flavors.ControllerV11(),
176+ controller=flavors.create_resource('1.1'),
177 collection={'detail': 'GET'})
178
179 mapper.resource("limit", "limits",
180- controller=limits.LimitsControllerV11())
181+ controller=limits.create_resource('1.1'))
182
183=== modified file 'nova/api/openstack/accounts.py'
184--- nova/api/openstack/accounts.py 2011-04-27 21:03:05 +0000
185+++ nova/api/openstack/accounts.py 2011-05-26 21:32:29 +0000
186@@ -20,8 +20,9 @@
187 from nova import log as logging
188
189 from nova.auth import manager
190-from nova.api.openstack import common
191 from nova.api.openstack import faults
192+from nova.api.openstack import wsgi
193+
194
195 FLAGS = flags.FLAGS
196 LOG = logging.getLogger('nova.api.openstack')
197@@ -34,12 +35,7 @@
198 manager=account.project_manager_id)
199
200
201-class Controller(common.OpenstackController):
202-
203- _serialization_metadata = {
204- 'application/xml': {
205- "attributes": {
206- "account": ["id", "name", "description", "manager"]}}}
207+class Controller(object):
208
209 def __init__(self):
210 self.manager = manager.AuthManager()
211@@ -66,20 +62,33 @@
212 self.manager.delete_project(id)
213 return {}
214
215- def create(self, req):
216+ def create(self, req, body):
217 """We use update with create-or-update semantics
218 because the id comes from an external source"""
219 raise faults.Fault(webob.exc.HTTPNotImplemented())
220
221- def update(self, req, id):
222+ def update(self, req, id, body):
223 """This is really create or update."""
224 self._check_admin(req.environ['nova.context'])
225- env = self._deserialize(req.body, req.get_content_type())
226- description = env['account'].get('description')
227- manager = env['account'].get('manager')
228+ description = body['account'].get('description')
229+ manager = body['account'].get('manager')
230 try:
231 account = self.manager.get_project(id)
232 self.manager.modify_project(id, manager, description)
233 except exception.NotFound:
234 account = self.manager.create_project(id, manager, description)
235 return dict(account=_translate_keys(account))
236+
237+
238+def create_resource():
239+ metadata = {
240+ "attributes": {
241+ "account": ["id", "name", "description", "manager"],
242+ },
243+ }
244+
245+ serializers = {
246+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
247+ }
248+
249+ return wsgi.Resource(Controller(), serializers=serializers)
250
251=== modified file 'nova/api/openstack/backup_schedules.py'
252--- nova/api/openstack/backup_schedules.py 2011-03-30 17:05:06 +0000
253+++ nova/api/openstack/backup_schedules.py 2011-05-26 21:32:29 +0000
254@@ -19,9 +19,8 @@
255
256 from webob import exc
257
258-from nova.api.openstack import common
259 from nova.api.openstack import faults
260-import nova.image.service
261+from nova.api.openstack import wsgi
262
263
264 def _translate_keys(inst):
265@@ -29,14 +28,9 @@
266 return dict(backupSchedule=inst)
267
268
269-class Controller(common.OpenstackController):
270+class Controller(object):
271 """ The backup schedule API controller for the Openstack API """
272
273- _serialization_metadata = {
274- 'application/xml': {
275- 'attributes': {
276- 'backupSchedule': []}}}
277-
278 def __init__(self):
279 pass
280
281@@ -48,7 +42,7 @@
282 """ Returns a single backup schedule for a given instance """
283 return faults.Fault(exc.HTTPNotImplemented())
284
285- def create(self, req, server_id):
286+ def create(self, req, server_id, body):
287 """ No actual update method required, since the existing API allows
288 both create and update through a POST """
289 return faults.Fault(exc.HTTPNotImplemented())
290@@ -56,3 +50,18 @@
291 def delete(self, req, server_id, id):
292 """ Deletes an existing backup schedule """
293 return faults.Fault(exc.HTTPNotImplemented())
294+
295+
296+def create_resource():
297+ metadata = {
298+ 'attributes': {
299+ 'backupSchedule': [],
300+ },
301+ }
302+
303+ serializers = {
304+ 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
305+ metadata=metadata),
306+ }
307+
308+ return wsgi.Resource(Controller(), serializers=serializers)
309
310=== modified file 'nova/api/openstack/common.py'
311--- nova/api/openstack/common.py 2011-05-02 20:14:41 +0000
312+++ nova/api/openstack/common.py 2011-05-26 21:32:29 +0000
313@@ -23,7 +23,6 @@
314 from nova import exception
315 from nova import flags
316 from nova import log as logging
317-from nova import wsgi
318
319
320 LOG = logging.getLogger('nova.api.openstack.common')
321@@ -146,9 +145,3 @@
322 except:
323 LOG.debug(_("Error extracting id from href: %s") % href)
324 raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
325-
326-
327-class OpenstackController(wsgi.Controller):
328- def get_default_xmlns(self, req):
329- # Use V10 by default
330- return XML_NS_V10
331
332=== modified file 'nova/api/openstack/consoles.py'
333--- nova/api/openstack/consoles.py 2011-03-30 17:05:06 +0000
334+++ nova/api/openstack/consoles.py 2011-05-26 21:32:29 +0000
335@@ -19,8 +19,8 @@
336
337 from nova import console
338 from nova import exception
339-from nova.api.openstack import common
340 from nova.api.openstack import faults
341+from nova.api.openstack import wsgi
342
343
344 def _translate_keys(cons):
345@@ -43,17 +43,11 @@
346 return dict(console=info)
347
348
349-class Controller(common.OpenstackController):
350- """The Consoles Controller for the Openstack API"""
351-
352- _serialization_metadata = {
353- 'application/xml': {
354- 'attributes': {
355- 'console': []}}}
356+class Controller(object):
357+ """The Consoles controller for the Openstack API"""
358
359 def __init__(self):
360 self.console_api = console.API()
361- super(Controller, self).__init__()
362
363 def index(self, req, server_id):
364 """Returns a list of consoles for this instance"""
365@@ -63,9 +57,8 @@
366 return dict(consoles=[_translate_keys(console)
367 for console in consoles])
368
369- def create(self, req, server_id):
370+ def create(self, req, server_id, body):
371 """Creates a new console"""
372- #info = self._deserialize(req.body, req.get_content_type())
373 self.console_api.create_console(
374 req.environ['nova.context'],
375 int(server_id))
376@@ -94,3 +87,17 @@
377 except exception.NotFound:
378 return faults.Fault(exc.HTTPNotFound())
379 return exc.HTTPAccepted()
380+
381+
382+def create_resource():
383+ metadata = {
384+ 'attributes': {
385+ 'console': [],
386+ },
387+ }
388+
389+ serializers = {
390+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
391+ }
392+
393+ return wsgi.Resource(Controller(), serializers=serializers)
394
395=== modified file 'nova/api/openstack/contrib/volumes.py'
396--- nova/api/openstack/contrib/volumes.py 2011-04-19 09:54:47 +0000
397+++ nova/api/openstack/contrib/volumes.py 2011-05-26 21:32:29 +0000
398@@ -22,7 +22,6 @@
399 from nova import flags
400 from nova import log as logging
401 from nova import volume
402-from nova import wsgi
403 from nova.api.openstack import common
404 from nova.api.openstack import extensions
405 from nova.api.openstack import faults
406@@ -64,7 +63,7 @@
407 return d
408
409
410-class VolumeController(wsgi.Controller):
411+class VolumeController(object):
412 """The Volumes API controller for the OpenStack API."""
413
414 _serialization_metadata = {
415@@ -124,15 +123,14 @@
416 res = [entity_maker(context, vol) for vol in limited_list]
417 return {'volumes': res}
418
419- def create(self, req):
420+ def create(self, req, body):
421 """Creates a new volume."""
422 context = req.environ['nova.context']
423
424- env = self._deserialize(req.body, req.get_content_type())
425- if not env:
426+ if not body:
427 return faults.Fault(exc.HTTPUnprocessableEntity())
428
429- vol = env['volume']
430+ vol = body['volume']
431 size = vol['size']
432 LOG.audit(_("Create volume of %s GB"), size, context=context)
433 new_volume = self.volume_api.create(context, size,
434@@ -175,7 +173,7 @@
435 return d
436
437
438-class VolumeAttachmentController(wsgi.Controller):
439+class VolumeAttachmentController(object):
440 """The volume attachment API controller for the Openstack API.
441
442 A child resource of the server. Note that we use the volume id
443@@ -219,17 +217,16 @@
444 return {'volumeAttachment': _translate_attachment_detail_view(context,
445 vol)}
446
447- def create(self, req, server_id):
448+ def create(self, req, server_id, body):
449 """Attach a volume to an instance."""
450 context = req.environ['nova.context']
451
452- env = self._deserialize(req.body, req.get_content_type())
453- if not env:
454+ if not body:
455 return faults.Fault(exc.HTTPUnprocessableEntity())
456
457 instance_id = server_id
458- volume_id = env['volumeAttachment']['volumeId']
459- device = env['volumeAttachment']['device']
460+ volume_id = body['volumeAttachment']['volumeId']
461+ device = body['volumeAttachment']['device']
462
463 msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
464 " at %(device)s") % locals()
465@@ -259,7 +256,7 @@
466 # TODO(justinsb): How do I return "accepted" here?
467 return {'volumeAttachment': attachment}
468
469- def update(self, _req, _server_id, _id):
470+ def update(self, req, server_id, id, body):
471 """Update a volume attachment. We don't currently support this."""
472 return faults.Fault(exc.HTTPBadRequest())
473
474
475=== modified file 'nova/api/openstack/extensions.py'
476--- nova/api/openstack/extensions.py 2011-05-17 03:14:51 +0000
477+++ nova/api/openstack/extensions.py 2011-05-26 21:32:29 +0000
478@@ -27,9 +27,10 @@
479 from nova import exception
480 from nova import flags
481 from nova import log as logging
482-from nova import wsgi
483+from nova import wsgi as base_wsgi
484 from nova.api.openstack import common
485 from nova.api.openstack import faults
486+from nova.api.openstack import wsgi
487
488
489 LOG = logging.getLogger('extensions')
490@@ -115,28 +116,34 @@
491 return request_exts
492
493
494-class ActionExtensionController(common.OpenstackController):
495-
496+class ActionExtensionController(object):
497 def __init__(self, application):
498-
499 self.application = application
500 self.action_handlers = {}
501
502 def add_action(self, action_name, handler):
503 self.action_handlers[action_name] = handler
504
505- def action(self, req, id):
506-
507- input_dict = self._deserialize(req.body, req.get_content_type())
508+ def action(self, req, id, body):
509 for action_name, handler in self.action_handlers.iteritems():
510- if action_name in input_dict:
511- return handler(input_dict, req, id)
512+ if action_name in body:
513+ return handler(body, req, id)
514 # no action handler found (bump to downstream application)
515 res = self.application
516 return res
517
518
519-class RequestExtensionController(common.OpenstackController):
520+class ActionExtensionResource(wsgi.Resource):
521+
522+ def __init__(self, application):
523+ controller = ActionExtensionController(application)
524+ super(ActionExtensionResource, self).__init__(controller)
525+
526+ def add_action(self, action_name, handler):
527+ self.controller.add_action(action_name, handler)
528+
529+
530+class RequestExtensionController(object):
531
532 def __init__(self, application):
533 self.application = application
534@@ -153,7 +160,17 @@
535 return res
536
537
538-class ExtensionController(common.OpenstackController):
539+class RequestExtensionResource(wsgi.Resource):
540+
541+ def __init__(self, application):
542+ controller = RequestExtensionController(application)
543+ super(RequestExtensionResource, self).__init__(controller)
544+
545+ def add_handler(self, handler):
546+ self.controller.add_handler(handler)
547+
548+
549+class ExtensionsResource(wsgi.Resource):
550
551 def __init__(self, extension_manager):
552 self.extension_manager = extension_manager
553@@ -186,7 +203,7 @@
554 raise faults.Fault(webob.exc.HTTPNotFound())
555
556
557-class ExtensionMiddleware(wsgi.Middleware):
558+class ExtensionMiddleware(base_wsgi.Middleware):
559 """Extensions middleware for WSGI."""
560 @classmethod
561 def factory(cls, global_config, **local_config):
562@@ -195,43 +212,43 @@
563 return cls(app, **local_config)
564 return _factory
565
566- def _action_ext_controllers(self, application, ext_mgr, mapper):
567- """Return a dict of ActionExtensionController-s by collection."""
568- action_controllers = {}
569+ def _action_ext_resources(self, application, ext_mgr, mapper):
570+ """Return a dict of ActionExtensionResource-s by collection."""
571+ action_resources = {}
572 for action in ext_mgr.get_actions():
573- if not action.collection in action_controllers.keys():
574- controller = ActionExtensionController(application)
575+ if not action.collection in action_resources.keys():
576+ resource = ActionExtensionResource(application)
577 mapper.connect("/%s/:(id)/action.:(format)" %
578 action.collection,
579 action='action',
580- controller=controller,
581+ controller=resource,
582 conditions=dict(method=['POST']))
583 mapper.connect("/%s/:(id)/action" % action.collection,
584 action='action',
585- controller=controller,
586+ controller=resource,
587 conditions=dict(method=['POST']))
588- action_controllers[action.collection] = controller
589-
590- return action_controllers
591-
592- def _request_ext_controllers(self, application, ext_mgr, mapper):
593- """Returns a dict of RequestExtensionController-s by collection."""
594- request_ext_controllers = {}
595+ action_resources[action.collection] = resource
596+
597+ return action_resources
598+
599+ def _request_ext_resources(self, application, ext_mgr, mapper):
600+ """Returns a dict of RequestExtensionResource-s by collection."""
601+ request_ext_resources = {}
602 for req_ext in ext_mgr.get_request_extensions():
603- if not req_ext.key in request_ext_controllers.keys():
604- controller = RequestExtensionController(application)
605+ if not req_ext.key in request_ext_resources.keys():
606+ resource = RequestExtensionResource(application)
607 mapper.connect(req_ext.url_route + '.:(format)',
608 action='process',
609- controller=controller,
610+ controller=resource,
611 conditions=req_ext.conditions)
612
613 mapper.connect(req_ext.url_route,
614 action='process',
615- controller=controller,
616+ controller=resource,
617 conditions=req_ext.conditions)
618- request_ext_controllers[req_ext.key] = controller
619+ request_ext_resources[req_ext.key] = resource
620
621- return request_ext_controllers
622+ return request_ext_resources
623
624 def __init__(self, application, ext_mgr=None):
625
626@@ -246,22 +263,22 @@
627 LOG.debug(_('Extended resource: %s'),
628 resource.collection)
629 mapper.resource(resource.collection, resource.collection,
630- controller=resource.controller,
631+ controller=wsgi.Resource(resource.controller),
632 collection=resource.collection_actions,
633 member=resource.member_actions,
634 parent_resource=resource.parent)
635
636 # extended actions
637- action_controllers = self._action_ext_controllers(application, ext_mgr,
638+ action_resources = self._action_ext_resources(application, ext_mgr,
639 mapper)
640 for action in ext_mgr.get_actions():
641 LOG.debug(_('Extended action: %s'), action.action_name)
642- controller = action_controllers[action.collection]
643- controller.add_action(action.action_name, action.handler)
644+ resource = action_resources[action.collection]
645+ resource.add_action(action.action_name, action.handler)
646
647 # extended requests
648- req_controllers = self._request_ext_controllers(application, ext_mgr,
649- mapper)
650+ req_controllers = self._request_ext_resources(application, ext_mgr,
651+ mapper)
652 for request_ext in ext_mgr.get_request_extensions():
653 LOG.debug(_('Extended request: %s'), request_ext.key)
654 controller = req_controllers[request_ext.key]
655@@ -313,7 +330,7 @@
656 """Returns a list of ResourceExtension objects."""
657 resources = []
658 resources.append(ResourceExtension('extensions',
659- ExtensionController(self)))
660+ ExtensionsResource(self)))
661 for alias, ext in self.extensions.iteritems():
662 try:
663 resources.extend(ext.get_resources())
664@@ -410,7 +427,7 @@
665
666
667 class RequestExtension(object):
668- """Extend requests and responses of core nova OpenStack API controllers.
669+ """Extend requests and responses of core nova OpenStack API resources.
670
671 Provide a way to add data to responses and handle custom request data
672 that is sent to core nova OpenStack API controllers.
673@@ -424,7 +441,7 @@
674
675
676 class ActionExtension(object):
677- """Add custom actions to core nova OpenStack API controllers."""
678+ """Add custom actions to core nova OpenStack API resources."""
679
680 def __init__(self, collection, action_name, handler):
681 self.collection = collection
682
683=== modified file 'nova/api/openstack/faults.py'
684--- nova/api/openstack/faults.py 2011-04-07 18:55:42 +0000
685+++ nova/api/openstack/faults.py 2011-05-26 21:32:29 +0000
686@@ -19,8 +19,7 @@
687 import webob.dec
688 import webob.exc
689
690-from nova import wsgi
691-from nova.api.openstack import common
692+from nova.api.openstack import wsgi
693
694
695 class Fault(webob.exc.HTTPException):
696@@ -55,13 +54,21 @@
697 if code == 413:
698 retry = self.wrapped_exc.headers['Retry-After']
699 fault_data[fault_name]['retryAfter'] = retry
700+
701 # 'code' is an attribute on the fault tag itself
702- metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
703- default_xmlns = common.XML_NS_V10
704- serializer = wsgi.Serializer(metadata, default_xmlns)
705+ metadata = {'attributes': {fault_name: 'code'}}
706+
707 content_type = req.best_match_content_type()
708- self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
709+
710+ serializer = {
711+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
712+ xmlns=wsgi.XMLNS_V10),
713+ 'application/json': wsgi.JSONDictSerializer(),
714+ }[content_type]
715+
716+ self.wrapped_exc.body = serializer.serialize(fault_data)
717 self.wrapped_exc.content_type = content_type
718+
719 return self.wrapped_exc
720
721
722@@ -70,14 +77,6 @@
723 Rate-limited request response.
724 """
725
726- _serialization_metadata = {
727- "application/xml": {
728- "attributes": {
729- "overLimitFault": "code",
730- },
731- },
732- }
733-
734 def __init__(self, message, details, retry_time):
735 """
736 Initialize new `OverLimitFault` with relevant information.
737@@ -97,8 +96,16 @@
738 Return the wrapped exception with a serialized body conforming to our
739 error format.
740 """
741- serializer = wsgi.Serializer(self._serialization_metadata)
742 content_type = request.best_match_content_type()
743- content = serializer.serialize(self.content, content_type)
744+ metadata = {"attributes": {"overLimitFault": "code"}}
745+
746+ serializer = {
747+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
748+ xmlns=wsgi.XMLNS_V10),
749+ 'application/json': wsgi.JSONDictSerializer(),
750+ }[content_type]
751+
752+ content = serializer.serialize(self.content)
753 self.wrapped_exc.body = content
754+
755 return self.wrapped_exc
756
757=== modified file 'nova/api/openstack/flavors.py'
758--- nova/api/openstack/flavors.py 2011-05-06 20:13:35 +0000
759+++ nova/api/openstack/flavors.py 2011-05-26 21:32:29 +0000
760@@ -19,22 +19,13 @@
761
762 from nova import db
763 from nova import exception
764-from nova.api.openstack import common
765 from nova.api.openstack import views
766-
767-
768-class Controller(common.OpenstackController):
769+from nova.api.openstack import wsgi
770+
771+
772+class Controller(object):
773 """Flavor controller for the OpenStack API."""
774
775- _serialization_metadata = {
776- 'application/xml': {
777- "attributes": {
778- "flavor": ["id", "name", "ram", "disk"],
779- "link": ["rel", "type", "href"],
780- }
781- }
782- }
783-
784 def index(self, req):
785 """Return all flavors in brief."""
786 items = self._get_flavors(req, is_detail=False)
787@@ -71,14 +62,31 @@
788
789
790 class ControllerV10(Controller):
791+
792 def _get_view_builder(self, req):
793 return views.flavors.ViewBuilder()
794
795
796 class ControllerV11(Controller):
797+
798 def _get_view_builder(self, req):
799 base_url = req.application_url
800 return views.flavors.ViewBuilderV11(base_url)
801
802- def get_default_xmlns(self, req):
803- return common.XML_NS_V11
804+
805+def create_resource(version='1.0'):
806+ controller = {
807+ '1.0': ControllerV10,
808+ '1.1': ControllerV11,
809+ }[version]()
810+
811+ xmlns = {
812+ '1.0': wsgi.XMLNS_V10,
813+ '1.1': wsgi.XMLNS_V11,
814+ }[version]
815+
816+ serializers = {
817+ 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
818+ }
819+
820+ return wsgi.Resource(controller, serializers=serializers)
821
822=== modified file 'nova/api/openstack/image_metadata.py'
823--- nova/api/openstack/image_metadata.py 2011-04-08 07:58:48 +0000
824+++ nova/api/openstack/image_metadata.py 2011-05-26 21:32:29 +0000
825@@ -20,20 +20,18 @@
826 from nova import flags
827 from nova import quota
828 from nova import utils
829-from nova import wsgi
830-from nova.api.openstack import common
831 from nova.api.openstack import faults
832+from nova.api.openstack import wsgi
833
834
835 FLAGS = flags.FLAGS
836
837
838-class Controller(common.OpenstackController):
839+class Controller(object):
840 """The image metadata API controller for the Openstack API"""
841
842 def __init__(self):
843 self.image_service = utils.import_object(FLAGS.image_service)
844- super(Controller, self).__init__()
845
846 def _get_metadata(self, context, image_id, image=None):
847 if not image:
848@@ -64,9 +62,8 @@
849 else:
850 return faults.Fault(exc.HTTPNotFound())
851
852- def create(self, req, image_id):
853+ def create(self, req, image_id, body):
854 context = req.environ['nova.context']
855- body = self._deserialize(req.body, req.get_content_type())
856 img = self.image_service.show(context, image_id)
857 metadata = self._get_metadata(context, image_id, img)
858 if 'metadata' in body:
859@@ -77,9 +74,8 @@
860 self.image_service.update(context, image_id, img, None)
861 return dict(metadata=metadata)
862
863- def update(self, req, image_id, id):
864+ def update(self, req, image_id, id, body):
865 context = req.environ['nova.context']
866- body = self._deserialize(req.body, req.get_content_type())
867 if not id in body:
868 expl = _('Request body and URI mismatch')
869 raise exc.HTTPBadRequest(explanation=expl)
870@@ -104,3 +100,11 @@
871 metadata.pop(id)
872 img['properties'] = metadata
873 self.image_service.update(context, image_id, img, None)
874+
875+
876+def create_resource():
877+ serializers = {
878+ 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
879+ }
880+
881+ return wsgi.Resource(Controller(), serializers=serializers)
882
883=== modified file 'nova/api/openstack/images.py'
884--- nova/api/openstack/images.py 2011-04-21 17:29:11 +0000
885+++ nova/api/openstack/images.py 2011-05-26 21:32:29 +0000
886@@ -23,24 +23,15 @@
887 from nova.api.openstack import common
888 from nova.api.openstack import faults
889 from nova.api.openstack.views import images as images_view
890+from nova.api.openstack import wsgi
891
892
893 LOG = log.getLogger('nova.api.openstack.images')
894 FLAGS = flags.FLAGS
895
896
897-class Controller(common.OpenstackController):
898- """Base `wsgi.Controller` for retrieving/displaying images."""
899-
900- _serialization_metadata = {
901- 'application/xml': {
902- "attributes": {
903- "image": ["id", "name", "updated", "created", "status",
904- "serverId", "progress"],
905- "link": ["rel", "type", "href"],
906- },
907- },
908- }
909+class Controller(object):
910+ """Base controller for retrieving/displaying images."""
911
912 def __init__(self, image_service=None, compute_service=None):
913 """Initialize new `ImageController`.
914@@ -108,21 +99,20 @@
915 self._image_service.delete(context, image_id)
916 return webob.exc.HTTPNoContent()
917
918- def create(self, req):
919+ def create(self, req, body):
920 """Snapshot a server instance and save the image.
921
922 :param req: `wsgi.Request` object
923 """
924 context = req.environ['nova.context']
925 content_type = req.get_content_type()
926- image = self._deserialize(req.body, content_type)
927
928- if not image:
929+ if not body:
930 raise webob.exc.HTTPBadRequest()
931
932 try:
933- server_id = image["image"]["serverId"]
934- image_name = image["image"]["name"]
935+ server_id = body["image"]["serverId"]
936+ image_name = body["image"]["name"]
937 except KeyError:
938 raise webob.exc.HTTPBadRequest()
939
940@@ -151,5 +141,29 @@
941 base_url = request.application_url
942 return images_view.ViewBuilderV11(base_url)
943
944- def get_default_xmlns(self, req):
945- return common.XML_NS_V11
946+
947+def create_resource(version='1.0'):
948+ controller = {
949+ '1.0': ControllerV10,
950+ '1.1': ControllerV11,
951+ }[version]()
952+
953+ xmlns = {
954+ '1.0': wsgi.XMLNS_V10,
955+ '1.1': wsgi.XMLNS_V11,
956+ }[version]
957+
958+ metadata = {
959+ "attributes": {
960+ "image": ["id", "name", "updated", "created", "status",
961+ "serverId", "progress"],
962+ "link": ["rel", "type", "href"],
963+ },
964+ }
965+
966+ serializers = {
967+ 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
968+ metadata=metadata),
969+ }
970+
971+ return wsgi.Resource(controller, serializers=serializers)
972
973=== modified file 'nova/api/openstack/ips.py'
974--- nova/api/openstack/ips.py 2011-04-06 21:50:11 +0000
975+++ nova/api/openstack/ips.py 2011-05-26 21:32:29 +0000
976@@ -20,23 +20,14 @@
977 from webob import exc
978
979 import nova
980+from nova.api.openstack import faults
981 import nova.api.openstack.views.addresses
982-from nova.api.openstack import common
983-from nova.api.openstack import faults
984-
985-
986-class Controller(common.OpenstackController):
987+from nova.api.openstack import wsgi
988+
989+
990+class Controller(object):
991 """The servers addresses API controller for the Openstack API."""
992
993- _serialization_metadata = {
994- 'application/xml': {
995- 'list_collections': {
996- 'public': {'item_name': 'ip', 'item_key': 'addr'},
997- 'private': {'item_name': 'ip', 'item_key': 'addr'},
998- },
999- },
1000- }
1001-
1002 def __init__(self):
1003 self.compute_api = nova.compute.API()
1004 self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
1005@@ -65,8 +56,24 @@
1006 def show(self, req, server_id, id):
1007 return faults.Fault(exc.HTTPNotImplemented())
1008
1009- def create(self, req, server_id):
1010+ def create(self, req, server_id, body):
1011 return faults.Fault(exc.HTTPNotImplemented())
1012
1013 def delete(self, req, server_id, id):
1014 return faults.Fault(exc.HTTPNotImplemented())
1015+
1016+
1017+def create_resource():
1018+ metadata = {
1019+ 'list_collections': {
1020+ 'public': {'item_name': 'ip', 'item_key': 'addr'},
1021+ 'private': {'item_name': 'ip', 'item_key': 'addr'},
1022+ },
1023+ }
1024+
1025+ serializers = {
1026+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
1027+ xmlns=wsgi.XMLNS_V10),
1028+ }
1029+
1030+ return wsgi.Resource(Controller(), serializers=serializers)
1031
1032=== modified file 'nova/api/openstack/limits.py'
1033--- nova/api/openstack/limits.py 2011-05-19 18:08:15 +0000
1034+++ nova/api/openstack/limits.py 2011-05-26 21:32:29 +0000
1035@@ -31,10 +31,12 @@
1036 from webob.dec import wsgify
1037
1038 from nova import quota
1039+from nova import wsgi as base_wsgi
1040 from nova import wsgi
1041 from nova.api.openstack import common
1042 from nova.api.openstack import faults
1043 from nova.api.openstack.views import limits as limits_views
1044+from nova.api.openstack import wsgi
1045
1046
1047 # Convenience constants for the limits dictionary passed to Limiter().
1048@@ -44,23 +46,11 @@
1049 PER_DAY = 60 * 60 * 24
1050
1051
1052-class LimitsController(common.OpenstackController):
1053+class LimitsController(object):
1054 """
1055 Controller for accessing limits in the OpenStack API.
1056 """
1057
1058- _serialization_metadata = {
1059- "application/xml": {
1060- "attributes": {
1061- "limit": ["verb", "URI", "uri", "regex", "value", "unit",
1062- "resetTime", "next-available", "remaining", "name"],
1063- },
1064- "plurals": {
1065- "rate": "limit",
1066- },
1067- },
1068- }
1069-
1070 def index(self, req):
1071 """
1072 Return all global and rate limit information.
1073@@ -86,6 +76,35 @@
1074 return limits_views.ViewBuilderV11()
1075
1076
1077+def create_resource(version='1.0'):
1078+ controller = {
1079+ '1.0': LimitsControllerV10,
1080+ '1.1': LimitsControllerV11,
1081+ }[version]()
1082+
1083+ xmlns = {
1084+ '1.0': wsgi.XMLNS_V10,
1085+ '1.1': wsgi.XMLNS_V11,
1086+ }[version]
1087+
1088+ metadata = {
1089+ "attributes": {
1090+ "limit": ["verb", "URI", "uri", "regex", "value", "unit",
1091+ "resetTime", "next-available", "remaining", "name"],
1092+ },
1093+ "plurals": {
1094+ "rate": "limit",
1095+ },
1096+ }
1097+
1098+ serializers = {
1099+ 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
1100+ metadata=metadata)
1101+ }
1102+
1103+ return wsgi.Resource(controller, serializers=serializers)
1104+
1105+
1106 class Limit(object):
1107 """
1108 Stores information about a limit for HTTP requets.
1109@@ -197,7 +216,7 @@
1110 ]
1111
1112
1113-class RateLimitingMiddleware(wsgi.Middleware):
1114+class RateLimitingMiddleware(base_wsgi.Middleware):
1115 """
1116 Rate-limits requests passing through this middleware. All limit information
1117 is stored in memory for this implementation.
1118@@ -211,7 +230,7 @@
1119 @param application: WSGI application to wrap
1120 @param limits: List of dictionaries describing limits
1121 """
1122- wsgi.Middleware.__init__(self, application)
1123+ base_wsgi.Middleware.__init__(self, application)
1124 self._limiter = Limiter(limits or DEFAULT_LIMITS)
1125
1126 @wsgify(RequestClass=wsgi.Request)
1127
1128=== modified file 'nova/api/openstack/server_metadata.py'
1129--- nova/api/openstack/server_metadata.py 2011-04-12 17:47:45 +0000
1130+++ nova/api/openstack/server_metadata.py 2011-05-26 21:32:29 +0000
1131@@ -19,12 +19,11 @@
1132
1133 from nova import compute
1134 from nova import quota
1135-from nova import wsgi
1136-from nova.api.openstack import common
1137 from nova.api.openstack import faults
1138-
1139-
1140-class Controller(common.OpenstackController):
1141+from nova.api.openstack import wsgi
1142+
1143+
1144+class Controller(object):
1145 """ The server metadata API controller for the Openstack API """
1146
1147 def __init__(self):
1148@@ -43,10 +42,9 @@
1149 context = req.environ['nova.context']
1150 return self._get_metadata(context, server_id)
1151
1152- def create(self, req, server_id):
1153+ def create(self, req, server_id, body):
1154 context = req.environ['nova.context']
1155- data = self._deserialize(req.body, req.get_content_type())
1156- metadata = data.get('metadata')
1157+ metadata = body.get('metadata')
1158 try:
1159 self.compute_api.update_or_create_instance_metadata(context,
1160 server_id,
1161@@ -55,9 +53,8 @@
1162 self._handle_quota_error(error)
1163 return req.body
1164
1165- def update(self, req, server_id, id):
1166+ def update(self, req, server_id, id, body):
1167 context = req.environ['nova.context']
1168- body = self._deserialize(req.body, req.get_content_type())
1169 if not id in body:
1170 expl = _('Request body and URI mismatch')
1171 raise exc.HTTPBadRequest(explanation=expl)
1172@@ -92,3 +89,11 @@
1173 if error.code == "MetadataLimitExceeded":
1174 raise exc.HTTPBadRequest(explanation=error.message)
1175 raise error
1176+
1177+
1178+def create_resource():
1179+ serializers = {
1180+ 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
1181+ }
1182+
1183+ return wsgi.Resource(Controller(), serializers=serializers)
1184
1185=== modified file 'nova/api/openstack/servers.py'
1186--- nova/api/openstack/servers.py 2011-05-25 17:55:51 +0000
1187+++ nova/api/openstack/servers.py 2011-05-26 21:32:29 +0000
1188@@ -31,6 +31,7 @@
1189 import nova.api.openstack.views.flavors
1190 import nova.api.openstack.views.images
1191 import nova.api.openstack.views.servers
1192+from nova.api.openstack import wsgi
1193 from nova.auth import manager as auth_manager
1194 from nova.compute import instance_types
1195 import nova.api.openstack
1196@@ -41,31 +42,12 @@
1197 FLAGS = flags.FLAGS
1198
1199
1200-class Controller(common.OpenstackController):
1201+class Controller(object):
1202 """ The Server API controller for the OpenStack API """
1203
1204- _serialization_metadata = {
1205- "application/xml": {
1206- "attributes": {
1207- "server": ["id", "imageId", "name", "flavorId", "hostId",
1208- "status", "progress", "adminPass", "flavorRef",
1209- "imageRef"],
1210- "link": ["rel", "type", "href"],
1211- },
1212- "dict_collections": {
1213- "metadata": {"item_name": "meta", "item_key": "key"},
1214- },
1215- "list_collections": {
1216- "public": {"item_name": "ip", "item_key": "addr"},
1217- "private": {"item_name": "ip", "item_key": "addr"},
1218- },
1219- },
1220- }
1221-
1222 def __init__(self):
1223 self.compute_api = compute.API()
1224 self._image_service = utils.import_object(FLAGS.image_service)
1225- super(Controller, self).__init__()
1226
1227 def index(self, req):
1228 """ Returns a list of server names and ids for a given user """
1229@@ -122,15 +104,14 @@
1230 return faults.Fault(exc.HTTPNotFound())
1231 return exc.HTTPAccepted()
1232
1233- def create(self, req):
1234+ def create(self, req, body):
1235 """ Creates a new server for a given user """
1236- env = self._deserialize_create(req)
1237- if not env:
1238+ if not body:
1239 return faults.Fault(exc.HTTPUnprocessableEntity())
1240
1241 context = req.environ['nova.context']
1242
1243- password = self._get_server_admin_password(env['server'])
1244+ password = self._get_server_admin_password(body['server'])
1245
1246 key_name = None
1247 key_data = None
1248@@ -140,7 +121,7 @@
1249 key_name = key_pair['name']
1250 key_data = key_pair['public_key']
1251
1252- requested_image_id = self._image_id_from_req_data(env)
1253+ requested_image_id = self._image_id_from_req_data(body)
1254 try:
1255 image_id = common.get_image_id_from_image_hash(self._image_service,
1256 context, requested_image_id)
1257@@ -151,18 +132,18 @@
1258 kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
1259 req, image_id)
1260
1261- personality = env['server'].get('personality')
1262+ personality = body['server'].get('personality')
1263 injected_files = []
1264 if personality:
1265 injected_files = self._get_injected_files(personality)
1266
1267- flavor_id = self._flavor_id_from_req_data(env)
1268+ flavor_id = self._flavor_id_from_req_data(body)
1269
1270- if not 'name' in env['server']:
1271+ if not 'name' in body['server']:
1272 msg = _("Server name is not defined")
1273 return exc.HTTPBadRequest(msg)
1274
1275- name = env['server']['name']
1276+ name = body['server']['name']
1277 self._validate_server_name(name)
1278 name = name.strip()
1279
1280@@ -179,7 +160,7 @@
1281 display_description=name,
1282 key_name=key_name,
1283 key_data=key_data,
1284- metadata=env['server'].get('metadata', {}),
1285+ metadata=body['server'].get('metadata', {}),
1286 injected_files=injected_files,
1287 admin_password=password)
1288 except quota.QuotaError as error:
1289@@ -193,18 +174,6 @@
1290 server['server']['adminPass'] = password
1291 return server
1292
1293- def _deserialize_create(self, request):
1294- """
1295- Deserialize a create request
1296-
1297- Overrides normal behavior in the case of xml content
1298- """
1299- if request.content_type == "application/xml":
1300- deserializer = ServerCreateRequestXMLDeserializer()
1301- return deserializer.deserialize(request.body)
1302- else:
1303- return self._deserialize(request.body, request.get_content_type())
1304-
1305 def _get_injected_files(self, personality):
1306 """
1307 Create a list of injected files from the personality attribute
1308@@ -254,24 +223,23 @@
1309 return utils.generate_password(16)
1310
1311 @scheduler_api.redirect_handler
1312- def update(self, req, id):
1313+ def update(self, req, id, body):
1314 """ Updates the server name or password """
1315 if len(req.body) == 0:
1316 raise exc.HTTPUnprocessableEntity()
1317
1318- inst_dict = self._deserialize(req.body, req.get_content_type())
1319- if not inst_dict:
1320+ if not body:
1321 return faults.Fault(exc.HTTPUnprocessableEntity())
1322
1323 ctxt = req.environ['nova.context']
1324 update_dict = {}
1325
1326- if 'name' in inst_dict['server']:
1327- name = inst_dict['server']['name']
1328+ if 'name' in body['server']:
1329+ name = body['server']['name']
1330 self._validate_server_name(name)
1331 update_dict['display_name'] = name.strip()
1332
1333- self._parse_update(ctxt, id, inst_dict, update_dict)
1334+ self._parse_update(ctxt, id, body, update_dict)
1335
1336 try:
1337 self.compute_api.update(ctxt, id, **update_dict)
1338@@ -293,7 +261,7 @@
1339 pass
1340
1341 @scheduler_api.redirect_handler
1342- def action(self, req, id):
1343+ def action(self, req, id, body):
1344 """Multi-purpose method used to reboot, rebuild, or
1345 resize a server"""
1346
1347@@ -306,10 +274,9 @@
1348 'rebuild': self._action_rebuild,
1349 }
1350
1351- input_dict = self._deserialize(req.body, req.get_content_type())
1352 for key in actions.keys():
1353- if key in input_dict:
1354- return actions[key](input_dict, req, id)
1355+ if key in body:
1356+ return actions[key](body, req, id)
1357 return faults.Fault(exc.HTTPNotImplemented())
1358
1359 def _action_change_password(self, input_dict, req, id):
1360@@ -409,7 +376,7 @@
1361 return exc.HTTPAccepted()
1362
1363 @scheduler_api.redirect_handler
1364- def reset_network(self, req, id):
1365+ def reset_network(self, req, id, body):
1366 """
1367 Reset networking on an instance (admin only).
1368
1369@@ -424,7 +391,7 @@
1370 return exc.HTTPAccepted()
1371
1372 @scheduler_api.redirect_handler
1373- def inject_network_info(self, req, id):
1374+ def inject_network_info(self, req, id, body):
1375 """
1376 Inject network info for an instance (admin only).
1377
1378@@ -439,7 +406,7 @@
1379 return exc.HTTPAccepted()
1380
1381 @scheduler_api.redirect_handler
1382- def pause(self, req, id):
1383+ def pause(self, req, id, body):
1384 """ Permit Admins to Pause the server. """
1385 ctxt = req.environ['nova.context']
1386 try:
1387@@ -451,7 +418,7 @@
1388 return exc.HTTPAccepted()
1389
1390 @scheduler_api.redirect_handler
1391- def unpause(self, req, id):
1392+ def unpause(self, req, id, body):
1393 """ Permit Admins to Unpause the server. """
1394 ctxt = req.environ['nova.context']
1395 try:
1396@@ -463,7 +430,7 @@
1397 return exc.HTTPAccepted()
1398
1399 @scheduler_api.redirect_handler
1400- def suspend(self, req, id):
1401+ def suspend(self, req, id, body):
1402 """permit admins to suspend the server"""
1403 context = req.environ['nova.context']
1404 try:
1405@@ -475,7 +442,7 @@
1406 return exc.HTTPAccepted()
1407
1408 @scheduler_api.redirect_handler
1409- def resume(self, req, id):
1410+ def resume(self, req, id, body):
1411 """permit admins to resume the server from suspend"""
1412 context = req.environ['nova.context']
1413 try:
1414@@ -735,11 +702,8 @@
1415 raise exc.HTTPBadRequest(msg)
1416 return password
1417
1418- def get_default_xmlns(self, req):
1419- return common.XML_NS_V11
1420-
1421-
1422-class ServerCreateRequestXMLDeserializer(object):
1423+
1424+class ServerXMLDeserializer(wsgi.XMLDeserializer):
1425 """
1426 Deserializer to handle xml-formatted server create requests.
1427
1428@@ -747,7 +711,7 @@
1429 and personality attributes
1430 """
1431
1432- def deserialize(self, string):
1433+ def create(self, string):
1434 """Deserialize an xml-formatted server create request"""
1435 dom = minidom.parseString(string)
1436 server = self._extract_server(dom)
1437@@ -814,3 +778,43 @@
1438 if child.nodeType == child.TEXT_NODE:
1439 return child.nodeValue
1440 return ""
1441+
1442+
1443+def create_resource(version='1.0'):
1444+ controller = {
1445+ '1.0': ControllerV10,
1446+ '1.1': ControllerV11,
1447+ }[version]()
1448+
1449+ metadata = {
1450+ "attributes": {
1451+ "server": ["id", "imageId", "name", "flavorId", "hostId",
1452+ "status", "progress", "adminPass", "flavorRef",
1453+ "imageRef"],
1454+ "link": ["rel", "type", "href"],
1455+ },
1456+ "dict_collections": {
1457+ "metadata": {"item_name": "meta", "item_key": "key"},
1458+ },
1459+ "list_collections": {
1460+ "public": {"item_name": "ip", "item_key": "addr"},
1461+ "private": {"item_name": "ip", "item_key": "addr"},
1462+ },
1463+ }
1464+
1465+ xmlns = {
1466+ '1.0': wsgi.XMLNS_V10,
1467+ '1.1': wsgi.XMLNS_V11,
1468+ }[version]
1469+
1470+ serializers = {
1471+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
1472+ xmlns=xmlns),
1473+ }
1474+
1475+ deserializers = {
1476+ 'application/xml': ServerXMLDeserializer(),
1477+ }
1478+
1479+ return wsgi.Resource(controller, serializers=serializers,
1480+ deserializers=deserializers)
1481
1482=== modified file 'nova/api/openstack/shared_ip_groups.py'
1483--- nova/api/openstack/shared_ip_groups.py 2011-03-30 17:05:06 +0000
1484+++ nova/api/openstack/shared_ip_groups.py 2011-05-26 21:32:29 +0000
1485@@ -17,29 +17,13 @@
1486
1487 from webob import exc
1488
1489-from nova.api.openstack import common
1490 from nova.api.openstack import faults
1491-
1492-
1493-def _translate_keys(inst):
1494- """ Coerces a shared IP group instance into proper dictionary format """
1495- return dict(sharedIpGroup=inst)
1496-
1497-
1498-def _translate_detail_keys(inst):
1499- """ Coerces a shared IP group instance into proper dictionary format with
1500- correctly mapped attributes """
1501- return dict(sharedIpGroups=inst)
1502-
1503-
1504-class Controller(common.OpenstackController):
1505+from nova.api.openstack import wsgi
1506+
1507+
1508+class Controller(object):
1509 """ The Shared IP Groups Controller for the Openstack API """
1510
1511- _serialization_metadata = {
1512- 'application/xml': {
1513- 'attributes': {
1514- 'sharedIpGroup': []}}}
1515-
1516 def index(self, req):
1517 """ Returns a list of Shared IP Groups for the user """
1518 raise faults.Fault(exc.HTTPNotImplemented())
1519@@ -48,7 +32,7 @@
1520 """ Shows in-depth information on a specific Shared IP Group """
1521 raise faults.Fault(exc.HTTPNotImplemented())
1522
1523- def update(self, req, id):
1524+ def update(self, req, id, body):
1525 """ You can't update a Shared IP Group """
1526 raise faults.Fault(exc.HTTPNotImplemented())
1527
1528@@ -60,6 +44,10 @@
1529 """ Returns a complete list of Shared IP Groups """
1530 raise faults.Fault(exc.HTTPNotImplemented())
1531
1532- def create(self, req):
1533+ def create(self, req, body):
1534 """ Creates a new Shared IP group """
1535 raise faults.Fault(exc.HTTPNotImplemented())
1536+
1537+
1538+def create_resource():
1539+ return wsgi.Resource(Controller())
1540
1541=== modified file 'nova/api/openstack/users.py'
1542--- nova/api/openstack/users.py 2011-04-27 21:03:05 +0000
1543+++ nova/api/openstack/users.py 2011-05-26 21:32:29 +0000
1544@@ -20,8 +20,10 @@
1545 from nova import log as logging
1546 from nova.api.openstack import common
1547 from nova.api.openstack import faults
1548+from nova.api.openstack import wsgi
1549 from nova.auth import manager
1550
1551+
1552 FLAGS = flags.FLAGS
1553 LOG = logging.getLogger('nova.api.openstack')
1554
1555@@ -34,12 +36,7 @@
1556 admin=user.admin)
1557
1558
1559-class Controller(common.OpenstackController):
1560-
1561- _serialization_metadata = {
1562- 'application/xml': {
1563- "attributes": {
1564- "user": ["id", "name", "access", "secret", "admin"]}}}
1565+class Controller(object):
1566
1567 def __init__(self):
1568 self.manager = manager.AuthManager()
1569@@ -81,23 +78,35 @@
1570 self.manager.delete_user(id)
1571 return {}
1572
1573- def create(self, req):
1574+ def create(self, req, body):
1575 self._check_admin(req.environ['nova.context'])
1576- env = self._deserialize(req.body, req.get_content_type())
1577- is_admin = env['user'].get('admin') in ('T', 'True', True)
1578- name = env['user'].get('name')
1579- access = env['user'].get('access')
1580- secret = env['user'].get('secret')
1581+ is_admin = body['user'].get('admin') in ('T', 'True', True)
1582+ name = body['user'].get('name')
1583+ access = body['user'].get('access')
1584+ secret = body['user'].get('secret')
1585 user = self.manager.create_user(name, access, secret, is_admin)
1586 return dict(user=_translate_keys(user))
1587
1588- def update(self, req, id):
1589+ def update(self, req, id, body):
1590 self._check_admin(req.environ['nova.context'])
1591- env = self._deserialize(req.body, req.get_content_type())
1592- is_admin = env['user'].get('admin')
1593+ is_admin = body['user'].get('admin')
1594 if is_admin is not None:
1595 is_admin = is_admin in ('T', 'True', True)
1596- access = env['user'].get('access')
1597- secret = env['user'].get('secret')
1598+ access = body['user'].get('access')
1599+ secret = body['user'].get('secret')
1600 self.manager.modify_user(id, access, secret, is_admin)
1601 return dict(user=_translate_keys(self.manager.get_user(id)))
1602+
1603+
1604+def create_resource():
1605+ metadata = {
1606+ "attributes": {
1607+ "user": ["id", "name", "access", "secret", "admin"],
1608+ },
1609+ }
1610+
1611+ serializers = {
1612+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
1613+ }
1614+
1615+ return wsgi.Resource(Controller(), serializers=serializers)
1616
1617=== modified file 'nova/api/openstack/versions.py'
1618--- nova/api/openstack/versions.py 2011-03-29 15:41:33 +0000
1619+++ nova/api/openstack/versions.py 2011-05-26 21:32:29 +0000
1620@@ -18,13 +18,26 @@
1621 import webob
1622 import webob.dec
1623
1624-from nova import wsgi
1625 import nova.api.openstack.views.versions
1626-
1627-
1628-class Versions(wsgi.Application):
1629- @webob.dec.wsgify(RequestClass=wsgi.Request)
1630- def __call__(self, req):
1631+from nova.api.openstack import wsgi
1632+
1633+
1634+class Versions(wsgi.Resource):
1635+ def __init__(self):
1636+ metadata = {
1637+ "attributes": {
1638+ "version": ["status", "id"],
1639+ "link": ["rel", "href"],
1640+ }
1641+ }
1642+
1643+ serializers = {
1644+ 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
1645+ }
1646+
1647+ super(Versions, self).__init__(None, serializers=serializers)
1648+
1649+ def dispatch(self, request, *args):
1650 """Respond to a request for all OpenStack API versions."""
1651 version_objs = [
1652 {
1653@@ -37,24 +50,6 @@
1654 },
1655 ]
1656
1657- builder = nova.api.openstack.views.versions.get_view_builder(req)
1658+ builder = nova.api.openstack.views.versions.get_view_builder(request)
1659 versions = [builder.build(version) for version in version_objs]
1660- response = dict(versions=versions)
1661-
1662- metadata = {
1663- "application/xml": {
1664- "attributes": {
1665- "version": ["status", "id"],
1666- "link": ["rel", "href"],
1667- }
1668- }
1669- }
1670-
1671- content_type = req.best_match_content_type()
1672- body = wsgi.Serializer(metadata).serialize(response, content_type)
1673-
1674- response = webob.Response()
1675- response.content_type = content_type
1676- response.body = body
1677-
1678- return response
1679+ return dict(versions=versions)
1680
1681=== added file 'nova/api/openstack/wsgi.py'
1682--- nova/api/openstack/wsgi.py 1970-01-01 00:00:00 +0000
1683+++ nova/api/openstack/wsgi.py 2011-05-26 21:32:29 +0000
1684@@ -0,0 +1,380 @@
1685+
1686+import json
1687+import webob
1688+from xml.dom import minidom
1689+
1690+from nova import exception
1691+from nova import log as logging
1692+from nova import utils
1693+from nova import wsgi
1694+
1695+
1696+XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
1697+XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
1698+
1699+LOG = logging.getLogger('nova.api.openstack.wsgi')
1700+
1701+
1702+class Request(webob.Request):
1703+ """Add some Openstack API-specific logic to the base webob.Request."""
1704+
1705+ def best_match_content_type(self):
1706+ """Determine the requested response content-type.
1707+
1708+ Based on the query extension then the Accept header.
1709+
1710+ """
1711+ supported = ('application/json', 'application/xml')
1712+
1713+ parts = self.path.rsplit('.', 1)
1714+ if len(parts) > 1:
1715+ ctype = 'application/{0}'.format(parts[1])
1716+ if ctype in supported:
1717+ return ctype
1718+
1719+ bm = self.accept.best_match(supported)
1720+
1721+ # default to application/json if we don't find a preference
1722+ return bm or 'application/json'
1723+
1724+ def get_content_type(self):
1725+ """Determine content type of the request body.
1726+
1727+ Does not do any body introspection, only checks header
1728+
1729+ """
1730+ if not "Content-Type" in self.headers:
1731+ raise exception.InvalidContentType(content_type=None)
1732+
1733+ allowed_types = ("application/xml", "application/json")
1734+ content_type = self.content_type
1735+
1736+ if content_type not in allowed_types:
1737+ raise exception.InvalidContentType(content_type=content_type)
1738+ else:
1739+ return content_type
1740+
1741+
1742+class TextDeserializer(object):
1743+ """Custom request body deserialization based on controller action name."""
1744+
1745+ def deserialize(self, datastring, action='default'):
1746+ """Find local deserialization method and parse request body."""
1747+ action_method = getattr(self, action, self.default)
1748+ return action_method(datastring)
1749+
1750+ def default(self, datastring):
1751+ """Default deserialization code should live here"""
1752+ raise NotImplementedError()
1753+
1754+
1755+class JSONDeserializer(TextDeserializer):
1756+
1757+ def default(self, datastring):
1758+ return utils.loads(datastring)
1759+
1760+
1761+class XMLDeserializer(TextDeserializer):
1762+
1763+ def __init__(self, metadata=None):
1764+ """
1765+ :param metadata: information needed to deserialize xml into
1766+ a dictionary.
1767+ """
1768+ super(XMLDeserializer, self).__init__()
1769+ self.metadata = metadata or {}
1770+
1771+ def default(self, datastring):
1772+ plurals = set(self.metadata.get('plurals', {}))
1773+ node = minidom.parseString(datastring).childNodes[0]
1774+ return {node.nodeName: self._from_xml_node(node, plurals)}
1775+
1776+ def _from_xml_node(self, node, listnames):
1777+ """Convert a minidom node to a simple Python type.
1778+
1779+ :param listnames: list of XML node names whose subnodes should
1780+ be considered list items.
1781+
1782+ """
1783+ if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
1784+ return node.childNodes[0].nodeValue
1785+ elif node.nodeName in listnames:
1786+ return [self._from_xml_node(n, listnames) for n in node.childNodes]
1787+ else:
1788+ result = dict()
1789+ for attr in node.attributes.keys():
1790+ result[attr] = node.attributes[attr].nodeValue
1791+ for child in node.childNodes:
1792+ if child.nodeType != node.TEXT_NODE:
1793+ result[child.nodeName] = self._from_xml_node(child,
1794+ listnames)
1795+ return result
1796+
1797+
1798+class RequestDeserializer(object):
1799+ """Break up a Request object into more useful pieces."""
1800+
1801+ def __init__(self, deserializers=None):
1802+ """
1803+ :param deserializers: dictionary of content-type-specific deserializers
1804+
1805+ """
1806+ self.deserializers = {
1807+ 'application/xml': XMLDeserializer(),
1808+ 'application/json': JSONDeserializer(),
1809+ }
1810+
1811+ self.deserializers.update(deserializers or {})
1812+
1813+ def deserialize(self, request):
1814+ """Extract necessary pieces of the request.
1815+
1816+ :param request: Request object
1817+ :returns tuple of expected controller action name, dictionary of
1818+ keyword arguments to pass to the controller, the expected
1819+ content type of the response
1820+
1821+ """
1822+ action_args = self.get_action_args(request.environ)
1823+ action = action_args.pop('action', None)
1824+
1825+ if request.method.lower() in ('post', 'put'):
1826+ if len(request.body) == 0:
1827+ action_args['body'] = None
1828+ else:
1829+ content_type = request.get_content_type()
1830+ deserializer = self.get_deserializer(content_type)
1831+
1832+ try:
1833+ body = deserializer.deserialize(request.body, action)
1834+ action_args['body'] = body
1835+ except exception.InvalidContentType:
1836+ action_args['body'] = None
1837+
1838+ accept = self.get_expected_content_type(request)
1839+
1840+ return (action, action_args, accept)
1841+
1842+ def get_deserializer(self, content_type):
1843+ try:
1844+ return self.deserializers[content_type]
1845+ except (KeyError, TypeError):
1846+ raise exception.InvalidContentType(content_type=content_type)
1847+
1848+ def get_expected_content_type(self, request):
1849+ return request.best_match_content_type()
1850+
1851+ def get_action_args(self, request_environment):
1852+ """Parse dictionary created by routes library."""
1853+ try:
1854+ args = request_environment['wsgiorg.routing_args'][1].copy()
1855+ except Exception:
1856+ return {}
1857+
1858+ try:
1859+ del args['controller']
1860+ except KeyError:
1861+ pass
1862+
1863+ try:
1864+ del args['format']
1865+ except KeyError:
1866+ pass
1867+
1868+ return args
1869+
1870+
1871+class DictSerializer(object):
1872+ """Custom response body serialization based on controller action name."""
1873+
1874+ def serialize(self, data, action='default'):
1875+ """Find local serialization method and encode response body."""
1876+ action_method = getattr(self, action, self.default)
1877+ return action_method(data)
1878+
1879+ def default(self, data):
1880+ """Default serialization code should live here"""
1881+ raise NotImplementedError()
1882+
1883+
1884+class JSONDictSerializer(DictSerializer):
1885+
1886+ def default(self, data):
1887+ return utils.dumps(data)
1888+
1889+
1890+class XMLDictSerializer(DictSerializer):
1891+
1892+ def __init__(self, metadata=None, xmlns=None):
1893+ """
1894+ :param metadata: information needed to deserialize xml into
1895+ a dictionary.
1896+ :param xmlns: XML namespace to include with serialized xml
1897+ """
1898+ super(XMLDictSerializer, self).__init__()
1899+ self.metadata = metadata or {}
1900+ self.xmlns = xmlns
1901+
1902+ def default(self, data):
1903+ # We expect data to contain a single key which is the XML root.
1904+ root_key = data.keys()[0]
1905+ doc = minidom.Document()
1906+ node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
1907+
1908+ xmlns = node.getAttribute('xmlns')
1909+ if not xmlns and self.xmlns:
1910+ node.setAttribute('xmlns', self.xmlns)
1911+
1912+ return node.toprettyxml(indent=' ')
1913+
1914+ def _to_xml_node(self, doc, metadata, nodename, data):
1915+ """Recursive method to convert data members to XML nodes."""
1916+ result = doc.createElement(nodename)
1917+
1918+ # Set the xml namespace if one is specified
1919+ # TODO(justinsb): We could also use prefixes on the keys
1920+ xmlns = metadata.get('xmlns', None)
1921+ if xmlns:
1922+ result.setAttribute('xmlns', xmlns)
1923+
1924+ #TODO(bcwaldon): accomplish this without a type-check
1925+ if type(data) is list:
1926+ collections = metadata.get('list_collections', {})
1927+ if nodename in collections:
1928+ metadata = collections[nodename]
1929+ for item in data:
1930+ node = doc.createElement(metadata['item_name'])
1931+ node.setAttribute(metadata['item_key'], str(item))
1932+ result.appendChild(node)
1933+ return result
1934+ singular = metadata.get('plurals', {}).get(nodename, None)
1935+ if singular is None:
1936+ if nodename.endswith('s'):
1937+ singular = nodename[:-1]
1938+ else:
1939+ singular = 'item'
1940+ for item in data:
1941+ node = self._to_xml_node(doc, metadata, singular, item)
1942+ result.appendChild(node)
1943+ #TODO(bcwaldon): accomplish this without a type-check
1944+ elif type(data) is dict:
1945+ collections = metadata.get('dict_collections', {})
1946+ if nodename in collections:
1947+ metadata = collections[nodename]
1948+ for k, v in data.items():
1949+ node = doc.createElement(metadata['item_name'])
1950+ node.setAttribute(metadata['item_key'], str(k))
1951+ text = doc.createTextNode(str(v))
1952+ node.appendChild(text)
1953+ result.appendChild(node)
1954+ return result
1955+ attrs = metadata.get('attributes', {}).get(nodename, {})
1956+ for k, v in data.items():
1957+ if k in attrs:
1958+ result.setAttribute(k, str(v))
1959+ else:
1960+ node = self._to_xml_node(doc, metadata, k, v)
1961+ result.appendChild(node)
1962+ else:
1963+ # Type is atom
1964+ node = doc.createTextNode(str(data))
1965+ result.appendChild(node)
1966+ return result
1967+
1968+
1969+class ResponseSerializer(object):
1970+ """Encode the necessary pieces into a response object"""
1971+
1972+ def __init__(self, serializers=None):
1973+ """
1974+ :param serializers: dictionary of content-type-specific serializers
1975+
1976+ """
1977+ self.serializers = {
1978+ 'application/xml': XMLDictSerializer(),
1979+ 'application/json': JSONDictSerializer(),
1980+ }
1981+ self.serializers.update(serializers or {})
1982+
1983+ def serialize(self, response_data, content_type):
1984+ """Serialize a dict into a string and wrap in a wsgi.Request object.
1985+
1986+ :param response_data: dict produced by the Controller
1987+ :param content_type: expected mimetype of serialized response body
1988+
1989+ """
1990+ response = webob.Response()
1991+ response.headers['Content-Type'] = content_type
1992+
1993+ serializer = self.get_serializer(content_type)
1994+ response.body = serializer.serialize(response_data)
1995+
1996+ return response
1997+
1998+ def get_serializer(self, content_type):
1999+ try:
2000+ return self.serializers[content_type]
2001+ except (KeyError, TypeError):
2002+ raise exception.InvalidContentType(content_type=content_type)
2003+
2004+
2005+class Resource(wsgi.Application):
2006+ """WSGI app that handles (de)serialization and controller dispatch.
2007+
2008+ WSGI app that reads routing information supplied by RoutesMiddleware
2009+ and calls the requested action method upon its controller. All
2010+ controller action methods must accept a 'req' argument, which is the
2011+ incoming wsgi.Request. If the operation is a PUT or POST, the controller
2012+ method must also accept a 'body' argument (the deserialized request body).
2013+ They may raise a webob.exc exception or return a dict, which will be
2014+ serialized by requested content type.
2015+
2016+ """
2017+ def __init__(self, controller, serializers=None, deserializers=None):
2018+ """
2019+ :param controller: object that implement methods created by routes lib
2020+ :param serializers: dict of content-type specific text serializers
2021+ :param deserializers: dict of content-type specific text deserializers
2022+
2023+ """
2024+ self.controller = controller
2025+ self.serializer = ResponseSerializer(serializers)
2026+ self.deserializer = RequestDeserializer(deserializers)
2027+
2028+ @webob.dec.wsgify(RequestClass=Request)
2029+ def __call__(self, request):
2030+ """WSGI method that controls (de)serialization and method dispatch."""
2031+
2032+ LOG.debug("%(method)s %(url)s" % {"method": request.method,
2033+ "url": request.url})
2034+
2035+ try:
2036+ action, action_args, accept = self.deserializer.deserialize(
2037+ request)
2038+ except exception.InvalidContentType:
2039+ return webob.exc.HTTPBadRequest(_("Unsupported Content-Type"))
2040+
2041+ action_result = self.dispatch(request, action, action_args)
2042+
2043+ #TODO(bcwaldon): find a more elegant way to pass through non-dict types
2044+ if type(action_result) is dict:
2045+ response = self.serializer.serialize(action_result, accept)
2046+ else:
2047+ response = action_result
2048+
2049+ try:
2050+ msg_dict = dict(url=request.url, status=response.status_int)
2051+ msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
2052+ except AttributeError:
2053+ msg_dict = dict(url=request.url)
2054+ msg = _("%(url)s returned a fault")
2055+
2056+ LOG.debug(msg)
2057+
2058+ return response
2059+
2060+ def dispatch(self, request, action, action_args):
2061+ """Find action-spefic method on controller and call it."""
2062+
2063+ controller_method = getattr(self.controller, action)
2064+ return controller_method(req=request, **action_args)
2065
2066=== modified file 'nova/api/openstack/zones.py'
2067--- nova/api/openstack/zones.py 2011-05-18 20:14:24 +0000
2068+++ nova/api/openstack/zones.py 2011-05-26 21:32:29 +0000
2069@@ -22,6 +22,7 @@
2070 from nova import flags
2071 from nova import log as logging
2072 from nova.api.openstack import common
2073+from nova.api.openstack import wsgi
2074 from nova.scheduler import api
2075
2076
2077@@ -52,12 +53,7 @@
2078 'deleted', 'deleted_at', 'updated_at'))
2079
2080
2081-class Controller(common.OpenstackController):
2082-
2083- _serialization_metadata = {
2084- 'application/xml': {
2085- "attributes": {
2086- "zone": ["id", "api_url", "name", "capabilities"]}}}
2087+class Controller(object):
2088
2089 def index(self, req):
2090 """Return all zones in brief"""
2091@@ -96,17 +92,15 @@
2092 api.zone_delete(req.environ['nova.context'], zone_id)
2093 return {}
2094
2095- def create(self, req):
2096+ def create(self, req, body):
2097 context = req.environ['nova.context']
2098- env = self._deserialize(req.body, req.get_content_type())
2099- zone = api.zone_create(context, env["zone"])
2100+ zone = api.zone_create(context, body["zone"])
2101 return dict(zone=_scrub_zone(zone))
2102
2103- def update(self, req, id):
2104+ def update(self, req, id, body):
2105 context = req.environ['nova.context']
2106- env = self._deserialize(req.body, req.get_content_type())
2107 zone_id = int(id)
2108- zone = api.zone_update(context, zone_id, env["zone"])
2109+ zone = api.zone_update(context, zone_id, body["zone"])
2110 return dict(zone=_scrub_zone(zone))
2111
2112 def select(self, req):
2113@@ -140,3 +134,18 @@
2114 cooked.append(dict(weight=entry['weight'],
2115 blob=cipher_text))
2116 return cooked
2117+
2118+
2119+def create_resource():
2120+ metadata = {
2121+ "attributes": {
2122+ "zone": ["id", "api_url", "name", "capabilities"],
2123+ },
2124+ }
2125+
2126+ serializers = {
2127+ 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
2128+ metadata=metadata),
2129+ }
2130+
2131+ return wsgi.Resource(Controller(), serializers=serializers)
2132
2133=== modified file 'nova/objectstore/s3server.py'
2134--- nova/objectstore/s3server.py 2011-03-24 23:38:31 +0000
2135+++ nova/objectstore/s3server.py 2011-05-26 21:32:29 +0000
2136@@ -81,7 +81,7 @@
2137 super(S3Application, self).__init__(mapper)
2138
2139
2140-class BaseRequestHandler(wsgi.Controller):
2141+class BaseRequestHandler(object):
2142 """Base class emulating Tornado's web framework pattern in WSGI.
2143
2144 This is a direct port of Tornado's implementation, so some key decisions
2145
2146=== modified file 'nova/tests/api/openstack/extensions/foxinsocks.py'
2147--- nova/tests/api/openstack/extensions/foxinsocks.py 2011-05-12 18:45:39 +0000
2148+++ nova/tests/api/openstack/extensions/foxinsocks.py 2011-05-26 21:32:29 +0000
2149@@ -17,12 +17,10 @@
2150
2151 import json
2152
2153-from nova import wsgi
2154-
2155 from nova.api.openstack import extensions
2156
2157
2158-class FoxInSocksController(wsgi.Controller):
2159+class FoxInSocksController(object):
2160
2161 def index(self, req):
2162 return "Try to say this Mr. Knox, sir..."
2163
2164=== modified file 'nova/tests/api/openstack/test_extensions.py'
2165--- nova/tests/api/openstack/test_extensions.py 2011-05-12 18:37:15 +0000
2166+++ nova/tests/api/openstack/test_extensions.py 2011-05-26 21:32:29 +0000
2167@@ -26,15 +26,15 @@
2168 from nova.api import openstack
2169 from nova.api.openstack import extensions
2170 from nova.api.openstack import flavors
2171+from nova.api.openstack import wsgi
2172 from nova.tests.api.openstack import fakes
2173-import nova.wsgi
2174
2175 FLAGS = flags.FLAGS
2176
2177 response_body = "Try to say this Mr. Knox, sir..."
2178
2179
2180-class StubController(nova.wsgi.Controller):
2181+class StubController(object):
2182
2183 def __init__(self, body):
2184 self.body = body
2185
2186=== modified file 'nova/tests/api/openstack/test_limits.py'
2187--- nova/tests/api/openstack/test_limits.py 2011-05-25 20:58:40 +0000
2188+++ nova/tests/api/openstack/test_limits.py 2011-05-26 21:32:29 +0000
2189@@ -73,7 +73,7 @@
2190 def setUp(self):
2191 """Run before each test."""
2192 BaseLimitTestSuite.setUp(self)
2193- self.controller = limits.LimitsControllerV10()
2194+ self.controller = limits.create_resource('1.0')
2195
2196 def _get_index_request(self, accept_header="application/json"):
2197 """Helper to set routing arguments."""
2198@@ -209,7 +209,7 @@
2199 def setUp(self):
2200 """Run before each test."""
2201 BaseLimitTestSuite.setUp(self)
2202- self.controller = limits.LimitsControllerV11()
2203+ self.controller = limits.create_resource('1.1')
2204
2205 def _get_index_request(self, accept_header="application/json"):
2206 """Helper to set routing arguments."""
2207
2208=== modified file 'nova/tests/api/openstack/test_servers.py'
2209--- nova/tests/api/openstack/test_servers.py 2011-05-25 20:10:25 +0000
2210+++ nova/tests/api/openstack/test_servers.py 2011-05-26 21:32:29 +0000
2211@@ -217,7 +217,6 @@
2212 },
2213 ]
2214
2215- print res_dict['server']
2216 self.assertEqual(res_dict['server']['links'], expected_links)
2217
2218 def test_get_server_by_id_with_addresses_xml(self):
2219@@ -844,7 +843,6 @@
2220 req = webob.Request.blank('/v1.0/servers/detail')
2221 req.headers['Accept'] = 'application/xml'
2222 res = req.get_response(fakes.wsgi_app())
2223- print res.body
2224 dom = minidom.parseString(res.body)
2225 for i, server in enumerate(dom.getElementsByTagName('server')):
2226 self.assertEqual(server.getAttribute('id'), str(i))
2227@@ -1008,6 +1006,14 @@
2228 res = req.get_response(fakes.wsgi_app())
2229 self.assertEqual(res.status_int, 501)
2230
2231+ def test_server_change_password_xml(self):
2232+ req = webob.Request.blank('/v1.0/servers/1/action')
2233+ req.method = 'POST'
2234+ req.content_type = 'application/xml'
2235+ req.body = '<changePassword adminPass="1234pass">'
2236+# res = req.get_response(fakes.wsgi_app())
2237+# self.assertEqual(res.status_int, 501)
2238+
2239 def test_server_change_password_v1_1(self):
2240 mock_method = MockSetAdminPassword()
2241 self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
2242@@ -1380,13 +1386,13 @@
2243 class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
2244
2245 def setUp(self):
2246- self.deserializer = servers.ServerCreateRequestXMLDeserializer()
2247+ self.deserializer = servers.ServerXMLDeserializer()
2248
2249 def test_minimal_request(self):
2250 serial_request = """
2251 <server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
2252 name="new-server-test" imageId="1" flavorId="1"/>"""
2253- request = self.deserializer.deserialize(serial_request)
2254+ request = self.deserializer.deserialize(serial_request, 'create')
2255 expected = {"server": {
2256 "name": "new-server-test",
2257 "imageId": "1",
2258@@ -1400,7 +1406,7 @@
2259 name="new-server-test" imageId="1" flavorId="1">
2260 <metadata/>
2261 </server>"""
2262- request = self.deserializer.deserialize(serial_request)
2263+ request = self.deserializer.deserialize(serial_request, 'create')
2264 expected = {"server": {
2265 "name": "new-server-test",
2266 "imageId": "1",
2267@@ -1415,7 +1421,7 @@
2268 name="new-server-test" imageId="1" flavorId="1">
2269 <personality/>
2270 </server>"""
2271- request = self.deserializer.deserialize(serial_request)
2272+ request = self.deserializer.deserialize(serial_request, 'create')
2273 expected = {"server": {
2274 "name": "new-server-test",
2275 "imageId": "1",
2276@@ -1431,7 +1437,7 @@
2277 <metadata/>
2278 <personality/>
2279 </server>"""
2280- request = self.deserializer.deserialize(serial_request)
2281+ request = self.deserializer.deserialize(serial_request, 'create')
2282 expected = {"server": {
2283 "name": "new-server-test",
2284 "imageId": "1",
2285@@ -1448,7 +1454,7 @@
2286 <personality/>
2287 <metadata/>
2288 </server>"""
2289- request = self.deserializer.deserialize(serial_request)
2290+ request = self.deserializer.deserialize(serial_request, 'create')
2291 expected = {"server": {
2292 "name": "new-server-test",
2293 "imageId": "1",
2294@@ -1466,7 +1472,7 @@
2295 <file path="/etc/conf">aabbccdd</file>
2296 </personality>
2297 </server>"""
2298- request = self.deserializer.deserialize(serial_request)
2299+ request = self.deserializer.deserialize(serial_request, 'create')
2300 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
2301 self.assertEquals(request["server"]["personality"], expected)
2302
2303@@ -1476,7 +1482,7 @@
2304 name="new-server-test" imageId="1" flavorId="1">
2305 <personality><file path="/etc/conf">aabbccdd</file>
2306 <file path="/etc/sudoers">abcd</file></personality></server>"""
2307- request = self.deserializer.deserialize(serial_request)
2308+ request = self.deserializer.deserialize(serial_request, 'create')
2309 expected = [{"path": "/etc/conf", "contents": "aabbccdd"},
2310 {"path": "/etc/sudoers", "contents": "abcd"}]
2311 self.assertEquals(request["server"]["personality"], expected)
2312@@ -1492,7 +1498,7 @@
2313 <file path="/etc/ignoreme">anything</file>
2314 </personality>
2315 </server>"""
2316- request = self.deserializer.deserialize(serial_request)
2317+ request = self.deserializer.deserialize(serial_request, 'create')
2318 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
2319 self.assertEquals(request["server"]["personality"], expected)
2320
2321@@ -1501,7 +1507,7 @@
2322 <server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
2323 name="new-server-test" imageId="1" flavorId="1">
2324 <personality><file>aabbccdd</file></personality></server>"""
2325- request = self.deserializer.deserialize(serial_request)
2326+ request = self.deserializer.deserialize(serial_request, 'create')
2327 expected = [{"contents": "aabbccdd"}]
2328 self.assertEquals(request["server"]["personality"], expected)
2329
2330@@ -1510,7 +1516,7 @@
2331 <server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
2332 name="new-server-test" imageId="1" flavorId="1">
2333 <personality><file path="/etc/conf"></file></personality></server>"""
2334- request = self.deserializer.deserialize(serial_request)
2335+ request = self.deserializer.deserialize(serial_request, 'create')
2336 expected = [{"path": "/etc/conf", "contents": ""}]
2337 self.assertEquals(request["server"]["personality"], expected)
2338
2339@@ -1519,7 +1525,7 @@
2340 <server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
2341 name="new-server-test" imageId="1" flavorId="1">
2342 <personality><file path="/etc/conf"/></personality></server>"""
2343- request = self.deserializer.deserialize(serial_request)
2344+ request = self.deserializer.deserialize(serial_request, 'create')
2345 expected = [{"path": "/etc/conf", "contents": ""}]
2346 self.assertEquals(request["server"]["personality"], expected)
2347
2348@@ -1531,7 +1537,7 @@
2349 <meta key="alpha">beta</meta>
2350 </metadata>
2351 </server>"""
2352- request = self.deserializer.deserialize(serial_request)
2353+ request = self.deserializer.deserialize(serial_request, 'create')
2354 expected = {"alpha": "beta"}
2355 self.assertEquals(request["server"]["metadata"], expected)
2356
2357@@ -1544,7 +1550,7 @@
2358 <meta key="foo">bar</meta>
2359 </metadata>
2360 </server>"""
2361- request = self.deserializer.deserialize(serial_request)
2362+ request = self.deserializer.deserialize(serial_request, 'create')
2363 expected = {"alpha": "beta", "foo": "bar"}
2364 self.assertEquals(request["server"]["metadata"], expected)
2365
2366@@ -1556,7 +1562,7 @@
2367 <meta key="alpha"></meta>
2368 </metadata>
2369 </server>"""
2370- request = self.deserializer.deserialize(serial_request)
2371+ request = self.deserializer.deserialize(serial_request, 'create')
2372 expected = {"alpha": ""}
2373 self.assertEquals(request["server"]["metadata"], expected)
2374
2375@@ -1569,7 +1575,7 @@
2376 <meta key="delta"/>
2377 </metadata>
2378 </server>"""
2379- request = self.deserializer.deserialize(serial_request)
2380+ request = self.deserializer.deserialize(serial_request, 'create')
2381 expected = {"alpha": "", "delta": ""}
2382 self.assertEquals(request["server"]["metadata"], expected)
2383
2384@@ -1581,7 +1587,7 @@
2385 <meta>beta</meta>
2386 </metadata>
2387 </server>"""
2388- request = self.deserializer.deserialize(serial_request)
2389+ request = self.deserializer.deserialize(serial_request, 'create')
2390 expected = {"": "beta"}
2391 self.assertEquals(request["server"]["metadata"], expected)
2392
2393@@ -1594,7 +1600,7 @@
2394 <meta>gamma</meta>
2395 </metadata>
2396 </server>"""
2397- request = self.deserializer.deserialize(serial_request)
2398+ request = self.deserializer.deserialize(serial_request, 'create')
2399 expected = {"": "gamma"}
2400 self.assertEquals(request["server"]["metadata"], expected)
2401
2402@@ -1607,7 +1613,7 @@
2403 <meta key="foo">baz</meta>
2404 </metadata>
2405 </server>"""
2406- request = self.deserializer.deserialize(serial_request)
2407+ request = self.deserializer.deserialize(serial_request, 'create')
2408 expected = {"foo": "baz"}
2409 self.assertEquals(request["server"]["metadata"], expected)
2410
2411@@ -1654,7 +1660,7 @@
2412 },
2413 ],
2414 }}
2415- request = self.deserializer.deserialize(serial_request)
2416+ request = self.deserializer.deserialize(serial_request, 'create')
2417 self.assertEqual(request, expected)
2418
2419 def test_request_xmlser_with_flavor_image_ref(self):
2420@@ -1664,7 +1670,7 @@
2421 imageRef="http://localhost:8774/v1.1/images/1"
2422 flavorRef="http://localhost:8774/v1.1/flavors/1">
2423 </server>"""
2424- request = self.deserializer.deserialize(serial_request)
2425+ request = self.deserializer.deserialize(serial_request, 'create')
2426 self.assertEquals(request["server"]["flavorRef"],
2427 "http://localhost:8774/v1.1/flavors/1")
2428 self.assertEquals(request["server"]["imageRef"],
2429
2430=== added file 'nova/tests/api/openstack/test_wsgi.py'
2431--- nova/tests/api/openstack/test_wsgi.py 1970-01-01 00:00:00 +0000
2432+++ nova/tests/api/openstack/test_wsgi.py 2011-05-26 21:32:29 +0000
2433@@ -0,0 +1,293 @@
2434+# vim: tabstop=4 shiftwidth=4 softtabstop=4
2435+
2436+import json
2437+import webob
2438+
2439+from nova import exception
2440+from nova import test
2441+from nova.api.openstack import wsgi
2442+
2443+
2444+class RequestTest(test.TestCase):
2445+ def test_content_type_missing(self):
2446+ request = wsgi.Request.blank('/tests/123')
2447+ request.body = "<body />"
2448+ self.assertRaises(exception.InvalidContentType,
2449+ request.get_content_type)
2450+
2451+ def test_content_type_unsupported(self):
2452+ request = wsgi.Request.blank('/tests/123')
2453+ request.headers["Content-Type"] = "text/html"
2454+ request.body = "asdf<br />"
2455+ self.assertRaises(exception.InvalidContentType,
2456+ request.get_content_type)
2457+
2458+ def test_content_type_with_charset(self):
2459+ request = wsgi.Request.blank('/tests/123')
2460+ request.headers["Content-Type"] = "application/json; charset=UTF-8"
2461+ result = request.get_content_type()
2462+ self.assertEqual(result, "application/json")
2463+
2464+ def test_content_type_from_accept_xml(self):
2465+ request = wsgi.Request.blank('/tests/123')
2466+ request.headers["Accept"] = "application/xml"
2467+ result = request.best_match_content_type()
2468+ self.assertEqual(result, "application/xml")
2469+
2470+ request = wsgi.Request.blank('/tests/123')
2471+ request.headers["Accept"] = "application/json"
2472+ result = request.best_match_content_type()
2473+ self.assertEqual(result, "application/json")
2474+
2475+ request = wsgi.Request.blank('/tests/123')
2476+ request.headers["Accept"] = "application/xml, application/json"
2477+ result = request.best_match_content_type()
2478+ self.assertEqual(result, "application/json")
2479+
2480+ request = wsgi.Request.blank('/tests/123')
2481+ request.headers["Accept"] = \
2482+ "application/json; q=0.3, application/xml; q=0.9"
2483+ result = request.best_match_content_type()
2484+ self.assertEqual(result, "application/xml")
2485+
2486+ def test_content_type_from_query_extension(self):
2487+ request = wsgi.Request.blank('/tests/123.xml')
2488+ result = request.best_match_content_type()
2489+ self.assertEqual(result, "application/xml")
2490+
2491+ request = wsgi.Request.blank('/tests/123.json')
2492+ result = request.best_match_content_type()
2493+ self.assertEqual(result, "application/json")
2494+
2495+ request = wsgi.Request.blank('/tests/123.invalid')
2496+ result = request.best_match_content_type()
2497+ self.assertEqual(result, "application/json")
2498+
2499+ def test_content_type_accept_and_query_extension(self):
2500+ request = wsgi.Request.blank('/tests/123.xml')
2501+ request.headers["Accept"] = "application/json"
2502+ result = request.best_match_content_type()
2503+ self.assertEqual(result, "application/xml")
2504+
2505+ def test_content_type_accept_default(self):
2506+ request = wsgi.Request.blank('/tests/123.unsupported')
2507+ request.headers["Accept"] = "application/unsupported1"
2508+ result = request.best_match_content_type()
2509+ self.assertEqual(result, "application/json")
2510+
2511+
2512+class DictSerializerTest(test.TestCase):
2513+ def test_dispatch(self):
2514+ serializer = wsgi.DictSerializer()
2515+ serializer.create = lambda x: 'pants'
2516+ serializer.default = lambda x: 'trousers'
2517+ self.assertEqual(serializer.serialize({}, 'create'), 'pants')
2518+
2519+ def test_dispatch_default(self):
2520+ serializer = wsgi.DictSerializer()
2521+ serializer.create = lambda x: 'pants'
2522+ serializer.default = lambda x: 'trousers'
2523+ self.assertEqual(serializer.serialize({}, 'update'), 'trousers')
2524+
2525+
2526+class XMLDictSerializerTest(test.TestCase):
2527+ def test_xml(self):
2528+ input_dict = dict(servers=dict(a=(2, 3)))
2529+ expected_xml = '<serversxmlns="asdf"><a>(2,3)</a></servers>'
2530+ serializer = wsgi.XMLDictSerializer(xmlns="asdf")
2531+ result = serializer.serialize(input_dict)
2532+ result = result.replace('\n', '').replace(' ', '')
2533+ self.assertEqual(result, expected_xml)
2534+
2535+
2536+class JSONDictSerializerTest(test.TestCase):
2537+ def test_json(self):
2538+ input_dict = dict(servers=dict(a=(2, 3)))
2539+ expected_json = '{"servers":{"a":[2,3]}}'
2540+ serializer = wsgi.JSONDictSerializer()
2541+ result = serializer.serialize(input_dict)
2542+ result = result.replace('\n', '').replace(' ', '')
2543+ self.assertEqual(result, expected_json)
2544+
2545+
2546+class TextDeserializerTest(test.TestCase):
2547+ def test_dispatch(self):
2548+ deserializer = wsgi.TextDeserializer()
2549+ deserializer.create = lambda x: 'pants'
2550+ deserializer.default = lambda x: 'trousers'
2551+ self.assertEqual(deserializer.deserialize({}, 'create'), 'pants')
2552+
2553+ def test_dispatch_default(self):
2554+ deserializer = wsgi.TextDeserializer()
2555+ deserializer.create = lambda x: 'pants'
2556+ deserializer.default = lambda x: 'trousers'
2557+ self.assertEqual(deserializer.deserialize({}, 'update'), 'trousers')
2558+
2559+
2560+class JSONDeserializerTest(test.TestCase):
2561+ def test_json(self):
2562+ data = """{"a": {
2563+ "a1": "1",
2564+ "a2": "2",
2565+ "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
2566+ "d": {"e": "1"},
2567+ "f": "1"}}"""
2568+ as_dict = dict(a={
2569+ 'a1': '1',
2570+ 'a2': '2',
2571+ 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
2572+ 'd': {'e': '1'},
2573+ 'f': '1'})
2574+ deserializer = wsgi.JSONDeserializer()
2575+ self.assertEqual(deserializer.deserialize(data), as_dict)
2576+
2577+
2578+class XMLDeserializerTest(test.TestCase):
2579+ def test_xml(self):
2580+ xml = """
2581+ <a a1="1" a2="2">
2582+ <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs>
2583+ <d><e>1</e></d>
2584+ <f>1</f>
2585+ </a>
2586+ """.strip()
2587+ as_dict = dict(a={
2588+ 'a1': '1',
2589+ 'a2': '2',
2590+ 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
2591+ 'd': {'e': '1'},
2592+ 'f': '1'})
2593+ metadata = {'plurals': {'bs': 'b', 'ts': 't'}}
2594+ deserializer = wsgi.XMLDeserializer(metadata=metadata)
2595+ self.assertEqual(deserializer.deserialize(xml), as_dict)
2596+
2597+ def test_xml_empty(self):
2598+ xml = """<a></a>"""
2599+ as_dict = {"a": {}}
2600+ deserializer = wsgi.XMLDeserializer()
2601+ self.assertEqual(deserializer.deserialize(xml), as_dict)
2602+
2603+
2604+class ResponseSerializerTest(test.TestCase):
2605+ def setUp(self):
2606+ class JSONSerializer(object):
2607+ def serialize(self, data):
2608+ return 'pew_json'
2609+
2610+ class XMLSerializer(object):
2611+ def serialize(self, data):
2612+ return 'pew_xml'
2613+
2614+ self.serializers = {
2615+ 'application/json': JSONSerializer(),
2616+ 'application/XML': XMLSerializer(),
2617+ }
2618+
2619+ self.serializer = wsgi.ResponseSerializer(serializers=self.serializers)
2620+
2621+ def tearDown(self):
2622+ pass
2623+
2624+ def test_get_serializer(self):
2625+ self.assertEqual(self.serializer.get_serializer('application/json'),
2626+ self.serializers['application/json'])
2627+
2628+ def test_get_serializer_unknown_content_type(self):
2629+ self.assertRaises(exception.InvalidContentType,
2630+ self.serializer.get_serializer,
2631+ 'application/unknown')
2632+
2633+ def test_serialize_response(self):
2634+ response = self.serializer.serialize({}, 'application/json')
2635+ self.assertEqual(response.headers['Content-Type'], 'application/json')
2636+ self.assertEqual(response.body, 'pew_json')
2637+
2638+ def test_serialize_response_dict_to_unknown_content_type(self):
2639+ self.assertRaises(exception.InvalidContentType,
2640+ self.serializer.serialize,
2641+ {}, 'application/unknown')
2642+
2643+
2644+class RequestDeserializerTest(test.TestCase):
2645+ def setUp(self):
2646+ class JSONDeserializer(object):
2647+ def deserialize(self, data):
2648+ return 'pew_json'
2649+
2650+ class XMLDeserializer(object):
2651+ def deserialize(self, data):
2652+ return 'pew_xml'
2653+
2654+ self.deserializers = {
2655+ 'application/json': JSONDeserializer(),
2656+ 'application/XML': XMLDeserializer(),
2657+ }
2658+
2659+ self.deserializer = wsgi.RequestDeserializer(
2660+ deserializers=self.deserializers)
2661+
2662+ def tearDown(self):
2663+ pass
2664+
2665+ def test_get_deserializer(self):
2666+ expected = self.deserializer.get_deserializer('application/json')
2667+ self.assertEqual(expected, self.deserializers['application/json'])
2668+
2669+ def test_get_deserializer_unknown_content_type(self):
2670+ self.assertRaises(exception.InvalidContentType,
2671+ self.deserializer.get_deserializer,
2672+ 'application/unknown')
2673+
2674+ def test_get_expected_content_type(self):
2675+ request = wsgi.Request.blank('/')
2676+ request.headers['Accept'] = 'application/json'
2677+ self.assertEqual(self.deserializer.get_expected_content_type(request),
2678+ 'application/json')
2679+
2680+ def test_get_action_args(self):
2681+ env = {
2682+ 'wsgiorg.routing_args': [None, {
2683+ 'controller': None,
2684+ 'format': None,
2685+ 'action': 'update',
2686+ 'id': 12,
2687+ }],
2688+ }
2689+
2690+ expected = {'action': 'update', 'id': 12}
2691+
2692+ self.assertEqual(self.deserializer.get_action_args(env), expected)
2693+
2694+ def test_deserialize(self):
2695+ def fake_get_routing_args(request):
2696+ return {'action': 'create'}
2697+ self.deserializer.get_action_args = fake_get_routing_args
2698+
2699+ request = wsgi.Request.blank('/')
2700+ request.headers['Accept'] = 'application/xml'
2701+
2702+ deserialized = self.deserializer.deserialize(request)
2703+ expected = ('create', {}, 'application/xml')
2704+
2705+ self.assertEqual(expected, deserialized)
2706+
2707+
2708+class ResourceTest(test.TestCase):
2709+ def test_dispatch(self):
2710+ class Controller(object):
2711+ def index(self, req, pants=None):
2712+ return pants
2713+
2714+ resource = wsgi.Resource(Controller())
2715+ actual = resource.dispatch(None, 'index', {'pants': 'off'})
2716+ expected = 'off'
2717+ self.assertEqual(actual, expected)
2718+
2719+ def test_dispatch_unknown_controller_action(self):
2720+ class Controller(object):
2721+ def index(self, req, pants=None):
2722+ return pants
2723+
2724+ resource = wsgi.Resource(Controller())
2725+ self.assertRaises(AttributeError, resource.dispatch,
2726+ None, 'create', {})
2727
2728=== modified file 'nova/tests/api/test_wsgi.py'
2729--- nova/tests/api/test_wsgi.py 2011-04-20 18:01:14 +0000
2730+++ nova/tests/api/test_wsgi.py 2011-05-26 21:32:29 +0000
2731@@ -67,192 +67,3 @@
2732 self.assertEqual(result.body, "Router result")
2733 result = webob.Request.blank('/bad').get_response(Router())
2734 self.assertNotEqual(result.body, "Router result")
2735-
2736-
2737-class ControllerTest(test.TestCase):
2738-
2739- class TestRouter(wsgi.Router):
2740-
2741- class TestController(wsgi.Controller):
2742-
2743- _serialization_metadata = {
2744- 'application/xml': {
2745- "attributes": {
2746- "test": ["id"]}}}
2747-
2748- def show(self, req, id): # pylint: disable=W0622,C0103
2749- return {"test": {"id": id}}
2750-
2751- def __init__(self):
2752- mapper = routes.Mapper()
2753- mapper.resource("test", "tests", controller=self.TestController())
2754- wsgi.Router.__init__(self, mapper)
2755-
2756- def test_show(self):
2757- request = wsgi.Request.blank('/tests/123')
2758- result = request.get_response(self.TestRouter())
2759- self.assertEqual(json.loads(result.body), {"test": {"id": "123"}})
2760-
2761- def test_response_content_type_from_accept_xml(self):
2762- request = webob.Request.blank('/tests/123')
2763- request.headers["Accept"] = "application/xml"
2764- result = request.get_response(self.TestRouter())
2765- self.assertEqual(result.headers["Content-Type"], "application/xml")
2766-
2767- def test_response_content_type_from_accept_json(self):
2768- request = wsgi.Request.blank('/tests/123')
2769- request.headers["Accept"] = "application/json"
2770- result = request.get_response(self.TestRouter())
2771- self.assertEqual(result.headers["Content-Type"], "application/json")
2772-
2773- def test_response_content_type_from_query_extension_xml(self):
2774- request = wsgi.Request.blank('/tests/123.xml')
2775- result = request.get_response(self.TestRouter())
2776- self.assertEqual(result.headers["Content-Type"], "application/xml")
2777-
2778- def test_response_content_type_from_query_extension_json(self):
2779- request = wsgi.Request.blank('/tests/123.json')
2780- result = request.get_response(self.TestRouter())
2781- self.assertEqual(result.headers["Content-Type"], "application/json")
2782-
2783- def test_response_content_type_default_when_unsupported(self):
2784- request = wsgi.Request.blank('/tests/123.unsupported')
2785- request.headers["Accept"] = "application/unsupported1"
2786- result = request.get_response(self.TestRouter())
2787- self.assertEqual(result.status_int, 200)
2788- self.assertEqual(result.headers["Content-Type"], "application/json")
2789-
2790-
2791-class RequestTest(test.TestCase):
2792-
2793- def test_request_content_type_missing(self):
2794- request = wsgi.Request.blank('/tests/123')
2795- request.body = "<body />"
2796- self.assertRaises(webob.exc.HTTPBadRequest, request.get_content_type)
2797-
2798- def test_request_content_type_unsupported(self):
2799- request = wsgi.Request.blank('/tests/123')
2800- request.headers["Content-Type"] = "text/html"
2801- request.body = "asdf<br />"
2802- self.assertRaises(webob.exc.HTTPBadRequest, request.get_content_type)
2803-
2804- def test_request_content_type_with_charset(self):
2805- request = wsgi.Request.blank('/tests/123')
2806- request.headers["Content-Type"] = "application/json; charset=UTF-8"
2807- result = request.get_content_type()
2808- self.assertEqual(result, "application/json")
2809-
2810- def test_content_type_from_accept_xml(self):
2811- request = wsgi.Request.blank('/tests/123')
2812- request.headers["Accept"] = "application/xml"
2813- result = request.best_match_content_type()
2814- self.assertEqual(result, "application/xml")
2815-
2816- request = wsgi.Request.blank('/tests/123')
2817- request.headers["Accept"] = "application/json"
2818- result = request.best_match_content_type()
2819- self.assertEqual(result, "application/json")
2820-
2821- request = wsgi.Request.blank('/tests/123')
2822- request.headers["Accept"] = "application/xml, application/json"
2823- result = request.best_match_content_type()
2824- self.assertEqual(result, "application/json")
2825-
2826- request = wsgi.Request.blank('/tests/123')
2827- request.headers["Accept"] = \
2828- "application/json; q=0.3, application/xml; q=0.9"
2829- result = request.best_match_content_type()
2830- self.assertEqual(result, "application/xml")
2831-
2832- def test_content_type_from_query_extension(self):
2833- request = wsgi.Request.blank('/tests/123.xml')
2834- result = request.best_match_content_type()
2835- self.assertEqual(result, "application/xml")
2836-
2837- request = wsgi.Request.blank('/tests/123.json')
2838- result = request.best_match_content_type()
2839- self.assertEqual(result, "application/json")
2840-
2841- request = wsgi.Request.blank('/tests/123.invalid')
2842- result = request.best_match_content_type()
2843- self.assertEqual(result, "application/json")
2844-
2845- def test_content_type_accept_and_query_extension(self):
2846- request = wsgi.Request.blank('/tests/123.xml')
2847- request.headers["Accept"] = "application/json"
2848- result = request.best_match_content_type()
2849- self.assertEqual(result, "application/xml")
2850-
2851- def test_content_type_accept_default(self):
2852- request = wsgi.Request.blank('/tests/123.unsupported')
2853- request.headers["Accept"] = "application/unsupported1"
2854- result = request.best_match_content_type()
2855- self.assertEqual(result, "application/json")
2856-
2857-
2858-class SerializerTest(test.TestCase):
2859-
2860- def test_xml(self):
2861- input_dict = dict(servers=dict(a=(2, 3)))
2862- expected_xml = '<servers><a>(2,3)</a></servers>'
2863- serializer = wsgi.Serializer()
2864- result = serializer.serialize(input_dict, "application/xml")
2865- result = result.replace('\n', '').replace(' ', '')
2866- self.assertEqual(result, expected_xml)
2867-
2868- def test_json(self):
2869- input_dict = dict(servers=dict(a=(2, 3)))
2870- expected_json = '{"servers":{"a":[2,3]}}'
2871- serializer = wsgi.Serializer()
2872- result = serializer.serialize(input_dict, "application/json")
2873- result = result.replace('\n', '').replace(' ', '')
2874- self.assertEqual(result, expected_json)
2875-
2876- def test_unsupported_content_type(self):
2877- serializer = wsgi.Serializer()
2878- self.assertRaises(exception.InvalidContentType, serializer.serialize,
2879- {}, "text/null")
2880-
2881- def test_deserialize_json(self):
2882- data = """{"a": {
2883- "a1": "1",
2884- "a2": "2",
2885- "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
2886- "d": {"e": "1"},
2887- "f": "1"}}"""
2888- as_dict = dict(a={
2889- 'a1': '1',
2890- 'a2': '2',
2891- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
2892- 'd': {'e': '1'},
2893- 'f': '1'})
2894- metadata = {}
2895- serializer = wsgi.Serializer(metadata)
2896- self.assertEqual(serializer.deserialize(data, "application/json"),
2897- as_dict)
2898-
2899- def test_deserialize_xml(self):
2900- xml = """
2901- <a a1="1" a2="2">
2902- <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs>
2903- <d><e>1</e></d>
2904- <f>1</f>
2905- </a>
2906- """.strip()
2907- as_dict = dict(a={
2908- 'a1': '1',
2909- 'a2': '2',
2910- 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
2911- 'd': {'e': '1'},
2912- 'f': '1'})
2913- metadata = {'application/xml': dict(plurals={'bs': 'b', 'ts': 't'})}
2914- serializer = wsgi.Serializer(metadata)
2915- self.assertEqual(serializer.deserialize(xml, "application/xml"),
2916- as_dict)
2917-
2918- def test_deserialize_empty_xml(self):
2919- xml = """<a></a>"""
2920- as_dict = {"a": {}}
2921- serializer = wsgi.Serializer()
2922- self.assertEqual(serializer.deserialize(xml, "application/xml"),
2923- as_dict)
2924
2925=== modified file 'nova/tests/integrated/test_xml.py'
2926--- nova/tests/integrated/test_xml.py 2011-03-30 17:05:06 +0000
2927+++ nova/tests/integrated/test_xml.py 2011-05-26 21:32:29 +0000
2928@@ -32,7 +32,7 @@
2929 """"Some basic XML sanity checks."""
2930
2931 def test_namespace_limits(self):
2932- """/limits should have v1.0 namespace (hasn't changed in 1.1)."""
2933+ """/limits should have v1.1 namespace (has changed in 1.1)."""
2934 headers = {}
2935 headers['Accept'] = 'application/xml'
2936
2937@@ -40,7 +40,7 @@
2938 data = response.read()
2939 LOG.debug("data: %s" % data)
2940
2941- prefix = '<limits xmlns="%s"' % common.XML_NS_V10
2942+ prefix = '<limits xmlns="%s"' % common.XML_NS_V11
2943 self.assertTrue(data.startswith(prefix))
2944
2945 def test_namespace_servers(self):
2946
2947=== modified file 'nova/wsgi.py'
2948--- nova/wsgi.py 2011-05-20 04:03:15 +0000
2949+++ nova/wsgi.py 2011-05-26 21:32:29 +0000
2950@@ -85,36 +85,7 @@
2951
2952
2953 class Request(webob.Request):
2954-
2955- def best_match_content_type(self):
2956- """Determine the most acceptable content-type.
2957-
2958- Based on the query extension then the Accept header.
2959-
2960- """
2961- parts = self.path.rsplit('.', 1)
2962-
2963- if len(parts) > 1:
2964- format = parts[1]
2965- if format in ['json', 'xml']:
2966- return 'application/{0}'.format(parts[1])
2967-
2968- ctypes = ['application/json', 'application/xml']
2969- bm = self.accept.best_match(ctypes)
2970-
2971- return bm or 'application/json'
2972-
2973- def get_content_type(self):
2974- allowed_types = ("application/xml", "application/json")
2975- if not "Content-Type" in self.headers:
2976- msg = _("Missing Content-Type")
2977- LOG.debug(msg)
2978- raise webob.exc.HTTPBadRequest(msg)
2979- type = self.content_type
2980- if type in allowed_types:
2981- return type
2982- LOG.debug(_("Wrong Content-Type: %s") % type)
2983- raise webob.exc.HTTPBadRequest("Invalid content type")
2984+ pass
2985
2986
2987 class Application(object):
2988@@ -289,8 +260,8 @@
2989
2990 Each route in `mapper` must specify a 'controller', which is a
2991 WSGI app to call. You'll probably want to specify an 'action' as
2992- well and have your controller be a wsgi.Controller, who will route
2993- the request to the action method.
2994+ well and have your controller be an object that can route
2995+ the request to the action-specific method.
2996
2997 Examples:
2998 mapper = routes.Mapper()
2999@@ -338,223 +309,6 @@
3000 return app
3001
3002
3003-class Controller(object):
3004- """WSGI app that dispatched to methods.
3005-
3006- WSGI app that reads routing information supplied by RoutesMiddleware
3007- and calls the requested action method upon itself. All action methods
3008- must, in addition to their normal parameters, accept a 'req' argument
3009- which is the incoming wsgi.Request. They raise a webob.exc exception,
3010- or return a dict which will be serialized by requested content type.
3011-
3012- """
3013-
3014- @webob.dec.wsgify(RequestClass=Request)
3015- def __call__(self, req):
3016- """Call the method specified in req.environ by RoutesMiddleware."""
3017- arg_dict = req.environ['wsgiorg.routing_args'][1]
3018- action = arg_dict['action']
3019- method = getattr(self, action)
3020- LOG.debug("%s %s" % (req.method, req.url))
3021- del arg_dict['controller']
3022- del arg_dict['action']
3023- if 'format' in arg_dict:
3024- del arg_dict['format']
3025- arg_dict['req'] = req
3026- result = method(**arg_dict)
3027-
3028- if type(result) is dict:
3029- content_type = req.best_match_content_type()
3030- default_xmlns = self.get_default_xmlns(req)
3031- body = self._serialize(result, content_type, default_xmlns)
3032-
3033- response = webob.Response()
3034- response.headers['Content-Type'] = content_type
3035- response.body = body
3036- msg_dict = dict(url=req.url, status=response.status_int)
3037- msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
3038- LOG.debug(msg)
3039- return response
3040- else:
3041- return result
3042-
3043- def _serialize(self, data, content_type, default_xmlns):
3044- """Serialize the given dict to the provided content_type.
3045-
3046- Uses self._serialization_metadata if it exists, which is a dict mapping
3047- MIME types to information needed to serialize to that type.
3048-
3049- """
3050- _metadata = getattr(type(self), '_serialization_metadata', {})
3051-
3052- serializer = Serializer(_metadata, default_xmlns)
3053- try:
3054- return serializer.serialize(data, content_type)
3055- except exception.InvalidContentType:
3056- raise webob.exc.HTTPNotAcceptable()
3057-
3058- def _deserialize(self, data, content_type):
3059- """Deserialize the request body to the specefied content type.
3060-
3061- Uses self._serialization_metadata if it exists, which is a dict mapping
3062- MIME types to information needed to serialize to that type.
3063-
3064- """
3065- _metadata = getattr(type(self), '_serialization_metadata', {})
3066- serializer = Serializer(_metadata)
3067- return serializer.deserialize(data, content_type)
3068-
3069- def get_default_xmlns(self, req):
3070- """Provide the XML namespace to use if none is otherwise specified."""
3071- return None
3072-
3073-
3074-class Serializer(object):
3075- """Serializes and deserializes dictionaries to certain MIME types."""
3076-
3077- def __init__(self, metadata=None, default_xmlns=None):
3078- """Create a serializer based on the given WSGI environment.
3079-
3080- 'metadata' is an optional dict mapping MIME types to information
3081- needed to serialize a dictionary to that type.
3082-
3083- """
3084- self.metadata = metadata or {}
3085- self.default_xmlns = default_xmlns
3086-
3087- def _get_serialize_handler(self, content_type):
3088- handlers = {
3089- 'application/json': self._to_json,
3090- 'application/xml': self._to_xml,
3091- }
3092-
3093- try:
3094- return handlers[content_type]
3095- except Exception:
3096- raise exception.InvalidContentType(content_type=content_type)
3097-
3098- def serialize(self, data, content_type):
3099- """Serialize a dictionary into the specified content type."""
3100- return self._get_serialize_handler(content_type)(data)
3101-
3102- def deserialize(self, datastring, content_type):
3103- """Deserialize a string to a dictionary.
3104-
3105- The string must be in the format of a supported MIME type.
3106-
3107- """
3108- return self.get_deserialize_handler(content_type)(datastring)
3109-
3110- def get_deserialize_handler(self, content_type):
3111- handlers = {
3112- 'application/json': self._from_json,
3113- 'application/xml': self._from_xml,
3114- }
3115-
3116- try:
3117- return handlers[content_type]
3118- except Exception:
3119- raise exception.InvalidContentType(content_type=content_type)
3120-
3121- def _from_json(self, datastring):
3122- return utils.loads(datastring)
3123-
3124- def _from_xml(self, datastring):
3125- xmldata = self.metadata.get('application/xml', {})
3126- plurals = set(xmldata.get('plurals', {}))
3127- node = minidom.parseString(datastring).childNodes[0]
3128- return {node.nodeName: self._from_xml_node(node, plurals)}
3129-
3130- def _from_xml_node(self, node, listnames):
3131- """Convert a minidom node to a simple Python type.
3132-
3133- listnames is a collection of names of XML nodes whose subnodes should
3134- be considered list items.
3135-
3136- """
3137- if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
3138- return node.childNodes[0].nodeValue
3139- elif node.nodeName in listnames:
3140- return [self._from_xml_node(n, listnames) for n in node.childNodes]
3141- else:
3142- result = dict()
3143- for attr in node.attributes.keys():
3144- result[attr] = node.attributes[attr].nodeValue
3145- for child in node.childNodes:
3146- if child.nodeType != node.TEXT_NODE:
3147- result[child.nodeName] = self._from_xml_node(child,
3148- listnames)
3149- return result
3150-
3151- def _to_json(self, data):
3152- return utils.dumps(data)
3153-
3154- def _to_xml(self, data):
3155- metadata = self.metadata.get('application/xml', {})
3156- # We expect data to contain a single key which is the XML root.
3157- root_key = data.keys()[0]
3158- doc = minidom.Document()
3159- node = self._to_xml_node(doc, metadata, root_key, data[root_key])
3160-
3161- xmlns = node.getAttribute('xmlns')
3162- if not xmlns and self.default_xmlns:
3163- node.setAttribute('xmlns', self.default_xmlns)
3164-
3165- return node.toprettyxml(indent=' ')
3166-
3167- def _to_xml_node(self, doc, metadata, nodename, data):
3168- """Recursive method to convert data members to XML nodes."""
3169- result = doc.createElement(nodename)
3170-
3171- # Set the xml namespace if one is specified
3172- # TODO(justinsb): We could also use prefixes on the keys
3173- xmlns = metadata.get('xmlns', None)
3174- if xmlns:
3175- result.setAttribute('xmlns', xmlns)
3176-
3177- if type(data) is list:
3178- collections = metadata.get('list_collections', {})
3179- if nodename in collections:
3180- metadata = collections[nodename]
3181- for item in data:
3182- node = doc.createElement(metadata['item_name'])
3183- node.setAttribute(metadata['item_key'], str(item))
3184- result.appendChild(node)
3185- return result
3186- singular = metadata.get('plurals', {}).get(nodename, None)
3187- if singular is None:
3188- if nodename.endswith('s'):
3189- singular = nodename[:-1]
3190- else:
3191- singular = 'item'
3192- for item in data:
3193- node = self._to_xml_node(doc, metadata, singular, item)
3194- result.appendChild(node)
3195- elif type(data) is dict:
3196- collections = metadata.get('dict_collections', {})
3197- if nodename in collections:
3198- metadata = collections[nodename]
3199- for k, v in data.items():
3200- node = doc.createElement(metadata['item_name'])
3201- node.setAttribute(metadata['item_key'], str(k))
3202- text = doc.createTextNode(str(v))
3203- node.appendChild(text)
3204- result.appendChild(node)
3205- return result
3206- attrs = metadata.get('attributes', {}).get(nodename, {})
3207- for k, v in data.items():
3208- if k in attrs:
3209- result.setAttribute(k, str(v))
3210- else:
3211- node = self._to_xml_node(doc, metadata, k, v)
3212- result.appendChild(node)
3213- else:
3214- # Type is atom
3215- node = doc.createTextNode(str(data))
3216- result.appendChild(node)
3217- return result
3218-
3219-
3220 def paste_config_file(basename):
3221 """Find the best location in the system for a paste config file.
3222