diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 371605692a..70bef1c8a6 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -15,6 +15,9 @@ import logging +from glanceclient import exc as gc_exceptions +from glanceclient.v1 import client as gc_v1_client +from glanceclient.v1 import images as gc_v1_images from openstackclient.common import utils @@ -22,8 +25,8 @@ LOG = logging.getLogger(__name__) API_NAME = "image" API_VERSIONS = { - "1": "glanceclient.v1.client.Client", - "2": "glanceclient.v2.client.Client" + "1": "openstackclient.image.client.Client_v1", + "2": "glanceclient.v2.client.Client", } @@ -38,3 +41,54 @@ def make_client(instance): instance._url = instance.get_endpoint_for_service_type(API_NAME) return image_client(instance._url, token=instance._token) + + +# NOTE(dtroyer): glanceclient.v1.image.ImageManager() doesn't have a find() +# method so add one here until the common client libs arrive +# A similar subclass will be required for v2 + +class Client_v1(gc_v1_client.Client): + """An image v1 client that uses ImageManager_v1""" + + def __init__(self, *args, **kwargs): + super(Client_v1, self).__init__(*args, **kwargs) + self.images = ImageManager_v1(self) + + +class ImageManager_v1(gc_v1_images.ImageManager): + """Add find() and findall() to the ImageManager class""" + + def find(self, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + rl = self.findall(**kwargs) + num = len(rl) + + if num == 0: + raise gc_exceptions.NotFound + elif num > 1: + raise gc_exceptions.NoUniqueMatch + else: + return rl[0] + + def findall(self, **kwargs): + """Find all items with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + found = [] + searches = kwargs.items() + + for obj in self.list(): + try: + if all(getattr(obj, attr) == value + for (attr, value) in searches): + found.append(obj) + except AttributeError: + continue + + return found diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 5b4a939daa..0213ed1e28 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -17,6 +17,7 @@ import logging import os +import six import sys if os.name == "nt": @@ -24,11 +25,18 @@ if os.name == "nt": else: msvcrt = None +from cliff import command +from cliff import lister from cliff import show +from glanceclient.common import utils as gc_utils +from openstackclient.common import exceptions +from openstackclient.common import parseractions +from openstackclient.common import utils + class CreateImage(show.ShowOne): - """Create image command""" + """Create/upload an image""" log = logging.getLogger(__name__ + ".CreateImage") @@ -37,89 +45,108 @@ class CreateImage(show.ShowOne): parser.add_argument( "name", metavar="<name>", - help="Name of image.") + help="Name of image", + ) parser.add_argument( "--disk_format", default="raw", metavar="<disk_format>", - help="Disk format of image.") + help="Disk format of image", + ) parser.add_argument( "--id", metavar="<id>", - help="ID of image to reserve.") + help="ID of image to reserve", + ) parser.add_argument( "--store", metavar="<store>", - help="Store to upload image to.") + help="Store to upload image to", + ) parser.add_argument( "--container-format", default="bare", metavar="<container_format>", - help="Container format of image.") + help="Container format of image", + ) parser.add_argument( "--owner", - metavar="<tenant_id>", - help="Owner of the image.") + metavar="<tenant>", + help="Owner of the image", + ) parser.add_argument( "--size", metavar="<size>", help="Size of image in bytes. Only used with --location and" - " --copy-from.") + " --copy-from", + ) parser.add_argument( "--min-disk", metavar="<disk_gb>", - help="Minimum size of disk needed to boot image in gigabytes.") + help="Minimum size of disk needed to boot image in gigabytes", + ) parser.add_argument( "--min-ram", metavar="<disk_ram>", - help="Minimum amount of ram needed to boot image in megabytes.") + help="Minimum amount of ram needed to boot image in megabytes", + ) parser.add_argument( "--location", metavar="<image_url>", - help="URL where the data for this image already resides.") + help="URL where the data for this image already resides", + ) parser.add_argument( "--file", metavar="<file>", - help="Local file that contains disk image.") + help="Local file that contains disk image", + ) parser.add_argument( "--checksum", metavar="<checksum>", - help="Hash of image data used for verification.") + help="Hash of image data used for verification", + ) parser.add_argument( "--copy-from", metavar="<image_url>", help="Similar to --location, but this indicates that the image" - " should immediately be copied from the data store.") + " should immediately be copied from the data store", + ) parser.add_argument( "--property", + dest="properties", metavar="<key=value>", - default=[], - action="append", - help="Arbitrary property to associate with image.") + action=parseractions.KeyValueAction, + help="Set property on this image " + '(repeat option to set multiple properties)', + ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", dest="protected", action="store_true", - help="Prevent image from being deleted (default: False).") + help="Prevent image from being deleted (default: False)", + ) protected_group.add_argument( "--unprotected", dest="protected", action="store_false", default=False, - help="Allow images to be deleted (default: True).") + help="Allow images to be deleted (default: True)", + ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", dest="is_public", action="store_true", default=True, - help="Image is accessible to the public (default).") + help="Image is accessible to the public (default)", + ) public_group.add_argument( "--private", dest="is_public", action="store_false", - help="Image is inaccessible to the public.") + help="Image is inaccessible to the public", + ) return parser def take_action(self, parsed_args): @@ -134,11 +161,6 @@ class CreateImage(show.ShowOne): args.pop("prefix") args.pop("variables") - args["properties"] = {} - for _property in args.pop("property"): - key, value = _property.split("=", 1) - args["properties"][key] = value - if "location" not in args and "copy_from" not in args: if "file" in args: args["data"] = open(args.pop("file"), "rb") @@ -150,6 +172,231 @@ class CreateImage(show.ShowOne): args["data"] = sys.stdin image_client = self.app.client_manager.image - data = image_client.images.create(**args)._info.copy() + try: + image = utils.find_resource( + image_client.images, + parsed_args.name, + ) + except exceptions.CommandError: + # This is normal for a create or reserve (create w/o an image) + image = image_client.images.create(**args) + else: + # It must be an update + # If an image is specified via --file, --location or --copy-from + # let the API handle it + image = image_client.images.update(image, **args) - return zip(*sorted(data.iteritems())) + info = {} + info.update(image._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteImage(command.Command): + """Delete an image""" + + log = logging.getLogger(__name__ + ".DeleteImage") + + def get_parser(self, prog_name): + parser = super(DeleteImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Name or ID of image to delete", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + image_client.images.delete(image) + + +class ListImage(lister.Lister): + """List available images""" + + log = logging.getLogger(__name__ + ".ListImage") + + def get_parser(self, prog_name): + parser = super(ListImage, self).get_parser(prog_name) + parser.add_argument( + "--page-size", + metavar="<size>", + help="Number of images to request in each paginated request", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + image_client = self.app.client_manager.image + + kwargs = {} + if parsed_args.page_size is not None: + kwargs["page_size"] = parsed_args.page_size + + data = image_client.images.list(**kwargs) + columns = ["ID", "Name"] + + return (columns, (utils.get_item_properties(s, columns) for s in data)) + + +class SaveImage(command.Command): + """Save an image locally""" + + log = logging.getLogger(__name__ + ".SaveImage") + + def get_parser(self, prog_name): + parser = super(SaveImage, self).get_parser(prog_name) + parser.add_argument( + "--file", + metavar="<filename>", + help="Downloaded image save filename [default: stdout]", + ) + parser.add_argument( + "image", + metavar="<image>", + help="Name or ID of image to delete", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + data = image_client.images.data(image) + + gc_utils.save_image(data, parsed_args.file) + + +class SetImage(show.ShowOne): + """Change image properties""" + + log = logging.getLogger(__name__ + ".SetImage") + + def get_parser(self, prog_name): + parser = super(SetImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Name or ID of image to change", + ) + parser.add_argument( + "--name", + metavar="<name>", + help="Name of image", + ) + parser.add_argument( + "--owner", + metavar="<tenant>", + help="Owner of the image", + ) + parser.add_argument( + "--min-disk", + metavar="<disk_gb>", + help="Minimum size of disk needed to boot image in gigabytes", + ) + parser.add_argument( + "--min-ram", + metavar="<disk_ram>", + help="Minimum amount of ram needed to boot image in megabytes", + ) + parser.add_argument( + "--property", + dest="properties", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set property on this image " + '(repeat option to set multiple properties)', + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( + "--protected", + dest="protected", + action="store_true", + help="Prevent image from being deleted (default: False)", + ) + protected_group.add_argument( + "--unprotected", + dest="protected", + action="store_false", + default=False, + help="Allow images to be deleted (default: True)", + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + dest="is_public", + action="store_true", + default=True, + help="Image is accessible to the public (default)", + ) + public_group.add_argument( + "--private", + dest="is_public", + action="store_false", + help="Image is inaccessible to the public", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + # NOTE(jk0): Since create() takes kwargs, it's easiest to just make a + # copy of parsed_args and remove what we don't need. + args = vars(parsed_args) + args = dict(filter(lambda x: x[1] is not None, args.items())) + args.pop("columns") + args.pop("formatter") + args.pop("prefix") + args.pop("variables") + image_arg = args.pop("image") + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + image_arg, + ) + # Merge properties + args["properties"].update(image.properties) + image = image_client.images.update(image, **args) + + info = {} + info.update(image._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowImage(show.ShowOne): + """Show image details""" + + log = logging.getLogger(__name__ + ".ShowImage") + + def get_parser(self, prog_name): + parser = super(ShowImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Name or ID of image to display", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + + info = {} + info.update(image._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 61273aa2c3..e84e0d0191 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -16,6 +16,7 @@ """Image V2 Action Implementations""" import logging +import six from cliff import command from cliff import lister @@ -26,27 +27,32 @@ from openstackclient.common import utils class DeleteImage(command.Command): - """Delete image command""" + """Delete an image""" log = logging.getLogger(__name__ + ".DeleteImage") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) parser.add_argument( - "id", - metavar="<image_id>", - help="ID of image to delete.") + "image", + metavar="<image>", + help="Name or ID of image to delete", + ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) image_client = self.app.client_manager.image - image_client.images.delete(parsed_args.id) + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + image_client.images.delete(image) class ListImage(lister.Lister): - """List image command""" + """List available images""" log = logging.getLogger(__name__ + ".ListImage") @@ -55,7 +61,8 @@ class ListImage(lister.Lister): parser.add_argument( "--page-size", metavar="<size>", - help="Number of images to request in each paginated request.") + help="Number of images to request in each paginated request", + ) return parser def take_action(self, parsed_args): @@ -74,7 +81,7 @@ class ListImage(lister.Lister): class SaveImage(command.Command): - """Save image command""" + """Save an image locally""" log = logging.getLogger(__name__ + ".SaveImage") @@ -82,42 +89,52 @@ class SaveImage(command.Command): parser = super(SaveImage, self).get_parser(prog_name) parser.add_argument( "--file", - metavar="<file>", - help="Local file to save downloaded image data " - "to. If this is not specified the image " - "data will be written to stdout.") + metavar="<filename>", + help="Downloaded image save filename [default: stdout]", + ) parser.add_argument( - "id", - metavar="<image_id>", - help="ID of image to describe.") + "image", + metavar="<image>", + help="Name or ID of image to delete", + ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) image_client = self.app.client_manager.image - data = image_client.images.data(parsed_args.id) + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + data = image_client.images.data(image) gc_utils.save_image(data, parsed_args.file) class ShowImage(show.ShowOne): - """Show image command""" + """Show image details""" log = logging.getLogger(__name__ + ".ShowImage") def get_parser(self, prog_name): parser = super(ShowImage, self).get_parser(prog_name) parser.add_argument( - "id", - metavar="<image_id>", - help="ID of image to describe.") + "image", + metavar="<image>", + help="Name or ID of image to display", + ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) image_client = self.app.client_manager.image - data = image_client.images.get(parsed_args.id) + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) - return zip(*sorted(data.iteritems())) + info = {} + info.update(image._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 561b8ddf6c..dad4a693a1 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -35,7 +35,7 @@ KEYRING_SERVICE = 'openstack' DEFAULT_COMPUTE_API_VERSION = '2' DEFAULT_IDENTITY_API_VERSION = '2.0' -DEFAULT_IMAGE_API_VERSION = '2' +DEFAULT_IMAGE_API_VERSION = '1' DEFAULT_VOLUME_API_VERSION = '1' DEFAULT_DOMAIN = 'default' diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index f479b11e4b..ca87997f67 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -36,7 +36,7 @@ DEFAULT_VOLUME_API_VERSION = "1" LIB_COMPUTE_API_VERSION = "2" LIB_IDENTITY_API_VERSION = "2.0" -LIB_IMAGE_API_VERSION = "2" +LIB_IMAGE_API_VERSION = "1" LIB_VOLUME_API_VERSION = "1" diff --git a/setup.cfg b/setup.cfg index 3178103929..b3a6657bf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -149,6 +149,11 @@ openstack.identity.v3 = openstack.image.v1 = image_create = openstackclient.image.v1.image:CreateImage + image_delete = openstackclient.image.v1.image:DeleteImage + image_list = openstackclient.image.v1.image:ListImage + image_save = openstackclient.image.v1.image:SaveImage + image_set = openstackclient.image.v1.image:SetImage + image_show = openstackclient.image.v1.image:ShowImage openstack.image.v2 = image_delete = openstackclient.image.v2.image:DeleteImage