Merge lp:~yolanda.robla/ubuntu/precise/glance/essex-sru into lp:ubuntu/precise-updates/glance
- Precise (12.04)
- essex-sru
- Merge into precise-updates
Status: | Rejected |
---|---|
Rejected by: | James Page |
Proposed branch: | lp:~yolanda.robla/ubuntu/precise/glance/essex-sru |
Merge into: | lp:ubuntu/precise-updates/glance |
Diff against target: |
1573 lines (+309/-1085) 21 files modified
.gitignore (+0/-11) .gitreview (+0/-5) .mailmap (+0/-19) .pc/CVE-2012-4573.patch/glance/api/v1/images.py (+0/-973) .pc/applied-patches (+0/-1) .pc/fix_migration_012_foreign_keys.patch/Authors (+1/-0) Authors (+1/-0) PKG-INFO (+15/-0) debian/changelog (+18/-0) debian/glance-api.logrotate (+1/-1) debian/glance-registry.logrotate (+1/-1) debian/patches/CVE-2012-4573.patch (+0/-35) debian/patches/fix_migration_012_foreign_keys.patch (+22/-26) debian/patches/series (+0/-1) glance.egg-info/PKG-INFO (+15/-0) glance.egg-info/SOURCES.txt (+217/-0) glance.egg-info/dependency_links.txt (+1/-0) glance.egg-info/top_level.txt (+1/-0) glance/vcsversion.py (+7/-0) setup.cfg (+8/-11) tools/pip-requires (+1/-1) |
To merge this branch: | bzr merge lp:~yolanda.robla/ubuntu/precise/glance/essex-sru |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Disapprove | ||
Review via email: mp+140451@code.launchpad.net |
This proposal supersedes a proposal from 2012-12-18.
Commit message
Description of the change
James Page (james-page) wrote : Posted in a previous version of this proposal | # |
James Page (james-page) wrote : | # |
As discussed with Yolanda on IRC, this update is pretty much no-change as the security issue is already patched and the jenkins problem does not effect Ubuntu.
Unmerged revisions
- 53. By Yolanda Robla
-
removed skipping pep8 tests
fix typos in changelog - 52. By Yolanda Robla
-
[ Adam Gandelman ]
* debian/glance- {registry, api}.logrotate: Fix incorrect logfile
locations. (LP: #1049314)[ Yolanda Robla ]
* debian/rules: skipping pep8 tests to allow building[ Yolanda Robla Mota ]
* Resynchronize with stable/essex (efd7e75b):
- [efd7e75] Non-admin users can cause public glance images to be deleted
from the backend storage repository (CVE-2012-4573)
- [e6be061] Jenkins jobs fail because of incompatibility between sqlalchemy-
migrate and the newest sqlalchemy-0.8.0b1 (LP: #1073569)* Dropped patches, superseeded by snapshot:
- debian/patches/ CVE-2012- 4573.patch: [efd7e75] - 51. By Jamie Strandboge
-
* SECURITY UPDATE: deletion of arbitrary public and shared images via
authenticated user
- debian/patches/ CVE-2012- 4573.patch: adjust glance/ api/v1/ images. py to
ensure image is owned by user before delayed_deletion
- CVE-2012-4573
Preview Diff
1 | === removed file '.gitignore' | |||
2 | --- .gitignore 2012-06-24 03:14:33 +0000 | |||
3 | +++ .gitignore 1970-01-01 00:00:00 +0000 | |||
4 | @@ -1,11 +0,0 @@ | |||
5 | 1 | *.pyc | ||
6 | 2 | *.swp | ||
7 | 3 | *.log | ||
8 | 4 | .glance-venv | ||
9 | 5 | .venv | ||
10 | 6 | .tox | ||
11 | 7 | build | ||
12 | 8 | dist | ||
13 | 9 | glance.egg-info | ||
14 | 10 | glance/vcsversion.py | ||
15 | 11 | tests.sqlite | ||
16 | 12 | 0 | ||
17 | === removed file '.gitreview' | |||
18 | --- .gitreview 2012-06-24 03:14:33 +0000 | |||
19 | +++ .gitreview 1970-01-01 00:00:00 +0000 | |||
20 | @@ -1,5 +0,0 @@ | |||
21 | 1 | [gerrit] | ||
22 | 2 | host=review.openstack.org | ||
23 | 3 | port=29418 | ||
24 | 4 | project=openstack/glance.git | ||
25 | 5 | defaultbranch=stable/essex | ||
26 | 6 | 0 | ||
27 | === removed file '.mailmap' | |||
28 | --- .mailmap 2012-06-24 03:14:33 +0000 | |||
29 | +++ .mailmap 1970-01-01 00:00:00 +0000 | |||
30 | @@ -1,19 +0,0 @@ | |||
31 | 1 | # Format is: | ||
32 | 2 | # <preferred e-mail> <other e-mail 1> | ||
33 | 3 | # <preferred e-mail> <other e-mail 2> | ||
34 | 4 | <adam.gandelman@canonical.com> <adamg@canonical.com> | ||
35 | 5 | <brian.waldon@rackspace.com> <bcwaldon@gmail.com> | ||
36 | 6 | <corywright@gmail.com> <cory.wright@rackspace.com> | ||
37 | 7 | <dprince@redhat.com> <dan.prince@rackspace.com> | ||
38 | 8 | <jsuh@isi.edu> <jsuh@bespin> | ||
39 | 9 | <josh@jk0.org> <josh.kearney@rackspace.com> | ||
40 | 10 | <rconradharris@gmail.com> <rick.harris@rackspace.com> | ||
41 | 11 | <rconradharris@gmail.com> <rick@quasar.racklabs.com> | ||
42 | 12 | <rick@openstack.org> <rclark@chat-blanc> | ||
43 | 13 | <soren.hansen@rackspace.com> <soren@linux2go.dk> | ||
44 | 14 | <soren.hansen@rackspace.com> <soren@openstack.org> | ||
45 | 15 | <jeblair@hp.com> <corvus@gnu.org> | ||
46 | 16 | <jeblair@hp.com> <james.blair@rackspace.com> | ||
47 | 17 | <chris@pistoncloud.com> <chris@slicehost.com> | ||
48 | 18 | <ken.pepple@gmail.com> <ken.pepple@rabbityard.com> | ||
49 | 19 | <P@draigBrady.com> <pbrady@redhat.com> | ||
50 | 20 | 0 | ||
51 | === removed directory '.pc/CVE-2012-4573.patch' | |||
52 | === removed directory '.pc/CVE-2012-4573.patch/glance' | |||
53 | === removed directory '.pc/CVE-2012-4573.patch/glance/api' | |||
54 | === removed directory '.pc/CVE-2012-4573.patch/glance/api/v1' | |||
55 | === removed file '.pc/CVE-2012-4573.patch/glance/api/v1/images.py' | |||
56 | --- .pc/CVE-2012-4573.patch/glance/api/v1/images.py 2012-11-08 07:19:39 +0000 | |||
57 | +++ .pc/CVE-2012-4573.patch/glance/api/v1/images.py 1970-01-01 00:00:00 +0000 | |||
58 | @@ -1,973 +0,0 @@ | |||
59 | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||
60 | 2 | |||
61 | 3 | # Copyright 2010 OpenStack LLC. | ||
62 | 4 | # All Rights Reserved. | ||
63 | 5 | # | ||
64 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
65 | 7 | # not use this file except in compliance with the License. You may obtain | ||
66 | 8 | # a copy of the License at | ||
67 | 9 | # | ||
68 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
69 | 11 | # | ||
70 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
71 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
72 | 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
73 | 15 | # License for the specific language governing permissions and limitations | ||
74 | 16 | # under the License. | ||
75 | 17 | |||
76 | 18 | """ | ||
77 | 19 | /images endpoint for Glance v1 API | ||
78 | 20 | """ | ||
79 | 21 | |||
80 | 22 | import errno | ||
81 | 23 | import logging | ||
82 | 24 | import sys | ||
83 | 25 | import traceback | ||
84 | 26 | |||
85 | 27 | from webob.exc import (HTTPError, | ||
86 | 28 | HTTPNotFound, | ||
87 | 29 | HTTPConflict, | ||
88 | 30 | HTTPBadRequest, | ||
89 | 31 | HTTPForbidden, | ||
90 | 32 | HTTPRequestEntityTooLarge, | ||
91 | 33 | HTTPServiceUnavailable, | ||
92 | 34 | ) | ||
93 | 35 | |||
94 | 36 | from glance.api import policy | ||
95 | 37 | import glance.api.v1 | ||
96 | 38 | from glance.api.v1 import controller | ||
97 | 39 | from glance.api.v1 import filters | ||
98 | 40 | from glance.common import cfg | ||
99 | 41 | from glance.common import exception | ||
100 | 42 | from glance.common import wsgi | ||
101 | 43 | from glance.common import utils | ||
102 | 44 | import glance.store | ||
103 | 45 | import glance.store.filesystem | ||
104 | 46 | import glance.store.http | ||
105 | 47 | import glance.store.rbd | ||
106 | 48 | import glance.store.s3 | ||
107 | 49 | import glance.store.swift | ||
108 | 50 | from glance.store import (get_from_backend, | ||
109 | 51 | get_size_from_backend, | ||
110 | 52 | schedule_delete_from_backend, | ||
111 | 53 | get_store_from_location, | ||
112 | 54 | get_store_from_scheme) | ||
113 | 55 | from glance import registry | ||
114 | 56 | from glance import notifier | ||
115 | 57 | |||
116 | 58 | |||
117 | 59 | logger = logging.getLogger(__name__) | ||
118 | 60 | SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS | ||
119 | 61 | SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS | ||
120 | 62 | |||
121 | 63 | |||
122 | 64 | # 1 PiB, which is a *huge* image by anyone's measure. This is just to protect | ||
123 | 65 | # against client programming errors (or DoS attacks) in the image metadata. | ||
124 | 66 | # We have a known limit of 1 << 63 in the database -- images.size is declared | ||
125 | 67 | # as a BigInteger. | ||
126 | 68 | IMAGE_SIZE_CAP = 1 << 50 | ||
127 | 69 | |||
128 | 70 | |||
129 | 71 | class Controller(controller.BaseController): | ||
130 | 72 | """ | ||
131 | 73 | WSGI controller for images resource in Glance v1 API | ||
132 | 74 | |||
133 | 75 | The images resource API is a RESTful web service for image data. The API | ||
134 | 76 | is as follows:: | ||
135 | 77 | |||
136 | 78 | GET /images -- Returns a set of brief metadata about images | ||
137 | 79 | GET /images/detail -- Returns a set of detailed metadata about | ||
138 | 80 | images | ||
139 | 81 | HEAD /images/<ID> -- Return metadata about an image with id <ID> | ||
140 | 82 | GET /images/<ID> -- Return image data for image with id <ID> | ||
141 | 83 | POST /images -- Store image data and return metadata about the | ||
142 | 84 | newly-stored image | ||
143 | 85 | PUT /images/<ID> -- Update image metadata and/or upload image | ||
144 | 86 | data for a previously-reserved image | ||
145 | 87 | DELETE /images/<ID> -- Delete the image with id <ID> | ||
146 | 88 | """ | ||
147 | 89 | |||
148 | 90 | default_store_opt = cfg.StrOpt('default_store', default='file') | ||
149 | 91 | |||
150 | 92 | def __init__(self, conf): | ||
151 | 93 | self.conf = conf | ||
152 | 94 | self.conf.register_opt(self.default_store_opt) | ||
153 | 95 | glance.store.create_stores(conf) | ||
154 | 96 | self.verify_store_or_exit(self.conf.default_store) | ||
155 | 97 | self.notifier = notifier.Notifier(conf) | ||
156 | 98 | registry.configure_registry_client(conf) | ||
157 | 99 | self.policy = policy.Enforcer(conf) | ||
158 | 100 | |||
159 | 101 | def _enforce(self, req, action): | ||
160 | 102 | """Authorize an action against our policies""" | ||
161 | 103 | try: | ||
162 | 104 | self.policy.enforce(req.context, action, {}) | ||
163 | 105 | except exception.Forbidden: | ||
164 | 106 | raise HTTPForbidden() | ||
165 | 107 | |||
166 | 108 | def index(self, req): | ||
167 | 109 | """ | ||
168 | 110 | Returns the following information for all public, available images: | ||
169 | 111 | |||
170 | 112 | * id -- The opaque image identifier | ||
171 | 113 | * name -- The name of the image | ||
172 | 114 | * disk_format -- The disk image format | ||
173 | 115 | * container_format -- The "container" format of the image | ||
174 | 116 | * checksum -- MD5 checksum of the image data | ||
175 | 117 | * size -- Size of image data in bytes | ||
176 | 118 | |||
177 | 119 | :param req: The WSGI/Webob Request object | ||
178 | 120 | :retval The response body is a mapping of the following form:: | ||
179 | 121 | |||
180 | 122 | {'images': [ | ||
181 | 123 | {'id': <ID>, | ||
182 | 124 | 'name': <NAME>, | ||
183 | 125 | 'disk_format': <DISK_FORMAT>, | ||
184 | 126 | 'container_format': <DISK_FORMAT>, | ||
185 | 127 | 'checksum': <CHECKSUM> | ||
186 | 128 | 'size': <SIZE>}, ... | ||
187 | 129 | ]} | ||
188 | 130 | """ | ||
189 | 131 | self._enforce(req, 'get_images') | ||
190 | 132 | params = self._get_query_params(req) | ||
191 | 133 | try: | ||
192 | 134 | images = registry.get_images_list(req.context, **params) | ||
193 | 135 | except exception.Invalid, e: | ||
194 | 136 | raise HTTPBadRequest(explanation="%s" % e) | ||
195 | 137 | |||
196 | 138 | return dict(images=images) | ||
197 | 139 | |||
198 | 140 | def detail(self, req): | ||
199 | 141 | """ | ||
200 | 142 | Returns detailed information for all public, available images | ||
201 | 143 | |||
202 | 144 | :param req: The WSGI/Webob Request object | ||
203 | 145 | :retval The response body is a mapping of the following form:: | ||
204 | 146 | |||
205 | 147 | {'images': [ | ||
206 | 148 | {'id': <ID>, | ||
207 | 149 | 'name': <NAME>, | ||
208 | 150 | 'size': <SIZE>, | ||
209 | 151 | 'disk_format': <DISK_FORMAT>, | ||
210 | 152 | 'container_format': <CONTAINER_FORMAT>, | ||
211 | 153 | 'checksum': <CHECKSUM>, | ||
212 | 154 | 'min_disk': <MIN_DISK>, | ||
213 | 155 | 'min_ram': <MIN_RAM>, | ||
214 | 156 | 'store': <STORE>, | ||
215 | 157 | 'status': <STATUS>, | ||
216 | 158 | 'created_at': <TIMESTAMP>, | ||
217 | 159 | 'updated_at': <TIMESTAMP>, | ||
218 | 160 | 'deleted_at': <TIMESTAMP>|<NONE>, | ||
219 | 161 | 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ... | ||
220 | 162 | ]} | ||
221 | 163 | """ | ||
222 | 164 | self._enforce(req, 'get_images') | ||
223 | 165 | params = self._get_query_params(req) | ||
224 | 166 | try: | ||
225 | 167 | images = registry.get_images_detail(req.context, **params) | ||
226 | 168 | # Strip out the Location attribute. Temporary fix for | ||
227 | 169 | # LP Bug #755916. This information is still coming back | ||
228 | 170 | # from the registry, since the API server still needs access | ||
229 | 171 | # to it, however we do not return this potential security | ||
230 | 172 | # information to the API end user... | ||
231 | 173 | for image in images: | ||
232 | 174 | del image['location'] | ||
233 | 175 | except exception.Invalid, e: | ||
234 | 176 | raise HTTPBadRequest(explanation="%s" % e) | ||
235 | 177 | return dict(images=images) | ||
236 | 178 | |||
237 | 179 | def _get_query_params(self, req): | ||
238 | 180 | """ | ||
239 | 181 | Extracts necessary query params from request. | ||
240 | 182 | |||
241 | 183 | :param req: the WSGI Request object | ||
242 | 184 | :retval dict of parameters that can be used by registry client | ||
243 | 185 | """ | ||
244 | 186 | params = {'filters': self._get_filters(req)} | ||
245 | 187 | |||
246 | 188 | for PARAM in SUPPORTED_PARAMS: | ||
247 | 189 | if PARAM in req.params: | ||
248 | 190 | params[PARAM] = req.params.get(PARAM) | ||
249 | 191 | return params | ||
250 | 192 | |||
251 | 193 | def _get_filters(self, req): | ||
252 | 194 | """ | ||
253 | 195 | Return a dictionary of query param filters from the request | ||
254 | 196 | |||
255 | 197 | :param req: the Request object coming from the wsgi layer | ||
256 | 198 | :retval a dict of key/value filters | ||
257 | 199 | """ | ||
258 | 200 | query_filters = {} | ||
259 | 201 | for param in req.params: | ||
260 | 202 | if param in SUPPORTED_FILTERS or param.startswith('property-'): | ||
261 | 203 | query_filters[param] = req.params.get(param) | ||
262 | 204 | if not filters.validate(param, query_filters[param]): | ||
263 | 205 | raise HTTPBadRequest('Bad value passed to filter %s ' | ||
264 | 206 | 'got %s' % (param, | ||
265 | 207 | query_filters[param])) | ||
266 | 208 | return query_filters | ||
267 | 209 | |||
268 | 210 | def meta(self, req, id): | ||
269 | 211 | """ | ||
270 | 212 | Returns metadata about an image in the HTTP headers of the | ||
271 | 213 | response object | ||
272 | 214 | |||
273 | 215 | :param req: The WSGI/Webob Request object | ||
274 | 216 | :param id: The opaque image identifier | ||
275 | 217 | :retval similar to 'show' method but without image_data | ||
276 | 218 | |||
277 | 219 | :raises HTTPNotFound if image metadata is not available to user | ||
278 | 220 | """ | ||
279 | 221 | self._enforce(req, 'get_image') | ||
280 | 222 | image_meta = self.get_image_meta_or_404(req, id) | ||
281 | 223 | del image_meta['location'] | ||
282 | 224 | return { | ||
283 | 225 | 'image_meta': image_meta | ||
284 | 226 | } | ||
285 | 227 | |||
286 | 228 | @staticmethod | ||
287 | 229 | def _validate_source(source, req): | ||
288 | 230 | """ | ||
289 | 231 | External sources (as specified via the location or copy-from headers) | ||
290 | 232 | are supported only over non-local store types, i.e. S3, Swift, HTTP. | ||
291 | 233 | Note the absence of file:// for security reasons, see LP bug #942118. | ||
292 | 234 | If the above constraint is violated, we reject with 400 "Bad Request". | ||
293 | 235 | """ | ||
294 | 236 | if source: | ||
295 | 237 | for scheme in ['s3', 'swift', 'http']: | ||
296 | 238 | if source.lower().startswith(scheme): | ||
297 | 239 | return source | ||
298 | 240 | msg = _("External sourcing not supported for store %s") % source | ||
299 | 241 | logger.error(msg) | ||
300 | 242 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") | ||
301 | 243 | |||
302 | 244 | @staticmethod | ||
303 | 245 | def _copy_from(req): | ||
304 | 246 | return req.headers.get('x-glance-api-copy-from') | ||
305 | 247 | |||
306 | 248 | @staticmethod | ||
307 | 249 | def _external_source(image_meta, req): | ||
308 | 250 | source = image_meta.get('location', Controller._copy_from(req)) | ||
309 | 251 | return Controller._validate_source(source, req) | ||
310 | 252 | |||
311 | 253 | @staticmethod | ||
312 | 254 | def _get_from_store(where): | ||
313 | 255 | try: | ||
314 | 256 | image_data, image_size = get_from_backend(where) | ||
315 | 257 | except exception.NotFound, e: | ||
316 | 258 | raise HTTPNotFound(explanation="%s" % e) | ||
317 | 259 | image_size = int(image_size) if image_size else None | ||
318 | 260 | return image_data, image_size | ||
319 | 261 | |||
320 | 262 | def show(self, req, id): | ||
321 | 263 | """ | ||
322 | 264 | Returns an iterator that can be used to retrieve an image's | ||
323 | 265 | data along with the image metadata. | ||
324 | 266 | |||
325 | 267 | :param req: The WSGI/Webob Request object | ||
326 | 268 | :param id: The opaque image identifier | ||
327 | 269 | |||
328 | 270 | :raises HTTPNotFound if image is not available to user | ||
329 | 271 | """ | ||
330 | 272 | self._enforce(req, 'get_image') | ||
331 | 273 | image_meta = self.get_active_image_meta_or_404(req, id) | ||
332 | 274 | |||
333 | 275 | if image_meta.get('size') == 0: | ||
334 | 276 | image_iterator = iter([]) | ||
335 | 277 | else: | ||
336 | 278 | image_iterator, size = self._get_from_store(image_meta['location']) | ||
337 | 279 | image_meta['size'] = size or image_meta['size'] | ||
338 | 280 | |||
339 | 281 | del image_meta['location'] | ||
340 | 282 | return { | ||
341 | 283 | 'image_iterator': image_iterator, | ||
342 | 284 | 'image_meta': image_meta, | ||
343 | 285 | } | ||
344 | 286 | |||
345 | 287 | def _reserve(self, req, image_meta): | ||
346 | 288 | """ | ||
347 | 289 | Adds the image metadata to the registry and assigns | ||
348 | 290 | an image identifier if one is not supplied in the request | ||
349 | 291 | headers. Sets the image's status to `queued`. | ||
350 | 292 | |||
351 | 293 | :param req: The WSGI/Webob Request object | ||
352 | 294 | :param id: The opaque image identifier | ||
353 | 295 | :param image_meta: The image metadata | ||
354 | 296 | |||
355 | 297 | :raises HTTPConflict if image already exists | ||
356 | 298 | :raises HTTPBadRequest if image metadata is not valid | ||
357 | 299 | """ | ||
358 | 300 | location = self._external_source(image_meta, req) | ||
359 | 301 | |||
360 | 302 | image_meta['status'] = ('active' if image_meta.get('size') == 0 | ||
361 | 303 | else 'queued') | ||
362 | 304 | |||
363 | 305 | if location: | ||
364 | 306 | store = get_store_from_location(location) | ||
365 | 307 | # check the store exists before we hit the registry, but we | ||
366 | 308 | # don't actually care what it is at this point | ||
367 | 309 | self.get_store_or_400(req, store) | ||
368 | 310 | |||
369 | 311 | # retrieve the image size from remote store (if not provided) | ||
370 | 312 | image_meta['size'] = self._get_size(image_meta, location) | ||
371 | 313 | else: | ||
372 | 314 | # Ensure that the size attribute is set to zero for directly | ||
373 | 315 | # uploadable images (if not provided). The size will be set | ||
374 | 316 | # to a non-zero value during upload | ||
375 | 317 | image_meta['size'] = image_meta.get('size', 0) | ||
376 | 318 | |||
377 | 319 | try: | ||
378 | 320 | image_meta = registry.add_image_metadata(req.context, image_meta) | ||
379 | 321 | return image_meta | ||
380 | 322 | except exception.Duplicate: | ||
381 | 323 | msg = (_("An image with identifier %s already exists") | ||
382 | 324 | % image_meta['id']) | ||
383 | 325 | logger.error(msg) | ||
384 | 326 | raise HTTPConflict(msg, request=req, content_type="text/plain") | ||
385 | 327 | except exception.Invalid, e: | ||
386 | 328 | msg = (_("Failed to reserve image. Got error: %(e)s") % locals()) | ||
387 | 329 | for line in msg.split('\n'): | ||
388 | 330 | logger.error(line) | ||
389 | 331 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") | ||
390 | 332 | except exception.Forbidden: | ||
391 | 333 | msg = _("Forbidden to reserve image.") | ||
392 | 334 | logger.error(msg) | ||
393 | 335 | raise HTTPForbidden(msg, request=req, content_type="text/plain") | ||
394 | 336 | |||
395 | 337 | def _upload(self, req, image_meta): | ||
396 | 338 | """ | ||
397 | 339 | Uploads the payload of the request to a backend store in | ||
398 | 340 | Glance. If the `x-image-meta-store` header is set, Glance | ||
399 | 341 | will attempt to use that store, if not, Glance will use the | ||
400 | 342 | store set by the flag `default_store`. | ||
401 | 343 | |||
402 | 344 | :param req: The WSGI/Webob Request object | ||
403 | 345 | :param image_meta: Mapping of metadata about image | ||
404 | 346 | |||
405 | 347 | :raises HTTPConflict if image already exists | ||
406 | 348 | :retval The location where the image was stored | ||
407 | 349 | """ | ||
408 | 350 | |||
409 | 351 | copy_from = self._copy_from(req) | ||
410 | 352 | if copy_from: | ||
411 | 353 | image_data, image_size = self._get_from_store(copy_from) | ||
412 | 354 | image_meta['size'] = image_size or image_meta['size'] | ||
413 | 355 | else: | ||
414 | 356 | try: | ||
415 | 357 | req.get_content_type('application/octet-stream') | ||
416 | 358 | except exception.InvalidContentType: | ||
417 | 359 | self._safe_kill(req, image_meta['id']) | ||
418 | 360 | msg = _("Content-Type must be application/octet-stream") | ||
419 | 361 | logger.error(msg) | ||
420 | 362 | raise HTTPBadRequest(explanation=msg) | ||
421 | 363 | |||
422 | 364 | image_data = req.body_file | ||
423 | 365 | |||
424 | 366 | if req.content_length: | ||
425 | 367 | image_size = int(req.content_length) | ||
426 | 368 | elif 'x-image-meta-size' in req.headers: | ||
427 | 369 | image_size = int(req.headers['x-image-meta-size']) | ||
428 | 370 | else: | ||
429 | 371 | logger.debug(_("Got request with no content-length and no " | ||
430 | 372 | "x-image-meta-size header")) | ||
431 | 373 | image_size = 0 | ||
432 | 374 | |||
433 | 375 | store_name = req.headers.get('x-image-meta-store', | ||
434 | 376 | self.conf.default_store) | ||
435 | 377 | |||
436 | 378 | store = self.get_store_or_400(req, store_name) | ||
437 | 379 | |||
438 | 380 | image_id = image_meta['id'] | ||
439 | 381 | logger.debug(_("Setting image %s to status 'saving'"), image_id) | ||
440 | 382 | registry.update_image_metadata(req.context, image_id, | ||
441 | 383 | {'status': 'saving'}) | ||
442 | 384 | try: | ||
443 | 385 | logger.debug(_("Uploading image data for image %(image_id)s " | ||
444 | 386 | "to %(store_name)s store"), locals()) | ||
445 | 387 | |||
446 | 388 | if image_size > IMAGE_SIZE_CAP: | ||
447 | 389 | max_image_size = IMAGE_SIZE_CAP | ||
448 | 390 | msg = _("Denying attempt to upload image larger than " | ||
449 | 391 | "%(max_image_size)d. Supplied image size was " | ||
450 | 392 | "%(image_size)d") % locals() | ||
451 | 393 | logger.warn(msg) | ||
452 | 394 | raise HTTPBadRequest(msg, request=req) | ||
453 | 395 | |||
454 | 396 | location, size, checksum = store.add(image_meta['id'], | ||
455 | 397 | image_data, | ||
456 | 398 | image_size) | ||
457 | 399 | |||
458 | 400 | # Verify any supplied checksum value matches checksum | ||
459 | 401 | # returned from store when adding image | ||
460 | 402 | supplied_checksum = image_meta.get('checksum') | ||
461 | 403 | if supplied_checksum and supplied_checksum != checksum: | ||
462 | 404 | msg = _("Supplied checksum (%(supplied_checksum)s) and " | ||
463 | 405 | "checksum generated from uploaded image " | ||
464 | 406 | "(%(checksum)s) did not match. Setting image " | ||
465 | 407 | "status to 'killed'.") % locals() | ||
466 | 408 | logger.error(msg) | ||
467 | 409 | self._safe_kill(req, image_id) | ||
468 | 410 | raise HTTPBadRequest(msg, content_type="text/plain", | ||
469 | 411 | request=req) | ||
470 | 412 | |||
471 | 413 | # Update the database with the checksum returned | ||
472 | 414 | # from the backend store | ||
473 | 415 | logger.debug(_("Updating image %(image_id)s data. " | ||
474 | 416 | "Checksum set to %(checksum)s, size set " | ||
475 | 417 | "to %(size)d"), locals()) | ||
476 | 418 | update_data = {'checksum': checksum, | ||
477 | 419 | 'size': size} | ||
478 | 420 | image_meta = registry.update_image_metadata(req.context, | ||
479 | 421 | image_id, | ||
480 | 422 | update_data) | ||
481 | 423 | self.notifier.info('image.upload', image_meta) | ||
482 | 424 | |||
483 | 425 | return location | ||
484 | 426 | |||
485 | 427 | except exception.Duplicate, e: | ||
486 | 428 | msg = _("Attempt to upload duplicate image: %s") % e | ||
487 | 429 | logger.error(msg) | ||
488 | 430 | self._safe_kill(req, image_id) | ||
489 | 431 | self.notifier.error('image.upload', msg) | ||
490 | 432 | raise HTTPConflict(msg, request=req) | ||
491 | 433 | |||
492 | 434 | except exception.Forbidden, e: | ||
493 | 435 | msg = _("Forbidden upload attempt: %s") % e | ||
494 | 436 | logger.error(msg) | ||
495 | 437 | self._safe_kill(req, image_id) | ||
496 | 438 | self.notifier.error('image.upload', msg) | ||
497 | 439 | raise HTTPForbidden(msg, request=req, content_type="text/plain") | ||
498 | 440 | |||
499 | 441 | except exception.StorageFull, e: | ||
500 | 442 | msg = _("Image storage media is full: %s") % e | ||
501 | 443 | logger.error(msg) | ||
502 | 444 | self._safe_kill(req, image_id) | ||
503 | 445 | self.notifier.error('image.upload', msg) | ||
504 | 446 | raise HTTPRequestEntityTooLarge(msg, request=req, | ||
505 | 447 | content_type='text/plain') | ||
506 | 448 | |||
507 | 449 | except exception.StorageWriteDenied, e: | ||
508 | 450 | msg = _("Insufficient permissions on image storage media: %s") % e | ||
509 | 451 | logger.error(msg) | ||
510 | 452 | self._safe_kill(req, image_id) | ||
511 | 453 | self.notifier.error('image.upload', msg) | ||
512 | 454 | raise HTTPServiceUnavailable(msg, request=req, | ||
513 | 455 | content_type='text/plain') | ||
514 | 456 | |||
515 | 457 | except HTTPError, e: | ||
516 | 458 | self._safe_kill(req, image_id) | ||
517 | 459 | self.notifier.error('image.upload', e.explanation) | ||
518 | 460 | raise | ||
519 | 461 | |||
520 | 462 | except Exception, e: | ||
521 | 463 | tb_info = traceback.format_exc() | ||
522 | 464 | logger.error(tb_info) | ||
523 | 465 | |||
524 | 466 | self._safe_kill(req, image_id) | ||
525 | 467 | |||
526 | 468 | msg = _("Error uploading image: (%(class_name)s): " | ||
527 | 469 | "%(exc)s") % ({'class_name': e.__class__.__name__, | ||
528 | 470 | 'exc': str(e)}) | ||
529 | 471 | |||
530 | 472 | self.notifier.error('image.upload', msg) | ||
531 | 473 | raise HTTPBadRequest(msg, request=req) | ||
532 | 474 | |||
533 | 475 | def _activate(self, req, image_id, location): | ||
534 | 476 | """ | ||
535 | 477 | Sets the image status to `active` and the image's location | ||
536 | 478 | attribute. | ||
537 | 479 | |||
538 | 480 | :param req: The WSGI/Webob Request object | ||
539 | 481 | :param image_id: Opaque image identifier | ||
540 | 482 | :param location: Location of where Glance stored this image | ||
541 | 483 | """ | ||
542 | 484 | image_meta = {} | ||
543 | 485 | image_meta['location'] = location | ||
544 | 486 | image_meta['status'] = 'active' | ||
545 | 487 | |||
546 | 488 | try: | ||
547 | 489 | return registry.update_image_metadata(req.context, | ||
548 | 490 | image_id, | ||
549 | 491 | image_meta) | ||
550 | 492 | except exception.Invalid, e: | ||
551 | 493 | msg = (_("Failed to activate image. Got error: %(e)s") | ||
552 | 494 | % locals()) | ||
553 | 495 | for line in msg.split('\n'): | ||
554 | 496 | logger.error(line) | ||
555 | 497 | self.notifier.error('image.update', msg) | ||
556 | 498 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") | ||
557 | 499 | |||
558 | 500 | def _kill(self, req, image_id): | ||
559 | 501 | """ | ||
560 | 502 | Marks the image status to `killed`. | ||
561 | 503 | |||
562 | 504 | :param req: The WSGI/Webob Request object | ||
563 | 505 | :param image_id: Opaque image identifier | ||
564 | 506 | """ | ||
565 | 507 | registry.update_image_metadata(req.context, image_id, | ||
566 | 508 | {'status': 'killed'}) | ||
567 | 509 | |||
568 | 510 | def _safe_kill(self, req, image_id): | ||
569 | 511 | """ | ||
570 | 512 | Mark image killed without raising exceptions if it fails. | ||
571 | 513 | |||
572 | 514 | Since _kill is meant to be called from exceptions handlers, it should | ||
573 | 515 | not raise itself, rather it should just log its error. | ||
574 | 516 | |||
575 | 517 | :param req: The WSGI/Webob Request object | ||
576 | 518 | :param image_id: Opaque image identifier | ||
577 | 519 | """ | ||
578 | 520 | try: | ||
579 | 521 | self._kill(req, image_id) | ||
580 | 522 | except Exception, e: | ||
581 | 523 | logger.error(_("Unable to kill image %(id)s: " | ||
582 | 524 | "%(exc)s") % ({'id': image_id, | ||
583 | 525 | 'exc': repr(e)})) | ||
584 | 526 | |||
585 | 527 | def _upload_and_activate(self, req, image_meta): | ||
586 | 528 | """ | ||
587 | 529 | Safely uploads the image data in the request payload | ||
588 | 530 | and activates the image in the registry after a successful | ||
589 | 531 | upload. | ||
590 | 532 | |||
591 | 533 | :param req: The WSGI/Webob Request object | ||
592 | 534 | :param image_meta: Mapping of metadata about image | ||
593 | 535 | |||
594 | 536 | :retval Mapping of updated image data | ||
595 | 537 | """ | ||
596 | 538 | image_id = image_meta['id'] | ||
597 | 539 | # This is necessary because of a bug in Webob 1.0.2 - 1.0.7 | ||
598 | 540 | # See: https://bitbucket.org/ianb/webob/ | ||
599 | 541 | # issue/12/fix-for-issue-6-broke-chunked-transfer | ||
600 | 542 | req.is_body_readable = True | ||
601 | 543 | location = self._upload(req, image_meta) | ||
602 | 544 | return self._activate(req, image_id, location) | ||
603 | 545 | |||
604 | 546 | def _get_size(self, image_meta, location): | ||
605 | 547 | # retrieve the image size from remote store (if not provided) | ||
606 | 548 | return image_meta.get('size', 0) or get_size_from_backend(location) | ||
607 | 549 | |||
608 | 550 | def _handle_source(self, req, image_id, image_meta, image_data): | ||
609 | 551 | if image_data or self._copy_from(req): | ||
610 | 552 | image_meta = self._upload_and_activate(req, image_meta) | ||
611 | 553 | else: | ||
612 | 554 | location = image_meta.get('location') | ||
613 | 555 | if location: | ||
614 | 556 | image_meta = self._activate(req, image_id, location) | ||
615 | 557 | return image_meta | ||
616 | 558 | |||
617 | 559 | def create(self, req, image_meta, image_data): | ||
618 | 560 | """ | ||
619 | 561 | Adds a new image to Glance. Four scenarios exist when creating an | ||
620 | 562 | image: | ||
621 | 563 | |||
622 | 564 | 1. If the image data is available directly for upload, create can be | ||
623 | 565 | passed the image data as the request body and the metadata as the | ||
624 | 566 | request headers. The image will initially be 'queued', during | ||
625 | 567 | upload it will be in the 'saving' status, and then 'killed' or | ||
626 | 568 | 'active' depending on whether the upload completed successfully. | ||
627 | 569 | |||
628 | 570 | 2. If the image data exists somewhere else, you can upload indirectly | ||
629 | 571 | from the external source using the x-glance-api-copy-from header. | ||
630 | 572 | Once the image is uploaded, the external store is not subsequently | ||
631 | 573 | consulted, i.e. the image content is served out from the configured | ||
632 | 574 | glance image store. State transitions are as for option #1. | ||
633 | 575 | |||
634 | 576 | 3. If the image data exists somewhere else, you can reference the | ||
635 | 577 | source using the x-image-meta-location header. The image content | ||
636 | 578 | will be served out from the external store, i.e. is never uploaded | ||
637 | 579 | to the configured glance image store. | ||
638 | 580 | |||
639 | 581 | 4. If the image data is not available yet, but you'd like reserve a | ||
640 | 582 | spot for it, you can omit the data and a record will be created in | ||
641 | 583 | the 'queued' state. This exists primarily to maintain backwards | ||
642 | 584 | compatibility with OpenStack/Rackspace API semantics. | ||
643 | 585 | |||
644 | 586 | The request body *must* be encoded as application/octet-stream, | ||
645 | 587 | otherwise an HTTPBadRequest is returned. | ||
646 | 588 | |||
647 | 589 | Upon a successful save of the image data and metadata, a response | ||
648 | 590 | containing metadata about the image is returned, including its | ||
649 | 591 | opaque identifier. | ||
650 | 592 | |||
651 | 593 | :param req: The WSGI/Webob Request object | ||
652 | 594 | :param image_meta: Mapping of metadata about image | ||
653 | 595 | :param image_data: Actual image data that is to be stored | ||
654 | 596 | |||
655 | 597 | :raises HTTPBadRequest if x-image-meta-location is missing | ||
656 | 598 | and the request body is not application/octet-stream | ||
657 | 599 | image data. | ||
658 | 600 | """ | ||
659 | 601 | self._enforce(req, 'add_image') | ||
660 | 602 | if image_meta.get('is_public'): | ||
661 | 603 | self._enforce(req, 'publicize_image') | ||
662 | 604 | if req.context.read_only: | ||
663 | 605 | msg = _("Read-only access") | ||
664 | 606 | logger.debug(msg) | ||
665 | 607 | raise HTTPForbidden(msg, request=req, | ||
666 | 608 | content_type="text/plain") | ||
667 | 609 | |||
668 | 610 | image_meta = self._reserve(req, image_meta) | ||
669 | 611 | id = image_meta['id'] | ||
670 | 612 | |||
671 | 613 | image_meta = self._handle_source(req, id, image_meta, image_data) | ||
672 | 614 | |||
673 | 615 | # Prevent client from learning the location, as it | ||
674 | 616 | # could contain security credentials | ||
675 | 617 | image_meta.pop('location', None) | ||
676 | 618 | |||
677 | 619 | return {'image_meta': image_meta} | ||
678 | 620 | |||
679 | 621 | def update(self, req, id, image_meta, image_data): | ||
680 | 622 | """ | ||
681 | 623 | Updates an existing image with the registry. | ||
682 | 624 | |||
683 | 625 | :param request: The WSGI/Webob Request object | ||
684 | 626 | :param id: The opaque image identifier | ||
685 | 627 | |||
686 | 628 | :retval Returns the updated image information as a mapping | ||
687 | 629 | """ | ||
688 | 630 | self._enforce(req, 'modify_image') | ||
689 | 631 | if image_meta.get('is_public'): | ||
690 | 632 | self._enforce(req, 'publicize_image') | ||
691 | 633 | if req.context.read_only: | ||
692 | 634 | msg = _("Read-only access") | ||
693 | 635 | logger.debug(msg) | ||
694 | 636 | raise HTTPForbidden(msg, request=req, | ||
695 | 637 | content_type="text/plain") | ||
696 | 638 | |||
697 | 639 | orig_image_meta = self.get_image_meta_or_404(req, id) | ||
698 | 640 | orig_status = orig_image_meta['status'] | ||
699 | 641 | |||
700 | 642 | # The default behaviour for a PUT /images/<IMAGE_ID> is to | ||
701 | 643 | # override any properties that were previously set. This, however, | ||
702 | 644 | # leads to a number of issues for the common use case where a caller | ||
703 | 645 | # registers an image with some properties and then almost immediately | ||
704 | 646 | # uploads an image file along with some more properties. Here, we | ||
705 | 647 | # check for a special header value to be false in order to force | ||
706 | 648 | # properties NOT to be purged. However we also disable purging of | ||
707 | 649 | # properties if an image file is being uploaded... | ||
708 | 650 | purge_props = req.headers.get('x-glance-registry-purge-props', True) | ||
709 | 651 | purge_props = (utils.bool_from_string(purge_props) and | ||
710 | 652 | image_data is None) | ||
711 | 653 | |||
712 | 654 | if image_data is not None and orig_status != 'queued': | ||
713 | 655 | raise HTTPConflict(_("Cannot upload to an unqueued image")) | ||
714 | 656 | |||
715 | 657 | # Only allow the Location|Copy-From fields to be modified if the | ||
716 | 658 | # image is in queued status, which indicates that the user called | ||
717 | 659 | # POST /images but originally supply neither a Location|Copy-From | ||
718 | 660 | # field NOR image data | ||
719 | 661 | location = self._external_source(image_meta, req) | ||
720 | 662 | reactivating = orig_status != 'queued' and location | ||
721 | 663 | activating = orig_status == 'queued' and (location or image_data) | ||
722 | 664 | |||
723 | 665 | if reactivating: | ||
724 | 666 | msg = _("Attempted to update Location field for an image " | ||
725 | 667 | "not in queued status.") | ||
726 | 668 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") | ||
727 | 669 | |||
728 | 670 | try: | ||
729 | 671 | if location: | ||
730 | 672 | image_meta['size'] = self._get_size(image_meta, location) | ||
731 | 673 | |||
732 | 674 | image_meta = registry.update_image_metadata(req.context, | ||
733 | 675 | id, | ||
734 | 676 | image_meta, | ||
735 | 677 | purge_props) | ||
736 | 678 | |||
737 | 679 | if activating: | ||
738 | 680 | image_meta = self._handle_source(req, id, image_meta, | ||
739 | 681 | image_data) | ||
740 | 682 | except exception.Invalid, e: | ||
741 | 683 | msg = (_("Failed to update image metadata. Got error: %(e)s") | ||
742 | 684 | % locals()) | ||
743 | 685 | for line in msg.split('\n'): | ||
744 | 686 | logger.error(line) | ||
745 | 687 | self.notifier.error('image.update', msg) | ||
746 | 688 | raise HTTPBadRequest(msg, request=req, content_type="text/plain") | ||
747 | 689 | except exception.NotFound, e: | ||
748 | 690 | msg = ("Failed to find image to update: %(e)s" % locals()) | ||
749 | 691 | for line in msg.split('\n'): | ||
750 | 692 | logger.info(line) | ||
751 | 693 | self.notifier.info('image.update', msg) | ||
752 | 694 | raise HTTPNotFound(msg, request=req, content_type="text/plain") | ||
753 | 695 | except exception.Forbidden, e: | ||
754 | 696 | msg = ("Forbidden to update image: %(e)s" % locals()) | ||
755 | 697 | for line in msg.split('\n'): | ||
756 | 698 | logger.info(line) | ||
757 | 699 | self.notifier.info('image.update', msg) | ||
758 | 700 | raise HTTPForbidden(msg, request=req, content_type="text/plain") | ||
759 | 701 | else: | ||
760 | 702 | self.notifier.info('image.update', image_meta) | ||
761 | 703 | |||
762 | 704 | # Prevent client from learning the location, as it | ||
763 | 705 | # could contain security credentials | ||
764 | 706 | image_meta.pop('location', None) | ||
765 | 707 | |||
766 | 708 | return {'image_meta': image_meta} | ||
767 | 709 | |||
768 | 710 | def delete(self, req, id): | ||
769 | 711 | """ | ||
770 | 712 | Deletes the image and all its chunks from the Glance | ||
771 | 713 | |||
772 | 714 | :param req: The WSGI/Webob Request object | ||
773 | 715 | :param id: The opaque image identifier | ||
774 | 716 | |||
775 | 717 | :raises HttpBadRequest if image registry is invalid | ||
776 | 718 | :raises HttpNotFound if image or any chunk is not available | ||
777 | 719 | :raises HttpUnauthorized if image or any chunk is not | ||
778 | 720 | deleteable by the requesting user | ||
779 | 721 | """ | ||
780 | 722 | self._enforce(req, 'delete_image') | ||
781 | 723 | if req.context.read_only: | ||
782 | 724 | msg = _("Read-only access") | ||
783 | 725 | logger.debug(msg) | ||
784 | 726 | raise HTTPForbidden(msg, request=req, | ||
785 | 727 | content_type="text/plain") | ||
786 | 728 | |||
787 | 729 | image = self.get_image_meta_or_404(req, id) | ||
788 | 730 | if image['protected']: | ||
789 | 731 | msg = _("Image is protected") | ||
790 | 732 | logger.debug(msg) | ||
791 | 733 | raise HTTPForbidden(msg, request=req, | ||
792 | 734 | content_type="text/plain") | ||
793 | 735 | |||
794 | 736 | # The image's location field may be None in the case | ||
795 | 737 | # of a saving or queued image, therefore don't ask a backend | ||
796 | 738 | # to delete the image if the backend doesn't yet store it. | ||
797 | 739 | # See https://bugs.launchpad.net/glance/+bug/747799 | ||
798 | 740 | try: | ||
799 | 741 | if image['location']: | ||
800 | 742 | schedule_delete_from_backend(image['location'], self.conf, | ||
801 | 743 | req.context, id) | ||
802 | 744 | registry.delete_image_metadata(req.context, id) | ||
803 | 745 | except exception.NotFound, e: | ||
804 | 746 | msg = ("Failed to find image to delete: %(e)s" % locals()) | ||
805 | 747 | for line in msg.split('\n'): | ||
806 | 748 | logger.info(line) | ||
807 | 749 | self.notifier.info('image.delete', msg) | ||
808 | 750 | raise HTTPNotFound(msg, request=req, content_type="text/plain") | ||
809 | 751 | except exception.Forbidden, e: | ||
810 | 752 | msg = ("Forbidden to delete image: %(e)s" % locals()) | ||
811 | 753 | for line in msg.split('\n'): | ||
812 | 754 | logger.info(line) | ||
813 | 755 | self.notifier.info('image.delete', msg) | ||
814 | 756 | raise HTTPForbidden(msg, request=req, content_type="text/plain") | ||
815 | 757 | else: | ||
816 | 758 | self.notifier.info('image.delete', id) | ||
817 | 759 | |||
818 | 760 | def get_store_or_400(self, request, store_name): | ||
819 | 761 | """ | ||
820 | 762 | Grabs the storage backend for the supplied store name | ||
821 | 763 | or raises an HTTPBadRequest (400) response | ||
822 | 764 | |||
823 | 765 | :param request: The WSGI/Webob Request object | ||
824 | 766 | :param store_name: The backend store name | ||
825 | 767 | |||
826 | 768 | :raises HTTPNotFound if store does not exist | ||
827 | 769 | """ | ||
828 | 770 | try: | ||
829 | 771 | return get_store_from_scheme(store_name) | ||
830 | 772 | except exception.UnknownScheme: | ||
831 | 773 | msg = (_("Requested store %s not available on this Glance server") | ||
832 | 774 | % store_name) | ||
833 | 775 | logger.error(msg) | ||
834 | 776 | raise HTTPBadRequest(msg, request=request, | ||
835 | 777 | content_type='text/plain') | ||
836 | 778 | |||
837 | 779 | def verify_store_or_exit(self, store_name): | ||
838 | 780 | """ | ||
839 | 781 | Verifies availability of the storage backend for the | ||
840 | 782 | given store name or exits | ||
841 | 783 | |||
842 | 784 | :param store_name: The backend store name | ||
843 | 785 | """ | ||
844 | 786 | try: | ||
845 | 787 | get_store_from_scheme(store_name) | ||
846 | 788 | except exception.UnknownScheme: | ||
847 | 789 | msg = (_("Default store %s not available on this Glance server\n") | ||
848 | 790 | % store_name) | ||
849 | 791 | logger.error(msg) | ||
850 | 792 | # message on stderr will only be visible if started directly via | ||
851 | 793 | # bin/glance-api, as opposed to being daemonized by glance-control | ||
852 | 794 | sys.stderr.write(msg) | ||
853 | 795 | sys.exit(255) | ||
854 | 796 | |||
855 | 797 | |||
856 | 798 | class ImageDeserializer(wsgi.JSONRequestDeserializer): | ||
857 | 799 | """Handles deserialization of specific controller method requests.""" | ||
858 | 800 | |||
859 | 801 | def _deserialize(self, request): | ||
860 | 802 | result = {} | ||
861 | 803 | try: | ||
862 | 804 | result['image_meta'] = utils.get_image_meta_from_headers(request) | ||
863 | 805 | except exception.Invalid: | ||
864 | 806 | image_size_str = request.headers['x-image-meta-size'] | ||
865 | 807 | msg = _("Incoming image size of %s was not convertible to " | ||
866 | 808 | "an integer.") % image_size_str | ||
867 | 809 | raise HTTPBadRequest(msg, request=request) | ||
868 | 810 | |||
869 | 811 | image_meta = result['image_meta'] | ||
870 | 812 | if 'size' in image_meta: | ||
871 | 813 | incoming_image_size = image_meta['size'] | ||
872 | 814 | if incoming_image_size > IMAGE_SIZE_CAP: | ||
873 | 815 | max_image_size = IMAGE_SIZE_CAP | ||
874 | 816 | msg = _("Denying attempt to upload image larger than " | ||
875 | 817 | "%(max_image_size)d. Supplied image size was " | ||
876 | 818 | "%(incoming_image_size)d") % locals() | ||
877 | 819 | logger.warn(msg) | ||
878 | 820 | raise HTTPBadRequest(msg, request=request) | ||
879 | 821 | |||
880 | 822 | data = request.body_file if self.has_body(request) else None | ||
881 | 823 | result['image_data'] = data | ||
882 | 824 | return result | ||
883 | 825 | |||
884 | 826 | def create(self, request): | ||
885 | 827 | return self._deserialize(request) | ||
886 | 828 | |||
887 | 829 | def update(self, request): | ||
888 | 830 | return self._deserialize(request) | ||
889 | 831 | |||
890 | 832 | |||
891 | 833 | class ImageSerializer(wsgi.JSONResponseSerializer): | ||
892 | 834 | """Handles serialization of specific controller method responses.""" | ||
893 | 835 | |||
894 | 836 | def __init__(self, conf): | ||
895 | 837 | self.conf = conf | ||
896 | 838 | self.notifier = notifier.Notifier(conf) | ||
897 | 839 | |||
898 | 840 | def _inject_location_header(self, response, image_meta): | ||
899 | 841 | location = self._get_image_location(image_meta) | ||
900 | 842 | response.headers['Location'] = location | ||
901 | 843 | |||
902 | 844 | def _inject_checksum_header(self, response, image_meta): | ||
903 | 845 | response.headers['ETag'] = image_meta['checksum'] | ||
904 | 846 | |||
905 | 847 | def _inject_image_meta_headers(self, response, image_meta): | ||
906 | 848 | """ | ||
907 | 849 | Given a response and mapping of image metadata, injects | ||
908 | 850 | the Response with a set of HTTP headers for the image | ||
909 | 851 | metadata. Each main image metadata field is injected | ||
910 | 852 | as a HTTP header with key 'x-image-meta-<FIELD>' except | ||
911 | 853 | for the properties field, which is further broken out | ||
912 | 854 | into a set of 'x-image-meta-property-<KEY>' headers | ||
913 | 855 | |||
914 | 856 | :param response: The Webob Response object | ||
915 | 857 | :param image_meta: Mapping of image metadata | ||
916 | 858 | """ | ||
917 | 859 | headers = utils.image_meta_to_http_headers(image_meta) | ||
918 | 860 | |||
919 | 861 | for k, v in headers.items(): | ||
920 | 862 | response.headers[k] = v | ||
921 | 863 | |||
922 | 864 | def _get_image_location(self, image_meta): | ||
923 | 865 | """Build a relative url to reach the image defined by image_meta.""" | ||
924 | 866 | return "/v1/images/%s" % image_meta['id'] | ||
925 | 867 | |||
926 | 868 | def meta(self, response, result): | ||
927 | 869 | image_meta = result['image_meta'] | ||
928 | 870 | self._inject_image_meta_headers(response, image_meta) | ||
929 | 871 | self._inject_location_header(response, image_meta) | ||
930 | 872 | self._inject_checksum_header(response, image_meta) | ||
931 | 873 | return response | ||
932 | 874 | |||
933 | 875 | def image_send_notification(self, bytes_written, expected_size, | ||
934 | 876 | image_meta, request): | ||
935 | 877 | """Send an image.send message to the notifier.""" | ||
936 | 878 | try: | ||
937 | 879 | context = request.context | ||
938 | 880 | payload = { | ||
939 | 881 | 'bytes_sent': bytes_written, | ||
940 | 882 | 'image_id': image_meta['id'], | ||
941 | 883 | 'owner_id': image_meta['owner'], | ||
942 | 884 | 'receiver_tenant_id': context.tenant, | ||
943 | 885 | 'receiver_user_id': context.user, | ||
944 | 886 | 'destination_ip': request.remote_addr, | ||
945 | 887 | } | ||
946 | 888 | if bytes_written != expected_size: | ||
947 | 889 | self.notifier.error('image.send', payload) | ||
948 | 890 | else: | ||
949 | 891 | self.notifier.info('image.send', payload) | ||
950 | 892 | except Exception, err: | ||
951 | 893 | msg = _("An error occurred during image.send" | ||
952 | 894 | " notification: %(err)s") % locals() | ||
953 | 895 | logger.error(msg) | ||
954 | 896 | |||
955 | 897 | def show(self, response, result): | ||
956 | 898 | image_meta = result['image_meta'] | ||
957 | 899 | image_id = image_meta['id'] | ||
958 | 900 | |||
959 | 901 | # We use a secondary iterator here to wrap the | ||
960 | 902 | # iterator coming back from the store driver in | ||
961 | 903 | # order to check for disconnections from the backend | ||
962 | 904 | # storage connections and log an error if the size of | ||
963 | 905 | # the transferred image is not the same as the expected | ||
964 | 906 | # size of the image file. See LP Bug #882585. | ||
965 | 907 | def checked_iter(image_id, expected_size, image_iter): | ||
966 | 908 | bytes_written = 0 | ||
967 | 909 | |||
968 | 910 | def notify_image_sent_hook(env): | ||
969 | 911 | self.image_send_notification(bytes_written, expected_size, | ||
970 | 912 | image_meta, response.request) | ||
971 | 913 | |||
972 | 914 | # Add hook to process after response is fully sent | ||
973 | 915 | if 'eventlet.posthooks' in response.request.environ: | ||
974 | 916 | response.request.environ['eventlet.posthooks'].append( | ||
975 | 917 | (notify_image_sent_hook, (), {})) | ||
976 | 918 | |||
977 | 919 | try: | ||
978 | 920 | for chunk in image_iter: | ||
979 | 921 | yield chunk | ||
980 | 922 | bytes_written += len(chunk) | ||
981 | 923 | except Exception, err: | ||
982 | 924 | msg = _("An error occurred reading from backend storage " | ||
983 | 925 | "for image %(image_id): %(err)s") % locals() | ||
984 | 926 | logger.error(msg) | ||
985 | 927 | raise | ||
986 | 928 | |||
987 | 929 | if expected_size != bytes_written: | ||
988 | 930 | msg = _("Backend storage for image %(image_id)s " | ||
989 | 931 | "disconnected after writing only %(bytes_written)d " | ||
990 | 932 | "bytes") % locals() | ||
991 | 933 | logger.error(msg) | ||
992 | 934 | raise IOError(errno.EPIPE, _("Corrupt image download for " | ||
993 | 935 | "image %(image_id)s") % locals()) | ||
994 | 936 | |||
995 | 937 | image_iter = result['image_iterator'] | ||
996 | 938 | # image_meta['size'] is a str | ||
997 | 939 | expected_size = int(image_meta['size']) | ||
998 | 940 | response.app_iter = checked_iter(image_id, expected_size, image_iter) | ||
999 | 941 | # Using app_iter blanks content-length, so we set it here... | ||
1000 | 942 | response.headers['Content-Length'] = image_meta['size'] | ||
1001 | 943 | response.headers['Content-Type'] = 'application/octet-stream' | ||
1002 | 944 | |||
1003 | 945 | self._inject_image_meta_headers(response, image_meta) | ||
1004 | 946 | self._inject_location_header(response, image_meta) | ||
1005 | 947 | self._inject_checksum_header(response, image_meta) | ||
1006 | 948 | |||
1007 | 949 | return response | ||
1008 | 950 | |||
1009 | 951 | def update(self, response, result): | ||
1010 | 952 | image_meta = result['image_meta'] | ||
1011 | 953 | response.body = self.to_json(dict(image=image_meta)) | ||
1012 | 954 | response.headers['Content-Type'] = 'application/json' | ||
1013 | 955 | self._inject_location_header(response, image_meta) | ||
1014 | 956 | self._inject_checksum_header(response, image_meta) | ||
1015 | 957 | return response | ||
1016 | 958 | |||
1017 | 959 | def create(self, response, result): | ||
1018 | 960 | image_meta = result['image_meta'] | ||
1019 | 961 | response.status = 201 | ||
1020 | 962 | response.headers['Content-Type'] = 'application/json' | ||
1021 | 963 | response.body = self.to_json(dict(image=image_meta)) | ||
1022 | 964 | self._inject_location_header(response, image_meta) | ||
1023 | 965 | self._inject_checksum_header(response, image_meta) | ||
1024 | 966 | return response | ||
1025 | 967 | |||
1026 | 968 | |||
1027 | 969 | def create_resource(conf): | ||
1028 | 970 | """Images resource factory method""" | ||
1029 | 971 | deserializer = ImageDeserializer() | ||
1030 | 972 | serializer = ImageSerializer(conf) | ||
1031 | 973 | return wsgi.Resource(Controller(conf), deserializer, serializer) | ||
1032 | 974 | 0 | ||
1033 | === modified file '.pc/applied-patches' | |||
1034 | --- .pc/applied-patches 2012-11-08 07:19:39 +0000 | |||
1035 | +++ .pc/applied-patches 2012-12-18 14:12:30 +0000 | |||
1036 | @@ -3,4 +3,3 @@ | |||
1037 | 3 | disable-network-for-docs.patch | 3 | disable-network-for-docs.patch |
1038 | 4 | disable_db_table_auto_create.patch | 4 | disable_db_table_auto_create.patch |
1039 | 5 | fix_migration_012_foreign_keys.patch | 5 | fix_migration_012_foreign_keys.patch |
1040 | 6 | CVE-2012-4573.patch | ||
1041 | 7 | 6 | ||
1042 | === modified file '.pc/fix_migration_012_foreign_keys.patch/Authors' | |||
1043 | --- .pc/fix_migration_012_foreign_keys.patch/Authors 2012-06-24 03:14:33 +0000 | |||
1044 | +++ .pc/fix_migration_012_foreign_keys.patch/Authors 2012-12-18 14:12:30 +0000 | |||
1045 | @@ -54,6 +54,7 @@ | |||
1046 | 54 | Rick Harris <rconradharris@gmail.com> | 54 | Rick Harris <rconradharris@gmail.com> |
1047 | 55 | Reynolds Chin <benzwt@gmail.com> | 55 | Reynolds Chin <benzwt@gmail.com> |
1048 | 56 | Russell Bryant <rbryant@redhat.com> | 56 | Russell Bryant <rbryant@redhat.com> |
1049 | 57 | Sean Dague <sdague@linux.vnet.ibm.com> | ||
1050 | 57 | Soren Hansen <soren.hansen@rackspace.com> | 58 | Soren Hansen <soren.hansen@rackspace.com> |
1051 | 58 | Stuart McLaren <stuart.mclaren@hp.com> | 59 | Stuart McLaren <stuart.mclaren@hp.com> |
1052 | 59 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> | 60 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> |
1053 | 60 | 61 | ||
1054 | === modified file 'Authors' | |||
1055 | --- Authors 2012-06-24 03:14:33 +0000 | |||
1056 | +++ Authors 2012-12-18 14:12:30 +0000 | |||
1057 | @@ -55,6 +55,7 @@ | |||
1058 | 55 | Reynolds Chin <benzwt@gmail.com> | 55 | Reynolds Chin <benzwt@gmail.com> |
1059 | 56 | Russell Bryant <rbryant@redhat.com> | 56 | Russell Bryant <rbryant@redhat.com> |
1060 | 57 | Sam Morrison <sorrison@gmail.com> | 57 | Sam Morrison <sorrison@gmail.com> |
1061 | 58 | Sean Dague <sdague@linux.vnet.ibm.com> | ||
1062 | 58 | Soren Hansen <soren.hansen@rackspace.com> | 59 | Soren Hansen <soren.hansen@rackspace.com> |
1063 | 59 | Stuart McLaren <stuart.mclaren@hp.com> | 60 | Stuart McLaren <stuart.mclaren@hp.com> |
1064 | 60 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> | 61 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> |
1065 | 61 | 62 | ||
1066 | === added file 'PKG-INFO' | |||
1067 | --- PKG-INFO 1970-01-01 00:00:00 +0000 | |||
1068 | +++ PKG-INFO 2012-12-18 14:12:30 +0000 | |||
1069 | @@ -0,0 +1,15 @@ | |||
1070 | 1 | Metadata-Version: 1.1 | ||
1071 | 2 | Name: glance | ||
1072 | 3 | Version: 2012.1.3 | ||
1073 | 4 | Summary: The Glance project provides services for discovering, registering, and retrieving virtual machine images | ||
1074 | 5 | Home-page: http://glance.openstack.org/ | ||
1075 | 6 | Author: OpenStack | ||
1076 | 7 | Author-email: openstack@lists.launchpad.net | ||
1077 | 8 | License: Apache License (2.0) | ||
1078 | 9 | Description: UNKNOWN | ||
1079 | 10 | Platform: UNKNOWN | ||
1080 | 11 | Classifier: Development Status :: 4 - Beta | ||
1081 | 12 | Classifier: License :: OSI Approved :: Apache Software License | ||
1082 | 13 | Classifier: Operating System :: POSIX :: Linux | ||
1083 | 14 | Classifier: Programming Language :: Python :: 2.6 | ||
1084 | 15 | Classifier: Environment :: No Input/Output (Daemon) | ||
1085 | 0 | 16 | ||
1086 | === modified file 'debian/changelog' | |||
1087 | --- debian/changelog 2012-11-08 07:19:39 +0000 | |||
1088 | +++ debian/changelog 2012-12-18 14:12:30 +0000 | |||
1089 | @@ -1,3 +1,21 @@ | |||
1090 | 1 | glance (2012.1.4+stable-20121217-efd7e75b-0ubuntu1) precise-proposed; urgency=low | ||
1091 | 2 | |||
1092 | 3 | [ Adam Gandelman ] | ||
1093 | 4 | * debian/glance-{registry, api}.logrotate: Fix incorrect logfile | ||
1094 | 5 | locations. (LP: #1049314) | ||
1095 | 6 | |||
1096 | 7 | [ Yolanda Robla Mota ] | ||
1097 | 8 | * Resynchronize with stable/essex (efd7e75b): | ||
1098 | 9 | - [efd7e75] Non-admin users can cause public glance images to be deleted | ||
1099 | 10 | from the backend storage repository (CVE-2012-4573) | ||
1100 | 11 | - [e6be061] Jenkins jobs fail because of incompatibility between sqlalchemy- | ||
1101 | 12 | migrate and the newest sqlalchemy-0.8.0b1 (LP: #1073569) | ||
1102 | 13 | |||
1103 | 14 | * Dropped patches, superseeded by snapshot: | ||
1104 | 15 | - debian/patches/CVE-2012-4573.patch: [efd7e75] | ||
1105 | 16 | |||
1106 | 17 | -- Yolanda Robla Mota <yolanda.robla@canonical.com> Mon, 17 Dec 2012 10:56:46 +0000 | ||
1107 | 18 | |||
1108 | 1 | glance (2012.1.3+stable~20120821-120fcf-0ubuntu1.2) precise-security; urgency=low | 19 | glance (2012.1.3+stable~20120821-120fcf-0ubuntu1.2) precise-security; urgency=low |
1109 | 2 | 20 | ||
1110 | 3 | * SECURITY UPDATE: deletion of arbitrary public and shared images via | 21 | * SECURITY UPDATE: deletion of arbitrary public and shared images via |
1111 | 4 | 22 | ||
1112 | === modified file 'debian/glance-api.logrotate' | |||
1113 | --- debian/glance-api.logrotate 2012-01-13 10:50:40 +0000 | |||
1114 | +++ debian/glance-api.logrotate 2012-12-18 14:12:30 +0000 | |||
1115 | @@ -1,4 +1,4 @@ | |||
1117 | 1 | /var/log/glance/glance-api.log { | 1 | /var/log/glance/api.log { |
1118 | 2 | daily | 2 | daily |
1119 | 3 | missingok | 3 | missingok |
1120 | 4 | compress | 4 | compress |
1121 | 5 | 5 | ||
1122 | === modified file 'debian/glance-registry.logrotate' | |||
1123 | --- debian/glance-registry.logrotate 2012-01-13 10:50:40 +0000 | |||
1124 | +++ debian/glance-registry.logrotate 2012-12-18 14:12:30 +0000 | |||
1125 | @@ -1,4 +1,4 @@ | |||
1127 | 1 | /var/log/glance/glance-api.log { | 1 | /var/log/glance/registry.log { |
1128 | 2 | daily | 2 | daily |
1129 | 3 | missingok | 3 | missingok |
1130 | 4 | compress | 4 | compress |
1131 | 5 | 5 | ||
1132 | === removed file 'debian/patches/CVE-2012-4573.patch' | |||
1133 | --- debian/patches/CVE-2012-4573.patch 2012-11-08 07:19:39 +0000 | |||
1134 | +++ debian/patches/CVE-2012-4573.patch 1970-01-01 00:00:00 +0000 | |||
1135 | @@ -1,35 +0,0 @@ | |||
1136 | 1 | From efd7e75b1f419a52c7103c7840e24af8e5deb29d Mon Sep 17 00:00:00 2001 | ||
1137 | 2 | From: Brian Waldon <bcwaldon@gmail.com> | ||
1138 | 3 | Date: Wed, 7 Nov 2012 10:06:43 -0500 | ||
1139 | 4 | Subject: [PATCH] Ensure image owned by user before delayed_deletion | ||
1140 | 5 | |||
1141 | 6 | Fixes bug 1065187. | ||
1142 | 7 | |||
1143 | 8 | Change-Id: Icf2f117a094c712bad645ef5f297e9f7da994c84 | ||
1144 | 9 | --- | ||
1145 | 10 | glance/api/v1/images.py | 9 +++++++++ | ||
1146 | 11 | 1 file changed, 9 insertions(+) | ||
1147 | 12 | |||
1148 | 13 | diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py | ||
1149 | 14 | index 9bedf20..1a8eac8 100644 | ||
1150 | 15 | --- a/glance/api/v1/images.py | ||
1151 | 16 | +++ b/glance/api/v1/images.py | ||
1152 | 17 | @@ -727,6 +727,15 @@ class Controller(controller.BaseController): | ||
1153 | 18 | content_type="text/plain") | ||
1154 | 19 | |||
1155 | 20 | image = self.get_image_meta_or_404(req, id) | ||
1156 | 21 | + | ||
1157 | 22 | + if not (req.context.is_admin | ||
1158 | 23 | + or image['owner'] == None | ||
1159 | 24 | + or image['owner'] == req.context.owner): | ||
1160 | 25 | + msg = _("Unable to delete image you do not own") | ||
1161 | 26 | + logger.debug(msg) | ||
1162 | 27 | + raise HTTPForbidden(msg, request=req, | ||
1163 | 28 | + content_type="text/plain") | ||
1164 | 29 | + | ||
1165 | 30 | if image['protected']: | ||
1166 | 31 | msg = _("Image is protected") | ||
1167 | 32 | logger.debug(msg) | ||
1168 | 33 | -- | ||
1169 | 34 | 1.7.9.5 | ||
1170 | 35 | |||
1171 | 36 | 0 | ||
1172 | === modified file 'debian/patches/fix_migration_012_foreign_keys.patch' | |||
1173 | --- debian/patches/fix_migration_012_foreign_keys.patch 2012-04-12 15:02:08 +0000 | |||
1174 | +++ debian/patches/fix_migration_012_foreign_keys.patch 2012-12-18 14:12:30 +0000 | |||
1175 | @@ -1,5 +1,3 @@ | |||
1176 | 1 | From: 4e35808f94db8c17a20608e137e2940195821fac | ||
1177 | 2 | Author: Sam Morrison <sorrison@gmail.com> | ||
1178 | 3 | Date: Mon Apr 2 11:22:10 2012 +1000 | 1 | Date: Mon Apr 2 11:22:10 2012 +1000 |
1179 | 4 | Bug: https://bugs.launchpad.net/glance/+bug/976908 | 2 | Bug: https://bugs.launchpad.net/glance/+bug/976908 |
1180 | 5 | Origin, upstream: https://review.openstack.org/#change,6061 | 3 | Origin, upstream: https://review.openstack.org/#change,6061 |
1181 | @@ -8,25 +6,21 @@ | |||
1182 | 8 | 6 | ||
1183 | 9 | * fix bug 976908 | 7 | * fix bug 976908 |
1184 | 10 | 8 | ||
1193 | 11 | Change-Id: I0248f825396d08688238e6d2ef37c8fcb49e8c9d | 9 | Change-Id: I0248f825396d08688238e6d2ef37c8fcb49e8c9 |
1194 | 12 | 10 | diff -Naurp glance-2012.1.3.orig/Authors glance-2012.1.3/Authors | |
1195 | 13 | 11 | --- glance-2012.1.3.orig/Authors 2012-11-26 15:19:40.000000000 -0600 | |
1196 | 14 | diff --git a/Authors b/Authors | 12 | +++ glance-2012.1.3/Authors 2012-11-26 15:42:42.691087411 -0600 |
1197 | 15 | index 8158c2a..d9c9302 100644 | 13 | @@ -54,6 +54,7 @@ Rick Clark <rick@openstack.org> |
1190 | 16 | --- a/Authors | ||
1191 | 17 | +++ b/Authors | ||
1192 | 18 | @@ -52,6 +52,7 @@ Rick Clark <rick@openstack.org> | ||
1198 | 19 | Rick Harris <rconradharris@gmail.com> | 14 | Rick Harris <rconradharris@gmail.com> |
1199 | 20 | Reynolds Chin <benzwt@gmail.com> | 15 | Reynolds Chin <benzwt@gmail.com> |
1200 | 21 | Russell Bryant <rbryant@redhat.com> | 16 | Russell Bryant <rbryant@redhat.com> |
1201 | 22 | +Sam Morrison <sorrison@gmail.com> | 17 | +Sam Morrison <sorrison@gmail.com> |
1202 | 18 | Sean Dague <sdague@linux.vnet.ibm.com> | ||
1203 | 23 | Soren Hansen <soren.hansen@rackspace.com> | 19 | Soren Hansen <soren.hansen@rackspace.com> |
1204 | 24 | Stuart McLaren <stuart.mclaren@hp.com> | 20 | Stuart McLaren <stuart.mclaren@hp.com> |
1210 | 25 | Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp> | 21 | diff -Naurp glance-2012.1.3.orig/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py glance-2012.1.3/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py |
1211 | 26 | diff --git a/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py b/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py | 22 | --- glance-2012.1.3.orig/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py 2012-11-26 15:19:40.000000000 -0600 |
1212 | 27 | index fe6bf86..8db18c1 100644 | 23 | +++ glance-2012.1.3/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py 2012-11-26 15:41:59.231087390 -0600 |
1208 | 28 | --- a/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py | ||
1209 | 29 | +++ b/glance/registry/db/migrate_repo/versions/012_id_to_uuid.py | ||
1213 | 30 | @@ -225,18 +225,24 @@ def _get_table(table_name, metadata): | 24 | @@ -225,18 +225,24 @@ def _get_table(table_name, metadata): |
1214 | 31 | 25 | ||
1215 | 32 | def _get_foreign_keys(t_images, t_image_members, t_image_properties): | 26 | def _get_foreign_keys(t_images, t_image_members, t_image_properties): |
1216 | @@ -36,27 +30,29 @@ | |||
1217 | 36 | + foreign_keys = [] | 30 | + foreign_keys = [] |
1218 | 37 | + if t_image_members.foreign_keys: | 31 | + if t_image_members.foreign_keys: |
1219 | 38 | + img_members_fk_name = list(t_image_members.foreign_keys)[0].name | 32 | + img_members_fk_name = list(t_image_members.foreign_keys)[0].name |
1224 | 39 | 33 | + | |
1221 | 40 | - fk1 = migrate.ForeignKeyConstraint([t_image_members.c.image_id], | ||
1222 | 41 | - [t_images.c.id], | ||
1223 | 42 | - name=image_members_fk_name) | ||
1225 | 43 | + fk1 = migrate.ForeignKeyConstraint([t_image_members.c.image_id], | 34 | + fk1 = migrate.ForeignKeyConstraint([t_image_members.c.image_id], |
1226 | 44 | + [t_images.c.id], | 35 | + [t_images.c.id], |
1227 | 45 | + name=img_members_fk_name) | 36 | + name=img_members_fk_name) |
1228 | 46 | + foreign_keys.append(fk1) | 37 | + foreign_keys.append(fk1) |
1233 | 47 | 38 | + | |
1230 | 48 | - fk2 = migrate.ForeignKeyConstraint([t_image_properties.c.image_id], | ||
1231 | 49 | - [t_images.c.id], | ||
1232 | 50 | - name=image_properties_fk_name) | ||
1234 | 51 | + if t_image_properties.foreign_keys: | 39 | + if t_image_properties.foreign_keys: |
1235 | 52 | + img_properties_fk_name = list(t_image_properties.foreign_keys)[0].name | 40 | + img_properties_fk_name = list(t_image_properties.foreign_keys)[0].name |
1238 | 53 | 41 | + | |
1237 | 54 | - return fk1, fk2 | ||
1239 | 55 | + fk2 = migrate.ForeignKeyConstraint([t_image_properties.c.image_id], | 42 | + fk2 = migrate.ForeignKeyConstraint([t_image_properties.c.image_id], |
1240 | 56 | + [t_images.c.id], | 43 | + [t_images.c.id], |
1241 | 57 | + name=img_properties_fk_name) | 44 | + name=img_properties_fk_name) |
1242 | 58 | + foreign_keys.append(fk2) | 45 | + foreign_keys.append(fk2) |
1244 | 59 | + | 46 | |
1245 | 47 | - fk1 = migrate.ForeignKeyConstraint([t_image_members.c.image_id], | ||
1246 | 48 | - [t_images.c.id], | ||
1247 | 49 | - name=image_members_fk_name) | ||
1248 | 50 | - | ||
1249 | 51 | - fk2 = migrate.ForeignKeyConstraint([t_image_properties.c.image_id], | ||
1250 | 52 | - [t_images.c.id], | ||
1251 | 53 | - name=image_properties_fk_name) | ||
1252 | 54 | - | ||
1253 | 55 | - return fk1, fk2 | ||
1254 | 60 | + return foreign_keys | 56 | + return foreign_keys |
1255 | 61 | 57 | ||
1256 | 62 | 58 | ||
1257 | 63 | 59 | ||
1258 | === modified file 'debian/patches/series' | |||
1259 | --- debian/patches/series 2012-11-08 07:19:39 +0000 | |||
1260 | +++ debian/patches/series 2012-12-18 14:12:30 +0000 | |||
1261 | @@ -3,4 +3,3 @@ | |||
1262 | 3 | disable-network-for-docs.patch | 3 | disable-network-for-docs.patch |
1263 | 4 | disable_db_table_auto_create.patch | 4 | disable_db_table_auto_create.patch |
1264 | 5 | fix_migration_012_foreign_keys.patch | 5 | fix_migration_012_foreign_keys.patch |
1265 | 6 | CVE-2012-4573.patch | ||
1266 | 7 | 6 | ||
1267 | === added directory 'glance.egg-info' | |||
1268 | === added file 'glance.egg-info/PKG-INFO' | |||
1269 | --- glance.egg-info/PKG-INFO 1970-01-01 00:00:00 +0000 | |||
1270 | +++ glance.egg-info/PKG-INFO 2012-12-18 14:12:30 +0000 | |||
1271 | @@ -0,0 +1,15 @@ | |||
1272 | 1 | Metadata-Version: 1.1 | ||
1273 | 2 | Name: glance | ||
1274 | 3 | Version: 2012.1.3 | ||
1275 | 4 | Summary: The Glance project provides services for discovering, registering, and retrieving virtual machine images | ||
1276 | 5 | Home-page: http://glance.openstack.org/ | ||
1277 | 6 | Author: OpenStack | ||
1278 | 7 | Author-email: openstack@lists.launchpad.net | ||
1279 | 8 | License: Apache License (2.0) | ||
1280 | 9 | Description: UNKNOWN | ||
1281 | 10 | Platform: UNKNOWN | ||
1282 | 11 | Classifier: Development Status :: 4 - Beta | ||
1283 | 12 | Classifier: License :: OSI Approved :: Apache Software License | ||
1284 | 13 | Classifier: Operating System :: POSIX :: Linux | ||
1285 | 14 | Classifier: Programming Language :: Python :: 2.6 | ||
1286 | 15 | Classifier: Environment :: No Input/Output (Daemon) | ||
1287 | 0 | 16 | ||
1288 | === added file 'glance.egg-info/SOURCES.txt' | |||
1289 | --- glance.egg-info/SOURCES.txt 1970-01-01 00:00:00 +0000 | |||
1290 | +++ glance.egg-info/SOURCES.txt 2012-12-18 14:12:30 +0000 | |||
1291 | @@ -0,0 +1,217 @@ | |||
1292 | 1 | Authors | ||
1293 | 2 | HACKING.rst | ||
1294 | 3 | LICENSE | ||
1295 | 4 | MANIFEST.in | ||
1296 | 5 | README.rst | ||
1297 | 6 | babel.cfg | ||
1298 | 7 | pylintrc | ||
1299 | 8 | run_tests.py | ||
1300 | 9 | run_tests.sh | ||
1301 | 10 | setup.cfg | ||
1302 | 11 | setup.py | ||
1303 | 12 | tox.ini | ||
1304 | 13 | bin/glance | ||
1305 | 14 | bin/glance-api | ||
1306 | 15 | bin/glance-cache-cleaner | ||
1307 | 16 | bin/glance-cache-manage | ||
1308 | 17 | bin/glance-cache-prefetcher | ||
1309 | 18 | bin/glance-cache-pruner | ||
1310 | 19 | bin/glance-control | ||
1311 | 20 | bin/glance-manage | ||
1312 | 21 | bin/glance-registry | ||
1313 | 22 | bin/glance-scrubber | ||
1314 | 23 | doc/source/architecture.rst | ||
1315 | 24 | doc/source/authentication.rst | ||
1316 | 25 | doc/source/cache.rst | ||
1317 | 26 | doc/source/client.rst | ||
1318 | 27 | doc/source/conf.py | ||
1319 | 28 | doc/source/configuring.rst | ||
1320 | 29 | doc/source/controllingservers.rst | ||
1321 | 30 | doc/source/formats.rst | ||
1322 | 31 | doc/source/glance.rst | ||
1323 | 32 | doc/source/glanceapi.rst | ||
1324 | 33 | doc/source/identifiers.rst | ||
1325 | 34 | doc/source/index.rst | ||
1326 | 35 | doc/source/installing.rst | ||
1327 | 36 | doc/source/notifications.rst | ||
1328 | 37 | doc/source/policies.rst | ||
1329 | 38 | doc/source/statuses.rst | ||
1330 | 39 | doc/source/_static/basic.css | ||
1331 | 40 | doc/source/_static/default.css | ||
1332 | 41 | doc/source/_static/jquery.tweet.js | ||
1333 | 42 | doc/source/_static/tweaks.css | ||
1334 | 43 | doc/source/_templates/.placeholder | ||
1335 | 44 | doc/source/_theme/layout.html | ||
1336 | 45 | doc/source/_theme/theme.conf | ||
1337 | 46 | doc/source/man/glance.rst | ||
1338 | 47 | doc/source/man/glanceapi.rst | ||
1339 | 48 | doc/source/man/glancecachecleaner.rst | ||
1340 | 49 | doc/source/man/glancecachemanage.rst | ||
1341 | 50 | doc/source/man/glancecacheprefetcher.rst | ||
1342 | 51 | doc/source/man/glancecachepruner.rst | ||
1343 | 52 | doc/source/man/glancecontrol.rst | ||
1344 | 53 | doc/source/man/glancemanage.rst | ||
1345 | 54 | doc/source/man/glanceregistry.rst | ||
1346 | 55 | doc/source/man/glancescrubber.rst | ||
1347 | 56 | etc/glance-api-paste.ini | ||
1348 | 57 | etc/glance-api.conf | ||
1349 | 58 | etc/glance-cache-paste.ini | ||
1350 | 59 | etc/glance-cache.conf | ||
1351 | 60 | etc/glance-registry-paste.ini | ||
1352 | 61 | etc/glance-registry.conf | ||
1353 | 62 | etc/glance-scrubber-paste.ini | ||
1354 | 63 | etc/glance-scrubber.conf | ||
1355 | 64 | etc/logging.cnf.sample | ||
1356 | 65 | etc/policy.json | ||
1357 | 66 | glance/__init__.py | ||
1358 | 67 | glance/client.py | ||
1359 | 68 | glance/vcsversion.py | ||
1360 | 69 | glance/version.py | ||
1361 | 70 | glance.egg-info/PKG-INFO | ||
1362 | 71 | glance.egg-info/SOURCES.txt | ||
1363 | 72 | glance.egg-info/dependency_links.txt | ||
1364 | 73 | glance.egg-info/top_level.txt | ||
1365 | 74 | glance/api/__init__.py | ||
1366 | 75 | glance/api/cached_images.py | ||
1367 | 76 | glance/api/policy.py | ||
1368 | 77 | glance/api/versions.py | ||
1369 | 78 | glance/api/middleware/__init__.py | ||
1370 | 79 | glance/api/middleware/cache.py | ||
1371 | 80 | glance/api/middleware/cache_manage.py | ||
1372 | 81 | glance/api/middleware/version_negotiation.py | ||
1373 | 82 | glance/api/v1/__init__.py | ||
1374 | 83 | glance/api/v1/controller.py | ||
1375 | 84 | glance/api/v1/filters.py | ||
1376 | 85 | glance/api/v1/images.py | ||
1377 | 86 | glance/api/v1/members.py | ||
1378 | 87 | glance/api/v1/router.py | ||
1379 | 88 | glance/common/__init__.py | ||
1380 | 89 | glance/common/animation.py | ||
1381 | 90 | glance/common/auth.py | ||
1382 | 91 | glance/common/cfg.py | ||
1383 | 92 | glance/common/client.py | ||
1384 | 93 | glance/common/config.py | ||
1385 | 94 | glance/common/context.py | ||
1386 | 95 | glance/common/crypt.py | ||
1387 | 96 | glance/common/exception.py | ||
1388 | 97 | glance/common/policy.py | ||
1389 | 98 | glance/common/utils.py | ||
1390 | 99 | glance/common/wsgi.py | ||
1391 | 100 | glance/image_cache/__init__.py | ||
1392 | 101 | glance/image_cache/cleaner.py | ||
1393 | 102 | glance/image_cache/prefetcher.py | ||
1394 | 103 | glance/image_cache/pruner.py | ||
1395 | 104 | glance/image_cache/queue_image.py | ||
1396 | 105 | glance/image_cache/drivers/__init__.py | ||
1397 | 106 | glance/image_cache/drivers/base.py | ||
1398 | 107 | glance/image_cache/drivers/sqlite.py | ||
1399 | 108 | glance/image_cache/drivers/xattr.py | ||
1400 | 109 | glance/locale/__init__.py | ||
1401 | 110 | glance/locale/glance.pot | ||
1402 | 111 | glance/notifier/__init__.py | ||
1403 | 112 | glance/notifier/notify_kombu.py | ||
1404 | 113 | glance/notifier/notify_log.py | ||
1405 | 114 | glance/notifier/notify_noop.py | ||
1406 | 115 | glance/notifier/notify_qpid.py | ||
1407 | 116 | glance/notifier/strategy.py | ||
1408 | 117 | glance/registry/__init__.py | ||
1409 | 118 | glance/registry/client.py | ||
1410 | 119 | glance/registry/context.py | ||
1411 | 120 | glance/registry/api/__init__.py | ||
1412 | 121 | glance/registry/api/v1/__init__.py | ||
1413 | 122 | glance/registry/api/v1/images.py | ||
1414 | 123 | glance/registry/api/v1/members.py | ||
1415 | 124 | glance/registry/db/__init__.py | ||
1416 | 125 | glance/registry/db/api.py | ||
1417 | 126 | glance/registry/db/migration.py | ||
1418 | 127 | glance/registry/db/models.py | ||
1419 | 128 | glance/registry/db/migrate_repo/README | ||
1420 | 129 | glance/registry/db/migrate_repo/__init__.py | ||
1421 | 130 | glance/registry/db/migrate_repo/manage.py | ||
1422 | 131 | glance/registry/db/migrate_repo/migrate.cfg | ||
1423 | 132 | glance/registry/db/migrate_repo/schema.py | ||
1424 | 133 | glance/registry/db/migrate_repo/versions/001_add_images_table.py | ||
1425 | 134 | glance/registry/db/migrate_repo/versions/002_add_image_properties_table.py | ||
1426 | 135 | glance/registry/db/migrate_repo/versions/003_add_disk_format.py | ||
1427 | 136 | glance/registry/db/migrate_repo/versions/003_sqlite_downgrade.sql | ||
1428 | 137 | glance/registry/db/migrate_repo/versions/003_sqlite_upgrade.sql | ||
1429 | 138 | glance/registry/db/migrate_repo/versions/004_add_checksum.py | ||
1430 | 139 | glance/registry/db/migrate_repo/versions/005_size_big_integer.py | ||
1431 | 140 | glance/registry/db/migrate_repo/versions/006_key_to_name.py | ||
1432 | 141 | glance/registry/db/migrate_repo/versions/006_mysql_downgrade.sql | ||
1433 | 142 | glance/registry/db/migrate_repo/versions/006_mysql_upgrade.sql | ||
1434 | 143 | glance/registry/db/migrate_repo/versions/006_sqlite_downgrade.sql | ||
1435 | 144 | glance/registry/db/migrate_repo/versions/006_sqlite_upgrade.sql | ||
1436 | 145 | glance/registry/db/migrate_repo/versions/007_add_owner.py | ||
1437 | 146 | glance/registry/db/migrate_repo/versions/008_add_image_members_table.py | ||
1438 | 147 | glance/registry/db/migrate_repo/versions/009_add_mindisk_and_minram.py | ||
1439 | 148 | glance/registry/db/migrate_repo/versions/010_default_update_at.py | ||
1440 | 149 | glance/registry/db/migrate_repo/versions/011_make_mindisk_and_minram_notnull.py | ||
1441 | 150 | glance/registry/db/migrate_repo/versions/012_id_to_uuid.py | ||
1442 | 151 | glance/registry/db/migrate_repo/versions/013_add_protected.py | ||
1443 | 152 | glance/registry/db/migrate_repo/versions/013_sqlite_downgrade.sql | ||
1444 | 153 | glance/registry/db/migrate_repo/versions/__init__.py | ||
1445 | 154 | glance/store/__init__.py | ||
1446 | 155 | glance/store/base.py | ||
1447 | 156 | glance/store/filesystem.py | ||
1448 | 157 | glance/store/http.py | ||
1449 | 158 | glance/store/location.py | ||
1450 | 159 | glance/store/rbd.py | ||
1451 | 160 | glance/store/s3.py | ||
1452 | 161 | glance/store/scrubber.py | ||
1453 | 162 | glance/store/swift.py | ||
1454 | 163 | glance/tests/__init__.py | ||
1455 | 164 | glance/tests/logcapture.py | ||
1456 | 165 | glance/tests/stubs.py | ||
1457 | 166 | glance/tests/utils.py | ||
1458 | 167 | glance/tests/etc/policy.json | ||
1459 | 168 | glance/tests/functional/__init__.py | ||
1460 | 169 | glance/tests/functional/store_utils.py | ||
1461 | 170 | glance/tests/functional/test_api.py | ||
1462 | 171 | glance/tests/functional/test_bin_glance.py | ||
1463 | 172 | glance/tests/functional/test_bin_glance_cache_manage.py | ||
1464 | 173 | glance/tests/functional/test_cache_middleware.py | ||
1465 | 174 | glance/tests/functional/test_client_exceptions.py | ||
1466 | 175 | glance/tests/functional/test_client_redirects.py | ||
1467 | 176 | glance/tests/functional/test_copy_to_file.py | ||
1468 | 177 | glance/tests/functional/test_logging.py | ||
1469 | 178 | glance/tests/functional/test_misc.py | ||
1470 | 179 | glance/tests/functional/test_multiprocessing.py | ||
1471 | 180 | glance/tests/functional/test_rbd.py | ||
1472 | 181 | glance/tests/functional/test_respawn.py | ||
1473 | 182 | glance/tests/functional/test_s3.py | ||
1474 | 183 | glance/tests/functional/test_scrubber.py | ||
1475 | 184 | glance/tests/functional/test_sqlite.py | ||
1476 | 185 | glance/tests/functional/test_ssl.py | ||
1477 | 186 | glance/tests/functional/test_swift.py | ||
1478 | 187 | glance/tests/unit/__init__.py | ||
1479 | 188 | glance/tests/unit/base.py | ||
1480 | 189 | glance/tests/unit/test_api.py | ||
1481 | 190 | glance/tests/unit/test_auth.py | ||
1482 | 191 | glance/tests/unit/test_cfg.py | ||
1483 | 192 | glance/tests/unit/test_clients.py | ||
1484 | 193 | glance/tests/unit/test_config.py | ||
1485 | 194 | glance/tests/unit/test_context.py | ||
1486 | 195 | glance/tests/unit/test_db.py | ||
1487 | 196 | glance/tests/unit/test_filesystem_store.py | ||
1488 | 197 | glance/tests/unit/test_http_store.py | ||
1489 | 198 | glance/tests/unit/test_image_cache.py | ||
1490 | 199 | glance/tests/unit/test_migrations.conf | ||
1491 | 200 | glance/tests/unit/test_migrations.py | ||
1492 | 201 | glance/tests/unit/test_misc.py | ||
1493 | 202 | glance/tests/unit/test_notifier.py | ||
1494 | 203 | glance/tests/unit/test_s3_store.py | ||
1495 | 204 | glance/tests/unit/test_skip_examples.py | ||
1496 | 205 | glance/tests/unit/test_store_location.py | ||
1497 | 206 | glance/tests/unit/test_swift_store.py | ||
1498 | 207 | glance/tests/unit/test_utils.py | ||
1499 | 208 | glance/tests/unit/test_versions.py | ||
1500 | 209 | glance/tests/unit/test_wsgi.py | ||
1501 | 210 | glance/tests/var/ca.crt | ||
1502 | 211 | glance/tests/var/certificate.crt | ||
1503 | 212 | glance/tests/var/privatekey.key | ||
1504 | 213 | tools/install_venv.py | ||
1505 | 214 | tools/migrate_image_owners.py | ||
1506 | 215 | tools/pip-requires | ||
1507 | 216 | tools/test-requires | ||
1508 | 217 | tools/with_venv.sh | ||
1509 | 0 | \ No newline at end of file | 218 | \ No newline at end of file |
1510 | 1 | 219 | ||
1511 | === added file 'glance.egg-info/dependency_links.txt' | |||
1512 | --- glance.egg-info/dependency_links.txt 1970-01-01 00:00:00 +0000 | |||
1513 | +++ glance.egg-info/dependency_links.txt 2012-12-18 14:12:30 +0000 | |||
1514 | @@ -0,0 +1,1 @@ | |||
1515 | 1 | |||
1516 | 0 | 2 | ||
1517 | === added file 'glance.egg-info/top_level.txt' | |||
1518 | --- glance.egg-info/top_level.txt 1970-01-01 00:00:00 +0000 | |||
1519 | +++ glance.egg-info/top_level.txt 2012-12-18 14:12:30 +0000 | |||
1520 | @@ -0,0 +1,1 @@ | |||
1521 | 1 | glance | ||
1522 | 0 | 2 | ||
1523 | === added file 'glance/vcsversion.py' | |||
1524 | --- glance/vcsversion.py 1970-01-01 00:00:00 +0000 | |||
1525 | +++ glance/vcsversion.py 2012-12-18 14:12:30 +0000 | |||
1526 | @@ -0,0 +1,7 @@ | |||
1527 | 1 | |||
1528 | 2 | # This file is automatically generated by setup.py, So don't edit it. :) | ||
1529 | 3 | version_info = { | ||
1530 | 4 | 'branch_nick': 'stable/essex', | ||
1531 | 5 | 'revision_id': 'efd7e75b1f419a52c7103c7840e24af8e5deb29d', | ||
1532 | 6 | 'revno': 1464 | ||
1533 | 7 | } | ||
1534 | 0 | 8 | ||
1535 | === modified file 'setup.cfg' | |||
1536 | --- setup.cfg 2012-06-24 03:14:33 +0000 | |||
1537 | +++ setup.cfg 2012-12-18 14:12:30 +0000 | |||
1538 | @@ -23,14 +23,11 @@ | |||
1539 | 23 | output_file = glance/locale/glance.pot | 23 | output_file = glance/locale/glance.pot |
1540 | 24 | 24 | ||
1541 | 25 | [nosetests] | 25 | [nosetests] |
1553 | 26 | # NOTE(jkoelker) To run the test suite under nose install the following | 26 | verbosity = 2 |
1554 | 27 | # coverage http://pypi.python.org/pypi/coverage | 27 | detailed-errors = 1 |
1555 | 28 | # tissue http://pypi.python.org/pypi/tissue (pep8 checker) | 28 | with-openstack = 1 |
1556 | 29 | # openstack-nose https://github.com/jkoelker/openstack-nose | 29 | openstack-red = 0.05 |
1557 | 30 | verbosity=2 | 30 | openstack-yellow = 0.025 |
1558 | 31 | detailed-errors=1 | 31 | openstack-show-elapsed = 1 |
1559 | 32 | with-openstack=1 | 32 | openstack-color = 1 |
1560 | 33 | openstack-red=0.05 | 33 | |
1550 | 34 | openstack-yellow=0.025 | ||
1551 | 35 | openstack-show-elapsed=1 | ||
1552 | 36 | openstack-color=1 | ||
1561 | 37 | 34 | ||
1562 | === modified file 'tools/pip-requires' | |||
1563 | --- tools/pip-requires 2012-06-24 03:14:33 +0000 | |||
1564 | +++ tools/pip-requires 2012-12-18 14:12:30 +0000 | |||
1565 | @@ -3,7 +3,7 @@ | |||
1566 | 3 | # package to get the right headers... | 3 | # package to get the right headers... |
1567 | 4 | greenlet>=0.3.1 | 4 | greenlet>=0.3.1 |
1568 | 5 | 5 | ||
1570 | 6 | SQLAlchemy>=0.7 | 6 | SQLAlchemy>=0.7,<=0.7.9 |
1571 | 7 | anyjson | 7 | anyjson |
1572 | 8 | eventlet>=0.9.12 | 8 | eventlet>=0.9.12 |
1573 | 9 | PasteDeploy | 9 | PasteDeploy |
Yolanda
Specifically what was the pep8 issue that was causing the build to fail?
[ Yolanda Robla ]
* debian/rules: skipping pep8 tests to allow building