Add cache-manage utility using v2 API
In Rocky, the v1 dependent glance-cache-manage command was removed while removing Images API v1 entry points. As a part of edge-computing image caching will play a big role. To provide support of image caching, added cache-manage utility using V2 API. This utility will have the same interface as Queens glance-cache-manage utility [0] as far as possible. [0] https://docs.openstack.org/glance/queens/cli/glancecachemanage.html In addition to above command line options added below new options for doamin information to use in v3 authentication: --os-domain-id --os-user-domain-id --os-project-domain-id Implements: blueprint cache-manage Change-Id: I127c84f8ea610cc20eb3ac2f4f6a9a47a7233f5f
This commit is contained in:
parent
88a8ad7823
commit
d03e80a735
@ -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"]))
|
||||
|
128
glance/api/v2/cached_images.py
Normal file
128
glance/api/v2/cached_images.py
Normal file
@ -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/<IMAGE_ID>
|
||||
|
||||
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/<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...
|
||||
"""
|
||||
self._enforce(req)
|
||||
self.cache.queue_image(image_id)
|
||||
|
||||
def delete_queued_image(self, req, image_id):
|
||||
"""
|
||||
DELETE /queued_images/<IMAGE_ID>
|
||||
|
||||
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)
|
528
glance/cmd/cache_manage.py
Normal file
528
glance/cmd/cache_manage.py
Normal file
@ -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 <IMAGE_ID> [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 <IMAGE_ID> [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 <IMAGE_ID> [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 <command>'
|
||||
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()
|
@ -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
|
||||
|
136
glance/image_cache/client.py
Normal file
136
glance/image_cache/client.py
Normal file
@ -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)
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user