summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErno Kuvaja <jokke@usr.fi>2018-01-10 10:37:53 +0000
committerErno Kuvaja <jokke@usr.fi>2018-07-13 10:21:49 +0100
commit3dde3204d5c1b5323dba2d7b7607e69bcc58bbb2 (patch)
tree47df7e0b627fbf380c692fb1428993659bb62dc5
parentf3496591ae34a332bf59432dc4e7c94fcd58170b (diff)
Remove Images API v1 entry points
This change removes option to configure Images API v1 This change removes Images API v1 endpoints from the router This change removes all v1 tests This change removes the v1 dependant glance-cache-manage command This change does not remove all v1 codebase. Further cleanup and decoupling will be needed. Change-Id: Ia086230cc8c92f7b7dfd5b001923110d5bc55d4d
Notes
Notes (review): Code-Review+2: Abhishek Kekane <akekane@redhat.com> Code-Review+1: Piotr Bielak <piotr.bielak@corp.ovh.com> Code-Review+2: Sean McGinnis <sean.mcginnis@gmail.com> Code-Review+2: Brian Rosmaita <rosmaita.fossdev@gmail.com> Workflow+1: Brian Rosmaita <rosmaita.fossdev@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Fri, 27 Jul 2018 06:08:31 +0000 Reviewed-on: https://review.openstack.org/532503 Project: openstack/glance Branch: refs/heads/master
-rw-r--r--glance/api/__init__.py2
-rw-r--r--glance/api/middleware/cache.py17
-rw-r--r--glance/api/middleware/version_negotiation.py4
-rw-r--r--glance/api/v1/images.py1351
-rw-r--r--glance/api/v1/members.py248
-rw-r--r--glance/api/v1/router.py80
-rw-r--r--glance/api/versions.py16
-rw-r--r--glance/cmd/cache_manage.py490
-rw-r--r--glance/common/config.py72
-rw-r--r--glance/common/store_utils.py1
-rw-r--r--glance/image_cache/client.py132
-rw-r--r--glance/tests/functional/__init__.py3
-rw-r--r--glance/tests/functional/serial/test_scrubber.py16
-rw-r--r--glance/tests/functional/test_api.py150
-rw-r--r--glance/tests/functional/test_bin_glance_cache_manage.py358
-rw-r--r--glance/tests/functional/test_cache_middleware.py746
-rw-r--r--glance/tests/functional/test_cors_middleware.py2
-rw-r--r--glance/tests/functional/test_glance_replicator.py33
-rw-r--r--glance/tests/functional/v2/test_images.py33
-rw-r--r--glance/tests/integration/legacy_functional/__init__.py0
-rw-r--r--glance/tests/integration/legacy_functional/base.py222
-rw-r--r--glance/tests/integration/legacy_functional/test_v1_api.py1735
-rw-r--r--glance/tests/unit/api/test_cmd_cache_manage.py298
-rw-r--r--glance/tests/unit/api/test_common.py19
-rw-r--r--glance/tests/unit/common/test_wsgi.py19
-rw-r--r--glance/tests/unit/test_cache_middleware.py446
-rw-r--r--glance/tests/unit/test_image_cache_client.py132
-rw-r--r--glance/tests/unit/test_versions.py33
28 files changed, 21 insertions, 6637 deletions
diff --git a/glance/api/__init__.py b/glance/api/__init__.py
index df41d7a..b62d4a1 100644
--- a/glance/api/__init__.py
+++ b/glance/api/__init__.py
@@ -20,8 +20,6 @@ CONF = cfg.CONF
20 20
21 21
22def root_app_factory(loader, global_conf, **local_conf): 22def root_app_factory(loader, global_conf, **local_conf):
23 if not CONF.enable_v1_api and '/v1' in local_conf:
24 del local_conf['/v1']
25 if not CONF.enable_v2_api and '/v2' in local_conf: 23 if not CONF.enable_v2_api and '/v2' in local_conf:
26 del local_conf['/v2'] 24 del local_conf['/v2']
27 return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf) 25 return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
diff --git a/glance/api/middleware/cache.py b/glance/api/middleware/cache.py
index 898cca7..3072c71 100644
--- a/glance/api/middleware/cache.py
+++ b/glance/api/middleware/cache.py
@@ -31,7 +31,6 @@ import webob
31 31
32from glance.api.common import size_checked_iter 32from glance.api.common import size_checked_iter
33from glance.api import policy 33from glance.api import policy
34from glance.api.v1 import images
35from glance.common import exception 34from glance.common import exception
36from glance.common import utils 35from glance.common import utils
37from glance.common import wsgi 36from glance.common import wsgi
@@ -55,7 +54,6 @@ class CacheFilter(wsgi.Middleware):
55 54
56 def __init__(self, app): 55 def __init__(self, app):
57 self.cache = image_cache.ImageCache() 56 self.cache = image_cache.ImageCache()
58 self.serializer = images.ImageSerializer()
59 self.policy = policy.Enforcer() 57 self.policy = policy.Enforcer()
60 LOG.info(_LI("Initialized image cache middleware")) 58 LOG.info(_LI("Initialized image cache middleware"))
61 super(CacheFilter, self).__init__(app) 59 super(CacheFilter, self).__init__(app)
@@ -214,21 +212,6 @@ class CacheFilter(wsgi.Middleware):
214 else: 212 else:
215 return (image_id, method, version) 213 return (image_id, method, version)
216 214
217 def _process_v1_request(self, request, image_id, image_iterator,
218 image_meta):
219 # Don't display location
220 if 'location' in image_meta:
221 del image_meta['location']
222 image_meta.pop('location_data', None)
223 self._verify_metadata(image_meta)
224
225 response = webob.Response(request=request)
226 raw_response = {
227 'image_iterator': image_iterator,
228 'image_meta': image_meta,
229 }
230 return self.serializer.show(response, raw_response)
231
232 def _process_v2_request(self, request, image_id, image_iterator, 215 def _process_v2_request(self, request, image_id, image_iterator,
233 image_meta): 216 image_meta):
234 # We do some contortions to get the image_metadata so 217 # We do some contortions to get the image_metadata so
diff --git a/glance/api/middleware/version_negotiation.py b/glance/api/middleware/version_negotiation.py
index 12839ef..4e6db7e 100644
--- a/glance/api/middleware/version_negotiation.py
+++ b/glance/api/middleware/version_negotiation.py
@@ -72,10 +72,6 @@ class VersionNegotiationFilter(wsgi.Middleware):
72 72
73 def _get_allowed_versions(self): 73 def _get_allowed_versions(self):
74 allowed_versions = {} 74 allowed_versions = {}
75 if CONF.enable_v1_api:
76 allowed_versions['v1'] = 1
77 allowed_versions['v1.0'] = 1
78 allowed_versions['v1.1'] = 1
79 if CONF.enable_v2_api: 75 if CONF.enable_v2_api:
80 allowed_versions['v2'] = 2 76 allowed_versions['v2'] = 2
81 allowed_versions['v2.0'] = 2 77 allowed_versions['v2.0'] = 2
diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py
deleted file mode 100644
index 0b74be8..0000000
--- a/glance/api/v1/images.py
+++ /dev/null
@@ -1,1351 +0,0 @@
1# Copyright 2013 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""
17/images endpoint for Glance v1 API
18"""
19
20import copy
21
22import glance_store as store
23import glance_store.location
24from oslo_config import cfg
25from oslo_log import log as logging
26from oslo_utils import encodeutils
27from oslo_utils import excutils
28from oslo_utils import strutils
29import six
30from webob.exc import HTTPBadRequest
31from webob.exc import HTTPConflict
32from webob.exc import HTTPForbidden
33from webob.exc import HTTPMethodNotAllowed
34from webob.exc import HTTPNotFound
35from webob.exc import HTTPRequestEntityTooLarge
36from webob.exc import HTTPServiceUnavailable
37from webob.exc import HTTPUnauthorized
38from webob import Response
39
40from glance.api import common
41from glance.api import policy
42import glance.api.v1
43from glance.api.v1 import controller
44from glance.api.v1 import filters
45from glance.api.v1 import upload_utils
46from glance.common import exception
47from glance.common import property_utils
48from glance.common import store_utils
49from glance.common import timeutils
50from glance.common import utils
51from glance.common import wsgi
52from glance.i18n import _, _LE, _LI, _LW
53from glance import notifier
54import glance.registry.client.v1.api as registry
55
56LOG = logging.getLogger(__name__)
57SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
58SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
59ACTIVE_IMMUTABLE = glance.api.v1.ACTIVE_IMMUTABLE
60IMMUTABLE = glance.api.v1.IMMUTABLE
61
62CONF = cfg.CONF
63CONF.import_opt('disk_formats', 'glance.common.config', group='image_format')
64CONF.import_opt('container_formats', 'glance.common.config',
65 group='image_format')
66CONF.import_opt('image_property_quota', 'glance.common.config')
67
68
69def _validate_time(req, values):
70 """Validates time formats for updated_at, created_at and deleted_at.
71 'strftime' only allows values after 1900 in glance v1 so this is enforced
72 here. This was introduced to keep modularity.
73 """
74 for time_field in ['created_at', 'updated_at', 'deleted_at']:
75 if time_field in values and values[time_field]:
76 try:
77 time = timeutils.parse_isotime(values[time_field])
78 # On Python 2, datetime.datetime.strftime() raises a ValueError
79 # for years older than 1900. On Python 3, years older than 1900
80 # are accepted. But we explicitly want to reject timestamps
81 # older than January 1st, 1900 for Glance API v1.
82 if time.year < 1900:
83 raise ValueError
84 values[time_field] = time.strftime(
85 timeutils.PERFECT_TIME_FORMAT)
86 except ValueError:
87 msg = (_("Invalid time format for %s.") % time_field)
88 raise HTTPBadRequest(explanation=msg, request=req)
89
90
91def _validate_format(req, values):
92 """Validates disk_format and container_format fields
93
94 Introduced to split too complex validate_image_meta method.
95 """
96 amazon_formats = ('aki', 'ari', 'ami')
97 disk_format = values.get('disk_format')
98 container_format = values.get('container_format')
99
100 if 'disk_format' in values:
101 if disk_format not in CONF.image_format.disk_formats:
102 msg = _("Invalid disk format '%s' for image.") % disk_format
103 raise HTTPBadRequest(explanation=msg, request=req)
104
105 if 'container_format' in values:
106 if container_format not in CONF.image_format.container_formats:
107 msg = _("Invalid container format '%s' "
108 "for image.") % container_format
109 raise HTTPBadRequest(explanation=msg, request=req)
110
111 if any(f in amazon_formats for f in [disk_format, container_format]):
112 if disk_format is None:
113 values['disk_format'] = container_format
114 elif container_format is None:
115 values['container_format'] = disk_format
116 elif container_format != disk_format:
117 msg = (_("Invalid mix of disk and container formats. "
118 "When setting a disk or container format to "
119 "one of 'aki', 'ari', or 'ami', the container "
120 "and disk formats must match."))
121 raise HTTPBadRequest(explanation=msg, request=req)
122
123
124def validate_image_meta(req, values):
125 _validate_format(req, values)
126 _validate_time(req, values)
127
128 name = values.get('name')
129 checksum = values.get('checksum')
130
131 if name and len(name) > 255:
132 msg = _('Image name too long: %d') % len(name)
133 raise HTTPBadRequest(explanation=msg, request=req)
134
135 # check that checksum retrieved is exactly 32 characters
136 # as long as we expect md5 checksum
137 # https://bugs.launchpad.net/glance/+bug/1454730
138 if checksum and len(checksum) > 32:
139 msg = (_("Invalid checksum '%s': can't exceed 32 characters") %
140 checksum)
141 raise HTTPBadRequest(explanation=msg, request=req)
142
143 return values
144
145
146def redact_loc(image_meta, copy_dict=True):
147 """
148 Create a shallow copy of image meta with 'location' removed
149 for security (as it can contain credentials).
150 """
151 if copy_dict:
152 new_image_meta = copy.copy(image_meta)
153 else:
154 new_image_meta = image_meta
155 new_image_meta.pop('location', None)
156 new_image_meta.pop('location_data', None)
157 return new_image_meta
158
159
160class Controller(controller.BaseController):
161 """
162 WSGI controller for images resource in Glance v1 API
163
164 The images resource API is a RESTful web service for image data. The API
165 is as follows::
166
167 GET /images -- Returns a set of brief metadata about images
168 GET /images/detail -- Returns a set of detailed metadata about
169 images
170 HEAD /images/<ID> -- Return metadata about an image with id <ID>
171 GET /images/<ID> -- Return image data for image with id <ID>
172 POST /images -- Store image data and return metadata about the
173 newly-stored image
174 PUT /images/<ID> -- Update image metadata and/or upload image
175 data for a previously-reserved image
176 DELETE /images/<ID> -- Delete the image with id <ID>
177 """
178
179 def __init__(self):
180 self.notifier = notifier.Notifier()
181 registry.configure_registry_client()
182 self.policy = policy.Enforcer()
183 if property_utils.is_property_protection_enabled():
184 self.prop_enforcer = property_utils.PropertyRules(self.policy)
185 else:
186 self.prop_enforcer = None
187
188 def _enforce(self, req, action, target=None):
189 """Authorize an action against our policies"""
190 if target is None:
191 target = {}
192 try:
193 self.policy.enforce(req.context, action, target)
194 except exception.Forbidden:
195 LOG.debug("User not permitted to perform '%s' action", action)
196 raise HTTPForbidden()
197
198 def _enforce_image_property_quota(self,
199 image_meta,
200 orig_image_meta=None,
201 purge_props=False,
202 req=None):
203 if CONF.image_property_quota < 0:
204 # If value is negative, allow unlimited number of properties
205 return
206
207 props = list(image_meta['properties'].keys())
208
209 # NOTE(ameade): If we are not removing existing properties,
210 # take them in to account
211 if (not purge_props) and orig_image_meta:
212 original_props = orig_image_meta['properties'].keys()
213 props.extend(original_props)
214 props = set(props)
215
216 if len(props) > CONF.image_property_quota:
217 msg = (_("The limit has been exceeded on the number of allowed "
218 "image properties. Attempted: %(num)s, Maximum: "
219 "%(quota)s") % {'num': len(props),
220 'quota': CONF.image_property_quota})
221 LOG.warn(msg)
222 raise HTTPRequestEntityTooLarge(explanation=msg,
223 request=req,
224 content_type="text/plain")
225
226 def _enforce_create_protected_props(self, create_props, req):
227 """
228 Check request is permitted to create certain properties
229
230 :param create_props: List of properties to check
231 :param req: The WSGI/Webob Request object
232
233 :raises HTTPForbidden: if request forbidden to create a property
234 """
235 if property_utils.is_property_protection_enabled():
236 for key in create_props:
237 if (self.prop_enforcer.check_property_rules(
238 key, 'create', req.context) is False):
239 msg = _("Property '%s' is protected") % key
240 LOG.warn(msg)
241 raise HTTPForbidden(explanation=msg,
242 request=req,
243 content_type="text/plain")
244
245 def _enforce_read_protected_props(self, image_meta, req):
246 """
247 Remove entries from metadata properties if they are read protected
248
249 :param image_meta: Mapping of metadata about image
250 :param req: The WSGI/Webob Request object
251 """
252 if property_utils.is_property_protection_enabled():
253 for key in list(image_meta['properties'].keys()):
254 if (self.prop_enforcer.check_property_rules(
255 key, 'read', req.context) is False):
256 image_meta['properties'].pop(key)
257
258 def _enforce_update_protected_props(self, update_props, image_meta,
259 orig_meta, req):
260 """
261 Check request is permitted to update certain properties. Read
262 permission is required to delete a property.
263
264 If the property value is unchanged, i.e. a noop, it is permitted,
265 however, it is important to ensure read access first. Otherwise the
266 value could be discovered using brute force.
267
268 :param update_props: List of properties to check
269 :param image_meta: Mapping of proposed new metadata about image
270 :param orig_meta: Mapping of existing metadata about image
271 :param req: The WSGI/Webob Request object
272
273 :raises HTTPForbidden: if request forbidden to create a property
274 """
275 if property_utils.is_property_protection_enabled():
276 for key in update_props:
277 has_read = self.prop_enforcer.check_property_rules(
278 key, 'read', req.context)
279 if ((self.prop_enforcer.check_property_rules(
280 key, 'update', req.context) is False and
281 image_meta['properties'][key] !=
282 orig_meta['properties'][key]) or not has_read):
283 msg = _("Property '%s' is protected") % key
284 LOG.warn(msg)
285 raise HTTPForbidden(explanation=msg,
286 request=req,
287 content_type="text/plain")
288
289 def _enforce_delete_protected_props(self, delete_props, image_meta,
290 orig_meta, req):
291 """
292 Check request is permitted to delete certain properties. Read
293 permission is required to delete a property.
294
295 Note, the absence of a property in a request does not necessarily
296 indicate a delete. The requester may not have read access, and so can
297 not know the property exists. Hence, read access is a requirement for
298 delete, otherwise the delete is ignored transparently.
299
300 :param delete_props: List of properties to check
301 :param image_meta: Mapping of proposed new metadata about image
302 :param orig_meta: Mapping of existing metadata about image
303 :param req: The WSGI/Webob Request object
304
305 :raises HTTPForbidden: if request forbidden to create a property
306 """
307 if property_utils.is_property_protection_enabled():
308 for key in delete_props:
309 if (self.prop_enforcer.check_property_rules(
310 key, 'read', req.context) is False):
311 # NOTE(bourke): if read protected, re-add to image_meta to
312 # prevent deletion
313 image_meta['properties'][key] = orig_meta[
314 'properties'][key]
315 elif (self.prop_enforcer.check_property_rules(
316 key, 'delete', req.context) is False):
317 msg = _("Property '%s' is protected") % key
318 LOG.warn(msg)
319 raise HTTPForbidden(explanation=msg,
320 request=req,
321 content_type="text/plain")
322
323 def index(self, req):
324 """
325 Returns the following information for all public, available images:
326
327 * id -- The opaque image identifier
328 * name -- The name of the image
329 * disk_format -- The disk image format
330 * container_format -- The "container" format of the image
331 * checksum -- MD5 checksum of the image data
332 * size -- Size of image data in bytes
333
334 :param req: The WSGI/Webob Request object
335 :returns: The response body is a mapping of the following form
336
337 ::
338
339 {'images': [
340 {'id': <ID>,
341 'name': <NAME>,
342 'disk_format': <DISK_FORMAT>,
343 'container_format': <DISK_FORMAT>,
344 'checksum': <CHECKSUM>,
345 'size': <SIZE>}, {...}]
346 }
347
348 """
349 self._enforce(req, 'get_images')
350 params = self._get_query_params(req)
351 try:
352 images = registry.get_images_list(req.context, **params)
353 except exception.Invalid as e:
354 raise HTTPBadRequest(explanation=e.msg, request=req)
355
356 return dict(images=images)
357
358 def detail(self, req):
359 """
360 Returns detailed information for all available images
361
362 :param req: The WSGI/Webob Request object
363 :returns: The response body is a mapping of the following form
364
365 ::
366
367 {'images':
368 [{
369 'id': <ID>,
370 'name': <NAME>,
371 'size': <SIZE>,
372 'disk_format': <DISK_FORMAT>,
373 'container_format': <CONTAINER_FORMAT>,
374 'checksum': <CHECKSUM>,
375 'min_disk': <MIN_DISK>,
376 'min_ram': <MIN_RAM>,
377 'store': <STORE>,
378 'status': <STATUS>,
379 'created_at': <TIMESTAMP>,
380 'updated_at': <TIMESTAMP>,
381 'deleted_at': <TIMESTAMP>|<NONE>,
382 'properties': {'distro': 'Ubuntu 10.04 LTS', {...}}
383 }, {...}]
384 }
385
386 """
387 if req.method == 'HEAD':
388 msg = (_("This operation is currently not permitted on "
389 "Glance images details."))
390 raise HTTPMethodNotAllowed(explanation=msg,
391 headers={'Allow': 'GET'},
392 body_template='${explanation}')
393 self._enforce(req, 'get_images')
394 params = self._get_query_params(req)
395 try:
396 images = registry.get_images_detail(req.context, **params)
397 # Strip out the Location attribute. Temporary fix for
398 # LP Bug #755916. This information is still coming back
399 # from the registry, since the API server still needs access
400 # to it, however we do not return this potential security
401 # information to the API end user...
402 for image in images:
403 redact_loc(image, copy_dict=False)
404 self._enforce_read_protected_props(image, req)
405 except exception.Invalid as e:
406 raise HTTPBadRequest(explanation=e.msg, request=req)
407 except exception.NotAuthenticated as e:
408 raise HTTPUnauthorized(explanation=e.msg, request=req)
409 return dict(images=images)
410
411 def _get_query_params(self, req):
412 """
413 Extracts necessary query params from request.
414
415 :param req: the WSGI Request object
416 :returns: dict of parameters that can be used by registry client
417 """
418 params = {'filters': self._get_filters(req)}
419
420 for PARAM in SUPPORTED_PARAMS:
421 if PARAM in req.params:
422 params[PARAM] = req.params.get(PARAM)
423
424 # Fix for LP Bug #1132294
425 # Ensure all shared images are returned in v1
426 params['member_status'] = 'all'
427 return params
428
429 def _get_filters(self, req):
430 """
431 Return a dictionary of query param filters from the request
432
433 :param req: the Request object coming from the wsgi layer
434 :returns: a dict of key/value filters
435 """
436 query_filters = {}
437 for param in req.params:
438 if param in SUPPORTED_FILTERS or param.startswith('property-'):
439 query_filters[param] = req.params.get(param)
440 if not filters.validate(param, query_filters[param]):
441 raise HTTPBadRequest(_('Bad value passed to filter '
442 '%(filter)s got %(val)s')
443 % {'filter': param,
444 'val': query_filters[param]})
445 return query_filters
446
447 def meta(self, req, id):
448 """
449 Returns metadata about an image in the HTTP headers of the
450 response object
451
452 :param req: The WSGI/Webob Request object
453 :param id: The opaque image identifier
454 :returns: similar to 'show' method but without image_data
455
456 :raises HTTPNotFound: if image metadata is not available to user
457 """
458 self._enforce(req, 'get_image')
459 image_meta = self.get_image_meta_or_404(req, id)
460 image_meta = redact_loc(image_meta)
461 self._enforce_read_protected_props(image_meta, req)
462 return {
463 'image_meta': image_meta
464 }
465
466 @staticmethod
467 def _validate_source(source, req):
468 """
469 Validate if external sources (as specified via the location
470 or copy-from headers) are supported. Otherwise we reject
471 with 400 "Bad Request".
472 """
473 if store_utils.validate_external_location(source):
474 return source
475 else:
476 if source:
477 msg = _("External sources are not supported: '%s'") % source
478 else:
479 msg = _("External source should not be empty")
480 LOG.warn(msg)
481 raise HTTPBadRequest(explanation=msg,
482 request=req,
483 content_type="text/plain")
484
485 @staticmethod
486 def _copy_from(req):
487 return req.headers.get('x-glance-api-copy-from')
488
489 def _external_source(self, image_meta, req):
490 if 'location' in image_meta:
491 self._enforce(req, 'set_image_location')
492 source = image_meta['location']
493 elif 'x-glance-api-copy-from' in req.headers:
494 source = Controller._copy_from(req)
495 else:
496 # we have an empty external source value
497 # so we are creating "draft" of the image and no need validation
498 return None
499 return Controller._validate_source(source, req)
500
501 @staticmethod
502 def _get_from_store(context, where, dest=None):
503 try:
504 loc = glance_store.location.get_location_from_uri(where)
505 src_store = store.get_store_from_uri(where)
506
507 if dest is not None:
508 src_store.READ_CHUNKSIZE = dest.WRITE_CHUNKSIZE
509
510 image_data, image_size = src_store.get(loc, context=context)
511
512 except store.RemoteServiceUnavailable as e:
513 raise HTTPServiceUnavailable(explanation=e.msg)
514 except store.NotFound as e:
515 raise HTTPNotFound(explanation=e.msg)
516 except (store.StoreGetNotSupported,
517 store.StoreRandomGetNotSupported,
518 store.UnknownScheme) as e:
519 raise HTTPBadRequest(explanation=e.msg)
520 image_size = int(image_size) if image_size else None
521 return image_data, image_size
522
523 def show(self, req, id):
524 """
525 Returns an iterator that can be used to retrieve an image's
526 data along with the image metadata.
527
528 :param req: The WSGI/Webob Request object
529 :param id: The opaque image identifier
530
531 :raises HTTPNotFound: if image is not available to user
532 """
533
534 self._enforce(req, 'get_image')
535
536 try:
537 image_meta = self.get_active_image_meta_or_error(req, id)
538 except HTTPNotFound:
539 # provision for backward-compatibility breaking issue
540 # catch the 404 exception and raise it after enforcing
541 # the policy
542 with excutils.save_and_reraise_exception():
543 self._enforce(req, 'download_image')
544 else:
545 target = utils.create_mashup_dict(image_meta)
546 self._enforce(req, 'download_image', target=target)
547
548 self._enforce_read_protected_props(image_meta, req)
549
550 if image_meta.get('size') == 0:
551 image_iterator = iter([])
552 else:
553 image_iterator, size = self._get_from_store(req.context,
554 image_meta['location'])
555 image_iterator = utils.cooperative_iter(image_iterator)
556 image_meta['size'] = size or image_meta['size']
557 image_meta = redact_loc(image_meta)
558 return {
559 'image_iterator': image_iterator,
560 'image_meta': image_meta,
561 }
562
563 def _reserve(self, req, image_meta):
564 """
565 Adds the image metadata to the registry and assigns
566 an image identifier if one is not supplied in the request
567 headers. Sets the image's status to `queued`.
568
569 :param req: The WSGI/Webob Request object
570 :param id: The opaque image identifier
571 :param image_meta: The image metadata
572
573 :raises HTTPConflict: if image already exists
574 :raises HTTPBadRequest: if image metadata is not valid
575 """
576 location = self._external_source(image_meta, req)
577 scheme = image_meta.get('store')
578 if scheme and scheme not in store.get_known_schemes():
579 msg = _("Required store %s is invalid") % scheme
580 LOG.warn(msg)
581 raise HTTPBadRequest(explanation=msg,
582 content_type='text/plain')
583
584 image_meta['status'] = ('active' if image_meta.get('size') == 0
585 else 'queued')
586
587 if location:
588 try:
589 backend = store.get_store_from_location(location)
590 except (store.UnknownScheme, store.BadStoreUri):
591 LOG.debug("Invalid location %s", location)
592 msg = _("Invalid location %s") % location
593 raise HTTPBadRequest(explanation=msg,
594 request=req,
595 content_type="text/plain")
596 # check the store exists before we hit the registry, but we
597 # don't actually care what it is at this point
598 self.get_store_or_400(req, backend)
599
600 # retrieve the image size from remote store (if not provided)
601 image_meta['size'] = self._get_size(req.context, image_meta,
602 location)
603 else:
604 # Ensure that the size attribute is set to zero for directly
605 # uploadable images (if not provided). The size will be set
606 # to a non-zero value during upload
607 image_meta['size'] = image_meta.get('size', 0)
608
609 try:
610 image_meta = registry.add_image_metadata(req.context, image_meta)
611 self.notifier.info("image.create", redact_loc(image_meta))
612 return image_meta
613 except exception.Duplicate:
614 msg = (_("An image with identifier %s already exists") %
615 image_meta['id'])
616 LOG.warn(msg)
617 raise HTTPConflict(explanation=msg,
618 request=req,
619 content_type="text/plain")
620 except exception.Invalid as e:
621 msg = (_("Failed to reserve image. Got error: %s") %
622 encodeutils.exception_to_unicode(e))
623 LOG.exception(msg)
624 raise HTTPBadRequest(explanation=msg,
625 request=req,
626 content_type="text/plain")
627 except exception.Forbidden:
628 msg = _("Forbidden to reserve image.")
629 LOG.warn(msg)
630 raise HTTPForbidden(explanation=msg,
631 request=req,
632 content_type="text/plain")
633
634 def _upload(self, req, image_meta):
635 """
636 Uploads the payload of the request to a backend store in
637 Glance. If the `x-image-meta-store` header is set, Glance
638 will attempt to use that scheme; if not, Glance will use the
639 scheme set by the flag `default_store` to find the backing store.
640
641 :param req: The WSGI/Webob Request object
642 :param image_meta: Mapping of metadata about image
643
644 :raises HTTPConflict: if image already exists
645 :returns: The location where the image was stored
646 """
647
648 scheme = req.headers.get('x-image-meta-store',
649 CONF.glance_store.default_store)
650
651 store = self.get_store_or_400(req, scheme)
652
653 copy_from = self._copy_from(req)
654 if copy_from:
655 try:
656 image_data, image_size = self._get_from_store(req.context,
657 copy_from,
658 dest=store)
659 except Exception:
660 upload_utils.safe_kill(req, image_meta['id'], 'queued')
661 msg = (_LE("Copy from external source '%(scheme)s' failed for "
662 "image: %(image)s") %
663 {'scheme': scheme, 'image': image_meta['id']})
664 LOG.exception(msg)
665 return
666 image_meta['size'] = image_size or image_meta['size']
667 else:
668 try:
669 req.get_content_type(('application/octet-stream',))
670 except exception.InvalidContentType:
671 upload_utils.safe_kill(req, image_meta['id'], 'queued')
672 msg = _("Content-Type must be application/octet-stream")
673 LOG.warn(msg)
674 raise HTTPBadRequest(explanation=msg)
675
676 image_data = req.body_file
677
678 image_id = image_meta['id']
679 LOG.debug("Setting image %s to status 'saving'", image_id)
680 registry.update_image_metadata(req.context, image_id,
681 {'status': 'saving'})
682
683 LOG.debug("Uploading image data for image %(image_id)s "
684 "to %(scheme)s store", {'image_id': image_id,
685 'scheme': scheme})
686
687 self.notifier.info("image.prepare", redact_loc(image_meta))
688
689 image_meta, location_data = upload_utils.upload_data_to_store(
690 req, image_meta, image_data, store, self.notifier)
691
692 self.notifier.info('image.upload', redact_loc(image_meta))
693
694 return location_data
695
696 def _activate(self, req, image_id, location_data, from_state=None):
697 """
698 Sets the image status to `active` and the image's location
699 attribute.
700
701 :param req: The WSGI/Webob Request object
702 :param image_id: Opaque image identifier
703 :param location_data: Location of where Glance stored this image
704 """
705 image_meta = {
706 'location': location_data['url'],
707 'status': 'active',
708 'location_data': [location_data]
709 }
710
711 try:
712 s = from_state
713 image_meta_data = registry.update_image_metadata(req.context,
714 image_id,
715 image_meta,
716 from_state=s)
717 self.notifier.info("image.activate", redact_loc(image_meta_data))
718 self.notifier.info("image.update", redact_loc(image_meta_data))
719 return image_meta_data
720 except exception.Duplicate:
721 with excutils.save_and_reraise_exception():
722 # Delete image data since it has been superseded by another
723 # upload and re-raise.
724 LOG.debug("duplicate operation - deleting image data for "
725 " %(id)s (location:%(location)s)",
726 {'id': image_id, 'location': image_meta['location']})
727 upload_utils.initiate_deletion(req, location_data, image_id)
728 except exception.Invalid as e:
729 msg = (_("Failed to activate image. Got error: %s") %
730 encodeutils.exception_to_unicode(e))
731 LOG.warn(msg)
732 raise HTTPBadRequest(explanation=msg,
733 request=req,
734 content_type="text/plain")
735
736 def _upload_and_activate(self, req, image_meta):
737 """
738 Safely uploads the image data in the request payload
739 and activates the image in the registry after a successful
740 upload.
741
742 :param req: The WSGI/Webob Request object
743 :param image_meta: Mapping of metadata about image
744
745 :returns: Mapping of updated image data
746 """
747 location_data = self._upload(req, image_meta)
748 image_id = image_meta['id']
749 LOG.info(_LI("Uploaded data of image %s from request "
750 "payload successfully."), image_id)
751
752 if location_data:
753 try:
754 image_meta = self._activate(req,
755 image_id,
756 location_data,
757 from_state='saving')
758 except exception.Duplicate:
759 raise
760 except Exception:
761 with excutils.save_and_reraise_exception():
762 # NOTE(zhiyan): Delete image data since it has already
763 # been added to store by above _upload() call.
764 LOG.warn(_LW("Failed to activate image %s in "
765 "registry. About to delete image "
766 "bits from store and update status "
767 "to 'killed'.") % image_id)
768 upload_utils.initiate_deletion(req, location_data,
769 image_id)
770 upload_utils.safe_kill(req, image_id, 'saving')
771 else:
772 image_meta = None
773
774 return image_meta
775
776 def _get_size(self, context, image_meta, location):
777 # retrieve the image size from remote store (if not provided)
778 try:
779 return (image_meta.get('size', 0) or
780 store.get_size_from_backend(location, context=context))
781 except store.NotFound as e:
782 # NOTE(rajesht): The exception is logged as debug message because
783 # the image is located at third-party server and it has nothing to
784 # do with glance. If log.exception is used here, in that case the
785 # log file might be flooded with exception log messages if
786 # malicious user keeps on trying image-create using non-existent
787 # location url. Used log.debug because administrator can
788 # disable debug logs.
789 LOG.debug(encodeutils.exception_to_unicode(e))
790 raise HTTPNotFound(explanation=e.msg, content_type="text/plain")
791 except (store.UnknownScheme, store.BadStoreUri) as e:
792 # NOTE(rajesht): See above note of store.NotFound
793 LOG.debug(encodeutils.exception_to_unicode(e))
794 raise HTTPBadRequest(explanation=e.msg, content_type="text/plain")
795
796 def _handle_source(self, req, image_id, image_meta, image_data):
797 copy_from = self._copy_from(req)
798 location = image_meta.get('location')
799 sources = [obj for obj in (copy_from, location, image_data) if obj]
800 if len(sources) >= 2:
801 msg = _("It's invalid to provide multiple image sources.")
802 LOG.warn(msg)
803 raise HTTPBadRequest(explanation=msg,
804 request=req,
805 content_type="text/plain")
806 if len(sources) == 0:
807 return image_meta
808 if image_data:
809 image_meta = self._validate_image_for_activation(req,
810 image_id,
811 image_meta)
812 image_meta = self._upload_and_activate(req, image_meta)
813 elif copy_from:
814 msg = _LI('Triggering asynchronous copy from external source')
815 LOG.info(msg)
816 pool = common.get_thread_pool("copy_from_eventlet_pool")
817 pool.spawn_n(self._upload_and_activate, req, image_meta)
818 else:
819 if location:
820 self._validate_image_for_activation(req, image_id, image_meta)
821 image_size_meta = image_meta.get('size')
822 if image_size_meta:
823 try:
824 image_size_store = store.get_size_from_backend(
825 location, req.context)
826 except (store.BadStoreUri, store.UnknownScheme) as e:
827 LOG.debug(encodeutils.exception_to_unicode(e))
828 raise HTTPBadRequest(explanation=e.msg,
829 request=req,
830 content_type="text/plain")
831 # NOTE(zhiyan): A returned size of zero usually means
832 # the driver encountered an error. In this case the
833 # size provided by the client will be used as-is.
834 if (image_size_store and
835 image_size_store != image_size_meta):
836 msg = (_("Provided image size must match the stored"
837 " image size. (provided size: %(ps)d, "
838 "stored size: %(ss)d)") %
839 {"ps": image_size_meta,
840 "ss": image_size_store})
841 LOG.warn(msg)
842 raise HTTPConflict(explanation=msg,
843 request=req,
844 content_type="text/plain")
845 location_data = {'url': location, 'metadata': {},
846 'status': 'active'}
847 image_meta = self._activate(req, image_id, location_data)
848 return image_meta
849
850 def _validate_image_for_activation(self, req, id, values):
851 """Ensures that all required image metadata values are valid."""
852 image = self.get_image_meta_or_404(req, id)
853 if values['disk_format'] is None:
854 if not image['disk_format']:
855 msg = _("Disk format is not specified.")
856 raise HTTPBadRequest(explanation=msg, request=req)
857 values['disk_format'] = image['disk_format']
858 if values['container_format'] is None:
859 if not image['container_format']:
860 msg = _("Container format is not specified.")
861 raise HTTPBadRequest(explanation=msg, request=req)
862 values['container_format'] = image['container_format']
863 if 'name' not in values:
864 values['name'] = image['name']
865
866 values = validate_image_meta(req, values)
867 return values
868
869 @utils.mutating
870 def create(self, req, image_meta, image_data):
871 """
872 Adds a new image to Glance. Four scenarios exist when creating an
873 image:
874
875 1. If the image data is available directly for upload, create can be
876 passed the image data as the request body and the metadata as the
877 request headers. The image will initially be 'queued', during
878 upload it will be in the 'saving' status, and then 'killed' or
879 'active' depending on whether the upload completed successfully.
880
881 2. If the image data exists somewhere else, you can upload indirectly
882 from the external source using the x-glance-api-copy-from header.
883 Once the image is uploaded, the external store is not subsequently
884 consulted, i.e. the image content is served out from the configured
885 glance image store. State transitions are as for option #1.
886
887 3. If the image data exists somewhere else, you can reference the
888 source using the x-image-meta-location header. The image content
889 will be served out from the external store, i.e. is never uploaded
890 to the configured glance image store.
891
892 4. If the image data is not available yet, but you'd like reserve a
893 spot for it, you can omit the data and a record will be created in
894 the 'queued' state. This exists primarily to maintain backwards
895 compatibility with OpenStack/Rackspace API semantics.
896
897 The request body *must* be encoded as application/octet-stream,
898 otherwise an HTTPBadRequest is returned.
899
900 Upon a successful save of the image data and metadata, a response
901 containing metadata about the image is returned, including its
902 opaque identifier.
903
904 :param req: The WSGI/Webob Request object
905 :param image_meta: Mapping of metadata about image
906 :param image_data: Actual image data that is to be stored
907
908 :raises HTTPBadRequest: if x-image-meta-location is missing
909 and the request body is not application/octet-stream
910 image data.
911 """
912 self._enforce(req, 'add_image')
913 is_public = image_meta.get('is_public')
914 if is_public:
915 self._enforce(req, 'publicize_image')
916 if Controller._copy_from(req):
917 self._enforce(req, 'copy_from')
918 if image_data or Controller._copy_from(req):
919 self._enforce(req, 'upload_image')
920
921 self._enforce_create_protected_props(image_meta['properties'].keys(),
922 req)
923
924 self._enforce_image_property_quota(image_meta, req=req)
925
926 image_meta = self._reserve(req, image_meta)
927 id = image_meta['id']
928
929 image_meta = self._handle_source(req, id, image_meta, image_data)
930
931 location_uri = image_meta.get('location')
932 if location_uri:
933 self.update_store_acls(req, id, location_uri, public=is_public)
934
935 # Prevent client from learning the location, as it
936 # could contain security credentials
937 image_meta = redact_loc(image_meta)
938
939 return {'image_meta': image_meta}
940
941 @utils.mutating
942 def update(self, req, id, image_meta, image_data):
943 """
944 Updates an existing image with the registry.
945
946 :param request: The WSGI/Webob Request object
947 :param id: The opaque image identifier
948
949 :returns: Returns the updated image information as a mapping
950 """
951 self._enforce(req, 'modify_image')
952 is_public = image_meta.get('is_public')
953 if is_public:
954 self._enforce(req, 'publicize_image')
955 if Controller._copy_from(req):
956 self._enforce(req, 'copy_from')
957 if image_data or Controller._copy_from(req):
958 self._enforce(req, 'upload_image')
959
960 orig_image_meta = self.get_image_meta_or_404(req, id)
961 orig_status = orig_image_meta['status']
962
963 # Do not allow any updates on a deleted image.
964 # Fix for LP Bug #1060930
965 if orig_status == 'deleted':
966 msg = _("Forbidden to update deleted image.")
967 raise HTTPForbidden(explanation=msg,
968 request=req,
969 content_type="text/plain")
970
971 if req.context.is_admin is False:
972 # Once an image is 'active' only an admin can
973 # modify certain core metadata keys
974 for key in ACTIVE_IMMUTABLE:
975 if ((orig_status == 'active' or orig_status == 'deactivated')
976 and key in image_meta
977 and image_meta.get(key) != orig_image_meta.get(key)):
978 msg = _("Forbidden to modify '%(key)s' of %(status)s "
979 "image.") % {'key': key, 'status': orig_status}
980 raise HTTPForbidden(explanation=msg,
981 request=req,
982 content_type="text/plain")
983
984 for key in IMMUTABLE:
985 if (key in image_meta and
986 image_meta.get(key) != orig_image_meta.get(key)):
987 msg = _("Forbidden to modify '%s' of image.") % key
988 raise HTTPForbidden(explanation=msg,
989 request=req,
990 content_type="text/plain")
991
992 # The default behaviour for a PUT /images/<IMAGE_ID> is to
993 # override any properties that were previously set. This, however,
994 # leads to a number of issues for the common use case where a caller
995 # registers an image with some properties and then almost immediately
996 # uploads an image file along with some more properties. Here, we
997 # check for a special header value to be false in order to force
998 # properties NOT to be purged. However we also disable purging of
999 # properties if an image file is being uploaded...
1000 purge_props = req.headers.get('x-glance-registry-purge-props', True)
1001 purge_props = (strutils.bool_from_string(purge_props) and
1002 image_data is None)
1003
1004 if image_data is not None and orig_status != 'queued':
1005 raise HTTPConflict(_("Cannot upload to an unqueued image"))
1006
1007 # Only allow the Location|Copy-From fields to be modified if the
1008 # image is in queued status, which indicates that the user called
1009 # POST /images but originally supply neither a Location|Copy-From
1010 # field NOR image data
1011 location = self._external_source(image_meta, req)
1012 reactivating = orig_status != 'queued' and location
1013 activating = orig_status == 'queued' and (location or image_data)
1014
1015 # Make image public in the backend store (if implemented)
1016 orig_or_updated_loc = location or orig_image_meta.get('location')
1017 if orig_or_updated_loc:
1018 try:
1019 if is_public is not None or location is not None:
1020 self.update_store_acls(req, id, orig_or_updated_loc,
1021 public=is_public)
1022 except store.BadStoreUri:
1023 msg = _("Invalid location: %s") % location
1024 LOG.warn(msg)
1025 raise HTTPBadRequest(explanation=msg,
1026 request=req,
1027 content_type="text/plain")
1028
1029 if reactivating:
1030 msg = _("Attempted to update Location field for an image "
1031 "not in queued status.")
1032 raise HTTPBadRequest(explanation=msg,
1033 request=req,
1034 content_type="text/plain")
1035
1036 # ensure requester has permissions to create/update/delete properties
1037 # according to property-protections.conf
1038 orig_keys = set(orig_image_meta['properties'])
1039 new_keys = set(image_meta['properties'])
1040 self._enforce_update_protected_props(
1041 orig_keys.intersection(new_keys), image_meta,
1042 orig_image_meta, req)
1043 self._enforce_create_protected_props(
1044 new_keys.difference(orig_keys), req)
1045 if purge_props:
1046 self._enforce_delete_protected_props(
1047 orig_keys.difference(new_keys), image_meta,
1048 orig_image_meta, req)
1049
1050 self._enforce_image_property_quota(image_meta,
1051 orig_image_meta=orig_image_meta,
1052 purge_props=purge_props,
1053 req=req)
1054
1055 try:
1056 if location:
1057 image_meta['size'] = self._get_size(req.context, image_meta,
1058 location)
1059
1060 image_meta = registry.update_image_metadata(req.context,
1061 id,
1062 image_meta,
1063 purge_props)
1064
1065 if activating:
1066 image_meta = self._handle_source(req, id, image_meta,
1067 image_data)
1068
1069 except exception.Invalid as e:
1070 msg = (_("Failed to update image metadata. Got error: %s") %
1071 encodeutils.exception_to_unicode(e))
1072 LOG.warn(msg)
1073 raise HTTPBadRequest(explanation=msg,
1074 request=req,
1075 content_type="text/plain")
1076 except exception.ImageNotFound as e:
1077 msg = (_("Failed to find image to update: %s") %
1078 encodeutils.exception_to_unicode(e))
1079 LOG.warn(msg)
1080 raise HTTPNotFound(explanation=msg,
1081 request=req,
1082 content_type="text/plain")
1083 except exception.Forbidden as e:
1084 msg = (_("Forbidden to update image: %s") %
1085 encodeutils.exception_to_unicode(e))
1086 LOG.warn(msg)
1087 raise HTTPForbidden(explanation=msg,
1088 request=req,
1089 content_type="text/plain")
1090 except (exception.Conflict, exception.Duplicate) as e:
1091 LOG.warn(encodeutils.exception_to_unicode(e))
1092 raise HTTPConflict(body=_('Image operation conflicts'),
1093 request=req,
1094 content_type='text/plain')
1095 else:
1096 self.notifier.info('image.update', redact_loc(image_meta))
1097
1098 # Prevent client from learning the location, as it
1099 # could contain security credentials
1100 image_meta = redact_loc(image_meta)
1101
1102 self._enforce_read_protected_props(image_meta, req)
1103
1104 return {'image_meta': image_meta}
1105
1106 @utils.mutating
1107 def delete(self, req, id):
1108 """
1109 Deletes the image and all its chunks from the Glance
1110
1111 :param req: The WSGI/Webob Request object
1112 :param id: The opaque image identifier
1113
1114 :raises HttpBadRequest: if image registry is invalid
1115 :raises HttpNotFound: if image or any chunk is not available
1116 :raises HttpUnauthorized: if image or any chunk is not
1117 deleteable by the requesting user
1118 """
1119 self._enforce(req, 'delete_image')
1120
1121 image = self.get_image_meta_or_404(req, id)
1122 if image['protected']:
1123 msg = _("Image is protected")
1124 LOG.warn(msg)
1125 raise HTTPForbidden(explanation=msg,
1126 request=req,
1127 content_type="text/plain")
1128
1129 if image['status'] == 'pending_delete':
1130 msg = (_("Forbidden to delete a %s image.") %
1131 image['status'])
1132 LOG.warn(msg)
1133 raise HTTPForbidden(explanation=msg,
1134 request=req,
1135 content_type="text/plain")
1136 elif image['status'] == 'deleted':
1137 msg = _("Image %s not found.") % id
1138 LOG.warn(msg)
1139 raise HTTPNotFound(explanation=msg, request=req,
1140 content_type="text/plain")
1141
1142 if image['location'] and CONF.delayed_delete:
1143 status = 'pending_delete'
1144 else:
1145 status = 'deleted'
1146
1147 ori_status = image['status']
1148
1149 try:
1150 # Update the image from the registry first, since we rely on it
1151 # for authorization checks.
1152 # See https://bugs.launchpad.net/glance/+bug/1065187
1153 image = registry.update_image_metadata(req.context, id,
1154 {'status': status})
1155
1156 try:
1157 # The image's location field may be None in the case
1158 # of a saving or queued image, therefore don't ask a backend
1159 # to delete the image if the backend doesn't yet store it.
1160 # See https://bugs.launchpad.net/glance/+bug/747799
1161 if image['location']:
1162 for loc_data in image['location_data']:
1163 if loc_data['status'] == 'active':
1164 upload_utils.initiate_deletion(req, loc_data, id)
1165 except Exception:
1166 with excutils.save_and_reraise_exception():
1167 registry.update_image_metadata(req.context, id,
1168 {'status': ori_status})
1169
1170 registry.delete_image_metadata(req.context, id)
1171 except exception.ImageNotFound as e:
1172 msg = (_("Failed to find image to delete: %s") %
1173 encodeutils.exception_to_unicode(e))
1174 LOG.warn(msg)
1175 raise HTTPNotFound(explanation=msg,
1176 request=req,
1177 content_type="text/plain")
1178 except exception.Forbidden as e:
1179 msg = (_("Forbidden to delete image: %s") %
1180 encodeutils.exception_to_unicode(e))
1181 LOG.warn(msg)
1182 raise HTTPForbidden(explanation=msg,
1183 request=req,
1184 content_type="text/plain")
1185 except store.InUseByStore as e:
1186 msg = (_("Image %(id)s could not be deleted because it is in use: "
1187 "%(exc)s")
1188 % {"id": id, "exc": encodeutils.exception_to_unicode(e)})
1189 LOG.warn(msg)
1190 raise HTTPConflict(explanation=msg,
1191 request=req,
1192 content_type="text/plain")
1193 else:
1194 self.notifier.info('image.delete', redact_loc(image))
1195 return Response(body='', status=200)
1196
1197 def get_store_or_400(self, request, scheme):
1198 """
1199 Grabs the storage backend for the supplied store name
1200 or raises an HTTPBadRequest (400) response
1201
1202 :param request: The WSGI/Webob Request object
1203 :param scheme: The backend store scheme
1204
1205 :raises HTTPBadRequest: if store does not exist
1206 """
1207 try:
1208 return store.get_store_from_scheme(scheme)
1209 except store.UnknownScheme:
1210 msg = _("Store for scheme %s not found") % scheme
1211 LOG.warn(msg)
1212 raise HTTPBadRequest(explanation=msg,
1213 request=request,
1214 content_type='text/plain')
1215
1216
1217class ImageDeserializer(wsgi.JSONRequestDeserializer):
1218 """Handles deserialization of specific controller method requests."""
1219
1220 def _deserialize(self, request):
1221 result = {}
1222 try:
1223 result['image_meta'] = utils.get_image_meta_from_headers(request)
1224 except exception.InvalidParameterValue as e:
1225 msg = encodeutils.exception_to_unicode(e)
1226 LOG.warn(msg, exc_info=True)
1227 raise HTTPBadRequest(explanation=e.msg, request=request)
1228
1229 image_meta = result['image_meta']
1230 image_meta = validate_image_meta(request, image_meta)
1231 if request.content_length:
1232 image_size = request.content_length
1233 elif 'size' in image_meta:
1234 image_size = image_meta['size']
1235 else:
1236 image_size = None
1237
1238 data = request.body_file if self.has_body(request) else None
1239
1240 if image_size is None and data is not None:
1241 data = utils.LimitingReader(data, CONF.image_size_cap)
1242
1243 # NOTE(bcwaldon): this is a hack to make sure the downstream code
1244 # gets the correct image data
1245 request.body_file = data
1246
1247 elif image_size is not None and image_size > CONF.image_size_cap:
1248 max_image_size = CONF.image_size_cap
1249 msg = (_("Denying attempt to upload image larger than %d"
1250 " bytes.") % max_image_size)
1251 LOG.warn(msg)
1252 raise HTTPBadRequest(explanation=msg, request=request)
1253
1254 result['image_data'] = data
1255 return result
1256
1257 def create(self, request):
1258 return self._deserialize(request)
1259
1260 def update(self, request):
1261 return self._deserialize(request)
1262
1263
1264class ImageSerializer(wsgi.JSONResponseSerializer):
1265 """Handles serialization of specific controller method responses."""
1266
1267 def __init__(self):
1268 self.notifier = notifier.Notifier()
1269
1270 def _inject_location_header(self, response, image_meta):
1271 location = self._get_image_location(image_meta)
1272 if six.PY2:
1273 location = location.encode('utf-8')
1274 response.headers['Location'] = location
1275
1276 def _inject_checksum_header(self, response, image_meta):
1277 if image_meta['checksum'] is not None:
1278 checksum = image_meta['checksum']
1279 if six.PY2:
1280 checksum = checksum.encode('utf-8')
1281 response.headers['ETag'] = checksum
1282
1283 def _inject_image_meta_headers(self, response, image_meta):
1284 """
1285 Given a response and mapping of image metadata, injects
1286 the Response with a set of HTTP headers for the image
1287 metadata. Each main image metadata field is injected
1288 as a HTTP header with key 'x-image-meta-<FIELD>' except
1289 for the properties field, which is further broken out
1290 into a set of 'x-image-meta-property-<KEY>' headers
1291
1292 :param response: The Webob Response object
1293 :param image_meta: Mapping of image metadata
1294 """
1295 headers = utils.image_meta_to_http_headers(image_meta)
1296
1297 for k, v in headers.items():
1298 if six.PY3:
1299 response.headers[str(k)] = str(v)
1300 else:
1301 response.headers[k.encode('utf-8')] = v.encode('utf-8')
1302
1303 def _get_image_location(self, image_meta):
1304 """Build a relative url to reach the image defined by image_meta."""
1305 return "/v1/images/%s" % image_meta['id']
1306
1307 def meta(self, response, result):
1308 image_meta = result['image_meta']
1309 self._inject_image_meta_headers(response, image_meta)
1310 self._inject_checksum_header(response, image_meta)
1311 return response
1312
1313 def show(self, response, result):
1314 image_meta = result['image_meta']
1315
1316 image_iter = result['image_iterator']
1317 # image_meta['size'] should be an int, but could possibly be a str
1318 expected_size = int(image_meta['size'])
1319 response.app_iter = common.size_checked_iter(
1320 response, image_meta, expected_size, image_iter, self.notifier)
1321 # Using app_iter blanks content-length, so we set it here...
1322 response.headers['Content-Length'] = str(image_meta['size'])
1323 response.headers['Content-Type'] = 'application/octet-stream'
1324
1325 self._inject_image_meta_headers(response, image_meta)
1326 self._inject_checksum_header(response, image_meta)
1327
1328 return response
1329
1330 def update(self, response, result):
1331 image_meta = result['image_meta']
1332 response.body = self.to_json(dict(image=image_meta))
1333 response.headers['Content-Type'] = 'application/json'
1334 self._inject_checksum_header(response, image_meta)
1335 return response
1336
1337 def create(self, response, result):
1338 image_meta = result['image_meta']
1339 response.status = 201
1340 response.headers['Content-Type'] = 'application/json'
1341 response.body = self.to_json(dict(image=image_meta))
1342 self._inject_location_header(response, image_meta)
1343 self._inject_checksum_header(response, image_meta)
1344 return response
1345
1346
1347def create_resource():
1348 """Images resource factory method"""
1349 deserializer = ImageDeserializer()
1350 serializer = ImageSerializer()
1351 return wsgi.Resource(Controller(), deserializer, serializer)
diff --git a/glance/api/v1/members.py b/glance/api/v1/members.py
deleted file mode 100644
index 4233ea3..0000000
--- a/glance/api/v1/members.py
+++ /dev/null
@@ -1,248 +0,0 @@
1# Copyright 2012 OpenStack Foundation.
2# Copyright 2013 NTT corp.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from oslo_config import cfg
18from oslo_log import log as logging
19from oslo_utils import encodeutils
20import webob.exc
21
22from glance.api import policy
23from glance.api.v1 import controller
24from glance.common import exception
25from glance.common import utils
26from glance.common import wsgi
27from glance.i18n import _
28import glance.registry.client.v1.api as registry
29
30LOG = logging.getLogger(__name__)
31CONF = cfg.CONF
32CONF.import_opt('image_member_quota', 'glance.common.config')
33
34
35class Controller(controller.BaseController):
36
37 def __init__(self):
38 self.policy = policy.Enforcer()
39
40 def _check_can_access_image_members(self, context):
41 if context.owner is None and not context.is_admin:
42 raise webob.exc.HTTPUnauthorized(_("No authenticated user"))
43
44 def _enforce(self, req, action):
45 """Authorize an action against our policies"""
46 try:
47 self.policy.enforce(req.context, action, {})
48 except exception.Forbidden:
49 LOG.debug("User not permitted to perform '%s' action", action)
50 raise webob.exc.HTTPForbidden()
51
52 def _raise_404_if_image_deleted(self, req, image_id):
53 image = self.get_image_meta_or_404(req, image_id)
54 if image['status'] == 'deleted':
55 msg = _("Image with identifier %s has been deleted.") % image_id
56 raise webob.exc.HTTPNotFound(msg)
57
58 def index(self, req, image_id):
59 """
60 Return a list of dictionaries indicating the members of the
61 image, i.e., those tenants the image is shared with.
62
63 :param req: the Request object coming from the wsgi layer
64 :param image_id: The opaque image identifier
65 :returns: The response body is a mapping of the following form
66
67 ::
68
69 {'members': [
70 {'member_id': <MEMBER>,
71 'can_share': <SHARE_PERMISSION>, ...}, ...
72 ]}
73
74 """
75 self._enforce(req, 'get_members')
76 self._raise_404_if_image_deleted(req, image_id)
77
78 try:
79 members = registry.get_image_members(req.context, image_id)
80 except exception.NotFound:
81 msg = _("Image with identifier %s not found") % image_id
82 LOG.warn(msg)
83 raise webob.exc.HTTPNotFound(msg)
84 except exception.Forbidden:
85 msg = _("Unauthorized image access")
86 LOG.warn(msg)
87 raise webob.exc.HTTPForbidden(msg)
88 return dict(members=members)
89
90 @utils.mutating
91 def delete(self, req, image_id, id):
92 """
93 Removes a membership from the image.
94 """
95 self._check_can_access_image_members(req.context)
96 self._enforce(req, 'delete_member')
97 self._raise_404_if_image_deleted(req, image_id)
98
99 try:
100 registry.delete_member(req.context, image_id, id)
101 self._update_store_acls(req, image_id)
102 except exception.NotFound as e:
103 LOG.debug(encodeutils.exception_to_unicode(e))
104 raise webob.exc.HTTPNotFound(explanation=e.msg)
105 except exception.Forbidden as e:
106 LOG.debug("User not permitted to remove membership from image "
107 "'%s'", image_id)
108 raise webob.exc.HTTPNotFound(explanation=e.msg)
109
110 return webob.exc.HTTPNoContent()
111
112 def default(self, req, image_id, id, body=None):
113 """This will cover the missing 'show' and 'create' actions"""
114 raise webob.exc.HTTPMethodNotAllowed()
115
116 def _enforce_image_member_quota(self, req, attempted):
117 if CONF.image_member_quota < 0:
118 # If value is negative, allow unlimited number of members
119 return
120
121 maximum = CONF.image_member_quota
122 if attempted > maximum:
123 msg = _("The limit has been exceeded on the number of allowed "
124 "image members for this image. Attempted: %(attempted)s, "
125 "Maximum: %(maximum)s") % {'attempted': attempted,
126 'maximum': maximum}
127 raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
128 request=req)
129
130 @utils.mutating
131 def update(self, req, image_id, id, body=None):
132 """
133 Adds a membership to the image, or updates an existing one.
134 If a body is present, it is a dict with the following format
135
136 ::
137
138 {'member': {
139 'can_share': [True|False]
140 }}
141
142 If `can_share` is provided, the member's ability to share is
143 set accordingly. If it is not provided, existing memberships
144 remain unchanged and new memberships default to False.
145 """
146 self._check_can_access_image_members(req.context)
147 self._enforce(req, 'modify_member')
148 self._raise_404_if_image_deleted(req, image_id)
149
150 new_number_of_members = len(registry.get_image_members(req.context,
151 image_id)) + 1
152 self._enforce_image_member_quota(req, new_number_of_members)
153
154 # Figure out can_share
155 can_share = None
156 if body and 'member' in body and 'can_share' in body['member']:
157 can_share = bool(body['member']['can_share'])
158 try:
159 registry.add_member(req.context, image_id, id, can_share)
160 self._update_store_acls(req, image_id)
161 except exception.Invalid as e:
162 LOG.debug(encodeutils.exception_to_unicode(e))
163 raise webob.exc.HTTPBadRequest(explanation=e.msg)
164 except exception.NotFound as e:
165 LOG.debug(encodeutils.exception_to_unicode(e))
166 raise webob.exc.HTTPNotFound(explanation=e.msg)
167 except exception.Forbidden as e:
168 LOG.debug(encodeutils.exception_to_unicode(e))
169 raise webob.exc.HTTPNotFound(explanation=e.msg)
170
171 return webob.exc.HTTPNoContent()
172
173 @utils.mutating
174 def update_all(self, req, image_id, body):
175 """
176 Replaces the members of the image with those specified in the
177 body. The body is a dict with the following format
178
179 ::
180
181 {'memberships': [
182 {'member_id': <MEMBER_ID>,
183 ['can_share': [True|False]]}, ...
184 ]}
185
186 """
187 self._check_can_access_image_members(req.context)
188 self._enforce(req, 'modify_member')
189 self._raise_404_if_image_deleted(req, image_id)
190
191 memberships = body.get('memberships')
192 if memberships:
193 new_number_of_members = len(body['memberships'])
194 self._enforce_image_member_quota(req, new_number_of_members)
195
196 try:
197 registry.replace_members(req.context, image_id, body)
198 self._update_store_acls(req, image_id)
199 except exception.Invalid as e:
200 LOG.debug(encodeutils.exception_to_unicode(e))
201 raise webob.exc.HTTPBadRequest(explanation=e.msg)
202 except exception.NotFound as e:
203 LOG.debug(encodeutils.exception_to_unicode(e))
204 raise webob.exc.HTTPNotFound(explanation=e.msg)
205 except exception.Forbidden as e:
206 LOG.debug(encodeutils.exception_to_unicode(e))
207 raise webob.exc.HTTPNotFound(explanation=e.msg)
208
209 return webob.exc.HTTPNoContent()
210
211 def index_shared_images(self, req, id):
212 """
213 Retrieves list of image memberships for the given member.
214
215 :param req: the Request object coming from the wsgi layer
216 :param id: the opaque member identifier
217 :returns: The response body is a mapping of the following form
218
219 ::
220
221 {'shared_images': [
222 {'image_id': <IMAGE>,
223 'can_share': <SHARE_PERMISSION>, ...}, ...
224 ]}
225
226 """
227 try:
228 members = registry.get_member_images(req.context, id)
229 except exception.NotFound as e:
230 LOG.debug(encodeutils.exception_to_unicode(e))
231 raise webob.exc.HTTPNotFound(explanation=e.msg)
232 except exception.Forbidden as e:
233 LOG.debug(encodeutils.exception_to_unicode(e))
234 raise webob.exc.HTTPForbidden(explanation=e.msg)
235 return dict(shared_images=members)
236
237 def _update_store_acls(self, req, image_id):
238 image_meta = self.get_image_meta_or_404(req, image_id)
239 location_uri = image_meta.get('location')
240 public = image_meta.get('is_public')
241 self.update_store_acls(req, image_id, location_uri, public)
242
243
244def create_resource():
245 """Image members resource factory method"""
246 deserializer = wsgi.JSONRequestDeserializer()
247 serializer = wsgi.JSONResponseSerializer()
248 return wsgi.Resource(Controller(), deserializer, serializer)
diff --git a/glance/api/v1/router.py b/glance/api/v1/router.py
index 1b3d075..05287ea 100644
--- a/glance/api/v1/router.py
+++ b/glance/api/v1/router.py
@@ -14,8 +14,6 @@
14# under the License. 14# under the License.
15 15
16 16
17from glance.api.v1 import images
18from glance.api.v1 import members
19from glance.common import wsgi 17from glance.common import wsgi
20 18
21 19
@@ -26,84 +24,8 @@ class API(wsgi.Router):
26 def __init__(self, mapper): 24 def __init__(self, mapper):
27 reject_method_resource = wsgi.Resource(wsgi.RejectMethodController()) 25 reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
28 26
29 images_resource = images.create_resource()
30
31 mapper.connect("/", 27 mapper.connect("/",
32 controller=images_resource,
33 action="index")
34 mapper.connect("/images",
35 controller=images_resource,
36 action='index',
37 conditions={'method': ['GET']})
38 mapper.connect("/images",
39 controller=images_resource,
40 action='create',
41 conditions={'method': ['POST']})
42 mapper.connect("/images",
43 controller=reject_method_resource,
44 action='reject',
45 allowed_methods='GET, POST')
46 mapper.connect("/images/detail",
47 controller=images_resource,
48 action='detail',
49 conditions={'method': ['GET', 'HEAD']})
50 mapper.connect("/images/detail",
51 controller=reject_method_resource,
52 action='reject',
53 allowed_methods='GET, HEAD')
54 mapper.connect("/images/{id}",
55 controller=images_resource,
56 action="meta",
57 conditions=dict(method=["HEAD"]))
58 mapper.connect("/images/{id}",
59 controller=images_resource,
60 action="show",
61 conditions=dict(method=["GET"]))
62 mapper.connect("/images/{id}",
63 controller=images_resource,
64 action="update",
65 conditions=dict(method=["PUT"]))
66 mapper.connect("/images/{id}",
67 controller=images_resource,
68 action="delete",
69 conditions=dict(method=["DELETE"]))
70 mapper.connect("/images/{id}",
71 controller=reject_method_resource,
72 action='reject',
73 allowed_methods='GET, HEAD, PUT, DELETE')
74
75 members_resource = members.create_resource()
76
77 mapper.connect("/images/{image_id}/members",
78 controller=members_resource,
79 action="index",
80 conditions={'method': ['GET']})
81 mapper.connect("/images/{image_id}/members",
82 controller=members_resource,
83 action="update_all",
84 conditions=dict(method=["PUT"]))
85 mapper.connect("/images/{image_id}/members",
86 controller=reject_method_resource,
87 action='reject',
88 allowed_methods='GET, PUT')
89 mapper.connect("/images/{image_id}/members/{id}",
90 controller=members_resource,
91 action="show",
92 conditions={'method': ['GET']})
93 mapper.connect("/images/{image_id}/members/{id}",
94 controller=members_resource,
95 action="update",
96 conditions={'method': ['PUT']})
97 mapper.connect("/images/{image_id}/members/{id}",
98 controller=members_resource,
99 action="delete",
100 conditions={'method': ['DELETE']})
101 mapper.connect("/images/{image_id}/members/{id}",
102 controller=reject_method_resource, 28 controller=reject_method_resource,
103 action='reject', 29 action="reject")
104 allowed_methods='GET, PUT, DELETE')
105 mapper.connect("/shared-images/{id}",
106 controller=members_resource,
107 action="index_shared_images")
108 30
109 super(API, self).__init__(mapper) 31 super(API, self).__init__(mapper)
diff --git a/glance/api/versions.py b/glance/api/versions.py
index 3d1dc16..28dc94b 100644
--- a/glance/api/versions.py
+++ b/glance/api/versions.py
@@ -20,7 +20,7 @@ from six.moves import http_client
20import webob.dec 20import webob.dec
21 21
22from glance.common import wsgi 22from glance.common import wsgi
23from glance.i18n import _, _LW 23from glance.i18n import _
24 24
25 25
26versions_opts = [ 26versions_opts = [
@@ -82,20 +82,6 @@ class Controller(object):
82 build_version_object(2.1, 'v2', 'SUPPORTED'), 82 build_version_object(2.1, 'v2', 'SUPPORTED'),
83 build_version_object(2.0, 'v2', 'SUPPORTED'), 83 build_version_object(2.0, 'v2', 'SUPPORTED'),
84 ]) 84 ])
85 if CONF.enable_v1_api:
86 LOG.warn(_LW('The Images (Glance) v1 API is deprecated and will '
87 'be removed on or after the Pike release, following '
88 'the standard OpenStack deprecation policy. '
89 'Currently, the solution is to set '
90 'enable_v1_api=False and enable_v2_api=True in your '
91 'glance-api.conf file. Once those options are '
92 'removed from the code, Images (Glance) v2 API will '
93 'be switched on by default and will be the only '
94 'option to deploy and use.'))
95 version_objs.extend([
96 build_version_object(1.1, 'v1', 'DEPRECATED'),
97 build_version_object(1.0, 'v1', 'DEPRECATED'),
98 ])
99 85
100 status = explicit and http_client.OK or http_client.MULTIPLE_CHOICES 86 status = explicit and http_client.OK or http_client.MULTIPLE_CHOICES
101 response = webob.Response(request=req, 87 response = webob.Response(request=req,
diff --git a/glance/cmd/cache_manage.py b/glance/cmd/cache_manage.py
deleted file mode 100644
index 3f021b2..0000000
--- a/glance/cmd/cache_manage.py
+++ /dev/null
@@ -1,490 +0,0 @@
1#!/usr/bin/env python
2
3# Copyright 2011 OpenStack Foundation
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18"""
19A simple cache management utility for Glance.
20"""
21from __future__ import print_function
22
23import argparse
24import collections
25import datetime
26import functools
27import os
28import sys
29import time
30
31from oslo_utils import encodeutils
32import prettytable
33
34from six.moves import input
35
36# If ../glance/__init__.py exists, add ../ to Python search path, so that
37# it will override what happens to be installed in /usr/(local/)lib/python...
38possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
39 os.pardir,
40 os.pardir))
41if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
42 sys.path.insert(0, possible_topdir)
43
44from glance.common import exception
45import glance.image_cache.client
46from glance.version import version_info as version
47
48
49SUCCESS = 0
50FAILURE = 1
51
52
53def catch_error(action):
54 """Decorator to provide sensible default error handling for actions."""
55 def wrap(func):
56 @functools.wraps(func)
57 def wrapper(*args, **kwargs):
58 try:
59 ret = func(*args, **kwargs)
60 return SUCCESS if ret is None else ret
61 except exception.NotFound:
62 options = args[0]
63 print("Cache management middleware not enabled on host %s" %
64 options.host)
65 return FAILURE
66 except exception.Forbidden:
67 print("Not authorized to make this request.")
68 return FAILURE
69 except Exception as e:
70 options = args[0]
71 if options.debug:
72 raise
73 print("Failed to %s. Got error:" % action)
74 pieces = encodeutils.exception_to_unicode(e).split('\n')
75 for piece in pieces:
76 print(piece)
77 return FAILURE
78
79 return wrapper
80 return wrap
81
82
83@catch_error('show cached images')
84def list_cached(args):
85 """%(prog)s list-cached [options]
86
87 List all images currently cached.
88 """
89 client = get_client(args)
90 images = client.get_cached_images()
91 if not images:
92 print("No cached images.")
93 return SUCCESS
94
95 print("Found %d cached images..." % len(images))
96
97 pretty_table = prettytable.PrettyTable(("ID",
98 "Last Accessed (UTC)",
99 "Last Modified (UTC)",
100 "Size",
101 "Hits"))
102 pretty_table.align['Size'] = "r"
103 pretty_table.align['Hits'] = "r"
104
105 for image in images:
106 last_accessed = image['last_accessed']
107 if last_accessed == 0:
108 last_accessed = "N/A"
109 else:
110 last_accessed = datetime.datetime.utcfromtimestamp(
111 last_accessed).isoformat()
112
113 pretty_table.add_row((
114 image['image_id'],
115 last_accessed,
116 datetime.datetime.utcfromtimestamp(
117 image['last_modified']).isoformat(),
118 image['size'],
119 image['hits']))
120
121 print(pretty_table.get_string())
122 return SUCCESS
123
124
125@catch_error('show queued images')
126def list_queued(args):
127 """%(prog)s list-queued [options]
128
129 List all images currently queued for caching.
130 """
131 client = get_client(args)
132 images = client.get_queued_images()
133 if not images:
134 print("No queued images.")
135 return SUCCESS
136
137 print("Found %d queued images..." % len(images))
138
139 pretty_table = prettytable.PrettyTable(("ID",))
140
141 for image in images:
142 pretty_table.add_row((image,))
143
144 print(pretty_table.get_string())
145
146
147@catch_error('queue the specified image for caching')
148def queue_image(args):
149 """%(prog)s queue-image <IMAGE_ID> [options]
150
151 Queues an image for caching.
152 """
153 if len(args.command) == 2:
154 image_id = args.command[1]
155 else:
156 print("Please specify one and only ID of the image you wish to ")
157 print("queue from the cache as the first argument")
158 return FAILURE
159
160 if (not args.force and
161 not user_confirm("Queue image %(image_id)s for caching?" %
162 {'image_id': image_id}, default=False)):
163 return SUCCESS
164
165 client = get_client(args)
166 client.queue_image_for_caching(image_id)
167
168 if args.verbose:
169 print("Queued image %(image_id)s for caching" %
170 {'image_id': image_id})
171
172 return SUCCESS
173
174
175@catch_error('delete the specified cached image')
176def delete_cached_image(args):
177 """%(prog)s delete-cached-image <IMAGE_ID> [options]
178
179 Deletes an image from the cache.
180 """
181 if len(args.command) == 2:
182 image_id = args.command[1]
183 else:
184 print("Please specify one and only ID of the image you wish to ")
185 print("delete from the cache as the first argument")
186 return FAILURE
187
188 if (not args.force and
189 not user_confirm("Delete cached image %(image_id)s?" %
190 {'image_id': image_id}, default=False)):
191 return SUCCESS
192
193 client = get_client(args)
194 client.delete_cached_image(image_id)
195
196 if args.verbose:
197 print("Deleted cached image %(image_id)s" % {'image_id': image_id})
198
199 return SUCCESS
200
201
202@catch_error('Delete all cached images')
203def delete_all_cached_images(args):
204 """%(prog)s delete-all-cached-images [options]
205
206 Remove all images from the cache.
207 """
208 if (not args.force and
209 not user_confirm("Delete all cached images?", default=False)):
210 return SUCCESS
211
212 client = get_client(args)
213 num_deleted = client.delete_all_cached_images()
214
215 if args.verbose:
216 print("Deleted %(num_deleted)s cached images" %
217 {'num_deleted': num_deleted})
218
219 return SUCCESS
220
221
222@catch_error('delete the specified queued image')
223def delete_queued_image(args):
224 """%(prog)s delete-queued-image <IMAGE_ID> [options]
225
226 Deletes an image from the cache.
227 """
228 if len(args.command) == 2:
229 image_id = args.command[1]
230 else:
231 print("Please specify one and only ID of the image you wish to ")
232 print("delete from the cache as the first argument")
233 return FAILURE
234
235 if (not args.force and
236 not user_confirm("Delete queued image %(image_id)s?" %
237 {'image_id': image_id}, default=False)):
238 return SUCCESS
239
240 client = get_client(args)
241 client.delete_queued_image(image_id)
242
243 if args.verbose:
244 print("Deleted queued image %(image_id)s" % {'image_id': image_id})
245
246 return SUCCESS
247
248
249@catch_error('Delete all queued images')
250def delete_all_queued_images(args):
251 """%(prog)s delete-all-queued-images [options]
252
253 Remove all images from the cache queue.
254 """
255 if (not args.force and
256 not user_confirm("Delete all queued images?", default=False)):
257 return SUCCESS
258
259 client = get_client(args)
260 num_deleted = client.delete_all_queued_images()
261
262 if args.verbose:
263 print("Deleted %(num_deleted)s queued images" %
264 {'num_deleted': num_deleted})
265
266 return SUCCESS
267
268
269def get_client(options):
270 """Return a new client object to a Glance server.
271
272 specified by the --host and --port options
273 supplied to the CLI
274 """
275 return glance.image_cache.client.get_client(
276 host=options.host,
277 port=options.port,
278 username=options.os_username,
279 password=options.os_password,
280 tenant=options.os_tenant_name,
281 auth_url=options.os_auth_url,
282 auth_strategy=options.os_auth_strategy,
283 auth_token=options.os_auth_token,
284 region=options.os_region_name,
285 insecure=options.insecure)
286
287
288def env(*vars, **kwargs):
289 """Search for the first defined of possibly many env vars.
290
291 Returns the first environment variable defined in vars, or
292 returns the default defined in kwargs.
293 """
294 for v in vars:
295 value = os.environ.get(v)
296 if value:
297 return value
298 return kwargs.get('default', '')
299
300
301def print_help(args):
302 """
303 Print help specific to a command
304 """
305 command = lookup_command(args.command[1])
306 print(command.__doc__ % {'prog': os.path.basename(sys.argv[0])})
307
308
309def parse_args(parser):
310 """Set up the CLI and config-file options that may be
311 parsed and program commands.
312
313 :param parser: The option parser
314 """
315 parser.add_argument('command', default='help', nargs='*',
316 help='The command to execute')
317 parser.add_argument('-v', '--verbose', default=False, action="store_true",
318 help="Print more verbose output.")
319 parser.add_argument('-d', '--debug', default=False, action="store_true",
320 help="Print debugging output.")
321 parser.add_argument('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
322 help="Address of Glance API host.")
323 parser.add_argument('-p', '--port', dest="port", metavar="PORT",
324 type=int, default=9292,
325 help="Port the Glance API host listens on.")
326 parser.add_argument('-k', '--insecure', dest="insecure",
327 default=False, action="store_true",
328 help='Explicitly allow glance to perform "insecure" '
329 "SSL (https) requests. The server's certificate "
330 "will not be verified against any certificate "
331 "authorities. This option should be used with "
332 "caution.")
333 parser.add_argument('-f', '--force', dest="force",
334 default=False, action="store_true",
335 help="Prevent select actions from requesting "
336 "user confirmation.")
337
338 parser.add_argument('--os-auth-token',
339 dest='os_auth_token',
340 default=env('OS_AUTH_TOKEN'),
341 help='Defaults to env[OS_AUTH_TOKEN].')
342 parser.add_argument('-A', '--os_auth_token', '--auth_token',
343 dest='os_auth_token',
344 help=argparse.SUPPRESS)
345
346 parser.add_argument('--os-username',
347 dest='os_username',
348 default=env('OS_USERNAME'),
349 help='Defaults to env[OS_USERNAME].')
350 parser.add_argument('-I', '--os_username',
351 dest='os_username',
352 help=argparse.SUPPRESS)
353
354 parser.add_argument('--os-password',
355 dest='os_password',
356 default=env('OS_PASSWORD'),
357 help='Defaults to env[OS_PASSWORD].')
358 parser.add_argument('-K', '--os_password',
359 dest='os_password',
360 help=argparse.SUPPRESS)
361
362 parser.add_argument('--os-region-name',
363 dest='os_region_name',
364 default=env('OS_REGION_NAME'),
365 help='Defaults to env[OS_REGION_NAME].')
366 parser.add_argument('-R', '--os_region_name',
367 dest='os_region_name',
368 help=argparse.SUPPRESS)
369
370 parser.add_argument('--os-tenant-id',
371 dest='os_tenant_id',
372 default=env('OS_TENANT_ID'),
373 help='Defaults to env[OS_TENANT_ID].')
374 parser.add_argument('--os_tenant_id',
375 dest='os_tenant_id',
376 help=argparse.SUPPRESS)
377
378 parser.add_argument('--os-tenant-name',
379 dest='os_tenant_name',
380 default=env('OS_TENANT_NAME'),
381 help='Defaults to env[OS_TENANT_NAME].')
382 parser.add_argument('-T', '--os_tenant_name',
383 dest='os_tenant_name',
384 help=argparse.SUPPRESS)
385
386 parser.add_argument('--os-auth-url',
387 default=env('OS_AUTH_URL'),
388 help='Defaults to env[OS_AUTH_URL].')
389 parser.add_argument('-N', '--os_auth_url',
390 dest='os_auth_url',
391 help=argparse.SUPPRESS)
392
393 parser.add_argument('-S', '--os_auth_strategy', dest="os_auth_strategy",
394 metavar="STRATEGY",
395 help="Authentication strategy (keystone or noauth).")
396
397 version_string = version.cached_version_string()
398 parser.add_argument('--version', action='version',
399 version=version_string)
400
401 return parser.parse_args()
402
403
404CACHE_COMMANDS = collections.OrderedDict()
405CACHE_COMMANDS['help'] = (
406 print_help, 'Output help for one of the commands below')
407CACHE_COMMANDS['list-cached'] = (
408 list_cached, 'List all images currently cached')
409CACHE_COMMANDS['list-queued'] = (
410 list_queued, 'List all images currently queued for caching')
411CACHE_COMMANDS['queue-image'] = (
412 queue_image, 'Queue an image for caching')
413CACHE_COMMANDS['delete-cached-image'] = (
414 delete_cached_image, 'Purges an image from the cache')
415CACHE_COMMANDS['delete-all-cached-images'] = (
416 delete_all_cached_images, 'Removes all images from the cache')
417CACHE_COMMANDS['delete-queued-image'] = (
418 delete_queued_image, 'Deletes an image from the cache queue')
419CACHE_COMMANDS['delete-all-queued-images'] = (
420 delete_all_queued_images, 'Deletes all images from the cache queue')
421
422
423def _format_command_help():
424 """Formats the help string for subcommands."""
425 help_msg = "Commands:\n\n"
426
427 for command, info in CACHE_COMMANDS.items():
428 if command == 'help':
429 command = 'help <command>'
430 help_msg += " %-28s%s\n\n" % (command, info[1])
431
432 return help_msg
433
434
435def lookup_command(command_name):
436 try:
437 command = CACHE_COMMANDS[command_name]
438 return command[0]
439 except KeyError:
440 print('\nError: "%s" is not a valid command.\n' % command_name)
441 print(_format_command_help())
442 sys.exit("Unknown command: %(cmd_name)s" % {'cmd_name': command_name})
443
444
445def user_confirm(prompt, default=False):
446 """Yes/No question dialog with user.
447
448 :param prompt: question/statement to present to user (string)
449 :param default: boolean value to return if empty string
450 is received as response to prompt
451
452 """
453 if default:
454 prompt_default = "[Y/n]"
455 else:
456 prompt_default = "[y/N]"
457
458 answer = input("%s %s " % (prompt, prompt_default))
459
460 if answer == "":
461 return default
462 else:
463 return answer.lower() in ("yes", "y")
464
465
466def main():
467 parser = argparse.ArgumentParser(
468 description=_format_command_help(),
469 formatter_class=argparse.RawDescriptionHelpFormatter)
470 args = parse_args(parser)
471
472 if args.command[0] == 'help' and len(args.command) == 1:
473 parser.print_help()
474 return
475
476 # Look up the command to run
477 command = lookup_command(args.command[0])
478
479 try:
480 start_time = time.time()
481 result = command(args)
482 end_time = time.time()
483 if args.verbose:
484 print("Completed in %-0.4f sec." % (end_time - start_time))
485 sys.exit(result)
486 except (RuntimeError, NotImplementedError) as e:
487 sys.exit("ERROR: %s" % e)
488
489if __name__ == '__main__':
490 main()
diff --git a/glance/common/config.py b/glance/common/config.py
index d7a981a..4ab65be 100644
--- a/glance/common/config.py
+++ b/glance/common/config.py
@@ -455,50 +455,6 @@ Related options:
455 * None 455 * None
456 456
457""")), 457""")),
458 # NOTE(nikhil): Even though deprecated, the configuration option
459 # ``enable_v1_api`` is set to True by default on purpose. Having it enabled
460 # helps the projects that haven't been able to fully move to v2 yet by
461 # keeping the devstack setup to use glance v1 as well. We need to switch it
462 # to False by default soon after Newton is cut so that we can identify the
463 # projects that haven't moved to v2 yet and start having some interesting
464 # conversations with them. Switching to False in Newton may result into
465 # destabilizing the gate and affect the release.
466 cfg.BoolOpt('enable_v1_api',
467 default=True,
468 deprecated_reason=_DEPRECATE_GLANCE_V1_MSG,
469 deprecated_since='Newton',
470 help=_("""
471Deploy the v1 OpenStack Images API.
472
473When this option is set to ``True``, Glance service will respond to
474requests on registered endpoints conforming to the v1 OpenStack
475Images API.
476
477NOTES:
478 * If this option is enabled, then ``enable_v1_registry`` must
479 also be set to ``True`` to enable mandatory usage of Registry
480 service with v1 API.
481
482 * If this option is disabled, then the ``enable_v1_registry``
483 option, which is enabled by default, is also recommended
484 to be disabled.
485
486 * This option is separate from ``enable_v2_api``, both v1 and v2
487 OpenStack Images API can be deployed independent of each
488 other.
489
490 * If deploying only the v2 Images API, this option, which is
491 enabled by default, should be disabled.
492
493Possible values:
494 * True
495 * False
496
497Related options:
498 * enable_v1_registry
499 * enable_v2_api
500
501""")),
502 cfg.BoolOpt('enable_v2_api', 458 cfg.BoolOpt('enable_v2_api',
503 default=True, 459 default=True,
504 deprecated_reason=_('The Images (Glance) version 1 API has ' 460 deprecated_reason=_('The Images (Glance) version 1 API has '
@@ -523,20 +479,12 @@ NOTES:
523 option, which is enabled by default, is also recommended 479 option, which is enabled by default, is also recommended
524 to be disabled. 480 to be disabled.
525 481
526 * This option is separate from ``enable_v1_api``, both v1 and v2
527 OpenStack Images API can be deployed independent of each
528 other.
529
530 * If deploying only the v1 Images API, this option, which is
531 enabled by default, should be disabled.
532
533Possible values: 482Possible values:
534 * True 483 * True
535 * False 484 * False
536 485
537Related options: 486Related options:
538 * enable_v2_registry 487 * enable_v2_registry
539 * enable_v1_api
540 488
541""")), 489""")),
542 cfg.BoolOpt('enable_v1_registry', 490 cfg.BoolOpt('enable_v1_registry',
@@ -544,25 +492,7 @@ Related options:
544 deprecated_reason=_DEPRECATE_GLANCE_V1_MSG, 492 deprecated_reason=_DEPRECATE_GLANCE_V1_MSG,
545 deprecated_since='Newton', 493 deprecated_since='Newton',
546 help=_(""" 494 help=_("""
547Deploy the v1 API Registry service. 495 DEPRECATED FOR REMOVAL
548
549When this option is set to ``True``, the Registry service
550will be enabled in Glance for v1 API requests.
551
552NOTES:
553 * Use of Registry is mandatory in v1 API, so this option must
554 be set to ``True`` if the ``enable_v1_api`` option is enabled.
555
556 * If deploying only the v2 OpenStack Images API, this option,
557 which is enabled by default, should be disabled.
558
559Possible values:
560 * True
561 * False
562
563Related options:
564 * enable_v1_api
565
566""")), 496""")),
567 cfg.BoolOpt('enable_v2_registry', 497 cfg.BoolOpt('enable_v2_registry',
568 default=True, 498 default=True,
diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py
index 45509d3..0593f11 100644
--- a/glance/common/store_utils.py
+++ b/glance/common/store_utils.py
@@ -27,6 +27,7 @@ from glance import scrubber
27LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
28 28
29CONF = cfg.CONF 29CONF = cfg.CONF
30CONF.import_opt('use_user_token', 'glance.registry.client')
30 31
31RESTRICTED_URI_SCHEMAS = frozenset(['file', 'filesystem', 'swift+config']) 32RESTRICTED_URI_SCHEMAS = frozenset(['file', 'filesystem', 'swift+config'])
32 33
diff --git a/glance/image_cache/client.py b/glance/image_cache/client.py
deleted file mode 100644
index 2216015..0000000
--- a/glance/image_cache/client.py
+++ /dev/null
@@ -1,132 +0,0 @@
1# Copyright 2012 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import os
17
18from oslo_serialization import jsonutils as json
19
20from glance.common import client as base_client
21from glance.common import exception
22from glance.i18n import _
23
24
25class CacheClient(base_client.BaseClient):
26
27 DEFAULT_PORT = 9292
28 DEFAULT_DOC_ROOT = '/v1'
29
30 def delete_cached_image(self, image_id):
31 """
32 Delete a specified image from the cache
33 """
34 self.do_request("DELETE", "/cached_images/%s" % image_id)
35 return True
36
37 def get_cached_images(self, **kwargs):
38 """
39 Returns a list of images stored in the image cache.
40 """
41 res = self.do_request("GET", "/cached_images")
42 data = json.loads(res.read())['cached_images']
43 return data
44
45 def get_queued_images(self, **kwargs):
46 """
47 Returns a list of images queued for caching
48 """
49 res = self.do_request("GET", "/queued_images")
50 data = json.loads(res.read())['queued_images']
51 return data
52
53 def delete_all_cached_images(self):
54 """
55 Delete all cached images
56 """
57 res = self.do_request("DELETE", "/cached_images")
58 data = json.loads(res.read())
59 num_deleted = data['num_deleted']
60 return num_deleted
61
62 def queue_image_for_caching(self, image_id):
63 """
64 Queue an image for prefetching into cache
65 """
66 self.do_request("PUT", "/queued_images/%s" % image_id)
67 return True
68
69 def delete_queued_image(self, image_id):
70 """
71 Delete a specified image from the cache queue
72 """
73 self.do_request("DELETE", "/queued_images/%s" % image_id)
74 return True
75
76 def delete_all_queued_images(self):
77 """
78 Delete all queued images
79 """
80 res = self.do_request("DELETE", "/queued_images")
81 data = json.loads(res.read())
82 num_deleted = data['num_deleted']
83 return num_deleted
84
85
86def get_client(host, port=None, timeout=None, use_ssl=False, username=None,
87 password=None, tenant=None,
88 auth_url=None, auth_strategy=None,
89 auth_token=None, region=None,
90 is_silent_upload=False, insecure=False):
91 """
92 Returns a new client Glance client object based on common kwargs.
93 If an option isn't specified falls back to common environment variable
94 defaults.
95 """
96
97 if auth_url or os.getenv('OS_AUTH_URL'):
98 force_strategy = 'keystone'
99 else:
100 force_strategy = None
101
102 creds = {
103 'username': username or
104 os.getenv('OS_AUTH_USER', os.getenv('OS_USERNAME')),
105 'password': password or
106 os.getenv('OS_AUTH_KEY', os.getenv('OS_PASSWORD')),
107 'tenant': tenant or
108 os.getenv('OS_AUTH_TENANT', os.getenv('OS_TENANT_NAME')),
109 'auth_url': auth_url or
110 os.getenv('OS_AUTH_URL'),
111 'strategy': force_strategy or
112 auth_strategy or
113 os.getenv('OS_AUTH_STRATEGY', 'noauth'),
114 'region': region or
115 os.getenv('OS_REGION_NAME'),
116 }
117
118 if creds['strategy'] == 'keystone' and not creds['auth_url']:
119 msg = _("--os_auth_url option or OS_AUTH_URL environment variable "
120 "required when keystone authentication strategy is enabled\n")
121 raise exception.ClientConfigurationError(msg)
122
123 return CacheClient(
124 host=host,
125 port=port,
126 timeout=timeout,
127 use_ssl=use_ssl,
128 auth_token=auth_token or
129 os.getenv('OS_TOKEN'),
130 creds=creds,
131 insecure=insecure,
132 configure_via_auth=False)
diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py
index 4969720..cbe94ec 100644
--- a/glance/tests/functional/__init__.py
+++ b/glance/tests/functional/__init__.py
@@ -74,9 +74,7 @@ class Server(object):
74 self.show_image_direct_url = False 74 self.show_image_direct_url = False
75 self.show_multiple_locations = False 75 self.show_multiple_locations = False
76 self.property_protection_file = '' 76 self.property_protection_file = ''
77 self.enable_v1_api = True
78 self.enable_v2_api = True 77 self.enable_v2_api = True
79 self.enable_v1_registry = True
80 self.enable_v2_registry = True 78 self.enable_v2_registry = True
81 self.needs_database = False 79 self.needs_database = False
82 self.log_file = None 80 self.log_file = None
@@ -346,7 +344,6 @@ sql_connection = %(sql_connection)s
346show_image_direct_url = %(show_image_direct_url)s 344show_image_direct_url = %(show_image_direct_url)s
347show_multiple_locations = %(show_multiple_locations)s 345show_multiple_locations = %(show_multiple_locations)s
348user_storage_quota = %(user_storage_quota)s 346user_storage_quota = %(user_storage_quota)s
349enable_v1_api = %(enable_v1_api)s
350enable_v2_api = %(enable_v2_api)s 347enable_v2_api = %(enable_v2_api)s
351lock_path = %(lock_path)s 348lock_path = %(lock_path)s
352property_protection_file = %(property_protection_file)s 349property_protection_file = %(property_protection_file)s
diff --git a/glance/tests/functional/serial/test_scrubber.py b/glance/tests/functional/serial/test_scrubber.py
index d8c50c5..2737776 100644
--- a/glance/tests/functional/serial/test_scrubber.py
+++ b/glance/tests/functional/serial/test_scrubber.py
@@ -78,8 +78,10 @@ class TestScrubber(functional.FunctionalTest):
78 scrubs them 78 scrubs them
79 """ 79 """
80 self.cleanup() 80 self.cleanup()
81 kwargs = self.__dict__.copy()
82 kwargs['use_user_token'] = True
81 self.start_servers(delayed_delete=True, daemon=True, 83 self.start_servers(delayed_delete=True, daemon=True,
82 metadata_encryption_key='') 84 metadata_encryption_key='', **kwargs)
83 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port) 85 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port)
84 response, content = self._send_create_image_http_request(path) 86 response, content = self._send_create_image_http_request(path)
85 self.assertEqual(http_client.CREATED, response.status) 87 self.assertEqual(http_client.CREATED, response.status)
@@ -112,8 +114,10 @@ class TestScrubber(functional.FunctionalTest):
112 daemon mode 114 daemon mode
113 """ 115 """
114 self.cleanup() 116 self.cleanup()
117 kwargs = self.__dict__.copy()
118 kwargs['use_user_token'] = True
115 self.start_servers(delayed_delete=True, daemon=False, 119 self.start_servers(delayed_delete=True, daemon=False,
116 metadata_encryption_key='') 120 metadata_encryption_key='', **kwargs)
117 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port) 121 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port)
118 response, content = self._send_create_image_http_request(path) 122 response, content = self._send_create_image_http_request(path)
119 self.assertEqual(http_client.CREATED, response.status) 123 self.assertEqual(http_client.CREATED, response.status)
@@ -159,8 +163,10 @@ class TestScrubber(functional.FunctionalTest):
159 163
160 # Start servers. 164 # Start servers.
161 self.cleanup() 165 self.cleanup()
166 kwargs = self.__dict__.copy()
167 kwargs['use_user_token'] = True
162 self.start_servers(delayed_delete=True, daemon=False, 168 self.start_servers(delayed_delete=True, daemon=False,
163 default_store='file') 169 default_store='file', **kwargs)
164 170
165 # Check that we are using a file backend. 171 # Check that we are using a file backend.
166 self.assertEqual(self.api_server.default_store, 'file') 172 self.assertEqual(self.api_server.default_store, 'file')
@@ -235,8 +241,10 @@ class TestScrubber(functional.FunctionalTest):
235 241
236 def test_scrubber_restore_image(self): 242 def test_scrubber_restore_image(self):
237 self.cleanup() 243 self.cleanup()
244 kwargs = self.__dict__.copy()
245 kwargs['use_user_token'] = True
238 self.start_servers(delayed_delete=True, daemon=False, 246 self.start_servers(delayed_delete=True, daemon=False,
239 metadata_encryption_key='') 247 metadata_encryption_key='', **kwargs)
240 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port) 248 path = "http://%s:%d/v2/images" % ("127.0.0.1", self.api_port)
241 response, content = self._send_create_image_http_request(path) 249 response, content = self._send_create_image_http_request(path)
242 self.assertEqual(http_client.CREATED, response.status) 250 self.assertEqual(http_client.CREATED, response.status)
diff --git a/glance/tests/functional/test_api.py b/glance/tests/functional/test_api.py
index 6aae275..19afcf5 100644
--- a/glance/tests/functional/test_api.py
+++ b/glance/tests/functional/test_api.py
@@ -22,25 +22,6 @@ from six.moves import http_client
22 22
23from glance.tests import functional 23from glance.tests import functional
24 24
25# TODO(rosmaita): all the EXPERIMENTAL stuff in this file can be ripped out
26# when v2.6 becomes CURRENT in Queens
27
28
29def _generate_v1_versions(url):
30 v1_versions = {'versions': [
31 {
32 'id': 'v1.1',
33 'status': 'DEPRECATED',
34 'links': [{'rel': 'self', 'href': url % '1'}],
35 },
36 {
37 'id': 'v1.0',
38 'status': 'DEPRECATED',
39 'links': [{'rel': 'self', 'href': url % '1'}],
40 },
41 ]}
42 return v1_versions
43
44 25
45def _generate_v2_versions(url): 26def _generate_v2_versions(url):
46 version_list = [] 27 version_list = []
@@ -86,9 +67,8 @@ def _generate_v2_versions(url):
86 67
87 68
88def _generate_all_versions(url): 69def _generate_all_versions(url):
89 v1 = _generate_v1_versions(url)
90 v2 = _generate_v2_versions(url) 70 v2 = _generate_v2_versions(url)
91 all_versions = {'versions': v2['versions'] + v1['versions']} 71 all_versions = {'versions': v2['versions']}
92 return all_versions 72 return all_versions
93 73
94 74
@@ -96,7 +76,6 @@ class TestApiVersions(functional.FunctionalTest):
96 76
97 def test_version_configurations(self): 77 def test_version_configurations(self):
98 """Test that versioning is handled properly through all channels""" 78 """Test that versioning is handled properly through all channels"""
99 # v1 and v2 api enabled
100 self.start_servers(**self.__dict__.copy()) 79 self.start_servers(**self.__dict__.copy())
101 80
102 url = 'http://127.0.0.1:%d/v%%s/' % self.api_port 81 url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
@@ -111,7 +90,6 @@ class TestApiVersions(functional.FunctionalTest):
111 self.assertEqual(versions, content) 90 self.assertEqual(versions, content)
112 91
113 def test_v2_api_configuration(self): 92 def test_v2_api_configuration(self):
114 self.api_server.enable_v1_api = False
115 self.api_server.enable_v2_api = True 93 self.api_server.enable_v2_api = True
116 self.start_servers(**self.__dict__.copy()) 94 self.start_servers(**self.__dict__.copy())
117 95
@@ -126,22 +104,6 @@ class TestApiVersions(functional.FunctionalTest):
126 content = jsonutils.loads(content_json.decode()) 104 content = jsonutils.loads(content_json.decode())
127 self.assertEqual(versions, content) 105 self.assertEqual(versions, content)
128 106
129 def test_v1_api_configuration(self):
130 self.api_server.enable_v1_api = True
131 self.api_server.enable_v2_api = False
132 self.start_servers(**self.__dict__.copy())
133
134 url = 'http://127.0.0.1:%d/v%%s/' % self.api_port
135 versions = _generate_v1_versions(url)
136
137 # Verify version choices returned.
138 path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
139 http = httplib2.Http()
140 response, content_json = http.request(path, 'GET')
141 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
142 content = jsonutils.loads(content_json.decode())
143 self.assertEqual(versions, content)
144
145 107
146class TestApiPaths(functional.FunctionalTest): 108class TestApiPaths(functional.FunctionalTest):
147 def setUp(self): 109 def setUp(self):
@@ -165,26 +127,6 @@ class TestApiPaths(functional.FunctionalTest):
165 content = jsonutils.loads(content_json.decode()) 127 content = jsonutils.loads(content_json.decode())
166 self.assertEqual(self.versions, content) 128 self.assertEqual(self.versions, content)
167 129
168 def test_get_images_path(self):
169 """Assert GET /images with `no Accept:` header.
170 Verify version choices returned.
171 """
172 path = 'http://%s:%d/images' % ('127.0.0.1', self.api_port)
173 http = httplib2.Http()
174 response, content_json = http.request(path, 'GET')
175 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
176 content = jsonutils.loads(content_json.decode())
177 self.assertEqual(self.versions, content)
178
179 def test_get_v1_images_path(self):
180 """GET /v1/images with `no Accept:` header.
181 Verify empty images list returned.
182 """
183 path = 'http://%s:%d/v1/images' % ('127.0.0.1', self.api_port)
184 http = httplib2.Http()
185 response, content = http.request(path, 'GET')
186 self.assertEqual(http_client.OK, response.status)
187
188 def test_get_root_path_with_unknown_header(self): 130 def test_get_root_path_with_unknown_header(self):
189 """Assert GET / with Accept: unknown header 131 """Assert GET / with Accept: unknown header
190 Verify version choices returned. Verify message in API log about 132 Verify version choices returned. Verify message in API log about
@@ -198,49 +140,6 @@ class TestApiPaths(functional.FunctionalTest):
198 content = jsonutils.loads(content_json.decode()) 140 content = jsonutils.loads(content_json.decode())
199 self.assertEqual(self.versions, content) 141 self.assertEqual(self.versions, content)
200 142
201 def test_get_root_path_with_openstack_header(self):
202 """Assert GET / with an Accept: application/vnd.openstack.images-v1
203 Verify empty image list returned
204 """
205 path = 'http://%s:%d/images' % ('127.0.0.1', self.api_port)
206 http = httplib2.Http()
207 headers = {'Accept': 'application/vnd.openstack.images-v1'}
208 response, content = http.request(path, 'GET', headers=headers)
209 self.assertEqual(http_client.OK, response.status)
210 self.assertEqual(self.images_json, content.decode())
211
212 def test_get_images_path_with_openstack_header(self):
213 """Assert GET /images with a
214 `Accept: application/vnd.openstack.compute-v1` header.
215 Verify version choices returned. Verify message in API log
216 about unknown accept header.
217 """
218 path = 'http://%s:%d/images' % ('127.0.0.1', self.api_port)
219 http = httplib2.Http()
220 headers = {'Accept': 'application/vnd.openstack.compute-v1'}
221 response, content_json = http.request(path, 'GET', headers=headers)
222 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
223 content = jsonutils.loads(content_json.decode())
224 self.assertEqual(self.versions, content)
225
226 def test_get_v10_images_path(self):
227 """Assert GET /v1.0/images with no Accept: header
228 Verify version choices returned
229 """
230 path = 'http://%s:%d/v1.a/images' % ('127.0.0.1', self.api_port)
231 http = httplib2.Http()
232 response, content = http.request(path, 'GET')
233 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
234
235 def test_get_v1a_images_path(self):
236 """Assert GET /v1.a/images with no Accept: header
237 Verify version choices returned
238 """
239 path = 'http://%s:%d/v1.a/images' % ('127.0.0.1', self.api_port)
240 http = httplib2.Http()
241 response, content = http.request(path, 'GET')
242 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
243
244 def test_get_va1_images_path(self): 143 def test_get_va1_images_path(self):
245 """Assert GET /va.1/images with no Accept: header 144 """Assert GET /va.1/images with no Accept: header
246 Verify version choices returned 145 Verify version choices returned
@@ -263,28 +162,6 @@ class TestApiPaths(functional.FunctionalTest):
263 content = jsonutils.loads(content_json.decode()) 162 content = jsonutils.loads(content_json.decode())
264 self.assertEqual(self.versions, content) 163 self.assertEqual(self.versions, content)
265 164
266 def test_get_versions_path_with_openstack_header(self):
267 """Assert GET /versions with the
268 `Accept: application/vnd.openstack.images-v1` header.
269 Verify version choices returned.
270 """
271 path = 'http://%s:%d/versions' % ('127.0.0.1', self.api_port)
272 http = httplib2.Http()
273 headers = {'Accept': 'application/vnd.openstack.images-v1'}
274 response, content_json = http.request(path, 'GET', headers=headers)
275 self.assertEqual(http_client.OK, response.status)
276 content = jsonutils.loads(content_json.decode())
277 self.assertEqual(self.versions, content)
278
279 def test_get_v1_versions_path(self):
280 """Assert GET /v1/versions with `no Accept:` header
281 Verify 404 returned
282 """
283 path = 'http://%s:%d/v1/versions' % ('127.0.0.1', self.api_port)
284 http = httplib2.Http()
285 response, content = http.request(path, 'GET')
286 self.assertEqual(http_client.NOT_FOUND, response.status)
287
288 def test_get_versions_choices(self): 165 def test_get_versions_choices(self):
289 """Verify version choices returned""" 166 """Verify version choices returned"""
290 path = 'http://%s:%d/v10' % ('127.0.0.1', self.api_port) 167 path = 'http://%s:%d/v10' % ('127.0.0.1', self.api_port)
@@ -293,28 +170,3 @@ class TestApiPaths(functional.FunctionalTest):
293 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status) 170 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
294 content = jsonutils.loads(content_json.decode()) 171 content = jsonutils.loads(content_json.decode())
295 self.assertEqual(self.versions, content) 172 self.assertEqual(self.versions, content)
296
297 def test_get_images_path_with_openstack_v2_header(self):
298 """Assert GET /images with a
299 `Accept: application/vnd.openstack.compute-v2` header.
300 Verify version choices returned. Verify message in API log
301 about unknown version in accept header.
302 """
303 path = 'http://%s:%d/images' % ('127.0.0.1', self.api_port)
304 http = httplib2.Http()
305 headers = {'Accept': 'application/vnd.openstack.images-v10'}
306 response, content_json = http.request(path, 'GET', headers=headers)
307 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
308 content = jsonutils.loads(content_json.decode())
309 self.assertEqual(self.versions, content)
310
311 def test_get_v12_images_path(self):
312 """Assert GET /v1.2/images with `no Accept:` header
313 Verify version choices returned
314 """
315 path = 'http://%s:%d/v1.2/images' % ('127.0.0.1', self.api_port)
316 http = httplib2.Http()
317 response, content_json = http.request(path, 'GET')
318 self.assertEqual(http_client.MULTIPLE_CHOICES, response.status)
319 content = jsonutils.loads(content_json.decode())
320 self.assertEqual(self.versions, content)
diff --git a/glance/tests/functional/test_bin_glance_cache_manage.py b/glance/tests/functional/test_bin_glance_cache_manage.py
deleted file mode 100644
index d933f78..0000000
--- a/glance/tests/functional/test_bin_glance_cache_manage.py
+++ /dev/null
@@ -1,358 +0,0 @@
1# Copyright 2011 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""Functional test case that utilizes the bin/glance-cache-manage CLI tool"""
17
18import datetime
19import hashlib
20import os
21import sys
22
23import httplib2
24from oslo_serialization import jsonutils
25from oslo_utils import units
26from six.moves import http_client
27# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
28from six.moves import range
29
30from glance.tests import functional
31from glance.tests.utils import execute
32from glance.tests.utils import minimal_headers
33
34FIVE_KB = 5 * units.Ki
35
36
37class TestBinGlanceCacheManage(functional.FunctionalTest):
38 """Functional tests for the bin/glance CLI tool"""
39
40 def setUp(self):
41 self.image_cache_driver = "sqlite"
42
43 super(TestBinGlanceCacheManage, self).setUp()
44
45 self.api_server.deployment_flavor = "cachemanagement"
46
47 # NOTE(sirp): This is needed in case we are running the tests under an
48 # environment in which OS_AUTH_STRATEGY=keystone. The test server we
49 # spin up won't have keystone support, so we need to switch to the
50 # NoAuth strategy.
51 os.environ['OS_AUTH_STRATEGY'] = 'noauth'
52 os.environ['OS_AUTH_URL'] = ''
53
54 def add_image(self, name):
55 """
56 Adds an image with supplied name and returns the newly-created
57 image identifier.
58 """
59 image_data = b"*" * FIVE_KB
60 headers = minimal_headers(name)
61 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
62 http = httplib2.Http()
63 response, content = http.request(path, 'POST', headers=headers,
64 body=image_data)
65 self.assertEqual(http_client.CREATED, response.status)
66 data = jsonutils.loads(content)
67 self.assertEqual(hashlib.md5(image_data).hexdigest(),
68 data['image']['checksum'])
69 self.assertEqual(FIVE_KB, data['image']['size'])
70 self.assertEqual(name, data['image']['name'])
71 self.assertTrue(data['image']['is_public'])
72 return data['image']['id']
73
74 def is_image_cached(self, image_id):
75 """
76 Return True if supplied image ID is cached, False otherwise
77 """
78 exe_cmd = '%s -m glance.cmd.cache_manage' % sys.executable
79 cmd = "%s --port=%d list-cached" % (exe_cmd, self.api_port)
80
81 exitcode, out, err = execute(cmd)
82
83 self.assertEqual(0, exitcode)
84 out = out.decode('utf-8')
85 return image_id in out
86
87 def iso_date(self, image_id):
88 """
89 Return True if supplied image ID is cached, False otherwise
90 """
91 exe_cmd = '%s -m glance.cmd.cache_manage' % sys.executable
92 cmd = "%s --port=%d list-cached" % (exe_cmd, self.api_port)
93
94 exitcode, out, err = execute(cmd)
95 out = out.decode('utf-8')
96
97 return datetime.datetime.utcnow().strftime("%Y-%m-%d") in out
98
99 def test_no_cache_enabled(self):
100 """
101 Test that cache index command works
102 """
103 self.cleanup()
104 self.api_server.deployment_flavor = ''
105 self.start_servers() # Not passing in cache_manage in pipeline...
106
107 api_port = self.api_port
108
109 # Verify decent error message returned
110 exe_cmd = '%s -m glance.cmd.cache_manage' % sys.executable
111 cmd = "%s --port=%d list-cached" % (exe_cmd, api_port)
112
113 exitcode, out, err = execute(cmd, raise_error=False)
114
115 self.assertEqual(1, exitcode)
116 self.assertIn(b'Cache management middleware not enabled on host',
117 out.strip())
118
119 self.stop_servers()
120
121 def test_cache_index(self):
122 """
123 Test that cache index command works
124 """
125 self.cleanup()
126 self.start_servers(**self.__dict__.copy())
127
128 api_port = self.api_port
129
130 # Verify no cached images
131 exe_cmd = '%s -m glance.cmd.cache_manage' % sys.executable
132 cmd = "%s --port=%d list-cached" % (exe_cmd, api_port)
133
134 exitcode, out, err = execute(cmd)
135
136 self.assertEqual(0, exitcode)
137 self.assertIn(b'No cached images', out.strip())
138
139 ids = {}
140
141 # Add a few images and cache the second one of them
142 # by GETing the image...
143 for x in range(4):
144 ids[x] = self.add_image("Image%s" % x)
145
146 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", api_port,
147 ids[1])
148 http = httplib2.Http()
149 response, content = http.request(path, 'GET')
150 self.assertEqual(http_client.OK, response.status)
151
152 self.assertTrue(self.is_image_cached(ids[1]),
153 "%s is not cached." % ids[1])
154
155 self.assertTrue(self.iso_date(ids[1]))
156
157 self.stop_servers()
158
159 def test_queue(self):
160 """
161 Test that we can queue and fetch images using the
162 CLI utility
163 """
164 self.cleanup()
165 self.start_servers(**self.__dict__.copy())
166
167 api_port = self.api_port
168
169 # Verify no cached images
170 exe_cmd = '%s -m glance.cmd.cache_manage' % sys.executable
171 cmd = "%s --port=%d list-cached" % (exe_cmd, api_port)
172
173 exitcode, out, err = execute(cmd)
174
175 self.assertEqual(0, exitcode)
176 self.assertIn(b'No cached images', out.strip())
177
178 # Verify no queued images
179 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
180
181 exitcode, out, err = execute(cmd)
182
183 self.assertEqual(0, exitcode)
184 self.assertIn(b'No queued images', out.strip())
185
186 ids = {}
187
188 # Add a few images and cache the second one of them
189 # by GETing the image...
190 for x in range(4):
191 ids[x] = self.add_image("Image%s" % x)
192
193 # Queue second image and then cache it
194 cmd = "%s --port=%d --force queue-image %s" % (
195 exe_cmd, api_port, ids[1])
196
197 exitcode, out, err = execute(cmd)
198
199 self.assertEqual(0, exitcode)
200
201 # Verify queued second image
202 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
203
204 exitcode, out, err = execute(cmd)
205
206 self.assertEqual(0, exitcode)
207 out = out.decode('utf-8')
208 self.assertIn(ids[1], out, 'Image %s was not queued!' % ids[1])
209
210 # Cache images in the queue by running the prefetcher
211 cache_config_filepath = os.path.join(self.test_dir, 'etc',
212 'glance-cache.conf')
213 cache_file_options = {
214 'image_cache_dir': self.api_server.image_cache_dir,
215 'image_cache_driver': self.image_cache_driver,
216 'registry_port': self.registry_server.bind_port,
217 'lock_path': self.test_dir,
218 'log_file': os.path.join(self.test_dir, 'cache.log'),
219 'metadata_encryption_key': "012345678901234567890123456789ab",
220 'filesystem_store_datadir': self.test_dir
221 }
222 with open(cache_config_filepath, 'w') as cache_file:
223 cache_file.write("""[DEFAULT]
224debug = True
225lock_path = %(lock_path)s
226image_cache_dir = %(image_cache_dir)s
227image_cache_driver = %(image_cache_driver)s
228registry_host = 127.0.0.1
229registry_port = %(registry_port)s
230metadata_encryption_key = %(metadata_encryption_key)s
231log_file = %(log_file)s
232
233[glance_store]
234filesystem_store_datadir=%(filesystem_store_datadir)s
235""" % cache_file_options)
236
237 cmd = ("%s -m glance.cmd.cache_prefetcher --config-file %s" %
238 (sys.executable, cache_config_filepath))
239
240 exitcode, out, err = execute(cmd)
241
242 self.assertEqual(0, exitcode)
243 self.assertEqual(b'', out.strip(), out)
244
245 # Verify no queued images
246 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
247
248 exitcode, out, err = execute(cmd)
249
250 self.assertEqual(0, exitcode)
251 self.assertIn(b'No queued images', out.strip())
252
253 # Verify second image now cached
254 cmd = "%s --port=%d list-cached" % (exe_cmd, api_port)
255
256 exitcode, out, err = execute(cmd)
257
258 self.assertEqual(0, exitcode)
259 out = out.decode('utf-8')
260 self.assertIn(ids[1], out, 'Image %s was not cached!' % ids[1])
261
262 # Queue third image and then delete it from queue
263 cmd = "%s --port=%d --force queue-image %s" % (
264 exe_cmd, api_port, ids[2])
265
266 exitcode, out, err = execute(cmd)
267
268 self.assertEqual(0, exitcode)
269
270 # Verify queued third image
271 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
272
273 exitcode, out, err = execute(cmd)
274
275 self.assertEqual(0, exitcode)
276 out = out.decode('utf-8')
277 self.assertIn(ids[2], out, 'Image %s was not queued!' % ids[2])
278
279 # Delete the image from the queue
280 cmd = ("%s --port=%d --force "
281 "delete-queued-image %s") % (exe_cmd, api_port, ids[2])
282
283 exitcode, out, err = execute(cmd)
284
285 self.assertEqual(0, exitcode)
286
287 # Verify no queued images
288 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
289
290 exitcode, out, err = execute(cmd)
291
292 self.assertEqual(0, exitcode)
293 self.assertIn(b'No queued images', out.strip())
294
295 # Queue all images
296 for x in range(4):
297 cmd = ("%s --port=%d --force "
298 "queue-image %s") % (exe_cmd, api_port, ids[x])
299
300 exitcode, out, err = execute(cmd)
301
302 self.assertEqual(0, exitcode)
303
304 # Verify queued third image
305 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
306
307 exitcode, out, err = execute(cmd)
308
309 self.assertEqual(0, exitcode)
310 self.assertIn(b'Found 3 queued images', out)
311
312 # Delete the image from the queue
313 cmd = ("%s --port=%d --force "
314 "delete-all-queued-images") % (exe_cmd, api_port)
315
316 exitcode, out, err = execute(cmd)
317
318 self.assertEqual(0, exitcode)
319
320 # Verify nothing in queue anymore
321 cmd = "%s --port=%d list-queued" % (exe_cmd, api_port)
322
323 exitcode, out, err = execute(cmd)
324
325 self.assertEqual(0, exitcode)
326 self.assertIn(b'No queued images', out.strip())
327
328 # verify two image id when queue-image
329 cmd = ("%s --port=%d --force "
330 "queue-image %s %s") % (exe_cmd, api_port, ids[0], ids[1])
331
332 exitcode, out, err = execute(cmd, raise_error=False)
333
334 self.assertEqual(1, exitcode)
335 self.assertIn(b'Please specify one and only ID of '
336 b'the image you wish to ', out.strip())
337
338 # verify two image id when delete-queued-image
339 cmd = ("%s --port=%d --force delete-queued-image "
340 "%s %s") % (exe_cmd, api_port, ids[0], ids[1])
341
342 exitcode, out, err = execute(cmd, raise_error=False)
343
344 self.assertEqual(1, exitcode)
345 self.assertIn(b'Please specify one and only ID of '
346 b'the image you wish to ', out.strip())
347
348 # verify two image id when delete-cached-image
349 cmd = ("%s --port=%d --force delete-cached-image "
350 "%s %s") % (exe_cmd, api_port, ids[0], ids[1])
351
352 exitcode, out, err = execute(cmd, raise_error=False)
353
354 self.assertEqual(1, exitcode)
355 self.assertIn(b'Please specify one and only ID of '
356 b'the image you wish to ', out.strip())
357
358 self.stop_servers()
diff --git a/glance/tests/functional/test_cache_middleware.py b/glance/tests/functional/test_cache_middleware.py
index 9508d97..178115a 100644
--- a/glance/tests/functional/test_cache_middleware.py
+++ b/glance/tests/functional/test_cache_middleware.py
@@ -20,25 +20,16 @@ but that is really not relevant, as the image cache is transparent
20to the backend store. 20to the backend store.
21""" 21"""
22 22
23import hashlib
24import os 23import os
25import shutil 24import shutil
26import sys
27import time
28import uuid 25import uuid
29 26
30import httplib2 27import httplib2
31from oslo_serialization import jsonutils 28from oslo_serialization import jsonutils
32from oslo_utils import units 29from oslo_utils import units
33from six.moves import http_client 30from six.moves import http_client
34# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
35from six.moves import range
36 31
37from glance.tests import functional 32from glance.tests import functional
38from glance.tests.functional.store_utils import get_http_uri
39from glance.tests.functional.store_utils import setup_http
40from glance.tests.utils import execute
41from glance.tests.utils import minimal_headers
42from glance.tests.utils import skip_if_disabled 33from glance.tests.utils import skip_if_disabled
43from glance.tests.utils import xattr_writes_supported 34from glance.tests.utils import xattr_writes_supported
44 35
@@ -48,78 +39,6 @@ FIVE_KB = 5 * units.Ki
48class BaseCacheMiddlewareTest(object): 39class BaseCacheMiddlewareTest(object):
49 40
50 @skip_if_disabled 41 @skip_if_disabled
51 def test_cache_middleware_transparent_v1(self):
52 """
53 We test that putting the cache middleware into the
54 application pipeline gives us transparent image caching
55 """
56 self.cleanup()
57 self.start_servers(**self.__dict__.copy())
58
59 # Add an image and verify a 200 OK is returned
60 image_data = b"*" * FIVE_KB
61 headers = minimal_headers('Image1')
62 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
63 http = httplib2.Http()
64 response, content = http.request(path, 'POST', headers=headers,
65 body=image_data)
66 self.assertEqual(http_client.CREATED, response.status)
67 data = jsonutils.loads(content)
68 self.assertEqual(hashlib.md5(image_data).hexdigest(),
69 data['image']['checksum'])
70 self.assertEqual(FIVE_KB, data['image']['size'])
71 self.assertEqual("Image1", data['image']['name'])
72 self.assertTrue(data['image']['is_public'])
73
74 image_id = data['image']['id']
75
76 # Verify image not in cache
77 image_cached_path = os.path.join(self.api_server.image_cache_dir,
78 image_id)
79 self.assertFalse(os.path.exists(image_cached_path))
80
81 # Grab the image
82 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
83 image_id)
84 http = httplib2.Http()
85 response, content = http.request(path, 'GET')
86 self.assertEqual(http_client.OK, response.status)
87
88 # Verify image now in cache
89 image_cached_path = os.path.join(self.api_server.image_cache_dir,
90 image_id)
91
92 # You might wonder why the heck this is here... well, it's here
93 # because it took me forever to figure out that the disk write
94 # cache in Linux was causing random failures of the os.path.exists
95 # assert directly below this. Basically, since the cache is writing
96 # the image file to disk in a different process, the write buffers
97 # don't flush the cache file during an os.rename() properly, resulting
98 # in a false negative on the file existence check below. This little
99 # loop pauses the execution of this process for no more than 1.5
100 # seconds. If after that time the cached image file still doesn't
101 # appear on disk, something really is wrong, and the assert should
102 # trigger...
103 i = 0
104 while not os.path.exists(image_cached_path) and i < 30:
105 time.sleep(0.05)
106 i = i + 1
107
108 self.assertTrue(os.path.exists(image_cached_path))
109
110 # Now, we delete the image from the server and verify that
111 # the image cache no longer contains the deleted image
112 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
113 image_id)
114 http = httplib2.Http()
115 response, content = http.request(path, 'DELETE')
116 self.assertEqual(http_client.OK, response.status)
117
118 self.assertFalse(os.path.exists(image_cached_path))
119
120 self.stop_servers()
121
122 @skip_if_disabled
123 def test_cache_middleware_transparent_v2(self): 42 def test_cache_middleware_transparent_v2(self):
124 """Ensure the v2 API image transfer calls trigger caching""" 43 """Ensure the v2 API image transfer calls trigger caching"""
125 self.cleanup() 44 self.cleanup()
@@ -355,102 +274,6 @@ class BaseCacheMiddlewareTest(object):
355 self.stop_servers() 274 self.stop_servers()
356 275
357 @skip_if_disabled 276 @skip_if_disabled
358 def test_cache_remote_image(self):
359 """
360 We test that caching is no longer broken for remote images
361 """
362 self.cleanup()
363 self.start_servers(**self.__dict__.copy())
364
365 setup_http(self)
366
367 # Add a remote image and verify a 201 Created is returned
368 remote_uri = get_http_uri(self, '2')
369 headers = {'X-Image-Meta-Name': 'Image2',
370 'X-Image-Meta-disk_format': 'raw',
371 'X-Image-Meta-container_format': 'ovf',
372 'X-Image-Meta-Is-Public': 'True',
373 'X-Image-Meta-Location': remote_uri}
374 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
375 http = httplib2.Http()
376 response, content = http.request(path, 'POST', headers=headers)
377 self.assertEqual(http_client.CREATED, response.status)
378 data = jsonutils.loads(content)
379 self.assertEqual(FIVE_KB, data['image']['size'])
380
381 image_id = data['image']['id']
382 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
383 image_id)
384
385 # Grab the image
386 http = httplib2.Http()
387 response, content = http.request(path, 'GET')
388 self.assertEqual(http_client.OK, response.status)
389
390 # Grab the image again to ensure it can be served out from
391 # cache with the correct size
392 http = httplib2.Http()
393 response, content = http.request(path, 'GET')
394 self.assertEqual(http_client.OK, response.status)
395 self.assertEqual(FIVE_KB, int(response['content-length']))
396
397 self.stop_servers()
398
399 @skip_if_disabled
400 def test_cache_middleware_trans_v1_without_download_image_policy(self):
401 """
402 Ensure the image v1 API image transfer applied 'download_image'
403 policy enforcement.
404 """
405 self.cleanup()
406 self.start_servers(**self.__dict__.copy())
407
408 # Add an image and verify a 200 OK is returned
409 image_data = b"*" * FIVE_KB
410 headers = minimal_headers('Image1')
411 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
412 http = httplib2.Http()
413 response, content = http.request(path, 'POST', headers=headers,
414 body=image_data)
415 self.assertEqual(http_client.CREATED, response.status)
416 data = jsonutils.loads(content)
417 self.assertEqual(hashlib.md5(image_data).hexdigest(),
418 data['image']['checksum'])
419 self.assertEqual(FIVE_KB, data['image']['size'])
420 self.assertEqual("Image1", data['image']['name'])
421 self.assertTrue(data['image']['is_public'])
422
423 image_id = data['image']['id']
424
425 # Verify image not in cache
426 image_cached_path = os.path.join(self.api_server.image_cache_dir,
427 image_id)
428 self.assertFalse(os.path.exists(image_cached_path))
429
430 rules = {"context_is_admin": "role:admin", "default": "",
431 "download_image": "!"}
432 self.set_policy_rules(rules)
433
434 # Grab the image
435 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
436 image_id)
437 http = httplib2.Http()
438 response, content = http.request(path, 'GET')
439 self.assertEqual(http_client.FORBIDDEN, response.status)
440
441 # Now, we delete the image from the server and verify that
442 # the image cache no longer contains the deleted image
443 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
444 image_id)
445 http = httplib2.Http()
446 response, content = http.request(path, 'DELETE')
447 self.assertEqual(http_client.OK, response.status)
448
449 self.assertFalse(os.path.exists(image_cached_path))
450
451 self.stop_servers()
452
453 @skip_if_disabled
454 def test_cache_middleware_trans_v2_without_download_image_policy(self): 277 def test_cache_middleware_trans_v2_without_download_image_policy(self):
455 """ 278 """
456 Ensure the image v2 API image transfer applied 'download_image' 279 Ensure the image v2 API image transfer applied 'download_image'
@@ -511,489 +334,6 @@ class BaseCacheMiddlewareTest(object):
511 334
512 self.stop_servers() 335 self.stop_servers()
513 336
514 @skip_if_disabled
515 def test_cache_middleware_trans_with_deactivated_image(self):
516 """
517 Ensure the image v1/v2 API image transfer forbids downloading
518 deactivated images.
519 Image deactivation is not available in v1. So, we'll deactivate the
520 image using v2 but test image transfer with both v1 and v2.
521 """
522 self.cleanup()
523 self.start_servers(**self.__dict__.copy())
524
525 # Add an image and verify a 200 OK is returned
526 image_data = b"*" * FIVE_KB
527 headers = minimal_headers('Image1')
528 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
529 http = httplib2.Http()
530 response, content = http.request(path, 'POST', headers=headers,
531 body=image_data)
532 self.assertEqual(http_client.CREATED, response.status)
533 data = jsonutils.loads(content)
534 self.assertEqual(hashlib.md5(image_data).hexdigest(),
535 data['image']['checksum'])
536 self.assertEqual(FIVE_KB, data['image']['size'])
537 self.assertEqual("Image1", data['image']['name'])
538 self.assertTrue(data['image']['is_public'])
539
540 image_id = data['image']['id']
541
542 # Grab the image
543 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
544 image_id)
545 http = httplib2.Http()
546 response, content = http.request(path, 'GET')
547 self.assertEqual(http_client.OK, response.status)
548
549 # Verify image in cache
550 image_cached_path = os.path.join(self.api_server.image_cache_dir,
551 image_id)
552 self.assertTrue(os.path.exists(image_cached_path))
553
554 # Deactivate the image using v2
555 path = "http://%s:%d/v2/images/%s/actions/deactivate"
556 path = path % ("127.0.0.1", self.api_port, image_id)
557 http = httplib2.Http()
558 response, content = http.request(path, 'POST')
559 self.assertEqual(http_client.NO_CONTENT, response.status)
560
561 # Download the image with v1. Ensure it is forbidden
562 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
563 image_id)
564 http = httplib2.Http()
565 response, content = http.request(path, 'GET')
566 self.assertEqual(http_client.FORBIDDEN, response.status)
567
568 # Download the image with v2. This succeeds because
569 # we are in admin context.
570 path = "http://%s:%d/v2/images/%s/file" % ("127.0.0.1", self.api_port,
571 image_id)
572 http = httplib2.Http()
573 response, content = http.request(path, 'GET')
574 self.assertEqual(http_client.OK, response.status)
575
576 # Reactivate the image using v2
577 path = "http://%s:%d/v2/images/%s/actions/reactivate"
578 path = path % ("127.0.0.1", self.api_port, image_id)
579 http = httplib2.Http()
580 response, content = http.request(path, 'POST')
581 self.assertEqual(http_client.NO_CONTENT, response.status)
582
583 # Download the image with v1. Ensure it is allowed
584 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
585 image_id)
586 http = httplib2.Http()
587 response, content = http.request(path, 'GET')
588 self.assertEqual(http_client.OK, response.status)
589
590 # Download the image with v2. Ensure it is allowed
591 path = "http://%s:%d/v2/images/%s/file" % ("127.0.0.1", self.api_port,
592 image_id)
593 http = httplib2.Http()
594 response, content = http.request(path, 'GET')
595 self.assertEqual(http_client.OK, response.status)
596
597 # Now, we delete the image from the server and verify that
598 # the image cache no longer contains the deleted image
599 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
600 image_id)
601 http = httplib2.Http()
602 response, content = http.request(path, 'DELETE')
603 self.assertEqual(http_client.OK, response.status)
604
605 self.assertFalse(os.path.exists(image_cached_path))
606
607 self.stop_servers()
608
609
610class BaseCacheManageMiddlewareTest(object):
611
612 """Base test class for testing cache management middleware"""
613
614 def verify_no_images(self):
615 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
616 http = httplib2.Http()
617 response, content = http.request(path, 'GET')
618 self.assertEqual(http_client.OK, response.status)
619 data = jsonutils.loads(content)
620 self.assertIn('images', data)
621 self.assertEqual(0, len(data['images']))
622
623 def add_image(self, name):
624 """
625 Adds an image and returns the newly-added image
626 identifier
627 """
628 image_data = b"*" * FIVE_KB
629 headers = minimal_headers('%s' % name)
630
631 path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
632 http = httplib2.Http()
633 response, content = http.request(path, 'POST', headers=headers,
634 body=image_data)
635 self.assertEqual(http_client.CREATED, response.status)
636 data = jsonutils.loads(content)
637 self.assertEqual(hashlib.md5(image_data).hexdigest(),
638 data['image']['checksum'])
639 self.assertEqual(FIVE_KB, data['image']['size'])
640 self.assertEqual(name, data['image']['name'])
641 self.assertTrue(data['image']['is_public'])
642 return data['image']['id']
643
644 def verify_no_cached_images(self):
645 """
646 Verify no images in the image cache
647 """
648 path = "http://%s:%d/v1/cached_images" % ("127.0.0.1", self.api_port)
649 http = httplib2.Http()
650 response, content = http.request(path, 'GET')
651 self.assertEqual(http_client.OK, response.status)
652
653 data = jsonutils.loads(content)
654 self.assertIn('cached_images', data)
655 self.assertEqual([], data['cached_images'])
656
657 @skip_if_disabled
658 def test_user_not_authorized(self):
659 self.cleanup()
660 self.start_servers(**self.__dict__.copy())
661 self.verify_no_images()
662
663 image_id1 = self.add_image("Image1")
664 image_id2 = self.add_image("Image2")
665
666 # Verify image does not yet show up in cache (we haven't "hit"
667 # it yet using a GET /images/1 ...
668 self.verify_no_cached_images()
669
670 # Grab the image
671 path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
672 image_id1)
673 http = httplib2.Http()
674 response, content = http.request(path, 'GET')
675 self.assertEqual(http_client.OK, response.status)
676
677 # Verify image now in cache
678 path = "http://%s:%d/v1/cached_images" % ("127.0.0.1", self.api_port)
679 http = httplib2.Http()
680 response, content = http.request(path, 'GET')
681 self.assertEqual(http_client.OK, response.status)
682
683 data = jsonutils.loads(content)
684 self.assertIn('cached_images', data)
685
686 cached_images = data['cached_images']
687 self.assertEqual(1, len(cached_images))
688 self.assertEqual(image_id1, cached_images[0]['image_id'])
689
690 # Set policy to disallow access to cache management
691 rules = {"manage_image_cache": '!'}
692 self.set_policy_rules(rules)
693
694 # Verify an unprivileged user cannot see cached images
695 path = "http://%s:%d/v1/cached_images" % ("127.0.0.1", self.api_port)
696 http = httplib2.Http()
697 response, content = http.request(path, 'GET')
698 self.assertEqual(http_client.FORBIDDEN, response.status)
699
700 # Verify an unprivileged user cannot delete images from the cache
701 path = "http://%s:%d/v1/cached_images/%s" % ("127.0.0.1",
702 self.api_port, image_id1)
703 http = httplib2.Http()
704 response, content = http.request(path, 'DELETE')
705 self.assertEqual(http_clien