From 2e7b4f2027a1147ca28301e4f88adf8274b39a1f Mon Sep 17 00:00:00 2001 From: DUVAL Thomas Date: Thu, 9 Jun 2016 09:11:50 +0200 Subject: Update Keystone core to Mitaka. Change-Id: Ia10d6add16f4a9d25d1f42d420661c46332e69db --- keystone-moon/keystone/catalog/__init__.py | 1 - keystone-moon/keystone/catalog/backends/sql.py | 429 +++++++++++++++------ .../keystone/catalog/backends/templated.py | 160 +++++++- keystone-moon/keystone/catalog/controllers.py | 234 +++++++++++ keystone-moon/keystone/catalog/core.py | 388 ++++++++++++++++++- keystone-moon/keystone/catalog/routers.py | 142 +++++++ keystone-moon/keystone/catalog/schema.py | 21 + 7 files changed, 1227 insertions(+), 148 deletions(-) (limited to 'keystone-moon/keystone/catalog') diff --git a/keystone-moon/keystone/catalog/__init__.py b/keystone-moon/keystone/catalog/__init__.py index 8d4d1567..29f297d6 100644 --- a/keystone-moon/keystone/catalog/__init__.py +++ b/keystone-moon/keystone/catalog/__init__.py @@ -14,4 +14,3 @@ from keystone.catalog import controllers # noqa from keystone.catalog.core import * # noqa -from keystone.catalog import routers # noqa diff --git a/keystone-moon/keystone/catalog/backends/sql.py b/keystone-moon/keystone/catalog/backends/sql.py index fe69db58..bd92f107 100644 --- a/keystone-moon/keystone/catalog/backends/sql.py +++ b/keystone-moon/keystone/catalog/backends/sql.py @@ -21,8 +21,10 @@ from sqlalchemy.sql import true from keystone import catalog from keystone.catalog import core +from keystone.common import driver_hints from keystone.common import sql from keystone import exception +from keystone.i18n import _ CONF = cfg.CONF @@ -43,13 +45,6 @@ class Region(sql.ModelBase, sql.DictBase): # "left" and "right" and provide support for a nested set # model. parent_region_id = sql.Column(sql.String(255), nullable=True) - - # TODO(jaypipes): I think it's absolutely stupid that every single model - # is required to have an "extra" column because of the - # DictBase in the keystone.common.sql.core module. Forcing - # tables to have pointless columns in the database is just - # bad. Remove all of this extra JSON blob stuff. - # See: https://bugs.launchpad.net/keystone/+bug/1265071 extra = sql.Column(sql.JsonBlob()) endpoints = sqlalchemy.orm.relationship("Endpoint", backref="region") @@ -89,10 +84,10 @@ class Endpoint(sql.ModelBase, sql.DictBase): class Catalog(catalog.CatalogDriverV8): # Regions def list_regions(self, hints): - session = sql.get_session() - regions = session.query(Region) - regions = sql.filter_limit_query(Region, regions, hints) - return [s.to_dict() for s in list(regions)] + with sql.session_for_read() as session: + regions = session.query(Region) + regions = sql.filter_limit_query(Region, regions, hints) + return [s.to_dict() for s in list(regions)] def _get_region(self, session, region_id): ref = session.query(Region).get(region_id) @@ -141,12 +136,11 @@ class Catalog(catalog.CatalogDriverV8): return False def get_region(self, region_id): - session = sql.get_session() - return self._get_region(session, region_id).to_dict() + with sql.session_for_read() as session: + return self._get_region(session, region_id).to_dict() def delete_region(self, region_id): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: ref = self._get_region(session, region_id) if self._has_endpoints(session, ref, ref): raise exception.RegionDeletionError(region_id=region_id) @@ -155,16 +149,14 @@ class Catalog(catalog.CatalogDriverV8): @sql.handle_conflicts(conflict_type='region') def create_region(self, region_ref): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: self._check_parent_region(session, region_ref) region = Region.from_dict(region_ref) session.add(region) - return region.to_dict() + return region.to_dict() def update_region(self, region_id, region_ref): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: self._check_parent_region(session, region_ref) ref = self._get_region(session, region_id) old_dict = ref.to_dict() @@ -174,15 +166,15 @@ class Catalog(catalog.CatalogDriverV8): for attr in Region.attributes: if attr != 'id': setattr(ref, attr, getattr(new_region, attr)) - return ref.to_dict() + return ref.to_dict() # Services - @sql.truncated + @driver_hints.truncated def list_services(self, hints): - session = sql.get_session() - services = session.query(Service) - services = sql.filter_limit_query(Service, services, hints) - return [s.to_dict() for s in list(services)] + with sql.session_for_read() as session: + services = session.query(Service) + services = sql.filter_limit_query(Service, services, hints) + return [s.to_dict() for s in list(services)] def _get_service(self, session, service_id): ref = session.query(Service).get(service_id) @@ -191,26 +183,23 @@ class Catalog(catalog.CatalogDriverV8): return ref def get_service(self, service_id): - session = sql.get_session() - return self._get_service(session, service_id).to_dict() + with sql.session_for_read() as session: + return self._get_service(session, service_id).to_dict() def delete_service(self, service_id): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: ref = self._get_service(session, service_id) session.query(Endpoint).filter_by(service_id=service_id).delete() session.delete(ref) def create_service(self, service_id, service_ref): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: service = Service.from_dict(service_ref) session.add(service) - return service.to_dict() + return service.to_dict() def update_service(self, service_id, service_ref): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: ref = self._get_service(session, service_id) old_dict = ref.to_dict() old_dict.update(service_ref) @@ -219,20 +208,17 @@ class Catalog(catalog.CatalogDriverV8): if attr != 'id': setattr(ref, attr, getattr(new_service, attr)) ref.extra = new_service.extra - return ref.to_dict() + return ref.to_dict() # Endpoints def create_endpoint(self, endpoint_id, endpoint_ref): - session = sql.get_session() new_endpoint = Endpoint.from_dict(endpoint_ref) - - with session.begin(): + with sql.session_for_write() as session: session.add(new_endpoint) return new_endpoint.to_dict() def delete_endpoint(self, endpoint_id): - session = sql.get_session() - with session.begin(): + with sql.session_for_write() as session: ref = self._get_endpoint(session, endpoint_id) session.delete(ref) @@ -243,20 +229,18 @@ class Catalog(catalog.CatalogDriverV8): raise exception.EndpointNotFound(endpoint_id=endpoint_id) def get_endpoint(self, endpoint_id): - session = sql.get_session() - return self._get_endpoint(session, endpoint_id).to_dict() + with sql.session_for_read() as session: + return self._get_endpoint(session, endpoint_id).to_dict() - @sql.truncated + @driver_hints.truncated def list_endpoints(self, hints): - session = sql.get_session() - endpoints = session.query(Endpoint) - endpoints = sql.filter_limit_query(Endpoint, endpoints, hints) - return [e.to_dict() for e in list(endpoints)] + with sql.session_for_read() as session: + endpoints = session.query(Endpoint) + endpoints = sql.filter_limit_query(Endpoint, endpoints, hints) + return [e.to_dict() for e in list(endpoints)] def update_endpoint(self, endpoint_id, endpoint_ref): - session = sql.get_session() - - with session.begin(): + with sql.session_for_write() as session: ref = self._get_endpoint(session, endpoint_id) old_dict = ref.to_dict() old_dict.update(endpoint_ref) @@ -265,7 +249,7 @@ class Catalog(catalog.CatalogDriverV8): if attr != 'id': setattr(ref, attr, getattr(new_endpoint, attr)) ref.extra = new_endpoint.extra - return ref.to_dict() + return ref.to_dict() def get_catalog(self, user_id, tenant_id): """Retrieve and format the V2 service catalog. @@ -287,44 +271,47 @@ class Catalog(catalog.CatalogDriverV8): substitutions.update({'user_id': user_id}) silent_keyerror_failures = [] if tenant_id: - substitutions.update({'tenant_id': tenant_id}) + substitutions.update({ + 'tenant_id': tenant_id, + 'project_id': tenant_id + }) else: - silent_keyerror_failures = ['tenant_id'] - - session = sql.get_session() - endpoints = (session.query(Endpoint). - options(sql.joinedload(Endpoint.service)). - filter(Endpoint.enabled == true()).all()) - - catalog = {} - - for endpoint in endpoints: - if not endpoint.service['enabled']: - continue - try: - formatted_url = core.format_url( - endpoint['url'], substitutions, - silent_keyerror_failures=silent_keyerror_failures) - if formatted_url is not None: - url = formatted_url - else: + silent_keyerror_failures = ['tenant_id', 'project_id', ] + + with sql.session_for_read() as session: + endpoints = (session.query(Endpoint). + options(sql.joinedload(Endpoint.service)). + filter(Endpoint.enabled == true()).all()) + + catalog = {} + + for endpoint in endpoints: + if not endpoint.service['enabled']: continue - except exception.MalformedEndpoint: - continue # this failure is already logged in format_url() - - region = endpoint['region_id'] - service_type = endpoint.service['type'] - default_service = { - 'id': endpoint['id'], - 'name': endpoint.service.extra.get('name', ''), - 'publicURL': '' - } - catalog.setdefault(region, {}) - catalog[region].setdefault(service_type, default_service) - interface_url = '%sURL' % endpoint['interface'] - catalog[region][service_type][interface_url] = url - - return catalog + try: + formatted_url = core.format_url( + endpoint['url'], substitutions, + silent_keyerror_failures=silent_keyerror_failures) + if formatted_url is not None: + url = formatted_url + else: + continue + except exception.MalformedEndpoint: + continue # this failure is already logged in format_url() + + region = endpoint['region_id'] + service_type = endpoint.service['type'] + default_service = { + 'id': endpoint['id'], + 'name': endpoint.service.extra.get('name', ''), + 'publicURL': '' + } + catalog.setdefault(region, {}) + catalog[region].setdefault(service_type, default_service) + interface_url = '%sURL' % endpoint['interface'] + catalog[region][service_type][interface_url] = url + + return catalog def get_v3_catalog(self, user_id, tenant_id): """Retrieve and format the current V3 service catalog. @@ -344,40 +331,242 @@ class Catalog(catalog.CatalogDriverV8): d.update({'user_id': user_id}) silent_keyerror_failures = [] if tenant_id: - d.update({'tenant_id': tenant_id}) + d.update({ + 'tenant_id': tenant_id, + 'project_id': tenant_id, + }) else: - silent_keyerror_failures = ['tenant_id'] - - session = sql.get_session() - services = (session.query(Service).filter(Service.enabled == true()). - options(sql.joinedload(Service.endpoints)). - all()) - - def make_v3_endpoints(endpoints): - for endpoint in (ep.to_dict() for ep in endpoints if ep.enabled): - del endpoint['service_id'] - del endpoint['legacy_endpoint_id'] - del endpoint['enabled'] - endpoint['region'] = endpoint['region_id'] - try: - formatted_url = core.format_url( - endpoint['url'], d, - silent_keyerror_failures=silent_keyerror_failures) - if formatted_url: - endpoint['url'] = formatted_url - else: + silent_keyerror_failures = ['tenant_id', 'project_id', ] + + with sql.session_for_read() as session: + services = (session.query(Service).filter( + Service.enabled == true()).options( + sql.joinedload(Service.endpoints)).all()) + + def make_v3_endpoints(endpoints): + for endpoint in (ep.to_dict() + for ep in endpoints if ep.enabled): + del endpoint['service_id'] + del endpoint['legacy_endpoint_id'] + del endpoint['enabled'] + endpoint['region'] = endpoint['region_id'] + try: + formatted_url = core.format_url( + endpoint['url'], d, + silent_keyerror_failures=silent_keyerror_failures) + if formatted_url: + endpoint['url'] = formatted_url + else: + continue + except exception.MalformedEndpoint: + # this failure is already logged in format_url() continue - except exception.MalformedEndpoint: - continue # this failure is already logged in format_url() - yield endpoint + yield endpoint + + # TODO(davechen): If there is service with no endpoints, we should + # skip the service instead of keeping it in the catalog, + # see bug #1436704. + def make_v3_service(svc): + eps = list(make_v3_endpoints(svc.endpoints)) + service = {'endpoints': eps, 'id': svc.id, 'type': svc.type} + service['name'] = svc.extra.get('name', '') + return service + + return [make_v3_service(svc) for svc in services] + + @sql.handle_conflicts(conflict_type='project_endpoint') + def add_endpoint_to_project(self, endpoint_id, project_id): + with sql.session_for_write() as session: + endpoint_filter_ref = ProjectEndpoint(endpoint_id=endpoint_id, + project_id=project_id) + session.add(endpoint_filter_ref) + + def _get_project_endpoint_ref(self, session, endpoint_id, project_id): + endpoint_filter_ref = session.query(ProjectEndpoint).get( + (endpoint_id, project_id)) + if endpoint_filter_ref is None: + msg = _('Endpoint %(endpoint_id)s not found in project ' + '%(project_id)s') % {'endpoint_id': endpoint_id, + 'project_id': project_id} + raise exception.NotFound(msg) + return endpoint_filter_ref + + def check_endpoint_in_project(self, endpoint_id, project_id): + with sql.session_for_read() as session: + self._get_project_endpoint_ref(session, endpoint_id, project_id) + + def remove_endpoint_from_project(self, endpoint_id, project_id): + with sql.session_for_write() as session: + endpoint_filter_ref = self._get_project_endpoint_ref( + session, endpoint_id, project_id) + session.delete(endpoint_filter_ref) + + def list_endpoints_for_project(self, project_id): + with sql.session_for_read() as session: + query = session.query(ProjectEndpoint) + query = query.filter_by(project_id=project_id) + endpoint_filter_refs = query.all() + return [ref.to_dict() for ref in endpoint_filter_refs] + + def list_projects_for_endpoint(self, endpoint_id): + with sql.session_for_read() as session: + query = session.query(ProjectEndpoint) + query = query.filter_by(endpoint_id=endpoint_id) + endpoint_filter_refs = query.all() + return [ref.to_dict() for ref in endpoint_filter_refs] + + def delete_association_by_endpoint(self, endpoint_id): + with sql.session_for_write() as session: + query = session.query(ProjectEndpoint) + query = query.filter_by(endpoint_id=endpoint_id) + query.delete(synchronize_session=False) + + def delete_association_by_project(self, project_id): + with sql.session_for_write() as session: + query = session.query(ProjectEndpoint) + query = query.filter_by(project_id=project_id) + query.delete(synchronize_session=False) + + def create_endpoint_group(self, endpoint_group_id, endpoint_group): + with sql.session_for_write() as session: + endpoint_group_ref = EndpointGroup.from_dict(endpoint_group) + session.add(endpoint_group_ref) + return endpoint_group_ref.to_dict() + + def _get_endpoint_group(self, session, endpoint_group_id): + endpoint_group_ref = session.query(EndpointGroup).get( + endpoint_group_id) + if endpoint_group_ref is None: + raise exception.EndpointGroupNotFound( + endpoint_group_id=endpoint_group_id) + return endpoint_group_ref + + def get_endpoint_group(self, endpoint_group_id): + with sql.session_for_read() as session: + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + return endpoint_group_ref.to_dict() + + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + with sql.session_for_write() as session: + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + old_endpoint_group = endpoint_group_ref.to_dict() + old_endpoint_group.update(endpoint_group) + new_endpoint_group = EndpointGroup.from_dict(old_endpoint_group) + for attr in EndpointGroup.mutable_attributes: + setattr(endpoint_group_ref, attr, + getattr(new_endpoint_group, attr)) + return endpoint_group_ref.to_dict() + + def delete_endpoint_group(self, endpoint_group_id): + with sql.session_for_write() as session: + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + self._delete_endpoint_group_association_by_endpoint_group( + session, endpoint_group_id) + session.delete(endpoint_group_ref) + + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + with sql.session_for_read() as session: + ref = self._get_endpoint_group_in_project(session, + endpoint_group_id, + project_id) + return ref.to_dict() + + @sql.handle_conflicts(conflict_type='project_endpoint_group') + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + with sql.session_for_write() as session: + # Create a new Project Endpoint group entity + endpoint_group_project_ref = ProjectEndpointGroupMembership( + endpoint_group_id=endpoint_group_id, project_id=project_id) + session.add(endpoint_group_project_ref) + + def _get_endpoint_group_in_project(self, session, + endpoint_group_id, project_id): + endpoint_group_project_ref = session.query( + ProjectEndpointGroupMembership).get((endpoint_group_id, + project_id)) + if endpoint_group_project_ref is None: + msg = _('Endpoint Group Project Association not found') + raise exception.NotFound(msg) + else: + return endpoint_group_project_ref + + def list_endpoint_groups(self): + with sql.session_for_read() as session: + query = session.query(EndpointGroup) + endpoint_group_refs = query.all() + return [e.to_dict() for e in endpoint_group_refs] + + def list_endpoint_groups_for_project(self, project_id): + with sql.session_for_read() as session: + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(project_id=project_id) + endpoint_group_refs = query.all() + return [ref.to_dict() for ref in endpoint_group_refs] + + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + with sql.session_for_write() as session: + endpoint_group_project_ref = self._get_endpoint_group_in_project( + session, endpoint_group_id, project_id) + session.delete(endpoint_group_project_ref) + + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + with sql.session_for_read() as session: + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(endpoint_group_id=endpoint_group_id) + endpoint_group_refs = query.all() + return [ref.to_dict() for ref in endpoint_group_refs] + + def _delete_endpoint_group_association_by_endpoint_group( + self, session, endpoint_group_id): + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(endpoint_group_id=endpoint_group_id) + query.delete() + + def delete_endpoint_group_association_by_project(self, project_id): + with sql.session_for_write() as session: + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(project_id=project_id) + query.delete() + + +class ProjectEndpoint(sql.ModelBase, sql.ModelDictMixin): + """project-endpoint relationship table.""" + + __tablename__ = 'project_endpoint' + attributes = ['endpoint_id', 'project_id'] + endpoint_id = sql.Column(sql.String(64), + primary_key=True, + nullable=False) + project_id = sql.Column(sql.String(64), + primary_key=True, + nullable=False) + - # TODO(davechen): If there is service with no endpoints, we should skip - # the service instead of keeping it in the catalog, see bug #1436704. - def make_v3_service(svc): - eps = list(make_v3_endpoints(svc.endpoints)) - service = {'endpoints': eps, 'id': svc.id, 'type': svc.type} - service['name'] = svc.extra.get('name', '') - return service +class EndpointGroup(sql.ModelBase, sql.ModelDictMixin): + """Endpoint Groups table.""" - return [make_v3_service(svc) for svc in services] + __tablename__ = 'endpoint_group' + attributes = ['id', 'name', 'description', 'filters'] + mutable_attributes = frozenset(['name', 'description', 'filters']) + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), nullable=False) + description = sql.Column(sql.Text, nullable=True) + filters = sql.Column(sql.JsonBlob(), nullable=False) + + +class ProjectEndpointGroupMembership(sql.ModelBase, sql.ModelDictMixin): + """Project to Endpoint group relationship table.""" + + __tablename__ = 'project_endpoint_group' + attributes = ['endpoint_group_id', 'project_id'] + endpoint_group_id = sql.Column(sql.String(64), + sql.ForeignKey('endpoint_group.id'), + nullable=False) + project_id = sql.Column(sql.String(64), nullable=False) + __table_args__ = (sql.PrimaryKeyConstraint('endpoint_group_id', + 'project_id'),) diff --git a/keystone-moon/keystone/catalog/backends/templated.py b/keystone-moon/keystone/catalog/backends/templated.py index 31d8b9e0..2e80fd32 100644 --- a/keystone-moon/keystone/catalog/backends/templated.py +++ b/keystone-moon/keystone/catalog/backends/templated.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack Foundationc +# Copyright 2012 OpenStack Foundation # # 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 @@ -17,8 +17,8 @@ import os.path from oslo_config import cfg from oslo_log import log +import six -from keystone.catalog.backends import kvs from keystone.catalog import core from keystone import exception from keystone.i18n import _LC @@ -56,7 +56,7 @@ def parse_templates(template_lines): return o -class Catalog(kvs.Catalog): +class Catalog(core.Driver): """A backend that generates endpoints for the Catalog based on templates. It is usually configured via config entries that look like: @@ -100,11 +100,101 @@ class Catalog(kvs.Catalog): def _load_templates(self, template_file): try: - self.templates = parse_templates(open(template_file)) + with open(template_file) as f: + self.templates = parse_templates(f) except IOError: LOG.critical(_LC('Unable to open template file %s'), template_file) raise + # region crud + + def create_region(self, region_ref): + raise exception.NotImplemented() + + def list_regions(self, hints): + return [{'id': region_id, 'description': '', 'parent_region_id': ''} + for region_id in self.templates] + + def get_region(self, region_id): + if region_id in self.templates: + return {'id': region_id, 'description': '', 'parent_region_id': ''} + raise exception.RegionNotFound(region_id=region_id) + + def update_region(self, region_id, region_ref): + raise exception.NotImplemented() + + def delete_region(self, region_id): + raise exception.NotImplemented() + + # service crud + + def create_service(self, service_id, service_ref): + raise exception.NotImplemented() + + def _list_services(self, hints): + for region_ref in six.itervalues(self.templates): + for service_type, service_ref in six.iteritems(region_ref): + yield { + 'id': service_type, + 'enabled': True, + 'name': service_ref.get('name', ''), + 'description': service_ref.get('description', ''), + 'type': service_type, + } + + def list_services(self, hints): + return list(self._list_services(hints=None)) + + def get_service(self, service_id): + for service in self._list_services(hints=None): + if service['id'] == service_id: + return service + raise exception.ServiceNotFound(service_id=service_id) + + def update_service(self, service_id, service_ref): + raise exception.NotImplemented() + + def delete_service(self, service_id): + raise exception.NotImplemented() + + # endpoint crud + + def create_endpoint(self, endpoint_id, endpoint_ref): + raise exception.NotImplemented() + + def _list_endpoints(self): + for region_id, region_ref in six.iteritems(self.templates): + for service_type, service_ref in six.iteritems(region_ref): + for key in service_ref: + if key.endswith('URL'): + interface = key[:-3] + endpoint_id = ('%s-%s-%s' % + (region_id, service_type, interface)) + yield { + 'id': endpoint_id, + 'service_id': service_type, + 'interface': interface, + 'url': service_ref[key], + 'legacy_endpoint_id': None, + 'region_id': region_id, + 'enabled': True, + } + + def list_endpoints(self, hints): + return list(self._list_endpoints()) + + def get_endpoint(self, endpoint_id): + for endpoint in self._list_endpoints(): + if endpoint['id'] == endpoint_id: + return endpoint + raise exception.EndpointNotFound(endpoint_id=endpoint_id) + + def update_endpoint(self, endpoint_id, endpoint_ref): + raise exception.NotImplemented() + + def delete_endpoint(self, endpoint_id): + raise exception.NotImplemented() + def get_catalog(self, user_id, tenant_id): """Retrieve and format the V2 service catalog. @@ -124,9 +214,12 @@ class Catalog(kvs.Catalog): substitutions.update({'user_id': user_id}) silent_keyerror_failures = [] if tenant_id: - substitutions.update({'tenant_id': tenant_id}) + substitutions.update({ + 'tenant_id': tenant_id, + 'project_id': tenant_id, + }) else: - silent_keyerror_failures = ['tenant_id'] + silent_keyerror_failures = ['tenant_id', 'project_id', ] catalog = {} # TODO(davechen): If there is service with no endpoints, we should @@ -148,3 +241,58 @@ class Catalog(kvs.Catalog): catalog[region][service] = service_data return catalog + + def add_endpoint_to_project(self, endpoint_id, project_id): + raise exception.NotImplemented() + + def remove_endpoint_from_project(self, endpoint_id, project_id): + raise exception.NotImplemented() + + def check_endpoint_in_project(self, endpoint_id, project_id): + raise exception.NotImplemented() + + def list_endpoints_for_project(self, project_id): + raise exception.NotImplemented() + + def list_projects_for_endpoint(self, endpoint_id): + raise exception.NotImplemented() + + def delete_association_by_endpoint(self, endpoint_id): + raise exception.NotImplemented() + + def delete_association_by_project(self, project_id): + raise exception.NotImplemented() + + def create_endpoint_group(self, endpoint_group): + raise exception.NotImplemented() + + def get_endpoint_group(self, endpoint_group_id): + raise exception.NotImplemented() + + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + raise exception.NotImplemented() + + def delete_endpoint_group(self, endpoint_group_id): + raise exception.NotImplemented() + + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + raise exception.NotImplemented() + + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + raise exception.NotImplemented() + + def list_endpoint_groups(self): + raise exception.NotImplemented() + + def list_endpoint_groups_for_project(self, project_id): + raise exception.NotImplemented() + + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + raise exception.NotImplemented() + + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + raise exception.NotImplemented() + + def delete_endpoint_group_association_by_project(self, project_id): + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/catalog/controllers.py b/keystone-moon/keystone/catalog/controllers.py index e14b268a..fc64c922 100644 --- a/keystone-moon/keystone/catalog/controllers.py +++ b/keystone-moon/keystone/catalog/controllers.py @@ -15,6 +15,8 @@ import uuid +import six + from keystone.catalog import core from keystone.catalog import schema from keystone.common import controller @@ -24,6 +26,7 @@ from keystone.common import wsgi from keystone import exception from keystone.i18n import _ from keystone import notifications +from keystone import resource INTERFACES = ['public', 'internal', 'admin'] @@ -379,3 +382,234 @@ class EndpointV3(controller.V3Controller): def delete_endpoint(self, context, endpoint_id): initiator = notifications._get_request_audit_info(context) return self.catalog_api.delete_endpoint(endpoint_id, initiator) + + +@dependency.requires('catalog_api', 'resource_api') +class EndpointFilterV3Controller(controller.V3Controller): + + def __init__(self): + super(EndpointFilterV3Controller, self).__init__() + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'project', + self._on_project_or_endpoint_delete) + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'endpoint', + self._on_project_or_endpoint_delete) + + def _on_project_or_endpoint_delete(self, service, resource_type, operation, + payload): + project_or_endpoint_id = payload['resource_info'] + if resource_type == 'project': + self.catalog_api.delete_association_by_project( + project_or_endpoint_id) + else: + self.catalog_api.delete_association_by_endpoint( + project_or_endpoint_id) + + @controller.protected() + def add_endpoint_to_project(self, context, project_id, endpoint_id): + """Establishes an association between an endpoint and a project.""" + # NOTE(gyee): we just need to make sure endpoint and project exist + # first. We don't really care whether if project is disabled. + # The relationship can still be established even with a disabled + # project as there are no security implications. + self.catalog_api.get_endpoint(endpoint_id) + self.resource_api.get_project(project_id) + self.catalog_api.add_endpoint_to_project(endpoint_id, + project_id) + + @controller.protected() + def check_endpoint_in_project(self, context, project_id, endpoint_id): + """Verifies endpoint is currently associated with given project.""" + self.catalog_api.get_endpoint(endpoint_id) + self.resource_api.get_project(project_id) + self.catalog_api.check_endpoint_in_project(endpoint_id, + project_id) + + @controller.protected() + def list_endpoints_for_project(self, context, project_id): + """List all endpoints currently associated with a given project.""" + self.resource_api.get_project(project_id) + filtered_endpoints = self.catalog_api.list_endpoints_for_project( + project_id) + + return EndpointV3.wrap_collection( + context, [v for v in six.itervalues(filtered_endpoints)]) + + @controller.protected() + def remove_endpoint_from_project(self, context, project_id, endpoint_id): + """Remove the endpoint from the association with given project.""" + self.catalog_api.remove_endpoint_from_project(endpoint_id, + project_id) + + @controller.protected() + def list_projects_for_endpoint(self, context, endpoint_id): + """Return a list of projects associated with the endpoint.""" + self.catalog_api.get_endpoint(endpoint_id) + refs = self.catalog_api.list_projects_for_endpoint(endpoint_id) + + projects = [self.resource_api.get_project( + ref['project_id']) for ref in refs] + return resource.controllers.ProjectV3.wrap_collection(context, + projects) + + +@dependency.requires('catalog_api', 'resource_api') +class EndpointGroupV3Controller(controller.V3Controller): + collection_name = 'endpoint_groups' + member_name = 'endpoint_group' + + VALID_FILTER_KEYS = ['service_id', 'region_id', 'interface'] + + def __init__(self): + super(EndpointGroupV3Controller, self).__init__() + + @classmethod + def base_url(cls, context, path=None): + """Construct a path and pass it to V3Controller.base_url method.""" + path = '/OS-EP-FILTER/' + cls.collection_name + return super(EndpointGroupV3Controller, cls).base_url(context, + path=path) + + @controller.protected() + @validation.validated(schema.endpoint_group_create, 'endpoint_group') + def create_endpoint_group(self, context, endpoint_group): + """Creates an Endpoint Group with the associated filters.""" + ref = self._assign_unique_id(self._normalize_dict(endpoint_group)) + self._require_attribute(ref, 'filters') + self._require_valid_filter(ref) + ref = self.catalog_api.create_endpoint_group(ref['id'], ref) + return EndpointGroupV3Controller.wrap_member(context, ref) + + def _require_valid_filter(self, endpoint_group): + filters = endpoint_group.get('filters') + for key in six.iterkeys(filters): + if key not in self.VALID_FILTER_KEYS: + raise exception.ValidationError( + attribute=self._valid_filter_keys(), + target='endpoint_group') + + def _valid_filter_keys(self): + return ' or '.join(self.VALID_FILTER_KEYS) + + @controller.protected() + def get_endpoint_group(self, context, endpoint_group_id): + """Retrieve the endpoint group associated with the id if exists.""" + ref = self.catalog_api.get_endpoint_group(endpoint_group_id) + return EndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + @validation.validated(schema.endpoint_group_update, 'endpoint_group') + def update_endpoint_group(self, context, endpoint_group_id, + endpoint_group): + """Update fixed values and/or extend the filters.""" + if 'filters' in endpoint_group: + self._require_valid_filter(endpoint_group) + ref = self.catalog_api.update_endpoint_group(endpoint_group_id, + endpoint_group) + return EndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + def delete_endpoint_group(self, context, endpoint_group_id): + """Delete endpoint_group.""" + self.catalog_api.delete_endpoint_group(endpoint_group_id) + + @controller.protected() + def list_endpoint_groups(self, context): + """List all endpoint groups.""" + refs = self.catalog_api.list_endpoint_groups() + return EndpointGroupV3Controller.wrap_collection( + context, refs) + + @controller.protected() + def list_endpoint_groups_for_project(self, context, project_id): + """List all endpoint groups associated with a given project.""" + return EndpointGroupV3Controller.wrap_collection( + context, + self.catalog_api.get_endpoint_groups_for_project(project_id)) + + @controller.protected() + def list_projects_associated_with_endpoint_group(self, + context, + endpoint_group_id): + """List all projects associated with endpoint group.""" + endpoint_group_refs = (self.catalog_api. + list_projects_associated_with_endpoint_group( + endpoint_group_id)) + projects = [] + for endpoint_group_ref in endpoint_group_refs: + project = self.resource_api.get_project( + endpoint_group_ref['project_id']) + if project: + projects.append(project) + return resource.controllers.ProjectV3.wrap_collection(context, + projects) + + @controller.protected() + def list_endpoints_associated_with_endpoint_group(self, + context, + endpoint_group_id): + """List all the endpoints filtered by a specific endpoint group.""" + filtered_endpoints = (self.catalog_api. + get_endpoints_filtered_by_endpoint_group( + endpoint_group_id)) + return EndpointV3.wrap_collection(context, filtered_endpoints) + + +@dependency.requires('catalog_api', 'resource_api') +class ProjectEndpointGroupV3Controller(controller.V3Controller): + collection_name = 'project_endpoint_groups' + member_name = 'project_endpoint_group' + + def __init__(self): + super(ProjectEndpointGroupV3Controller, self).__init__() + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'project', + self._on_project_delete) + + def _on_project_delete(self, service, resource_type, + operation, payload): + project_id = payload['resource_info'] + (self.catalog_api. + delete_endpoint_group_association_by_project( + project_id)) + + @controller.protected() + def get_endpoint_group_in_project(self, context, endpoint_group_id, + project_id): + """Retrieve the endpoint group associated with the id if exists.""" + self.resource_api.get_project(project_id) + self.catalog_api.get_endpoint_group(endpoint_group_id) + ref = self.catalog_api.get_endpoint_group_in_project( + endpoint_group_id, project_id) + return ProjectEndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + def add_endpoint_group_to_project(self, context, endpoint_group_id, + project_id): + """Creates an association between an endpoint group and project.""" + self.resource_api.get_project(project_id) + self.catalog_api.get_endpoint_group(endpoint_group_id) + self.catalog_api.add_endpoint_group_to_project( + endpoint_group_id, project_id) + + @controller.protected() + def remove_endpoint_group_from_project(self, context, endpoint_group_id, + project_id): + """Remove the endpoint group from associated project.""" + self.resource_api.get_project(project_id) + self.catalog_api.get_endpoint_group(endpoint_group_id) + self.catalog_api.remove_endpoint_group_from_project( + endpoint_group_id, project_id) + + @classmethod + def _add_self_referential_link(cls, context, ref): + url = ('/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' + '/projects/%(project_id)s' % { + 'endpoint_group_id': ref['endpoint_group_id'], + 'project_id': ref['project_id']}) + ref.setdefault('links', {}) + ref['links']['self'] = url diff --git a/keystone-moon/keystone/catalog/core.py b/keystone-moon/keystone/catalog/core.py index 8bb72619..384a9b2b 100644 --- a/keystone-moon/keystone/catalog/core.py +++ b/keystone-moon/keystone/catalog/core.py @@ -18,6 +18,7 @@ import abc import itertools +from oslo_cache import core as oslo_cache from oslo_config import cfg from oslo_log import log import six @@ -35,12 +36,24 @@ from keystone import notifications CONF = cfg.CONF LOG = log.getLogger(__name__) -MEMOIZE = cache.get_memoization_decorator(section='catalog') WHITELISTED_PROPERTIES = [ - 'tenant_id', 'user_id', 'public_bind_host', 'admin_bind_host', + 'tenant_id', 'project_id', 'user_id', + 'public_bind_host', 'admin_bind_host', 'compute_host', 'admin_port', 'public_port', 'public_endpoint', 'admin_endpoint', ] +# This is a general cache region for catalog administration (CRUD operations). +MEMOIZE = cache.get_memoization_decorator(group='catalog') + +# This builds a discrete cache region dedicated to complete service catalogs +# computed for a given user + project pair. Any write operation to create, +# modify or delete elements of the service catalog should invalidate this +# entire cache region. +COMPUTED_CATALOG_REGION = oslo_cache.create_region() +MEMOIZE_COMPUTED_CATALOG = cache.get_memoization_decorator( + group='catalog', + region=COMPUTED_CATALOG_REGION) + def format_url(url, substitutions, silent_keyerror_failures=None): """Formats a user-defined URL with the given substitutions. @@ -52,7 +65,6 @@ def format_url(url, substitutions, silent_keyerror_failures=None): :returns: a formatted URL """ - substitutions = utils.WhiteListedItemFilter( WHITELISTED_PROPERTIES, substitutions) @@ -108,6 +120,7 @@ def check_endpoint_url(url): @dependency.provider('catalog_api') +@dependency.requires('resource_api') class Manager(manager.Manager): """Default pivot point for the Catalog backend. @@ -129,7 +142,8 @@ class Manager(manager.Manager): # Check duplicate ID try: self.get_region(region_ref['id']) - except exception.RegionNotFound: + except exception.RegionNotFound: # nosec + # A region with the same id doesn't exist already, good. pass else: msg = _('Duplicate ID, %s.') % region_ref['id'] @@ -148,6 +162,7 @@ class Manager(manager.Manager): raise exception.RegionNotFound(region_id=parent_region_id) notifications.Audit.created(self._REGION, ret['id'], initiator) + COMPUTED_CATALOG_REGION.invalidate() return ret @MEMOIZE @@ -166,6 +181,7 @@ class Manager(manager.Manager): ref = self.driver.update_region(region_id, region_ref) notifications.Audit.updated(self._REGION, region_id, initiator) self.get_region.invalidate(self, region_id) + COMPUTED_CATALOG_REGION.invalidate() return ref def delete_region(self, region_id, initiator=None): @@ -173,6 +189,7 @@ class Manager(manager.Manager): ret = self.driver.delete_region(region_id) notifications.Audit.deleted(self._REGION, region_id, initiator) self.get_region.invalidate(self, region_id) + COMPUTED_CATALOG_REGION.invalidate() return ret except exception.NotFound: raise exception.RegionNotFound(region_id=region_id) @@ -186,6 +203,7 @@ class Manager(manager.Manager): service_ref.setdefault('name', '') ref = self.driver.create_service(service_id, service_ref) notifications.Audit.created(self._SERVICE, service_id, initiator) + COMPUTED_CATALOG_REGION.invalidate() return ref @MEMOIZE @@ -199,6 +217,7 @@ class Manager(manager.Manager): ref = self.driver.update_service(service_id, service_ref) notifications.Audit.updated(self._SERVICE, service_id, initiator) self.get_service.invalidate(self, service_id) + COMPUTED_CATALOG_REGION.invalidate() return ref def delete_service(self, service_id, initiator=None): @@ -210,6 +229,7 @@ class Manager(manager.Manager): for endpoint in endpoints: if endpoint['service_id'] == service_id: self.get_endpoint.invalidate(self, endpoint['id']) + COMPUTED_CATALOG_REGION.invalidate() return ret except exception.NotFound: raise exception.ServiceNotFound(service_id=service_id) @@ -240,6 +260,7 @@ class Manager(manager.Manager): ref = self.driver.create_endpoint(endpoint_id, endpoint_ref) notifications.Audit.created(self._ENDPOINT, endpoint_id, initiator) + COMPUTED_CATALOG_REGION.invalidate() return ref def update_endpoint(self, endpoint_id, endpoint_ref, initiator=None): @@ -248,6 +269,7 @@ class Manager(manager.Manager): ref = self.driver.update_endpoint(endpoint_id, endpoint_ref) notifications.Audit.updated(self._ENDPOINT, endpoint_id, initiator) self.get_endpoint.invalidate(self, endpoint_id) + COMPUTED_CATALOG_REGION.invalidate() return ref def delete_endpoint(self, endpoint_id, initiator=None): @@ -255,6 +277,7 @@ class Manager(manager.Manager): ret = self.driver.delete_endpoint(endpoint_id) notifications.Audit.deleted(self._ENDPOINT, endpoint_id, initiator) self.get_endpoint.invalidate(self, endpoint_id) + COMPUTED_CATALOG_REGION.invalidate() return ret except exception.NotFound: raise exception.EndpointNotFound(endpoint_id=endpoint_id) @@ -270,12 +293,96 @@ class Manager(manager.Manager): def list_endpoints(self, hints=None): return self.driver.list_endpoints(hints or driver_hints.Hints()) + @MEMOIZE_COMPUTED_CATALOG def get_catalog(self, user_id, tenant_id): try: return self.driver.get_catalog(user_id, tenant_id) except exception.NotFound: raise exception.NotFound('Catalog not found for user and tenant') + @MEMOIZE_COMPUTED_CATALOG + def get_v3_catalog(self, user_id, tenant_id): + return self.driver.get_v3_catalog(user_id, tenant_id) + + def add_endpoint_to_project(self, endpoint_id, project_id): + self.driver.add_endpoint_to_project(endpoint_id, project_id) + COMPUTED_CATALOG_REGION.invalidate() + + def remove_endpoint_from_project(self, endpoint_id, project_id): + self.driver.remove_endpoint_from_project(endpoint_id, project_id) + COMPUTED_CATALOG_REGION.invalidate() + + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + self.driver.add_endpoint_group_to_project( + endpoint_group_id, project_id) + COMPUTED_CATALOG_REGION.invalidate() + + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + self.driver.remove_endpoint_group_from_project( + endpoint_group_id, project_id) + COMPUTED_CATALOG_REGION.invalidate() + + def get_endpoint_groups_for_project(self, project_id): + # recover the project endpoint group memberships and for each + # membership recover the endpoint group + self.resource_api.get_project(project_id) + try: + refs = self.list_endpoint_groups_for_project(project_id) + endpoint_groups = [self.get_endpoint_group( + ref['endpoint_group_id']) for ref in refs] + return endpoint_groups + except exception.EndpointGroupNotFound: + return [] + + def get_endpoints_filtered_by_endpoint_group(self, endpoint_group_id): + endpoints = self.list_endpoints() + filters = self.get_endpoint_group(endpoint_group_id)['filters'] + filtered_endpoints = [] + + for endpoint in endpoints: + is_candidate = True + for key, value in filters.items(): + if endpoint[key] != value: + is_candidate = False + break + if is_candidate: + filtered_endpoints.append(endpoint) + return filtered_endpoints + + def list_endpoints_for_project(self, project_id): + """List all endpoints associated with a project. + + :param project_id: project identifier to check + :type project_id: string + :returns: a list of endpoint ids or an empty list. + + """ + refs = self.driver.list_endpoints_for_project(project_id) + filtered_endpoints = {} + for ref in refs: + try: + endpoint = self.get_endpoint(ref['endpoint_id']) + filtered_endpoints.update({ref['endpoint_id']: endpoint}) + except exception.EndpointNotFound: + # remove bad reference from association + self.remove_endpoint_from_project(ref['endpoint_id'], + project_id) + + # need to recover endpoint_groups associated with project + # then for each endpoint group return the endpoints. + endpoint_groups = self.get_endpoint_groups_for_project(project_id) + for endpoint_group in endpoint_groups: + endpoint_refs = self.get_endpoints_filtered_by_endpoint_group( + endpoint_group['id']) + # now check if any endpoints for current endpoint group are not + # contained in the list of filtered endpoints + for endpoint_ref in endpoint_refs: + if endpoint_ref['id'] not in filtered_endpoints: + filtered_endpoints[endpoint_ref['id']] = endpoint_ref + + return filtered_endpoints + @six.add_metaclass(abc.ABCMeta) class CatalogDriverV8(object): @@ -304,8 +411,9 @@ class CatalogDriverV8(object): def create_region(self, region_ref): """Creates a new region. - :raises: keystone.exception.Conflict - :raises: keystone.exception.RegionNotFound (if parent region invalid) + :raises keystone.exception.Conflict: If the region already exists. + :raises keystone.exception.RegionNotFound: If the parent region + is invalid. """ raise exception.NotImplemented() # pragma: no cover @@ -328,7 +436,7 @@ class CatalogDriverV8(object): """Get region by id. :returns: region_ref dict - :raises: keystone.exception.RegionNotFound + :raises keystone.exception.RegionNotFound: If the region doesn't exist. """ raise exception.NotImplemented() # pragma: no cover @@ -338,7 +446,7 @@ class CatalogDriverV8(object): """Update region by id. :returns: region_ref dict - :raises: keystone.exception.RegionNotFound + :raises keystone.exception.RegionNotFound: If the region doesn't exist. """ raise exception.NotImplemented() # pragma: no cover @@ -347,7 +455,7 @@ class CatalogDriverV8(object): def delete_region(self, region_id): """Deletes an existing region. - :raises: keystone.exception.RegionNotFound + :raises keystone.exception.RegionNotFound: If the region doesn't exist. """ raise exception.NotImplemented() # pragma: no cover @@ -356,7 +464,7 @@ class CatalogDriverV8(object): def create_service(self, service_id, service_ref): """Creates a new service. - :raises: keystone.exception.Conflict + :raises keystone.exception.Conflict: If a duplicate service exists. """ raise exception.NotImplemented() # pragma: no cover @@ -379,7 +487,8 @@ class CatalogDriverV8(object): """Get service by id. :returns: service_ref dict - :raises: keystone.exception.ServiceNotFound + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -389,7 +498,8 @@ class CatalogDriverV8(object): """Update service by id. :returns: service_ref dict - :raises: keystone.exception.ServiceNotFound + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -398,7 +508,8 @@ class CatalogDriverV8(object): def delete_service(self, service_id): """Deletes an existing service. - :raises: keystone.exception.ServiceNotFound + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -407,8 +518,9 @@ class CatalogDriverV8(object): def create_endpoint(self, endpoint_id, endpoint_ref): """Creates a new endpoint for a service. - :raises: keystone.exception.Conflict, - keystone.exception.ServiceNotFound + :raises keystone.exception.Conflict: If a duplicate endpoint exists. + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -418,7 +530,8 @@ class CatalogDriverV8(object): """Get endpoint by id. :returns: endpoint_ref dict - :raises: keystone.exception.EndpointNotFound + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -441,8 +554,10 @@ class CatalogDriverV8(object): """Get endpoint by id. :returns: endpoint_ref dict - :raises: keystone.exception.EndpointNotFound - keystone.exception.ServiceNotFound + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. + :raises keystone.exception.ServiceNotFound: If the service doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -451,7 +566,8 @@ class CatalogDriverV8(object): def delete_endpoint(self, endpoint_id): """Deletes an endpoint for a service. - :raises: keystone.exception.EndpointNotFound + :raises keystone.exception.EndpointNotFound: If the endpoint doesn't + exist. """ raise exception.NotImplemented() # pragma: no cover @@ -476,7 +592,7 @@ class CatalogDriverV8(object): :returns: A nested dict representing the service catalog or an empty dict. - :raises: keystone.exception.NotFound + :raises keystone.exception.NotFound: If the endpoint doesn't exist. """ raise exception.NotImplemented() # pragma: no cover @@ -508,7 +624,7 @@ class CatalogDriverV8(object): }] :returns: A list representing the service catalog or an empty list - :raises: keystone.exception.NotFound + :raises keystone.exception.NotFound: If the endpoint doesn't exist. """ v2_catalog = self.get_catalog(user_id, tenant_id) @@ -544,5 +660,235 @@ class CatalogDriverV8(object): return v3_catalog + @abc.abstractmethod + def add_endpoint_to_project(self, endpoint_id, project_id): + """Create an endpoint to project association. + + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param project_id: identity of the project to be associated with + :type project_id: string + :raises: keystone.exception.Conflict: If the endpoint was already + added to project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_from_project(self, endpoint_id, project_id): + """Removes an endpoint to project association. + + :param endpoint_id: identity of endpoint to remove + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint was not found + in the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_endpoint_in_project(self, endpoint_id, project_id): + """Checks if an endpoint is associated with a project. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint was not found + in the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints_for_project(self, project_id): + """List all endpoints associated with a project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: a list of identity endpoint ids or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_for_endpoint(self, endpoint_id): + """List all projects associated with an endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: a list of projects or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_endpoint(self, endpoint_id): + """Removes all the endpoints to project association with endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def delete_association_by_project(self, project_id): + """Removes all the endpoints to project association with project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def create_endpoint_group(self, endpoint_group): + """Create an endpoint group. + + :param endpoint_group: endpoint group to create + :type endpoint_group: dictionary + :raises: keystone.exception.Conflict: If a duplicate endpoint group + already exists. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group(self, endpoint_group_id): + """Get an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + """Update an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param endpoint_group: A full or partial endpoint_group + :type endpoint_group: dictionary + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group(self, endpoint_group_id): + """Delete an endpoint group. + + :param endpoint_group_id: identity of endpoint group to delete + :type endpoint_group_id: string + :raises keystone.exception.NotFound: If the endpoint group was not + found. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + """Adds an endpoint group to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.Conflict: If the endpoint group was already + added to the project. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + """Get endpoint group to project association. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.NotFound: If the endpoint group to the + project association was not found. + :returns: a project endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups(self): + """List all endpoint groups. + + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups_for_project(self, project_id): + """List all endpoint group to project associations for a project. + + :param project_id: identity of project to associate + :type project_id: string + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + """List all projects associated with endpoint group. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + """Remove an endpoint to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises keystone.exception.NotFound: If endpoint group project + association was not found. + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group_association_by_project(self, project_id): + """Remove endpoint group to project associations. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover Driver = manager.create_legacy_driver(CatalogDriverV8) diff --git a/keystone-moon/keystone/catalog/routers.py b/keystone-moon/keystone/catalog/routers.py index f3bd988b..8c6e96f0 100644 --- a/keystone-moon/keystone/catalog/routers.py +++ b/keystone-moon/keystone/catalog/routers.py @@ -12,15 +12,72 @@ # License for the specific language governing permissions and limitations # under the License. +import functools + from keystone.catalog import controllers +from keystone.common import json_home from keystone.common import router from keystone.common import wsgi +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-EP-FILTER', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-EP-FILTER', extension_version='1.0') + +ENDPOINT_GROUP_PARAMETER_RELATION = build_parameter_relation( + parameter_name='endpoint_group_id') + + class Routers(wsgi.RoutersBase): + """API for the keystone catalog. + + The API Endpoint Filter looks like:: + + PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + GET /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + GET /OS-EP-FILTER/endpoints/{endpoint_id}/projects + GET /OS-EP-FILTER/projects/{project_id}/endpoints + GET /OS-EP-FILTER/projects/{project_id}/endpoint_groups + + GET /OS-EP-FILTER/endpoint_groups + POST /OS-EP-FILTER/endpoint_groups + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + PATCH /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/projects + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group_id}/endpoints + + PUT /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + GET /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + DELETE /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects/ + {project_id} + + """ + + PATH_PREFIX = '/OS-EP-FILTER' + PATH_PROJECT_ENDPOINT = '/projects/{project_id}/endpoints/{endpoint_id}' + PATH_ENDPOINT_GROUPS = '/endpoint_groups/{endpoint_group_id}' + PATH_ENDPOINT_GROUP_PROJECTS = PATH_ENDPOINT_GROUPS + ( + '/projects/{project_id}') def append_v3_routers(self, mapper, routers): regions_controller = controllers.RegionV3() + endpoint_filter_controller = controllers.EndpointFilterV3Controller() + endpoint_group_controller = controllers.EndpointGroupV3Controller() + project_endpoint_group_controller = ( + controllers.ProjectEndpointGroupV3Controller()) routers.append(router.Router(regions_controller, 'regions', 'region', resource_descriptions=self.v3_resources)) @@ -38,3 +95,88 @@ class Routers(wsgi.RoutersBase): routers.append(router.Router(controllers.EndpointV3(), 'endpoints', 'endpoint', resource_descriptions=self.v3_resources)) + + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + '/endpoints/{endpoint_id}/projects', + get_action='list_projects_for_endpoint', + rel=build_resource_relation(resource_name='endpoint_projects'), + path_vars={ + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + }) + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + self.PATH_PROJECT_ENDPOINT, + get_head_action='check_endpoint_in_project', + put_action='add_endpoint_to_project', + delete_action='remove_endpoint_from_project', + rel=build_resource_relation(resource_name='project_endpoint'), + path_vars={ + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + '/projects/{project_id}/endpoints', + get_action='list_endpoints_for_project', + rel=build_resource_relation(resource_name='project_endpoints'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + '/projects/{project_id}/endpoint_groups', + get_action='list_endpoint_groups_for_project', + rel=build_resource_relation( + resource_name='project_endpoint_groups'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + '/endpoint_groups', + get_action='list_endpoint_groups', + post_action='create_endpoint_group', + rel=build_resource_relation(resource_name='endpoint_groups')) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS, + get_head_action='get_endpoint_group', + patch_action='update_endpoint_group', + delete_action='delete_endpoint_group', + rel=build_resource_relation(resource_name='endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, project_endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUP_PROJECTS, + get_head_action='get_endpoint_group_in_project', + put_action='add_endpoint_group_to_project', + delete_action='remove_endpoint_group_from_project', + rel=build_resource_relation( + resource_name='endpoint_group_to_project_association'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS + ( + '/projects'), + get_action='list_projects_associated_with_endpoint_group', + rel=build_resource_relation( + resource_name='projects_associated_with_endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS + ( + '/endpoints'), + get_action='list_endpoints_associated_with_endpoint_group', + rel=build_resource_relation( + resource_name='endpoints_in_endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) diff --git a/keystone-moon/keystone/catalog/schema.py b/keystone-moon/keystone/catalog/schema.py index 671f1233..b9643131 100644 --- a/keystone-moon/keystone/catalog/schema.py +++ b/keystone-moon/keystone/catalog/schema.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.common import validation from keystone.common.validation import parameter_types @@ -96,3 +97,23 @@ endpoint_update = { 'minProperties': 1, 'additionalProperties': True } + +_endpoint_group_properties = { + 'description': validation.nullable(parameter_types.description), + 'filters': { + 'type': 'object' + }, + 'name': parameter_types.name +} + +endpoint_group_create = { + 'type': 'object', + 'properties': _endpoint_group_properties, + 'required': ['name', 'filters'] +} + +endpoint_group_update = { + 'type': 'object', + 'properties': _endpoint_group_properties, + 'minProperties': 1 +} -- cgit 1.2.3-korg