summaryrefslogtreecommitdiff
path: root/openstackclient/image
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient/image')
-rw-r--r--openstackclient/image/v2/image.py493
-rw-r--r--openstackclient/image/v2/metadef_namespaces.py247
2 files changed, 670 insertions, 70 deletions
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 2342fd3e..71dcc731 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -22,6 +22,7 @@ import os
import sys
from cinderclient import api_versions
+from openstack import exceptions as sdk_exceptions
from openstack.image import image_signer
from osc_lib.api import utils as api_utils
from osc_lib.cli import format_columns
@@ -29,11 +30,10 @@ from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
-from oslo_utils import uuidutils
from openstackclient.common import progressbar
from openstackclient.i18n import _
-from openstackclient.identity import common
+from openstackclient.identity import common as identity_common
if os.name == "nt":
import msvcrt
@@ -134,34 +134,32 @@ def _get_member_columns(item):
)
-def get_data_file(args):
- if args.file:
- return (open(args.file, 'rb'), args.file)
+def get_data_from_stdin():
+ # distinguish cases where:
+ # (1) stdin is not valid (as in cron jobs):
+ # openstack ... <&-
+ # (2) image data is provided through stdin:
+ # openstack ... < /tmp/file
+ # (3) no image data provided
+ # openstack ...
+ try:
+ os.fstat(0)
+ except OSError:
+ # (1) stdin is not valid
+ return None
+
+ if not sys.stdin.isatty():
+ # (2) image data is provided through stdin
+ image = sys.stdin
+ if hasattr(sys.stdin, 'buffer'):
+ image = sys.stdin.buffer
+ if msvcrt:
+ msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+
+ return image
else:
- # distinguish cases where:
- # (1) stdin is not valid (as in cron jobs):
- # openstack ... <&-
- # (2) image data is provided through stdin:
- # openstack ... < /tmp/file
- # (3) no image data provided
- # openstack ...
- try:
- os.fstat(0)
- except OSError:
- # (1) stdin is not valid
- return (None, None)
- if not sys.stdin.isatty():
- # (2) image data is provided through stdin
- image = sys.stdin
- if hasattr(sys.stdin, 'buffer'):
- image = sys.stdin.buffer
- if msvcrt:
- msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
-
- return (image, None)
- else:
- # (3)
- return (None, None)
+ # (3)
+ return None
class AddProjectToImage(command.ShowOne):
@@ -179,24 +177,22 @@ class AddProjectToImage(command.ShowOne):
metavar="<project>",
help=_("Project to associate with image (ID)"),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
identity_client = self.app.client_manager.identity
- if uuidutils.is_uuid_like(parsed_args.project):
- project_id = parsed_args.project
- else:
- project_id = common.find_project(
- identity_client,
- parsed_args.project,
- parsed_args.project_domain,
- ).id
+ project_id = identity_common.find_project(
+ identity_client,
+ parsed_args.project,
+ parsed_args.project_domain,
+ ).id
image = image_client.find_image(
- parsed_args.image, ignore_missing=False
+ parsed_args.image,
+ ignore_missing=False,
)
obj = image_client.add_member(
@@ -277,6 +273,7 @@ class CreateImage(command.ShowOne):
source_group = parser.add_mutually_exclusive_group()
source_group.add_argument(
"--file",
+ dest="filename",
metavar="<file>",
help=_("Upload image from local file"),
)
@@ -299,7 +296,10 @@ class CreateImage(command.ShowOne):
"--progress",
action="store_true",
default=False,
- help=_("Show upload progress bar."),
+ help=_(
+ "Show upload progress bar "
+ "(ignored if passing data via stdin)"
+ ),
)
parser.add_argument(
'--sign-key-path',
@@ -398,7 +398,7 @@ class CreateImage(command.ShowOne):
"Force the use of glance image import instead of direct upload"
),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
for deadopt in self.deadopts:
parser.add_argument(
"--%s" % deadopt,
@@ -451,7 +451,7 @@ class CreateImage(command.ShowOne):
kwargs['visibility'] = parsed_args.visibility
if parsed_args.project:
- kwargs['owner_id'] = common.find_project(
+ kwargs['owner_id'] = identity_common.find_project(
identity_client,
parsed_args.project,
parsed_args.project_domain,
@@ -463,7 +463,15 @@ 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)
+ if parsed_args.filename:
+ try:
+ fp = open(parsed_args.filename, 'rb')
+ except FileNotFoundError:
+ raise exceptions.CommandError(
+ '%r is not a valid file' % parsed_args.filename,
+ )
+ else:
+ fp = get_data_from_stdin()
if fp is not None and parsed_args.volume:
msg = _(
@@ -472,24 +480,24 @@ class CreateImage(command.ShowOne):
)
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 parsed_args.progress and parsed_args.filename:
+ # NOTE(stephenfin): we only show a progress bar if the user
+ # requested it *and* we're reading from a file (not stdin)
+ filesize = os.path.getsize(parsed_args.filename)
if filesize is not None:
kwargs['validate_checksum'] = False
kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize)
- elif fname:
- kwargs['filename'] = fname
+ else:
+ kwargs['data'] = fp
+ elif parsed_args.filename:
+ kwargs['filename'] = parsed_args.filename
elif fp:
kwargs['validate_checksum'] = False
kwargs['data'] = fp
# 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:
+ if not parsed_args.filename:
msg = _(
"signing an image requires the --file option, "
"passing files via stdin when signing is not "
@@ -544,6 +552,10 @@ class CreateImage(command.ShowOne):
kwargs['img_signature_key_type'] = signer.padding_method
image = image_client.create_image(**kwargs)
+
+ if parsed_args.filename:
+ fp.close()
+
return _format_image(image)
def _take_action_volume(self, parsed_args):
@@ -653,7 +665,8 @@ class DeleteImage(command.Command):
for image in parsed_args.images:
try:
image_obj = image_client.find_image(
- image, ignore_missing=False
+ image,
+ ignore_missing=False,
)
image_client.delete_image(image_obj.id)
except Exception as e:
@@ -754,7 +767,7 @@ class ListImage(command.Lister):
metavar='<project>',
help=_("Search by project (admin only) (name or ID)"),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
parser.add_argument(
'--tag',
metavar='<tag>',
@@ -824,7 +837,10 @@ class ListImage(command.Lister):
if parsed_args.limit:
kwargs['limit'] = parsed_args.limit
if parsed_args.marker:
- kwargs['marker'] = image_client.find_image(parsed_args.marker).id
+ kwargs['marker'] = image_client.find_image(
+ parsed_args.marker,
+ ignore_missing=False,
+ ).id
if parsed_args.name:
kwargs['name'] = parsed_args.name
if parsed_args.status:
@@ -835,7 +851,7 @@ class ListImage(command.Lister):
kwargs['tag'] = parsed_args.tag
project_id = None
if parsed_args.project:
- project_id = common.find_project(
+ project_id = identity_common.find_project(
identity_client,
parsed_args.project,
parsed_args.project_domain,
@@ -914,14 +930,17 @@ class ListImageProjects(command.Lister):
metavar="<image>",
help=_("Image (name or ID)"),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
return parser
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
columns = ("Image ID", "Member ID", "Status")
- image_id = image_client.find_image(parsed_args.image).id
+ image_id = image_client.find_image(
+ parsed_args.image,
+ ignore_missing=False,
+ ).id
data = image_client.members(image=image_id)
@@ -952,19 +971,22 @@ class RemoveProjectImage(command.Command):
metavar="<project>",
help=_("Project to disassociate with image (name or ID)"),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
return parser
def take_action(self, parsed_args):
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
+ project_id = identity_common.find_project(
+ identity_client,
+ parsed_args.project,
+ parsed_args.project_domain,
).id
image = image_client.find_image(
- parsed_args.image, ignore_missing=False
+ parsed_args.image,
+ ignore_missing=False,
)
image_client.remove_member(member=project_id, image=image.id)
@@ -978,6 +1000,7 @@ class SaveImage(command.Command):
parser.add_argument(
"--file",
metavar="<filename>",
+ dest="filename",
help=_("Downloaded image save filename (default: stdout)"),
)
parser.add_argument(
@@ -989,9 +1012,12 @@ class SaveImage(command.Command):
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
- image = image_client.find_image(parsed_args.image)
+ image = image_client.find_image(
+ parsed_args.image,
+ ignore_missing=False,
+ )
- output_file = parsed_args.file
+ output_file = parsed_args.filename
if output_file is None:
output_file = getattr(sys.stdout, "buffer", sys.stdout)
@@ -1164,7 +1190,7 @@ class SetImage(command.Command):
metavar="<project>",
help=_("Set an alternate project on this image (name or ID)"),
)
- common.add_project_domain_option_to_parser(parser)
+ identity_common.add_project_domain_option_to_parser(parser)
for deadopt in self.deadopts:
parser.add_argument(
"--%s" % deadopt,
@@ -1180,7 +1206,10 @@ class SetImage(command.Command):
const="accepted",
dest="membership",
default=None,
- help=_("Accept the image membership"),
+ help=_(
+ "Accept the image membership for either the project indicated "
+ "by '--project', if provided, or the current user's project"
+ ),
)
membership_group.add_argument(
"--reject",
@@ -1188,7 +1217,10 @@ class SetImage(command.Command):
const="rejected",
dest="membership",
default=None,
- help=_("Reject the image membership"),
+ help=_(
+ "Reject the image membership for either the project indicated "
+ "by '--project', if provided, or the current user's project"
+ ),
)
membership_group.add_argument(
"--pending",
@@ -1236,7 +1268,7 @@ class SetImage(command.Command):
)
project_id = None
if parsed_args.project:
- project_id = common.find_project(
+ project_id = identity_common.find_project(
identity_client,
parsed_args.project,
parsed_args.project_domain,
@@ -1357,7 +1389,8 @@ class ShowImage(command.ShowOne):
image_client = self.app.client_manager.image
image = image_client.find_image(
- parsed_args.image, ignore_missing=False
+ parsed_args.image,
+ ignore_missing=False,
)
info = _format_image(image, parsed_args.human_readable)
@@ -1401,7 +1434,8 @@ class UnsetImage(command.Command):
def take_action(self, parsed_args):
image_client = self.app.client_manager.image
image = image_client.find_image(
- parsed_args.image, ignore_missing=False
+ parsed_args.image,
+ ignore_missing=False,
)
kwargs = {}
@@ -1469,3 +1503,322 @@ class UnsetImage(command.Command):
"Failed to unset %(propret)s of %(proptotal)s" " properties."
) % {'propret': propret, 'proptotal': proptotal}
raise exceptions.CommandError(msg)
+
+
+class StageImage(command.Command):
+ _description = _(
+ "Upload data for a specific image to staging.\n"
+ "This requires support for the interoperable image import process, "
+ "which was first introduced in Image API version 2.6 "
+ "(Glance 16.0.0 (Queens))"
+ )
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+
+ parser.add_argument(
+ '--file',
+ metavar='<file>',
+ dest='filename',
+ help=_(
+ 'Local file that contains disk image to be uploaded. '
+ 'Alternatively, images can be passed via stdin.'
+ ),
+ )
+ # NOTE(stephenfin): glanceclient had a --size argument but it didn't do
+ # anything so we have chosen not to port this
+ parser.add_argument(
+ '--progress',
+ action='store_true',
+ default=False,
+ help=_(
+ 'Show upload progress bar '
+ '(ignored if passing data via stdin)'
+ ),
+ )
+ parser.add_argument(
+ 'image',
+ metavar='<image>',
+ help=_('Image to upload data for (name or ID)'),
+ )
+
+ 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,
+ )
+ # 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.
+ if parsed_args.filename:
+ try:
+ fp = open(parsed_args.filename, 'rb')
+ except FileNotFoundError:
+ raise exceptions.CommandError(
+ '%r is not a valid file' % parsed_args.filename,
+ )
+ else:
+ fp = get_data_from_stdin()
+
+ kwargs = {}
+
+ if parsed_args.progress and parsed_args.filename:
+ # NOTE(stephenfin): we only show a progress bar if the user
+ # requested it *and* we're reading from a file (not stdin)
+ filesize = os.path.getsize(parsed_args.filename)
+ if filesize is not None:
+ kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize)
+ else:
+ kwargs['data'] = fp
+ elif parsed_args.filename:
+ kwargs['filename'] = parsed_args.filename
+ elif fp:
+ kwargs['data'] = fp
+
+ image_client.stage_image(image, **kwargs)
+
+
+class ImportImage(command.ShowOne):
+ _description = _(
+ "Initiate the image import process.\n"
+ "This requires support for the interoperable image import process, "
+ "which was first introduced in Image API version 2.6 "
+ "(Glance 16.0.0 (Queens))"
+ )
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+
+ parser.add_argument(
+ 'image',
+ metavar='<image>',
+ help=_('Image to initiate import process for (name or ID)'),
+ )
+ # TODO(stephenfin): Uncomment help text when we have this command
+ # implemented
+ parser.add_argument(
+ '--method',
+ metavar='<method>',
+ default='glance-direct',
+ dest='import_method',
+ choices=[
+ 'glance-direct',
+ 'web-download',
+ 'glance-download',
+ 'copy-image',
+ ],
+ help=_(
+ "Import method used for image import process. "
+ "Not all deployments will support all methods. "
+ # "Valid values can be retrieved with the 'image import "
+ # "methods' command. "
+ "The 'glance-direct' method (default) requires images be "
+ "first staged using the 'image-stage' command."
+ ),
+ )
+ parser.add_argument(
+ '--uri',
+ metavar='<uri>',
+ help=_(
+ "URI to download the external image "
+ "(only valid with the 'web-download' import method)"
+ ),
+ )
+ parser.add_argument(
+ '--remote-image',
+ metavar='<REMOTE_IMAGE>',
+ help=_(
+ "The image of remote glance (ID only) to be imported "
+ "(only valid with the 'glance-download' import method)"
+ ),
+ )
+ parser.add_argument(
+ '--remote-region',
+ metavar='<REMOTE_GLANCE_REGION>',
+ help=_(
+ "The remote Glance region to download the image from "
+ "(only valid with the 'glance-download' import method)"
+ ),
+ )
+ parser.add_argument(
+ '--remote-service-interface',
+ metavar='<REMOTE_SERVICE_INTERFACE>',
+ help=_(
+ "The remote Glance service interface to use when importing "
+ "images "
+ "(only valid with the 'glance-download' import method)"
+ ),
+ )
+ stores_group = parser.add_mutually_exclusive_group()
+ stores_group.add_argument(
+ '--store',
+ metavar='<STORE>',
+ dest='stores',
+ nargs='*',
+ help=_(
+ "Backend store to upload image to "
+ "(specify multiple times to upload to multiple stores) "
+ "(either '--store' or '--all-stores' required with the "
+ "'copy-image' import method)"
+ ),
+ )
+ stores_group.add_argument(
+ '--all-stores',
+ help=_(
+ "Make image available to all stores "
+ "(either '--store' or '--all-stores' required with the "
+ "'copy-image' import method)"
+ ),
+ )
+ parser.add_argument(
+ '--allow-failure',
+ action='store_true',
+ dest='allow_failure',
+ default=True,
+ help=_(
+ 'When uploading to multiple stores, indicate that the import '
+ 'should be continue should any of the uploads fail. '
+ 'Only usable with --stores or --all-stores'
+ ),
+ )
+ parser.add_argument(
+ '--disallow-failure',
+ action='store_true',
+ dest='allow_failure',
+ default=True,
+ help=_(
+ 'When uploading to multiple stores, indicate that the import '
+ 'should be reverted should any of the uploads fail. '
+ 'Only usable with --stores or --all-stores'
+ ),
+ )
+ parser.add_argument(
+ '--wait',
+ action='store_true',
+ help=_('Wait for operation to complete'),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ image_client = self.app.client_manager.image
+
+ try:
+ import_info = image_client.get_import_info()
+ except sdk_exceptions.ResourceNotFound:
+ msg = _(
+ 'The Image Import feature is not supported by this deployment'
+ )
+ raise exceptions.CommandError(msg)
+
+ import_methods = import_info.import_methods['value']
+
+ if parsed_args.import_method not in import_methods:
+ msg = _(
+ "The '%s' import method is not supported by this deployment. "
+ "Supported: %s"
+ )
+ raise exceptions.CommandError(
+ msg % (parsed_args.import_method, ', '.join(import_methods)),
+ )
+
+ if parsed_args.import_method == 'web-download':
+ if not parsed_args.uri:
+ msg = _(
+ "The '--uri' option is required when using "
+ "'--method=web-download'"
+ )
+ raise exceptions.CommandError(msg)
+ else:
+ if parsed_args.uri:
+ msg = _(
+ "The '--uri' option is only supported when using "
+ "'--method=web-download'"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.import_method == 'glance-download':
+ if not (parsed_args.remote_region and parsed_args.remote_image):
+ msg = _(
+ "The '--remote-region' and '--remote-image' options are "
+ "required when using '--method=web-download'"
+ )
+ raise exceptions.CommandError(msg)
+ else:
+ if parsed_args.remote_region:
+ msg = _(
+ "The '--remote-region' option is only supported when "
+ "using '--method=glance-download'"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.remote_image:
+ msg = _(
+ "The '--remote-image' option is only supported when using "
+ "'--method=glance-download'"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.remote_service_interface:
+ msg = _(
+ "The '--remote-service-interface' option is only "
+ "supported when using '--method=glance-download'"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.import_method == 'copy-image':
+ if not (parsed_args.stores or parsed_args.all_stores):
+ msg = _(
+ "The '--stores' or '--all-stores' options are required "
+ "when using '--method=copy-image'"
+ )
+ raise exceptions.CommandError(msg)
+
+ image = image_client.find_image(parsed_args.image)
+
+ if not image.container_format and not image.disk_format:
+ msg = _(
+ "The 'container_format' and 'disk_format' properties "
+ "must be set on an image before it can be imported"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.import_method == 'glance-direct':
+ if image.status != 'uploading':
+ msg = _(
+ "The 'glance-direct' import method can only be used with "
+ "an image in status 'uploading'"
+ )
+ raise exceptions.CommandError(msg)
+ elif parsed_args.import_method == 'web-download':
+ if image.status != 'queued':
+ msg = _(
+ "The 'web-download' import method can only be used with "
+ "an image in status 'queued'"
+ )
+ raise exceptions.CommandError(msg)
+ elif parsed_args.import_method == 'copy-image':
+ if image.status != 'active':
+ msg = _(
+ "The 'copy-image' import method can only be used with "
+ "an image in status 'active'"
+ )
+ raise exceptions.CommandError(msg)
+
+ image_client.import_image(
+ image,
+ method=parsed_args.import_method,
+ uri=parsed_args.uri,
+ remote_region=parsed_args.remote_region,
+ remote_image=parsed_args.remote_image,
+ remote_service_interface=parsed_args.remote_service_interface,
+ stores=parsed_args.stores,
+ all_stores=parsed_args.all_stores,
+ all_stores_must_succeed=not parsed_args.allow_failure,
+ )
+
+ info = _format_image(image)
+ return zip(*sorted(info.items()))
diff --git a/openstackclient/image/v2/metadef_namespaces.py b/openstackclient/image/v2/metadef_namespaces.py
index 158fd94e..f09f2002 100644
--- a/openstackclient/image/v2/metadef_namespaces.py
+++ b/openstackclient/image/v2/metadef_namespaces.py
@@ -15,8 +15,11 @@
"""Image V2 Action Implementations"""
+import logging
+
from osc_lib.cli import format_columns
from osc_lib.command import command
+from osc_lib import exceptions
from osc_lib import utils
from openstackclient.i18n import _
@@ -25,6 +28,149 @@ _formatters = {
'tags': format_columns.ListColumn,
}
+LOG = logging.getLogger(__name__)
+
+
+def _format_namespace(namespace):
+ info = {}
+
+ fields_to_show = [
+ 'created_at',
+ 'description',
+ 'display_name',
+ 'namespace',
+ 'owner',
+ 'protected',
+ 'schema',
+ 'visibility',
+ ]
+
+ namespace = namespace.to_dict(ignore_none=True, original_names=True)
+
+ # split out the usual key and the properties which are top-level
+ for key in namespace:
+ if key in fields_to_show:
+ info[key] = namespace.get(key)
+ elif key == "resource_type_associations":
+ info[key] = [resource_type['name']
+ for resource_type in namespace.get(key)]
+ elif key == 'properties':
+ info['properties'] = list(namespace.get(key).keys())
+
+ return info
+
+
+class CreateMetadefNameSpace(command.ShowOne):
+ _description = _("Create a metadef namespace")
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ "namespace",
+ metavar="<namespace>",
+ help=_("New metadef namespace name"),
+ )
+ parser.add_argument(
+ "--display-name",
+ metavar="<display_name>",
+ help=_("A user-friendly name for the namespace."),
+ )
+ parser.add_argument(
+ "--description",
+ metavar="<description>",
+ help=_("A description of the namespace"),
+ )
+ visibility_group = parser.add_mutually_exclusive_group()
+ visibility_group.add_argument(
+ "--public",
+ action="store_const",
+ const="public",
+ dest="visibility",
+ help=_("Set namespace visibility 'public'"),
+ )
+ visibility_group.add_argument(
+ "--private",
+ action="store_const",
+ const="private",
+ dest="visibility",
+ help=_("Set namespace visibility 'private'"),
+ )
+ protected_group = parser.add_mutually_exclusive_group()
+ protected_group.add_argument(
+ "--protected",
+ action="store_const",
+ const=True,
+ dest="is_protected",
+ help=_("Prevent metadef namespace from being deleted"),
+ )
+ protected_group.add_argument(
+ "--unprotected",
+ action="store_const",
+ const=False,
+ dest="is_protected",
+ help=_("Allow metadef namespace to be deleted (default)"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ image_client = self.app.client_manager.image
+ filter_keys = [
+ 'namespace',
+ 'display_name',
+ 'description'
+ ]
+ kwargs = {}
+
+ for key in filter_keys:
+ argument = getattr(parsed_args, key, None)
+ if argument is not None:
+ kwargs[key] = argument
+
+ if parsed_args.is_protected is not None:
+ kwargs['protected'] = parsed_args.is_protected
+
+ if parsed_args.visibility is not None:
+ kwargs['visibility'] = parsed_args.visibility
+
+ data = image_client.create_metadef_namespace(**kwargs)
+
+ return zip(*sorted(data.items()))
+
+
+class DeleteMetadefNameSpace(command.Command):
+ _description = _("Delete metadef namespace")
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ "namespace_name",
+ metavar="<namespace_name>",
+ nargs="+",
+ help=_("An identifier (a name) for the namespace"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ image_client = self.app.client_manager.image
+
+ result = 0
+ for i in parsed_args.namespace_name:
+ try:
+ namespace = image_client.get_metadef_namespace(i)
+ image_client.delete_metadef_namespace(namespace.id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete namespace with name or "
+ "ID '%(namespace)s': %(e)s"),
+ {'namespace': i, 'e': e}
+ )
+
+ if result > 0:
+ total = len(parsed_args.namespace_name)
+ msg = (_("%(result)s of %(total)s namespace failed "
+ "to delete.") % {'result': result, 'total': total})
+ raise exceptions.CommandError(msg)
+
class ListMetadefNameSpaces(command.Lister):
_description = _("List metadef namespaces")
@@ -63,3 +209,104 @@ class ListMetadefNameSpaces(command.Lister):
formatters=_formatters,
) for s in data)
)
+
+
+class SetMetadefNameSpace(command.Command):
+ _description = _("Set metadef namespace properties")
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ "namespace",
+ metavar="<namespace>",
+ help=_("Namespace (name) for the namespace"),
+ )
+ parser.add_argument(
+ "--display-name",
+ metavar="<display_name>",
+ help=_("Set a user-friendly name for the namespace."),
+ )
+ parser.add_argument(
+ "--description",
+ metavar="<description>",
+ help=_("Set the description of the namespace"),
+ )
+ visibility_group = parser.add_mutually_exclusive_group()
+ visibility_group.add_argument(
+ "--public",
+ action="store_const",
+ const="public",
+ dest="visibility",
+ help=_("Set namespace visibility 'public'"),
+ )
+ visibility_group.add_argument(
+ "--private",
+ action="store_const",
+ const="private",
+ dest="visibility",
+ help=_("Set namespace visibility 'private'"),
+ )
+ protected_group = parser.add_mutually_exclusive_group()
+ protected_group.add_argument(
+ "--protected",
+ action="store_const",
+ const=True,
+ dest="is_protected",
+ help=_("Prevent metadef namespace from being deleted"),
+ )
+ protected_group.add_argument(
+ "--unprotected",
+ action="store_const",
+ const=False,
+ dest="is_protected",
+ help=_("Allow metadef namespace to be deleted (default)"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ image_client = self.app.client_manager.image
+
+ namespace = parsed_args.namespace
+
+ filter_keys = [
+ 'namespace',
+ 'display_name',
+ 'description'
+ ]
+ kwargs = {}
+
+ for key in filter_keys:
+ argument = getattr(parsed_args, key, None)
+ if argument is not None:
+ kwargs[key] = argument
+
+ if parsed_args.is_protected is not None:
+ kwargs['protected'] = parsed_args.is_protected
+
+ if parsed_args.visibility is not None:
+ kwargs['visibility'] = parsed_args.visibility
+
+ image_client.update_metadef_namespace(namespace, **kwargs)
+
+
+class ShowMetadefNameSpace(command.ShowOne):
+ _description = _("Show a metadef namespace")
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ "namespace_name",
+ metavar="<namespace_name>",
+ help=_("Namespace (name) for the namespace"),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ image_client = self.app.client_manager.image
+
+ namespace_name = parsed_args.namespace_name
+
+ data = image_client.get_metadef_namespace(namespace_name)
+ info = _format_namespace(data)
+
+ return zip(*sorted(info.items()))