bda6517c97
Bringing in the Endpoint Filter Group blueprint into Openstack client. Endpoint filter groups have been around in Keystone since the days of KILO but only picked up by Openstack client in v3.15. In the absence of Endpoint filter groups, clients are responsible for filtering the endpoint list, instead of Keystone sending a tailored list. For e.g. in Distributed Cloud, local Openstack services in the Central Region should not be seeing endpoints for subclouds, as they don't directly communicate with subclouds. As part of this blueprint we also had to bring in an important commit that Optimizes the command "openstack endpoint list". The current behavior is to make a Service call for each Endpoint in the Endpoint list, so for N endpoints yo have N + 1 calls to Keystone. The new behavior makes 1 call for Endpoint list, and 1 call for Service list and works with the two lists... so you do down from (N+1) calls to 2 Change-Id: I9c03938c25b56d64b59ce42cae5026f2830f02b7 Signed-off-by: Tyler Smith <tyler.smith@windriver.com>
473 lines
18 KiB
Diff
473 lines
18 KiB
Diff
From d800b1821e4aa3e3e49173be6c5b1ea370200d96 Mon Sep 17 00:00:00 2001
|
|
From: Jose Castro Leon <jose.castro.leon@cern.ch>
|
|
Date: Wed, 25 Oct 2017 15:39:44 +0200
|
|
Subject: [PATCH] Add support for endpoint group commands
|
|
|
|
Implements the commands for endpoint group filter management.
|
|
Includes the CRUD management of the endpoint groups and the
|
|
association management between them and the projects that are
|
|
using this method.
|
|
|
|
Implements: blueprint keystone-endpoint-filter
|
|
Change-Id: I4265f7f8598d028191e90d76781b7b6ece6fef64
|
|
|
|
Signed-off-by: Kam Nasim <kam.nasim@windriver.com>
|
|
---
|
|
doc/source/cli/command-objects/endpoint_group.rst | 28 ++
|
|
doc/source/cli/commands.rst | 1 +
|
|
openstackclient/identity/v3/endpoint_group.py | 324 +++++++++++++++++++++
|
|
openstackclient/tests/unit/identity/v3/fakes.py | 16 +
|
|
.../keystone-endpoint-group-0c55debbb66844f2.yaml | 7 +
|
|
setup.cfg | 9 +
|
|
6 files changed, 385 insertions(+)
|
|
create mode 100644 doc/source/cli/command-objects/endpoint_group.rst
|
|
create mode 100644 openstackclient/identity/v3/endpoint_group.py
|
|
create mode 100644 releasenotes/notes/keystone-endpoint-group-0c55debbb66844f2.yaml
|
|
|
|
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 0000000..ccfe5f6
|
|
--- /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 5a7977e..50a6f6e 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 0000000..e254973
|
|
--- /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='<endpoint-group>',
|
|
+ help=_('Endpoint group (name or ID)'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ 'project',
|
|
+ metavar='<project>',
|
|
+ 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='<name>',
|
|
+ help=_('Name of the endpoint group'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ 'filters',
|
|
+ metavar='<filename>',
|
|
+ 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='<endpoint-group>',
|
|
+ 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='<endpoint-group>',
|
|
+ help=_('Endpoint Group (name or ID)'),
|
|
+ )
|
|
+ list_group.add_argument(
|
|
+ '--project',
|
|
+ metavar='<project>',
|
|
+ help=_('Project (name or ID)'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ '--domain',
|
|
+ metavar='<domain>',
|
|
+ help=_('Domain owning <project> (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='<endpoint-group>',
|
|
+ help=_('Endpoint group (name or ID)'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ 'project',
|
|
+ metavar='<project>',
|
|
+ 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='<endpoint-group>',
|
|
+ help=_('Endpoint Group to modify (name or ID)'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ '--name',
|
|
+ metavar='<name>',
|
|
+ help=_('New enpoint group name'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ '--filters',
|
|
+ metavar='<filename>',
|
|
+ help=_('Filename that contains a new set of filters'),
|
|
+ )
|
|
+ parser.add_argument(
|
|
+ '--description',
|
|
+ metavar='<description>',
|
|
+ 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='<endpointgroup>',
|
|
+ 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 549a1aa..76431b1 100644
|
|
--- a/openstackclient/tests/unit/identity/v3/fakes.py
|
|
+++ b/openstackclient/tests/unit/identity/v3/fakes.py
|
|
@@ -221,6 +221,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'
|
|
@@ -493,6 +507,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 0000000..dc3c5be
|
|
--- /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 <https://blueprints.launchpad.net/python-openstackclient/+spec/keystone-endpoint-filter>`_]
|
|
diff --git a/setup.cfg b/setup.cfg
|
|
index 5f9c04a..d87b387 100644
|
|
--- a/setup.cfg
|
|
+++ b/setup.cfg
|
|
@@ -202,6 +202,15 @@ openstack.identity.v3 =
|
|
endpoint_remove_project = openstackclient.identity.v3.endpoint:RemoveProjectFromEndpoint
|
|
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
|
|
--
|
|
1.8.3.1
|
|
|