# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import copy import functools import os import sys from oslo_utils import encodeutils from oslo_utils import strutils from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc import glanceclient.v1.images CONTAINER_FORMATS = ('Acceptable formats: ami, ari, aki, bare, ovf, ova,' 'docker.') DISK_FORMATS = ('Acceptable formats: ami, ari, aki, vhd, vdhx, vmdk, raw, ' 'qcow2, vdi, iso, and ploop.') DATA_FIELDS = ('location', 'copy_from', 'file') _bool_strict = functools.partial(strutils.bool_from_string, strict=True) @utils.arg('--name', metavar='', help='Filter images to those that have this name.') @utils.arg('--status', metavar='', help='Filter images to those that have this status.') @utils.arg('--changes-since', metavar='', help='Filter images to those that changed since the given time' ', which will include the deleted images.') @utils.arg('--container-format', metavar='', help='Filter images to those that have this container format. ' + CONTAINER_FORMATS) @utils.arg('--disk-format', metavar='', help='Filter images to those that have this disk format. ' + DISK_FORMATS) @utils.arg('--size-min', metavar='', type=int, help='Filter images to those with a size greater than this.') @utils.arg('--size-max', metavar='', type=int, help='Filter images to those with a size less than this.') @utils.arg('--property-filter', metavar='', help="Filter images by a user-defined image property.", action='append', dest='properties', default=[]) @utils.arg('--page-size', metavar='', default=None, type=int, help='Number of images to request in each paginated request.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--sort-key', default='name', choices=glanceclient.v1.images.SORT_KEY_VALUES, help='Sort image list by specified field.') @utils.arg('--sort-dir', default='asc', choices=glanceclient.v1.images.SORT_DIR_VALUES, help='Sort image list in specified direction.') @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help=('Allows the user to select a listing of public or non ' 'public images.')) @utils.arg('--owner', default=None, metavar='', help='Display only images owned by this tenant id. Filtering ' 'occurs on the client side so may be inefficient. This option ' 'is mainly intended for admin use. Use an empty string (\'\') ' 'to list images with no owner. Note: This option overrides ' 'the --is-public argument if present. Note: the v2 API ' 'supports more efficient server-side owner based filtering.') @utils.arg('--all-tenants', action='store_true', default=False, help=('Allows the admin user to list all images ' 'irrespective of the image\'s owner or is_public value.')) def do_image_list(gc, args): """List images you can access.""" filter_keys = ['name', 'status', 'container_format', 'disk_format', 'size_min', 'size_max', 'is_public', 'changes_since'] filter_items = [(key, getattr(args, key)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) if 'changes_since' in filters: filters['changes-since'] = filters.pop('changes_since') if args.properties: property_filter_items = [p.split('=', 1) for p in args.properties] if any(len(pair) != 2 for pair in property_filter_items): utils.exit('Argument --property-filter requires properties in the' ' format KEY=VALUE') filters['properties'] = dict(property_filter_items) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size kwargs['sort_key'] = args.sort_key kwargs['sort_dir'] = args.sort_dir kwargs['owner'] = args.owner if args.all_tenants is True: kwargs['is_public'] = None images = gc.images.list(**kwargs) if args.human_readable: def convert_size(image): image.size = utils.make_size_human_readable(image.size) return image images = (convert_size(image) for image in images) columns = ['ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status'] utils.print_list(images, columns) def _image_show(image, human_readable=False, max_column_width=80): # Flatten image properties dict for display info = copy.deepcopy(image._info) if human_readable: info['size'] = utils.make_size_human_readable(info['size']) for (k, v) in info.pop('properties').items(): info['Property \'%s\'' % k] = v utils.print_dict(info, max_column_width=max_column_width) def _set_data_field(fields, args): if 'location' not in fields and 'copy_from' not in fields: fields['data'] = utils.get_data_file(args) @utils.arg('image', metavar='', help='Name or ID of image to describe.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--max-column-width', metavar='', default=80, help='The max column width of the printed table.') def do_image_show(gc, args): """Describe a specific image.""" image_id = utils.find_resource(gc.images, args.image).id image = gc.images.get(image_id) _image_show(image, args.human_readable, max_column_width=int(args.max_column_width)) @utils.arg('--file', metavar='', help='Local file to save downloaded image data to. ' 'If this is not specified and there is no redirection ' 'the image data will not be saved.') @utils.arg('image', metavar='', help='Name or ID of image to download.') @utils.arg('--progress', action='store_true', default=False, help='Show download progress bar.') def do_image_download(gc, args): """Download a specific image.""" image = utils.find_resource(gc.images, args.image) body = image.data() if args.progress: body = progressbar.VerboseIteratorWrapper(body, len(body)) if not (sys.stdout.isatty() and args.file is None): utils.save_image(body, args.file) else: print('No redirection or local file specified for downloaded image ' 'data. Please specify a local file with --file to save ' 'downloaded image or redirect output to another source.') @utils.arg('--id', metavar='', help='ID of image to reserve.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--store', metavar='', help='Store to upload image to.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help=('Size of image data (in bytes). Only used with' ' \'--location\' and \'--copy_from\'.')) @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.)')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' creation. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help=('Hash of image data used Glance can use for verification.' ' Provide a md5 checksum here.')) @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') @utils.on_data_require_fields(DATA_FIELDS) def do_image_create(gc, args): """Create a new image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) fields['is_public'] = fields.get('is_public') if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items())) _set_data_field(fields, args) # Only show progress bar for local image files if fields.get('data') and args.progress: filesize = utils.get_file_size(fields['data']) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) image = gc.images.create(**fields) _image_show(image, args.human_readable) def _is_image_data_provided(args): """Return True if some image data has probably been provided by the user""" # NOTE(kragniz): Check stdin works, then check is there is any data # on stdin or a filename has been provided with --file try: os.fstat(0) except OSError: return False return not sys.stdin.isatty() or args.file or args.copy_from @utils.arg('image', metavar='', help='Name or ID of image to modify.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help='Size of image data (in bytes).') @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.) ' 'This option only works for images in \'queued\' status.')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' update. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help='Hash of image data used Glance can use for verification.') @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.' ' This option only works for images in \'queued\' status.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--purge-props', action='store_true', default=False, help=("If this flag is present, delete all image properties " "not explicitly set in the update request. Otherwise, " "those properties not referenced are preserved.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') def do_image_update(gc, args): """Update a specific image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) image_arg = fields.pop('image') image = utils.find_resource(gc.images, image_arg) if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use UPDATE_PARAMS = glanceclient.v1.images.UPDATE_PARAMS fields = dict(filter(lambda x: x[0] in UPDATE_PARAMS, fields.items())) if image.status == 'queued': _set_data_field(fields, args) if args.progress: filesize = utils.get_file_size(fields['data']) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) elif _is_image_data_provided(args): # NOTE(kragniz): Exit with an error if the status is not queued # and image data was provided utils.exit('Unable to upload image data to an image which ' 'is %s.' % image.status) image = gc.images.update(image, purge_props=args.purge_props, **fields) _image_show(image, args.human_readable) @utils.arg('images', metavar='', nargs='+', help='Name or ID of image(s) to delete.') def do_image_delete(gc, args): """Delete specified image(s).""" for args_image in args.images: image = utils.find_resource(gc.images, args_image) if image and image.status == "deleted": msg = "No image with an ID of '%s' exists." % image.id raise exc.CommandError(msg) try: if args.verbose: print('Requesting image delete for %s ...' % encodeutils.safe_decode(args_image), end=' ') gc.images.delete(image) if args.verbose: print('[Done]') except exc.HTTPException as e: if args.verbose: print('[Fail]') print('%s: Unable to delete image %s' % (e, args_image)) @utils.arg('--image-id', metavar='', help='Filter results by an image ID.') @utils.arg('--tenant-id', metavar='', help='Filter results by a tenant ID.') def do_member_list(gc, args): """Describe sharing permissions by image or tenant.""" if args.image_id and args.tenant_id: utils.exit('Unable to filter members by both --image-id and' ' --tenant-id.') elif args.image_id: kwargs = {'image': args.image_id} elif args.tenant_id: kwargs = {'member': args.tenant_id} else: utils.exit('Unable to list all members. Specify --image-id or' ' --tenant-id') members = gc.image_members.list(**kwargs) columns = ['Image ID', 'Member ID', 'Can Share'] utils.print_list(members, columns) @utils.arg('image', metavar='', help='Image to add member to.') @utils.arg('tenant_id', metavar='', help='Tenant to add as member.') @utils.arg('--can-share', action='store_true', default=False, help='Allow the specified tenant to share this image.') def do_member_create(gc, args): """Share a specific image with a tenant.""" image = utils.find_resource(gc.images, args.image) gc.image_members.create(image, args.tenant_id, args.can_share) @utils.arg('image', metavar='', help='Image from which to remove member.') @utils.arg('tenant_id', metavar='', help='Tenant to remove as member.') def do_member_delete(gc, args): """Remove a shared image from a tenant.""" image_id = utils.find_resource(gc.images, args.image).id gc.image_members.delete(image_id, args.tenant_id)