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
=== modified file 'nova/api/direct.py'
--- nova/api/direct.py 2011-04-11 16:34:19 +0000
+++ nova/api/direct.py 2011-05-26 21:32:29 +0000
@@ -42,6 +42,7 @@
42from nova import flags42from nova import flags
43from nova import utils43from nova import utils
44from nova import wsgi44from nova import wsgi
45import nova.api.openstack.wsgi
4546
4647
47# Global storage for registering modules.48# Global storage for registering modules.
@@ -251,7 +252,7 @@
251 return self._methods[method]252 return self._methods[method]
252253
253254
254class ServiceWrapper(wsgi.Controller):255class ServiceWrapper(object):
255 """Wrapper to dynamically povide a WSGI controller for arbitrary objects.256 """Wrapper to dynamically povide a WSGI controller for arbitrary objects.
256257
257 With lightweight introspection allows public methods on the object to258 With lightweight introspection allows public methods on the object to
@@ -265,7 +266,7 @@
265 def __init__(self, service_handle):266 def __init__(self, service_handle):
266 self.service_handle = service_handle267 self.service_handle = service_handle
267268
268 @webob.dec.wsgify(RequestClass=wsgi.Request)269 @webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request)
269 def __call__(self, req):270 def __call__(self, req):
270 arg_dict = req.environ['wsgiorg.routing_args'][1]271 arg_dict = req.environ['wsgiorg.routing_args'][1]
271 action = arg_dict['action']272 action = arg_dict['action']
@@ -289,8 +290,11 @@
289290
290 try:291 try:
291 content_type = req.best_match_content_type()292 content_type = req.best_match_content_type()
292 default_xmlns = self.get_default_xmlns(req)293 serializer = {
293 return self._serialize(result, content_type, default_xmlns)294 'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(),
295 'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
296 }[content_type]
297 return serializer.serialize(result)
294 except:298 except:
295 raise exception.Error("returned non-serializable type: %s"299 raise exception.Error("returned non-serializable type: %s"
296 % result)300 % result)
297301
=== modified file 'nova/api/openstack/__init__.py'
--- nova/api/openstack/__init__.py 2011-05-13 15:17:19 +0000
+++ nova/api/openstack/__init__.py 2011-05-26 21:32:29 +0000
@@ -26,7 +26,7 @@
2626
27from nova import flags27from nova import flags
28from nova import log as logging28from nova import log as logging
29from nova import wsgi29from nova import wsgi as base_wsgi
30from nova.api.openstack import accounts30from nova.api.openstack import accounts
31from nova.api.openstack import faults31from nova.api.openstack import faults
32from nova.api.openstack import backup_schedules32from nova.api.openstack import backup_schedules
@@ -40,6 +40,7 @@
40from nova.api.openstack import server_metadata40from nova.api.openstack import server_metadata
41from nova.api.openstack import shared_ip_groups41from nova.api.openstack import shared_ip_groups
42from nova.api.openstack import users42from nova.api.openstack import users
43from nova.api.openstack import wsgi
43from nova.api.openstack import zones44from nova.api.openstack import zones
4445
4546
@@ -50,7 +51,7 @@
50 'When True, this API service will accept admin operations.')51 'When True, this API service will accept admin operations.')
5152
5253
53class FaultWrapper(wsgi.Middleware):54class FaultWrapper(base_wsgi.Middleware):
54 """Calls down the middleware stack, making exceptions into faults."""55 """Calls down the middleware stack, making exceptions into faults."""
5556
56 @webob.dec.wsgify(RequestClass=wsgi.Request)57 @webob.dec.wsgify(RequestClass=wsgi.Request)
@@ -63,7 +64,7 @@
63 return faults.Fault(exc)64 return faults.Fault(exc)
6465
6566
66class APIRouter(wsgi.Router):67class APIRouter(base_wsgi.Router):
67 """68 """
68 Routes requests on the OpenStack API to the appropriate controller69 Routes requests on the OpenStack API to the appropriate controller
69 and method.70 and method.
@@ -97,19 +98,21 @@
97 server_members['reset_network'] = 'POST'98 server_members['reset_network'] = 'POST'
98 server_members['inject_network_info'] = 'POST'99 server_members['inject_network_info'] = 'POST'
99100
100 mapper.resource("zone", "zones", controller=zones.Controller(),101 mapper.resource("zone", "zones",
102 controller=zones.create_resource(),
101 collection={'detail': 'GET', 'info': 'GET',103 collection={'detail': 'GET', 'info': 'GET',
102 'select': 'GET'})104 'select': 'GET'})
103105
104 mapper.resource("user", "users", controller=users.Controller(),106 mapper.resource("user", "users",
107 controller=users.create_resource(),
105 collection={'detail': 'GET'})108 collection={'detail': 'GET'})
106109
107 mapper.resource("account", "accounts",110 mapper.resource("account", "accounts",
108 controller=accounts.Controller(),111 controller=accounts.create_resource(),
109 collection={'detail': 'GET'})112 collection={'detail': 'GET'})
110113
111 mapper.resource("console", "consoles",114 mapper.resource("console", "consoles",
112 controller=consoles.Controller(),115 controller=consoles.create_resource(),
113 parent_resource=dict(member_name='server',116 parent_resource=dict(member_name='server',
114 collection_name='servers'))117 collection_name='servers'))
115118
@@ -122,31 +125,31 @@
122 def _setup_routes(self, mapper):125 def _setup_routes(self, mapper):
123 super(APIRouterV10, self)._setup_routes(mapper)126 super(APIRouterV10, self)._setup_routes(mapper)
124 mapper.resource("server", "servers",127 mapper.resource("server", "servers",
125 controller=servers.ControllerV10(),128 controller=servers.create_resource('1.0'),
126 collection={'detail': 'GET'},129 collection={'detail': 'GET'},
127 member=self.server_members)130 member=self.server_members)
128131
129 mapper.resource("image", "images",132 mapper.resource("image", "images",
130 controller=images.ControllerV10(),133 controller=images.create_resource('1.0'),
131 collection={'detail': 'GET'})134 collection={'detail': 'GET'})
132135
133 mapper.resource("flavor", "flavors",136 mapper.resource("flavor", "flavors",
134 controller=flavors.ControllerV10(),137 controller=flavors.create_resource('1.0'),
135 collection={'detail': 'GET'})138 collection={'detail': 'GET'})
136139
137 mapper.resource("shared_ip_group", "shared_ip_groups",140 mapper.resource("shared_ip_group", "shared_ip_groups",
138 collection={'detail': 'GET'},141 collection={'detail': 'GET'},
139 controller=shared_ip_groups.Controller())142 controller=shared_ip_groups.create_resource())
140143
141 mapper.resource("backup_schedule", "backup_schedule",144 mapper.resource("backup_schedule", "backup_schedule",
142 controller=backup_schedules.Controller(),145 controller=backup_schedules.create_resource(),
143 parent_resource=dict(member_name='server',146 parent_resource=dict(member_name='server',
144 collection_name='servers'))147 collection_name='servers'))
145148
146 mapper.resource("limit", "limits",149 mapper.resource("limit", "limits",
147 controller=limits.LimitsControllerV10())150 controller=limits.create_resource('1.0'))
148151
149 mapper.resource("ip", "ips", controller=ips.Controller(),152 mapper.resource("ip", "ips", controller=ips.create_resource(),
150 collection=dict(public='GET', private='GET'),153 collection=dict(public='GET', private='GET'),
151 parent_resource=dict(member_name='server',154 parent_resource=dict(member_name='server',
152 collection_name='servers'))155 collection_name='servers'))
@@ -158,27 +161,27 @@
158 def _setup_routes(self, mapper):161 def _setup_routes(self, mapper):
159 super(APIRouterV11, self)._setup_routes(mapper)162 super(APIRouterV11, self)._setup_routes(mapper)
160 mapper.resource("server", "servers",163 mapper.resource("server", "servers",
161 controller=servers.ControllerV11(),164 controller=servers.create_resource('1.1'),
162 collection={'detail': 'GET'},165 collection={'detail': 'GET'},
163 member=self.server_members)166 member=self.server_members)
164167
165 mapper.resource("image", "images",168 mapper.resource("image", "images",
166 controller=images.ControllerV11(),169 controller=images.create_resource('1.1'),
167 collection={'detail': 'GET'})170 collection={'detail': 'GET'})
168171
169 mapper.resource("image_meta", "meta",172 mapper.resource("image_meta", "meta",
170 controller=image_metadata.Controller(),173 controller=image_metadata.create_resource(),
171 parent_resource=dict(member_name='image',174 parent_resource=dict(member_name='image',
172 collection_name='images'))175 collection_name='images'))
173176
174 mapper.resource("server_meta", "meta",177 mapper.resource("server_meta", "meta",
175 controller=server_metadata.Controller(),178 controller=server_metadata.create_resource(),
176 parent_resource=dict(member_name='server',179 parent_resource=dict(member_name='server',
177 collection_name='servers'))180 collection_name='servers'))
178181
179 mapper.resource("flavor", "flavors",182 mapper.resource("flavor", "flavors",
180 controller=flavors.ControllerV11(),183 controller=flavors.create_resource('1.1'),
181 collection={'detail': 'GET'})184 collection={'detail': 'GET'})
182185
183 mapper.resource("limit", "limits",186 mapper.resource("limit", "limits",
184 controller=limits.LimitsControllerV11())187 controller=limits.create_resource('1.1'))
185188
=== modified file 'nova/api/openstack/accounts.py'
--- nova/api/openstack/accounts.py 2011-04-27 21:03:05 +0000
+++ nova/api/openstack/accounts.py 2011-05-26 21:32:29 +0000
@@ -20,8 +20,9 @@
20from nova import log as logging20from nova import log as logging
2121
22from nova.auth import manager22from nova.auth import manager
23from nova.api.openstack import common
24from nova.api.openstack import faults23from nova.api.openstack import faults
24from nova.api.openstack import wsgi
25
2526
26FLAGS = flags.FLAGS27FLAGS = flags.FLAGS
27LOG = logging.getLogger('nova.api.openstack')28LOG = logging.getLogger('nova.api.openstack')
@@ -34,12 +35,7 @@
34 manager=account.project_manager_id)35 manager=account.project_manager_id)
3536
3637
37class Controller(common.OpenstackController):38class Controller(object):
38
39 _serialization_metadata = {
40 'application/xml': {
41 "attributes": {
42 "account": ["id", "name", "description", "manager"]}}}
4339
44 def __init__(self):40 def __init__(self):
45 self.manager = manager.AuthManager()41 self.manager = manager.AuthManager()
@@ -66,20 +62,33 @@
66 self.manager.delete_project(id)62 self.manager.delete_project(id)
67 return {}63 return {}
6864
69 def create(self, req):65 def create(self, req, body):
70 """We use update with create-or-update semantics66 """We use update with create-or-update semantics
71 because the id comes from an external source"""67 because the id comes from an external source"""
72 raise faults.Fault(webob.exc.HTTPNotImplemented())68 raise faults.Fault(webob.exc.HTTPNotImplemented())
7369
74 def update(self, req, id):70 def update(self, req, id, body):
75 """This is really create or update."""71 """This is really create or update."""
76 self._check_admin(req.environ['nova.context'])72 self._check_admin(req.environ['nova.context'])
77 env = self._deserialize(req.body, req.get_content_type())73 description = body['account'].get('description')
78 description = env['account'].get('description')74 manager = body['account'].get('manager')
79 manager = env['account'].get('manager')
80 try:75 try:
81 account = self.manager.get_project(id)76 account = self.manager.get_project(id)
82 self.manager.modify_project(id, manager, description)77 self.manager.modify_project(id, manager, description)
83 except exception.NotFound:78 except exception.NotFound:
84 account = self.manager.create_project(id, manager, description)79 account = self.manager.create_project(id, manager, description)
85 return dict(account=_translate_keys(account))80 return dict(account=_translate_keys(account))
81
82
83def create_resource():
84 metadata = {
85 "attributes": {
86 "account": ["id", "name", "description", "manager"],
87 },
88 }
89
90 serializers = {
91 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
92 }
93
94 return wsgi.Resource(Controller(), serializers=serializers)
8695
=== modified file 'nova/api/openstack/backup_schedules.py'
--- nova/api/openstack/backup_schedules.py 2011-03-30 17:05:06 +0000
+++ nova/api/openstack/backup_schedules.py 2011-05-26 21:32:29 +0000
@@ -19,9 +19,8 @@
1919
20from webob import exc20from webob import exc
2121
22from nova.api.openstack import common
23from nova.api.openstack import faults22from nova.api.openstack import faults
24import nova.image.service23from nova.api.openstack import wsgi
2524
2625
27def _translate_keys(inst):26def _translate_keys(inst):
@@ -29,14 +28,9 @@
29 return dict(backupSchedule=inst)28 return dict(backupSchedule=inst)
3029
3130
32class Controller(common.OpenstackController):31class Controller(object):
33 """ The backup schedule API controller for the Openstack API """32 """ The backup schedule API controller for the Openstack API """
3433
35 _serialization_metadata = {
36 'application/xml': {
37 'attributes': {
38 'backupSchedule': []}}}
39
40 def __init__(self):34 def __init__(self):
41 pass35 pass
4236
@@ -48,7 +42,7 @@
48 """ Returns a single backup schedule for a given instance """42 """ Returns a single backup schedule for a given instance """
49 return faults.Fault(exc.HTTPNotImplemented())43 return faults.Fault(exc.HTTPNotImplemented())
5044
51 def create(self, req, server_id):45 def create(self, req, server_id, body):
52 """ No actual update method required, since the existing API allows46 """ No actual update method required, since the existing API allows
53 both create and update through a POST """47 both create and update through a POST """
54 return faults.Fault(exc.HTTPNotImplemented())48 return faults.Fault(exc.HTTPNotImplemented())
@@ -56,3 +50,18 @@
56 def delete(self, req, server_id, id):50 def delete(self, req, server_id, id):
57 """ Deletes an existing backup schedule """51 """ Deletes an existing backup schedule """
58 return faults.Fault(exc.HTTPNotImplemented())52 return faults.Fault(exc.HTTPNotImplemented())
53
54
55def create_resource():
56 metadata = {
57 'attributes': {
58 'backupSchedule': [],
59 },
60 }
61
62 serializers = {
63 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
64 metadata=metadata),
65 }
66
67 return wsgi.Resource(Controller(), serializers=serializers)
5968
=== modified file 'nova/api/openstack/common.py'
--- nova/api/openstack/common.py 2011-05-02 20:14:41 +0000
+++ nova/api/openstack/common.py 2011-05-26 21:32:29 +0000
@@ -23,7 +23,6 @@
23from nova import exception23from nova import exception
24from nova import flags24from nova import flags
25from nova import log as logging25from nova import log as logging
26from nova import wsgi
2726
2827
29LOG = logging.getLogger('nova.api.openstack.common')28LOG = logging.getLogger('nova.api.openstack.common')
@@ -146,9 +145,3 @@
146 except:145 except:
147 LOG.debug(_("Error extracting id from href: %s") % href)146 LOG.debug(_("Error extracting id from href: %s") % href)
148 raise webob.exc.HTTPBadRequest(_('could not parse id from href'))147 raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
149
150
151class OpenstackController(wsgi.Controller):
152 def get_default_xmlns(self, req):
153 # Use V10 by default
154 return XML_NS_V10
155148
=== modified file 'nova/api/openstack/consoles.py'
--- nova/api/openstack/consoles.py 2011-03-30 17:05:06 +0000
+++ nova/api/openstack/consoles.py 2011-05-26 21:32:29 +0000
@@ -19,8 +19,8 @@
1919
20from nova import console20from nova import console
21from nova import exception21from nova import exception
22from nova.api.openstack import common
23from nova.api.openstack import faults22from nova.api.openstack import faults
23from nova.api.openstack import wsgi
2424
2525
26def _translate_keys(cons):26def _translate_keys(cons):
@@ -43,17 +43,11 @@
43 return dict(console=info)43 return dict(console=info)
4444
4545
46class Controller(common.OpenstackController):46class Controller(object):
47 """The Consoles Controller for the Openstack API"""47 """The Consoles controller for the Openstack API"""
48
49 _serialization_metadata = {
50 'application/xml': {
51 'attributes': {
52 'console': []}}}
5348
54 def __init__(self):49 def __init__(self):
55 self.console_api = console.API()50 self.console_api = console.API()
56 super(Controller, self).__init__()
5751
58 def index(self, req, server_id):52 def index(self, req, server_id):
59 """Returns a list of consoles for this instance"""53 """Returns a list of consoles for this instance"""
@@ -63,9 +57,8 @@
63 return dict(consoles=[_translate_keys(console)57 return dict(consoles=[_translate_keys(console)
64 for console in consoles])58 for console in consoles])
6559
66 def create(self, req, server_id):60 def create(self, req, server_id, body):
67 """Creates a new console"""61 """Creates a new console"""
68 #info = self._deserialize(req.body, req.get_content_type())
69 self.console_api.create_console(62 self.console_api.create_console(
70 req.environ['nova.context'],63 req.environ['nova.context'],
71 int(server_id))64 int(server_id))
@@ -94,3 +87,17 @@
94 except exception.NotFound:87 except exception.NotFound:
95 return faults.Fault(exc.HTTPNotFound())88 return faults.Fault(exc.HTTPNotFound())
96 return exc.HTTPAccepted()89 return exc.HTTPAccepted()
90
91
92def create_resource():
93 metadata = {
94 'attributes': {
95 'console': [],
96 },
97 }
98
99 serializers = {
100 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
101 }
102
103 return wsgi.Resource(Controller(), serializers=serializers)
97104
=== modified file 'nova/api/openstack/contrib/volumes.py'
--- nova/api/openstack/contrib/volumes.py 2011-04-19 09:54:47 +0000
+++ nova/api/openstack/contrib/volumes.py 2011-05-26 21:32:29 +0000
@@ -22,7 +22,6 @@
22from nova import flags22from nova import flags
23from nova import log as logging23from nova import log as logging
24from nova import volume24from nova import volume
25from nova import wsgi
26from nova.api.openstack import common25from nova.api.openstack import common
27from nova.api.openstack import extensions26from nova.api.openstack import extensions
28from nova.api.openstack import faults27from nova.api.openstack import faults
@@ -64,7 +63,7 @@
64 return d63 return d
6564
6665
67class VolumeController(wsgi.Controller):66class VolumeController(object):
68 """The Volumes API controller for the OpenStack API."""67 """The Volumes API controller for the OpenStack API."""
6968
70 _serialization_metadata = {69 _serialization_metadata = {
@@ -124,15 +123,14 @@
124 res = [entity_maker(context, vol) for vol in limited_list]123 res = [entity_maker(context, vol) for vol in limited_list]
125 return {'volumes': res}124 return {'volumes': res}
126125
127 def create(self, req):126 def create(self, req, body):
128 """Creates a new volume."""127 """Creates a new volume."""
129 context = req.environ['nova.context']128 context = req.environ['nova.context']
130129
131 env = self._deserialize(req.body, req.get_content_type())130 if not body:
132 if not env:
133 return faults.Fault(exc.HTTPUnprocessableEntity())131 return faults.Fault(exc.HTTPUnprocessableEntity())
134132
135 vol = env['volume']133 vol = body['volume']
136 size = vol['size']134 size = vol['size']
137 LOG.audit(_("Create volume of %s GB"), size, context=context)135 LOG.audit(_("Create volume of %s GB"), size, context=context)
138 new_volume = self.volume_api.create(context, size,136 new_volume = self.volume_api.create(context, size,
@@ -175,7 +173,7 @@
175 return d173 return d
176174
177175
178class VolumeAttachmentController(wsgi.Controller):176class VolumeAttachmentController(object):
179 """The volume attachment API controller for the Openstack API.177 """The volume attachment API controller for the Openstack API.
180178
181 A child resource of the server. Note that we use the volume id179 A child resource of the server. Note that we use the volume id
@@ -219,17 +217,16 @@
219 return {'volumeAttachment': _translate_attachment_detail_view(context,217 return {'volumeAttachment': _translate_attachment_detail_view(context,
220 vol)}218 vol)}
221219
222 def create(self, req, server_id):220 def create(self, req, server_id, body):
223 """Attach a volume to an instance."""221 """Attach a volume to an instance."""
224 context = req.environ['nova.context']222 context = req.environ['nova.context']
225223
226 env = self._deserialize(req.body, req.get_content_type())224 if not body:
227 if not env:
228 return faults.Fault(exc.HTTPUnprocessableEntity())225 return faults.Fault(exc.HTTPUnprocessableEntity())
229226
230 instance_id = server_id227 instance_id = server_id
231 volume_id = env['volumeAttachment']['volumeId']228 volume_id = body['volumeAttachment']['volumeId']
232 device = env['volumeAttachment']['device']229 device = body['volumeAttachment']['device']
233230
234 msg = _("Attach volume %(volume_id)s to instance %(server_id)s"231 msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
235 " at %(device)s") % locals()232 " at %(device)s") % locals()
@@ -259,7 +256,7 @@
259 # TODO(justinsb): How do I return "accepted" here?256 # TODO(justinsb): How do I return "accepted" here?
260 return {'volumeAttachment': attachment}257 return {'volumeAttachment': attachment}
261258
262 def update(self, _req, _server_id, _id):259 def update(self, req, server_id, id, body):
263 """Update a volume attachment. We don't currently support this."""260 """Update a volume attachment. We don't currently support this."""
264 return faults.Fault(exc.HTTPBadRequest())261 return faults.Fault(exc.HTTPBadRequest())
265262
266263
=== modified file 'nova/api/openstack/extensions.py'
--- nova/api/openstack/extensions.py 2011-05-17 03:14:51 +0000
+++ nova/api/openstack/extensions.py 2011-05-26 21:32:29 +0000
@@ -27,9 +27,10 @@
27from nova import exception27from nova import exception
28from nova import flags28from nova import flags
29from nova import log as logging29from nova import log as logging
30from nova import wsgi30from nova import wsgi as base_wsgi
31from nova.api.openstack import common31from nova.api.openstack import common
32from nova.api.openstack import faults32from nova.api.openstack import faults
33from nova.api.openstack import wsgi
3334
3435
35LOG = logging.getLogger('extensions')36LOG = logging.getLogger('extensions')
@@ -115,28 +116,34 @@
115 return request_exts116 return request_exts
116117
117118
118class ActionExtensionController(common.OpenstackController):119class ActionExtensionController(object):
119
120 def __init__(self, application):120 def __init__(self, application):
121
122 self.application = application121 self.application = application
123 self.action_handlers = {}122 self.action_handlers = {}
124123
125 def add_action(self, action_name, handler):124 def add_action(self, action_name, handler):
126 self.action_handlers[action_name] = handler125 self.action_handlers[action_name] = handler
127126
128 def action(self, req, id):127 def action(self, req, id, body):
129
130 input_dict = self._deserialize(req.body, req.get_content_type())
131 for action_name, handler in self.action_handlers.iteritems():128 for action_name, handler in self.action_handlers.iteritems():
132 if action_name in input_dict:129 if action_name in body:
133 return handler(input_dict, req, id)130 return handler(body, req, id)
134 # no action handler found (bump to downstream application)131 # no action handler found (bump to downstream application)
135 res = self.application132 res = self.application
136 return res133 return res
137134
138135
139class RequestExtensionController(common.OpenstackController):136class ActionExtensionResource(wsgi.Resource):
137
138 def __init__(self, application):
139 controller = ActionExtensionController(application)
140 super(ActionExtensionResource, self).__init__(controller)
141
142 def add_action(self, action_name, handler):
143 self.controller.add_action(action_name, handler)
144
145
146class RequestExtensionController(object):
140147
141 def __init__(self, application):148 def __init__(self, application):
142 self.application = application149 self.application = application
@@ -153,7 +160,17 @@
153 return res160 return res
154161
155162
156class ExtensionController(common.OpenstackController):163class RequestExtensionResource(wsgi.Resource):
164
165 def __init__(self, application):
166 controller = RequestExtensionController(application)
167 super(RequestExtensionResource, self).__init__(controller)
168
169 def add_handler(self, handler):
170 self.controller.add_handler(handler)
171
172
173class ExtensionsResource(wsgi.Resource):
157174
158 def __init__(self, extension_manager):175 def __init__(self, extension_manager):
159 self.extension_manager = extension_manager176 self.extension_manager = extension_manager
@@ -186,7 +203,7 @@
186 raise faults.Fault(webob.exc.HTTPNotFound())203 raise faults.Fault(webob.exc.HTTPNotFound())
187204
188205
189class ExtensionMiddleware(wsgi.Middleware):206class ExtensionMiddleware(base_wsgi.Middleware):
190 """Extensions middleware for WSGI."""207 """Extensions middleware for WSGI."""
191 @classmethod208 @classmethod
192 def factory(cls, global_config, **local_config):209 def factory(cls, global_config, **local_config):
@@ -195,43 +212,43 @@
195 return cls(app, **local_config)212 return cls(app, **local_config)
196 return _factory213 return _factory
197214
198 def _action_ext_controllers(self, application, ext_mgr, mapper):215 def _action_ext_resources(self, application, ext_mgr, mapper):
199 """Return a dict of ActionExtensionController-s by collection."""216 """Return a dict of ActionExtensionResource-s by collection."""
200 action_controllers = {}217 action_resources = {}
201 for action in ext_mgr.get_actions():218 for action in ext_mgr.get_actions():
202 if not action.collection in action_controllers.keys():219 if not action.collection in action_resources.keys():
203 controller = ActionExtensionController(application)220 resource = ActionExtensionResource(application)
204 mapper.connect("/%s/:(id)/action.:(format)" %221 mapper.connect("/%s/:(id)/action.:(format)" %
205 action.collection,222 action.collection,
206 action='action',223 action='action',
207 controller=controller,224 controller=resource,
208 conditions=dict(method=['POST']))225 conditions=dict(method=['POST']))
209 mapper.connect("/%s/:(id)/action" % action.collection,226 mapper.connect("/%s/:(id)/action" % action.collection,
210 action='action',227 action='action',
211 controller=controller,228 controller=resource,
212 conditions=dict(method=['POST']))229 conditions=dict(method=['POST']))
213 action_controllers[action.collection] = controller230 action_resources[action.collection] = resource
214231
215 return action_controllers232 return action_resources
216233
217 def _request_ext_controllers(self, application, ext_mgr, mapper):234 def _request_ext_resources(self, application, ext_mgr, mapper):
218 """Returns a dict of RequestExtensionController-s by collection."""235 """Returns a dict of RequestExtensionResource-s by collection."""
219 request_ext_controllers = {}236 request_ext_resources = {}
220 for req_ext in ext_mgr.get_request_extensions():237 for req_ext in ext_mgr.get_request_extensions():
221 if not req_ext.key in request_ext_controllers.keys():238 if not req_ext.key in request_ext_resources.keys():
222 controller = RequestExtensionController(application)239 resource = RequestExtensionResource(application)
223 mapper.connect(req_ext.url_route + '.:(format)',240 mapper.connect(req_ext.url_route + '.:(format)',
224 action='process',241 action='process',
225 controller=controller,242 controller=resource,
226 conditions=req_ext.conditions)243 conditions=req_ext.conditions)
227244
228 mapper.connect(req_ext.url_route,245 mapper.connect(req_ext.url_route,
229 action='process',246 action='process',
230 controller=controller,247 controller=resource,
231 conditions=req_ext.conditions)248 conditions=req_ext.conditions)
232 request_ext_controllers[req_ext.key] = controller249 request_ext_resources[req_ext.key] = resource
233250
234 return request_ext_controllers251 return request_ext_resources
235252
236 def __init__(self, application, ext_mgr=None):253 def __init__(self, application, ext_mgr=None):
237254
@@ -246,22 +263,22 @@
246 LOG.debug(_('Extended resource: %s'),263 LOG.debug(_('Extended resource: %s'),
247 resource.collection)264 resource.collection)
248 mapper.resource(resource.collection, resource.collection,265 mapper.resource(resource.collection, resource.collection,
249 controller=resource.controller,266 controller=wsgi.Resource(resource.controller),
250 collection=resource.collection_actions,267 collection=resource.collection_actions,
251 member=resource.member_actions,268 member=resource.member_actions,
252 parent_resource=resource.parent)269 parent_resource=resource.parent)
253270
254 # extended actions271 # extended actions
255 action_controllers = self._action_ext_controllers(application, ext_mgr,272 action_resources = self._action_ext_resources(application, ext_mgr,
256 mapper)273 mapper)
257 for action in ext_mgr.get_actions():274 for action in ext_mgr.get_actions():
258 LOG.debug(_('Extended action: %s'), action.action_name)275 LOG.debug(_('Extended action: %s'), action.action_name)
259 controller = action_controllers[action.collection]276 resource = action_resources[action.collection]
260 controller.add_action(action.action_name, action.handler)277 resource.add_action(action.action_name, action.handler)
261278
262 # extended requests279 # extended requests
263 req_controllers = self._request_ext_controllers(application, ext_mgr,280 req_controllers = self._request_ext_resources(application, ext_mgr,
264 mapper)281 mapper)
265 for request_ext in ext_mgr.get_request_extensions():282 for request_ext in ext_mgr.get_request_extensions():
266 LOG.debug(_('Extended request: %s'), request_ext.key)283 LOG.debug(_('Extended request: %s'), request_ext.key)
267 controller = req_controllers[request_ext.key]284 controller = req_controllers[request_ext.key]
@@ -313,7 +330,7 @@
313 """Returns a list of ResourceExtension objects."""330 """Returns a list of ResourceExtension objects."""
314 resources = []331 resources = []
315 resources.append(ResourceExtension('extensions',332 resources.append(ResourceExtension('extensions',
316 ExtensionController(self)))333 ExtensionsResource(self)))
317 for alias, ext in self.extensions.iteritems():334 for alias, ext in self.extensions.iteritems():
318 try:335 try:
319 resources.extend(ext.get_resources())336 resources.extend(ext.get_resources())
@@ -410,7 +427,7 @@
410427
411428
412class RequestExtension(object):429class RequestExtension(object):
413 """Extend requests and responses of core nova OpenStack API controllers.430 """Extend requests and responses of core nova OpenStack API resources.
414431
415 Provide a way to add data to responses and handle custom request data432 Provide a way to add data to responses and handle custom request data
416 that is sent to core nova OpenStack API controllers.433 that is sent to core nova OpenStack API controllers.
@@ -424,7 +441,7 @@
424441
425442
426class ActionExtension(object):443class ActionExtension(object):
427 """Add custom actions to core nova OpenStack API controllers."""444 """Add custom actions to core nova OpenStack API resources."""
428445
429 def __init__(self, collection, action_name, handler):446 def __init__(self, collection, action_name, handler):
430 self.collection = collection447 self.collection = collection
431448
=== modified file 'nova/api/openstack/faults.py'
--- nova/api/openstack/faults.py 2011-04-07 18:55:42 +0000
+++ nova/api/openstack/faults.py 2011-05-26 21:32:29 +0000
@@ -19,8 +19,7 @@
19import webob.dec19import webob.dec
20import webob.exc20import webob.exc
2121
22from nova import wsgi22from nova.api.openstack import wsgi
23from nova.api.openstack import common
2423
2524
26class Fault(webob.exc.HTTPException):25class Fault(webob.exc.HTTPException):
@@ -55,13 +54,21 @@
55 if code == 413:54 if code == 413:
56 retry = self.wrapped_exc.headers['Retry-After']55 retry = self.wrapped_exc.headers['Retry-After']
57 fault_data[fault_name]['retryAfter'] = retry56 fault_data[fault_name]['retryAfter'] = retry
57
58 # 'code' is an attribute on the fault tag itself58 # 'code' is an attribute on the fault tag itself
59 metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}59 metadata = {'attributes': {fault_name: 'code'}}
60 default_xmlns = common.XML_NS_V1060
61 serializer = wsgi.Serializer(metadata, default_xmlns)
62 content_type = req.best_match_content_type()61 content_type = req.best_match_content_type()
63 self.wrapped_exc.body = serializer.serialize(fault_data, content_type)62
63 serializer = {
64 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
65 xmlns=wsgi.XMLNS_V10),
66 'application/json': wsgi.JSONDictSerializer(),
67 }[content_type]
68
69 self.wrapped_exc.body = serializer.serialize(fault_data)
64 self.wrapped_exc.content_type = content_type70 self.wrapped_exc.content_type = content_type
71
65 return self.wrapped_exc72 return self.wrapped_exc
6673
6774
@@ -70,14 +77,6 @@
70 Rate-limited request response.77 Rate-limited request response.
71 """78 """
7279
73 _serialization_metadata = {
74 "application/xml": {
75 "attributes": {
76 "overLimitFault": "code",
77 },
78 },
79 }
80
81 def __init__(self, message, details, retry_time):80 def __init__(self, message, details, retry_time):
82 """81 """
83 Initialize new `OverLimitFault` with relevant information.82 Initialize new `OverLimitFault` with relevant information.
@@ -97,8 +96,16 @@
97 Return the wrapped exception with a serialized body conforming to our96 Return the wrapped exception with a serialized body conforming to our
98 error format.97 error format.
99 """98 """
100 serializer = wsgi.Serializer(self._serialization_metadata)
101 content_type = request.best_match_content_type()99 content_type = request.best_match_content_type()
102 content = serializer.serialize(self.content, content_type)100 metadata = {"attributes": {"overLimitFault": "code"}}
101
102 serializer = {
103 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
104 xmlns=wsgi.XMLNS_V10),
105 'application/json': wsgi.JSONDictSerializer(),
106 }[content_type]
107
108 content = serializer.serialize(self.content)
103 self.wrapped_exc.body = content109 self.wrapped_exc.body = content
110
104 return self.wrapped_exc111 return self.wrapped_exc
105112
=== modified file 'nova/api/openstack/flavors.py'
--- nova/api/openstack/flavors.py 2011-05-06 20:13:35 +0000
+++ nova/api/openstack/flavors.py 2011-05-26 21:32:29 +0000
@@ -19,22 +19,13 @@
1919
20from nova import db20from nova import db
21from nova import exception21from nova import exception
22from nova.api.openstack import common
23from nova.api.openstack import views22from nova.api.openstack import views
2423from nova.api.openstack import wsgi
2524
26class Controller(common.OpenstackController):25
26class Controller(object):
27 """Flavor controller for the OpenStack API."""27 """Flavor controller for the OpenStack API."""
2828
29 _serialization_metadata = {
30 'application/xml': {
31 "attributes": {
32 "flavor": ["id", "name", "ram", "disk"],
33 "link": ["rel", "type", "href"],
34 }
35 }
36 }
37
38 def index(self, req):29 def index(self, req):
39 """Return all flavors in brief."""30 """Return all flavors in brief."""
40 items = self._get_flavors(req, is_detail=False)31 items = self._get_flavors(req, is_detail=False)
@@ -71,14 +62,31 @@
7162
7263
73class ControllerV10(Controller):64class ControllerV10(Controller):
65
74 def _get_view_builder(self, req):66 def _get_view_builder(self, req):
75 return views.flavors.ViewBuilder()67 return views.flavors.ViewBuilder()
7668
7769
78class ControllerV11(Controller):70class ControllerV11(Controller):
71
79 def _get_view_builder(self, req):72 def _get_view_builder(self, req):
80 base_url = req.application_url73 base_url = req.application_url
81 return views.flavors.ViewBuilderV11(base_url)74 return views.flavors.ViewBuilderV11(base_url)
8275
83 def get_default_xmlns(self, req):76
84 return common.XML_NS_V1177def create_resource(version='1.0'):
78 controller = {
79 '1.0': ControllerV10,
80 '1.1': ControllerV11,
81 }[version]()
82
83 xmlns = {
84 '1.0': wsgi.XMLNS_V10,
85 '1.1': wsgi.XMLNS_V11,
86 }[version]
87
88 serializers = {
89 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
90 }
91
92 return wsgi.Resource(controller, serializers=serializers)
8593
=== modified file 'nova/api/openstack/image_metadata.py'
--- nova/api/openstack/image_metadata.py 2011-04-08 07:58:48 +0000
+++ nova/api/openstack/image_metadata.py 2011-05-26 21:32:29 +0000
@@ -20,20 +20,18 @@
20from nova import flags20from nova import flags
21from nova import quota21from nova import quota
22from nova import utils22from nova import utils
23from nova import wsgi
24from nova.api.openstack import common
25from nova.api.openstack import faults23from nova.api.openstack import faults
24from nova.api.openstack import wsgi
2625
2726
28FLAGS = flags.FLAGS27FLAGS = flags.FLAGS
2928
3029
31class Controller(common.OpenstackController):30class Controller(object):
32 """The image metadata API controller for the Openstack API"""31 """The image metadata API controller for the Openstack API"""
3332
34 def __init__(self):33 def __init__(self):
35 self.image_service = utils.import_object(FLAGS.image_service)34 self.image_service = utils.import_object(FLAGS.image_service)
36 super(Controller, self).__init__()
3735
38 def _get_metadata(self, context, image_id, image=None):36 def _get_metadata(self, context, image_id, image=None):
39 if not image:37 if not image:
@@ -64,9 +62,8 @@
64 else:62 else:
65 return faults.Fault(exc.HTTPNotFound())63 return faults.Fault(exc.HTTPNotFound())
6664
67 def create(self, req, image_id):65 def create(self, req, image_id, body):
68 context = req.environ['nova.context']66 context = req.environ['nova.context']
69 body = self._deserialize(req.body, req.get_content_type())
70 img = self.image_service.show(context, image_id)67 img = self.image_service.show(context, image_id)
71 metadata = self._get_metadata(context, image_id, img)68 metadata = self._get_metadata(context, image_id, img)
72 if 'metadata' in body:69 if 'metadata' in body:
@@ -77,9 +74,8 @@
77 self.image_service.update(context, image_id, img, None)74 self.image_service.update(context, image_id, img, None)
78 return dict(metadata=metadata)75 return dict(metadata=metadata)
7976
80 def update(self, req, image_id, id):77 def update(self, req, image_id, id, body):
81 context = req.environ['nova.context']78 context = req.environ['nova.context']
82 body = self._deserialize(req.body, req.get_content_type())
83 if not id in body:79 if not id in body:
84 expl = _('Request body and URI mismatch')80 expl = _('Request body and URI mismatch')
85 raise exc.HTTPBadRequest(explanation=expl)81 raise exc.HTTPBadRequest(explanation=expl)
@@ -104,3 +100,11 @@
104 metadata.pop(id)100 metadata.pop(id)
105 img['properties'] = metadata101 img['properties'] = metadata
106 self.image_service.update(context, image_id, img, None)102 self.image_service.update(context, image_id, img, None)
103
104
105def create_resource():
106 serializers = {
107 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
108 }
109
110 return wsgi.Resource(Controller(), serializers=serializers)
107111
=== modified file 'nova/api/openstack/images.py'
--- nova/api/openstack/images.py 2011-04-21 17:29:11 +0000
+++ nova/api/openstack/images.py 2011-05-26 21:32:29 +0000
@@ -23,24 +23,15 @@
23from nova.api.openstack import common23from nova.api.openstack import common
24from nova.api.openstack import faults24from nova.api.openstack import faults
25from nova.api.openstack.views import images as images_view25from nova.api.openstack.views import images as images_view
26from nova.api.openstack import wsgi
2627
2728
28LOG = log.getLogger('nova.api.openstack.images')29LOG = log.getLogger('nova.api.openstack.images')
29FLAGS = flags.FLAGS30FLAGS = flags.FLAGS
3031
3132
32class Controller(common.OpenstackController):33class Controller(object):
33 """Base `wsgi.Controller` for retrieving/displaying images."""34 """Base controller for retrieving/displaying images."""
34
35 _serialization_metadata = {
36 'application/xml': {
37 "attributes": {
38 "image": ["id", "name", "updated", "created", "status",
39 "serverId", "progress"],
40 "link": ["rel", "type", "href"],
41 },
42 },
43 }
4435
45 def __init__(self, image_service=None, compute_service=None):36 def __init__(self, image_service=None, compute_service=None):
46 """Initialize new `ImageController`.37 """Initialize new `ImageController`.
@@ -108,21 +99,20 @@
108 self._image_service.delete(context, image_id)99 self._image_service.delete(context, image_id)
109 return webob.exc.HTTPNoContent()100 return webob.exc.HTTPNoContent()
110101
111 def create(self, req):102 def create(self, req, body):
112 """Snapshot a server instance and save the image.103 """Snapshot a server instance and save the image.
113104
114 :param req: `wsgi.Request` object105 :param req: `wsgi.Request` object
115 """106 """
116 context = req.environ['nova.context']107 context = req.environ['nova.context']
117 content_type = req.get_content_type()108 content_type = req.get_content_type()
118 image = self._deserialize(req.body, content_type)
119109
120 if not image:110 if not body:
121 raise webob.exc.HTTPBadRequest()111 raise webob.exc.HTTPBadRequest()
122112
123 try:113 try:
124 server_id = image["image"]["serverId"]114 server_id = body["image"]["serverId"]
125 image_name = image["image"]["name"]115 image_name = body["image"]["name"]
126 except KeyError:116 except KeyError:
127 raise webob.exc.HTTPBadRequest()117 raise webob.exc.HTTPBadRequest()
128118
@@ -151,5 +141,29 @@
151 base_url = request.application_url141 base_url = request.application_url
152 return images_view.ViewBuilderV11(base_url)142 return images_view.ViewBuilderV11(base_url)
153143
154 def get_default_xmlns(self, req):144
155 return common.XML_NS_V11145def create_resource(version='1.0'):
146 controller = {
147 '1.0': ControllerV10,
148 '1.1': ControllerV11,
149 }[version]()
150
151 xmlns = {
152 '1.0': wsgi.XMLNS_V10,
153 '1.1': wsgi.XMLNS_V11,
154 }[version]
155
156 metadata = {
157 "attributes": {
158 "image": ["id", "name", "updated", "created", "status",
159 "serverId", "progress"],
160 "link": ["rel", "type", "href"],
161 },
162 }
163
164 serializers = {
165 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
166 metadata=metadata),
167 }
168
169 return wsgi.Resource(controller, serializers=serializers)
156170
=== modified file 'nova/api/openstack/ips.py'
--- nova/api/openstack/ips.py 2011-04-06 21:50:11 +0000
+++ nova/api/openstack/ips.py 2011-05-26 21:32:29 +0000
@@ -20,23 +20,14 @@
20from webob import exc20from webob import exc
2121
22import nova22import nova
23from nova.api.openstack import faults
23import nova.api.openstack.views.addresses24import nova.api.openstack.views.addresses
24from nova.api.openstack import common25from nova.api.openstack import wsgi
25from nova.api.openstack import faults26
2627
2728class Controller(object):
28class Controller(common.OpenstackController):
29 """The servers addresses API controller for the Openstack API."""29 """The servers addresses API controller for the Openstack API."""
3030
31 _serialization_metadata = {
32 'application/xml': {
33 'list_collections': {
34 'public': {'item_name': 'ip', 'item_key': 'addr'},
35 'private': {'item_name': 'ip', 'item_key': 'addr'},
36 },
37 },
38 }
39
40 def __init__(self):31 def __init__(self):
41 self.compute_api = nova.compute.API()32 self.compute_api = nova.compute.API()
42 self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()33 self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
@@ -65,8 +56,24 @@
65 def show(self, req, server_id, id):56 def show(self, req, server_id, id):
66 return faults.Fault(exc.HTTPNotImplemented())57 return faults.Fault(exc.HTTPNotImplemented())
6758
68 def create(self, req, server_id):59 def create(self, req, server_id, body):
69 return faults.Fault(exc.HTTPNotImplemented())60 return faults.Fault(exc.HTTPNotImplemented())
7061
71 def delete(self, req, server_id, id):62 def delete(self, req, server_id, id):
72 return faults.Fault(exc.HTTPNotImplemented())63 return faults.Fault(exc.HTTPNotImplemented())
64
65
66def create_resource():
67 metadata = {
68 'list_collections': {
69 'public': {'item_name': 'ip', 'item_key': 'addr'},
70 'private': {'item_name': 'ip', 'item_key': 'addr'},
71 },
72 }
73
74 serializers = {
75 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
76 xmlns=wsgi.XMLNS_V10),
77 }
78
79 return wsgi.Resource(Controller(), serializers=serializers)
7380
=== modified file 'nova/api/openstack/limits.py'
--- nova/api/openstack/limits.py 2011-05-19 18:08:15 +0000
+++ nova/api/openstack/limits.py 2011-05-26 21:32:29 +0000
@@ -31,10 +31,12 @@
31from webob.dec import wsgify31from webob.dec import wsgify
3232
33from nova import quota33from nova import quota
34from nova import wsgi as base_wsgi
34from nova import wsgi35from nova import wsgi
35from nova.api.openstack import common36from nova.api.openstack import common
36from nova.api.openstack import faults37from nova.api.openstack import faults
37from nova.api.openstack.views import limits as limits_views38from nova.api.openstack.views import limits as limits_views
39from nova.api.openstack import wsgi
3840
3941
40# Convenience constants for the limits dictionary passed to Limiter().42# Convenience constants for the limits dictionary passed to Limiter().
@@ -44,23 +46,11 @@
44PER_DAY = 60 * 60 * 2446PER_DAY = 60 * 60 * 24
4547
4648
47class LimitsController(common.OpenstackController):49class LimitsController(object):
48 """50 """
49 Controller for accessing limits in the OpenStack API.51 Controller for accessing limits in the OpenStack API.
50 """52 """
5153
52 _serialization_metadata = {
53 "application/xml": {
54 "attributes": {
55 "limit": ["verb", "URI", "uri", "regex", "value", "unit",
56 "resetTime", "next-available", "remaining", "name"],
57 },
58 "plurals": {
59 "rate": "limit",
60 },
61 },
62 }
63
64 def index(self, req):54 def index(self, req):
65 """55 """
66 Return all global and rate limit information.56 Return all global and rate limit information.
@@ -86,6 +76,35 @@
86 return limits_views.ViewBuilderV11()76 return limits_views.ViewBuilderV11()
8777
8878
79def create_resource(version='1.0'):
80 controller = {
81 '1.0': LimitsControllerV10,
82 '1.1': LimitsControllerV11,
83 }[version]()
84
85 xmlns = {
86 '1.0': wsgi.XMLNS_V10,
87 '1.1': wsgi.XMLNS_V11,
88 }[version]
89
90 metadata = {
91 "attributes": {
92 "limit": ["verb", "URI", "uri", "regex", "value", "unit",
93 "resetTime", "next-available", "remaining", "name"],
94 },
95 "plurals": {
96 "rate": "limit",
97 },
98 }
99
100 serializers = {
101 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
102 metadata=metadata)
103 }
104
105 return wsgi.Resource(controller, serializers=serializers)
106
107
89class Limit(object):108class Limit(object):
90 """109 """
91 Stores information about a limit for HTTP requets.110 Stores information about a limit for HTTP requets.
@@ -197,7 +216,7 @@
197]216]
198217
199218
200class RateLimitingMiddleware(wsgi.Middleware):219class RateLimitingMiddleware(base_wsgi.Middleware):
201 """220 """
202 Rate-limits requests passing through this middleware. All limit information221 Rate-limits requests passing through this middleware. All limit information
203 is stored in memory for this implementation.222 is stored in memory for this implementation.
@@ -211,7 +230,7 @@
211 @param application: WSGI application to wrap230 @param application: WSGI application to wrap
212 @param limits: List of dictionaries describing limits231 @param limits: List of dictionaries describing limits
213 """232 """
214 wsgi.Middleware.__init__(self, application)233 base_wsgi.Middleware.__init__(self, application)
215 self._limiter = Limiter(limits or DEFAULT_LIMITS)234 self._limiter = Limiter(limits or DEFAULT_LIMITS)
216235
217 @wsgify(RequestClass=wsgi.Request)236 @wsgify(RequestClass=wsgi.Request)
218237
=== modified file 'nova/api/openstack/server_metadata.py'
--- nova/api/openstack/server_metadata.py 2011-04-12 17:47:45 +0000
+++ nova/api/openstack/server_metadata.py 2011-05-26 21:32:29 +0000
@@ -19,12 +19,11 @@
1919
20from nova import compute20from nova import compute
21from nova import quota21from nova import quota
22from nova import wsgi
23from nova.api.openstack import common
24from nova.api.openstack import faults22from nova.api.openstack import faults
2523from nova.api.openstack import wsgi
2624
27class Controller(common.OpenstackController):25
26class Controller(object):
28 """ The server metadata API controller for the Openstack API """27 """ The server metadata API controller for the Openstack API """
2928
30 def __init__(self):29 def __init__(self):
@@ -43,10 +42,9 @@
43 context = req.environ['nova.context']42 context = req.environ['nova.context']
44 return self._get_metadata(context, server_id)43 return self._get_metadata(context, server_id)
4544
46 def create(self, req, server_id):45 def create(self, req, server_id, body):
47 context = req.environ['nova.context']46 context = req.environ['nova.context']
48 data = self._deserialize(req.body, req.get_content_type())47 metadata = body.get('metadata')
49 metadata = data.get('metadata')
50 try:48 try:
51 self.compute_api.update_or_create_instance_metadata(context,49 self.compute_api.update_or_create_instance_metadata(context,
52 server_id,50 server_id,
@@ -55,9 +53,8 @@
55 self._handle_quota_error(error)53 self._handle_quota_error(error)
56 return req.body54 return req.body
5755
58 def update(self, req, server_id, id):56 def update(self, req, server_id, id, body):
59 context = req.environ['nova.context']57 context = req.environ['nova.context']
60 body = self._deserialize(req.body, req.get_content_type())
61 if not id in body:58 if not id in body:
62 expl = _('Request body and URI mismatch')59 expl = _('Request body and URI mismatch')
63 raise exc.HTTPBadRequest(explanation=expl)60 raise exc.HTTPBadRequest(explanation=expl)
@@ -92,3 +89,11 @@
92 if error.code == "MetadataLimitExceeded":89 if error.code == "MetadataLimitExceeded":
93 raise exc.HTTPBadRequest(explanation=error.message)90 raise exc.HTTPBadRequest(explanation=error.message)
94 raise error91 raise error
92
93
94def create_resource():
95 serializers = {
96 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
97 }
98
99 return wsgi.Resource(Controller(), serializers=serializers)
95100
=== modified file 'nova/api/openstack/servers.py'
--- nova/api/openstack/servers.py 2011-05-25 17:55:51 +0000
+++ nova/api/openstack/servers.py 2011-05-26 21:32:29 +0000
@@ -31,6 +31,7 @@
31import nova.api.openstack.views.flavors31import nova.api.openstack.views.flavors
32import nova.api.openstack.views.images32import nova.api.openstack.views.images
33import nova.api.openstack.views.servers33import nova.api.openstack.views.servers
34from nova.api.openstack import wsgi
34from nova.auth import manager as auth_manager35from nova.auth import manager as auth_manager
35from nova.compute import instance_types36from nova.compute import instance_types
36import nova.api.openstack37import nova.api.openstack
@@ -41,31 +42,12 @@
41FLAGS = flags.FLAGS42FLAGS = flags.FLAGS
4243
4344
44class Controller(common.OpenstackController):45class Controller(object):
45 """ The Server API controller for the OpenStack API """46 """ The Server API controller for the OpenStack API """
4647
47 _serialization_metadata = {
48 "application/xml": {
49 "attributes": {
50 "server": ["id", "imageId", "name", "flavorId", "hostId",
51 "status", "progress", "adminPass", "flavorRef",
52 "imageRef"],
53 "link": ["rel", "type", "href"],
54 },
55 "dict_collections": {
56 "metadata": {"item_name": "meta", "item_key": "key"},
57 },
58 "list_collections": {
59 "public": {"item_name": "ip", "item_key": "addr"},
60 "private": {"item_name": "ip", "item_key": "addr"},
61 },
62 },
63 }
64
65 def __init__(self):48 def __init__(self):
66 self.compute_api = compute.API()49 self.compute_api = compute.API()
67 self._image_service = utils.import_object(FLAGS.image_service)50 self._image_service = utils.import_object(FLAGS.image_service)
68 super(Controller, self).__init__()
6951
70 def index(self, req):52 def index(self, req):
71 """ Returns a list of server names and ids for a given user """53 """ Returns a list of server names and ids for a given user """
@@ -122,15 +104,14 @@
122 return faults.Fault(exc.HTTPNotFound())104 return faults.Fault(exc.HTTPNotFound())
123 return exc.HTTPAccepted()105 return exc.HTTPAccepted()
124106
125 def create(self, req):107 def create(self, req, body):
126 """ Creates a new server for a given user """108 """ Creates a new server for a given user """
127 env = self._deserialize_create(req)109 if not body:
128 if not env:
129 return faults.Fault(exc.HTTPUnprocessableEntity())110 return faults.Fault(exc.HTTPUnprocessableEntity())
130111
131 context = req.environ['nova.context']112 context = req.environ['nova.context']
132113
133 password = self._get_server_admin_password(env['server'])114 password = self._get_server_admin_password(body['server'])
134115
135 key_name = None116 key_name = None
136 key_data = None117 key_data = None
@@ -140,7 +121,7 @@
140 key_name = key_pair['name']121 key_name = key_pair['name']
141 key_data = key_pair['public_key']122 key_data = key_pair['public_key']
142123
143 requested_image_id = self._image_id_from_req_data(env)124 requested_image_id = self._image_id_from_req_data(body)
144 try:125 try:
145 image_id = common.get_image_id_from_image_hash(self._image_service,126 image_id = common.get_image_id_from_image_hash(self._image_service,
146 context, requested_image_id)127 context, requested_image_id)
@@ -151,18 +132,18 @@
151 kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(132 kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
152 req, image_id)133 req, image_id)
153134
154 personality = env['server'].get('personality')135 personality = body['server'].get('personality')
155 injected_files = []136 injected_files = []
156 if personality:137 if personality:
157 injected_files = self._get_injected_files(personality)138 injected_files = self._get_injected_files(personality)
158139
159 flavor_id = self._flavor_id_from_req_data(env)140 flavor_id = self._flavor_id_from_req_data(body)
160141
161 if not 'name' in env['server']:142 if not 'name' in body['server']:
162 msg = _("Server name is not defined")143 msg = _("Server name is not defined")
163 return exc.HTTPBadRequest(msg)144 return exc.HTTPBadRequest(msg)
164145
165 name = env['server']['name']146 name = body['server']['name']
166 self._validate_server_name(name)147 self._validate_server_name(name)
167 name = name.strip()148 name = name.strip()
168149
@@ -179,7 +160,7 @@
179 display_description=name,160 display_description=name,
180 key_name=key_name,161 key_name=key_name,
181 key_data=key_data,162 key_data=key_data,
182 metadata=env['server'].get('metadata', {}),163 metadata=body['server'].get('metadata', {}),
183 injected_files=injected_files,164 injected_files=injected_files,
184 admin_password=password)165 admin_password=password)
185 except quota.QuotaError as error:166 except quota.QuotaError as error:
@@ -193,18 +174,6 @@
193 server['server']['adminPass'] = password174 server['server']['adminPass'] = password
194 return server175 return server
195176
196 def _deserialize_create(self, request):
197 """
198 Deserialize a create request
199
200 Overrides normal behavior in the case of xml content
201 """
202 if request.content_type == "application/xml":
203 deserializer = ServerCreateRequestXMLDeserializer()
204 return deserializer.deserialize(request.body)
205 else:
206 return self._deserialize(request.body, request.get_content_type())
207
208 def _get_injected_files(self, personality):177 def _get_injected_files(self, personality):
209 """178 """
210 Create a list of injected files from the personality attribute179 Create a list of injected files from the personality attribute
@@ -254,24 +223,23 @@
254 return utils.generate_password(16)223 return utils.generate_password(16)
255224
256 @scheduler_api.redirect_handler225 @scheduler_api.redirect_handler
257 def update(self, req, id):226 def update(self, req, id, body):
258 """ Updates the server name or password """227 """ Updates the server name or password """
259 if len(req.body) == 0:228 if len(req.body) == 0:
260 raise exc.HTTPUnprocessableEntity()229 raise exc.HTTPUnprocessableEntity()
261230
262 inst_dict = self._deserialize(req.body, req.get_content_type())231 if not body:
263 if not inst_dict:
264 return faults.Fault(exc.HTTPUnprocessableEntity())232 return faults.Fault(exc.HTTPUnprocessableEntity())
265233
266 ctxt = req.environ['nova.context']234 ctxt = req.environ['nova.context']
267 update_dict = {}235 update_dict = {}
268236
269 if 'name' in inst_dict['server']:237 if 'name' in body['server']:
270 name = inst_dict['server']['name']238 name = body['server']['name']
271 self._validate_server_name(name)239 self._validate_server_name(name)
272 update_dict['display_name'] = name.strip()240 update_dict['display_name'] = name.strip()
273241
274 self._parse_update(ctxt, id, inst_dict, update_dict)242 self._parse_update(ctxt, id, body, update_dict)
275243
276 try:244 try:
277 self.compute_api.update(ctxt, id, **update_dict)245 self.compute_api.update(ctxt, id, **update_dict)
@@ -293,7 +261,7 @@
293 pass261 pass
294262
295 @scheduler_api.redirect_handler263 @scheduler_api.redirect_handler
296 def action(self, req, id):264 def action(self, req, id, body):
297 """Multi-purpose method used to reboot, rebuild, or265 """Multi-purpose method used to reboot, rebuild, or
298 resize a server"""266 resize a server"""
299267
@@ -306,10 +274,9 @@
306 'rebuild': self._action_rebuild,274 'rebuild': self._action_rebuild,
307 }275 }
308276
309 input_dict = self._deserialize(req.body, req.get_content_type())
310 for key in actions.keys():277 for key in actions.keys():
311 if key in input_dict:278 if key in body:
312 return actions[key](input_dict, req, id)279 return actions[key](body, req, id)
313 return faults.Fault(exc.HTTPNotImplemented())280 return faults.Fault(exc.HTTPNotImplemented())
314281
315 def _action_change_password(self, input_dict, req, id):282 def _action_change_password(self, input_dict, req, id):
@@ -409,7 +376,7 @@
409 return exc.HTTPAccepted()376 return exc.HTTPAccepted()
410377
411 @scheduler_api.redirect_handler378 @scheduler_api.redirect_handler
412 def reset_network(self, req, id):379 def reset_network(self, req, id, body):
413 """380 """
414 Reset networking on an instance (admin only).381 Reset networking on an instance (admin only).
415382
@@ -424,7 +391,7 @@
424 return exc.HTTPAccepted()391 return exc.HTTPAccepted()
425392
426 @scheduler_api.redirect_handler393 @scheduler_api.redirect_handler
427 def inject_network_info(self, req, id):394 def inject_network_info(self, req, id, body):
428 """395 """
429 Inject network info for an instance (admin only).396 Inject network info for an instance (admin only).
430397
@@ -439,7 +406,7 @@
439 return exc.HTTPAccepted()406 return exc.HTTPAccepted()
440407
441 @scheduler_api.redirect_handler408 @scheduler_api.redirect_handler
442 def pause(self, req, id):409 def pause(self, req, id, body):
443 """ Permit Admins to Pause the server. """410 """ Permit Admins to Pause the server. """
444 ctxt = req.environ['nova.context']411 ctxt = req.environ['nova.context']
445 try:412 try:
@@ -451,7 +418,7 @@
451 return exc.HTTPAccepted()418 return exc.HTTPAccepted()
452419
453 @scheduler_api.redirect_handler420 @scheduler_api.redirect_handler
454 def unpause(self, req, id):421 def unpause(self, req, id, body):
455 """ Permit Admins to Unpause the server. """422 """ Permit Admins to Unpause the server. """
456 ctxt = req.environ['nova.context']423 ctxt = req.environ['nova.context']
457 try:424 try:
@@ -463,7 +430,7 @@
463 return exc.HTTPAccepted()430 return exc.HTTPAccepted()
464431
465 @scheduler_api.redirect_handler432 @scheduler_api.redirect_handler
466 def suspend(self, req, id):433 def suspend(self, req, id, body):
467 """permit admins to suspend the server"""434 """permit admins to suspend the server"""
468 context = req.environ['nova.context']435 context = req.environ['nova.context']
469 try:436 try:
@@ -475,7 +442,7 @@
475 return exc.HTTPAccepted()442 return exc.HTTPAccepted()
476443
477 @scheduler_api.redirect_handler444 @scheduler_api.redirect_handler
478 def resume(self, req, id):445 def resume(self, req, id, body):
479 """permit admins to resume the server from suspend"""446 """permit admins to resume the server from suspend"""
480 context = req.environ['nova.context']447 context = req.environ['nova.context']
481 try:448 try:
@@ -735,11 +702,8 @@
735 raise exc.HTTPBadRequest(msg)702 raise exc.HTTPBadRequest(msg)
736 return password703 return password
737704
738 def get_default_xmlns(self, req):705
739 return common.XML_NS_V11706class ServerXMLDeserializer(wsgi.XMLDeserializer):
740
741
742class ServerCreateRequestXMLDeserializer(object):
743 """707 """
744 Deserializer to handle xml-formatted server create requests.708 Deserializer to handle xml-formatted server create requests.
745709
@@ -747,7 +711,7 @@
747 and personality attributes711 and personality attributes
748 """712 """
749713
750 def deserialize(self, string):714 def create(self, string):
751 """Deserialize an xml-formatted server create request"""715 """Deserialize an xml-formatted server create request"""
752 dom = minidom.parseString(string)716 dom = minidom.parseString(string)
753 server = self._extract_server(dom)717 server = self._extract_server(dom)
@@ -814,3 +778,43 @@
814 if child.nodeType == child.TEXT_NODE:778 if child.nodeType == child.TEXT_NODE:
815 return child.nodeValue779 return child.nodeValue
816 return ""780 return ""
781
782
783def create_resource(version='1.0'):
784 controller = {
785 '1.0': ControllerV10,
786 '1.1': ControllerV11,
787 }[version]()
788
789 metadata = {
790 "attributes": {
791 "server": ["id", "imageId", "name", "flavorId", "hostId",
792 "status", "progress", "adminPass", "flavorRef",
793 "imageRef"],
794 "link": ["rel", "type", "href"],
795 },
796 "dict_collections": {
797 "metadata": {"item_name": "meta", "item_key": "key"},
798 },
799 "list_collections": {
800 "public": {"item_name": "ip", "item_key": "addr"},
801 "private": {"item_name": "ip", "item_key": "addr"},
802 },
803 }
804
805 xmlns = {
806 '1.0': wsgi.XMLNS_V10,
807 '1.1': wsgi.XMLNS_V11,
808 }[version]
809
810 serializers = {
811 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
812 xmlns=xmlns),
813 }
814
815 deserializers = {
816 'application/xml': ServerXMLDeserializer(),
817 }
818
819 return wsgi.Resource(controller, serializers=serializers,
820 deserializers=deserializers)
817821
=== modified file 'nova/api/openstack/shared_ip_groups.py'
--- nova/api/openstack/shared_ip_groups.py 2011-03-30 17:05:06 +0000
+++ nova/api/openstack/shared_ip_groups.py 2011-05-26 21:32:29 +0000
@@ -17,29 +17,13 @@
1717
18from webob import exc18from webob import exc
1919
20from nova.api.openstack import common
21from nova.api.openstack import faults20from nova.api.openstack import faults
2221from nova.api.openstack import wsgi
2322
24def _translate_keys(inst):23
25 """ Coerces a shared IP group instance into proper dictionary format """24class Controller(object):
26 return dict(sharedIpGroup=inst)
27
28
29def _translate_detail_keys(inst):
30 """ Coerces a shared IP group instance into proper dictionary format with
31 correctly mapped attributes """
32 return dict(sharedIpGroups=inst)
33
34
35class Controller(common.OpenstackController):
36 """ The Shared IP Groups Controller for the Openstack API """25 """ The Shared IP Groups Controller for the Openstack API """
3726
38 _serialization_metadata = {
39 'application/xml': {
40 'attributes': {
41 'sharedIpGroup': []}}}
42
43 def index(self, req):27 def index(self, req):
44 """ Returns a list of Shared IP Groups for the user """28 """ Returns a list of Shared IP Groups for the user """
45 raise faults.Fault(exc.HTTPNotImplemented())29 raise faults.Fault(exc.HTTPNotImplemented())
@@ -48,7 +32,7 @@
48 """ Shows in-depth information on a specific Shared IP Group """32 """ Shows in-depth information on a specific Shared IP Group """
49 raise faults.Fault(exc.HTTPNotImplemented())33 raise faults.Fault(exc.HTTPNotImplemented())
5034
51 def update(self, req, id):35 def update(self, req, id, body):
52 """ You can't update a Shared IP Group """36 """ You can't update a Shared IP Group """
53 raise faults.Fault(exc.HTTPNotImplemented())37 raise faults.Fault(exc.HTTPNotImplemented())
5438
@@ -60,6 +44,10 @@
60 """ Returns a complete list of Shared IP Groups """44 """ Returns a complete list of Shared IP Groups """
61 raise faults.Fault(exc.HTTPNotImplemented())45 raise faults.Fault(exc.HTTPNotImplemented())
6246
63 def create(self, req):47 def create(self, req, body):
64 """ Creates a new Shared IP group """48 """ Creates a new Shared IP group """
65 raise faults.Fault(exc.HTTPNotImplemented())49 raise faults.Fault(exc.HTTPNotImplemented())
50
51
52def create_resource():
53 return wsgi.Resource(Controller())
6654
=== modified file 'nova/api/openstack/users.py'
--- nova/api/openstack/users.py 2011-04-27 21:03:05 +0000
+++ nova/api/openstack/users.py 2011-05-26 21:32:29 +0000
@@ -20,8 +20,10 @@
20from nova import log as logging20from nova import log as logging
21from nova.api.openstack import common21from nova.api.openstack import common
22from nova.api.openstack import faults22from nova.api.openstack import faults
23from nova.api.openstack import wsgi
23from nova.auth import manager24from nova.auth import manager
2425
26
25FLAGS = flags.FLAGS27FLAGS = flags.FLAGS
26LOG = logging.getLogger('nova.api.openstack')28LOG = logging.getLogger('nova.api.openstack')
2729
@@ -34,12 +36,7 @@
34 admin=user.admin)36 admin=user.admin)
3537
3638
37class Controller(common.OpenstackController):39class Controller(object):
38
39 _serialization_metadata = {
40 'application/xml': {
41 "attributes": {
42 "user": ["id", "name", "access", "secret", "admin"]}}}
4340
44 def __init__(self):41 def __init__(self):
45 self.manager = manager.AuthManager()42 self.manager = manager.AuthManager()
@@ -81,23 +78,35 @@
81 self.manager.delete_user(id)78 self.manager.delete_user(id)
82 return {}79 return {}
8380
84 def create(self, req):81 def create(self, req, body):
85 self._check_admin(req.environ['nova.context'])82 self._check_admin(req.environ['nova.context'])
86 env = self._deserialize(req.body, req.get_content_type())83 is_admin = body['user'].get('admin') in ('T', 'True', True)
87 is_admin = env['user'].get('admin') in ('T', 'True', True)84 name = body['user'].get('name')
88 name = env['user'].get('name')85 access = body['user'].get('access')
89 access = env['user'].get('access')86 secret = body['user'].get('secret')
90 secret = env['user'].get('secret')
91 user = self.manager.create_user(name, access, secret, is_admin)87 user = self.manager.create_user(name, access, secret, is_admin)
92 return dict(user=_translate_keys(user))88 return dict(user=_translate_keys(user))
9389
94 def update(self, req, id):90 def update(self, req, id, body):
95 self._check_admin(req.environ['nova.context'])91 self._check_admin(req.environ['nova.context'])
96 env = self._deserialize(req.body, req.get_content_type())92 is_admin = body['user'].get('admin')
97 is_admin = env['user'].get('admin')
98 if is_admin is not None:93 if is_admin is not None:
99 is_admin = is_admin in ('T', 'True', True)94 is_admin = is_admin in ('T', 'True', True)
100 access = env['user'].get('access')95 access = body['user'].get('access')
101 secret = env['user'].get('secret')96 secret = body['user'].get('secret')
102 self.manager.modify_user(id, access, secret, is_admin)97 self.manager.modify_user(id, access, secret, is_admin)
103 return dict(user=_translate_keys(self.manager.get_user(id)))98 return dict(user=_translate_keys(self.manager.get_user(id)))
99
100
101def create_resource():
102 metadata = {
103 "attributes": {
104 "user": ["id", "name", "access", "secret", "admin"],
105 },
106 }
107
108 serializers = {
109 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
110 }
111
112 return wsgi.Resource(Controller(), serializers=serializers)
104113
=== modified file 'nova/api/openstack/versions.py'
--- nova/api/openstack/versions.py 2011-03-29 15:41:33 +0000
+++ nova/api/openstack/versions.py 2011-05-26 21:32:29 +0000
@@ -18,13 +18,26 @@
18import webob18import webob
19import webob.dec19import webob.dec
2020
21from nova import wsgi
22import nova.api.openstack.views.versions21import nova.api.openstack.views.versions
2322from nova.api.openstack import wsgi
2423
25class Versions(wsgi.Application):24
26 @webob.dec.wsgify(RequestClass=wsgi.Request)25class Versions(wsgi.Resource):
27 def __call__(self, req):26 def __init__(self):
27 metadata = {
28 "attributes": {
29 "version": ["status", "id"],
30 "link": ["rel", "href"],
31 }
32 }
33
34 serializers = {
35 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
36 }
37
38 super(Versions, self).__init__(None, serializers=serializers)
39
40 def dispatch(self, request, *args):
28 """Respond to a request for all OpenStack API versions."""41 """Respond to a request for all OpenStack API versions."""
29 version_objs = [42 version_objs = [
30 {43 {
@@ -37,24 +50,6 @@
37 },50 },
38 ]51 ]
3952
40 builder = nova.api.openstack.views.versions.get_view_builder(req)53 builder = nova.api.openstack.views.versions.get_view_builder(request)
41 versions = [builder.build(version) for version in version_objs]54 versions = [builder.build(version) for version in version_objs]
42 response = dict(versions=versions)55 return dict(versions=versions)
43
44 metadata = {
45 "application/xml": {
46 "attributes": {
47 "version": ["status", "id"],
48 "link": ["rel", "href"],
49 }
50 }
51 }
52
53 content_type = req.best_match_content_type()
54 body = wsgi.Serializer(metadata).serialize(response, content_type)
55
56 response = webob.Response()
57 response.content_type = content_type
58 response.body = body
59
60 return response
6156
=== added file 'nova/api/openstack/wsgi.py'
--- nova/api/openstack/wsgi.py 1970-01-01 00:00:00 +0000
+++ nova/api/openstack/wsgi.py 2011-05-26 21:32:29 +0000
@@ -0,0 +1,380 @@
1
2import json
3import webob
4from xml.dom import minidom
5
6from nova import exception
7from nova import log as logging
8from nova import utils
9from nova import wsgi
10
11
12XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
13XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
14
15LOG = logging.getLogger('nova.api.openstack.wsgi')
16
17
18class Request(webob.Request):
19 """Add some Openstack API-specific logic to the base webob.Request."""
20
21 def best_match_content_type(self):
22 """Determine the requested response content-type.
23
24 Based on the query extension then the Accept header.
25
26 """
27 supported = ('application/json', 'application/xml')
28
29 parts = self.path.rsplit('.', 1)
30 if len(parts) > 1:
31 ctype = 'application/{0}'.format(parts[1])
32 if ctype in supported:
33 return ctype
34
35 bm = self.accept.best_match(supported)
36
37 # default to application/json if we don't find a preference
38 return bm or 'application/json'
39
40 def get_content_type(self):
41 """Determine content type of the request body.
42
43 Does not do any body introspection, only checks header
44
45 """
46 if not "Content-Type" in self.headers:
47 raise exception.InvalidContentType(content_type=None)
48
49 allowed_types = ("application/xml", "application/json")
50 content_type = self.content_type
51
52 if content_type not in allowed_types:
53 raise exception.InvalidContentType(content_type=content_type)
54 else:
55 return content_type
56
57
58class TextDeserializer(object):
59 """Custom request body deserialization based on controller action name."""
60
61 def deserialize(self, datastring, action='default'):
62 """Find local deserialization method and parse request body."""
63 action_method = getattr(self, action, self.default)
64 return action_method(datastring)
65
66 def default(self, datastring):
67 """Default deserialization code should live here"""
68 raise NotImplementedError()
69
70
71class JSONDeserializer(TextDeserializer):
72
73 def default(self, datastring):
74 return utils.loads(datastring)
75
76
77class XMLDeserializer(TextDeserializer):
78
79 def __init__(self, metadata=None):
80 """
81 :param metadata: information needed to deserialize xml into
82 a dictionary.
83 """
84 super(XMLDeserializer, self).__init__()
85 self.metadata = metadata or {}
86
87 def default(self, datastring):
88 plurals = set(self.metadata.get('plurals', {}))
89 node = minidom.parseString(datastring).childNodes[0]
90 return {node.nodeName: self._from_xml_node(node, plurals)}
91
92 def _from_xml_node(self, node, listnames):
93 """Convert a minidom node to a simple Python type.
94
95 :param listnames: list of XML node names whose subnodes should
96 be considered list items.
97
98 """
99 if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
100 return node.childNodes[0].nodeValue
101 elif node.nodeName in listnames:
102 return [self._from_xml_node(n, listnames) for n in node.childNodes]
103 else:
104 result = dict()
105 for attr in node.attributes.keys():
106 result[attr] = node.attributes[attr].nodeValue
107 for child in node.childNodes:
108 if child.nodeType != node.TEXT_NODE:
109 result[child.nodeName] = self._from_xml_node(child,
110 listnames)
111 return result
112
113
114class RequestDeserializer(object):
115 """Break up a Request object into more useful pieces."""
116
117 def __init__(self, deserializers=None):
118 """
119 :param deserializers: dictionary of content-type-specific deserializers
120
121 """
122 self.deserializers = {
123 'application/xml': XMLDeserializer(),
124 'application/json': JSONDeserializer(),
125 }
126
127 self.deserializers.update(deserializers or {})
128
129 def deserialize(self, request):
130 """Extract necessary pieces of the request.
131
132 :param request: Request object
133 :returns tuple of expected controller action name, dictionary of
134 keyword arguments to pass to the controller, the expected
135 content type of the response
136
137 """
138 action_args = self.get_action_args(request.environ)
139 action = action_args.pop('action', None)
140
141 if request.method.lower() in ('post', 'put'):
142 if len(request.body) == 0:
143 action_args['body'] = None
144 else:
145 content_type = request.get_content_type()
146 deserializer = self.get_deserializer(content_type)
147
148 try:
149 body = deserializer.deserialize(request.body, action)
150 action_args['body'] = body
151 except exception.InvalidContentType:
152 action_args['body'] = None
153
154 accept = self.get_expected_content_type(request)
155
156 return (action, action_args, accept)
157
158 def get_deserializer(self, content_type):
159 try:
160 return self.deserializers[content_type]
161 except (KeyError, TypeError):
162 raise exception.InvalidContentType(content_type=content_type)
163
164 def get_expected_content_type(self, request):
165 return request.best_match_content_type()
166
167 def get_action_args(self, request_environment):
168 """Parse dictionary created by routes library."""
169 try:
170 args = request_environment['wsgiorg.routing_args'][1].copy()
171 except Exception:
172 return {}
173
174 try:
175 del args['controller']
176 except KeyError:
177 pass
178
179 try:
180 del args['format']
181 except KeyError:
182 pass
183
184 return args
185
186
187class DictSerializer(object):
188 """Custom response body serialization based on controller action name."""
189
190 def serialize(self, data, action='default'):
191 """Find local serialization method and encode response body."""
192 action_method = getattr(self, action, self.default)
193 return action_method(data)
194
195 def default(self, data):
196 """Default serialization code should live here"""
197 raise NotImplementedError()
198
199
200class JSONDictSerializer(DictSerializer):
201
202 def default(self, data):
203 return utils.dumps(data)
204
205
206class XMLDictSerializer(DictSerializer):
207
208 def __init__(self, metadata=None, xmlns=None):
209 """
210 :param metadata: information needed to deserialize xml into
211 a dictionary.
212 :param xmlns: XML namespace to include with serialized xml
213 """
214 super(XMLDictSerializer, self).__init__()
215 self.metadata = metadata or {}
216 self.xmlns = xmlns
217
218 def default(self, data):
219 # We expect data to contain a single key which is the XML root.
220 root_key = data.keys()[0]
221 doc = minidom.Document()
222 node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
223
224 xmlns = node.getAttribute('xmlns')
225 if not xmlns and self.xmlns:
226 node.setAttribute('xmlns', self.xmlns)
227
228 return node.toprettyxml(indent=' ')
229
230 def _to_xml_node(self, doc, metadata, nodename, data):
231 """Recursive method to convert data members to XML nodes."""
232 result = doc.createElement(nodename)
233
234 # Set the xml namespace if one is specified
235 # TODO(justinsb): We could also use prefixes on the keys
236 xmlns = metadata.get('xmlns', None)
237 if xmlns:
238 result.setAttribute('xmlns', xmlns)
239
240 #TODO(bcwaldon): accomplish this without a type-check
241 if type(data) is list:
242 collections = metadata.get('list_collections', {})
243 if nodename in collections:
244 metadata = collections[nodename]
245 for item in data:
246 node = doc.createElement(metadata['item_name'])
247 node.setAttribute(metadata['item_key'], str(item))
248 result.appendChild(node)
249 return result
250 singular = metadata.get('plurals', {}).get(nodename, None)
251 if singular is None:
252 if nodename.endswith('s'):
253 singular = nodename[:-1]
254 else:
255 singular = 'item'
256 for item in data:
257 node = self._to_xml_node(doc, metadata, singular, item)
258 result.appendChild(node)
259 #TODO(bcwaldon): accomplish this without a type-check
260 elif type(data) is dict:
261 collections = metadata.get('dict_collections', {})
262 if nodename in collections:
263 metadata = collections[nodename]
264 for k, v in data.items():
265 node = doc.createElement(metadata['item_name'])
266 node.setAttribute(metadata['item_key'], str(k))
267 text = doc.createTextNode(str(v))
268 node.appendChild(text)
269 result.appendChild(node)
270 return result
271 attrs = metadata.get('attributes', {}).get(nodename, {})
272 for k, v in data.items():
273 if k in attrs:
274 result.setAttribute(k, str(v))
275 else:
276 node = self._to_xml_node(doc, metadata, k, v)
277 result.appendChild(node)
278 else:
279 # Type is atom
280 node = doc.createTextNode(str(data))
281 result.appendChild(node)
282 return result
283
284
285class ResponseSerializer(object):
286 """Encode the necessary pieces into a response object"""
287
288 def __init__(self, serializers=None):
289 """
290 :param serializers: dictionary of content-type-specific serializers
291
292 """
293 self.serializers = {
294 'application/xml': XMLDictSerializer(),
295 'application/json': JSONDictSerializer(),
296 }
297 self.serializers.update(serializers or {})
298
299 def serialize(self, response_data, content_type):
300 """Serialize a dict into a string and wrap in a wsgi.Request object.
301
302 :param response_data: dict produced by the Controller
303 :param content_type: expected mimetype of serialized response body
304
305 """
306 response = webob.Response()
307 response.headers['Content-Type'] = content_type
308
309 serializer = self.get_serializer(content_type)
310 response.body = serializer.serialize(response_data)
311
312 return response
313
314 def get_serializer(self, content_type):
315 try:
316 return self.serializers[content_type]
317 except (KeyError, TypeError):
318 raise exception.InvalidContentType(content_type=content_type)
319
320
321class Resource(wsgi.Application):
322 """WSGI app that handles (de)serialization and controller dispatch.
323
324 WSGI app that reads routing information supplied by RoutesMiddleware
325 and calls the requested action method upon its controller. All
326 controller action methods must accept a 'req' argument, which is the
327 incoming wsgi.Request. If the operation is a PUT or POST, the controller
328 method must also accept a 'body' argument (the deserialized request body).
329 They may raise a webob.exc exception or return a dict, which will be
330 serialized by requested content type.
331
332 """
333 def __init__(self, controller, serializers=None, deserializers=None):
334 """
335 :param controller: object that implement methods created by routes lib
336 :param serializers: dict of content-type specific text serializers
337 :param deserializers: dict of content-type specific text deserializers
338
339 """
340 self.controller = controller
341 self.serializer = ResponseSerializer(serializers)
342 self.deserializer = RequestDeserializer(deserializers)
343
344 @webob.dec.wsgify(RequestClass=Request)
345 def __call__(self, request):
346 """WSGI method that controls (de)serialization and method dispatch."""
347
348 LOG.debug("%(method)s %(url)s" % {"method": request.method,
349 "url": request.url})
350
351 try:
352 action, action_args, accept = self.deserializer.deserialize(
353 request)
354 except exception.InvalidContentType:
355 return webob.exc.HTTPBadRequest(_("Unsupported Content-Type"))
356
357 action_result = self.dispatch(request, action, action_args)
358
359 #TODO(bcwaldon): find a more elegant way to pass through non-dict types
360 if type(action_result) is dict:
361 response = self.serializer.serialize(action_result, accept)
362 else:
363 response = action_result
364
365 try:
366 msg_dict = dict(url=request.url, status=response.status_int)
367 msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
368 except AttributeError:
369 msg_dict = dict(url=request.url)
370 msg = _("%(url)s returned a fault")
371
372 LOG.debug(msg)
373
374 return response
375
376 def dispatch(self, request, action, action_args):
377 """Find action-spefic method on controller and call it."""
378
379 controller_method = getattr(self.controller, action)
380 return controller_method(req=request, **action_args)
0381
=== modified file 'nova/api/openstack/zones.py'
--- nova/api/openstack/zones.py 2011-05-18 20:14:24 +0000
+++ nova/api/openstack/zones.py 2011-05-26 21:32:29 +0000
@@ -22,6 +22,7 @@
22from nova import flags22from nova import flags
23from nova import log as logging23from nova import log as logging
24from nova.api.openstack import common24from nova.api.openstack import common
25from nova.api.openstack import wsgi
25from nova.scheduler import api26from nova.scheduler import api
2627
2728
@@ -52,12 +53,7 @@
52 'deleted', 'deleted_at', 'updated_at'))53 'deleted', 'deleted_at', 'updated_at'))
5354
5455
55class Controller(common.OpenstackController):56class Controller(object):
56
57 _serialization_metadata = {
58 'application/xml': {
59 "attributes": {
60 "zone": ["id", "api_url", "name", "capabilities"]}}}
6157
62 def index(self, req):58 def index(self, req):
63 """Return all zones in brief"""59 """Return all zones in brief"""
@@ -96,17 +92,15 @@
96 api.zone_delete(req.environ['nova.context'], zone_id)92 api.zone_delete(req.environ['nova.context'], zone_id)
97 return {}93 return {}
9894
99 def create(self, req):95 def create(self, req, body):
100 context = req.environ['nova.context']96 context = req.environ['nova.context']
101 env = self._deserialize(req.body, req.get_content_type())97 zone = api.zone_create(context, body["zone"])
102 zone = api.zone_create(context, env["zone"])
103 return dict(zone=_scrub_zone(zone))98 return dict(zone=_scrub_zone(zone))
10499
105 def update(self, req, id):100 def update(self, req, id, body):
106 context = req.environ['nova.context']101 context = req.environ['nova.context']
107 env = self._deserialize(req.body, req.get_content_type())
108 zone_id = int(id)102 zone_id = int(id)
109 zone = api.zone_update(context, zone_id, env["zone"])103 zone = api.zone_update(context, zone_id, body["zone"])
110 return dict(zone=_scrub_zone(zone))104 return dict(zone=_scrub_zone(zone))
111105
112 def select(self, req):106 def select(self, req):
@@ -140,3 +134,18 @@
140 cooked.append(dict(weight=entry['weight'],134 cooked.append(dict(weight=entry['weight'],
141 blob=cipher_text))135 blob=cipher_text))
142 return cooked136 return cooked
137
138
139def create_resource():
140 metadata = {
141 "attributes": {
142 "zone": ["id", "api_url", "name", "capabilities"],
143 },
144 }
145
146 serializers = {
147 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
148 metadata=metadata),
149 }
150
151 return wsgi.Resource(Controller(), serializers=serializers)
143152
=== modified file 'nova/objectstore/s3server.py'
--- nova/objectstore/s3server.py 2011-03-24 23:38:31 +0000
+++ nova/objectstore/s3server.py 2011-05-26 21:32:29 +0000
@@ -81,7 +81,7 @@
81 super(S3Application, self).__init__(mapper)81 super(S3Application, self).__init__(mapper)
8282
8383
84class BaseRequestHandler(wsgi.Controller):84class BaseRequestHandler(object):
85 """Base class emulating Tornado's web framework pattern in WSGI.85 """Base class emulating Tornado's web framework pattern in WSGI.
8686
87 This is a direct port of Tornado's implementation, so some key decisions87 This is a direct port of Tornado's implementation, so some key decisions
8888
=== modified file 'nova/tests/api/openstack/extensions/foxinsocks.py'
--- nova/tests/api/openstack/extensions/foxinsocks.py 2011-05-12 18:45:39 +0000
+++ nova/tests/api/openstack/extensions/foxinsocks.py 2011-05-26 21:32:29 +0000
@@ -17,12 +17,10 @@
1717
18import json18import json
1919
20from nova import wsgi
21
22from nova.api.openstack import extensions20from nova.api.openstack import extensions
2321
2422
25class FoxInSocksController(wsgi.Controller):23class FoxInSocksController(object):
2624
27 def index(self, req):25 def index(self, req):
28 return "Try to say this Mr. Knox, sir..."26 return "Try to say this Mr. Knox, sir..."
2927
=== modified file 'nova/tests/api/openstack/test_extensions.py'
--- nova/tests/api/openstack/test_extensions.py 2011-05-12 18:37:15 +0000
+++ nova/tests/api/openstack/test_extensions.py 2011-05-26 21:32:29 +0000
@@ -26,15 +26,15 @@
26from nova.api import openstack26from nova.api import openstack
27from nova.api.openstack import extensions27from nova.api.openstack import extensions
28from nova.api.openstack import flavors28from nova.api.openstack import flavors
29from nova.api.openstack import wsgi
29from nova.tests.api.openstack import fakes30from nova.tests.api.openstack import fakes
30import nova.wsgi
3131
32FLAGS = flags.FLAGS32FLAGS = flags.FLAGS
3333
34response_body = "Try to say this Mr. Knox, sir..."34response_body = "Try to say this Mr. Knox, sir..."
3535
3636
37class StubController(nova.wsgi.Controller):37class StubController(object):
3838
39 def __init__(self, body):39 def __init__(self, body):
40 self.body = body40 self.body = body
4141
=== modified file 'nova/tests/api/openstack/test_limits.py'
--- nova/tests/api/openstack/test_limits.py 2011-05-25 20:58:40 +0000
+++ nova/tests/api/openstack/test_limits.py 2011-05-26 21:32:29 +0000
@@ -73,7 +73,7 @@
73 def setUp(self):73 def setUp(self):
74 """Run before each test."""74 """Run before each test."""
75 BaseLimitTestSuite.setUp(self)75 BaseLimitTestSuite.setUp(self)
76 self.controller = limits.LimitsControllerV10()76 self.controller = limits.create_resource('1.0')
7777
78 def _get_index_request(self, accept_header="application/json"):78 def _get_index_request(self, accept_header="application/json"):
79 """Helper to set routing arguments."""79 """Helper to set routing arguments."""
@@ -209,7 +209,7 @@
209 def setUp(self):209 def setUp(self):
210 """Run before each test."""210 """Run before each test."""
211 BaseLimitTestSuite.setUp(self)211 BaseLimitTestSuite.setUp(self)
212 self.controller = limits.LimitsControllerV11()212 self.controller = limits.create_resource('1.1')
213213
214 def _get_index_request(self, accept_header="application/json"):214 def _get_index_request(self, accept_header="application/json"):
215 """Helper to set routing arguments."""215 """Helper to set routing arguments."""
216216
=== modified file 'nova/tests/api/openstack/test_servers.py'
--- nova/tests/api/openstack/test_servers.py 2011-05-25 20:10:25 +0000
+++ nova/tests/api/openstack/test_servers.py 2011-05-26 21:32:29 +0000
@@ -217,7 +217,6 @@
217 },217 },
218 ]218 ]
219219
220 print res_dict['server']
221 self.assertEqual(res_dict['server']['links'], expected_links)220 self.assertEqual(res_dict['server']['links'], expected_links)
222221
223 def test_get_server_by_id_with_addresses_xml(self):222 def test_get_server_by_id_with_addresses_xml(self):
@@ -844,7 +843,6 @@
844 req = webob.Request.blank('/v1.0/servers/detail')843 req = webob.Request.blank('/v1.0/servers/detail')
845 req.headers['Accept'] = 'application/xml'844 req.headers['Accept'] = 'application/xml'
846 res = req.get_response(fakes.wsgi_app())845 res = req.get_response(fakes.wsgi_app())
847 print res.body
848 dom = minidom.parseString(res.body)846 dom = minidom.parseString(res.body)
849 for i, server in enumerate(dom.getElementsByTagName('server')):847 for i, server in enumerate(dom.getElementsByTagName('server')):
850 self.assertEqual(server.getAttribute('id'), str(i))848 self.assertEqual(server.getAttribute('id'), str(i))
@@ -1008,6 +1006,14 @@
1008 res = req.get_response(fakes.wsgi_app())1006 res = req.get_response(fakes.wsgi_app())
1009 self.assertEqual(res.status_int, 501)1007 self.assertEqual(res.status_int, 501)
10101008
1009 def test_server_change_password_xml(self):
1010 req = webob.Request.blank('/v1.0/servers/1/action')
1011 req.method = 'POST'
1012 req.content_type = 'application/xml'
1013 req.body = '<changePassword adminPass="1234pass">'
1014# res = req.get_response(fakes.wsgi_app())
1015# self.assertEqual(res.status_int, 501)
1016
1011 def test_server_change_password_v1_1(self):1017 def test_server_change_password_v1_1(self):
1012 mock_method = MockSetAdminPassword()1018 mock_method = MockSetAdminPassword()
1013 self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)1019 self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
@@ -1380,13 +1386,13 @@
1380class TestServerCreateRequestXMLDeserializer(unittest.TestCase):1386class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
13811387
1382 def setUp(self):1388 def setUp(self):
1383 self.deserializer = servers.ServerCreateRequestXMLDeserializer()1389 self.deserializer = servers.ServerXMLDeserializer()
13841390
1385 def test_minimal_request(self):1391 def test_minimal_request(self):
1386 serial_request = """1392 serial_request = """
1387<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"1393<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
1388 name="new-server-test" imageId="1" flavorId="1"/>"""1394 name="new-server-test" imageId="1" flavorId="1"/>"""
1389 request = self.deserializer.deserialize(serial_request)1395 request = self.deserializer.deserialize(serial_request, 'create')
1390 expected = {"server": {1396 expected = {"server": {
1391 "name": "new-server-test",1397 "name": "new-server-test",
1392 "imageId": "1",1398 "imageId": "1",
@@ -1400,7 +1406,7 @@
1400 name="new-server-test" imageId="1" flavorId="1">1406 name="new-server-test" imageId="1" flavorId="1">
1401 <metadata/>1407 <metadata/>
1402</server>"""1408</server>"""
1403 request = self.deserializer.deserialize(serial_request)1409 request = self.deserializer.deserialize(serial_request, 'create')
1404 expected = {"server": {1410 expected = {"server": {
1405 "name": "new-server-test",1411 "name": "new-server-test",
1406 "imageId": "1",1412 "imageId": "1",
@@ -1415,7 +1421,7 @@
1415 name="new-server-test" imageId="1" flavorId="1">1421 name="new-server-test" imageId="1" flavorId="1">
1416 <personality/>1422 <personality/>
1417</server>"""1423</server>"""
1418 request = self.deserializer.deserialize(serial_request)1424 request = self.deserializer.deserialize(serial_request, 'create')
1419 expected = {"server": {1425 expected = {"server": {
1420 "name": "new-server-test",1426 "name": "new-server-test",
1421 "imageId": "1",1427 "imageId": "1",
@@ -1431,7 +1437,7 @@
1431 <metadata/>1437 <metadata/>
1432 <personality/>1438 <personality/>
1433</server>"""1439</server>"""
1434 request = self.deserializer.deserialize(serial_request)1440 request = self.deserializer.deserialize(serial_request, 'create')
1435 expected = {"server": {1441 expected = {"server": {
1436 "name": "new-server-test",1442 "name": "new-server-test",
1437 "imageId": "1",1443 "imageId": "1",
@@ -1448,7 +1454,7 @@
1448 <personality/>1454 <personality/>
1449 <metadata/>1455 <metadata/>
1450</server>"""1456</server>"""
1451 request = self.deserializer.deserialize(serial_request)1457 request = self.deserializer.deserialize(serial_request, 'create')
1452 expected = {"server": {1458 expected = {"server": {
1453 "name": "new-server-test",1459 "name": "new-server-test",
1454 "imageId": "1",1460 "imageId": "1",
@@ -1466,7 +1472,7 @@
1466 <file path="/etc/conf">aabbccdd</file>1472 <file path="/etc/conf">aabbccdd</file>
1467 </personality>1473 </personality>
1468</server>"""1474</server>"""
1469 request = self.deserializer.deserialize(serial_request)1475 request = self.deserializer.deserialize(serial_request, 'create')
1470 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]1476 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
1471 self.assertEquals(request["server"]["personality"], expected)1477 self.assertEquals(request["server"]["personality"], expected)
14721478
@@ -1476,7 +1482,7 @@
1476 name="new-server-test" imageId="1" flavorId="1">1482 name="new-server-test" imageId="1" flavorId="1">
1477<personality><file path="/etc/conf">aabbccdd</file>1483<personality><file path="/etc/conf">aabbccdd</file>
1478<file path="/etc/sudoers">abcd</file></personality></server>"""1484<file path="/etc/sudoers">abcd</file></personality></server>"""
1479 request = self.deserializer.deserialize(serial_request)1485 request = self.deserializer.deserialize(serial_request, 'create')
1480 expected = [{"path": "/etc/conf", "contents": "aabbccdd"},1486 expected = [{"path": "/etc/conf", "contents": "aabbccdd"},
1481 {"path": "/etc/sudoers", "contents": "abcd"}]1487 {"path": "/etc/sudoers", "contents": "abcd"}]
1482 self.assertEquals(request["server"]["personality"], expected)1488 self.assertEquals(request["server"]["personality"], expected)
@@ -1492,7 +1498,7 @@
1492 <file path="/etc/ignoreme">anything</file>1498 <file path="/etc/ignoreme">anything</file>
1493 </personality>1499 </personality>
1494</server>"""1500</server>"""
1495 request = self.deserializer.deserialize(serial_request)1501 request = self.deserializer.deserialize(serial_request, 'create')
1496 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]1502 expected = [{"path": "/etc/conf", "contents": "aabbccdd"}]
1497 self.assertEquals(request["server"]["personality"], expected)1503 self.assertEquals(request["server"]["personality"], expected)
14981504
@@ -1501,7 +1507,7 @@
1501<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"1507<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
1502 name="new-server-test" imageId="1" flavorId="1">1508 name="new-server-test" imageId="1" flavorId="1">
1503<personality><file>aabbccdd</file></personality></server>"""1509<personality><file>aabbccdd</file></personality></server>"""
1504 request = self.deserializer.deserialize(serial_request)1510 request = self.deserializer.deserialize(serial_request, 'create')
1505 expected = [{"contents": "aabbccdd"}]1511 expected = [{"contents": "aabbccdd"}]
1506 self.assertEquals(request["server"]["personality"], expected)1512 self.assertEquals(request["server"]["personality"], expected)
15071513
@@ -1510,7 +1516,7 @@
1510<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"1516<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
1511 name="new-server-test" imageId="1" flavorId="1">1517 name="new-server-test" imageId="1" flavorId="1">
1512<personality><file path="/etc/conf"></file></personality></server>"""1518<personality><file path="/etc/conf"></file></personality></server>"""
1513 request = self.deserializer.deserialize(serial_request)1519 request = self.deserializer.deserialize(serial_request, 'create')
1514 expected = [{"path": "/etc/conf", "contents": ""}]1520 expected = [{"path": "/etc/conf", "contents": ""}]
1515 self.assertEquals(request["server"]["personality"], expected)1521 self.assertEquals(request["server"]["personality"], expected)
15161522
@@ -1519,7 +1525,7 @@
1519<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"1525<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"
1520 name="new-server-test" imageId="1" flavorId="1">1526 name="new-server-test" imageId="1" flavorId="1">
1521<personality><file path="/etc/conf"/></personality></server>"""1527<personality><file path="/etc/conf"/></personality></server>"""
1522 request = self.deserializer.deserialize(serial_request)1528 request = self.deserializer.deserialize(serial_request, 'create')
1523 expected = [{"path": "/etc/conf", "contents": ""}]1529 expected = [{"path": "/etc/conf", "contents": ""}]
1524 self.assertEquals(request["server"]["personality"], expected)1530 self.assertEquals(request["server"]["personality"], expected)
15251531
@@ -1531,7 +1537,7 @@
1531 <meta key="alpha">beta</meta>1537 <meta key="alpha">beta</meta>
1532 </metadata>1538 </metadata>
1533</server>"""1539</server>"""
1534 request = self.deserializer.deserialize(serial_request)1540 request = self.deserializer.deserialize(serial_request, 'create')
1535 expected = {"alpha": "beta"}1541 expected = {"alpha": "beta"}
1536 self.assertEquals(request["server"]["metadata"], expected)1542 self.assertEquals(request["server"]["metadata"], expected)
15371543
@@ -1544,7 +1550,7 @@
1544 <meta key="foo">bar</meta>1550 <meta key="foo">bar</meta>
1545 </metadata>1551 </metadata>
1546</server>"""1552</server>"""
1547 request = self.deserializer.deserialize(serial_request)1553 request = self.deserializer.deserialize(serial_request, 'create')
1548 expected = {"alpha": "beta", "foo": "bar"}1554 expected = {"alpha": "beta", "foo": "bar"}
1549 self.assertEquals(request["server"]["metadata"], expected)1555 self.assertEquals(request["server"]["metadata"], expected)
15501556
@@ -1556,7 +1562,7 @@
1556 <meta key="alpha"></meta>1562 <meta key="alpha"></meta>
1557 </metadata>1563 </metadata>
1558</server>"""1564</server>"""
1559 request = self.deserializer.deserialize(serial_request)1565 request = self.deserializer.deserialize(serial_request, 'create')
1560 expected = {"alpha": ""}1566 expected = {"alpha": ""}
1561 self.assertEquals(request["server"]["metadata"], expected)1567 self.assertEquals(request["server"]["metadata"], expected)
15621568
@@ -1569,7 +1575,7 @@
1569 <meta key="delta"/>1575 <meta key="delta"/>
1570 </metadata>1576 </metadata>
1571</server>"""1577</server>"""
1572 request = self.deserializer.deserialize(serial_request)1578 request = self.deserializer.deserialize(serial_request, 'create')
1573 expected = {"alpha": "", "delta": ""}1579 expected = {"alpha": "", "delta": ""}
1574 self.assertEquals(request["server"]["metadata"], expected)1580 self.assertEquals(request["server"]["metadata"], expected)
15751581
@@ -1581,7 +1587,7 @@
1581 <meta>beta</meta>1587 <meta>beta</meta>
1582 </metadata>1588 </metadata>
1583</server>"""1589</server>"""
1584 request = self.deserializer.deserialize(serial_request)1590 request = self.deserializer.deserialize(serial_request, 'create')
1585 expected = {"": "beta"}1591 expected = {"": "beta"}
1586 self.assertEquals(request["server"]["metadata"], expected)1592 self.assertEquals(request["server"]["metadata"], expected)
15871593
@@ -1594,7 +1600,7 @@
1594 <meta>gamma</meta>1600 <meta>gamma</meta>
1595 </metadata>1601 </metadata>
1596</server>"""1602</server>"""
1597 request = self.deserializer.deserialize(serial_request)1603 request = self.deserializer.deserialize(serial_request, 'create')
1598 expected = {"": "gamma"}1604 expected = {"": "gamma"}
1599 self.assertEquals(request["server"]["metadata"], expected)1605 self.assertEquals(request["server"]["metadata"], expected)
16001606
@@ -1607,7 +1613,7 @@
1607 <meta key="foo">baz</meta>1613 <meta key="foo">baz</meta>
1608 </metadata>1614 </metadata>
1609</server>"""1615</server>"""
1610 request = self.deserializer.deserialize(serial_request)1616 request = self.deserializer.deserialize(serial_request, 'create')
1611 expected = {"foo": "baz"}1617 expected = {"foo": "baz"}
1612 self.assertEquals(request["server"]["metadata"], expected)1618 self.assertEquals(request["server"]["metadata"], expected)
16131619
@@ -1654,7 +1660,7 @@
1654 },1660 },
1655 ],1661 ],
1656 }}1662 }}
1657 request = self.deserializer.deserialize(serial_request)1663 request = self.deserializer.deserialize(serial_request, 'create')
1658 self.assertEqual(request, expected)1664 self.assertEqual(request, expected)
16591665
1660 def test_request_xmlser_with_flavor_image_ref(self):1666 def test_request_xmlser_with_flavor_image_ref(self):
@@ -1664,7 +1670,7 @@
1664 imageRef="http://localhost:8774/v1.1/images/1"1670 imageRef="http://localhost:8774/v1.1/images/1"
1665 flavorRef="http://localhost:8774/v1.1/flavors/1">1671 flavorRef="http://localhost:8774/v1.1/flavors/1">
1666 </server>"""1672 </server>"""
1667 request = self.deserializer.deserialize(serial_request)1673 request = self.deserializer.deserialize(serial_request, 'create')
1668 self.assertEquals(request["server"]["flavorRef"],1674 self.assertEquals(request["server"]["flavorRef"],
1669 "http://localhost:8774/v1.1/flavors/1")1675 "http://localhost:8774/v1.1/flavors/1")
1670 self.assertEquals(request["server"]["imageRef"],1676 self.assertEquals(request["server"]["imageRef"],
16711677
=== added file 'nova/tests/api/openstack/test_wsgi.py'
--- nova/tests/api/openstack/test_wsgi.py 1970-01-01 00:00:00 +0000
+++ nova/tests/api/openstack/test_wsgi.py 2011-05-26 21:32:29 +0000
@@ -0,0 +1,293 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3import json
4import webob
5
6from nova import exception
7from nova import test
8from nova.api.openstack import wsgi
9
10
11class RequestTest(test.TestCase):
12 def test_content_type_missing(self):
13 request = wsgi.Request.blank('/tests/123')
14 request.body = "<body />"
15 self.assertRaises(exception.InvalidContentType,
16 request.get_content_type)
17
18 def test_content_type_unsupported(self):
19 request = wsgi.Request.blank('/tests/123')
20 request.headers["Content-Type"] = "text/html"
21 request.body = "asdf<br />"
22 self.assertRaises(exception.InvalidContentType,
23 request.get_content_type)
24
25 def test_content_type_with_charset(self):
26 request = wsgi.Request.blank('/tests/123')
27 request.headers["Content-Type"] = "application/json; charset=UTF-8"
28 result = request.get_content_type()
29 self.assertEqual(result, "application/json")
30
31 def test_content_type_from_accept_xml(self):
32 request = wsgi.Request.blank('/tests/123')
33 request.headers["Accept"] = "application/xml"
34 result = request.best_match_content_type()
35 self.assertEqual(result, "application/xml")
36
37 request = wsgi.Request.blank('/tests/123')
38 request.headers["Accept"] = "application/json"
39 result = request.best_match_content_type()
40 self.assertEqual(result, "application/json")
41
42 request = wsgi.Request.blank('/tests/123')
43 request.headers["Accept"] = "application/xml, application/json"
44 result = request.best_match_content_type()
45 self.assertEqual(result, "application/json")
46
47 request = wsgi.Request.blank('/tests/123')
48 request.headers["Accept"] = \
49 "application/json; q=0.3, application/xml; q=0.9"
50 result = request.best_match_content_type()
51 self.assertEqual(result, "application/xml")
52
53 def test_content_type_from_query_extension(self):
54 request = wsgi.Request.blank('/tests/123.xml')
55 result = request.best_match_content_type()
56 self.assertEqual(result, "application/xml")
57
58 request = wsgi.Request.blank('/tests/123.json')
59 result = request.best_match_content_type()
60 self.assertEqual(result, "application/json")
61
62 request = wsgi.Request.blank('/tests/123.invalid')
63 result = request.best_match_content_type()
64 self.assertEqual(result, "application/json")
65
66 def test_content_type_accept_and_query_extension(self):
67 request = wsgi.Request.blank('/tests/123.xml')
68 request.headers["Accept"] = "application/json"
69 result = request.best_match_content_type()
70 self.assertEqual(result, "application/xml")
71
72 def test_content_type_accept_default(self):
73 request = wsgi.Request.blank('/tests/123.unsupported')
74 request.headers["Accept"] = "application/unsupported1"
75 result = request.best_match_content_type()
76 self.assertEqual(result, "application/json")
77
78
79class DictSerializerTest(test.TestCase):
80 def test_dispatch(self):
81 serializer = wsgi.DictSerializer()
82 serializer.create = lambda x: 'pants'
83 serializer.default = lambda x: 'trousers'
84 self.assertEqual(serializer.serialize({}, 'create'), 'pants')
85
86 def test_dispatch_default(self):
87 serializer = wsgi.DictSerializer()
88 serializer.create = lambda x: 'pants'
89 serializer.default = lambda x: 'trousers'
90 self.assertEqual(serializer.serialize({}, 'update'), 'trousers')
91
92
93class XMLDictSerializerTest(test.TestCase):
94 def test_xml(self):
95 input_dict = dict(servers=dict(a=(2, 3)))
96 expected_xml = '<serversxmlns="asdf"><a>(2,3)</a></servers>'
97 serializer = wsgi.XMLDictSerializer(xmlns="asdf")
98 result = serializer.serialize(input_dict)
99 result = result.replace('\n', '').replace(' ', '')
100 self.assertEqual(result, expected_xml)
101
102
103class JSONDictSerializerTest(test.TestCase):
104 def test_json(self):
105 input_dict = dict(servers=dict(a=(2, 3)))
106 expected_json = '{"servers":{"a":[2,3]}}'
107 serializer = wsgi.JSONDictSerializer()
108 result = serializer.serialize(input_dict)
109 result = result.replace('\n', '').replace(' ', '')
110 self.assertEqual(result, expected_json)
111
112
113class TextDeserializerTest(test.TestCase):
114 def test_dispatch(self):
115 deserializer = wsgi.TextDeserializer()
116 deserializer.create = lambda x: 'pants'
117 deserializer.default = lambda x: 'trousers'
118 self.assertEqual(deserializer.deserialize({}, 'create'), 'pants')
119
120 def test_dispatch_default(self):
121 deserializer = wsgi.TextDeserializer()
122 deserializer.create = lambda x: 'pants'
123 deserializer.default = lambda x: 'trousers'
124 self.assertEqual(deserializer.deserialize({}, 'update'), 'trousers')
125
126
127class JSONDeserializerTest(test.TestCase):
128 def test_json(self):
129 data = """{"a": {
130 "a1": "1",
131 "a2": "2",
132 "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
133 "d": {"e": "1"},
134 "f": "1"}}"""
135 as_dict = dict(a={
136 'a1': '1',
137 'a2': '2',
138 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
139 'd': {'e': '1'},
140 'f': '1'})
141 deserializer = wsgi.JSONDeserializer()
142 self.assertEqual(deserializer.deserialize(data), as_dict)
143
144
145class XMLDeserializerTest(test.TestCase):
146 def test_xml(self):
147 xml = """
148 <a a1="1" a2="2">
149 <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs>
150 <d><e>1</e></d>
151 <f>1</f>
152 </a>
153 """.strip()
154 as_dict = dict(a={
155 'a1': '1',
156 'a2': '2',
157 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
158 'd': {'e': '1'},
159 'f': '1'})
160 metadata = {'plurals': {'bs': 'b', 'ts': 't'}}
161 deserializer = wsgi.XMLDeserializer(metadata=metadata)
162 self.assertEqual(deserializer.deserialize(xml), as_dict)
163
164 def test_xml_empty(self):
165 xml = """<a></a>"""
166 as_dict = {"a": {}}
167 deserializer = wsgi.XMLDeserializer()
168 self.assertEqual(deserializer.deserialize(xml), as_dict)
169
170
171class ResponseSerializerTest(test.TestCase):
172 def setUp(self):
173 class JSONSerializer(object):
174 def serialize(self, data):
175 return 'pew_json'
176
177 class XMLSerializer(object):
178 def serialize(self, data):
179 return 'pew_xml'
180
181 self.serializers = {
182 'application/json': JSONSerializer(),
183 'application/XML': XMLSerializer(),
184 }
185
186 self.serializer = wsgi.ResponseSerializer(serializers=self.serializers)
187
188 def tearDown(self):
189 pass
190
191 def test_get_serializer(self):
192 self.assertEqual(self.serializer.get_serializer('application/json'),
193 self.serializers['application/json'])
194
195 def test_get_serializer_unknown_content_type(self):
196 self.assertRaises(exception.InvalidContentType,
197 self.serializer.get_serializer,
198 'application/unknown')
199
200 def test_serialize_response(self):
201 response = self.serializer.serialize({}, 'application/json')
202 self.assertEqual(response.headers['Content-Type'], 'application/json')
203 self.assertEqual(response.body, 'pew_json')
204
205 def test_serialize_response_dict_to_unknown_content_type(self):
206 self.assertRaises(exception.InvalidContentType,
207 self.serializer.serialize,
208 {}, 'application/unknown')
209
210
211class RequestDeserializerTest(test.TestCase):
212 def setUp(self):
213 class JSONDeserializer(object):
214 def deserialize(self, data):
215 return 'pew_json'
216
217 class XMLDeserializer(object):
218 def deserialize(self, data):
219 return 'pew_xml'
220
221 self.deserializers = {
222 'application/json': JSONDeserializer(),
223 'application/XML': XMLDeserializer(),
224 }
225
226 self.deserializer = wsgi.RequestDeserializer(
227 deserializers=self.deserializers)
228
229 def tearDown(self):
230 pass
231
232 def test_get_deserializer(self):
233 expected = self.deserializer.get_deserializer('application/json')
234 self.assertEqual(expected, self.deserializers['application/json'])
235
236 def test_get_deserializer_unknown_content_type(self):
237 self.assertRaises(exception.InvalidContentType,
238 self.deserializer.get_deserializer,
239 'application/unknown')
240
241 def test_get_expected_content_type(self):
242 request = wsgi.Request.blank('/')
243 request.headers['Accept'] = 'application/json'
244 self.assertEqual(self.deserializer.get_expected_content_type(request),
245 'application/json')
246
247 def test_get_action_args(self):
248 env = {
249 'wsgiorg.routing_args': [None, {
250 'controller': None,
251 'format': None,
252 'action': 'update',
253 'id': 12,
254 }],
255 }
256
257 expected = {'action': 'update', 'id': 12}
258
259 self.assertEqual(self.deserializer.get_action_args(env), expected)
260
261 def test_deserialize(self):
262 def fake_get_routing_args(request):
263 return {'action': 'create'}
264 self.deserializer.get_action_args = fake_get_routing_args
265
266 request = wsgi.Request.blank('/')
267 request.headers['Accept'] = 'application/xml'
268
269 deserialized = self.deserializer.deserialize(request)
270 expected = ('create', {}, 'application/xml')
271
272 self.assertEqual(expected, deserialized)
273
274
275class ResourceTest(test.TestCase):
276 def test_dispatch(self):
277 class Controller(object):
278 def index(self, req, pants=None):
279 return pants
280
281 resource = wsgi.Resource(Controller())
282 actual = resource.dispatch(None, 'index', {'pants': 'off'})
283 expected = 'off'
284 self.assertEqual(actual, expected)
285
286 def test_dispatch_unknown_controller_action(self):
287 class Controller(object):
288 def index(self, req, pants=None):
289 return pants
290
291 resource = wsgi.Resource(Controller())
292 self.assertRaises(AttributeError, resource.dispatch,
293 None, 'create', {})
0294
=== modified file 'nova/tests/api/test_wsgi.py'
--- nova/tests/api/test_wsgi.py 2011-04-20 18:01:14 +0000
+++ nova/tests/api/test_wsgi.py 2011-05-26 21:32:29 +0000
@@ -67,192 +67,3 @@
67 self.assertEqual(result.body, "Router result")67 self.assertEqual(result.body, "Router result")
68 result = webob.Request.blank('/bad').get_response(Router())68 result = webob.Request.blank('/bad').get_response(Router())
69 self.assertNotEqual(result.body, "Router result")69 self.assertNotEqual(result.body, "Router result")
70
71
72class ControllerTest(test.TestCase):
73
74 class TestRouter(wsgi.Router):
75
76 class TestController(wsgi.Controller):
77
78 _serialization_metadata = {
79 'application/xml': {
80 "attributes": {
81 "test": ["id"]}}}
82
83 def show(self, req, id): # pylint: disable=W0622,C0103
84 return {"test": {"id": id}}
85
86 def __init__(self):
87 mapper = routes.Mapper()
88 mapper.resource("test", "tests", controller=self.TestController())
89 wsgi.Router.__init__(self, mapper)
90
91 def test_show(self):
92 request = wsgi.Request.blank('/tests/123')
93 result = request.get_response(self.TestRouter())
94 self.assertEqual(json.loads(result.body), {"test": {"id": "123"}})
95
96 def test_response_content_type_from_accept_xml(self):
97 request = webob.Request.blank('/tests/123')
98 request.headers["Accept"] = "application/xml"
99 result = request.get_response(self.TestRouter())
100 self.assertEqual(result.headers["Content-Type"], "application/xml")
101
102 def test_response_content_type_from_accept_json(self):
103 request = wsgi.Request.blank('/tests/123')
104 request.headers["Accept"] = "application/json"
105 result = request.get_response(self.TestRouter())
106 self.assertEqual(result.headers["Content-Type"], "application/json")
107
108 def test_response_content_type_from_query_extension_xml(self):
109 request = wsgi.Request.blank('/tests/123.xml')
110 result = request.get_response(self.TestRouter())
111 self.assertEqual(result.headers["Content-Type"], "application/xml")
112
113 def test_response_content_type_from_query_extension_json(self):
114 request = wsgi.Request.blank('/tests/123.json')
115 result = request.get_response(self.TestRouter())
116 self.assertEqual(result.headers["Content-Type"], "application/json")
117
118 def test_response_content_type_default_when_unsupported(self):
119 request = wsgi.Request.blank('/tests/123.unsupported')
120 request.headers["Accept"] = "application/unsupported1"
121 result = request.get_response(self.TestRouter())
122 self.assertEqual(result.status_int, 200)
123 self.assertEqual(result.headers["Content-Type"], "application/json")
124
125
126class RequestTest(test.TestCase):
127
128 def test_request_content_type_missing(self):
129 request = wsgi.Request.blank('/tests/123')
130 request.body = "<body />"
131 self.assertRaises(webob.exc.HTTPBadRequest, request.get_content_type)
132
133 def test_request_content_type_unsupported(self):
134 request = wsgi.Request.blank('/tests/123')
135 request.headers["Content-Type"] = "text/html"
136 request.body = "asdf<br />"
137 self.assertRaises(webob.exc.HTTPBadRequest, request.get_content_type)
138
139 def test_request_content_type_with_charset(self):
140 request = wsgi.Request.blank('/tests/123')
141 request.headers["Content-Type"] = "application/json; charset=UTF-8"
142 result = request.get_content_type()
143 self.assertEqual(result, "application/json")
144
145 def test_content_type_from_accept_xml(self):
146 request = wsgi.Request.blank('/tests/123')
147 request.headers["Accept"] = "application/xml"
148 result = request.best_match_content_type()
149 self.assertEqual(result, "application/xml")
150
151 request = wsgi.Request.blank('/tests/123')
152 request.headers["Accept"] = "application/json"
153 result = request.best_match_content_type()
154 self.assertEqual(result, "application/json")
155
156 request = wsgi.Request.blank('/tests/123')
157 request.headers["Accept"] = "application/xml, application/json"
158 result = request.best_match_content_type()
159 self.assertEqual(result, "application/json")
160
161 request = wsgi.Request.blank('/tests/123')
162 request.headers["Accept"] = \
163 "application/json; q=0.3, application/xml; q=0.9"
164 result = request.best_match_content_type()
165 self.assertEqual(result, "application/xml")
166
167 def test_content_type_from_query_extension(self):
168 request = wsgi.Request.blank('/tests/123.xml')
169 result = request.best_match_content_type()
170 self.assertEqual(result, "application/xml")
171
172 request = wsgi.Request.blank('/tests/123.json')
173 result = request.best_match_content_type()
174 self.assertEqual(result, "application/json")
175
176 request = wsgi.Request.blank('/tests/123.invalid')
177 result = request.best_match_content_type()
178 self.assertEqual(result, "application/json")
179
180 def test_content_type_accept_and_query_extension(self):
181 request = wsgi.Request.blank('/tests/123.xml')
182 request.headers["Accept"] = "application/json"
183 result = request.best_match_content_type()
184 self.assertEqual(result, "application/xml")
185
186 def test_content_type_accept_default(self):
187 request = wsgi.Request.blank('/tests/123.unsupported')
188 request.headers["Accept"] = "application/unsupported1"
189 result = request.best_match_content_type()
190 self.assertEqual(result, "application/json")
191
192
193class SerializerTest(test.TestCase):
194
195 def test_xml(self):
196 input_dict = dict(servers=dict(a=(2, 3)))
197 expected_xml = '<servers><a>(2,3)</a></servers>'
198 serializer = wsgi.Serializer()
199 result = serializer.serialize(input_dict, "application/xml")
200 result = result.replace('\n', '').replace(' ', '')
201 self.assertEqual(result, expected_xml)
202
203 def test_json(self):
204 input_dict = dict(servers=dict(a=(2, 3)))
205 expected_json = '{"servers":{"a":[2,3]}}'
206 serializer = wsgi.Serializer()
207 result = serializer.serialize(input_dict, "application/json")
208 result = result.replace('\n', '').replace(' ', '')
209 self.assertEqual(result, expected_json)
210
211 def test_unsupported_content_type(self):
212 serializer = wsgi.Serializer()
213 self.assertRaises(exception.InvalidContentType, serializer.serialize,
214 {}, "text/null")
215
216 def test_deserialize_json(self):
217 data = """{"a": {
218 "a1": "1",
219 "a2": "2",
220 "bs": ["1", "2", "3", {"c": {"c1": "1"}}],
221 "d": {"e": "1"},
222 "f": "1"}}"""
223 as_dict = dict(a={
224 'a1': '1',
225 'a2': '2',
226 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
227 'd': {'e': '1'},
228 'f': '1'})
229 metadata = {}
230 serializer = wsgi.Serializer(metadata)
231 self.assertEqual(serializer.deserialize(data, "application/json"),
232 as_dict)
233
234 def test_deserialize_xml(self):
235 xml = """
236 <a a1="1" a2="2">
237 <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs>
238 <d><e>1</e></d>
239 <f>1</f>
240 </a>
241 """.strip()
242 as_dict = dict(a={
243 'a1': '1',
244 'a2': '2',
245 'bs': ['1', '2', '3', {'c': dict(c1='1')}],
246 'd': {'e': '1'},
247 'f': '1'})
248 metadata = {'application/xml': dict(plurals={'bs': 'b', 'ts': 't'})}
249 serializer = wsgi.Serializer(metadata)
250 self.assertEqual(serializer.deserialize(xml, "application/xml"),
251 as_dict)
252
253 def test_deserialize_empty_xml(self):
254 xml = """<a></a>"""
255 as_dict = {"a": {}}
256 serializer = wsgi.Serializer()
257 self.assertEqual(serializer.deserialize(xml, "application/xml"),
258 as_dict)
25970
=== modified file 'nova/tests/integrated/test_xml.py'
--- nova/tests/integrated/test_xml.py 2011-03-30 17:05:06 +0000
+++ nova/tests/integrated/test_xml.py 2011-05-26 21:32:29 +0000
@@ -32,7 +32,7 @@
32 """"Some basic XML sanity checks."""32 """"Some basic XML sanity checks."""
3333
34 def test_namespace_limits(self):34 def test_namespace_limits(self):
35 """/limits should have v1.0 namespace (hasn't changed in 1.1)."""35 """/limits should have v1.1 namespace (has changed in 1.1)."""
36 headers = {}36 headers = {}
37 headers['Accept'] = 'application/xml'37 headers['Accept'] = 'application/xml'
3838
@@ -40,7 +40,7 @@
40 data = response.read()40 data = response.read()
41 LOG.debug("data: %s" % data)41 LOG.debug("data: %s" % data)
4242
43 prefix = '<limits xmlns="%s"' % common.XML_NS_V1043 prefix = '<limits xmlns="%s"' % common.XML_NS_V11
44 self.assertTrue(data.startswith(prefix))44 self.assertTrue(data.startswith(prefix))
4545
46 def test_namespace_servers(self):46 def test_namespace_servers(self):
4747
=== modified file 'nova/wsgi.py'
--- nova/wsgi.py 2011-05-20 04:03:15 +0000
+++ nova/wsgi.py 2011-05-26 21:32:29 +0000
@@ -85,36 +85,7 @@
8585
8686
87class Request(webob.Request):87class Request(webob.Request):
8888 pass
89 def best_match_content_type(self):
90 """Determine the most acceptable content-type.
91
92 Based on the query extension then the Accept header.
93
94 """
95 parts = self.path.rsplit('.', 1)
96
97 if len(parts) > 1:
98 format = parts[1]
99 if format in ['json', 'xml']:
100 return 'application/{0}'.format(parts[1])
101
102 ctypes = ['application/json', 'application/xml']
103 bm = self.accept.best_match(ctypes)
104
105 return bm or 'application/json'
106
107 def get_content_type(self):
108 allowed_types = ("application/xml", "application/json")
109 if not "Content-Type" in self.headers:
110 msg = _("Missing Content-Type")
111 LOG.debug(msg)
112 raise webob.exc.HTTPBadRequest(msg)
113 type = self.content_type
114 if type in allowed_types:
115 return type
116 LOG.debug(_("Wrong Content-Type: %s") % type)
117 raise webob.exc.HTTPBadRequest("Invalid content type")
11889
11990
120class Application(object):91class Application(object):
@@ -289,8 +260,8 @@
289260
290 Each route in `mapper` must specify a 'controller', which is a261 Each route in `mapper` must specify a 'controller', which is a
291 WSGI app to call. You'll probably want to specify an 'action' as262 WSGI app to call. You'll probably want to specify an 'action' as
292 well and have your controller be a wsgi.Controller, who will route263 well and have your controller be an object that can route
293 the request to the action method.264 the request to the action-specific method.
294265
295 Examples:266 Examples:
296 mapper = routes.Mapper()267 mapper = routes.Mapper()
@@ -338,223 +309,6 @@
338 return app309 return app
339310
340311
341class Controller(object):
342 """WSGI app that dispatched to methods.
343
344 WSGI app that reads routing information supplied by RoutesMiddleware
345 and calls the requested action method upon itself. All action methods
346 must, in addition to their normal parameters, accept a 'req' argument
347 which is the incoming wsgi.Request. They raise a webob.exc exception,
348 or return a dict which will be serialized by requested content type.
349
350 """
351
352 @webob.dec.wsgify(RequestClass=Request)
353 def __call__(self, req):
354 """Call the method specified in req.environ by RoutesMiddleware."""
355 arg_dict = req.environ['wsgiorg.routing_args'][1]
356 action = arg_dict['action']
357 method = getattr(self, action)
358 LOG.debug("%s %s" % (req.method, req.url))
359 del arg_dict['controller']
360 del arg_dict['action']
361 if 'format' in arg_dict:
362 del arg_dict['format']
363 arg_dict['req'] = req
364 result = method(**arg_dict)
365
366 if type(result) is dict:
367 content_type = req.best_match_content_type()
368 default_xmlns = self.get_default_xmlns(req)
369 body = self._serialize(result, content_type, default_xmlns)
370
371 response = webob.Response()
372 response.headers['Content-Type'] = content_type
373 response.body = body
374 msg_dict = dict(url=req.url, status=response.status_int)
375 msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
376 LOG.debug(msg)
377 return response
378 else:
379 return result
380
381 def _serialize(self, data, content_type, default_xmlns):
382 """Serialize the given dict to the provided content_type.
383
384 Uses self._serialization_metadata if it exists, which is a dict mapping
385 MIME types to information needed to serialize to that type.
386
387 """
388 _metadata = getattr(type(self), '_serialization_metadata', {})
389
390 serializer = Serializer(_metadata, default_xmlns)
391 try:
392 return serializer.serialize(data, content_type)
393 except exception.InvalidContentType:
394 raise webob.exc.HTTPNotAcceptable()
395
396 def _deserialize(self, data, content_type):
397 """Deserialize the request body to the specefied content type.
398
399 Uses self._serialization_metadata if it exists, which is a dict mapping
400 MIME types to information needed to serialize to that type.
401
402 """
403 _metadata = getattr(type(self), '_serialization_metadata', {})
404 serializer = Serializer(_metadata)
405 return serializer.deserialize(data, content_type)
406
407 def get_default_xmlns(self, req):
408 """Provide the XML namespace to use if none is otherwise specified."""
409 return None
410
411
412class Serializer(object):
413 """Serializes and deserializes dictionaries to certain MIME types."""
414
415 def __init__(self, metadata=None, default_xmlns=None):
416 """Create a serializer based on the given WSGI environment.
417
418 'metadata' is an optional dict mapping MIME types to information
419 needed to serialize a dictionary to that type.
420
421 """
422 self.metadata = metadata or {}
423 self.default_xmlns = default_xmlns
424
425 def _get_serialize_handler(self, content_type):
426 handlers = {
427 'application/json': self._to_json,
428 'application/xml': self._to_xml,
429 }
430
431 try:
432 return handlers[content_type]
433 except Exception:
434 raise exception.InvalidContentType(content_type=content_type)
435
436 def serialize(self, data, content_type):
437 """Serialize a dictionary into the specified content type."""
438 return self._get_serialize_handler(content_type)(data)
439
440 def deserialize(self, datastring, content_type):
441 """Deserialize a string to a dictionary.
442
443 The string must be in the format of a supported MIME type.
444
445 """
446 return self.get_deserialize_handler(content_type)(datastring)
447
448 def get_deserialize_handler(self, content_type):
449 handlers = {
450 'application/json': self._from_json,
451 'application/xml': self._from_xml,
452 }
453
454 try:
455 return handlers[content_type]
456 except Exception:
457 raise exception.InvalidContentType(content_type=content_type)
458
459 def _from_json(self, datastring):
460 return utils.loads(datastring)
461
462 def _from_xml(self, datastring):
463 xmldata = self.metadata.get('application/xml', {})
464 plurals = set(xmldata.get('plurals', {}))
465 node = minidom.parseString(datastring).childNodes[0]
466 return {node.nodeName: self._from_xml_node(node, plurals)}
467
468 def _from_xml_node(self, node, listnames):
469 """Convert a minidom node to a simple Python type.
470
471 listnames is a collection of names of XML nodes whose subnodes should
472 be considered list items.
473
474 """
475 if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
476 return node.childNodes[0].nodeValue
477 elif node.nodeName in listnames:
478 return [self._from_xml_node(n, listnames) for n in node.childNodes]
479 else:
480 result = dict()
481 for attr in node.attributes.keys():
482 result[attr] = node.attributes[attr].nodeValue
483 for child in node.childNodes:
484 if child.nodeType != node.TEXT_NODE:
485 result[child.nodeName] = self._from_xml_node(child,
486 listnames)
487 return result
488
489 def _to_json(self, data):
490 return utils.dumps(data)
491
492 def _to_xml(self, data):
493 metadata = self.metadata.get('application/xml', {})
494 # We expect data to contain a single key which is the XML root.
495 root_key = data.keys()[0]
496 doc = minidom.Document()
497 node = self._to_xml_node(doc, metadata, root_key, data[root_key])
498
499 xmlns = node.getAttribute('xmlns')
500 if not xmlns and self.default_xmlns:
501 node.setAttribute('xmlns', self.default_xmlns)
502
503 return node.toprettyxml(indent=' ')
504
505 def _to_xml_node(self, doc, metadata, nodename, data):
506 """Recursive method to convert data members to XML nodes."""
507 result = doc.createElement(nodename)
508
509 # Set the xml namespace if one is specified
510 # TODO(justinsb): We could also use prefixes on the keys
511 xmlns = metadata.get('xmlns', None)
512 if xmlns:
513 result.setAttribute('xmlns', xmlns)
514
515 if type(data) is list:
516 collections = metadata.get('list_collections', {})
517 if nodename in collections:
518 metadata = collections[nodename]
519 for item in data:
520 node = doc.createElement(metadata['item_name'])
521 node.setAttribute(metadata['item_key'], str(item))
522 result.appendChild(node)
523 return result
524 singular = metadata.get('plurals', {}).get(nodename, None)
525 if singular is None:
526 if nodename.endswith('s'):
527 singular = nodename[:-1]
528 else:
529 singular = 'item'
530 for item in data:
531 node = self._to_xml_node(doc, metadata, singular, item)
532 result.appendChild(node)
533 elif type(data) is dict:
534 collections = metadata.get('dict_collections', {})
535 if nodename in collections:
536 metadata = collections[nodename]
537 for k, v in data.items():
538 node = doc.createElement(metadata['item_name'])
539 node.setAttribute(metadata['item_key'], str(k))
540 text = doc.createTextNode(str(v))
541 node.appendChild(text)
542 result.appendChild(node)
543 return result
544 attrs = metadata.get('attributes', {}).get(nodename, {})
545 for k, v in data.items():
546 if k in attrs:
547 result.setAttribute(k, str(v))
548 else:
549 node = self._to_xml_node(doc, metadata, k, v)
550 result.appendChild(node)
551 else:
552 # Type is atom
553 node = doc.createTextNode(str(data))
554 result.appendChild(node)
555 return result
556
557
558def paste_config_file(basename):312def paste_config_file(basename):
559 """Find the best location in the system for a paste config file.313 """Find the best location in the system for a paste config file.
560314