Distributed Keystone for Distributed Cloud - Endpoint filters

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>
This commit is contained in:
Kam Nasim 2018-05-24 11:46:50 -04:00 committed by Tyler Smith
parent bc7abb4399
commit bda6517c97
6 changed files with 1214 additions and 1 deletions

View File

@ -1 +1 @@
TIS_PATCH_VER=16
TIS_PATCH_VER=17

View File

@ -0,0 +1,27 @@
From ab8fc2b85ab7b60bdfeca496a32e90bc8f575478 Mon Sep 17 00:00:00 2001
From: Kam Nasim <kam.nasim@windriver.com>
Date: Fri, 11 May 2018 13:13:15 -0400
Subject: [PATCH] meta patch for endpoint groups
Signed-off-by: Kam Nasim <kam.nasim@windriver.com>
---
SPECS/python-openstackclient.spec | 3 +++
1 file changed, 3 insertions(+)
diff --git a/SPECS/python-openstackclient.spec b/SPECS/python-openstackclient.spec
index 5d75107..26941e4 100644
--- a/SPECS/python-openstackclient.spec
+++ b/SPECS/python-openstackclient.spec
@@ -27,6 +27,9 @@ Patch0006: 0002-US101470-Openstackclient-implementation-of-novaclien.patc
Patch0007: 0001-US106901-Openstack-CLI-Adoption.patch
Patch0008: 0002-US106901-Openstack-CLI-Adoption.patch
Patch0009: 0003-US106901-Openstack-CLI-Adoption.patch
+Patch0010: 0001-Optimize-getting-endpoint-list.patch
+Patch0011: 0002-Add-support-for-endpoing-filter-commands.patch
+Patch0012: 0003-Add-support-for-endpoint-group-commands.patch
BuildArch: noarch
--
1.8.3.1

View File

@ -14,3 +14,4 @@
0002-meta-US106901-Openstack-CLI-Adoption.patch
0003-meta-US106901-Openstack-CLI-Adoption.patch
1002-require-python-ceilometerclient.patch
1003-meta-patch-for-endpoint-groups.patch

View File

