diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/image/v2/image.py | 775 | ||||
| -rw-r--r-- | openstackclient/tests/unit/image/v2/test_image.py | 116 |
2 files changed, 511 insertions, 380 deletions
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 38c64db9..2342fd3e 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -44,8 +44,19 @@ else: CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] DEFAULT_CONTAINER_FORMAT = 'bare' DEFAULT_DISK_FORMAT = 'raw' -DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", - "vdi", "iso", "ploop"] +DISK_CHOICES = [ + "ami", + "ari", + "aki", + "vhd", + "vmdk", + "raw", + "qcow2", + "vhdx", + "vdi", + "iso", + "ploop", +] MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"] @@ -59,10 +70,25 @@ def _format_image(image, human_readable=False): properties = {} # the only fields we're not including is "links", "tags" and the properties - fields_to_show = ['status', 'name', 'container_format', 'created_at', - 'size', 'disk_format', 'updated_at', 'visibility', - 'min_disk', 'protected', 'id', 'file', 'checksum', - 'owner', 'virtual_size', 'min_ram', 'schema'] + fields_to_show = [ + 'status', + 'name', + 'container_format', + 'created_at', + 'size', + 'disk_format', + 'updated_at', + 'visibility', + 'min_disk', + 'protected', + 'id', + 'file', + 'checksum', + 'owner', + 'virtual_size', + 'min_ram', + 'schema', + ] # TODO(gtema/anybody): actually it should be possible to drop this method, # since SDK already delivers a proper object @@ -99,12 +125,12 @@ _formatters = { def _get_member_columns(item): - column_map = { - 'image_id': 'image_id' - } + column_map = {'image_id': 'image_id'} hidden_columns = ['id', 'location', 'name'] return utils.get_osc_show_columns_for_sdk_resource( - item.to_dict(), column_map, hidden_columns, + item.to_dict(), + column_map, + hidden_columns, ) @@ -142,7 +168,7 @@ class AddProjectToImage(command.ShowOne): _description = _("Associate project with image") def get_parser(self, prog_name): - parser = super(AddProjectToImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "image", metavar="<image>", @@ -166,10 +192,12 @@ class AddProjectToImage(command.ShowOne): project_id = common.find_project( identity_client, parsed_args.project, - parsed_args.project_domain).id + parsed_args.project_domain, + ).id - image = image_client.find_image(parsed_args.image, - ignore_missing=False) + image = image_client.find_image( + parsed_args.image, ignore_missing=False + ) obj = image_client.add_member( image=image.id, @@ -188,7 +216,7 @@ class CreateImage(command.ShowOne): deadopts = ('size', 'location', 'copy-from', 'checksum', 'store') def get_parser(self, prog_name): - parser = super(CreateImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) # TODO(bunting): There are additional arguments that v1 supported # that v2 either doesn't support or supports weirdly. # --checksum - could be faked clientside perhaps? @@ -211,19 +239,28 @@ class CreateImage(command.ShowOne): default=DEFAULT_CONTAINER_FORMAT, choices=CONTAINER_CHOICES, metavar="<container-format>", - help=(_("Image container format. " + help=( + _( + "Image container format. " "The supported options are: %(option_list)s. " - "The default format is: %(default_opt)s") % - {'option_list': ', '.join(CONTAINER_CHOICES), - 'default_opt': DEFAULT_CONTAINER_FORMAT}) + "The default format is: %(default_opt)s" + ) + % { + 'option_list': ', '.join(CONTAINER_CHOICES), + 'default_opt': DEFAULT_CONTAINER_FORMAT, + } + ), ) parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, choices=DISK_CHOICES, metavar="<disk-format>", - help=_("Image disk format. The supported options are: %s. " - "The default format is: raw") % ', '.join(DISK_CHOICES) + help=_( + "Image disk format. The supported options are: %s. " + "The default format is: raw" + ) + % ', '.join(DISK_CHOICES), ) parser.add_argument( "--min-disk", @@ -253,8 +290,10 @@ class CreateImage(command.ShowOne): dest='force', action='store_true', default=False, - help=_("Force image creation if volume is in use " - "(only meaningful with --volume)"), + help=_( + "Force image creation if volume is in use " + "(only meaningful with --volume)" + ), ) parser.add_argument( "--progress", @@ -266,48 +305,64 @@ class CreateImage(command.ShowOne): '--sign-key-path', metavar="<sign-key-path>", default=[], - help=_("Sign the image using the specified private key. " - "Only use in combination with --sign-cert-id") + help=_( + "Sign the image using the specified private key. " + "Only use in combination with --sign-cert-id" + ), ) parser.add_argument( '--sign-cert-id', metavar="<sign-cert-id>", default=[], - help=_("The specified certificate UUID is a reference to " - "the certificate in the key manager that corresponds " - "to the public key and is used for signature validation. " - "Only use in combination with --sign-key-path") + help=_( + "The specified certificate UUID is a reference to " + "the certificate in the key manager that corresponds " + "to the public key and is used for signature validation. " + "Only use in combination with --sign-key-path" + ), ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", + dest="is_protected", + default=None, help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", - action="store_true", + action="store_false", + dest="is_protected", + default=None, help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", - action="store_true", + action="store_const", + const="public", + dest="visibility", help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", - action="store_true", + action="store_const", + const="private", + dest="visibility", help=_("Image is inaccessible to the public (default)"), ) public_group.add_argument( "--community", - action="store_true", + action="store_const", + const="community", + dest="visibility", help=_("Image is accessible to the community"), ) public_group.add_argument( "--shared", - action="store_true", + action="store_const", + const="shared", + dest="visibility", help=_("Image can be shared"), ) parser.add_argument( @@ -315,16 +370,20 @@ class CreateImage(command.ShowOne): dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help=_("Set a property on this image " - "(repeat option to set multiple properties)"), + help=_( + "Set a property on this image " + "(repeat option to set multiple properties)" + ), ) parser.add_argument( "--tag", dest="tags", metavar="<tag>", action='append', - help=_("Set a tag on this image " - "(repeat option to set multiple tags)"), + help=_( + "Set a tag on this image " + "(repeat option to set multiple tags)" + ), ) parser.add_argument( "--project", @@ -336,8 +395,8 @@ class CreateImage(command.ShowOne): dest="use_import", action="store_true", help=_( - "Force the use of glance image import instead of" - " direct upload") + "Force the use of glance image import instead of direct upload" + ), ) common.add_project_domain_option_to_parser(parser) for deadopt in self.deadopts: @@ -349,22 +408,23 @@ class CreateImage(command.ShowOne): ) return parser - def take_action(self, parsed_args): + def _take_action_image(self, parsed_args): identity_client = self.app.client_manager.identity image_client = self.app.client_manager.image - for deadopt in self.deadopts: - if getattr(parsed_args, deadopt.replace('-', '_'), None): - raise exceptions.CommandError( - _("ERROR: --%s was given, which is an Image v1 option" - " that is no longer supported in Image v2") % deadopt) - # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line kwargs = {'allow_duplicates': True} - copy_attrs = ('name', 'id', - 'container_format', 'disk_format', - 'min_disk', 'min_ram', 'tags', 'visibility') + copy_attrs = ( + 'name', + 'id', + 'container_format', + 'disk_format', + 'min_disk', + 'min_ram', + 'tags', + 'visibility', + ) for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -384,18 +444,12 @@ class CreateImage(command.ShowOne): # a single value for the pair of options because the default must be # to do nothing when no options are present as opposed to always # setting a default. - if parsed_args.protected: - kwargs['is_protected'] = True - if parsed_args.unprotected: - kwargs['is_protected'] = False - if parsed_args.public: - kwargs['visibility'] = 'public' - if parsed_args.private: - kwargs['visibility'] = 'private' - if parsed_args.community: - kwargs['visibility'] = 'community' - if parsed_args.shared: - kwargs['visibility'] = 'shared' + if parsed_args.is_protected is not None: + kwargs['is_protected'] = parsed_args.is_protected + + if parsed_args.visibility is not None: + kwargs['visibility'] = parsed_args.visibility + if parsed_args.project: kwargs['owner_id'] = common.find_project( identity_client, @@ -409,16 +463,19 @@ class CreateImage(command.ShowOne): # open the file first to ensure any failures are handled before the # image is created. Get the file name (if it is file, and not stdin) # for easier further handling. - (fp, fname) = get_data_file(parsed_args) - info = {} + fp, fname = get_data_file(parsed_args) if fp is not None and parsed_args.volume: - raise exceptions.CommandError(_("Uploading data and using " - "container are not allowed at " - "the same time")) + msg = _( + "Uploading data and using container are not allowed at " + "the same time" + ) + raise exceptions.CommandError(msg) + if fp is None and parsed_args.file: LOG.warning(_("Failed to get an image file.")) return {}, {} + if fp is not None and parsed_args.progress: filesize = os.path.getsize(fname) if filesize is not None: @@ -433,89 +490,145 @@ class CreateImage(command.ShowOne): # sign an image using a given local private key file if parsed_args.sign_key_path or parsed_args.sign_cert_id: if not parsed_args.file: - msg = (_("signing an image requires the --file option, " - "passing files via stdin when signing is not " - "supported.")) - raise exceptions.CommandError(msg) - if (len(parsed_args.sign_key_path) < 1 or - len(parsed_args.sign_cert_id) < 1): - msg = (_("'sign-key-path' and 'sign-cert-id' must both be " - "specified when attempting to sign an image.")) + msg = _( + "signing an image requires the --file option, " + "passing files via stdin when signing is not " + "supported." + ) raise exceptions.CommandError(msg) - else: - sign_key_path = parsed_args.sign_key_path - sign_cert_id = parsed_args.sign_cert_id - signer = image_signer.ImageSigner() - try: - pw = utils.get_password( - self.app.stdin, - prompt=("Please enter private key password, leave " - "empty if none: "), - confirm=False) - - if not pw or len(pw) < 1: - pw = None - else: - # load_private_key() requires the password to be - # passed as bytes - pw = pw.encode() - - signer.load_private_key( - sign_key_path, - password=pw) - except Exception: - msg = (_("Error during sign operation: private key " - "could not be loaded.")) - raise exceptions.CommandError(msg) - - signature = signer.generate_signature(fp) - signature_b64 = b64encode(signature) - kwargs['img_signature'] = signature_b64 - kwargs['img_signature_certificate_uuid'] = sign_cert_id - kwargs['img_signature_hash_method'] = signer.hash_method - if signer.padding_method: - kwargs['img_signature_key_type'] = \ - signer.padding_method - - # If a volume is specified. - if parsed_args.volume: - volume_client = self.app.client_manager.volume - source_volume = utils.find_resource( - volume_client.volumes, - parsed_args.volume, - ) - mv_kwargs = {} - if volume_client.api_version >= api_versions.APIVersion('3.1'): - mv_kwargs.update( - visibility=kwargs.get('visibility', 'private'), - protected=bool(parsed_args.protected) + + if ( + len(parsed_args.sign_key_path) < 1 or + len(parsed_args.sign_cert_id) < 1 + ): + msg = _( + "'sign-key-path' and 'sign-cert-id' must both be " + "specified when attempting to sign an image." ) - else: - if kwargs.get('visibility') or parsed_args.protected: - msg = _( - '--os-volume-api-version 3.1 or greater is required ' - 'to support the --public, --private, --community, ' - '--shared or --protected option.' - ) - raise exceptions.CommandError(msg) - response, body = volume_client.volumes.upload_to_image( - source_volume.id, - parsed_args.force, - parsed_args.name, - parsed_args.container_format, - parsed_args.disk_format, - **mv_kwargs - ) - info = body['os-volume_upload_image'] + raise exceptions.CommandError(msg) + + sign_key_path = parsed_args.sign_key_path + sign_cert_id = parsed_args.sign_cert_id + signer = image_signer.ImageSigner() try: - info['volume_type'] = info['volume_type']['name'] - except TypeError: - info['volume_type'] = None + pw = utils.get_password( + self.app.stdin, + prompt=( + "Please enter private key password, leave " + "empty if none: " + ), + confirm=False, + ) + + if not pw or len(pw) < 1: + pw = None + else: + # load_private_key() requires the password to be + # passed as bytes + pw = pw.encode() + + signer.load_private_key(sign_key_path, password=pw) + except Exception: + msg = _( + "Error during sign operation: private key " + "could not be loaded." + ) + raise exceptions.CommandError(msg) + + signature = signer.generate_signature(fp) + signature_b64 = b64encode(signature) + kwargs['img_signature'] = signature_b64 + kwargs['img_signature_certificate_uuid'] = sign_cert_id + kwargs['img_signature_hash_method'] = signer.hash_method + if signer.padding_method: + kwargs['img_signature_key_type'] = signer.padding_method + + image = image_client.create_image(**kwargs) + return _format_image(image) + + def _take_action_volume(self, parsed_args): + volume_client = self.app.client_manager.volume + + unsupported_opts = { + # 'name', # 'name' is a positional argument and will always exist + 'id', + 'min_disk', + 'min_ram', + 'file', + 'force', + 'progress', + 'sign_key_path', + 'sign_cert_id', + 'properties', + 'tags', + 'project', + 'use_import', + } + for unsupported_opt in unsupported_opts: + if getattr(parsed_args, unsupported_opt, None): + opt_name = unsupported_opt.replace('-', '_') + if unsupported_opt == 'use_import': + opt_name = 'import' + msg = _( + "'--%s' was given, which is not supported when " + "creating an image from a volume. " + "This will be an error in a future version." + ) + # TODO(stephenfin): These should be an error in a future + # version + LOG.warning(msg % opt_name) + + source_volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ) + kwargs = {} + if volume_client.api_version < api_versions.APIVersion('3.1'): + if ( + parsed_args.visibility or + parsed_args.is_protected is not None + ): + msg = _( + '--os-volume-api-version 3.1 or greater is required ' + 'to support the --public, --private, --community, ' + '--shared or --protected option.' + ) + raise exceptions.CommandError(msg) else: - image = image_client.create_image(**kwargs) + kwargs.update( + visibility=parsed_args.visibility or 'private', + protected=parsed_args.is_protected or False, + ) + + response, body = volume_client.volumes.upload_to_image( + source_volume.id, + parsed_args.force, + parsed_args.name, + parsed_args.container_format, + parsed_args.disk_format, + **kwargs + ) + info = body['os-volume_upload_image'] + try: + info['volume_type'] = info['volume_type']['name'] + except TypeError: + info['volume_type'] = None + + return info + + def take_action(self, parsed_args): + for deadopt in self.deadopts: + if getattr(parsed_args, deadopt.replace('-', '_'), None): + msg = _( + "ERROR: --%s was given, which is an Image v1 option " + "that is no longer supported in Image v2" + ) + raise exceptions.CommandError(msg % deadopt) - if not info: - info = _format_image(image) + if parsed_args.volume: + info = self._take_action_volume(parsed_args) + else: + info = self._take_action_image(parsed_args) return zip(*sorted(info.items())) @@ -524,7 +637,7 @@ class DeleteImage(command.Command): _description = _("Delete image(s)") def get_parser(self, prog_name): - parser = super(DeleteImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "images", metavar="<image>", @@ -539,19 +652,24 @@ class DeleteImage(command.Command): image_client = self.app.client_manager.image for image in parsed_args.images: try: - image_obj = image_client.find_image(image, - ignore_missing=False) + image_obj = image_client.find_image( + image, ignore_missing=False + ) image_client.delete_image(image_obj.id) except Exception as e: del_result += 1 - LOG.error(_("Failed to delete image with name or " - "ID '%(image)s': %(e)s"), - {'image': image, 'e': e}) + msg = _( + "Failed to delete image with name or " + "ID '%(image)s': %(e)s" + ) + LOG.error(msg, {'image': image, 'e': e}) total = len(parsed_args.images) - if (del_result > 0): - msg = (_("Failed to delete %(dresult)s of %(total)s images.") - % {'dresult': del_result, 'total': total}) + if del_result > 0: + msg = _("Failed to delete %(dresult)s of %(total)s images.") % { + 'dresult': del_result, + 'total': total, + } raise exceptions.CommandError(msg) @@ -559,61 +677,63 @@ class ListImage(command.Lister): _description = _("List available images") def get_parser(self, prog_name): - parser = super(ListImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", - dest="public", - action="store_true", - default=False, + action="store_const", + const="public", + dest="visibility", help=_("List only public images"), ) public_group.add_argument( "--private", - dest="private", - action="store_true", - default=False, + action="store_const", + const="private", + dest="visibility", help=_("List only private images"), ) public_group.add_argument( "--community", - dest="community", - action="store_true", - default=False, + action="store_const", + const="community", + dest="visibility", help=_("List only community images"), ) public_group.add_argument( "--shared", - dest="shared", - action="store_true", - default=False, + action="store_const", + const="shared", + dest="visibility", help=_("List only shared images"), ) public_group.add_argument( "--all", - dest="all", - action="store_true", - default=False, + action="store_const", + const="all", + dest="visibility", help=_("List all images"), ) parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, - help=_('Filter output based on property ' - '(repeat option to filter on multiple properties)'), + help=_( + 'Filter output based on property ' + '(repeat option to filter on multiple properties)' + ), ) parser.add_argument( '--name', metavar='<name>', default=None, - help=_("Filter images based on name.") + help=_("Filter images based on name."), ) parser.add_argument( '--status', metavar='<status>', default=None, - help=_("Filter images based on status.") + help=_("Filter images based on status."), ) parser.add_argument( '--member-status', @@ -621,14 +741,18 @@ class ListImage(command.Lister): default=None, type=lambda s: s.lower(), choices=MEMBER_STATUS_CHOICES, - help=(_("Filter images based on member status. " - "The supported options are: %s. ") % - ', '.join(MEMBER_STATUS_CHOICES)) + help=( + _( + "Filter images based on member status. " + "The supported options are: %s. " + ) + % ', '.join(MEMBER_STATUS_CHOICES) + ), ) parser.add_argument( '--project', metavar='<project>', - help=_("Search by project (admin only) (name or ID)") + help=_("Search by project (admin only) (name or ID)"), ) common.add_project_domain_option_to_parser(parser) parser.add_argument( @@ -636,12 +760,15 @@ class ListImage(command.Lister): metavar='<tag>', action='append', default=[], - help=_('Filter images based on tag. ' - '(repeat option to filter on multiple tags)'), + help=_( + 'Filter images based on tag. ' + '(repeat option to filter on multiple tags)' + ), ) parser.add_argument( '--hidden', action='store_true', + dest='is_hidden', default=False, help=_('List hidden images'), ) @@ -663,9 +790,11 @@ class ListImage(command.Lister): '--sort', metavar="<key>[:<direction>]", default='name:asc', - help=_("Sort output by selected keys and directions(asc or desc) " - "(default: name:asc), multiple keys and directions can be " - "specified separated by comma"), + help=_( + "Sort output by selected keys and directions (asc or desc) " + "(default: name:asc), multiple keys and directions can be " + "specified separated by comma" + ), ) parser.add_argument( "--limit", @@ -677,9 +806,11 @@ class ListImage(command.Lister): '--marker', metavar='<image>', default=None, - help=_("The last image of the previous page. Display " - "list of images after marker. Display all images if not " - "specified. (name or ID)"), + help=_( + "The last image of the previous page. Display " + "list of images after marker. Display all images if not " + "specified. (name or ID)" + ), ) return parser @@ -688,16 +819,8 @@ class ListImage(command.Lister): image_client = self.app.client_manager.image kwargs = {} - if parsed_args.public: - kwargs['visibility'] = 'public' - if parsed_args.private: - kwargs['visibility'] = 'private' - if parsed_args.community: - kwargs['visibility'] = 'community' - if parsed_args.shared: - kwargs['visibility'] = 'shared' - if parsed_args.all: - kwargs['visibility'] = 'all' + if parsed_args.visibility is not None: + kwargs['visibility'] = parsed_args.visibility if parsed_args.limit: kwargs['limit'] = parsed_args.limit if parsed_args.marker: @@ -718,8 +841,8 @@ class ListImage(command.Lister): parsed_args.project_domain, ).id kwargs['owner'] = project_id - if parsed_args.hidden: - kwargs['is_hidden'] = True + if parsed_args.is_hidden: + kwargs['is_hidden'] = parsed_args.is_hidden if parsed_args.long: columns = ( 'ID', @@ -770,11 +893,14 @@ class ListImage(command.Lister): return ( column_headers, - (utils.get_item_properties( - s, - columns, - formatters=_formatters, - ) for s in data) + ( + utils.get_item_properties( + s, + columns, + formatters=_formatters, + ) + for s in data + ), ) @@ -782,7 +908,7 @@ class ListImageProjects(command.Lister): _description = _("List projects associated with image") def get_parser(self, prog_name): - parser = super(ListImageProjects, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "image", metavar="<image>", @@ -793,27 +919,29 @@ class ListImageProjects(command.Lister): def take_action(self, parsed_args): image_client = self.app.client_manager.image - columns = ( - "Image ID", - "Member ID", - "Status" - ) + columns = ("Image ID", "Member ID", "Status") image_id = image_client.find_image(parsed_args.image).id data = image_client.members(image=image_id) - return (columns, - (utils.get_item_properties( - s, columns, - ) for s in data)) + return ( + columns, + ( + utils.get_item_properties( + s, + columns, + ) + for s in data + ), + ) class RemoveProjectImage(command.Command): _description = _("Disassociate project with image") def get_parser(self, prog_name): - parser = super(RemoveProjectImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "image", metavar="<image>", @@ -831,23 +959,22 @@ class RemoveProjectImage(command.Command): image_client = self.app.client_manager.image identity_client = self.app.client_manager.identity - project_id = common.find_project(identity_client, - parsed_args.project, - parsed_args.project_domain).id + project_id = common.find_project( + identity_client, parsed_args.project, parsed_args.project_domain + ).id - image = image_client.find_image(parsed_args.image, - ignore_missing=False) + image = image_client.find_image( + parsed_args.image, ignore_missing=False + ) - image_client.remove_member( - member=project_id, - image=image.id) + image_client.remove_member(member=project_id, image=image.id) class SaveImage(command.Command): _description = _("Save an image locally") def get_parser(self, prog_name): - parser = super(SaveImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "--file", metavar="<filename>", @@ -877,7 +1004,7 @@ class SetImage(command.Command): deadopts = ('visibility',) def get_parser(self, prog_name): - parser = super(SetImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) # TODO(bunting): There are additional arguments that v1 supported # --size - does not exist in v2 # --store - does not exist in v2 @@ -889,20 +1016,16 @@ class SetImage(command.Command): # --checksum - maybe could be done client side # --stdin - could be implemented parser.add_argument( - "image", - metavar="<image>", - help=_("Image to modify (name or ID)") + "image", metavar="<image>", help=_("Image to modify (name or ID)") ) parser.add_argument( - "--name", - metavar="<name>", - help=_("New image name") + "--name", metavar="<name>", help=_("New image name") ) parser.add_argument( "--min-disk", type=int, metavar="<disk-gb>", - help=_("Minimum disk size needed to boot image, in gigabytes") + help=_("Minimum disk size needed to boot image, in gigabytes"), ) parser.add_argument( "--min-ram", @@ -914,46 +1037,58 @@ class SetImage(command.Command): "--container-format", metavar="<container-format>", choices=CONTAINER_CHOICES, - help=_("Image container format. The supported options are: %s") % - ', '.join(CONTAINER_CHOICES) + help=_("Image container format. The supported options are: %s") + % ', '.join(CONTAINER_CHOICES), ) parser.add_argument( "--disk-format", metavar="<disk-format>", choices=DISK_CHOICES, - help=_("Image disk format. The supported options are: %s") % - ', '.join(DISK_CHOICES) + help=_("Image disk format. The supported options are: %s") + % ', '.join(DISK_CHOICES), ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", + dest="is_protected", + default=None, help=_("Prevent image from being deleted"), ) protected_group.add_argument( "--unprotected", - action="store_true", + action="store_false", + dest="is_protected", + default=None, help=_("Allow image to be deleted (default)"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", - action="store_true", + action="store_const", + const="public", + dest="visibility", help=_("Image is accessible to the public"), ) public_group.add_argument( "--private", - action="store_true", + action="store_const", + const="private", + dest="visibility", help=_("Image is inaccessible to the public (default)"), ) public_group.add_argument( "--community", - action="store_true", + action="store_const", + const="community", + dest="visibility", help=_("Image is accessible to the community"), ) public_group.add_argument( "--shared", - action="store_true", + action="store_const", + const="shared", + dest="visibility", help=_("Image can be shared"), ) parser.add_argument( @@ -961,8 +1096,10 @@ class SetImage(command.Command): dest="properties", metavar="<key=value>", action=parseractions.KeyValueAction, - help=_("Set a property on this image " - "(repeat option to set multiple properties)"), + help=_( + "Set a property on this image " + "(repeat option to set multiple properties)" + ), ) parser.add_argument( "--tag", @@ -970,8 +1107,10 @@ class SetImage(command.Command): metavar="<tag>", default=None, action='append', - help=_("Set a tag on this image " - "(repeat option to set multiple tags)"), + help=_( + "Set a tag on this image " + "(repeat option to set multiple tags)" + ), ) parser.add_argument( "--architecture", @@ -1030,7 +1169,7 @@ class SetImage(command.Command): parser.add_argument( "--%s" % deadopt, metavar="<%s>" % deadopt, - dest=deadopt.replace('-', '_'), + dest=f"dead_{deadopt.replace('-', '_')}", help=argparse.SUPPRESS, ) @@ -1063,14 +1202,14 @@ class SetImage(command.Command): hidden_group = parser.add_mutually_exclusive_group() hidden_group.add_argument( "--hidden", - dest='hidden', + dest="is_hidden", default=None, action="store_true", help=_("Hide the image"), ) hidden_group.add_argument( "--unhidden", - dest='hidden', + dest="is_hidden", default=None, action="store_false", help=_("Unhide the image"), @@ -1082,13 +1221,18 @@ class SetImage(command.Command): image_client = self.app.client_manager.image for deadopt in self.deadopts: - if getattr(parsed_args, deadopt.replace('-', '_'), None): + if getattr(parsed_args, f"dead_{deadopt.replace('-', '_')}", None): raise exceptions.CommandError( - _("ERROR: --%s was given, which is an Image v1 option" - " that is no longer supported in Image v2") % deadopt) + _( + "ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2" + ) + % deadopt + ) image = image_client.find_image( - parsed_args.image, ignore_missing=False, + parsed_args.image, + ignore_missing=False, ) project_id = None if parsed_args.project: @@ -1125,10 +1269,25 @@ class SetImage(command.Command): # handle everything else kwargs = {} - copy_attrs = ('architecture', 'container_format', 'disk_format', - 'file', 'instance_id', 'kernel_id', 'locations', - 'min_disk', 'min_ram', 'name', 'os_distro', 'os_version', - 'prefix', 'progress', 'ramdisk_id', 'tags', 'visibility') + copy_attrs = ( + 'architecture', + 'container_format', + 'disk_format', + 'file', + 'instance_id', + 'kernel_id', + 'locations', + 'min_disk', + 'min_ram', + 'name', + 'os_distro', + 'os_version', + 'prefix', + 'progress', + 'ramdisk_id', + 'tags', + 'visibility', + ) for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -1148,33 +1307,31 @@ class SetImage(command.Command): # a single value for the pair of options because the default must be # to do nothing when no options are present as opposed to always # setting a default. - if parsed_args.protected: - kwargs['is_protected'] = True - if parsed_args.unprotected: - kwargs['is_protected'] = False - if parsed_args.public: - kwargs['visibility'] = 'public' - if parsed_args.private: - kwargs['visibility'] = 'private' - if parsed_args.community: - kwargs['visibility'] = 'community' - if parsed_args.shared: - kwargs['visibility'] = 'shared' + if parsed_args.is_protected is not None: + kwargs['is_protected'] = parsed_args.is_protected + + if parsed_args.visibility is not None: + kwargs['visibility'] = parsed_args.visibility + if parsed_args.project: # We already did the project lookup above kwargs['owner_id'] = project_id + if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) - if parsed_args.hidden is not None: - kwargs['is_hidden'] = parsed_args.hidden + + if parsed_args.is_hidden is not None: + kwargs['is_hidden'] = parsed_args.is_hidden try: image = image_client.update_image(image.id, **kwargs) except Exception: if activation_status is not None: - LOG.info(_("Image %(id)s was %(status)s."), - {'id': image.id, 'status': activation_status}) + LOG.info( + _("Image %(id)s was %(status)s."), + {'id': image.id, 'status': activation_status}, + ) raise @@ -1182,7 +1339,7 @@ class ShowImage(command.ShowOne): _description = _("Display image details") def get_parser(self, prog_name): - parser = super(ShowImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "--human-readable", default=False, @@ -1199,8 +1356,9 @@ class ShowImage(command.ShowOne): def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = image_client.find_image(parsed_args.image, - ignore_missing=False) + image = image_client.find_image( + parsed_args.image, ignore_missing=False + ) info = _format_image(image, parsed_args.human_readable) return zip(*sorted(info.items())) @@ -1210,7 +1368,7 @@ class UnsetImage(command.Command): _description = _("Unset image tags and properties") def get_parser(self, prog_name): - parser = super(UnsetImage, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( "image", metavar="<image>", @@ -1222,8 +1380,10 @@ class UnsetImage(command.Command): metavar="<tag>", default=[], action='append', - help=_("Unset a tag on this image " - "(repeat option to unset multiple tags)"), + help=_( + "Unset a tag on this image " + "(repeat option to unset multiple tags)" + ), ) parser.add_argument( "--property", @@ -1231,15 +1391,18 @@ class UnsetImage(command.Command): metavar="<property-key>", default=[], action='append', - help=_("Unset a property on this image " - "(repeat option to unset multiple properties)"), + help=_( + "Unset a property on this image " + "(repeat option to unset multiple properties)" + ), ) return parser def take_action(self, parsed_args): image_client = self.app.client_manager.image - image = image_client.find_image(parsed_args.image, - ignore_missing=False) + image = image_client.find_image( + parsed_args.image, ignore_missing=False + ) kwargs = {} tagret = 0 @@ -1249,8 +1412,9 @@ class UnsetImage(command.Command): try: image_client.remove_tag(image.id, k) except Exception: - LOG.error(_("tag unset failed, '%s' is a " - "nonexistent tag "), k) + LOG.error( + _("tag unset failed, '%s' is a " "nonexistent tag "), k + ) tagret += 1 if parsed_args.properties: @@ -1262,35 +1426,46 @@ class UnsetImage(command.Command): # pass modified properties object, so that SDK can figure # out, what was changed inside # NOTE: ping gtema to improve that in SDK - new_props = kwargs.get('properties', - image.get('properties').copy()) + new_props = kwargs.get( + 'properties', image.get('properties').copy() + ) new_props.pop(k, None) kwargs['properties'] = new_props else: - LOG.error(_("property unset failed, '%s' is a " - "nonexistent property "), k) + LOG.error( + _( + "property unset failed, '%s' is a " + "nonexistent property " + ), + k, + ) propret += 1 # We must give to update a current image for the reference on what # has changed - image_client.update_image( - image, - **kwargs) + image_client.update_image(image, **kwargs) tagtotal = len(parsed_args.tags) proptotal = len(parsed_args.properties) - if (tagret > 0 and propret > 0): - msg = (_("Failed to unset %(tagret)s of %(tagtotal)s tags," - "Failed to unset %(propret)s of %(proptotal)s properties.") - % {'tagret': tagret, 'tagtotal': tagtotal, - 'propret': propret, 'proptotal': proptotal}) + if tagret > 0 and propret > 0: + msg = _( + "Failed to unset %(tagret)s of %(tagtotal)s tags," + "Failed to unset %(propret)s of %(proptotal)s properties." + ) % { + 'tagret': tagret, + 'tagtotal': tagtotal, + 'propret': propret, + 'proptotal': proptotal, + } raise exceptions.CommandError(msg) elif tagret > 0: - msg = (_("Failed to unset %(tagret)s of %(tagtotal)s tags.") - % {'tagret': tagret, 'tagtotal': tagtotal}) + msg = _("Failed to unset %(tagret)s of %(tagtotal)s tags.") % { + 'tagret': tagret, + 'tagtotal': tagtotal, + } raise exceptions.CommandError(msg) elif propret > 0: - msg = (_("Failed to unset %(propret)s of %(proptotal)s" - " properties.") - % {'propret': propret, 'proptotal': proptotal}) + msg = _( + "Failed to unset %(propret)s of %(proptotal)s" " properties." + ) % {'propret': propret, 'proptotal': proptotal} raise exceptions.CommandError(msg) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 9241b0a4..02f7b411 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -32,7 +32,7 @@ from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes class TestImage(image_fakes.TestImagev2, volume_fakes.TestVolume): def setUp(self): - super(TestImage, self).setUp() + super().setUp() # Get shortcuts to mocked image client self.client = self.app.client_manager.image @@ -62,7 +62,7 @@ class TestImageCreate(TestImage): domain = identity_fakes.FakeDomain.create_one_domain() def setUp(self): - super(TestImageCreate, self).setUp() + super().setUp() self.new_image = image_fakes.create_one_image() self.client.create_image.return_value = self.new_image @@ -134,10 +134,8 @@ class TestImageCreate(TestImage): ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), - ('protected', self.new_image.is_protected), - ('unprotected', not self.new_image.is_protected), - ('public', self.new_image.visibility == 'public'), - ('private', self.new_image.visibility == 'private'), + ('is_protected', self.new_image.is_protected), + ('visibility', self.new_image.visibility), ('project', self.new_image.owner_id), ('project_domain', self.domain.id), ('name', self.new_image.name), @@ -188,10 +186,8 @@ class TestImageCreate(TestImage): ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), - ('protected', True), - ('unprotected', False), - ('public', False), - ('private', True), + ('is_protected', True), + ('visibility', 'private'), ('project', 'unexist_owner'), ('name', 'graven'), ] @@ -222,10 +218,8 @@ class TestImageCreate(TestImage): ] verifylist = [ ('file', imagefile.name), - ('protected', self.new_image.is_protected), - ('unprotected', not self.new_image.is_protected), - ('public', self.new_image.visibility == 'public'), - ('private', self.new_image.visibility == 'private'), + ('is_protected', self.new_image.is_protected), + ('visibility', self.new_image.visibility), ('properties', {'Alpha': '1', 'Beta': '2'}), ('tags', self.new_image.tags), ('name', self.new_image.name), @@ -421,7 +415,7 @@ class TestAddProjectToImage(TestImage): ) def setUp(self): - super(TestAddProjectToImage, self).setUp() + super().setUp() # This is the return value for utils.find_resource() self.client.find_image.return_value = self._image @@ -485,7 +479,7 @@ class TestAddProjectToImage(TestImage): class TestImageDelete(TestImage): def setUp(self): - super(TestImageDelete, self).setUp() + super().setUp() self.client.delete_image.return_value = None @@ -576,7 +570,7 @@ class TestImageList(TestImage): ), def setUp(self): - super(TestImageList, self).setUp() + super().setUp() self.client.images.side_effect = [[self._image], []] @@ -586,11 +580,7 @@ class TestImageList(TestImage): def test_image_list_no_options(self): arglist = [] verifylist = [ - ('public', False), - ('private', False), - ('community', False), - ('shared', False), - ('all', False), + ('visibility', None), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -611,11 +601,7 @@ class TestImageList(TestImage): '--public', ] verifylist = [ - ('public', True), - ('private', False), - ('community', False), - ('shared', False), - ('all', False), + ('visibility', 'public'), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -636,11 +622,7 @@ class TestImageList(TestImage): '--private', ] verifylist = [ - ('public', False), - ('private', True), - ('community', False), - ('shared', False), - ('all', False), + ('visibility', 'private'), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -661,11 +643,7 @@ class TestImageList(TestImage): '--community', ] verifylist = [ - ('public', False), - ('private', False), - ('community', True), - ('shared', False), - ('all', False), + ('visibility', 'community'), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -686,11 +664,7 @@ class TestImageList(TestImage): '--shared', ] verifylist = [ - ('public', False), - ('private', False), - ('community', False), - ('shared', True), - ('all', False), + ('visibility', 'shared'), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -711,11 +685,7 @@ class TestImageList(TestImage): '--all', ] verifylist = [ - ('public', False), - ('private', False), - ('community', False), - ('shared', False), - ('all', True), + ('visibility', 'all'), ('long', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -737,11 +707,7 @@ class TestImageList(TestImage): '--member-status', 'all' ] verifylist = [ - ('public', False), - ('private', False), - ('community', False), - ('shared', True), - ('all', False), + ('visibility', 'shared'), ('long', False), ('member_status', 'all') ] @@ -765,11 +731,7 @@ class TestImageList(TestImage): '--member-status', 'ALl' ] verifylist = [ - ('public', False), - ('private', False), - ('community', False), - ('shared', True), - ('all', False), + ('visibility', 'shared'), ('long', False), ('member_status', 'all') ] @@ -959,7 +921,7 @@ class TestImageList(TestImage): '--hidden', ] verifylist = [ - ('hidden', True), + ('is_hidden', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1006,7 +968,7 @@ class TestListImageProjects(TestImage): )] def setUp(self): - super(TestListImageProjects, self).setUp() + super().setUp() self.client.find_image.return_value = self._image self.client.members.return_value = [self.member] @@ -1036,7 +998,7 @@ class TestRemoveProjectImage(TestImage): domain = identity_fakes.FakeDomain.create_one_domain() def setUp(self): - super(TestRemoveProjectImage, self).setUp() + super().setUp() self._image = image_fakes.create_one_image() # This is the return value for utils.find_resource() @@ -1100,7 +1062,7 @@ class TestImageSet(TestImage): _image = image_fakes.create_one_image({'tags': []}) def setUp(self): - super(TestImageSet, self).setUp() + super().setUp() self.project_mock.get.return_value = self.project @@ -1282,10 +1244,8 @@ class TestImageSet(TestImage): 'graven', ] verifylist = [ - ('protected', True), - ('unprotected', False), - ('public', False), - ('private', True), + ('is_protected', True), + ('visibility', 'private'), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1310,10 +1270,8 @@ class TestImageSet(TestImage): 'graven', ] verifylist = [ - ('protected', False), - ('unprotected', True), - ('public', True), - ('private', False), + ('is_protected', False), + ('visibility', 'public'), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1532,7 +1490,7 @@ class TestImageSet(TestImage): 'graven', ] verifylist = [ - ('visibility', '1-mile'), + ('dead_visibility', '1-mile'), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1574,9 +1532,8 @@ class TestImageSet(TestImage): 'graven', ] verifylist = [ - ('hidden', True), - ('public', True), - ('private', False), + ('is_hidden', True), + ('visibility', 'public'), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1601,9 +1558,8 @@ class TestImageSet(TestImage): 'graven', ] verifylist = [ - ('hidden', False), - ('public', True), - ('private', False), + ('is_hidden', False), + ('visibility', 'public'), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1643,7 +1599,7 @@ class TestImageShow(TestImage): ) def setUp(self): - super(TestImageShow, self).setUp() + super().setUp() self.client.find_image = mock.Mock(return_value=self._data) @@ -1699,7 +1655,7 @@ class TestImageShow(TestImage): class TestImageUnset(TestImage): def setUp(self): - super(TestImageUnset, self).setUp() + super().setUp() attrs = {} attrs['tags'] = ['test'] @@ -1798,7 +1754,7 @@ class TestImageSave(TestImage): image = image_fakes.create_one_image({}) def setUp(self): - super(TestImageSave, self).setUp() + super().setUp() self.client.find_image.return_value = self.image self.client.download_image.return_value = self.image @@ -1827,7 +1783,7 @@ class TestImageSave(TestImage): class TestImageGetData(TestImage): def setUp(self): - super(TestImageGetData, self).setUp() + super().setUp() self.args = mock.Mock() def test_get_data_file_file(self): |
