diff --git a/openstackclient/image/v2/metadef_namespaces.py b/openstackclient/image/v2/metadef_namespaces.py index 158fd94e3..f09f20024 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="", + help=_("New metadef namespace name"), + ) + parser.add_argument( + "--display-name", + metavar="", + help=_("A user-friendly name for the namespace."), + ) + parser.add_argument( + "--description", + metavar="", + 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="", + 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="", + help=_("Namespace (name) for the namespace"), + ) + parser.add_argument( + "--display-name", + metavar="", + help=_("Set a user-friendly name for the namespace."), + ) + parser.add_argument( + "--description", + metavar="", + 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="", + 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())) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index ded9ff313..8ddd9a099 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -239,7 +239,11 @@ def create_tasks(attrs=None, count=2): class FakeMetadefNamespaceClient: def __init__(self, **kwargs): + self.create_metadef_namespace = mock.Mock() + self.delete_metadef_namespace = mock.Mock() self.metadef_namespaces = mock.Mock() + self.get_metadef_namespace = mock.Mock() + self.update_metadef_namespace = mock.Mock() self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -277,10 +281,11 @@ def create_one_metadef_namespace(attrs=None): 'display_name': 'Flavor Quota', 'namespace': 'OS::Compute::Quota', 'owner': 'admin', - 'resource_type_associations': ['OS::Nova::Flavor'], + # 'resource_type_associations': ['OS::Nova::Flavor'], + # The part that receives the list type factor is not implemented. 'visibility': 'public', } # Overwrite default attributes if there are some attributes set metadef_namespace_list.update(attrs) - return metadef_namespace.MetadefNamespace(metadef_namespace_list) + return metadef_namespace.MetadefNamespace(**metadef_namespace_list) diff --git a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py index 5eae289cd..7ed118386 100644 --- a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py +++ b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py @@ -30,8 +30,89 @@ class TestMetadefNamespaces(md_namespace_fakes.TestMetadefNamespaces): self.domain_mock.reset_mock() -class TestMetadefNamespaceList(TestMetadefNamespaces): +class TestMetadefNamespaceCreate(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + expected_columns = ( + 'created_at', + 'description', + 'display_name', + 'id', + 'is_protected', + 'location', + 'name', + 'namespace', + 'owner', + 'resource_type_associations', + 'updated_at', + 'visibility' + ) + expected_data = ( + _metadef_namespace.created_at, + _metadef_namespace.description, + _metadef_namespace.display_name, + _metadef_namespace.id, + _metadef_namespace.is_protected, + _metadef_namespace.location, + _metadef_namespace.name, + _metadef_namespace.namespace, + _metadef_namespace.owner, + _metadef_namespace.resource_type_associations, + _metadef_namespace.updated_at, + _metadef_namespace.visibility + ) + + def setUp(self): + super().setUp() + + self.client.create_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.CreateMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_create(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + +class TestMetadefNamespaceDelete(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + def setUp(self): + super().setUp() + + self.client.delete_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.DeleteMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_create(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + + +class TestMetadefNamespaceList(TestMetadefNamespaces): _metadef_namespace = [md_namespace_fakes.create_one_metadef_namespace()] columns = [ @@ -65,3 +146,70 @@ class TestMetadefNamespaceList(TestMetadefNamespaces): self.assertEqual(self.columns, columns) self.assertEqual(getattr(self.datalist[0], 'namespace'), next(data)[0]) + + +class TestMetadefNamespaceSet(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + def setUp(self): + super().setUp() + + self.client.update_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.SetMetadefNameSpace(self.app, None) + self.datalist = self._metadef_namespace + + def test_namespace_set_no_options(self): + arglist = [ + self._metadef_namespace.namespace + ] + verifylist = [ + ('namespace', self._metadef_namespace.namespace), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + + +class TestMetadefNamespaceShow(TestMetadefNamespaces): + _metadef_namespace = md_namespace_fakes.create_one_metadef_namespace() + + expected_columns = ( + 'created_at', + 'display_name', + 'namespace', + 'owner', + 'visibility' + ) + expected_data = ( + _metadef_namespace.created_at, + _metadef_namespace.display_name, + _metadef_namespace.namespace, + _metadef_namespace.owner, + _metadef_namespace.visibility + ) + + def setUp(self): + super().setUp() + + self.client.get_metadef_namespace.return_value \ + = self._metadef_namespace + self.cmd = metadef_namespaces.ShowMetadefNameSpace(self.app, None) + + def test_namespace_show_no_options(self): + arglist = [ + self._metadef_namespace.namespace + ] + + verifylist = [ + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) diff --git a/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml b/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml new file mode 100644 index 000000000..361e57fe7 --- /dev/null +++ b/releasenotes/notes/image-metadef-namespace-b940206bece64f97.yaml @@ -0,0 +1,10 @@ +--- +features: + - Add ``openstack image metadef namespace create`` command + to create metadef namespace for the image service. + - Add ``openstack image metadef namespace delete`` command + to delete image metadef namespace. + - Add ``openstack image metadef namespace set`` command + to update metadef namespace for the image service. + - Add ``openstack image metadef namespace show`` command + to show metadef namespace for the image service. diff --git a/setup.cfg b/setup.cfg index fa3d30fe3..42ce970b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -386,7 +386,12 @@ openstack.image.v2 = image_stage = openstackclient.image.v2.image:StageImage image_task_show = openstackclient.image.v2.task:ShowTask image_task_list = openstackclient.image.v2.task:ListTask + + image_metadef_namespace_create = openstackclient.image.v2.metadef_namespaces:CreateMetadefNameSpace + image_metadef_namespace_delete = openstackclient.image.v2.metadef_namespaces:DeleteMetadefNameSpace image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNameSpaces + image_metadef_namespace_set = openstackclient.image.v2.metadef_namespaces:SetMetadefNameSpace + image_metadef_namespace_show = openstackclient.image.v2.metadef_namespaces:ShowMetadefNameSpace openstack.network.v2 = address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup