diff --git a/glanceclient/common/base.py b/glanceclient/common/base.py index d037267..3cedd32 100644 --- a/glanceclient/common/base.py +++ b/glanceclient/common/base.py @@ -17,6 +17,9 @@ Base utilities to build API operation managers and objects on top of. """ +import copy + + # Python 2.4 compat try: all @@ -123,3 +126,6 @@ class Resource(object): def set_loaded(self, val): self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) diff --git a/glanceclient/shell.py b/glanceclient/shell.py index 043d501..3902d24 100644 --- a/glanceclient/shell.py +++ b/glanceclient/shell.py @@ -47,12 +47,16 @@ class OpenStackImagesShell(object): help=argparse.SUPPRESS, ) - parser.add_argument('--debug', + parser.add_argument('-d', '--debug', default=bool(utils.env('GLANCECLIENT_DEBUG')), action='store_true', help='Defaults to env[GLANCECLIENT_DEBUG]') - parser.add_argument('--insecure', + parser.add_argument('-v', '--verbose', + default=False, action="store_true", + help="Print more verbose output") + + parser.add_argument('-k', '--insecure', default=False, action='store_true', help="Explicitly allow glanceclient to perform \"insecure\" " @@ -64,6 +68,41 @@ class OpenStackImagesShell(object): default=600, help='Number of seconds to wait for a response') + parser.add_argument('-f', '--force', + dest='force', + default=False, action='store_true', + help='Prevent select actions from requesting ' + 'user confirmation.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('--dry-run', + default=False, + action='store_true', + help='DEPRECATED! Only used for deprecated legacy commands.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('--ssl', + dest='use_ssl', + default=False, + action='store_true', + help='DEPRECATED! Send a fully-formed endpoint using ' + '--os-image-url instead.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-H', '--host', + metavar='ADDRESS', + help='DEPRECATED! Send a fully-formed endpoint using ' + '--os-image-url instead.') + + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-p', '--port', + dest='port', + metavar='PORT', + type=int, + default=9292, + help='DEPRECATED! Send a fully-formed endpoint using ' + '--os-image-url instead.') + parser.add_argument('--os-username', default=utils.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME]') @@ -71,6 +110,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_username', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-I', + dest='os_username', + help='DEPRECATED! Use --os-username.') + parser.add_argument('--os-password', default=utils.env('OS_PASSWORD'), help='Defaults to env[OS_PASSWORD]') @@ -78,6 +122,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_password', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-K', + dest='os_password', + help='DEPRECATED! Use --os-password.') + parser.add_argument('--os-tenant-id', default=utils.env('OS_TENANT_ID'), help='Defaults to env[OS_TENANT_ID]') @@ -92,6 +141,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-T', + dest='os_tenant_name', + help='DEPRECATED! Use --os-tenant-name.') + parser.add_argument('--os-auth-url', default=utils.env('OS_AUTH_URL'), help='Defaults to env[OS_AUTH_URL]') @@ -99,6 +153,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-N', + dest='os_auth_url', + help='DEPRECATED! Use --os-auth-url.') + parser.add_argument('--os-region-name', default=utils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME]') @@ -106,6 +165,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_region_name', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-R', + dest='os_region_name', + help='DEPRECATED! Use --os-region-name.') + parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN]') @@ -113,6 +177,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_auth_token', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-A', '--auth_token', + dest='os_auth_token', + help='DEPRECATED! Use --os-auth-token.') + parser.add_argument('--os-image-url', default=utils.env('OS_IMAGE_URL'), help='Defaults to env[OS_IMAGE_URL]') @@ -120,6 +189,11 @@ class OpenStackImagesShell(object): parser.add_argument('--os_image_url', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-U', '--url', + dest='os_image_url', + help='DEPRECATED! Use --os-image-url.') + parser.add_argument('--os-image-api-version', default=utils.env('OS_IMAGE_API_VERSION', default='1'), help='Defaults to env[OS_IMAGE_API_VERSION] or 1') @@ -141,6 +215,10 @@ class OpenStackImagesShell(object): parser.add_argument('--os_endpoint_type', help=argparse.SUPPRESS) + #NOTE(bcwaldon): DEPRECATED + parser.add_argument('-S', '--os_auth_strategy', + help='DEPRECATED! This option is completely ignored.') + return parser def get_subcommand_parser(self, version): @@ -216,6 +294,20 @@ class OpenStackImagesShell(object): endpoint = self._strip_version(endpoint) return (endpoint, _ksclient.auth_token) + def _get_image_url(self, args): + """Translate the available url-related options into a single string. + + Return the endpoint that should be used to talk to Glance if a + clear decision can be made. Otherwise, return None. + """ + if args.os_image_url: + return args.os_image_url + elif args.host: + scheme = 'https' if args.use_ssl else 'http' + return '%s://%s:%s/' % (scheme, args.host, args.port) + else: + return None + def main(self, argv): # Parse args once to find version parser = self.get_base_parser() @@ -244,11 +336,12 @@ class OpenStackImagesShell(object): LOG.addHandler(logging.StreamHandler()) LOG.setLevel(logging.DEBUG if args.debug else logging.INFO) + image_url = self._get_image_url(args) auth_reqd = (utils.is_authentication_required(args.func) and - not (args.os_auth_token and args.os_image_url)) + not (args.os_auth_token and image_url)) if not auth_reqd: - endpoint = args.os_image_url + endpoint = image_url token = args.os_auth_token else: if not args.os_username: diff --git a/glanceclient/v1/legacy_shell.py b/glanceclient/v1/legacy_shell.py new file mode 100755 index 0000000..030cee4 --- /dev/null +++ b/glanceclient/v1/legacy_shell.py @@ -0,0 +1,510 @@ +# Copyright 2012 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. + +""" +DEPRECATED functions that implement the same command line interface as the +legacy glance client. +""" + +import argparse +import sys + +from glanceclient.common import utils + + +SUCCESS = 0 +FAILURE = 1 + + +def get_image_fields_from_args(args): + """ + Validate the set of arguments passed as field name/value pairs + and return them as a mapping. + """ + fields = {} + for arg in args: + pieces = arg.strip(',').split('=') + if len(pieces) != 2: + msg = ("Arguments should be in the form of field=value. " + "You specified %s." % arg) + raise RuntimeError(msg) + fields[pieces[0]] = pieces[1] + + return fields + + +def get_image_filters_from_args(args): + """Build a dictionary of query filters based on the supplied args.""" + try: + fields = get_image_fields_from_args(args) + except RuntimeError, e: + print e + return FAILURE + + SUPPORTED_FILTERS = ['name', 'disk_format', 'container_format', 'status', + 'min_ram', 'min_disk', 'size_min', 'size_max', + 'changes-since'] + filters = {} + for (key, value) in fields.items(): + if key not in SUPPORTED_FILTERS: + key = 'property-%s' % (key,) + filters[key] = value + + return filters + + +def print_image_formatted(client, image): + """ + Formatted print of image metadata. + + :param client: The Glance client object + :param image: The image metadata + """ + print "URI: http://%s:%s/v1/images/%s" % ( + client.endpoint[0], + client.endpoint[1], + image.id) + print "Id: %s" % image.id + print "Public: " + (image.is_public and "Yes" or "No") + print "Protected: " + (image.protected and "Yes" or "No") + print "Name: %s" % getattr(image, 'name', '') + print "Status: %s" % image.status + print "Size: %d" % int(image.size) + print "Disk format: %s" % getattr(image, 'disk_format', '') + print "Container format: %s" % getattr(image, 'container_format', '') + print "Minimum Ram Required (MB): %s" % image.min_ram + print "Minimum Disk Required (GB): %s" % image.min_disk + if hasattr(image, 'owner'): + print "Owner: %s" % image.owner + if len(image.properties) > 0: + for k, v in image.properties.items(): + print "Property '%s': %s" % (k, v) + print "Created at: %s" % image.created_at + if hasattr(image, 'deleted_at'): + print "Deleted at: %s" % image.deleted_at + if hasattr(image, 'updated_at'): + print "Updated at: %s" % image.updated_at + + +@utils.arg('--silent-upload', action="store_true", + help="DEPRECATED! Animations are always off.") +@utils.arg('fields', default=[], nargs='*', help=argparse.SUPPRESS) +def do_add(gc, args): + """DEPRECATED! Use image-create instead.""" + try: + fields = get_image_fields_from_args(args.fields) + except RuntimeError, e: + print e + return FAILURE + + image_meta = { + 'is_public': utils.string_to_bool( + fields.pop('is_public', 'False')), + 'protected': utils.string_to_bool( + fields.pop('protected', 'False')), + 'min_disk': fields.pop('min_disk', 0), + 'min_ram': fields.pop('min_ram', 0), + } + + #NOTE(bcwaldon): Use certain properties only if they are explicitly set + optional = ['id', 'name', 'disk_format', 'container_format', 'copy_from'] + for field in optional: + if field in fields: + image_meta[field] = fields.pop(field) + + # Strip any args that are not supported + unsupported_fields = ['status', 'size'] + for field in unsupported_fields: + if field in fields.keys(): + print 'Found non-settable field %s. Removing.' % field + fields.pop(field) + + def _external_source(fields, image_data): + source = None + if 'location' in fields.keys(): + source = fields.pop('location') + image_meta['location'] = source + if 'checksum' in fields.keys(): + image_meta['checksum'] = fields.pop('checksum') + elif 'copy_from' in fields.keys(): + source = fields.pop('copy_from') + image_meta['copy_from'] = source + return source + + # We need either a location or image data/stream to add... + location = _external_source(fields, image_meta) + image_data = None + if not location: + # Grab the image data stream from stdin or redirect, + # otherwise error out + image_data = sys.stdin + + image_meta['data'] = image_data + + # allow owner to be set when image is created + if 'owner' in fields.keys(): + image_meta['owner'] = fields.pop('owner') + + # Add custom attributes, which are all the arguments remaining + image_meta['properties'] = fields + + if not args.dry_run: + image = gc.images.create(**image_meta) + print "Added new image with ID: %s" % image.id + if args.verbose: + print "Returned the following metadata for the new image:" + for k, v in sorted(image.to_dict().items()): + print " %(k)30s => %(v)s" % locals() + else: + print "Dry run. We would have done the following:" + + def _dump(dict): + for k, v in sorted(dict.items()): + print " %(k)30s => %(v)s" % locals() + + print "Add new image with metadata:" + _dump(image_meta) + + return SUCCESS + + +@utils.arg('id', metavar='', help='ID of image to describe.') +@utils.arg('fields', default=[], nargs='*', help=argparse.SUPPRESS) +def do_update(gc, args): + """DEPRECATED! Use image-update instead.""" + try: + fields = get_image_fields_from_args(args.fields) + except RuntimeError, e: + print e + return FAILURE + + image_meta = {} + + # Strip any args that are not supported + nonmodifiable_fields = ['created_at', 'deleted_at', 'deleted', + 'updated_at', 'size', 'status'] + for field in nonmodifiable_fields: + if field in fields.keys(): + print 'Found non-modifiable field %s. Removing.' % field + fields.pop(field) + + base_image_fields = ['disk_format', 'container_format', 'name', + 'min_disk', 'min_ram', 'location', 'owner', + 'copy_from'] + for field in base_image_fields: + fvalue = fields.pop(field, None) + if fvalue is not None: + image_meta[field] = fvalue + + # Have to handle "boolean" values specially... + if 'is_public' in fields: + image_meta['is_public'] = utils.string_to_bool(fields.pop('is_public')) + if 'protected' in fields: + image_meta['protected'] = utils.string_to_bool(fields.pop('protected')) + + # Add custom attributes, which are all the arguments remaining + image_meta['properties'] = fields + + if not args.dry_run: + image = gc.images.update(args.id, **image_meta) + print "Updated image %s" % args.id + + if args.verbose: + print "Updated image metadata for image %s:" % args.id + print_image_formatted(gc, image) + else: + def _dump(dict): + for k, v in sorted(dict.items()): + print " %(k)30s => %(v)s" % locals() + + print "Dry run. We would have done the following:" + print "Update existing image with metadata:" + _dump(image_meta) + + return SUCCESS + + +@utils.arg('id', metavar='', help='ID of image to describe.') +def do_delete(gc, args): + """DEPRECATED! Use image-delete instead.""" + if not (args.force or + user_confirm("Delete image %s?" % args.id, default=False)): + print 'Not deleting image %s' % args.id + return FAILURE + + gc.images.get(args.id).delete() + + +@utils.arg('id', metavar='', help='ID of image to describe.') +def do_show(gc, args): + image = gc.images.get(args.id) + print_image_formatted(gc, image) + return SUCCESS + + +def _get_images(gc, args): + parameters = { + 'filters': get_image_filters_from_args(args.filters), + 'page_size': args.limit, + } + + optional_kwargs = ['marker', 'sort_key', 'sort_dir'] + for kwarg in optional_kwargs: + if getattr(args, kwarg): + parameters[kwarg] = getattr(args.kwarg) + + return gc.images.list(**parameters) + + +@utils.arg('--limit', dest="limit", metavar="LIMIT", default=10, + type=int, help="Page size to use while requesting image metadata") +@utils.arg('--marker', dest="marker", metavar="MARKER", + default=None, help="Image index after which to begin pagination") +@utils.arg('--sort_key', dest="sort_key", metavar="KEY", + help="Sort results by this image attribute.") +@utils.arg('--sort_dir', dest="sort_dir", metavar="[desc|asc]", + help="Sort results in this direction.") +@utils.arg('filters', default=[], nargs='*', help=argparse.SUPPRESS) +def do_index(gc, args): + """DEPRECATED! Use image-list instead.""" + images = _get_images(gc, args) + + if not images: + return SUCCESS + + pretty_table = PrettyTable() + pretty_table.add_column(36, label="ID") + pretty_table.add_column(30, label="Name") + pretty_table.add_column(20, label="Disk Format") + pretty_table.add_column(20, label="Container Format") + pretty_table.add_column(14, label="Size", just="r") + + print pretty_table.make_header() + + for image in images: + print pretty_table.make_row(image.id, + image.name, + image.disk_format, + image.container_format, + image.size) + + +@utils.arg('--limit', dest="limit", metavar="LIMIT", default=10, + type=int, help="Page size to use while requesting image metadata") +@utils.arg('--marker', dest="marker", metavar="MARKER", + default=None, help="Image index after which to begin pagination") +@utils.arg('--sort_key', dest="sort_key", metavar="KEY", + help="Sort results by this image attribute.") +@utils.arg('--sort_dir', dest="sort_dir", metavar="[desc|asc]", + help="Sort results in this direction.") +@utils.arg('filters', default='', nargs='*', help=argparse.SUPPRESS) +def do_details(gc, args): + """DEPRECATED! Use image-list instead.""" + images = _get_images(gc, args) + for i, image in enumerate(images): + if i == 0: + print "=" * 80 + print_image_formatted(gc, image) + print "=" * 80 + + +def do_clear(gc, args): + """DEPRECATED!""" + if not (args.force or + user_confirm("Delete all images?", default=False)): + print 'Not deleting any images' + return FAILURE + + images = gc.images.list() + for image in images: + if args.verbose: + print 'Deleting image %s "%s" ...' % (image.id, image.name), + try: + image.delete() + if args.verbose: + print 'done' + except Exception, e: + print 'Failed to delete image %s' % image.id + print e + return FAILURE + return SUCCESS + + +@utils.arg('image_id', help='Image ID to filters members with.') +def do_image_members(gc, args): + """DEPRECATED! Use member-list instead.""" + members = gc.image_members.list(image=args.image_id) + sharers = 0 + # Output the list of members + for memb in members: + can_share = '' + if memb.can_share: + can_share = ' *' + sharers += 1 + print "%s%s" % (memb.member_id, can_share) + + # Emit a footnote + if sharers > 0: + print "\n(*: Can share image)" + + +@utils.arg('--can-share', default=False, action="store_true", + help="Allow member to further share image.") +@utils.arg('member_id', + help='ID of member (typically tenant) to grant access.') +def do_member_images(gc, args): + """DEPRECATED! Use member-list instead.""" + members = gc.image_members.list(member=args.member_id) + + if not len(members): + print "No images shared with member %s" % args.member_id + return SUCCESS + + sharers = 0 + # Output the list of images + for memb in members: + can_share = '' + if memb.can_share: + can_share = ' *' + sharers += 1 + print "%s%s" % (memb.image_id, can_share) + + # Emit a footnote + if sharers > 0: + print "\n(*: Can share image)" + + +@utils.arg('--can-share', default=False, action="store_true", + help="Allow member to further share image.") +@utils.arg('image_id', help='ID of image to describe.') +@utils.arg('member_id', + help='ID of member (typically tenant) to grant access.') +def do_members_replace(gc, args): + """DEPRECATED!""" + if not args.dry_run: + for member in gc.image_members.list(image=args.image_id): + gc.image_members.delete(args.image_id, member.member_id) + gc.image_members.create(args.image_id, args.member_id, args.can_share) + else: + print "Dry run. We would have done the following:" + print ('Replace members of image %s with "%s"' + % (args.image_id, args.member_id)) + if args.can_share: + print "New member would have been able to further share image." + + +@utils.arg('--can-share', default=False, action="store_true", + help="Allow member to further share image.") +@utils.arg('image_id', help='ID of image to describe.') +@utils.arg('member_id', + help='ID of member (typically tenant) to grant access.') +def do_member_add(gc, args): + """DEPRECATED! Use member-create instead.""" + if not args.dry_run: + gc.image_members.create(args.image_id, args.member_id, args.can_share) + else: + print "Dry run. We would have done the following:" + print ('Add "%s" to membership of image %s' % + (args.member_id, args.image_id)) + if args.can_share: + print "New member would have been able to further share image." + + +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]" + + # for bug 884116, don't issue the prompt if stdin isn't a tty + if not (hasattr(sys.stdin, 'isatty') and sys.stdin.isatty()): + return default + + answer = raw_input("%s %s " % (prompt, prompt_default)) + + if answer == "": + return default + else: + return answer.lower() in ("yes", "y") + + +class PrettyTable(object): + """Creates an ASCII art table + + Example: + + ID Name Size Hits + --- ----------------- ------------ ----- + 122 image 22 0 + """ + def __init__(self): + self.columns = [] + + def add_column(self, width, label="", just='l'): + """Add a column to the table + + :param width: number of characters wide the column should be + :param label: column heading + :param just: justification for the column, 'l' for left, + 'r' for right + """ + self.columns.append((width, label, just)) + + def make_header(self): + label_parts = [] + break_parts = [] + for width, label, _ in self.columns: + # NOTE(sirp): headers are always left justified + label_part = self._clip_and_justify(label, width, 'l') + label_parts.append(label_part) + + break_part = '-' * width + break_parts.append(break_part) + + label_line = ' '.join(label_parts) + break_line = ' '.join(break_parts) + return '\n'.join([label_line, break_line]) + + def make_row(self, *args): + row = args + row_parts = [] + for data, (width, _, just) in zip(row, self.columns): + row_part = self._clip_and_justify(data, width, just) + row_parts.append(row_part) + + row_line = ' '.join(row_parts) + return row_line + + @staticmethod + def _clip_and_justify(data, width, just): + # clip field to column width + clipped_data = str(data)[:width] + + if just == 'r': + # right justify + justified = clipped_data.rjust(width) + else: + # left justify + justified = clipped_data.ljust(width) + + return justified diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py index 9639724..c521ea2 100644 --- a/glanceclient/v1/shell.py +++ b/glanceclient/v1/shell.py @@ -20,6 +20,9 @@ import sys from glanceclient.common import utils import glanceclient.v1.images +#NOTE(bcwaldon): import deprecated cli functions +from glanceclient.v1.legacy_shell import * + @utils.arg('--name', metavar='', help='Filter images to those that have this name.') @@ -258,4 +261,9 @@ def do_member_create(gc, args): @utils.arg('tenant_id', metavar='', help='Tenant to add as member') def do_member_delete(gc, args): - gc.image_members.delete(args.image_id, args.tenant_id) + if not options.dry_run: + gc.image_members.delete(args.image_id, args.tenant_id) + else: + print "Dry run. We would have done the following:" + print ('Remove "%(member_id)s" from the member list of image ' + '"%(image_id)s"' % locals())