Merge lp:~rackspace-titan/nova/openstack-api-version-split into lp:~hudson-openstack/nova/trunk
- openstack-api-version-split
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Paul Voccio |
Approved revision: | 805 |
Merged at revision: | 821 |
Proposed branch: | lp:~rackspace-titan/nova/openstack-api-version-split |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
632 lines (+371/-77) 12 files modified
etc/api-paste.ini (+1/-0) nova/api/openstack/__init__.py (+5/-2) nova/api/openstack/auth.py (+2/-0) nova/api/openstack/common.py (+4/-0) nova/api/openstack/servers.py (+25/-63) nova/api/openstack/views/addresses.py (+54/-0) nova/api/openstack/views/flavors.py (+51/-0) nova/api/openstack/views/images.py (+51/-0) nova/api/openstack/views/servers.py (+132/-0) nova/tests/api/openstack/fakes.py (+1/-0) nova/tests/api/openstack/test_servers.py (+44/-11) plugins/xenserver/xenapi/etc/xapi.d/plugins/glance (+1/-1) |
To merge this branch: | bzr merge lp:~rackspace-titan/nova/openstack-api-version-split |
Related bugs: | |
Related blueprints: |
OpenStack API 1.1
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Voccio (community) | Approve | ||
Brian Lamar (community) | Approve | ||
Eric Day (community) | Approve | ||
Review via email: mp+53510@code.launchpad.net |
Commit message
- general approach for openstack api versioning
- openstack api version now preserved in request context
- added view builder classes to handle os api responses
- added imageRef and flavorRef to os api v1.1 servers
- modified addresses container structure in os api v1.1 servers
Description of the change
- general approach for openstack api versioning
- openstack api version now preserved in request context
- added view builder classes to handle os api responses
- added imageRef and flavorRef to os api v1.1 servers
- modified addresses container structure in os api v1.1 servers
Naveed Massjouni (ironcamel) wrote : | # |
Eric Day (eday) wrote : | # |
40: nova.context should not hold anything openstack API specific. You probably want to store version in the req.environ dict if you need it somewhere.
592: how did that get in there? :)
Overall, I'm not sure I agree with the approach. It versions the serialization, but not the controllers. I'm wondering if we should instead use the old serialization conventions and just make versioned controllers that would call the correct one. You would then specify versioned controllers in the paste config which seems a bit more straightforward.
- 798. By Naveed Massjouni
-
Per Eric Day's suggest, the verson is not store in the request environ instead
of the nova.context. - 799. By Naveed Massjouni
-
Updating paste config.
Naveed Massjouni (ironcamel) wrote : | # |
> 40: nova.context should not hold anything openstack API specific. You probably
> want to store version in the req.environ dict if you need it somewhere.
Done. Thanks.
>
> 592: how did that get in there? :)
Modified that line to make pep8 happy, after merge from trunk.
>
> Overall, I'm not sure I agree with the approach. It versions the
> serialization, but not the controllers. I'm wondering if we should instead use
> the old serialization conventions and just make versioned controllers that
> would call the correct one. You would then specify versioned controllers in
> the paste config which seems a bit more straightforward.
I agree that controllers should also be versioned. The good thing about this merge prop as it stands is that it separates out the view logic from the controller logic. We considered versioning controllers as well. We decided to hold off on that until it became necessary for a feature we were working on. We will be working in that direction, as we are implementing rest of 1.1 features for the openstack api in the next couple of days. We wanted to make our approach public as soon as possible to get feedback.
Eric Day (eday) wrote : | # |
39: might want something more specific than 'version' for the var, as I could see this conflicting with other things. Something like nova.api.
256,296: Shouldn't use python3 formatting, we're still supporting at least 2.5 afaik and this isn't consistent with the rest of the code base
Need license headers on all the new files.
As I mentioned on IRC, I think the get_view_builder functions should go away once the controllers are versioned since you can call the version directly. This won't block once these other issues are fixed though.
Naveed Massjouni (ironcamel) wrote : | # |
Thanks Eric. I just incorporated your suggestions. I changed 'version' key of the request environ to the more descriptive 'api.version', as we discussed on irc.
- 800. By Naveed Massjouni
-
As suggested by Eric Day:
* changed request.environ version key to more descriptive 'api.version'
* removed python3 string formatting
* added licenses to headers on new files - 801. By Brian Waldon
-
adding imageRef and flavorRef attributes to servers serialization metadata
- 802. By Naveed Massjouni
-
Adding newlines for pep8.
- 803. By Naveed Massjouni
-
req envirom param 'nova.api.
openstack. version' should be 'api.version'
Brian Lamar (blamar) wrote : | # |
Line 150: except exception.NotFound:
Potentially nitpicky, which one of the three statements in the try/except can raise the NotFound exception? Can we just put the one line which can raise the exception in there?
Line 518: req.environ[
Should this be 'openstack.
Brian Waldon (bcwaldon) wrote : | # |
> Line 150: except exception.NotFound:
>
> Potentially nitpicky, which one of the three statements in the try/except can
> raise the NotFound exception? Can we just put the one line which can raise the
> exception in there?
>
Fixed.
>
> Line 518: req.environ[
>
> Should this be 'openstack.
> OpenStack?
In our chat, we didn't have a good enough reason to make these versions api-specific. Leaving it alone for now.
Eric Day (eday) wrote : | # |
Looks good! I'll be looking forward to see how view classes change with versioned controllers. :)
Naveed Massjouni (ironcamel) wrote : | # |
Thanks Eric! And as you requested, here is the merge prop for versioned controllers :)
https:/
I branched that off of this branch, so this branch needs to go in first.
Brian Lamar (blamar) : | # |
- 804. By Naveed Massjouni
-
Merge from trunk.
- 805. By Naveed Massjouni
-
moving code out of try/except that would never trigger NotFound
Preview Diff
1 | === modified file 'etc/api-paste.ini' | |||
2 | --- etc/api-paste.ini 2011-03-07 19:33:24 +0000 | |||
3 | +++ etc/api-paste.ini 2011-03-17 17:53:41 +0000 | |||
4 | @@ -68,6 +68,7 @@ | |||
5 | 68 | use = egg:Paste#urlmap | 68 | use = egg:Paste#urlmap |
6 | 69 | /: osversions | 69 | /: osversions |
7 | 70 | /v1.0: openstackapi | 70 | /v1.0: openstackapi |
8 | 71 | /v1.1: openstackapi | ||
9 | 71 | 72 | ||
10 | 72 | [pipeline:openstackapi] | 73 | [pipeline:openstackapi] |
11 | 73 | pipeline = faultwrap auth ratelimit osapiapp | 74 | pipeline = faultwrap auth ratelimit osapiapp |
12 | 74 | 75 | ||
13 | === modified file 'nova/api/openstack/__init__.py' | |||
14 | --- nova/api/openstack/__init__.py 2011-03-11 19:49:32 +0000 | |||
15 | +++ nova/api/openstack/__init__.py 2011-03-17 17:53:41 +0000 | |||
16 | @@ -128,8 +128,11 @@ | |||
17 | 128 | def __call__(self, req): | 128 | def __call__(self, req): |
18 | 129 | """Respond to a request for all OpenStack API versions.""" | 129 | """Respond to a request for all OpenStack API versions.""" |
19 | 130 | response = { | 130 | response = { |
22 | 131 | "versions": [ | 131 | "versions": [ |
23 | 132 | dict(status="CURRENT", id="v1.0")]} | 132 | dict(status="DEPRECATED", id="v1.0"), |
24 | 133 | dict(status="CURRENT", id="v1.1"), | ||
25 | 134 | ], | ||
26 | 135 | } | ||
27 | 133 | metadata = { | 136 | metadata = { |
28 | 134 | "application/xml": { | 137 | "application/xml": { |
29 | 135 | "attributes": dict(version=["status", "id"])}} | 138 | "attributes": dict(version=["status", "id"])}} |
30 | 136 | 139 | ||
31 | === modified file 'nova/api/openstack/auth.py' | |||
32 | --- nova/api/openstack/auth.py 2011-03-11 22:19:14 +0000 | |||
33 | +++ nova/api/openstack/auth.py 2011-03-17 17:53:41 +0000 | |||
34 | @@ -69,6 +69,8 @@ | |||
35 | 69 | return faults.Fault(webob.exc.HTTPUnauthorized()) | 69 | return faults.Fault(webob.exc.HTTPUnauthorized()) |
36 | 70 | 70 | ||
37 | 71 | req.environ['nova.context'] = context.RequestContext(user, account) | 71 | req.environ['nova.context'] = context.RequestContext(user, account) |
38 | 72 | version = req.path.split('/')[1].replace('v', '') | ||
39 | 73 | req.environ['api.version'] = version | ||
40 | 72 | return self.application | 74 | return self.application |
41 | 73 | 75 | ||
42 | 74 | def has_authentication(self, req): | 76 | def has_authentication(self, req): |
43 | 75 | 77 | ||
44 | === modified file 'nova/api/openstack/common.py' | |||
45 | --- nova/api/openstack/common.py 2011-03-09 22:10:24 +0000 | |||
46 | +++ nova/api/openstack/common.py 2011-03-17 17:53:41 +0000 | |||
47 | @@ -74,3 +74,7 @@ | |||
48 | 74 | if abs(hash(image_id)) == int(image_hash): | 74 | if abs(hash(image_id)) == int(image_hash): |
49 | 75 | return image_id | 75 | return image_id |
50 | 76 | raise exception.NotFound(image_hash) | 76 | raise exception.NotFound(image_hash) |
51 | 77 | |||
52 | 78 | |||
53 | 79 | def get_api_version(req): | ||
54 | 80 | return req.environ.get('api.version') | ||
55 | 77 | 81 | ||
56 | === modified file 'nova/api/openstack/servers.py' | |||
57 | --- nova/api/openstack/servers.py 2011-03-16 19:16:16 +0000 | |||
58 | +++ nova/api/openstack/servers.py 2011-03-17 17:53:41 +0000 | |||
59 | @@ -29,6 +29,8 @@ | |||
60 | 29 | from nova import utils | 29 | from nova import utils |
61 | 30 | from nova.api.openstack import common | 30 | from nova.api.openstack import common |
62 | 31 | from nova.api.openstack import faults | 31 | from nova.api.openstack import faults |
63 | 32 | from nova.api.openstack.views import servers as servers_views | ||
64 | 33 | from nova.api.openstack.views import addresses as addresses_views | ||
65 | 32 | from nova.auth import manager as auth_manager | 34 | from nova.auth import manager as auth_manager |
66 | 33 | from nova.compute import instance_types | 35 | from nova.compute import instance_types |
67 | 34 | from nova.compute import power_state | 36 | from nova.compute import power_state |
68 | @@ -36,63 +38,9 @@ | |||
69 | 36 | 38 | ||
70 | 37 | 39 | ||
71 | 38 | LOG = logging.getLogger('server') | 40 | LOG = logging.getLogger('server') |
72 | 39 | |||
73 | 40 | |||
74 | 41 | FLAGS = flags.FLAGS | 41 | FLAGS = flags.FLAGS |
75 | 42 | 42 | ||
76 | 43 | 43 | ||
77 | 44 | def _translate_detail_keys(inst): | ||
78 | 45 | """ Coerces into dictionary format, mapping everything to Rackspace-like | ||
79 | 46 | attributes for return""" | ||
80 | 47 | power_mapping = { | ||
81 | 48 | None: 'build', | ||
82 | 49 | power_state.NOSTATE: 'build', | ||
83 | 50 | power_state.RUNNING: 'active', | ||
84 | 51 | power_state.BLOCKED: 'active', | ||
85 | 52 | power_state.SUSPENDED: 'suspended', | ||
86 | 53 | power_state.PAUSED: 'paused', | ||
87 | 54 | power_state.SHUTDOWN: 'active', | ||
88 | 55 | power_state.SHUTOFF: 'active', | ||
89 | 56 | power_state.CRASHED: 'error', | ||
90 | 57 | power_state.FAILED: 'error'} | ||
91 | 58 | inst_dict = {} | ||
92 | 59 | |||
93 | 60 | mapped_keys = dict(status='state', imageId='image_id', | ||
94 | 61 | flavorId='instance_type', name='display_name', id='id') | ||
95 | 62 | |||
96 | 63 | for k, v in mapped_keys.iteritems(): | ||
97 | 64 | inst_dict[k] = inst[v] | ||
98 | 65 | |||
99 | 66 | inst_dict['status'] = power_mapping[inst_dict['status']] | ||
100 | 67 | inst_dict['addresses'] = dict(public=[], private=[]) | ||
101 | 68 | |||
102 | 69 | # grab single private fixed ip | ||
103 | 70 | private_ips = utils.get_from_path(inst, 'fixed_ip/address') | ||
104 | 71 | inst_dict['addresses']['private'] = private_ips | ||
105 | 72 | |||
106 | 73 | # grab all public floating ips | ||
107 | 74 | public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') | ||
108 | 75 | inst_dict['addresses']['public'] = public_ips | ||
109 | 76 | |||
110 | 77 | # Return the metadata as a dictionary | ||
111 | 78 | metadata = {} | ||
112 | 79 | for item in inst['metadata']: | ||
113 | 80 | metadata[item['key']] = item['value'] | ||
114 | 81 | inst_dict['metadata'] = metadata | ||
115 | 82 | |||
116 | 83 | inst_dict['hostId'] = '' | ||
117 | 84 | if inst['host']: | ||
118 | 85 | inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() | ||
119 | 86 | |||
120 | 87 | return dict(server=inst_dict) | ||
121 | 88 | |||
122 | 89 | |||
123 | 90 | def _translate_keys(inst): | ||
124 | 91 | """ Coerces into dictionary format, excluding all model attributes | ||
125 | 92 | save for id and name """ | ||
126 | 93 | return dict(server=dict(id=inst['id'], name=inst['display_name'])) | ||
127 | 94 | |||
128 | 95 | |||
129 | 96 | class Controller(wsgi.Controller): | 44 | class Controller(wsgi.Controller): |
130 | 97 | """ The Server API controller for the OpenStack API """ | 45 | """ The Server API controller for the OpenStack API """ |
131 | 98 | 46 | ||
132 | @@ -100,36 +48,49 @@ | |||
133 | 100 | 'application/xml': { | 48 | 'application/xml': { |
134 | 101 | "attributes": { | 49 | "attributes": { |
135 | 102 | "server": ["id", "imageId", "name", "flavorId", "hostId", | 50 | "server": ["id", "imageId", "name", "flavorId", "hostId", |
137 | 103 | "status", "progress", "adminPass"]}}} | 51 | "status", "progress", "adminPass", "flavorRef", |
138 | 52 | "imageRef"]}}} | ||
139 | 104 | 53 | ||
140 | 105 | def __init__(self): | 54 | def __init__(self): |
141 | 106 | self.compute_api = compute.API() | 55 | self.compute_api = compute.API() |
142 | 107 | self._image_service = utils.import_object(FLAGS.image_service) | 56 | self._image_service = utils.import_object(FLAGS.image_service) |
143 | 108 | super(Controller, self).__init__() | 57 | super(Controller, self).__init__() |
144 | 109 | 58 | ||
145 | 59 | def ips(self, req, id): | ||
146 | 60 | try: | ||
147 | 61 | instance = self.compute_api.get(req.environ['nova.context'], id) | ||
148 | 62 | except exception.NotFound: | ||
149 | 63 | return faults.Fault(exc.HTTPNotFound()) | ||
150 | 64 | |||
151 | 65 | builder = addresses_views.get_view_builder(req) | ||
152 | 66 | return builder.build(instance) | ||
153 | 67 | |||
154 | 110 | def index(self, req): | 68 | def index(self, req): |
155 | 111 | """ Returns a list of server names and ids for a given user """ | 69 | """ Returns a list of server names and ids for a given user """ |
157 | 112 | return self._items(req, entity_maker=_translate_keys) | 70 | return self._items(req, is_detail=False) |
158 | 113 | 71 | ||
159 | 114 | def detail(self, req): | 72 | def detail(self, req): |
160 | 115 | """ Returns a list of server details for a given user """ | 73 | """ Returns a list of server details for a given user """ |
162 | 116 | return self._items(req, entity_maker=_translate_detail_keys) | 74 | return self._items(req, is_detail=True) |
163 | 117 | 75 | ||
165 | 118 | def _items(self, req, entity_maker): | 76 | def _items(self, req, is_detail): |
166 | 119 | """Returns a list of servers for a given user. | 77 | """Returns a list of servers for a given user. |
167 | 120 | 78 | ||
169 | 121 | entity_maker - either _translate_detail_keys or _translate_keys | 79 | builder - the response model builder |
170 | 122 | """ | 80 | """ |
171 | 123 | instance_list = self.compute_api.get_all(req.environ['nova.context']) | 81 | instance_list = self.compute_api.get_all(req.environ['nova.context']) |
172 | 124 | limited_list = common.limited(instance_list, req) | 82 | limited_list = common.limited(instance_list, req) |
175 | 125 | res = [entity_maker(inst)['server'] for inst in limited_list] | 83 | builder = servers_views.get_view_builder(req) |
176 | 126 | return dict(servers=res) | 84 | servers = [builder.build(inst, is_detail)['server'] |
177 | 85 | for inst in limited_list] | ||
178 | 86 | return dict(servers=servers) | ||
179 | 127 | 87 | ||
180 | 128 | def show(self, req, id): | 88 | def show(self, req, id): |
181 | 129 | """ Returns server details by server id """ | 89 | """ Returns server details by server id """ |
182 | 130 | try: | 90 | try: |
183 | 131 | instance = self.compute_api.get(req.environ['nova.context'], id) | 91 | instance = self.compute_api.get(req.environ['nova.context'], id) |
185 | 132 | return _translate_detail_keys(instance) | 92 | builder = servers_views.get_view_builder(req) |
186 | 93 | return builder.build(instance, is_detail=True) | ||
187 | 133 | except exception.NotFound: | 94 | except exception.NotFound: |
188 | 134 | return faults.Fault(exc.HTTPNotFound()) | 95 | return faults.Fault(exc.HTTPNotFound()) |
189 | 135 | 96 | ||
190 | @@ -191,7 +152,8 @@ | |||
191 | 191 | except QuotaError as error: | 152 | except QuotaError as error: |
192 | 192 | self._handle_quota_error(error) | 153 | self._handle_quota_error(error) |
193 | 193 | 154 | ||
195 | 194 | server = _translate_keys(instances[0]) | 155 | builder = servers_views.get_view_builder(req) |
196 | 156 | server = builder.build(instances[0], is_detail=False) | ||
197 | 195 | password = "%s%s" % (server['server']['name'][:4], | 157 | password = "%s%s" % (server['server']['name'][:4], |
198 | 196 | utils.generate_password(12)) | 158 | utils.generate_password(12)) |
199 | 197 | server['server']['adminPass'] = password | 159 | server['server']['adminPass'] = password |
200 | 198 | 160 | ||
201 | === added directory 'nova/api/openstack/views' | |||
202 | === added file 'nova/api/openstack/views/__init__.py' | |||
203 | === added file 'nova/api/openstack/views/addresses.py' | |||
204 | --- nova/api/openstack/views/addresses.py 1970-01-01 00:00:00 +0000 | |||
205 | +++ nova/api/openstack/views/addresses.py 2011-03-17 17:53:41 +0000 | |||
206 | @@ -0,0 +1,54 @@ | |||
207 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
208 | 2 | |||
209 | 3 | # Copyright 2010-2011 OpenStack LLC. | ||
210 | 4 | # All Rights Reserved. | ||
211 | 5 | # | ||
212 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
213 | 7 | # not use this file except in compliance with the License. You may obtain | ||
214 | 8 | # a copy of the License at | ||
215 | 9 | # | ||
216 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
217 | 11 | # | ||
218 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
219 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
220 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
221 | 15 | # License for the specific language governing permissions and limitations | ||
222 | 16 | # under the License. | ||
223 | 17 | |||
224 | 18 | from nova import utils | ||
225 | 19 | from nova.api.openstack import common | ||
226 | 20 | |||
227 | 21 | |||
228 | 22 | def get_view_builder(req): | ||
229 | 23 | ''' | ||
230 | 24 | A factory method that returns the correct builder based on the version of | ||
231 | 25 | the api requested. | ||
232 | 26 | ''' | ||
233 | 27 | version = common.get_api_version(req) | ||
234 | 28 | if version == '1.1': | ||
235 | 29 | return ViewBuilder_1_1() | ||
236 | 30 | else: | ||
237 | 31 | return ViewBuilder_1_0() | ||
238 | 32 | |||
239 | 33 | |||
240 | 34 | class ViewBuilder(object): | ||
241 | 35 | ''' Models a server addresses response as a python dictionary.''' | ||
242 | 36 | |||
243 | 37 | def build(self, inst): | ||
244 | 38 | raise NotImplementedError() | ||
245 | 39 | |||
246 | 40 | |||
247 | 41 | class ViewBuilder_1_0(ViewBuilder): | ||
248 | 42 | def build(self, inst): | ||
249 | 43 | private_ips = utils.get_from_path(inst, 'fixed_ip/address') | ||
250 | 44 | public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') | ||
251 | 45 | return dict(public=public_ips, private=private_ips) | ||
252 | 46 | |||
253 | 47 | |||
254 | 48 | class ViewBuilder_1_1(ViewBuilder): | ||
255 | 49 | def build(self, inst): | ||
256 | 50 | private_ips = utils.get_from_path(inst, 'fixed_ip/address') | ||
257 | 51 | private_ips = [dict(version=4, addr=a) for a in private_ips] | ||
258 | 52 | public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') | ||
259 | 53 | public_ips = [dict(version=4, addr=a) for a in public_ips] | ||
260 | 54 | return dict(public=public_ips, private=private_ips) | ||
261 | 0 | 55 | ||
262 | === added file 'nova/api/openstack/views/flavors.py' | |||
263 | --- nova/api/openstack/views/flavors.py 1970-01-01 00:00:00 +0000 | |||
264 | +++ nova/api/openstack/views/flavors.py 2011-03-17 17:53:41 +0000 | |||
265 | @@ -0,0 +1,51 @@ | |||
266 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
267 | 2 | |||
268 | 3 | # Copyright 2010-2011 OpenStack LLC. | ||
269 | 4 | # All Rights Reserved. | ||
270 | 5 | # | ||
271 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
272 | 7 | # not use this file except in compliance with the License. You may obtain | ||
273 | 8 | # a copy of the License at | ||
274 | 9 | # | ||
275 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
276 | 11 | # | ||
277 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
278 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
279 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
280 | 15 | # License for the specific language governing permissions and limitations | ||
281 | 16 | # under the License. | ||
282 | 17 | |||
283 | 18 | from nova.api.openstack import common | ||
284 | 19 | |||
285 | 20 | |||
286 | 21 | def get_view_builder(req): | ||
287 | 22 | ''' | ||
288 | 23 | A factory method that returns the correct builder based on the version of | ||
289 | 24 | the api requested. | ||
290 | 25 | ''' | ||
291 | 26 | version = common.get_api_version(req) | ||
292 | 27 | base_url = req.application_url | ||
293 | 28 | if version == '1.1': | ||
294 | 29 | return ViewBuilder_1_1(base_url) | ||
295 | 30 | else: | ||
296 | 31 | return ViewBuilder_1_0() | ||
297 | 32 | |||
298 | 33 | |||
299 | 34 | class ViewBuilder(object): | ||
300 | 35 | def __init__(self): | ||
301 | 36 | pass | ||
302 | 37 | |||
303 | 38 | def build(self, flavor_obj): | ||
304 | 39 | raise NotImplementedError() | ||
305 | 40 | |||
306 | 41 | |||
307 | 42 | class ViewBuilder_1_1(ViewBuilder): | ||
308 | 43 | def __init__(self, base_url): | ||
309 | 44 | self.base_url = base_url | ||
310 | 45 | |||
311 | 46 | def generate_href(self, flavor_id): | ||
312 | 47 | return "%s/flavors/%s" % (self.base_url, flavor_id) | ||
313 | 48 | |||
314 | 49 | |||
315 | 50 | class ViewBuilder_1_0(ViewBuilder): | ||
316 | 51 | pass | ||
317 | 0 | 52 | ||
318 | === added file 'nova/api/openstack/views/images.py' | |||
319 | --- nova/api/openstack/views/images.py 1970-01-01 00:00:00 +0000 | |||
320 | +++ nova/api/openstack/views/images.py 2011-03-17 17:53:41 +0000 | |||
321 | @@ -0,0 +1,51 @@ | |||
322 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
323 | 2 | |||
324 | 3 | # Copyright 2010-2011 OpenStack LLC. | ||
325 | 4 | # All Rights Reserved. | ||
326 | 5 | # | ||
327 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
328 | 7 | # not use this file except in compliance with the License. You may obtain | ||
329 | 8 | # a copy of the License at | ||
330 | 9 | # | ||
331 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
332 | 11 | # | ||
333 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
334 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
335 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
336 | 15 | # License for the specific language governing permissions and limitations | ||
337 | 16 | # under the License. | ||
338 | 17 | |||
339 | 18 | from nova.api.openstack import common | ||
340 | 19 | |||
341 | 20 | |||
342 | 21 | def get_view_builder(req): | ||
343 | 22 | ''' | ||
344 | 23 | A factory method that returns the correct builder based on the version of | ||
345 | 24 | the api requested. | ||
346 | 25 | ''' | ||
347 | 26 | version = common.get_api_version(req) | ||
348 | 27 | base_url = req.application_url | ||
349 | 28 | if version == '1.1': | ||
350 | 29 | return ViewBuilder_1_1(base_url) | ||
351 | 30 | else: | ||
352 | 31 | return ViewBuilder_1_0() | ||
353 | 32 | |||
354 | 33 | |||
355 | 34 | class ViewBuilder(object): | ||
356 | 35 | def __init__(self): | ||
357 | 36 | pass | ||
358 | 37 | |||
359 | 38 | def build(self, image_obj): | ||
360 | 39 | raise NotImplementedError() | ||
361 | 40 | |||
362 | 41 | |||
363 | 42 | class ViewBuilder_1_1(ViewBuilder): | ||
364 | 43 | def __init__(self, base_url): | ||
365 | 44 | self.base_url = base_url | ||
366 | 45 | |||
367 | 46 | def generate_href(self, image_id): | ||
368 | 47 | return "%s/images/%s" % (self.base_url, image_id) | ||
369 | 48 | |||
370 | 49 | |||
371 | 50 | class ViewBuilder_1_0(ViewBuilder): | ||
372 | 51 | pass | ||
373 | 0 | 52 | ||
374 | === added file 'nova/api/openstack/views/servers.py' | |||
375 | --- nova/api/openstack/views/servers.py 1970-01-01 00:00:00 +0000 | |||
376 | +++ nova/api/openstack/views/servers.py 2011-03-17 17:53:41 +0000 | |||
377 | @@ -0,0 +1,132 @@ | |||
378 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
379 | 2 | |||
380 | 3 | # Copyright 2010-2011 OpenStack LLC. | ||
381 | 4 | # All Rights Reserved. | ||
382 | 5 | # | ||
383 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
384 | 7 | # not use this file except in compliance with the License. You may obtain | ||
385 | 8 | # a copy of the License at | ||
386 | 9 | # | ||
387 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
388 | 11 | # | ||
389 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
390 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
391 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
392 | 15 | # License for the specific language governing permissions and limitations | ||
393 | 16 | # under the License. | ||
394 | 17 | |||
395 | 18 | import hashlib | ||
396 | 19 | from nova.compute import power_state | ||
397 | 20 | from nova.api.openstack import common | ||
398 | 21 | from nova.api.openstack.views import addresses as addresses_view | ||
399 | 22 | from nova.api.openstack.views import flavors as flavors_view | ||
400 | 23 | from nova.api.openstack.views import images as images_view | ||
401 | 24 | from nova import utils | ||
402 | 25 | |||
403 | 26 | |||
404 | 27 | def get_view_builder(req): | ||
405 | 28 | ''' | ||
406 | 29 | A factory method that returns the correct builder based on the version of | ||
407 | 30 | the api requested. | ||
408 | 31 | ''' | ||
409 | 32 | version = common.get_api_version(req) | ||
410 | 33 | addresses_builder = addresses_view.get_view_builder(req) | ||
411 | 34 | if version == '1.1': | ||
412 | 35 | flavor_builder = flavors_view.get_view_builder(req) | ||
413 | 36 | image_builder = images_view.get_view_builder(req) | ||
414 | 37 | return ViewBuilder_1_1(addresses_builder, flavor_builder, | ||
415 | 38 | image_builder) | ||
416 | 39 | else: | ||
417 | 40 | return ViewBuilder_1_0(addresses_builder) | ||
418 | 41 | |||
419 | 42 | |||
420 | 43 | class ViewBuilder(object): | ||
421 | 44 | ''' | ||
422 | 45 | Models a server response as a python dictionary. | ||
423 | 46 | Abstract methods: _build_image, _build_flavor | ||
424 | 47 | ''' | ||
425 | 48 | |||
426 | 49 | def __init__(self, addresses_builder): | ||
427 | 50 | self.addresses_builder = addresses_builder | ||
428 | 51 | |||
429 | 52 | def build(self, inst, is_detail): | ||
430 | 53 | """ | ||
431 | 54 | Coerces into dictionary format, mapping everything to | ||
432 | 55 | Rackspace-like attributes for return | ||
433 | 56 | """ | ||
434 | 57 | if is_detail: | ||
435 | 58 | return self._build_detail(inst) | ||
436 | 59 | else: | ||
437 | 60 | return self._build_simple(inst) | ||
438 | 61 | |||
439 | 62 | def _build_simple(self, inst): | ||
440 | 63 | return dict(server=dict(id=inst['id'], name=inst['display_name'])) | ||
441 | 64 | |||
442 | 65 | def _build_detail(self, inst): | ||
443 | 66 | power_mapping = { | ||
444 | 67 | None: 'build', | ||
445 | 68 | power_state.NOSTATE: 'build', | ||
446 | 69 | power_state.RUNNING: 'active', | ||
447 | 70 | power_state.BLOCKED: 'active', | ||
448 | 71 | power_state.SUSPENDED: 'suspended', | ||
449 | 72 | power_state.PAUSED: 'paused', | ||
450 | 73 | power_state.SHUTDOWN: 'active', | ||
451 | 74 | power_state.SHUTOFF: 'active', | ||
452 | 75 | power_state.CRASHED: 'error', | ||
453 | 76 | power_state.FAILED: 'error'} | ||
454 | 77 | inst_dict = {} | ||
455 | 78 | |||
456 | 79 | #mapped_keys = dict(status='state', imageId='image_id', | ||
457 | 80 | # flavorId='instance_type', name='display_name', id='id') | ||
458 | 81 | |||
459 | 82 | mapped_keys = dict(status='state', name='display_name', id='id') | ||
460 | 83 | |||
461 | 84 | for k, v in mapped_keys.iteritems(): | ||
462 | 85 | inst_dict[k] = inst[v] | ||
463 | 86 | |||
464 | 87 | inst_dict['status'] = power_mapping[inst_dict['status']] | ||
465 | 88 | inst_dict['addresses'] = self.addresses_builder.build(inst) | ||
466 | 89 | |||
467 | 90 | # Return the metadata as a dictionary | ||
468 | 91 | metadata = {} | ||
469 | 92 | for item in inst['metadata']: | ||
470 | 93 | metadata[item['key']] = item['value'] | ||
471 | 94 | inst_dict['metadata'] = metadata | ||
472 | 95 | |||
473 | 96 | inst_dict['hostId'] = '' | ||
474 | 97 | if inst['host']: | ||
475 | 98 | inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() | ||
476 | 99 | |||
477 | 100 | self._build_image(inst_dict, inst) | ||
478 | 101 | self._build_flavor(inst_dict, inst) | ||
479 | 102 | |||
480 | 103 | return dict(server=inst_dict) | ||
481 | 104 | |||
482 | 105 | def _build_image(self, response, inst): | ||
483 | 106 | raise NotImplementedError() | ||
484 | 107 | |||
485 | 108 | def _build_flavor(self, response, inst): | ||
486 | 109 | raise NotImplementedError() | ||
487 | 110 | |||
488 | 111 | |||
489 | 112 | class ViewBuilder_1_0(ViewBuilder): | ||
490 | 113 | def _build_image(self, response, inst): | ||
491 | 114 | response["imageId"] = inst["image_id"] | ||
492 | 115 | |||
493 | 116 | def _build_flavor(self, response, inst): | ||
494 | 117 | response["flavorId"] = inst["instance_type"] | ||
495 | 118 | |||
496 | 119 | |||
497 | 120 | class ViewBuilder_1_1(ViewBuilder): | ||
498 | 121 | def __init__(self, addresses_builder, flavor_builder, image_builder): | ||
499 | 122 | ViewBuilder.__init__(self, addresses_builder) | ||
500 | 123 | self.flavor_builder = flavor_builder | ||
501 | 124 | self.image_builder = image_builder | ||
502 | 125 | |||
503 | 126 | def _build_image(self, response, inst): | ||
504 | 127 | image_id = inst["image_id"] | ||
505 | 128 | response["imageRef"] = self.image_builder.generate_href(image_id) | ||
506 | 129 | |||
507 | 130 | def _build_flavor(self, response, inst): | ||
508 | 131 | flavor_id = inst["instance_type"] | ||
509 | 132 | response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) | ||
510 | 0 | 133 | ||
511 | === modified file 'nova/tests/api/openstack/fakes.py' | |||
512 | --- nova/tests/api/openstack/fakes.py 2011-03-16 19:16:16 +0000 | |||
513 | +++ nova/tests/api/openstack/fakes.py 2011-03-17 17:53:41 +0000 | |||
514 | @@ -79,6 +79,7 @@ | |||
515 | 79 | api = openstack.FaultWrapper(auth.AuthMiddleware( | 79 | api = openstack.FaultWrapper(auth.AuthMiddleware( |
516 | 80 | ratelimiting.RateLimitingMiddleware(inner_application))) | 80 | ratelimiting.RateLimitingMiddleware(inner_application))) |
517 | 81 | mapper['/v1.0'] = api | 81 | mapper['/v1.0'] = api |
518 | 82 | mapper['/v1.1'] = api | ||
519 | 82 | mapper['/'] = openstack.FaultWrapper(openstack.Versions()) | 83 | mapper['/'] = openstack.FaultWrapper(openstack.Versions()) |
520 | 83 | return mapper | 84 | return mapper |
521 | 84 | 85 | ||
522 | 85 | 86 | ||
523 | === modified file 'nova/tests/api/openstack/test_servers.py' | |||
524 | --- nova/tests/api/openstack/test_servers.py 2011-03-16 19:16:16 +0000 | |||
525 | +++ nova/tests/api/openstack/test_servers.py 2011-03-17 17:53:41 +0000 | |||
526 | @@ -24,6 +24,7 @@ | |||
527 | 24 | import stubout | 24 | import stubout |
528 | 25 | import webob | 25 | import webob |
529 | 26 | 26 | ||
530 | 27 | from nova import context | ||
531 | 27 | from nova import db | 28 | from nova import db |
532 | 28 | from nova import flags | 29 | from nova import flags |
533 | 29 | from nova import test | 30 | from nova import test |
534 | @@ -81,7 +82,7 @@ | |||
535 | 81 | "admin_pass": "", | 82 | "admin_pass": "", |
536 | 82 | "user_id": user_id, | 83 | "user_id": user_id, |
537 | 83 | "project_id": "", | 84 | "project_id": "", |
539 | 84 | "image_id": 10, | 85 | "image_id": "10", |
540 | 85 | "kernel_id": "", | 86 | "kernel_id": "", |
541 | 86 | "ramdisk_id": "", | 87 | "ramdisk_id": "", |
542 | 87 | "launch_index": 0, | 88 | "launch_index": 0, |
543 | @@ -94,7 +95,7 @@ | |||
544 | 94 | "local_gb": 0, | 95 | "local_gb": 0, |
545 | 95 | "hostname": "", | 96 | "hostname": "", |
546 | 96 | "host": None, | 97 | "host": None, |
548 | 97 | "instance_type": "", | 98 | "instance_type": "1", |
549 | 98 | "user_data": "", | 99 | "user_data": "", |
550 | 99 | "reservation_id": "", | 100 | "reservation_id": "", |
551 | 100 | "mac_address": "", | 101 | "mac_address": "", |
552 | @@ -179,6 +180,25 @@ | |||
553 | 179 | self.assertEqual(len(addresses["private"]), 1) | 180 | self.assertEqual(len(addresses["private"]), 1) |
554 | 180 | self.assertEqual(addresses["private"][0], private) | 181 | self.assertEqual(addresses["private"][0], private) |
555 | 181 | 182 | ||
556 | 183 | def test_get_server_by_id_with_addresses_v1_1(self): | ||
557 | 184 | private = "192.168.0.3" | ||
558 | 185 | public = ["1.2.3.4"] | ||
559 | 186 | new_return_server = return_server_with_addresses(private, public) | ||
560 | 187 | self.stubs.Set(nova.db.api, 'instance_get', new_return_server) | ||
561 | 188 | req = webob.Request.blank('/v1.1/servers/1') | ||
562 | 189 | req.environ['api.version'] = '1.1' | ||
563 | 190 | res = req.get_response(fakes.wsgi_app()) | ||
564 | 191 | res_dict = json.loads(res.body) | ||
565 | 192 | self.assertEqual(res_dict['server']['id'], '1') | ||
566 | 193 | self.assertEqual(res_dict['server']['name'], 'server1') | ||
567 | 194 | addresses = res_dict['server']['addresses'] | ||
568 | 195 | self.assertEqual(len(addresses["public"]), len(public)) | ||
569 | 196 | self.assertEqual(addresses["public"][0], | ||
570 | 197 | {"version": 4, "addr": public[0]}) | ||
571 | 198 | self.assertEqual(len(addresses["private"]), 1) | ||
572 | 199 | self.assertEqual(addresses["private"][0], | ||
573 | 200 | {"version": 4, "addr": private}) | ||
574 | 201 | |||
575 | 182 | def test_get_server_list(self): | 202 | def test_get_server_list(self): |
576 | 183 | req = webob.Request.blank('/v1.0/servers') | 203 | req = webob.Request.blank('/v1.0/servers') |
577 | 184 | res = req.get_response(fakes.wsgi_app()) | 204 | res = req.get_response(fakes.wsgi_app()) |
578 | @@ -339,19 +359,32 @@ | |||
579 | 339 | res = req.get_response(fakes.wsgi_app()) | 359 | res = req.get_response(fakes.wsgi_app()) |
580 | 340 | self.assertEqual(res.status, '404 Not Found') | 360 | self.assertEqual(res.status, '404 Not Found') |
581 | 341 | 361 | ||
583 | 342 | def test_get_all_server_details(self): | 362 | def test_get_all_server_details_v1_0(self): |
584 | 343 | req = webob.Request.blank('/v1.0/servers/detail') | 363 | req = webob.Request.blank('/v1.0/servers/detail') |
585 | 344 | res = req.get_response(fakes.wsgi_app()) | 364 | res = req.get_response(fakes.wsgi_app()) |
586 | 345 | res_dict = json.loads(res.body) | 365 | res_dict = json.loads(res.body) |
587 | 346 | 366 | ||
596 | 347 | i = 0 | 367 | for i, s in enumerate(res_dict['servers']): |
597 | 348 | for s in res_dict['servers']: | 368 | self.assertEqual(s['id'], i) |
598 | 349 | self.assertEqual(s['id'], i) | 369 | self.assertEqual(s['hostId'], '') |
599 | 350 | self.assertEqual(s['hostId'], '') | 370 | self.assertEqual(s['name'], 'server%d' % i) |
600 | 351 | self.assertEqual(s['name'], 'server%d' % i) | 371 | self.assertEqual(s['imageId'], '10') |
601 | 352 | self.assertEqual(s['imageId'], 10) | 372 | self.assertEqual(s['flavorId'], '1') |
602 | 353 | self.assertEqual(s['metadata']['seq'], i) | 373 | self.assertEqual(s['metadata']['seq'], i) |
603 | 354 | i += 1 | 374 | |
604 | 375 | def test_get_all_server_details_v1_1(self): | ||
605 | 376 | req = webob.Request.blank('/v1.1/servers/detail') | ||
606 | 377 | req.environ['api.version'] = '1.1' | ||
607 | 378 | res = req.get_response(fakes.wsgi_app()) | ||
608 | 379 | res_dict = json.loads(res.body) | ||
609 | 380 | |||
610 | 381 | for i, s in enumerate(res_dict['servers']): | ||
611 | 382 | self.assertEqual(s['id'], i) | ||
612 | 383 | self.assertEqual(s['hostId'], '') | ||
613 | 384 | self.assertEqual(s['name'], 'server%d' % i) | ||
614 | 385 | self.assertEqual(s['imageRef'], 'http://localhost/v1.1/images/10') | ||
615 | 386 | self.assertEqual(s['flavorRef'], 'http://localhost/v1.1/flavors/1') | ||
616 | 387 | self.assertEqual(s['metadata']['seq'], i) | ||
617 | 355 | 388 | ||
618 | 356 | def test_get_all_server_details_with_host(self): | 389 | def test_get_all_server_details_with_host(self): |
619 | 357 | ''' | 390 | ''' |
620 | 358 | 391 | ||
621 | === modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/glance' | |||
622 | --- plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2011-03-11 20:14:48 +0000 | |||
623 | +++ plugins/xenserver/xenapi/etc/xapi.d/plugins/glance 2011-03-17 17:53:41 +0000 | |||
624 | @@ -216,7 +216,7 @@ | |||
625 | 216 | 'x-image-meta-status': 'queued', | 216 | 'x-image-meta-status': 'queued', |
626 | 217 | 'x-image-meta-disk-format': 'vhd', | 217 | 'x-image-meta-disk-format': 'vhd', |
627 | 218 | 'x-image-meta-container-format': 'ovf', | 218 | 'x-image-meta-container-format': 'ovf', |
629 | 219 | 'x-image-meta-property-os-type': os_type | 219 | 'x-image-meta-property-os-type': os_type, |
630 | 220 | } | 220 | } |
631 | 221 | 221 | ||
632 | 222 | for header, value in headers.iteritems(): | 222 | for header, value in headers.iteritems(): |
I really like the view builders concept. Good job!