diff --git a/doc/source/cli/command-objects/endpoint_group.rst b/doc/source/cli/command-objects/endpoint_group.rst new file mode 100644 index 000000000..ccfe5f661 --- /dev/null +++ b/doc/source/cli/command-objects/endpoint_group.rst @@ -0,0 +1,28 @@ +============== +endpoint group +============== + +A **endpoint group** is used to create groups of endpoints that then +can be used to filter the endpoints that are available to a project. +Applicable to Identity v3 + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group add project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group remove project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group set + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group show diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 0c1992ad2..d840549c3 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -91,6 +91,7 @@ referring to both Compute and Volume quotas. * ``domain``: (**Identity**) a grouping of projects * ``ec2 credentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service +* ``endpoint group``: (**Identity**) group endpoints to be used as filters * ``extension``: (**Compute**, **Identity**, **Network**, **Volume**) OpenStack server API extensions * ``federation protocol``: (**Identity**) the underlying protocol used while federating identities * ``flavor``: (**Compute**) predefined server configurations: ram, root disk and so on diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py new file mode 100644 index 000000000..e254973bb --- /dev/null +++ b/openstackclient/identity/v3/endpoint_group.py @@ -0,0 +1,324 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Identity v3 Endpoint Group action implementations""" + +import json +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common + + +LOG = logging.getLogger(__name__) + + +class _FiltersReader(object): + _description = _("Helper class capable of reading filters from files") + + def _read_filters(self, path): + """Read and parse rules from path + + Expect the file to contain a valid JSON structure. + + :param path: path to the file + :return: loaded and valid dictionary with filters + :raises exception.CommandError: In case the file cannot be + accessed or the content is not a valid JSON. + + Example of the content of the file: + { + "interface": "admin", + "service_id": "1b501a" + } + """ + blob = utils.read_blob_file_contents(path) + try: + rules = json.loads(blob) + except ValueError as e: + msg = _("An error occurred when reading filters from file " + "%(path)s: %(error)s") % {"path": path, "error": e} + raise exceptions.CommandError(msg) + else: + return rules + + +class AddProjectToEndpointGroup(command.Command): + _description = _("Add a project to an endpoint group") + + def get_parser(self, prog_name): + parser = super( + AddProjectToEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpointgroup', + metavar='', + help=_('Endpoint group (name or ID)'), + ) + parser.add_argument( + 'project', + metavar='', + help=_('Project to associate (name or ID)'), + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.identity + + endpointgroup = utils.find_resource(client.endpoint_groups, + parsed_args.endpointgroup) + + project = common.find_project(client, + parsed_args.project, + parsed_args.project_domain) + + client.endpoint_filter.add_endpoint_group_to_project( + endpoint_group=endpointgroup.id, + project=project.id) + + +class CreateEndpointGroup(command.ShowOne, _FiltersReader): + _description = _("Create new endpoint group") + + def get_parser(self, prog_name): + parser = super(CreateEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the endpoint group'), + ) + parser.add_argument( + 'filters', + metavar='', + help=_('Filename that contains a new set of filters'), + ) + parser.add_argument( + '--description', + help=_('Description of the endpoint group'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + filters = None + if parsed_args.filters: + filters = self._read_filters(parsed_args.filters) + + endpoint_group = identity_client.endpoint_groups.create( + name=parsed_args.name, + filters=filters, + description=parsed_args.description + ) + + info = {} + endpoint_group._info.pop('links') + info.update(endpoint_group._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteEndpointGroup(command.Command): + _description = _("Delete endpoint group(s)") + + def get_parser(self, prog_name): + parser = super(DeleteEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpointgroup', + metavar='', + nargs='+', + help=_('Endpoint group(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + result = 0 + for i in parsed_args.endpointgroup: + try: + endpoint_id = utils.find_resource( + identity_client.endpoint_groups, i).id + identity_client.endpoint_groups.delete(endpoint_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete endpoint group with " + "ID '%(endpointgroup)s': %(e)s"), + {'endpointgroup': i, 'e': e}) + + if result > 0: + total = len(parsed_args.endpointgroup) + msg = (_("%(result)s of %(total)s endpointgroups failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListEndpointGroup(command.Lister): + _description = _("List endpoint groups") + + def get_parser(self, prog_name): + parser = super(ListEndpointGroup, self).get_parser(prog_name) + list_group = parser.add_mutually_exclusive_group() + list_group.add_argument( + '--endpointgroup', + metavar='', + help=_('Endpoint Group (name or ID)'), + ) + list_group.add_argument( + '--project', + metavar='', + help=_('Project (name or ID)'), + ) + parser.add_argument( + '--domain', + metavar='', + help=_('Domain owning (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.identity + + endpointgroup = None + if parsed_args.endpointgroup: + endpointgroup = utils.find_resource(client.endpoint_groups, + parsed_args.endpointgroup) + project = None + if parsed_args.project: + project = common.find_project(client, + parsed_args.project, + parsed_args.domain) + + if endpointgroup: + # List projects associated to the endpoint group + columns = ('ID', 'Name') + data = client.endpoint_filter.list_projects_for_endpoint_group( + endpoint_group=endpointgroup.id) + elif project: + columns = ('ID', 'Name') + data = client.endpoint_filter.list_endpoint_groups_for_project( + project=project.id) + else: + columns = ('ID', 'Name', 'Description') + data = client.endpoint_groups.list() + + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class RemoveProjectFromEndpointGroup(command.Command): + _description = _("Remove project from endpoint group") + + def get_parser(self, prog_name): + parser = super( + RemoveProjectFromEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpointgroup', + metavar='', + help=_('Endpoint group (name or ID)'), + ) + parser.add_argument( + 'project', + metavar='', + help=_('Project to remove (name or ID)'), + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.identity + + endpointgroup = utils.find_resource(client.endpoint_groups, + parsed_args.endpointgroup) + + project = common.find_project(client, + parsed_args.project, + parsed_args.project_domain) + + client.endpoint_filter.delete_endpoint_group_to_project( + endpoint_group=endpointgroup.id, + project=project.id) + + +class SetEndpointGroup(command.Command, _FiltersReader): + _description = _("Set endpoint group properties") + + def get_parser(self, prog_name): + parser = super(SetEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpointgroup', + metavar='', + help=_('Endpoint Group to modify (name or ID)'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('New enpoint group name'), + ) + parser.add_argument( + '--filters', + metavar='', + help=_('Filename that contains a new set of filters'), + ) + parser.add_argument( + '--description', + metavar='', + default='', + help=_('New endpoint group description'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + endpointgroup = utils.find_resource(identity_client.endpoint_groups, + parsed_args.endpointgroup) + + filters = None + if parsed_args.filters: + filters = self._read_filters(parsed_args.filters) + + identity_client.endpoint_groups.update( + endpointgroup.id, + name=parsed_args.name, + filters=filters, + description=parsed_args.description + ) + + +class ShowEndpointGroup(command.ShowOne): + _description = _("Display endpoint group details") + + def get_parser(self, prog_name): + parser = super(ShowEndpointGroup, self).get_parser(prog_name) + parser.add_argument( + 'endpointgroup', + metavar='', + help=_('Endpoint group (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + endpoint_group = utils.find_resource(identity_client.endpoint_groups, + parsed_args.endpointgroup) + + info = {} + endpoint_group._info.pop('links') + info.update(endpoint_group._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 3e2caf01d..68e38e523 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -223,6 +223,20 @@ ENDPOINT = { 'links': base_url + 'endpoints/' + endpoint_id, } +endpoint_group_id = 'eg-123' +endpoint_group_description = 'eg 123 description' +endpoint_group_filters = { + 'service_id': service_id, + 'region_id': endpoint_region, +} + +ENDPOINT_GROUP = { + 'id': endpoint_group_id, + 'filters': endpoint_group_filters, + 'description': endpoint_group_description, + 'links': base_url + 'endpoint_groups/' + endpoint_group_id, +} + user_id = 'bbbbbbb-aaaa-aaaa-aaaa-bbbbbbbaaaa' user_name = 'paul' user_description = 'Sir Paul' @@ -495,6 +509,8 @@ class FakeIdentityv3Client(object): self.endpoints.resource_class = fakes.FakeResource(None, {}) self.endpoint_filter = mock.Mock() self.endpoint_filter.resource_class = fakes.FakeResource(None, {}) + self.endpoint_groups = mock.Mock() + self.endpoint_groups.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() self.groups.resource_class = fakes.FakeResource(None, {}) self.oauth1 = mock.Mock() diff --git a/releasenotes/notes/keystone-endpoint-group-0c55debbb66844f2.yaml b/releasenotes/notes/keystone-endpoint-group-0c55debbb66844f2.yaml new file mode 100644 index 000000000..dc3c5be65 --- /dev/null +++ b/releasenotes/notes/keystone-endpoint-group-0c55debbb66844f2.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add endpoint group commands: ``endpoint group add project``, ``endpoint group create``, + ``endpoint group delete``, ``endpoint group list``, ``endpoint group remove project``, + ``endpoint group set`` and ``endpoint group show``. + [Blueprint `keystone-endpoint-filter `_] diff --git a/setup.cfg b/setup.cfg index 63bfdafb1..df062e362 100644 --- a/setup.cfg +++ b/setup.cfg @@ -236,6 +236,14 @@ openstack.identity.v3 = endpoint_set = openstackclient.identity.v3.endpoint:SetEndpoint endpoint_show = openstackclient.identity.v3.endpoint:ShowEndpoint + endpoint_group_add_project = openstackclient.identity.v3.endpoint_group:AddProjectToEndpointGroup + endpoint_group_create = openstackclient.identity.v3.endpoint_group:CreateEndpointGroup + endpoint_group_delete = openstackclient.identity.v3.endpoint_group:DeleteEndpointGroup + endpoint_group_list = openstackclient.identity.v3.endpoint_group:ListEndpointGroup + endpoint_group_remove_project = openstackclient.identity.v3.endpoint_group:RemoveProjectFromEndpointGroup + endpoint_group_set = openstackclient.identity.v3.endpoint_group:SetEndpointGroup + endpoint_group_show = openstackclient.identity.v3.endpoint_group:ShowEndpointGroup + group_add_user = openstackclient.identity.v3.group:AddUserToGroup group_contains_user = openstackclient.identity.v3.group:CheckUserInGroup group_create = openstackclient.identity.v3.group:CreateGroup