Cache management API endpoints
This change adds the new cache API endpoints and their related new policies. Implements-bp: https://blueprints.launchpad.net/glance/+spec/cache-api Change-Id: I69162e19bf095ef11fbac56a1ea2159d1caefba7
This commit is contained in:
parent
4f4fc9b15d
commit
87eae327bf
84
api-ref/source/v2/cache-manage.inc
Normal file
84
api-ref/source/v2/cache-manage.inc
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.. -*- rst -*-
|
||||||
|
|
||||||
|
Cache Manage
|
||||||
|
************
|
||||||
|
|
||||||
|
List and manage the cache.
|
||||||
|
|
||||||
|
|
||||||
|
Query cache status
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: GET /v2/cache/
|
||||||
|
|
||||||
|
Lists all images in cache or queue.
|
||||||
|
*(Since Image API v2.14)*
|
||||||
|
|
||||||
|
Normal response codes: 200
|
||||||
|
|
||||||
|
Error response codes: 400, 401, 403
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
No request parameters.
|
||||||
|
|
||||||
|
|
||||||
|
Queue image
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: PUT /v2/cache/{image_id}/
|
||||||
|
|
||||||
|
Queues image for caching.
|
||||||
|
*(Since Image API v2.14)*
|
||||||
|
|
||||||
|
Normal response codes: 200
|
||||||
|
|
||||||
|
Error response codes: 400, 401, 403, 404
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
- image_id: image_id-in-path
|
||||||
|
|
||||||
|
|
||||||
|
Delete image from cache
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: DELETE /v2/cache/{image_id}/
|
||||||
|
|
||||||
|
Deletes a image from cache.
|
||||||
|
*(Since Image API v2.14)*
|
||||||
|
|
||||||
|
Normal response codes: 204
|
||||||
|
|
||||||
|
Error response codes: 400, 401, 403, 404
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
- image_id: image_id-in-path
|
||||||
|
|
||||||
|
|
||||||
|
Clear images from cache
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: DELETE /v2/cache/
|
||||||
|
|
||||||
|
Clears the cache and its queue.
|
||||||
|
*(Since Image API v2.14)*
|
||||||
|
|
||||||
|
Normal response codes: 204
|
||||||
|
|
||||||
|
Error response codes: 400, 401, 403
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: images-parameters.yaml
|
||||||
|
|
||||||
|
- x-image-cache-clear-target: cache-clear-header
|
@ -1,4 +1,12 @@
|
|||||||
# variables in header
|
# variables in header
|
||||||
|
cache-clear-header:
|
||||||
|
description: |
|
||||||
|
A keyword indicating 'cache', 'queue' or empty string to indicate the delete
|
||||||
|
API to delete images from cache or queue or delete from both. If this header
|
||||||
|
is missing then all cached and queued images for caching will be deleted.
|
||||||
|
in: header
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
Content-Length:
|
Content-Length:
|
||||||
description: |
|
description: |
|
||||||
The length of the body in octets (8-bit bytes)
|
The length of the body in octets (8-bit bytes)
|
||||||
|
@ -33,3 +33,4 @@ Image Service API v2 (CURRENT)
|
|||||||
.. include:: discovery.inc
|
.. include:: discovery.inc
|
||||||
.. include:: tasks.inc
|
.. include:: tasks.inc
|
||||||
.. include:: tasks-schemas.inc
|
.. include:: tasks-schemas.inc
|
||||||
|
.. include:: cache-manage.inc
|
||||||
|
@ -1390,8 +1390,8 @@ configuration file, select the appropriate deployment flavor like so::
|
|||||||
[paste_deploy]
|
[paste_deploy]
|
||||||
flavor = caching
|
flavor = caching
|
||||||
|
|
||||||
Enabling the Image Cache Management Middleware
|
Enabling the Image Cache Management Middleware (DEPRECATED)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
There is an optional ``cachemanage`` middleware that allows you to
|
There is an optional ``cachemanage`` middleware that allows you to
|
||||||
directly interact with cache images. Use this flavor in place of the
|
directly interact with cache images. Use this flavor in place of the
|
||||||
@ -1402,6 +1402,11 @@ can chose: ``cachemanagement``, ``keystone+cachemanagement`` and
|
|||||||
[paste_deploy]
|
[paste_deploy]
|
||||||
flavor = keystone+cachemanagement
|
flavor = keystone+cachemanagement
|
||||||
|
|
||||||
|
The new cache management endpoints were introduced in Images API v. 2.13.
|
||||||
|
If cache middleware is configured the new endpoints will be active and
|
||||||
|
there is no need to use the cachemanagement middleware unless the old
|
||||||
|
`glance-cache-manage` tooling is desired to be still used.
|
||||||
|
|
||||||
Configuration Options Affecting the Image Cache
|
Configuration Options Affecting the Image Cache
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -83,6 +83,8 @@ class VersionNegotiationFilter(wsgi.Middleware):
|
|||||||
allowed_versions['v2.7'] = 2
|
allowed_versions['v2.7'] = 2
|
||||||
allowed_versions['v2.9'] = 2
|
allowed_versions['v2.9'] = 2
|
||||||
allowed_versions['v2.13'] = 2
|
allowed_versions['v2.13'] = 2
|
||||||
|
if CONF.image_cache_dir:
|
||||||
|
allowed_versions['v2.14'] = 2
|
||||||
if CONF.enabled_backends:
|
if CONF.enabled_backends:
|
||||||
allowed_versions['v2.8'] = 2
|
allowed_versions['v2.8'] = 2
|
||||||
allowed_versions['v2.10'] = 2
|
allowed_versions['v2.10'] = 2
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
Controller for Image Cache Management API
|
Controller for Image Cache Management API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import glance_store
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
@ -24,8 +26,14 @@ from glance.api import policy
|
|||||||
from glance.api.v2 import policy as api_policy
|
from glance.api.v2 import policy as api_policy
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
|
import glance.db
|
||||||
|
import glance.gateway
|
||||||
|
from glance.i18n import _
|
||||||
from glance import image_cache
|
from glance import image_cache
|
||||||
|
import glance.notifier
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -34,19 +42,36 @@ class CacheController(object):
|
|||||||
A controller for managing cached images.
|
A controller for managing cached images.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, db_api=None, policy_enforcer=None, notifier=None,
|
||||||
self.cache = image_cache.ImageCache()
|
store_api=None):
|
||||||
self.policy = policy.Enforcer()
|
if not CONF.image_cache_dir:
|
||||||
|
self.cache = None
|
||||||
|
else:
|
||||||
|
self.cache = image_cache.ImageCache()
|
||||||
|
|
||||||
def _enforce(self, req):
|
self.policy = policy_enforcer or policy.Enforcer()
|
||||||
"""Authorize request against 'manage_image_cache' policy"""
|
self.db_api = db_api or glance.db.get_api()
|
||||||
|
self.notifier = notifier or glance.notifier.Notifier()
|
||||||
|
self.store_api = store_api or glance_store
|
||||||
|
self.gateway = glance.gateway.Gateway(self.db_api, self.store_api,
|
||||||
|
self.notifier, self.policy)
|
||||||
|
|
||||||
|
def _enforce(self, req, image=None, new_policy=None):
|
||||||
|
"""Authorize request against given policy"""
|
||||||
|
if not new_policy:
|
||||||
|
new_policy = 'manage_image_cache'
|
||||||
try:
|
try:
|
||||||
api_policy.CacheImageAPIPolicy(
|
api_policy.CacheImageAPIPolicy(
|
||||||
req.context, enforcer=self.policy).manage_image_cache()
|
req.context, image=image, enforcer=self.policy,
|
||||||
|
policy_str=new_policy).manage_image_cache()
|
||||||
except exception.Forbidden:
|
except exception.Forbidden:
|
||||||
LOG.debug("User not permitted to manage the image cache")
|
LOG.debug("User not permitted by '%s' policy" % new_policy)
|
||||||
raise webob.exc.HTTPForbidden()
|
raise webob.exc.HTTPForbidden()
|
||||||
|
|
||||||
|
if not CONF.image_cache_dir:
|
||||||
|
msg = _("Caching via API is not supported at this site.")
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
def get_cached_images(self, req):
|
def get_cached_images(self, req):
|
||||||
"""
|
"""
|
||||||
GET /cached_images
|
GET /cached_images
|
||||||
@ -114,6 +139,99 @@ class CacheController(object):
|
|||||||
self._enforce(req)
|
self._enforce(req)
|
||||||
return dict(num_deleted=self.cache.delete_all_queued_images())
|
return dict(num_deleted=self.cache.delete_all_queued_images())
|
||||||
|
|
||||||
|
def delete_cache_entry(self, req, image_id):
|
||||||
|
"""
|
||||||
|
DELETE /cache/<IMAGE_ID> - Remove image from cache
|
||||||
|
|
||||||
|
Removes the image from cache or queue.
|
||||||
|
"""
|
||||||
|
image_repo = self.gateway.get_repo(
|
||||||
|
req.context, authorization_layer=False)
|
||||||
|
try:
|
||||||
|
image = image_repo.get(image_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
# We are going to raise this error only if image is
|
||||||
|
# not present in cache or queue list
|
||||||
|
image = None
|
||||||
|
if not self.image_exists_in_cache(image_id):
|
||||||
|
msg = _("Image %s not found.") % image_id
|
||||||
|
LOG.warning(msg)
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
|
self._enforce(req, new_policy='cache_delete', image=image)
|
||||||
|
self.cache.delete_cached_image(image_id)
|
||||||
|
self.cache.delete_queued_image(image_id)
|
||||||
|
|
||||||
|
def image_exists_in_cache(self, image_id):
|
||||||
|
queued_images = self.cache.get_queued_images()
|
||||||
|
if image_id in queued_images:
|
||||||
|
return True
|
||||||
|
|
||||||
|
cached_images = self.cache.get_cached_images()
|
||||||
|
if image_id in [image['image_id'] for image in cached_images]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_cache(self, req):
|
||||||
|
"""
|
||||||
|
DELETE /cache - Clear cache and queue
|
||||||
|
|
||||||
|
Removes all images from cache and queue.
|
||||||
|
"""
|
||||||
|
self._enforce(req, new_policy='cache_delete')
|
||||||
|
target = req.headers.get('x-image-cache-clear-target', '').lower()
|
||||||
|
if target == '':
|
||||||
|
res = dict(cache_deleted=self.cache.delete_all_cached_images(),
|
||||||
|
queue_deleted=self.cache.delete_all_queued_images())
|
||||||
|
elif target == 'cache':
|
||||||
|
res = dict(cache_deleted=self.cache.delete_all_cached_images())
|
||||||
|
elif target == 'queue':
|
||||||
|
res = dict(queue_deleted=self.cache.delete_all_queued_images())
|
||||||
|
else:
|
||||||
|
reason = (_("If provided 'x-image-cache-clear-target' must be "
|
||||||
|
"'cache', 'queue' or empty string."))
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=reason,
|
||||||
|
request=req,
|
||||||
|
content_type='text/plain')
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_cache_state(self, req):
|
||||||
|
"""
|
||||||
|
GET /cache/ - Get currently cached and queued images
|
||||||
|
|
||||||
|
Returns dict of cached and queued images
|
||||||
|
"""
|
||||||
|
self._enforce(req, new_policy='cache_list')
|
||||||
|
return dict(cached_images=self.cache.get_cached_images(),
|
||||||
|
queued_images=self.cache.get_queued_images())
|
||||||
|
|
||||||
|
def queue_image_from_api(self, req, image_id):
|
||||||
|
"""
|
||||||
|
PUT /cache/<IMAGE_ID>
|
||||||
|
|
||||||
|
Queues an image for caching. We do not check to see if
|
||||||
|
the image is in the registry here. That is done by the
|
||||||
|
prefetcher...
|
||||||
|
"""
|
||||||
|
image_repo = self.gateway.get_repo(
|
||||||
|
req.context, authorization_layer=False)
|
||||||
|
try:
|
||||||
|
image = image_repo.get(image_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _("Image %s not found.") % image_id
|
||||||
|
LOG.warning(msg)
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
|
self._enforce(req, new_policy='cache_image', image=image)
|
||||||
|
|
||||||
|
if image.status != 'active':
|
||||||
|
msg = _("Only images with status active can be targeted for "
|
||||||
|
"queueing")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
self.cache.queue_image(image_id)
|
||||||
|
|
||||||
|
|
||||||
class CachedImageDeserializer(wsgi.JSONRequestDeserializer):
|
class CachedImageDeserializer(wsgi.JSONRequestDeserializer):
|
||||||
pass
|
pass
|
||||||
|
@ -105,14 +105,21 @@ class APIPolicyBase(object):
|
|||||||
|
|
||||||
|
|
||||||
class CacheImageAPIPolicy(APIPolicyBase):
|
class CacheImageAPIPolicy(APIPolicyBase):
|
||||||
def __init__(self, context, target=None, enforcer=None):
|
def __init__(self, context, image=None, policy_str=None,
|
||||||
|
target=None, enforcer=None):
|
||||||
self._context = context
|
self._context = context
|
||||||
self._target = target or {}
|
target = {}
|
||||||
|
self._image = image
|
||||||
|
if self._image:
|
||||||
|
target = policy.ImageTarget(self._image)
|
||||||
|
|
||||||
|
self._target = target
|
||||||
self.enforcer = enforcer or policy.Enforcer()
|
self.enforcer = enforcer or policy.Enforcer()
|
||||||
|
self.policy_str = policy_str
|
||||||
super(CacheImageAPIPolicy, self).__init__(context, target, enforcer)
|
super(CacheImageAPIPolicy, self).__init__(context, target, enforcer)
|
||||||
|
|
||||||
def manage_image_cache(self):
|
def manage_image_cache(self):
|
||||||
self._enforce('manage_image_cache')
|
self._enforce(self.policy_str)
|
||||||
|
|
||||||
|
|
||||||
class ImageAPIPolicy(APIPolicyBase):
|
class ImageAPIPolicy(APIPolicyBase):
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from glance.api.v2 import cached_images
|
||||||
from glance.api.v2 import discovery
|
from glance.api.v2 import discovery
|
||||||
from glance.api.v2 import image_actions
|
from glance.api.v2 import image_actions
|
||||||
from glance.api.v2 import image_data
|
from glance.api.v2 import image_data
|
||||||
@ -593,4 +594,32 @@ class API(wsgi.Router):
|
|||||||
action='get_usage',
|
action='get_usage',
|
||||||
conditions={'method': ['GET']})
|
conditions={'method': ['GET']})
|
||||||
|
|
||||||
|
# Cache Management API
|
||||||
|
cache_manage_resource = cached_images.create_resource()
|
||||||
|
mapper.connect('/cache',
|
||||||
|
controller=cache_manage_resource,
|
||||||
|
action='get_cache_state',
|
||||||
|
conditions={'method': ['GET']},
|
||||||
|
body_reject=True)
|
||||||
|
mapper.connect('/cache',
|
||||||
|
controller=cache_manage_resource,
|
||||||
|
action='clear_cache',
|
||||||
|
conditions={'method': ['DELETE']})
|
||||||
|
mapper.connect('/cache',
|
||||||
|
controller=reject_method_resource,
|
||||||
|
action='reject',
|
||||||
|
allowed_methods='GET, DELETE')
|
||||||
|
mapper.connect('/cache/{image_id}',
|
||||||
|
controller=cache_manage_resource,
|
||||||
|
action='delete_cache_entry',
|
||||||
|
conditions={'method': ['DELETE']})
|
||||||
|
mapper.connect('/cache/{image_id}',
|
||||||
|
controller=cache_manage_resource,
|
||||||
|
action='queue_image_from_api',
|
||||||
|
conditions={'method': ['PUT']})
|
||||||
|
mapper.connect('/cache/{image_id}',
|
||||||
|
controller=reject_method_resource,
|
||||||
|
action='reject',
|
||||||
|
allowed_methods='DELETE, PUT')
|
||||||
|
|
||||||
super(API, self).__init__(mapper)
|
super(API, self).__init__(mapper)
|
||||||
|
@ -77,6 +77,15 @@ class Controller(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
version_objs = []
|
version_objs = []
|
||||||
|
if CONF.image_cache_dir:
|
||||||
|
version_objs.extend([
|
||||||
|
build_version_object(2.14, 'v2', 'CURRENT'),
|
||||||
|
build_version_object(2.13, 'v2', 'SUPPORTED'),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
version_objs.extend([
|
||||||
|
build_version_object(2.13, 'v2', 'CURRENT'),
|
||||||
|
])
|
||||||
if CONF.enabled_backends:
|
if CONF.enabled_backends:
|
||||||
version_objs.extend([
|
version_objs.extend([
|
||||||
build_version_object(2.12, 'v2', 'SUPPORTED'),
|
build_version_object(2.12, 'v2', 'SUPPORTED'),
|
||||||
@ -90,7 +99,6 @@ class Controller(object):
|
|||||||
build_version_object(2.9, 'v2', 'SUPPORTED'),
|
build_version_object(2.9, 'v2', 'SUPPORTED'),
|
||||||
])
|
])
|
||||||
version_objs.extend([
|
version_objs.extend([
|
||||||
build_version_object(2.13, 'v2', 'CURRENT'),
|
|
||||||
build_version_object(2.7, 'v2', 'SUPPORTED'),
|
build_version_object(2.7, 'v2', 'SUPPORTED'),
|
||||||
build_version_object(2.6, 'v2', 'SUPPORTED'),
|
build_version_object(2.6, 'v2', 'SUPPORTED'),
|
||||||
build_version_object(2.5, 'v2', 'SUPPORTED'),
|
build_version_object(2.5, 'v2', 'SUPPORTED'),
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from glance.policies import base
|
from glance.policies import base
|
||||||
|
from glance.policies import cache
|
||||||
from glance.policies import image
|
from glance.policies import image
|
||||||
from glance.policies import metadef
|
from glance.policies import metadef
|
||||||
from glance.policies import tasks
|
from glance.policies import tasks
|
||||||
@ -24,4 +25,5 @@ def list_rules():
|
|||||||
image.list_rules(),
|
image.list_rules(),
|
||||||
tasks.list_rules(),
|
tasks.list_rules(),
|
||||||
metadef.list_rules(),
|
metadef.list_rules(),
|
||||||
|
cache.list_rules(),
|
||||||
)
|
)
|
||||||
|
@ -90,6 +90,7 @@ ADMIN_OR_PROJECT_READER_OR_SHARED_MEMBER = (
|
|||||||
f'role:reader and (project_id:%(project_id)s or {IMAGE_MEMBER_CHECK})'
|
f'role:reader and (project_id:%(project_id)s or {IMAGE_MEMBER_CHECK})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ADMIN = f'role:admin'
|
||||||
|
|
||||||
rules = [
|
rules = [
|
||||||
policy.RuleDefault(name='default', check_str='',
|
policy.RuleDefault(name='default', check_str='',
|
||||||
|
75
glance/policies/cache.py
Normal file
75
glance/policies/cache.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from oslo_log import versionutils
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from glance.policies import base
|
||||||
|
|
||||||
|
|
||||||
|
DEPRECATED_REASON = """
|
||||||
|
The image API now supports roles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
cache_policies = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name="cache_image",
|
||||||
|
check_str=base.ADMIN,
|
||||||
|
scope_types=['project'],
|
||||||
|
description='Queue image for caching',
|
||||||
|
operations=[
|
||||||
|
{'path': '/v2/cache/{image_id}',
|
||||||
|
'method': 'PUT'}
|
||||||
|
],
|
||||||
|
deprecated_rule=policy.DeprecatedRule(
|
||||||
|
name="cache_image", check_str="rule:manage_image_cache",
|
||||||
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
|
deprecated_since=versionutils.deprecated.XENA
|
||||||
|
),
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name="cache_list",
|
||||||
|
check_str=base.ADMIN,
|
||||||
|
scope_types=['project'],
|
||||||
|
description='List cache status',
|
||||||
|
operations=[
|
||||||
|
{'path': '/v2/cache',
|
||||||
|
'method': 'GET'}
|
||||||
|
],
|
||||||
|
deprecated_rule=policy.DeprecatedRule(
|
||||||
|
name="cache_list", check_str="rule:manage_image_cache",
|
||||||
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
|
deprecated_since=versionutils.deprecated.XENA
|
||||||
|
),
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name="cache_delete",
|
||||||
|
check_str=base.ADMIN,
|
||||||
|
scope_types=['project'],
|
||||||
|
description='Delete image(s) from cache and/or queue',
|
||||||
|
operations=[
|
||||||
|
{'path': '/v2/cache',
|
||||||
|
'method': 'DELETE'},
|
||||||
|
{'path': '/v2/cache/{image_id}',
|
||||||
|
'method': 'DELETE'}
|
||||||
|
],
|
||||||
|
deprecated_rule=policy.DeprecatedRule(
|
||||||
|
name="cache_delete", check_str="rule:manage_image_cache",
|
||||||
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
|
deprecated_since=versionutils.deprecated.XENA
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return cache_policies
|
@ -1550,6 +1550,8 @@ class SynchronousAPIBase(test_utils.BaseTestCase):
|
|||||||
CacheManageFilter.factory
|
CacheManageFilter.factory
|
||||||
[pipeline:glance-api-cachemanagement]
|
[pipeline:glance-api-cachemanagement]
|
||||||
pipeline = context cache cachemanage rootapp
|
pipeline = context cache cachemanage rootapp
|
||||||
|
[pipeline:glance-api-caching]
|
||||||
|
pipeline = context cache rootapp
|
||||||
[pipeline:glance-api]
|
[pipeline:glance-api]
|
||||||
pipeline = context rootapp
|
pipeline = context rootapp
|
||||||
[composite:rootapp]
|
[composite:rootapp]
|
||||||
|
@ -30,7 +30,8 @@ class TestApiVersions(functional.FunctionalTest):
|
|||||||
self.start_servers(**self.__dict__.copy())
|
self.start_servers(**self.__dict__.copy())
|
||||||
|
|
||||||
url = 'http://127.0.0.1:%d' % self.api_port
|
url = 'http://127.0.0.1:%d' % self.api_port
|
||||||
versions = {'versions': tv.get_versions_list(url)}
|
versions = {'versions': tv.get_versions_list(url,
|
||||||
|
enabled_cache=True)}
|
||||||
|
|
||||||
# Verify version choices returned.
|
# Verify version choices returned.
|
||||||
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
||||||
@ -44,7 +45,8 @@ class TestApiVersions(functional.FunctionalTest):
|
|||||||
self.start_servers(**self.__dict__.copy())
|
self.start_servers(**self.__dict__.copy())
|
||||||
|
|
||||||
url = 'http://127.0.0.1:%d' % self.api_port
|
url = 'http://127.0.0.1:%d' % self.api_port
|
||||||
versions = {'versions': tv.get_versions_list(url)}
|
versions = {'versions': tv.get_versions_list(url,
|
||||||
|
enabled_cache=True)}
|
||||||
|
|
||||||
# Verify version choices returned.
|
# Verify version choices returned.
|
||||||
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
||||||
@ -62,7 +64,8 @@ class TestApiVersionsMultistore(functional.MultipleBackendFunctionalTest):
|
|||||||
|
|
||||||
url = 'http://127.0.0.1:%d' % self.api_port
|
url = 'http://127.0.0.1:%d' % self.api_port
|
||||||
versions = {'versions': tv.get_versions_list(url,
|
versions = {'versions': tv.get_versions_list(url,
|
||||||
enabled_backends=True)}
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)}
|
||||||
|
|
||||||
# Verify version choices returned.
|
# Verify version choices returned.
|
||||||
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
||||||
@ -77,7 +80,8 @@ class TestApiVersionsMultistore(functional.MultipleBackendFunctionalTest):
|
|||||||
|
|
||||||
url = 'http://127.0.0.1:%d' % self.api_port
|
url = 'http://127.0.0.1:%d' % self.api_port
|
||||||
versions = {'versions': tv.get_versions_list(url,
|
versions = {'versions': tv.get_versions_list(url,
|
||||||
enabled_backends=True)}
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)}
|
||||||
|
|
||||||
# Verify version choices returned.
|
# Verify version choices returned.
|
||||||
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
path = 'http://%s:%d' % ('127.0.0.1', self.api_port)
|
||||||
@ -94,7 +98,8 @@ class TestApiPaths(functional.FunctionalTest):
|
|||||||
self.start_servers(**self.__dict__.copy())
|
self.start_servers(**self.__dict__.copy())
|
||||||
|
|
||||||
url = 'http://127.0.0.1:%d' % self.api_port
|
url = 'http://127.0.0.1:%d' % self.api_port
|
||||||
self.versions = {'versions': tv.get_versions_list(url)}
|
self.versions = {'versions': tv.get_versions_list(url,
|
||||||
|
enabled_cache=True)}
|
||||||
images = {'images': []}
|
images = {'images': []}
|
||||||
self.images_json = jsonutils.dumps(images)
|
self.images_json = jsonutils.dumps(images)
|
||||||
|
|
||||||
|
360
glance/tests/functional/v2/test_cache_api.py
Normal file
360
glance/tests/functional/v2/test_cache_api.py
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
# Copyright 2021 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import oslo_policy.policy
|
||||||
|
|
||||||
|
from glance.api import policy
|
||||||
|
from glance.image_cache import prefetcher
|
||||||
|
from glance.tests import functional
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageCache(functional.SynchronousAPIBase):
|
||||||
|
# ToDo(abhishekk): Once system scope is enabled and RBAC is fully
|
||||||
|
# supported, enable these tests for RBAC as well
|
||||||
|
def setUp(self):
|
||||||
|
super(TestImageCache, self).setUp()
|
||||||
|
self.policy = policy.Enforcer(suppress_deprecation_warnings=True)
|
||||||
|
|
||||||
|
def set_policy_rules(self, rules):
|
||||||
|
self.policy.set_rules(
|
||||||
|
oslo_policy.policy.Rules.from_dict(rules),
|
||||||
|
overwrite=True)
|
||||||
|
|
||||||
|
def start_server(self, enable_cache=True):
|
||||||
|
with mock.patch.object(policy, 'Enforcer') as mock_enf:
|
||||||
|
mock_enf.return_value = self.policy
|
||||||
|
super(TestImageCache, self).start_server(enable_cache=enable_cache)
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
output = {}
|
||||||
|
# Create 1 queued image as well for testing
|
||||||
|
path = "/v2/images"
|
||||||
|
data = {
|
||||||
|
'name': 'queued-image',
|
||||||
|
'container_format': 'bare',
|
||||||
|
'disk_format': 'raw'
|
||||||
|
}
|
||||||
|
response = self.api_post(path, json=data)
|
||||||
|
self.assertEqual(201, response.status_code)
|
||||||
|
image_id = response.json['id']
|
||||||
|
output['queued'] = image_id
|
||||||
|
|
||||||
|
for visibility in ['public', 'private', 'community', 'shared']:
|
||||||
|
data = {
|
||||||
|
'name': '%s-image' % visibility,
|
||||||
|
'visibility': visibility,
|
||||||
|
'container_format': 'bare',
|
||||||
|
'disk_format': 'raw'
|
||||||
|
}
|
||||||
|
response = self.api_post(path, json=data)
|
||||||
|
self.assertEqual(201, response.status_code)
|
||||||
|
image_id = response.json['id']
|
||||||
|
# Upload some data to image
|
||||||
|
response = self.api_put(
|
||||||
|
'/v2/images/%s/file' % image_id,
|
||||||
|
headers={'Content-Type': 'application/octet-stream'},
|
||||||
|
data=b'IMAGEDATA')
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
output[visibility] = image_id
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def list_cache(self, expected_code=200):
|
||||||
|
path = '/v2/cache'
|
||||||
|
response = self.api_get(path)
|
||||||
|
self.assertEqual(expected_code, response.status_code)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json
|
||||||
|
|
||||||
|
def cache_queue(self, image_id, expected_code=200):
|
||||||
|
# Queue image for prefetching
|
||||||
|
path = '/v2/cache/%s' % image_id
|
||||||
|
response = self.api_put(path)
|
||||||
|
self.assertEqual(expected_code, response.status_code)
|
||||||
|
|
||||||
|
def cache_delete(self, image_id, expected_code=200):
|
||||||
|
path = '/v2/cache/%s' % image_id
|
||||||
|
response = self.api_delete(path)
|
||||||
|
self.assertEqual(expected_code, response.status_code)
|
||||||
|
|
||||||
|
def cache_clear(self, target='', expected_code=200):
|
||||||
|
path = '/v2/cache'
|
||||||
|
headers = {}
|
||||||
|
if target:
|
||||||
|
headers['x-image-cache-clear-target'] = target
|
||||||
|
response = self.api_delete(path, headers=headers)
|
||||||
|
if target not in ('', 'cache', 'queue'):
|
||||||
|
self.assertEqual(expected_code, response.status_code)
|
||||||
|
else:
|
||||||
|
self.assertEqual(expected_code, response.status_code)
|
||||||
|
|
||||||
|
def cache_image(self):
|
||||||
|
# NOTE(abhishekk): Here we are not running periodic job which caches
|
||||||
|
# queued images as precaching is not part of this patch, so to test
|
||||||
|
# all caching operations we are using this way to cache images for us
|
||||||
|
cache_prefetcher = prefetcher.Prefetcher()
|
||||||
|
cache_prefetcher.run()
|
||||||
|
|
||||||
|
def test_cache_api_lifecycle(self):
|
||||||
|
self.start_server(enable_cache=True)
|
||||||
|
images = self.load_data()
|
||||||
|
|
||||||
|
# Ensure that nothing is cached and nothing is queued for caching
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Try non-existing image to queue for caching
|
||||||
|
self.cache_queue('non-existing-image-id', expected_code=404)
|
||||||
|
|
||||||
|
# Verify that you can not queue non-active image
|
||||||
|
self.cache_queue(images['queued'], expected_code=400)
|
||||||
|
|
||||||
|
# Queue 1 image for caching
|
||||||
|
self.cache_queue(images['public'])
|
||||||
|
# Now verify that we have 1 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['public'], output['queued_images'])
|
||||||
|
|
||||||
|
# Cache the image
|
||||||
|
self.cache_image()
|
||||||
|
# Now verify that we have 0 queued image and 1 cached image
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(1, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['public'], output['cached_images'][0]['image_id'])
|
||||||
|
|
||||||
|
# Queue 2nd image for caching
|
||||||
|
self.cache_queue(images['community'])
|
||||||
|
# Now verify that we have 1 image queued for caching and 1
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.assertEqual(1, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['community'], output['queued_images'])
|
||||||
|
self.assertIn(images['public'], output['cached_images'][0]['image_id'])
|
||||||
|
|
||||||
|
# Queue 3rd image for caching
|
||||||
|
self.cache_queue(images['private'])
|
||||||
|
# Now verify that we have 2 images queued for caching and 1
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(2, len(output['queued_images']))
|
||||||
|
self.assertEqual(1, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['private'], output['queued_images'])
|
||||||
|
|
||||||
|
# Try to delete non-existing image from cache
|
||||||
|
self.cache_delete('non-existing-image-id', expected_code=404)
|
||||||
|
|
||||||
|
# Delete public image from cache
|
||||||
|
self.cache_delete(images['public'])
|
||||||
|
# Now verify that we have 2 image queued for caching and no
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(2, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Verify clearing cache fails with 400 if invalid header is passed
|
||||||
|
self.cache_clear(target='both', expected_code=400)
|
||||||
|
|
||||||
|
# Delete all queued images
|
||||||
|
self.cache_clear(target='queue')
|
||||||
|
# Now verify that we have 0 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Queue and cache image so we have something to clear
|
||||||
|
self.cache_queue(images['public'])
|
||||||
|
# Now verify that we have 1 queued image
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.cache_image()
|
||||||
|
# Now verify that we have 0 queued image and 1 cached image
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(1, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Delete all cached images
|
||||||
|
self.cache_clear(target='cache')
|
||||||
|
# Now verify that we have 0 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Now we need 2 queued images and 2 cached images in order
|
||||||
|
# to delete both of them together
|
||||||
|
self.cache_queue(images['public'])
|
||||||
|
self.cache_queue(images['private'])
|
||||||
|
# Now verify that we have 2 queued images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(2, len(output['queued_images']))
|
||||||
|
|
||||||
|
self.cache_image()
|
||||||
|
# Now verify that we have 0 queued images and 2 cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(2, len(output['cached_images']))
|
||||||
|
|
||||||
|
self.cache_queue(images['community'])
|
||||||
|
self.cache_queue(images['shared'])
|
||||||
|
# Verify we have 2 queued and 2 cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(2, len(output['queued_images']))
|
||||||
|
self.assertEqual(2, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Now delete all queued and all cached images at once
|
||||||
|
self.cache_clear()
|
||||||
|
# Now verify that we have 0 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
# Try to cache image again to validate nothing will be cached
|
||||||
|
self.cache_image()
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
def test_cache_image_queue_delete(self):
|
||||||
|
# This test verifies that if image is queued for caching
|
||||||
|
# and user deletes the original image, but it is still
|
||||||
|
# present in queued list and deleted with cache-delete API.
|
||||||
|
self.start_server(enable_cache=True)
|
||||||
|
images = self.load_data()
|
||||||
|
|
||||||
|
# Ensure that nothing is cached and nothing is queued for caching
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
self.cache_queue(images['public'])
|
||||||
|
# Now verify that we have 1 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['public'], output['queued_images'])
|
||||||
|
|
||||||
|
# Delete image and verify that it is still present
|
||||||
|
# in queued list
|
||||||
|
path = '/v2/images/%s' % images['public']
|
||||||
|
response = self.api_delete(path)
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
self.assertIn(images['public'], output['queued_images'])
|
||||||
|
|
||||||
|
# Deleted the image from queued list
|
||||||
|
self.cache_delete(images['public'])
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
def test_cache_image_cache_delete(self):
|
||||||
|
# This test verifies that if image is queued for caching
|
||||||
|
# and user deletes the original image, but it is still
|
||||||
|
# present in queued list and deleted with cache-delete API.
|
||||||
|
self.start_server(enable_cache=True)
|
||||||
|
images = self.load_data()
|
||||||
|
|
||||||
|
# Ensure that nothing is cached and nothing is queued for caching
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
self.cache_queue(images['public'])
|
||||||
|
# Now verify that we have 1 image queued for caching and 0
|
||||||
|
# cached images
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(1, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['public'], output['queued_images'])
|
||||||
|
|
||||||
|
# Cache the image
|
||||||
|
self.cache_image()
|
||||||
|
# Now verify that we have 0 queued image and 1 cached image
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(1, len(output['cached_images']))
|
||||||
|
# Verify same image is queued for caching
|
||||||
|
self.assertIn(images['public'], output['cached_images'][0]['image_id'])
|
||||||
|
|
||||||
|
# Delete image and verify that it is deleted from
|
||||||
|
# cache as well
|
||||||
|
path = '/v2/images/%s' % images['public']
|
||||||
|
response = self.api_delete(path)
|
||||||
|
self.assertEqual(204, response.status_code)
|
||||||
|
|
||||||
|
output = self.list_cache()
|
||||||
|
self.assertEqual(0, len(output['queued_images']))
|
||||||
|
self.assertEqual(0, len(output['cached_images']))
|
||||||
|
|
||||||
|
def test_cache_api_cache_disabled(self):
|
||||||
|
self.start_server(enable_cache=False)
|
||||||
|
images = self.load_data()
|
||||||
|
# As cache is not enabled each API call should return 404 response
|
||||||
|
self.list_cache(expected_code=404)
|
||||||
|
self.cache_queue(images['public'], expected_code=404)
|
||||||
|
self.cache_delete(images['public'], expected_code=404)
|
||||||
|
self.cache_clear(expected_code=404)
|
||||||
|
self.cache_clear(target='both', expected_code=404)
|
||||||
|
|
||||||
|
# Now disable cache policies and ensure that you will get 403
|
||||||
|
self.set_policy_rules({
|
||||||
|
'cache_list': '!',
|
||||||
|
'cache_delete': '!',
|
||||||
|
'cache_image': '!',
|
||||||
|
'add_image': '',
|
||||||
|
'upload_image': ''
|
||||||
|
})
|
||||||
|
self.list_cache(expected_code=403)
|
||||||
|
self.cache_queue(images['public'], expected_code=403)
|
||||||
|
self.cache_delete(images['public'], expected_code=403)
|
||||||
|
self.cache_clear(expected_code=403)
|
||||||
|
self.cache_clear(target='both', expected_code=403)
|
||||||
|
|
||||||
|
def test_cache_api_not_allowed(self):
|
||||||
|
self.start_server(enable_cache=True)
|
||||||
|
images = self.load_data()
|
||||||
|
# As cache operations are not allowed each API call should return
|
||||||
|
# 403 response
|
||||||
|
self.set_policy_rules({
|
||||||
|
'cache_list': '!',
|
||||||
|
'cache_delete': '!',
|
||||||
|
'cache_image': '!',
|
||||||
|
'add_image': '',
|
||||||
|
'upload_image': ''
|
||||||
|
})
|
||||||
|
self.list_cache(expected_code=403)
|
||||||
|
self.cache_queue(images['public'], expected_code=403)
|
||||||
|
self.cache_delete(images['public'], expected_code=403)
|
||||||
|
self.cache_clear(expected_code=403)
|
||||||
|
self.cache_clear(target='both', expected_code=403)
|
@ -13,30 +13,51 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import testtools
|
from unittest import mock
|
||||||
|
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from glance.api import policy
|
|
||||||
from glance.api.v2 import cached_images
|
from glance.api.v2 import cached_images
|
||||||
from glance.common import exception
|
import glance.gateway
|
||||||
from glance import image_cache
|
from glance import image_cache
|
||||||
|
from glance import notifier
|
||||||
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
|
import glance.tests.utils as test_utils
|
||||||
|
|
||||||
|
|
||||||
class FakePolicyEnforcer(policy.Enforcer):
|
UUID4 = '6bbe7cc2-eae7-4c0f-b50d-a7160b0c6a86'
|
||||||
def __init__(self):
|
|
||||||
self.default_rule = ''
|
|
||||||
self.policy_path = ''
|
|
||||||
self.policy_file_mtime = None
|
|
||||||
self.policy_file_contents = None
|
|
||||||
|
|
||||||
def enforce(self, context, action, target):
|
|
||||||
return 'pass'
|
|
||||||
|
|
||||||
def check(rule, target, creds, exc=None, *args, **kwargs):
|
class FakeImage(object):
|
||||||
return 'pass'
|
def __init__(self, id=None, status='active', container_format='ami',
|
||||||
|
disk_format='ami', locations=None):
|
||||||
|
self.id = id or UUID4
|
||||||
|
self.status = status
|
||||||
|
self.container_format = container_format
|
||||||
|
self.disk_format = disk_format
|
||||||
|
self.locations = locations
|
||||||
|
self.owner = unit_test_utils.TENANT1
|
||||||
|
self.created_at = ''
|
||||||
|
self.updated_at = ''
|
||||||
|
self.min_disk = ''
|
||||||
|
self.min_ram = ''
|
||||||
|
self.protected = False
|
||||||
|
self.checksum = ''
|
||||||
|
self.os_hash_algo = ''
|
||||||
|
self.os_hash_value = ''
|
||||||
|
self.size = 0
|
||||||
|
self.virtual_size = 0
|
||||||
|
self.visibility = 'public'
|
||||||
|
self.os_hidden = False
|
||||||
|
self.name = 'foo'
|
||||||
|
self.tags = []
|
||||||
|
self.extra_properties = {}
|
||||||
|
self.member = self.owner
|
||||||
|
|
||||||
def _check(self, context, rule, target, *args, **kwargs):
|
# NOTE(danms): This fixture looks more like the db object than
|
||||||
return 'pass'
|
# the proxy model. This needs fixing all through the tests
|
||||||
|
# below.
|
||||||
|
self.image_id = self.id
|
||||||
|
|
||||||
|
|
||||||
class FakeCache(image_cache.ImageCache):
|
class FakeCache(image_cache.ImageCache):
|
||||||
@ -48,13 +69,14 @@ class FakeCache(image_cache.ImageCache):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def get_cached_images(self):
|
def get_cached_images(self):
|
||||||
return {'id': 'test'}
|
return [{'image_id': 'test'}]
|
||||||
|
|
||||||
def delete_cached_image(self, image_id):
|
def delete_cached_image(self, image_id):
|
||||||
self.deleted_images.append(image_id)
|
self.deleted_images.append(image_id)
|
||||||
|
|
||||||
def delete_all_cached_images(self):
|
def delete_all_cached_images(self):
|
||||||
self.delete_cached_image(self.get_cached_images().get('id'))
|
self.delete_cached_image(
|
||||||
|
self.get_cached_images()[0].get('image_id'))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def get_queued_images(self):
|
def get_queued_images(self):
|
||||||
@ -74,72 +96,315 @@ class FakeCache(image_cache.ImageCache):
|
|||||||
class FakeController(cached_images.CacheController):
|
class FakeController(cached_images.CacheController):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cache = FakeCache()
|
self.cache = FakeCache()
|
||||||
self.policy = FakePolicyEnforcer()
|
self.db = unit_test_utils.FakeDB(initialize=False)
|
||||||
|
self.policy = unit_test_utils.FakePolicyEnforcer()
|
||||||
|
self.notifier = unit_test_utils.FakeNotifier()
|
||||||
|
self.store = unit_test_utils.FakeStoreAPI()
|
||||||
|
self.gateway = glance.gateway.Gateway(self.db, self.store,
|
||||||
|
self.notifier, self.policy)
|
||||||
|
|
||||||
|
|
||||||
class TestController(testtools.TestCase):
|
class TestController(test_utils.BaseTestCase):
|
||||||
def test_initialization_without_conf(self):
|
def test_initialization_without_conf(self):
|
||||||
self.assertRaises(exception.BadDriverConfiguration,
|
# NOTE(abhishekk): Since we are initializing cache driver only
|
||||||
cached_images.CacheController)
|
# if image_cache_dir is set, here we are checking that cache
|
||||||
|
# object is None when it is not set
|
||||||
|
caching_controller = cached_images.CacheController()
|
||||||
|
self.assertIsNone(caching_controller.cache)
|
||||||
|
|
||||||
|
|
||||||
class TestCachedImages(testtools.TestCase):
|
class TestCachedImages(test_utils.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCachedImages, self).setUp()
|
super(TestCachedImages, self).setUp()
|
||||||
test_controller = FakeController()
|
test_controller = FakeController()
|
||||||
self.controller = test_controller
|
self.controller = test_controller
|
||||||
|
|
||||||
def test_get_cached_images(self):
|
def test_get_cached_images(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req = webob.Request.blank('')
|
req = webob.Request.blank('')
|
||||||
req.context = 'test'
|
req.context = 'test'
|
||||||
result = self.controller.get_cached_images(req)
|
result = self.controller.get_cached_images(req)
|
||||||
self.assertEqual({'cached_images': {'id': 'test'}}, result)
|
self.assertEqual({'cached_images': [{'image_id': 'test'}]}, result)
|
||||||
|
|
||||||
def test_delete_cached_image(self):
|
def test_delete_cached_image(self):
|
||||||
req = webob.Request.blank('')
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req.context = 'test'
|
req = unit_test_utils.get_fake_request()
|
||||||
self.controller.delete_cached_image(req, image_id='test')
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
self.assertEqual(['test'], self.controller.cache.deleted_images)
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.controller.delete_cached_image(req, image_id=UUID4)
|
||||||
|
self.assertEqual([UUID4], self.controller.cache.deleted_images)
|
||||||
|
|
||||||
def test_delete_cached_images(self):
|
def test_delete_cached_images(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req = webob.Request.blank('')
|
req = webob.Request.blank('')
|
||||||
req.context = 'test'
|
req.context = 'test'
|
||||||
self.assertEqual({'num_deleted': 1},
|
self.assertEqual({'num_deleted': 1},
|
||||||
self.controller.delete_cached_images(req))
|
self.controller.delete_cached_images(req))
|
||||||
self.assertEqual(['test'], self.controller.cache.deleted_images)
|
self.assertEqual(['test'], self.controller.cache.deleted_images)
|
||||||
|
|
||||||
def test_policy_enforce_forbidden(self):
|
|
||||||
def fake_enforce(context, action, target):
|
|
||||||
raise exception.Forbidden()
|
|
||||||
|
|
||||||
self.controller.policy.enforce = fake_enforce
|
|
||||||
req = webob.Request.blank('')
|
|
||||||
req.context = 'test'
|
|
||||||
self.assertRaises(webob.exc.HTTPForbidden,
|
|
||||||
self.controller.get_cached_images, req)
|
|
||||||
|
|
||||||
def test_get_queued_images(self):
|
def test_get_queued_images(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req = webob.Request.blank('')
|
req = webob.Request.blank('')
|
||||||
req.context = 'test'
|
req.context = 'test'
|
||||||
result = self.controller.get_queued_images(req)
|
result = self.controller.get_queued_images(req)
|
||||||
self.assertEqual({'queued_images': {'test': 'passed'}}, result)
|
self.assertEqual({'queued_images': {'test': 'passed'}}, result)
|
||||||
|
|
||||||
def test_queue_image(self):
|
def test_queue_image(self):
|
||||||
req = webob.Request.blank('')
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req.context = 'test'
|
req = unit_test_utils.get_fake_request()
|
||||||
self.controller.queue_image(req, image_id='test1')
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.controller.queue_image(req, image_id=UUID4)
|
||||||
|
|
||||||
def test_delete_queued_image(self):
|
def test_delete_queued_image(self):
|
||||||
req = webob.Request.blank('')
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req.context = 'test'
|
req = unit_test_utils.get_fake_request()
|
||||||
self.controller.delete_queued_image(req, 'deleted_img')
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
self.assertEqual(['deleted_img'],
|
'get') as mock_get:
|
||||||
self.controller.cache.deleted_images)
|
mock_get.return_value = FakeImage()
|
||||||
|
self.controller.delete_queued_image(req, UUID4)
|
||||||
|
self.assertEqual([UUID4],
|
||||||
|
self.controller.cache.deleted_images)
|
||||||
|
|
||||||
def test_delete_queued_images(self):
|
def test_delete_queued_images(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
req = webob.Request.blank('')
|
req = webob.Request.blank('')
|
||||||
req.context = 'test'
|
req.context = 'test'
|
||||||
self.assertEqual({'num_deleted': 1},
|
self.assertEqual({'num_deleted': 1},
|
||||||
self.controller.delete_queued_images(req))
|
self.controller.delete_queued_images(req))
|
||||||
self.assertEqual(['deleted_img'],
|
self.assertEqual(['deleted_img'],
|
||||||
self.controller.cache.deleted_images)
|
self.controller.cache.deleted_images)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCachedImagesNegative(test_utils.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCachedImagesNegative, self).setUp()
|
||||||
|
test_controller = FakeController()
|
||||||
|
self.controller = test_controller
|
||||||
|
|
||||||
|
def test_get_cached_images_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.get_cached_images, req)
|
||||||
|
|
||||||
|
def test_get_cached_images_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.get_cached_images,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_delete_cached_image_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_cached_image, req,
|
||||||
|
image_id='test')
|
||||||
|
|
||||||
|
def test_delete_cached_image_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete_cached_image,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_delete_cached_images_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_cached_images, req)
|
||||||
|
|
||||||
|
def test_delete_cached_images_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete_cached_images,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_get_queued_images_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.get_queued_images, req)
|
||||||
|
|
||||||
|
def test_get_queued_images_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.get_queued_images,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_queue_image_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.queue_image,
|
||||||
|
req, image_id='test1')
|
||||||
|
|
||||||
|
def test_queue_image_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.queue_image,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_delete_queued_image_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_queued_image,
|
||||||
|
req, image_id='test1')
|
||||||
|
|
||||||
|
def test_delete_queued_image_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete_queued_image,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_delete_queued_images_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_queued_images, req)
|
||||||
|
|
||||||
|
def test_delete_queued_images_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"manage_image_cache": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete_queued_images,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_delete_cache_entry_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"cache_delete": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.delete_cache_entry,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_delete_cache_entry_disabled(self):
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_cache_entry,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_delete_non_existing_cache_entries(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.delete_cache_entry,
|
||||||
|
req, image_id='non-existing-queued-image')
|
||||||
|
|
||||||
|
def test_clear_cache_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"cache_delete": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.clear_cache,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_clear_cache_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.clear_cache, req)
|
||||||
|
|
||||||
|
def test_cache_clear_invalid_target(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
req.headers.update({'x-image-cache-clear-target': 'invalid'})
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller.clear_cache,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_get_cache_state_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.get_cache_state, req)
|
||||||
|
|
||||||
|
def test_get_cache_state_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"cache_list": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.get_cache_state,
|
||||||
|
req)
|
||||||
|
|
||||||
|
def test_queue_image_from_api_disabled(self):
|
||||||
|
req = webob.Request.blank('')
|
||||||
|
req.context = 'test'
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.queue_image_from_api,
|
||||||
|
req, image_id='test1')
|
||||||
|
|
||||||
|
def test_queue_image_from_api_forbidden(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
self.controller.policy.rules = {"cache_image": False}
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage()
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.queue_image_from_api,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_non_active_image_for_queue_api(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
for status in ('saving', 'queued', 'pending_delete',
|
||||||
|
'deactivated', 'importing', 'uploading'):
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
mock_get.return_value = FakeImage(status=status)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller.queue_image_from_api,
|
||||||
|
req, image_id=UUID4)
|
||||||
|
|
||||||
|
def test_queue_api_non_existing_image_(self):
|
||||||
|
self.config(image_cache_dir='fake_cache_directory')
|
||||||
|
req = unit_test_utils.get_fake_request()
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller.queue_image_from_api,
|
||||||
|
req, image_id='non-existing-image-id')
|
||||||
|
@ -28,7 +28,8 @@ from glance.tests.unit import base
|
|||||||
|
|
||||||
# make this public so it doesn't need to be repeated for the
|
# make this public so it doesn't need to be repeated for the
|
||||||
# functional tests
|
# functional tests
|
||||||
def get_versions_list(url, enabled_backends=False):
|
def get_versions_list(url, enabled_backends=False,
|
||||||
|
enabled_cache=False):
|
||||||
image_versions = [
|
image_versions = [
|
||||||
{
|
{
|
||||||
'id': 'v2.13',
|
'id': 'v2.13',
|
||||||
@ -36,6 +37,12 @@ def get_versions_list(url, enabled_backends=False):
|
|||||||
'links': [{'rel': 'self',
|
'links': [{'rel': 'self',
|
||||||
'href': '%s/v2/' % url}],
|
'href': '%s/v2/' % url}],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'id': 'v2.9',
|
||||||
|
'status': 'SUPPORTED',
|
||||||
|
'links': [{'rel': 'self',
|
||||||
|
'href': '%s/v2/' % url}],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'id': 'v2.7',
|
'id': 'v2.7',
|
||||||
'status': 'SUPPORTED',
|
'status': 'SUPPORTED',
|
||||||
@ -87,6 +94,12 @@ def get_versions_list(url, enabled_backends=False):
|
|||||||
]
|
]
|
||||||
if enabled_backends:
|
if enabled_backends:
|
||||||
image_versions = [
|
image_versions = [
|
||||||
|
{
|
||||||
|
'id': 'v2.13',
|
||||||
|
'status': 'CURRENT',
|
||||||
|
'links': [{'rel': 'self',
|
||||||
|
'href': '%s/v2/' % url}],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'id': 'v2.12',
|
'id': 'v2.12',
|
||||||
'status': 'SUPPORTED',
|
'status': 'SUPPORTED',
|
||||||
@ -117,14 +130,16 @@ def get_versions_list(url, enabled_backends=False):
|
|||||||
'links': [{'rel': 'self',
|
'links': [{'rel': 'self',
|
||||||
'href': '%s/v2/' % url}],
|
'href': '%s/v2/' % url}],
|
||||||
}
|
}
|
||||||
] + image_versions
|
] + image_versions[2:]
|
||||||
else:
|
|
||||||
|
if enabled_cache:
|
||||||
image_versions.insert(0, {
|
image_versions.insert(0, {
|
||||||
'id': 'v2.9',
|
'id': 'v2.14',
|
||||||
'status': 'SUPPORTED',
|
'status': 'CURRENT',
|
||||||
'links': [{'rel': 'self',
|
'links': [{'rel': 'self',
|
||||||
'href': '%s/v2/' % url}],
|
'href': '%s/v2/' % url}],
|
||||||
})
|
})
|
||||||
|
image_versions[1]['status'] = 'SUPPORTED'
|
||||||
|
|
||||||
return image_versions
|
return image_versions
|
||||||
|
|
||||||
@ -151,6 +166,14 @@ class VersionsTest(base.IsolatedUnitTest):
|
|||||||
enabled_backends=True)
|
enabled_backends=True)
|
||||||
self.assertEqual(expected, results)
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
|
res = versions.Controller().index(req)
|
||||||
|
results = jsonutils.loads(res.body)['versions']
|
||||||
|
expected = get_versions_list('http://127.0.0.1:9292',
|
||||||
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)
|
||||||
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
def test_get_version_list_public_endpoint(self):
|
def test_get_version_list_public_endpoint(self):
|
||||||
req = webob.Request.blank('/', base_url='http://127.0.0.1:9292/')
|
req = webob.Request.blank('/', base_url='http://127.0.0.1:9292/')
|
||||||
req.accept = 'application/json'
|
req.accept = 'application/json'
|
||||||
@ -170,6 +193,14 @@ class VersionsTest(base.IsolatedUnitTest):
|
|||||||
enabled_backends=True)
|
enabled_backends=True)
|
||||||
self.assertEqual(expected, results)
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
|
res = versions.Controller().index(req)
|
||||||
|
results = jsonutils.loads(res.body)['versions']
|
||||||
|
expected = get_versions_list('https://example.com:9292',
|
||||||
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)
|
||||||
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
def test_get_version_list_secure_proxy_ssl_header(self):
|
def test_get_version_list_secure_proxy_ssl_header(self):
|
||||||
self.config(secure_proxy_ssl_header='HTTP_X_FORWARDED_PROTO')
|
self.config(secure_proxy_ssl_header='HTTP_X_FORWARDED_PROTO')
|
||||||
url = 'http://localhost:9292'
|
url = 'http://localhost:9292'
|
||||||
@ -188,6 +219,14 @@ class VersionsTest(base.IsolatedUnitTest):
|
|||||||
expected = get_versions_list(url, enabled_backends=True)
|
expected = get_versions_list(url, enabled_backends=True)
|
||||||
self.assertEqual(expected, results)
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
|
res = versions.Controller().index(req)
|
||||||
|
results = jsonutils.loads(res.body)['versions']
|
||||||
|
expected = get_versions_list(url,
|
||||||
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)
|
||||||
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
def test_get_version_list_secure_proxy_ssl_header_https(self):
|
def test_get_version_list_secure_proxy_ssl_header_https(self):
|
||||||
self.config(secure_proxy_ssl_header='HTTP_X_FORWARDED_PROTO')
|
self.config(secure_proxy_ssl_header='HTTP_X_FORWARDED_PROTO')
|
||||||
url = 'http://localhost:9292'
|
url = 'http://localhost:9292'
|
||||||
@ -208,6 +247,14 @@ class VersionsTest(base.IsolatedUnitTest):
|
|||||||
expected = get_versions_list(ssl_url, enabled_backends=True)
|
expected = get_versions_list(ssl_url, enabled_backends=True)
|
||||||
self.assertEqual(expected, results)
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
|
res = versions.Controller().index(req)
|
||||||
|
results = jsonutils.loads(res.body)['versions']
|
||||||
|
expected = get_versions_list(ssl_url,
|
||||||
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)
|
||||||
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
def test_get_version_list_for_external_app(self):
|
def test_get_version_list_for_external_app(self):
|
||||||
url = 'http://customhost:9292/app/api'
|
url = 'http://customhost:9292/app/api'
|
||||||
req = webob.Request.blank('/', base_url=url)
|
req = webob.Request.blank('/', base_url=url)
|
||||||
@ -225,6 +272,13 @@ class VersionsTest(base.IsolatedUnitTest):
|
|||||||
expected = get_versions_list(url, enabled_backends=True)
|
expected = get_versions_list(url, enabled_backends=True)
|
||||||
self.assertEqual(expected, results)
|
self.assertEqual(expected, results)
|
||||||
|
|
||||||
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
|
res = versions.Controller().index(req)
|
||||||
|
results = jsonutils.loads(res.body)['versions']
|
||||||
|
expected = get_versions_list(url,
|
||||||
|
enabled_backends=True,
|
||||||
|
enabled_cache=True)
|
||||||
|
|
||||||
|
|
||||||
class VersionNegotiationTest(base.IsolatedUnitTest):
|
class VersionNegotiationTest(base.IsolatedUnitTest):
|
||||||
|
|
||||||
@ -333,15 +387,21 @@ class VersionNegotiationTest(base.IsolatedUnitTest):
|
|||||||
self.middleware.process_request(request)
|
self.middleware.process_request(request)
|
||||||
self.assertEqual('/v2/images', request.path_info)
|
self.assertEqual('/v2/images', request.path_info)
|
||||||
|
|
||||||
# version 2.14 does not exist
|
def test_request_url_v2_14_enabled_supported(self):
|
||||||
def test_request_url_v2_14_default_unsupported(self):
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
request = webob.Request.blank('/v2.14/images')
|
request = webob.Request.blank('/v2.14/images')
|
||||||
|
self.middleware.process_request(request)
|
||||||
|
self.assertEqual('/v2/images', request.path_info)
|
||||||
|
|
||||||
|
# version 2.15 does not exist
|
||||||
|
def test_request_url_v2_15_default_unsupported(self):
|
||||||
|
request = webob.Request.blank('/v2.15/images')
|
||||||
resp = self.middleware.process_request(request)
|
resp = self.middleware.process_request(request)
|
||||||
self.assertIsInstance(resp, versions.Controller)
|
self.assertIsInstance(resp, versions.Controller)
|
||||||
|
|
||||||
def test_request_url_v2_14_enabled_unsupported(self):
|
def test_request_url_v2_15_enabled_unsupported(self):
|
||||||
self.config(enabled_backends='slow:one,fast:two')
|
self.config(image_cache_dir='/tmp/cache')
|
||||||
request = webob.Request.blank('/v2.14/images')
|
request = webob.Request.blank('/v2.15/images')
|
||||||
resp = self.middleware.process_request(request)
|
resp = self.middleware.process_request(request)
|
||||||
self.assertIsInstance(resp, versions.Controller)
|
self.assertIsInstance(resp, versions.Controller)
|
||||||
|
|
||||||
|
123
glance/tests/unit/v2/test_cache_management_api.py
Normal file
123
glance/tests/unit/v2/test_cache_management_api.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Copyright 2021 Red Hat Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from glance.api.v2 import cached_images
|
||||||
|
from glance import notifier
|
||||||
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
|
import glance.tests.utils as test_utils
|
||||||
|
|
||||||
|
|
||||||
|
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeImage(object):
|
||||||
|
def __init__(self, id=None, status='active', container_format='ami',
|
||||||
|
disk_format='ami', locations=None):
|
||||||
|
self.id = id or UUID1
|
||||||
|
self.status = status
|
||||||
|
self.container_format = container_format
|
||||||
|
self.disk_format = disk_format
|
||||||
|
self.locations = locations
|
||||||
|
self.owner = unit_test_utils.TENANT1
|
||||||
|
self.created_at = ''
|
||||||
|
self.updated_at = ''
|
||||||
|
self.min_disk = ''
|
||||||
|
self.min_ram = ''
|
||||||
|
self.protected = False
|
||||||
|
self.checksum = ''
|
||||||
|
self.os_hash_algo = ''
|
||||||
|
self.os_hash_value = ''
|
||||||
|
self.size = 0
|
||||||
|
self.virtual_size = 0
|
||||||
|
self.visibility = 'public'
|
||||||
|
self.os_hidden = False
|
||||||
|
self.name = 'foo'
|
||||||
|
self.tags = []
|
||||||
|
self.extra_properties = {}
|
||||||
|
self.member = self.owner
|
||||||
|
|
||||||
|
# NOTE(danms): This fixture looks more like the db object than
|
||||||
|
# the proxy model. This needs fixing all through the tests
|
||||||
|
# below.
|
||||||
|
self.image_id = self.id
|
||||||
|
|
||||||
|
|
||||||
|
class TestCacheManageAPI(test_utils.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCacheManageAPI, self).setUp()
|
||||||
|
self.req = unit_test_utils.get_fake_request()
|
||||||
|
|
||||||
|
def _main_test_helper(self, argv, status='active', image_mock=True):
|
||||||
|
with mock.patch.object(notifier.ImageRepoProxy,
|
||||||
|
'get') as mock_get:
|
||||||
|
image = FakeImage(status=status)
|
||||||
|
mock_get.return_value = image
|
||||||
|
with mock.patch.object(cached_images.CacheController,
|
||||||
|
'_enforce') as e:
|
||||||
|
with mock.patch('glance.image_cache.ImageCache') as ic:
|
||||||
|
cc = cached_images.CacheController()
|
||||||
|
cc.cache = ic
|
||||||
|
c_calls = []
|
||||||
|
c_calls += argv[0].split(',')
|
||||||
|
for call in c_calls:
|
||||||
|
mock.patch.object(ic, call)
|
||||||
|
test_call = getattr(cc, argv[1])
|
||||||
|
new_policy = argv[2]
|
||||||
|
args = []
|
||||||
|
if len(argv) == 4:
|
||||||
|
args = argv[3:]
|
||||||
|
test_call(self.req, *args)
|
||||||
|
if image_mock:
|
||||||
|
e.assert_called_once_with(self.req, image=image,
|
||||||
|
new_policy=new_policy)
|
||||||
|
else:
|
||||||
|
e.assert_called_once_with(self.req,
|
||||||
|
new_policy=new_policy)
|
||||||
|
mcs = []
|
||||||
|
for method in ic.method_calls:
|
||||||
|
mcs.append(str(method))
|
||||||
|
for call in c_calls:
|
||||||
|
if args == []:
|
||||||
|
args.append("")
|
||||||
|
elif args[0] and not args[0].endswith("'"):
|
||||||
|
args[0] = "'" + args[0] + "'"
|
||||||
|
self.assertIn("call." + call + "(" + args[0] + ")",
|
||||||
|
mcs)
|
||||||
|
self.assertEqual(len(c_calls), len(mcs))
|
||||||
|
|
||||||
|
def test_delete_cache_entry(self):
|
||||||
|
self._main_test_helper(['delete_cached_image,delete_queued_image',
|
||||||
|
'delete_cache_entry',
|
||||||
|
'cache_delete',
|
||||||
|
UUID1])
|
||||||
|
|
||||||
|
def test_clear_cache(self):
|
||||||
|
self._main_test_helper(
|
||||||
|
['delete_all_cached_images,delete_all_queued_images',
|
||||||
|
'clear_cache',
|
||||||
|
'cache_delete'], image_mock=False)
|
||||||
|
|
||||||
|
def test_get_cache_state(self):
|
||||||
|
self._main_test_helper(['get_cached_images,get_queued_images',
|
||||||
|
'get_cache_state',
|
||||||
|
'cache_list'], image_mock=False)
|
||||||
|
|
||||||
|
def test_queue_image_from_api(self):
|
||||||
|
self._main_test_helper(['queue_image',
|
||||||
|
'queue_image_from_api',
|
||||||
|
'cache_image',
|
||||||
|
UUID1])
|
@ -780,16 +780,44 @@ class TestTasksAPIPolicy(APIPolicyBase):
|
|||||||
mock.ANY)
|
mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
class TestCacheImageAPIPolicy(APIPolicyBase):
|
class TestCacheImageAPIPolicy(utils.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCacheImageAPIPolicy, self).setUp()
|
super(TestCacheImageAPIPolicy, self).setUp()
|
||||||
self.enforcer = mock.MagicMock()
|
self.enforcer = mock.MagicMock()
|
||||||
self.context = mock.MagicMock()
|
self.context = mock.MagicMock()
|
||||||
self.policy = policy.CacheImageAPIPolicy(
|
|
||||||
self.context, enforcer=self.enforcer)
|
|
||||||
|
|
||||||
def test_manage_image_cache(self):
|
def test_manage_image_cache(self):
|
||||||
|
self.policy = policy.CacheImageAPIPolicy(
|
||||||
|
self.context, enforcer=self.enforcer,
|
||||||
|
policy_str='manage_image_cache')
|
||||||
self.policy.manage_image_cache()
|
self.policy.manage_image_cache()
|
||||||
self.enforcer.enforce.assert_called_once_with(self.context,
|
self.enforcer.enforce.assert_called_once_with(self.context,
|
||||||
'manage_image_cache',
|
'manage_image_cache',
|
||||||
mock.ANY)
|
mock.ANY)
|
||||||
|
|
||||||
|
def test_manage_image_cache_with_cache_delete(self):
|
||||||
|
self.policy = policy.CacheImageAPIPolicy(
|
||||||
|
self.context, enforcer=self.enforcer,
|
||||||
|
policy_str='cache_delete')
|
||||||
|
self.policy.manage_image_cache()
|
||||||
|
self.enforcer.enforce.assert_called_once_with(self.context,
|
||||||
|
'cache_delete',
|
||||||
|
mock.ANY)
|
||||||
|
|
||||||
|
def test_manage_image_cache_with_cache_list(self):
|
||||||
|
self.policy = policy.CacheImageAPIPolicy(
|
||||||
|
self.context, enforcer=self.enforcer,
|
||||||
|
policy_str='cache_list')
|
||||||
|
self.policy.manage_image_cache()
|
||||||
|
self.enforcer.enforce.assert_called_once_with(self.context,
|
||||||
|
'cache_list',
|
||||||
|
mock.ANY)
|
||||||
|
|
||||||
|
def test_manage_image_cache_with_cache_image(self):
|
||||||
|
self.policy = policy.CacheImageAPIPolicy(
|
||||||
|
self.context, enforcer=self.enforcer,
|
||||||
|
policy_str='cache_image')
|
||||||
|
self.policy.manage_image_cache()
|
||||||
|
self.enforcer.enforce.assert_called_once_with(self.context,
|
||||||
|
'cache_image',
|
||||||
|
mock.ANY)
|
||||||
|
9
releasenotes/notes/cache-api-b806ccfb8c5d9bb6.yaml
Normal file
9
releasenotes/notes/cache-api-b806ccfb8c5d9bb6.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
This release introduces new APIs for cache related operations. This new
|
||||||
|
version of the cache API will help administrators to cache images on
|
||||||
|
dedicated glance nodes as well. For more information, see the
|
||||||
|
``Cache Manage`` section in the `api-ref-guide
|
||||||
|
<https://developer.openstack.org/api-ref/image/v2/index.html#cache-manage>`_.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user