diff --git a/glance/api/middleware/cache_manage.py b/glance/api/middleware/cache_manage.py index 77c6954193..44af6742e1 100644 --- a/glance/api/middleware/cache_manage.py +++ b/glance/api/middleware/cache_manage.py @@ -20,7 +20,7 @@ Image Cache Management API from oslo_log import log as logging import routes -from glance.api import cached_images +from glance.api.v2 import cached_images from glance.common import wsgi from glance.i18n import _LI @@ -32,37 +32,37 @@ class CacheManageFilter(wsgi.Middleware): mapper = routes.Mapper() resource = cached_images.create_resource() - mapper.connect("/v1/cached_images", + mapper.connect("/v2/cached_images", controller=resource, action="get_cached_images", conditions=dict(method=["GET"])) - mapper.connect("/v1/cached_images/{image_id}", + mapper.connect("/v2/cached_images/{image_id}", controller=resource, action="delete_cached_image", conditions=dict(method=["DELETE"])) - mapper.connect("/v1/cached_images", + mapper.connect("/v2/cached_images", controller=resource, action="delete_cached_images", conditions=dict(method=["DELETE"])) - mapper.connect("/v1/queued_images/{image_id}", + mapper.connect("/v2/queued_images/{image_id}", controller=resource, action="queue_image", conditions=dict(method=["PUT"])) - mapper.connect("/v1/queued_images", + mapper.connect("/v2/queued_images", controller=resource, action="get_queued_images", conditions=dict(method=["GET"])) - mapper.connect("/v1/queued_images/{image_id}", + mapper.connect("/v2/queued_images/{image_id}", controller=resource, action="delete_queued_image", conditions=dict(method=["DELETE"])) - mapper.connect("/v1/queued_images", + mapper.connect("/v2/queued_images", controller=resource, action="delete_queued_images", conditions=dict(method=["DELETE"])) diff --git a/glance/api/v2/cached_images.py b/glance/api/v2/cached_images.py new file mode 100644 index 0000000000..72f405b3f0 --- /dev/null +++ b/glance/api/v2/cached_images.py @@ -0,0 +1,128 @@ +# Copyright 2018 RedHat 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. + +""" +Controller for Image Cache Management API +""" + +from oslo_log import log as logging +import webob.exc + +from glance.api import policy +from glance.common import exception +from glance.common import wsgi +from glance import image_cache + +LOG = logging.getLogger(__name__) + + +class CacheController(object): + """ + A controller for managing cached images. + """ + + def __init__(self): + self.cache = image_cache.ImageCache() + self.policy = policy.Enforcer() + + def _enforce(self, req): + """Authorize request against 'manage_image_cache' policy""" + try: + self.policy.enforce(req.context, 'manage_image_cache', {}) + except exception.Forbidden: + LOG.debug("User not permitted to manage the image cache") + raise webob.exc.HTTPForbidden() + + def get_cached_images(self, req): + """ + GET /cached_images + + Returns a mapping of records about cached images. + """ + self._enforce(req) + images = self.cache.get_cached_images() + return dict(cached_images=images) + + def delete_cached_image(self, req, image_id): + """ + DELETE /cached_images/ + + Removes an image from the cache. + """ + self._enforce(req) + self.cache.delete_cached_image(image_id) + + def delete_cached_images(self, req): + """ + DELETE /cached_images - Clear all active cached images + + Removes all images from the cache. + """ + self._enforce(req) + return dict(num_deleted=self.cache.delete_all_cached_images()) + + def get_queued_images(self, req): + """ + GET /queued_images + + Returns a mapping of records about queued images. + """ + self._enforce(req) + images = self.cache.get_queued_images() + return dict(queued_images=images) + + def queue_image(self, req, image_id): + """ + PUT /queued_images/ + + 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... + """ + self._enforce(req) + self.cache.queue_image(image_id) + + def delete_queued_image(self, req, image_id): + """ + DELETE /queued_images/ + + Removes an image from the cache. + """ + self._enforce(req) + self.cache.delete_queued_image(image_id) + + def delete_queued_images(self, req): + """ + DELETE /queued_images - Clear all active queued images + + Removes all images from the cache. + """ + self._enforce(req) + return dict(num_deleted=self.cache.delete_all_queued_images()) + + +class CachedImageDeserializer(wsgi.JSONRequestDeserializer): + pass + + +class CachedImageSerializer(wsgi.JSONResponseSerializer): + pass + + +def create_resource(): + """Cached Images resource factory method""" + deserializer = CachedImageDeserializer() + serializer = CachedImageSerializer() + return wsgi.Resource(CacheController(), deserializer, serializer) diff --git a/glance/cmd/cache_manage.py b/glance/cmd/cache_manage.py new file mode 100644 index 0000000000..26021a4815 --- /dev/null +++ b/glance/cmd/cache_manage.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python + +# Copyright 2018 RedHat 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. + +""" +A simple cache management utility for Glance. +""" +from __future__ import print_function + +import argparse +import collections +import datetime +import functools +import os +import sys +import time +import uuid + +from oslo_utils import encodeutils +import prettytable + +from six.moves import input + +# If ../glance/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')): + sys.path.insert(0, possible_topdir) + +from glance.common import exception +import glance.image_cache.client +from glance.version import version_info as version + + +SUCCESS = 0 +FAILURE = 1 + + +def validate_input(func): + """Decorator to enforce validation on input""" + @functools.wraps(func) + def wrapped(*args, **kwargs): + if len(args[0].command) > 2: + print("Please specify the ID of the image you wish for command " + "'%s' from the cache as the first and only " + "argument." % args[0].command[0]) + return FAILURE + if len(args[0].command) == 2: + image_id = args[0].command[1] + try: + image_id = uuid.UUID(image_id) + except ValueError: + print("Image ID '%s' is not a valid UUID." % image_id) + return FAILURE + + return func(args[0], **kwargs) + return wrapped + + +def catch_error(action): + """Decorator to provide sensible default error handling for actions.""" + def wrap(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + ret = func(*args, **kwargs) + return SUCCESS if ret is None else ret + except exception.NotFound: + options = args[0] + print("Cache management middleware not enabled on host %s" % + options.host) + return FAILURE + except exception.Forbidden: + print("Not authorized to make this request.") + return FAILURE + except Exception as e: + options = args[0] + if options.debug: + raise + print("Failed to %s. Got error:" % action) + pieces = encodeutils.exception_to_unicode(e).split('\n') + for piece in pieces: + print(piece) + return FAILURE + + return wrapper + return wrap + + +@catch_error('show cached images') +def list_cached(args): + """%(prog)s list-cached [options] + + List all images currently cached. + """ + client = get_client(args) + images = client.get_cached_images() + if not images: + print("No cached images.") + return SUCCESS + + print("Found %d cached images..." % len(images)) + + pretty_table = prettytable.PrettyTable(("ID", + "Last Accessed (UTC)", + "Last Modified (UTC)", + "Size", + "Hits")) + pretty_table.align['Size'] = "r" + pretty_table.align['Hits'] = "r" + + for image in images: + last_accessed = image['last_accessed'] + if last_accessed == 0: + last_accessed = "N/A" + else: + last_accessed = datetime.datetime.utcfromtimestamp( + last_accessed).isoformat() + + pretty_table.add_row(( + image['image_id'], + last_accessed, + datetime.datetime.utcfromtimestamp( + image['last_modified']).isoformat(), + image['size'], + image['hits'])) + + print(pretty_table.get_string()) + return SUCCESS + + +@catch_error('show queued images') +def list_queued(args): + """%(prog)s list-queued [options] + + List all images currently queued for caching. + """ + client = get_client(args) + images = client.get_queued_images() + if not images: + print("No queued images.") + return SUCCESS + + print("Found %d queued images..." % len(images)) + + pretty_table = prettytable.PrettyTable(("ID",)) + + for image in images: + pretty_table.add_row((image,)) + + print(pretty_table.get_string()) + + +@catch_error('queue the specified image for caching') +@validate_input +def queue_image(args): + """%(prog)s queue-image [options] + + Queues an image for caching. + """ + image_id = args.command[1] + if (not args.force and + not user_confirm("Queue image %(image_id)s for caching?" % + {'image_id': image_id}, default=False)): + return SUCCESS + + client = get_client(args) + client.queue_image_for_caching(image_id) + + if args.verbose: + print("Queued image %(image_id)s for caching" % + {'image_id': image_id}) + + return SUCCESS + + +@catch_error('delete the specified cached image') +@validate_input +def delete_cached_image(args): + """%(prog)s delete-cached-image [options] + + Deletes an image from the cache. + """ + image_id = args.command[1] + if (not args.force and + not user_confirm("Delete cached image %(image_id)s?" % + {'image_id': image_id}, default=False)): + return SUCCESS + + client = get_client(args) + client.delete_cached_image(image_id) + + if args.verbose: + print("Deleted cached image %(image_id)s" % {'image_id': image_id}) + + return SUCCESS + + +@catch_error('Delete all cached images') +def delete_all_cached_images(args): + """%(prog)s delete-all-cached-images [options] + + Remove all images from the cache. + """ + if (not args.force and + not user_confirm("Delete all cached images?", default=False)): + return SUCCESS + + client = get_client(args) + num_deleted = client.delete_all_cached_images() + + if args.verbose: + print("Deleted %(num_deleted)s cached images" % + {'num_deleted': num_deleted}) + + return SUCCESS + + +@catch_error('delete the specified queued image') +@validate_input +def delete_queued_image(args): + """%(prog)s delete-queued-image [options] + + Deletes an image from the cache. + """ + image_id = args.command[1] + if (not args.force and + not user_confirm("Delete queued image %(image_id)s?" % + {'image_id': image_id}, default=False)): + return SUCCESS + + client = get_client(args) + client.delete_queued_image(image_id) + + if args.verbose: + print("Deleted queued image %(image_id)s" % {'image_id': image_id}) + + return SUCCESS + + +@catch_error('Delete all queued images') +def delete_all_queued_images(args): + """%(prog)s delete-all-queued-images [options] + + Remove all images from the cache queue. + """ + if (not args.force and + not user_confirm("Delete all queued images?", default=False)): + return SUCCESS + + client = get_client(args) + num_deleted = client.delete_all_queued_images() + + if args.verbose: + print("Deleted %(num_deleted)s queued images" % + {'num_deleted': num_deleted}) + + return SUCCESS + + +def get_client(options): + """Return a new client object to a Glance server. + + specified by the --host and --port options + supplied to the CLI + """ + # Generate auth_url based on identity_api_version + identity_version = env('OS_IDENTITY_API_VERSION', default='3') + auth_url = options.os_auth_url + if identity_version == '3' and "/v3" not in auth_url: + auth_url = auth_url + "/v3" + elif identity_version == '2' and "/v2" not in auth_url: + auth_url = auth_url + "/v2.0" + + user_domain_id = options.os_user_domain_id + if not user_domain_id: + user_domain_id = options.os_domain_id + project_domain_id = options.os_project_domain_id + if not user_domain_id: + project_domain_id = options.os_domain_id + + return glance.image_cache.client.get_client( + host=options.host, + port=options.port, + username=options.os_username, + password=options.os_password, + project=options.os_project_name, + user_domain_id=user_domain_id, + project_domain_id=project_domain_id, + auth_url=auth_url, + auth_strategy=options.os_auth_strategy, + auth_token=options.os_auth_token, + region=options.os_region_name, + insecure=options.insecure) + + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars. + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + """ + for v in vars: + value = os.environ.get(v) + if value: + return value + return kwargs.get('default', '') + + +def print_help(args): + """ + Print help specific to a command + """ + command = lookup_command(args.command[1]) + print(command.__doc__ % {'prog': os.path.basename(sys.argv[0])}) + + +def parse_args(parser): + """Set up the CLI and config-file options that may be + parsed and program commands. + + :param parser: The option parser + """ + parser.add_argument('command', default='help', nargs='+', + help='The command to execute') + parser.add_argument('-v', '--verbose', default=False, action="store_true", + help="Print more verbose output.") + parser.add_argument('-d', '--debug', default=False, action="store_true", + help="Print debugging output.") + parser.add_argument('-H', '--host', metavar="ADDRESS", default="0.0.0.0", + help="Address of Glance API host.") + parser.add_argument('-p', '--port', dest="port", metavar="PORT", + type=int, default=9292, + help="Port the Glance API host listens on.") + parser.add_argument('-k', '--insecure', dest="insecure", + default=False, action="store_true", + help='Explicitly allow glance to perform "insecure" ' + "SSL (https) requests. The server's certificate " + "will not be verified against any certificate " + "authorities. This option should be used with " + "caution.") + parser.add_argument('-f', '--force', dest="force", + default=False, action="store_true", + help="Prevent select actions from requesting " + "user confirmation.") + + parser.add_argument('--os-auth-token', + dest='os_auth_token', + default=env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN].') + parser.add_argument('-A', '--os_auth_token', '--auth_token', + dest='os_auth_token', + help=argparse.SUPPRESS) + + parser.add_argument('--os-username', + dest='os_username', + default=env('OS_USERNAME'), + help='Defaults to env[OS_USERNAME].') + parser.add_argument('-I', '--os_username', + dest='os_username', + help=argparse.SUPPRESS) + + parser.add_argument('--os-password', + dest='os_password', + default=env('OS_PASSWORD'), + help='Defaults to env[OS_PASSWORD].') + parser.add_argument('-K', '--os_password', + dest='os_password', + help=argparse.SUPPRESS) + + parser.add_argument('--os-region-name', + dest='os_region_name', + default=env('OS_REGION_NAME'), + help='Defaults to env[OS_REGION_NAME].') + parser.add_argument('-R', '--os_region_name', + dest='os_region_name', + help=argparse.SUPPRESS) + + parser.add_argument('--os-project-id', + dest='os_project_id', + default=env('OS_PROJECT_ID'), + help='Defaults to env[OS_PROJECT_ID].') + parser.add_argument('--os_project_id', + dest='os_project_id', + help=argparse.SUPPRESS) + + parser.add_argument('--os-project-name', + dest='os_project_name', + default=env('OS_PROJECT_NAME'), + help='Defaults to env[OS_PROJECT_NAME].') + parser.add_argument('-T', '--os_project_name', + dest='os_project_name', + help=argparse.SUPPRESS) + + # arguments related user, project domain + parser.add_argument('--os-user-domain-id', + dest='os_user_domain_id', + default=env('OS_USER_DOMAIN_ID'), + help='Defaults to env[OS_USER_DOMAIN_ID].') + parser.add_argument('--os-project-domain-id', + dest='os_project_domain_id', + default=env('OS_PROJECT_DOMAIN_ID'), + help='Defaults to env[OS_PROJECT_DOMAIN_ID].') + parser.add_argument('--os-domain-id', + dest='os_domain_id', + default=env('OS_DOMAIN_ID', default='default'), + help='Defaults to env[OS_DOMAIN_ID].') + + parser.add_argument('--os-auth-url', + default=env('OS_AUTH_URL'), + help='Defaults to env[OS_AUTH_URL].') + parser.add_argument('-N', '--os_auth_url', + dest='os_auth_url', + help=argparse.SUPPRESS) + + parser.add_argument('-S', '--os_auth_strategy', dest="os_auth_strategy", + metavar="STRATEGY", + help="Authentication strategy (keystone or noauth).") + + version_string = version.cached_version_string() + parser.add_argument('--version', action='version', + version=version_string) + + return parser.parse_args() + + +CACHE_COMMANDS = collections.OrderedDict() +CACHE_COMMANDS['help'] = ( + print_help, 'Output help for one of the commands below') +CACHE_COMMANDS['list-cached'] = ( + list_cached, 'List all images currently cached') +CACHE_COMMANDS['list-queued'] = ( + list_queued, 'List all images currently queued for caching') +CACHE_COMMANDS['queue-image'] = ( + queue_image, 'Queue an image for caching') +CACHE_COMMANDS['delete-cached-image'] = ( + delete_cached_image, 'Purges an image from the cache') +CACHE_COMMANDS['delete-all-cached-images'] = ( + delete_all_cached_images, 'Removes all images from the cache') +CACHE_COMMANDS['delete-queued-image'] = ( + delete_queued_image, 'Deletes an image from the cache queue') +CACHE_COMMANDS['delete-all-queued-images'] = ( + delete_all_queued_images, 'Deletes all images from the cache queue') + + +def _format_command_help(): + """Formats the help string for subcommands.""" + help_msg = "Commands:\n\n" + + for command, info in CACHE_COMMANDS.items(): + if command == 'help': + command = 'help ' + help_msg += " %-28s%s\n\n" % (command, info[1]) + + return help_msg + + +def lookup_command(command_name): + try: + command = CACHE_COMMANDS[command_name] + return command[0] + except KeyError: + print('\nError: "%s" is not a valid command.\n' % command_name) + print(_format_command_help()) + sys.exit("Unknown command: %(cmd_name)s" % {'cmd_name': command_name}) + + +def user_confirm(prompt, default=False): + """Yes/No question dialog with user. + + :param prompt: question/statement to present to user (string) + :param default: boolean value to return if empty string + is received as response to prompt + + """ + if default: + prompt_default = "[Y/n]" + else: + prompt_default = "[y/N]" + + answer = input("%s %s " % (prompt, prompt_default)) + + if answer == "": + return default + else: + return answer.lower() in ("yes", "y") + + +def main(): + parser = argparse.ArgumentParser( + description=_format_command_help(), + formatter_class=argparse.RawDescriptionHelpFormatter) + args = parse_args(parser) + + if args.command[0] == 'help' and len(args.command) == 1: + parser.print_help() + return + + # Look up the command to run + command = lookup_command(args.command[0]) + + try: + start_time = time.time() + result = command(args) + end_time = time.time() + if args.verbose: + print("Completed in %-0.4f sec." % (end_time - start_time)) + sys.exit(result) + except (RuntimeError, NotImplementedError) as e: + sys.exit("ERROR: %s" % e) + +if __name__ == '__main__': + main() diff --git a/glance/common/auth.py b/glance/common/auth.py index 2f9734754f..ea76932b32 100644 --- a/glance/common/auth.py +++ b/glance/common/auth.py @@ -94,6 +94,11 @@ class KeystoneStrategy(BaseStrategy): if self.creds.get("tenant") is None: raise exception.MissingCredentialError(required='tenant') + # For v3 also check project is present + if self.creds['auth_url'].rstrip('/').endswith('v3'): + if self.creds.get("project") is None: + raise exception.MissingCredentialError(required='project') + def authenticate(self): """Authenticate with the Keystone service. @@ -113,10 +118,15 @@ class KeystoneStrategy(BaseStrategy): # If OS_AUTH_URL is missing a trailing slash add one if not auth_url.endswith('/'): auth_url += '/' + token_url = urlparse.urljoin(auth_url, "tokens") # 1. Check Keystone version is_v2 = auth_url.rstrip('/').endswith('v2.0') - if is_v2: + is_v3 = auth_url.rstrip('/').endswith('v3') + if is_v3: + token_url = urlparse.urljoin(auth_url, "auth/tokens") + self._v3_auth(token_url) + elif is_v2: self._v2_auth(token_url) else: self._v1_auth(token_url) @@ -186,6 +196,52 @@ class KeystoneStrategy(BaseStrategy): else: raise Exception(_('Unexpected response: %s') % resp.status) + def _v3_auth(self, token_url): + creds = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": self.creds['username'], + "domain": {"id": self.creds['user_domain_id']}, + "password": self.creds['password'] + } + } + }, + "scope": { + "project": { + "name": self.creds['project'], + "domain": { + "id": self.creds['project_domain_id'] + } + } + } + } + } + + headers = {'Content-Type': 'application/json'} + req_body = jsonutils.dumps(creds) + + resp, resp_body = self._do_request( + token_url, 'POST', headers=headers, body=req_body) + resp_body = jsonutils.loads(resp_body) + + if resp.status == 201: + resp_auth = resp['x-subject-token'] + creds_region = self.creds.get('region') + if self.configure_via_auth: + endpoint = get_endpoint(resp_body['token']['catalog'], + endpoint_region=creds_region) + self.management_url = endpoint + self.auth_token = resp_auth + elif resp.status == 305: + raise exception.RedirectException(resp['location']) + elif resp.status == 400: + raise exception.AuthBadRequest(url=token_url) + elif resp.status == 401: + raise Exception(_('Unexpected response: %s') % resp.status) + def _v2_auth(self, token_url): creds = self.creds diff --git a/glance/image_cache/client.py b/glance/image_cache/client.py new file mode 100644 index 0000000000..fdc8155274 --- /dev/null +++ b/glance/image_cache/client.py @@ -0,0 +1,136 @@ +# Copyright 2018 RedHat 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. + +import os + +from oslo_serialization import jsonutils as json + +from glance.common import client as base_client +from glance.common import exception +from glance.i18n import _ + + +class CacheClient(base_client.BaseClient): + + DEFAULT_PORT = 9292 + DEFAULT_DOC_ROOT = '/v2' + + def delete_cached_image(self, image_id): + """ + Delete a specified image from the cache + """ + self.do_request("DELETE", "/cached_images/%s" % image_id) + return True + + def get_cached_images(self, **kwargs): + """ + Returns a list of images stored in the image cache. + """ + res = self.do_request("GET", "/cached_images") + data = json.loads(res.read())['cached_images'] + return data + + def get_queued_images(self, **kwargs): + """ + Returns a list of images queued for caching + """ + res = self.do_request("GET", "/queued_images") + data = json.loads(res.read())['queued_images'] + return data + + def delete_all_cached_images(self): + """ + Delete all cached images + """ + res = self.do_request("DELETE", "/cached_images") + data = json.loads(res.read()) + num_deleted = data['num_deleted'] + return num_deleted + + def queue_image_for_caching(self, image_id): + """ + Queue an image for prefetching into cache + """ + self.do_request("PUT", "/queued_images/%s" % image_id) + return True + + def delete_queued_image(self, image_id): + """ + Delete a specified image from the cache queue + """ + self.do_request("DELETE", "/queued_images/%s" % image_id) + return True + + def delete_all_queued_images(self): + """ + Delete all queued images + """ + res = self.do_request("DELETE", "/queued_images") + data = json.loads(res.read()) + num_deleted = data['num_deleted'] + return num_deleted + + +def get_client(host, port=None, timeout=None, use_ssl=False, username=None, + password=None, project=None, + user_domain_id=None, project_domain_id=None, + auth_url=None, auth_strategy=None, + auth_token=None, region=None, insecure=False): + """ + Returns a new client Glance client object based on common kwargs. + If an option isn't specified falls back to common environment variable + defaults. + """ + + if auth_url or os.getenv('OS_AUTH_URL'): + force_strategy = 'keystone' + else: + force_strategy = None + + creds = { + 'username': username or + os.getenv('OS_AUTH_USER', os.getenv('OS_USERNAME')), + 'password': password or + os.getenv('OS_AUTH_KEY', os.getenv('OS_PASSWORD')), + 'project': project or + os.getenv('OS_AUTH_PROJECT', os.getenv('OS_PROJECT_NAME')), + 'auth_url': auth_url or + os.getenv('OS_AUTH_URL'), + 'strategy': force_strategy or + auth_strategy or + os.getenv('OS_AUTH_STRATEGY', 'noauth'), + 'region': region or + os.getenv('OS_REGION_NAME'), + 'user_domain_id': user_domain_id or os.getenv( + 'OS_USER_DOMAIN_ID', 'default'), + 'project_domain_id': project_domain_id or os.getenv( + 'OS_PROJECT_DOMAIN_ID', 'default') + } + + if creds['strategy'] == 'keystone' and not creds['auth_url']: + msg = _("--os_auth_url option or OS_AUTH_URL environment variable " + "required when keystone authentication strategy is enabled\n") + raise exception.ClientConfigurationError(msg) + + return CacheClient( + host=host, + port=port, + timeout=timeout, + use_ssl=use_ssl, + auth_token=auth_token or + os.getenv('OS_TOKEN'), + creds=creds, + insecure=insecure, + configure_via_auth=False) diff --git a/glance/tests/unit/api/middleware/test_cache_manage.py b/glance/tests/unit/api/middleware/test_cache_manage.py index d66aacb87f..04751f5786 100644 --- a/glance/tests/unit/api/middleware/test_cache_manage.py +++ b/glance/tests/unit/api/middleware/test_cache_manage.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from glance.api import cached_images from glance.api.middleware import cache_manage +from glance.api.v2 import cached_images import glance.common.config import glance.common.wsgi import glance.image_cache @@ -44,14 +44,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): # check self.assertIsNone(resource) - @mock.patch.object(cached_images.Controller, "get_cached_images") + @mock.patch.object(cached_images.CacheController, "get_cached_images") def test_get_cached_images(self, mock_get_cached_images): # setup mock_get_cached_images.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/cached_images") + request = webob.Request.blank("/v2/cached_images") # call resource = self.cache_manage_filter.process_request(request) @@ -61,14 +61,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "delete_cached_image") + @mock.patch.object(cached_images.CacheController, "delete_cached_image") def test_delete_cached_image(self, mock_delete_cached_image): # setup mock_delete_cached_image.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/cached_images/" + self.image_id, + request = webob.Request.blank("/v2/cached_images/" + self.image_id, environ={'REQUEST_METHOD': "DELETE"}) # call @@ -80,14 +80,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "delete_cached_images") + @mock.patch.object(cached_images.CacheController, "delete_cached_images") def test_delete_cached_images(self, mock_delete_cached_images): # setup mock_delete_cached_images.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/cached_images", + request = webob.Request.blank("/v2/cached_images", environ={'REQUEST_METHOD': "DELETE"}) # call @@ -98,14 +98,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "queue_image") + @mock.patch.object(cached_images.CacheController, "queue_image") def test_put_queued_image(self, mock_queue_image): # setup mock_queue_image.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/queued_images/" + self.image_id, + request = webob.Request.blank("/v2/queued_images/" + self.image_id, environ={'REQUEST_METHOD': "PUT"}) # call @@ -116,14 +116,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "get_queued_images") + @mock.patch.object(cached_images.CacheController, "get_queued_images") def test_get_queued_images(self, mock_get_queued_images): # setup mock_get_queued_images.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/queued_images") + request = webob.Request.blank("/v2/queued_images") # call resource = self.cache_manage_filter.process_request(request) @@ -133,14 +133,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "delete_queued_image") + @mock.patch.object(cached_images.CacheController, "delete_queued_image") def test_delete_queued_image(self, mock_delete_queued_image): # setup mock_delete_queued_image.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/queued_images/" + self.image_id, + request = webob.Request.blank("/v2/queued_images/" + self.image_id, environ={'REQUEST_METHOD': 'DELETE'}) # call @@ -152,14 +152,14 @@ class TestCacheManageFilter(test_utils.BaseTestCase): self.assertEqual('"' + self.stub_value + '"', resource.body.decode('utf-8')) - @mock.patch.object(cached_images.Controller, "delete_queued_images") + @mock.patch.object(cached_images.CacheController, "delete_queued_images") def test_delete_queued_images(self, mock_delete_queued_images): # setup mock_delete_queued_images.return_value = self.stub_value # prepare - request = webob.Request.blank("/v1/queued_images", + request = webob.Request.blank("/v2/queued_images", environ={'REQUEST_METHOD': 'DELETE'}) # call