diff --git a/openstack/python-openstackclient/centos/build_srpm.data b/openstack/python-openstackclient/centos/build_srpm.data index 52b88651..922ab862 100644 --- a/openstack/python-openstackclient/centos/build_srpm.data +++ b/openstack/python-openstackclient/centos/build_srpm.data @@ -1 +1 @@ -TIS_PATCH_VER=16 +TIS_PATCH_VER=17 diff --git a/openstack/python-openstackclient/centos/meta_patches/1003-meta-patch-for-endpoint-groups.patch b/openstack/python-openstackclient/centos/meta_patches/1003-meta-patch-for-endpoint-groups.patch new file mode 100644 index 00000000..6c040b1f --- /dev/null +++ b/openstack/python-openstackclient/centos/meta_patches/1003-meta-patch-for-endpoint-groups.patch @@ -0,0 +1,27 @@ +From ab8fc2b85ab7b60bdfeca496a32e90bc8f575478 Mon Sep 17 00:00:00 2001 +From: Kam Nasim +Date: Fri, 11 May 2018 13:13:15 -0400 +Subject: [PATCH] meta patch for endpoint groups + +Signed-off-by: Kam Nasim +--- + 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 + diff --git a/openstack/python-openstackclient/centos/meta_patches/PATCH_ORDER b/openstack/python-openstackclient/centos/meta_patches/PATCH_ORDER index 624ca310..b977cc06 100644 --- a/openstack/python-openstackclient/centos/meta_patches/PATCH_ORDER +++ b/openstack/python-openstackclient/centos/meta_patches/PATCH_ORDER @@ -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 diff --git a/openstack/python-openstackclient/centos/patches/0001-Optimize-getting-endpoint-list.patch b/openstack/python-openstackclient/centos/patches/0001-Optimize-getting-endpoint-list.patch new file mode 100644 index 00000000..2d004cb3 --- /dev/null +++ b/openstack/python-openstackclient/centos/patches/0001-Optimize-getting-endpoint-list.patch @@ -0,0 +1,99 @@ +From f6f5ce03c5b8a03180db24a02dda5b30f40b4cee Mon Sep 17 00:00:00 2001 +From: Anton Frolov +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/ +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/ 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/ Keystone API for each endpoint. In case of HTTPS ++ keystone endpoint and multiple regions it can take significant amount of time. ++ [Bug `1719413 `_] ++ +-- +1.8.3.1 + diff --git a/openstack/python-openstackclient/centos/patches/0002-Add-support-for-endpoing-filter-commands.patch b/openstack/python-openstackclient/centos/patches/0002-Add-support-for-endpoing-filter-commands.patch new file mode 100644 index 00000000..201eed54 --- /dev/null +++ b/openstack/python-openstackclient/centos/patches/0002-Add-support-for-endpoing-filter-commands.patch @@ -0,0 +1,614 @@ +From 8d106e1f1b3e536127818e98e495343e3c85f6b1 Mon Sep 17 00:00:00 2001 +From: Jose Castro Leon +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 +--- + 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 ] ++ ++ ++ ++.. option:: --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 to associate with specified project (name or ID) ++ ++.. _endpoint_add_project-project: ++.. describe:: ++ ++ Project to associate with specified endpoint (name or ID) ++ + endpoint create + --------------- + +@@ -107,6 +135,8 @@ List endpoints + [--interface ] + [--region ] + [--long] ++ [--endpoint | ++ --project [--project-domain ]] + + .. option:: --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 ] ++ ++ ++ ++.. option:: --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 to dissociate with specified project (name or ID) ++ ++.. _endpoint_remove_project-project: ++.. describe:: ++ ++ 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='', ++ help=_('Endpoint to associate with ' ++ 'specified project (name or ID)'), ++ ) ++ parser.add_argument( ++ 'project', ++ metavar='', ++ 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='', + help=_('Filter by region ID'), + ) ++ list_group = parser.add_mutually_exclusive_group() ++ list_group.add_argument( ++ '--endpoint', ++ metavar='', ++ help=_('Endpoint to list filters'), ++ ) ++ list_group.add_argument( ++ '--project', ++ metavar='', ++ 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='', ++ help=_('Endpoint to dissociate from ' ++ 'specified project (name or ID)'), ++ ) ++ parser.add_argument( ++ 'project', ++ metavar='', ++ 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 + diff --git a/openstack/python-openstackclient/centos/patches/0003-Add-support-for-endpoint-group-commands.patch b/openstack/python-openstackclient/centos/patches/0003-Add-support-for-endpoint-group-commands.patch new file mode 100644 index 00000000..29094ffa --- /dev/null +++ b/openstack/python-openstackclient/centos/patches/0003-Add-support-for-endpoint-group-commands.patch @@ -0,0 +1,472 @@ +From d800b1821e4aa3e3e49173be6c5b1ea370200d96 Mon Sep 17 00:00:00 2001 +From: Jose Castro Leon +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 +--- + 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='', ++ 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 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 `_] +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 +