#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack, LLC # 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. """ import functools import gettext import optparse import os import sys import time # 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) gettext.install('glance', unicode=1) from glance import client as glance_client from glance import version from glance.common import exception from glance.common import utils SUCCESS = 0 FAILURE = 1 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.NotAuthorized: print "Not authorized to make this request. Check "\ "your credentials (OS_AUTH_USER, OS_AUTH_KEY, ...)." return FAILURE except Exception, e: options = args[0] if options.debug: raise print "Failed to %s. Got error:" % action pieces = unicode(e).split('\n') for piece in pieces: print piece return FAILURE return wrapper return wrap @catch_error('show cached images') def list_cached(options, args): """ %(prog)s list-cached [options] List all images currently cached""" client = get_client(options) images = client.get_cached_images() if not images: print "No cached images." return SUCCESS print "Found %d cached images..." % len(images) pretty_table = utils.PrettyTable() pretty_table.add_column(36, label="ID") pretty_table.add_column(19, label="Last Accessed (UTC)") pretty_table.add_column(19, label="Last Modified (UTC)") # 1 TB takes 13 characters to display: len(str(2**40)) == 13 pretty_table.add_column(14, label="Size", just="r") pretty_table.add_column(10, label="Hits", just="r") print pretty_table.make_header() for image in images: print pretty_table.make_row( image['image_id'], image['last_accessed'], image['last_modified'], image['size'], image['hits']) @catch_error('show queued images') def list_queued(options, args): """ %(prog)s list-queued [options] List all images currently queued for caching""" client = get_client(options) images = client.get_queued_images() if not images: print "No queued images." return SUCCESS print "Found %d queued images..." % len(images) pretty_table = utils.PrettyTable() pretty_table.add_column(36, label="ID") print pretty_table.make_header() for image in images: print pretty_table.make_row(image) @catch_error('queue the specified image for caching') def queue_image(options, args): """ %(prog)s queue-image [options] Queues an image for caching""" try: image_id = args.pop() except IndexError: print "Please specify the ID of the image you wish to queue " print "from the cache as the first argument" return FAILURE if not options.force and \ not user_confirm("Queue image %s for caching?" % (image_id,), default=False): return SUCCESS client = get_client(options) client.queue_image_for_caching(image_id) if options.verbose: print "Queued image %(image_id)s for caching" % locals() return SUCCESS @catch_error('delete the specified cached image') def delete_cached_image(options, args): """ %(prog)s delete-cached-image [options] Deletes an image from the cache""" try: image_id = args.pop() except IndexError: print "Please specify the ID of the image you wish to delete " print "from the cache as the first argument" return FAILURE if not options.force and \ not user_confirm("Delete cached image %s?" % (image_id,), default=False): return SUCCESS client = get_client(options) client.delete_cached_image(image_id) if options.verbose: print "Deleted cached image %(image_id)s" % locals() return SUCCESS @catch_error('Delete all cached images') def delete_all_cached_images(options, args): """ %(prog)s delete-all-cached-images [options] Removes all images from the cache""" if not options.force and \ not user_confirm("Delete all cached images?", default=False): return SUCCESS client = get_client(options) num_deleted = client.delete_all_cached_images() if options.verbose: print "Deleted %(num_deleted)s cached images" % locals() return SUCCESS @catch_error('delete the specified queued image') def delete_queued_image(options, args): """ %(prog)s delete-queued-image [options] Deletes an image from the cache""" try: image_id = args.pop() except IndexError: print "Please specify the ID of the image you wish to delete " print "from the cache as the first argument" return FAILURE if not options.force and \ not user_confirm("Delete queued image %s?" % (image_id,), default=False): return SUCCESS client = get_client(options) client.delete_queued_image(image_id) if options.verbose: print "Deleted queued image %(image_id)s" % locals() return SUCCESS @catch_error('Delete all queued images') def delete_all_queued_images(options, args): """ %(prog)s delete-all-queued-images [options] Removes all images from the cache queue""" if not options.force and \ not user_confirm("Delete all queued images?", default=False): return SUCCESS client = get_client(options) num_deleted = client.delete_all_queued_images() if options.verbose: print "Deleted %(num_deleted)s queued images" % locals() return SUCCESS def get_client(options): """ Returns a new client object to a Glance server specified by the --host and --port options supplied to the CLI """ creds = dict(username=os.getenv('OS_AUTH_USER'), password=os.getenv('OS_AUTH_KEY'), tenant=os.getenv('OS_AUTH_TENANT'), auth_url=os.getenv('OS_AUTH_URL'), strategy=os.getenv('OS_AUTH_STRATEGY', 'noauth')) use_ssl = (options.host.find('https') != -1 or ( creds['auth_url'] is not None and creds['auth_url'].find('https') != -1)) return glance_client.Client(host=options.host, port=options.port, use_ssl=use_ssl, auth_tok=options.auth_token, creds=creds) def create_options(parser): """ Sets up the CLI and config-file options that may be parsed and program commands. :param parser: The option parser """ parser.add_option('-v', '--verbose', default=False, action="store_true", help="Print more verbose output") parser.add_option('-d', '--debug', default=False, action="store_true", help="Print more verbose output") parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0", help="Address of Glance API host. " "Default: %default") parser.add_option('-p', '--port', dest="port", metavar="PORT", type=int, default=9292, help="Port the Glance API host listens on. " "Default: %default") parser.add_option('-A', '--auth_token', dest="auth_token", metavar="TOKEN", default=None, help="Authentication token to use to identify the " "client to the glance server") parser.add_option('-f', '--force', dest="force", metavar="FORCE", default=False, action="store_true", help="Prevent select actions from requesting " "user confirmation") def parse_options(parser, cli_args): """ Returns the parsed CLI options, command to run and its arguments, merged with any same-named options found in a configuration file :param parser: The option parser """ if not cli_args: cli_args.append('-h') # Show options in usage output... (options, args) = parser.parse_args(cli_args) # HACK(sirp): Make the parser available to the print_help method # print_help is a command, so it only accepts (options, args); we could # one-off have it take (parser, options, args), however, for now, I think # this little hack will suffice options.__parser = parser if not args: parser.print_usage() sys.exit(0) command_name = args.pop(0) command = lookup_command(parser, command_name) return (options, command, args) def print_help(options, args): """ Print help specific to a command """ if len(args) != 1: sys.exit("Please specify a command") parser = options.__parser command_name = args.pop() command = lookup_command(parser, command_name) print command.__doc__ % {'prog': os.path.basename(sys.argv[0])} def lookup_command(parser, command_name): BASE_COMMANDS = {'help': print_help} CACHE_COMMANDS = { 'list-cached': list_cached, 'list-queued': list_queued, 'queue-image': queue_image, 'delete-cached-image': delete_cached_image, 'delete-all-cached-images': delete_all_cached_images, 'delete-queued-image': delete_queued_image, 'delete-all-queued-images': delete_all_queued_images, } commands = {} for command_set in (BASE_COMMANDS, CACHE_COMMANDS): commands.update(command_set) try: command = commands[command_name] except KeyError: parser.print_usage() sys.exit("Unknown command: %s" % command_name) return command 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 = raw_input("%s %s " % (prompt, prompt_default)) if answer == "": return default else: return answer.lower() in ("yes", "y") if __name__ == '__main__': usage = """ %prog [options] [args] Commands: help Output help for one of the commands below list-cached List all images currently cached list-queued List all images currently queued for caching queue-image Queue an image for caching delete-cached-image Purges an image from the cache delete-all-cached-images Removes all images from the cache delete-queued-image Deletes an image from the cache queue delete-all-queued-images Deletes all images from the cache queue clean Removes any stale or invalid image files from the cache """ oparser = optparse.OptionParser(version='%%prog %s' % version.version_string(), usage=usage.strip()) create_options(oparser) (options, command, args) = parse_options(oparser, sys.argv[1:]) try: start_time = time.time() result = command(options, args) end_time = time.time() if options.verbose: print "Completed in %-0.4f sec." % (end_time - start_time) sys.exit(result) except (RuntimeError, NotImplementedError), e: print "ERROR: ", e