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