@ -0,0 +1,99 @@
From f6f5ce03c5b8a03180db24a02dda5b30f40b4cee Mon Sep 17 00:00:00 2001
From: Anton Frolov <af9740@att.com>
Date: Mon, 25 Sep 2017 12:31:24 -0700
Subject: [PATCH] Optimize getting endpoint list
Currently ListEndpoint.take_action method unconditionally iterates
over all endpoints and issue GET /v3/services/<ep.service_id>
request for each endpoint. In case of HTTPS keystone endpoint this
can take significant amout of time, and it only getting worse in
case of multiple regions.
This commit change this logic to making just two GET requests: first
it gets endpoint list, then it gets service list, searching service
in the list instead of issuing GET /v3/services/<id> request.
Change-Id: I22b61c0b45b0205a2f5a4608c2473cb7814fe3cf
Closes-Bug: 1719413
---
openstackclient/identity/common.py | 10 ++++++++++
openstackclient/identity/v3/endpoint.py | 3 ++-
openstackclient/tests/unit/identity/v3/test_endpoint.py | 2 ++
releasenotes/notes/bug-1719413-0401d05c91cc9094.yaml | 8 ++++++++
4 files changed, 22 insertions(+), 1 deletion(-)
create mode 100644 releasenotes/notes/bug-1719413-0401d05c91cc9094.yaml
diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py
index 3dc5adb..e119f66 100644
--- a/openstackclient/identity/common.py
+++ b/openstackclient/identity/common.py
@@ -26,6 +26,16 @@ from osc_lib import utils
from openstackclient.i18n import _
+def find_service_in_list(service_list, service_id):
+ """Find a service by id in service list."""
+
+ for service in service_list:
+ if service.id == service_id:
+ return service
+ raise exceptions.CommandError(
+ "No service with a type, name or ID of '%s' exists." % service_id)
+
+
def find_service(identity_client, name_type_or_id):
"""Find a service by id, name or type."""
diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py
index 15760a1..3b4dd0d 100644
--- a/openstackclient/identity/v3/endpoint.py
+++ b/openstackclient/identity/v3/endpoint.py
@@ -167,9 +167,10 @@ class ListEndpoint(command.Lister):
if parsed_args.region:
kwargs['region'] = parsed_args.region
data = identity_client.endpoints.list(**kwargs)
+ service_list = identity_client.services.list()
for ep in data:
- service = common.find_service(identity_client, ep.service_id)
+ service = common.find_service_in_list(service_list, ep.service_id)
ep.service_name = get_service_name(service)
ep.service_type = service.type
return (columns,
diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint.py b/openstackclient/tests/unit/identity/v3/test_endpoint.py
index 765fbed..fad53fc 100644
--- a/openstackclient/tests/unit/identity/v3/test_endpoint.py
+++ b/openstackclient/tests/unit/identity/v3/test_endpoint.py
@@ -295,6 +295,7 @@ class TestEndpointList(TestEndpoint):
# This is the return value for common.find_resource(service)
self.services_mock.get.return_value = self.service
+ self.services_mock.list.return_value = [self.service]
# Get the command object to test
self.cmd = endpoint.ListEndpoint(self.app, None)
@@ -726,6 +727,7 @@ class TestEndpointListServiceWithoutName(TestEndpointList):
# This is the return value for common.find_resource(service)
self.services_mock.get.return_value = self.service
+ self.services_mock.list.return_value = [self.service]
# Get the command object to test
self.cmd = endpoint.ListEndpoint(self.app, None)
diff --git a/releasenotes/notes/bug-1719413-0401d05c91cc9094.yaml b/releasenotes/notes/bug-1719413-0401d05c91cc9094.yaml
new file mode 100644
index 0000000..784d19e
--- /dev/null
+++ b/releasenotes/notes/bug-1719413-0401d05c91cc9094.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+ - |
+ Fix an issue with ``endpoint list`` working slow because it is issuing one GET
+ request to /v3/services/<id> Keystone API for each endpoint. In case of HTTPS
+ keystone endpoint and multiple regions it can take significant amount of time.
+ [Bug `1719413 <https://bugs.launchpad.net/python-openstackclient/+bug/1719413>`_]
+
--
1.8.3.1

View File

@ -0,0 +1,614 @@
From 8d106e1f1b3e536127818e98e495343e3c85f6b1 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 endpoing filter commands
Implements the commands that allow to link and endpoint to
a project for endpoint filter management.
Implements: blueprint keystone-endpoint-filter
Change-Id: Iecf61495664fb8413d35ef69f07ea929d190d002
Signed-off-by: Kam Nasim <kam.nasim@windriver.com>
---
doc/source/cli/command-objects/endpoint.rst | 79 +++++++++++
openstackclient/identity/v3/endpoint.py | 147 ++++++++++++++++++---
.../tests/functional/identity/v3/common.py | 1 +
.../tests/functional/identity/v3/test_endpoint.py | 42 ++++++
openstackclient/tests/unit/identity/v3/fakes.py | 27 ++++
.../tests/unit/identity/v3/test_endpoint.py | 139 +++++++++++++++++++
.../keystone-endpoint-filter-e930a7b72276fa2c.yaml | 5 +
setup.cfg | 9 +-
8 files changed, 429 insertions(+), 20 deletions(-)
create mode 100644 releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml
diff --git a/doc/source/cli/command-objects/endpoint.rst b/doc/source/cli/command-objects/endpoint.rst
index 02a75be..030947c 100644
--- a/doc/source/cli/command-objects/endpoint.rst
+++ b/doc/source/cli/command-objects/endpoint.rst
@@ -4,6 +4,34 @@ endpoint
Identity v2, v3
+endpoint add project
+--------------------
+
+Associate a project to and endpoint for endpoint filtering
+
+.. program:: endpoint add project
+.. code:: bash
+
+ openstack endpoint add project
+ [--project-domain <project-domain>]
+ <endpoint>
+ <project>
+
+.. option:: --project-domain <project-domain>
+
+ Domain the project belongs to (name or ID).
+ This can be used in case collisions between project names exist.
+
+.. _endpoint_add_project-endpoint:
+.. describe:: <endpoint>
+
+ Endpoint to associate with specified project (name or ID)
+
+.. _endpoint_add_project-project:
+.. describe:: <project>
+
+ Project to associate with specified endpoint (name or ID)
+
endpoint create
---------------
@@ -107,6 +135,8 @@ List endpoints
[--interface <interface>]
[--region <region-id>]
[--long]
+ [--endpoint <endpoint> |
+ --project <project> [--project-domain <project-domain>]]
.. option:: --service <service>
@@ -132,6 +162,55 @@ List endpoints
*Identity version 2 only*
+.. option:: --endpoint
+
+ List projects that have access to that endpoint using
+ endpoint filtering
+
+ *Identity version 3 only*
+
+.. option:: --project
+
+ List endpoints available for the project using
+ endpoint filtering
+
+ *Identity version 3 only*
+
+.. option:: --project-domain
+
+ Domain the project belongs to (name or ID).
+ This can be used in case collisions between project names exist.
+
+ *Identity version 3 only*
+
+endpoint remove project
+-----------------------
+
+Dissociate a project from an endpoint.
+
+.. program:: endpoint remove project
+.. code:: bash
+
+ openstack endpoint remove project
+ [--project-domain <project-domain>]
+ <endpoint>
+ <project>
+
+.. option:: --project-domain <project-domain>
+
+ Domain the project belongs to (name or ID).
+ This can be used in case collisions between project names exist.
+
+.. _endpoint_remove_project-endpoint:
+.. describe:: <endpoint>
+
+ Endpoint to dissociate with specified project (name or ID)
+
+.. _endpoint_remove_project-project:
+.. describe:: <project>
+
+ Project to dissociate with specified endpoint (name or ID)
+
endpoint set
------------
diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py
index 3b4dd0d..649a230 100644
--- a/openstackclient/identity/v3/endpoint.py
+++ b/openstackclient/identity/v3/endpoint.py
@@ -36,6 +36,42 @@ def get_service_name(service):
return ''
+class AddProjectToEndpoint(command.Command):
+ _description = _("Associate a project to an endpoint")
+
+ def get_parser(self, prog_name):
+ parser = super(
+ AddProjectToEndpoint, self).get_parser(prog_name)
+ parser.add_argument(
+ 'endpoint',
+ metavar='<endpoint>',
+ help=_('Endpoint to associate with '
+ 'specified project (name or ID)'),
+ )
+ parser.add_argument(
+ 'project',
+ metavar='<project>',
+ help=_('Project to associate with '
+ 'specified endpoint 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
+
+ endpoint = utils.find_resource(client.endpoints,
+ parsed_args.endpoint)
+
+ project = common.find_project(client,
+ parsed_args.project,
+ parsed_args.project_domain)
+
+ client.endpoint_filter.add_endpoint_to_project(
+ project=project.id,
+ endpoint=endpoint.id)
+
+
class CreateEndpoint(command.ShowOne):
_description = _("Create new endpoint")
@@ -152,27 +188,68 @@ class ListEndpoint(command.Lister):
metavar='<region-id>',
help=_('Filter by region ID'),
)
+ list_group = parser.add_mutually_exclusive_group()
+ list_group.add_argument(
+ '--endpoint',
+ metavar='<endpoint-group>',
+ help=_('Endpoint to list filters'),
+ )
+ list_group.add_argument(
+ '--project',
+ metavar='<project>',
+ help=_('Project to list filters (name or ID)'),
+ )
+ common.add_project_domain_option_to_parser(list_group)
return parser
def take_action(self, parsed_args):
identity_client = self.app.client_manager.identity
- columns = ('ID', 'Region', 'Service Name', 'Service Type',
- 'Enabled', 'Interface', 'URL')
- kwargs = {}
- if parsed_args.service:
- service = common.find_service(identity_client, parsed_args.service)
- kwargs['service'] = service.id
- if parsed_args.interface:
- kwargs['interface'] = parsed_args.interface
- if parsed_args.region:
- kwargs['region'] = parsed_args.region
- data = identity_client.endpoints.list(**kwargs)
- service_list = identity_client.services.list()
-
- for ep in data:
- service = common.find_service_in_list(service_list, ep.service_id)
- ep.service_name = get_service_name(service)
- ep.service_type = service.type
+
+ endpoint = None
+ if parsed_args.endpoint:
+ endpoint = utils.find_resource(identity_client.endpoints,
+ parsed_args.endpoint)
+ project = None
+ if parsed_args.project:
+ project = common.find_project(identity_client,
+ parsed_args.project,
+ parsed_args.project_domain)
+
+ if endpoint:
+ columns = ('ID', 'Name')
+ data = (
+ identity_client.endpoint_filter
+ .list_projects_for_endpoint(endpoint=endpoint.id)
+ )
+ else:
+ columns = ('ID', 'Region', 'Service Name', 'Service Type',
+ 'Enabled', 'Interface', 'URL')
+ kwargs = {}
+ if parsed_args.service:
+ service = common.find_service(identity_client,
+ parsed_args.service)
+ kwargs['service'] = service.id
+ if parsed_args.interface:
+ kwargs['interface'] = parsed_args.interface
+ if parsed_args.region:
+ kwargs['region'] = parsed_args.region
+
+ if project:
+ data = (
+ identity_client.endpoint_filter
+ .list_endpoints_for_project(project=project.id)
+ )
+ else:
+ data = identity_client.endpoints.list(**kwargs)
+
+ service_list = identity_client.services.list()
+
+ for ep in data:
+ service = common.find_service_in_list(service_list,
+ ep.service_id)
+ ep.service_name = get_service_name(service)
+ ep.service_type = service.type
+
return (columns,
(utils.get_item_properties(
s, columns,
@@ -180,6 +257,42 @@ class ListEndpoint(command.Lister):
) for s in data))
+class RemoveProjectFromEndpoint(command.Command):
+ _description = _("Dissociate a project from an endpoint")
+
+ def get_parser(self, prog_name):
+ parser = super(
+ RemoveProjectFromEndpoint, self).get_parser(prog_name)
+ parser.add_argument(
+ 'endpoint',
+ metavar='<endpoint>',
+ help=_('Endpoint to dissociate from '
+ 'specified project (name or ID)'),
+ )
+ parser.add_argument(
+ 'project',
+ metavar='<project>',
+ help=_('Project to dissociate from '
+ 'specified endpoint 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
+
+ endpoint = utils.find_resource(client.endpoints,
+ parsed_args.endpoint)
+
+ project = common.find_project(client,
+ parsed_args.project,
+ parsed_args.project_domain)
+
+ client.endpoint_filter.delete_endpoint_from_project(
+ project=project.id,
+ endpoint=endpoint.id)
+
+
class SetEndpoint(command.Command):
_description = _("Set endpoint properties")
diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py
index 6d7896d..33cb5d8 100644
--- a/openstackclient/tests/functional/identity/v3/common.py
+++ b/openstackclient/tests/functional/identity/v3/common.py
@@ -42,6 +42,7 @@ class IdentityTests(base.TestCase):
REGION_LIST_HEADERS = ['Region', 'Parent Region', 'Description']
ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type',
'Enabled', 'Interface', 'URL']
+ ENDPOINT_LIST_PROJECT_HEADERS = ['ID', 'Name']
IDENTITY_PROVIDER_FIELDS = ['description', 'enabled', 'id', 'remote_ids',
'domain_id']
diff --git a/openstackclient/tests/functional/identity/v3/test_endpoint.py b/openstackclient/tests/functional/identity/v3/test_endpoint.py
index 22dc1b6..41f0b4c 100644
--- a/openstackclient/tests/functional/identity/v3/test_endpoint.py
+++ b/openstackclient/tests/functional/identity/v3/test_endpoint.py
@@ -42,6 +42,29 @@ class EndpointTests(common.IdentityTests):
items = self.parse_listing(raw_output)
self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS)
+ def test_endpoint_list_filter(self):
+ endpoint_id = self._create_dummy_endpoint(add_clean_up=False)
+ project_id = self._create_dummy_project(add_clean_up=False)
+ raw_output = self.openstack(
+ 'endpoint add project '
+ '%(endpoint_id)s '
+ '%(project_id)s' % {
+ 'project_id': project_id,
+ 'endpoint_id': endpoint_id})
+ self.assertEqual(0, len(raw_output))
+ raw_output = self.openstack(
+ 'endpoint list --endpoint %s' % endpoint_id)
+ self.assertIn(project_id, raw_output)
+ items = self.parse_listing(raw_output)
+ self.assert_table_structure(items,
+ self.ENDPOINT_LIST_PROJECT_HEADERS)
+
+ raw_output = self.openstack(
+ 'endpoint list --project %s' % project_id)
+ self.assertIn(endpoint_id, raw_output)
+ items = self.parse_listing(raw_output)
+ self.assert_table_structure(items, self.ENDPOINT_LIST_HEADERS)
+
def test_endpoint_set(self):
endpoint_id = self._create_dummy_endpoint()
new_endpoint_url = data_utils.rand_url()
@@ -65,3 +88,22 @@ class EndpointTests(common.IdentityTests):
raw_output = self.openstack('endpoint show %s' % endpoint_id)
items = self.parse_show(raw_output)
self.assert_show_fields(items, self.ENDPOINT_FIELDS)
+
+ def test_endpoint_add_remove_project(self):
+ endpoint_id = self._create_dummy_endpoint(add_clean_up=False)
+ project_id = self._create_dummy_project(add_clean_up=False)
+ raw_output = self.openstack(
+ 'endpoint add project '
+ '%(endpoint_id)s '
+ '%(project_id)s' % {
+ 'project_id': project_id,
+ 'endpoint_id': endpoint_id})
+ self.assertEqual(0, len(raw_output))
+
+ raw_output = self.openstack(
+ 'endpoint remove project '
+ '%(endpoint_id)s '
+ '%(project_id)s' % {
+ 'project_id': project_id,
+ 'endpoint_id': endpoint_id})
+ self.assertEqual(0, len(raw_output))
diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py
index c7d2988..549a1aa 100644
--- a/openstackclient/tests/unit/identity/v3/fakes.py
+++ b/openstackclient/tests/unit/identity/v3/fakes.py
@@ -491,6 +491,8 @@ class FakeIdentityv3Client(object):
self.credentials.resource_class = fakes.FakeResource(None, {})
self.endpoints = mock.Mock()
self.endpoints.resource_class = fakes.FakeResource(None, {})
+ self.endpoint_filter = mock.Mock()
+ self.endpoint_filter.resource_class = fakes.FakeResource(None, {})
self.groups = mock.Mock()
self.groups.resource_class = fakes.FakeResource(None, {})
self.oauth1 = mock.Mock()
@@ -909,6 +911,31 @@ class FakeEndpoint(object):
loaded=True)
return endpoint
+ @staticmethod
+ def create_one_endpoint_filter(attrs=None):
+ """Create a fake endpoint project relationship.
+
+ :param Dictionary attrs:
+ A dictionary with all attributes of endpoint filter
+ :return:
+ A FakeResource object with project, endpoint and so on
+ """
+ attrs = attrs or {}
+
+ # Set default attribute
+ endpoint_filter_info = {
+ 'project': 'project-id-' + uuid.uuid4().hex,
+ 'endpoint': 'endpoint-id-' + uuid.uuid4().hex,
+ }
+
+ # Overwrite default attributes if there are some attributes set
+ endpoint_filter_info.update(attrs)
+
+ endpoint_filter = fakes.FakeModel(
+ copy.deepcopy(endpoint_filter_info))
+
+ return endpoint_filter
+
class FakeService(object):
"""Fake one or more service."""
diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint.py b/openstackclient/tests/unit/identity/v3/test_endpoint.py
index fad53fc..bfe930d 100644
--- a/openstackclient/tests/unit/identity/v3/test_endpoint.py
+++ b/openstackclient/tests/unit/identity/v3/test_endpoint.py
@@ -22,11 +22,23 @@ class TestEndpoint(identity_fakes.TestIdentityv3):
# Get a shortcut to the EndpointManager Mock
self.endpoints_mock = self.app.client_manager.identity.endpoints
self.endpoints_mock.reset_mock()
+ self.ep_filter_mock = (
+ self.app.client_manager.identity.endpoint_filter
+ )
+ self.ep_filter_mock.reset_mock()
# Get a shortcut to the ServiceManager Mock
self.services_mock = self.app.client_manager.identity.services
self.services_mock.reset_mock()
+ # Get a shortcut to the DomainManager Mock
+ self.domains_mock = self.app.client_manager.identity.domains
+ self.domains_mock.reset_mock()
+
+ # Get a shortcut to the ProjectManager Mock
+ self.projects_mock = self.app.client_manager.identity.projects
+ self.projects_mock.reset_mock()
+
class TestEndpointCreate(TestEndpoint):
@@ -750,3 +762,130 @@ class TestEndpointShowServiceWithoutName(TestEndpointShow):
# Get the command object to test
self.cmd = endpoint.ShowEndpoint(self.app, None)
+
+
+class TestAddProjectToEndpoint(TestEndpoint):
+
+ project = identity_fakes.FakeProject.create_one_project()
+ domain = identity_fakes.FakeDomain.create_one_domain()
+ service = identity_fakes.FakeService.create_one_service()
+ endpoint = identity_fakes.FakeEndpoint.create_one_endpoint(
+ attrs={'service_id': service.id})
+
+ new_ep_filter = identity_fakes.FakeEndpoint.create_one_endpoint_filter(
+ attrs={'endpoint': endpoint.id,
+ 'project': project.id}
+ )
+
+ def setUp(self):
+ super(TestAddProjectToEndpoint, self).setUp()
+
+ # This is the return value for utils.find_resource()
+ self.endpoints_mock.get.return_value = self.endpoint
+
+ # Update the image_id in the MEMBER dict
+ self.ep_filter_mock.create.return_value = self.new_ep_filter
+ self.projects_mock.get.return_value = self.project
+ self.domains_mock.get.return_value = self.domain
+ # Get the command object to test
+ self.cmd = endpoint.AddProjectToEndpoint(self.app, None)
+
+ def test_add_project_to_endpoint_no_option(self):
+ arglist = [
+ self.endpoint.id,
+ self.project.id,
+ ]
+ verifylist = [
+ ('endpoint', self.endpoint.id),
+ ('project', self.project.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.ep_filter_mock.add_endpoint_to_project.assert_called_with(
+ project=self.project.id,
+ endpoint=self.endpoint.id
+ )
+ self.assertIsNone(result)
+
+ def test_add_project_to_endpoint_with_option(self):
+ arglist = [
+ self.endpoint.id,
+ self.project.id,
+ '--project-domain', self.domain.id,
+ ]
+ verifylist = [
+ ('endpoint', self.endpoint.id),
+ ('project', self.project.id),
+ ('project_domain', self.domain.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.ep_filter_mock.add_endpoint_to_project.assert_called_with(
+ project=self.project.id,
+ endpoint=self.endpoint.id
+ )
+ self.assertIsNone(result)
+
+
+class TestRemoveProjectEndpoint(TestEndpoint):
+
+ project = identity_fakes.FakeProject.create_one_project()
+ domain = identity_fakes.FakeDomain.create_one_domain()
+ service = identity_fakes.FakeService.create_one_service()
+ endpoint = identity_fakes.FakeEndpoint.create_one_endpoint(
+ attrs={'service_id': service.id})
+
+ def setUp(self):
+ super(TestRemoveProjectEndpoint, self).setUp()
+
+ # This is the return value for utils.find_resource()
+ self.endpoints_mock.get.return_value = self.endpoint
+
+ self.projects_mock.get.return_value = self.project
+ self.domains_mock.get.return_value = self.domain
+ self.ep_filter_mock.delete.return_value = None
+
+ # Get the command object to test
+ self.cmd = endpoint.RemoveProjectFromEndpoint(self.app, None)
+
+ def test_remove_project_endpoint_no_options(self):
+ arglist = [
+ self.endpoint.id,
+ self.project.id,
+ ]
+ verifylist = [
+ ('endpoint', self.endpoint.id),
+ ('project', self.project.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.ep_filter_mock.delete_endpoint_from_project.assert_called_with(
+ project=self.project.id,
+ endpoint=self.endpoint.id,
+ )
+ self.assertIsNone(result)
+
+ def test_remove_project_endpoint_with_options(self):
+ arglist = [
+ self.endpoint.id,
+ self.project.id,
+ '--project-domain', self.domain.id,
+ ]
+ verifylist = [
+ ('endpoint', self.endpoint.id),
+ ('project', self.project.id),
+ ('project_domain', self.domain.id),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ self.ep_filter_mock.delete_endpoint_from_project.assert_called_with(
+ project=self.project.id,
+ endpoint=self.endpoint.id,
+ )
+ self.assertIsNone(result)
diff --git a/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml b/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml
new file mode 100644
index 0000000..5a633ee
--- /dev/null
+++ b/releasenotes/notes/keystone-endpoint-filter-e930a7b72276fa2c.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add ``endpoint add project``, ``endpoint remove project`` and ``endpoint
+ list`` commands to manage endpoint filters in identity v3.
diff --git a/setup.cfg b/setup.cfg
index 1b8e006..d60657f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -195,12 +195,15 @@ openstack.identity.v3 =
ec2_credentials_delete = openstackclient.identity.v3.ec2creds:DeleteEC2Creds
ec2_credentials_list = openstackclient.identity.v3.ec2creds:ListEC2Creds
ec2_credentials_show = openstackclient.identity.v3.ec2creds:ShowEC2Creds
- endpoint_create = openstackclient.identity.v3.endpoint:CreateEndpoint
+
+ endpoint_add_project = openstackclient.identity.v3.endpoint:AddProjectToEndpoint
+ endpoint_create = openstackclient.identity.v3.endpoint:CreateEndpoint
endpoint_delete = openstackclient.identity.v3.endpoint:DeleteEndpoint
+ endpoint_list = openstackclient.identity.v3.endpoint:ListEndpoint
+ endpoint_remove_project = openstackclient.identity.v3.endpoint:RemoveProjectFromEndpoint
endpoint_set = openstackclient.identity.v3.endpoint:SetEndpoint
endpoint_show = openstackclient.identity.v3.endpoint:ShowEndpoint
- endpoint_list = openstackclient.identity.v3.endpoint:ListEndpoint
- group_add_user = openstackclient.identity.v3.group:AddUserToGroup
+ group_add_user = openstackclient.identity.v3.group:AddUserToGroup
group_contains_user = openstackclient.identity.v3.group:CheckUserInGroup
group_create = openstackclient.identity.v3.group:CreateGroup
group_delete = openstackclient.identity.v3.group:DeleteGroup
--
1.8.3.1

View File

@ -0,0 +1,472 @@
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