Source code for keystone.resource.backends.base

# 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
# 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.

import abc
import copy

from oslo_log import log
from oslo_log import versionutils
import six

import keystone.conf
from keystone import exception
from keystone.i18n import _, _LE


CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)


[docs]def get_project_from_domain(domain_ref): """Create a project ref from the provided domain ref.""" project_ref = domain_ref.copy() project_ref['is_domain'] = True project_ref['domain_id'] = None project_ref['parent_id'] = None return project_ref # The ResourceDriverBase class is the set of driver methods from earlier # drivers that we still support, that have not been removed or modified. This # class is then used to created the augmented V8 and V9 version abstract driver # classes, without having to duplicate a lot of abstract method signatures. # If you remove a method from V9, then move the abstract methods from this Base # class to the V8 class. Do not modify any of the method signatures in the Base # class - changes should only be made in the V8 and subsequent classes. # Starting with V9, some drivers use a special value to represent a domain_id # of None. See comment in Project class of resource/backends/sql.py for more # details.
NULL_DOMAIN_ID = '<<keystone.domain.root>>' @six.add_metaclass(abc.ABCMeta)
[docs]class ResourceDriverBase(object): def _get_list_limit(self): return CONF.resource.list_limit or CONF.list_limit # project crud @abc.abstractmethod
[docs] def list_projects(self, hints): """List projects in the system. :param hints: filter hints which the driver should implement if at all possible. :returns: a list of project_refs or an empty list. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_projects_from_ids(self, project_ids): """List projects for the provided list of ids. :param project_ids: list of ids :returns: a list of project_refs. This method is used internally by the assignment manager to bulk read a set of projects given their ids. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_project_ids_from_domain_ids(self, domain_ids): """List project ids for the provided list of domain ids. :param domain_ids: list of domain ids :returns: a list of project ids owned by the specified domain ids. This method is used internally by the assignment manager to bulk read a set of project ids given a list of domain ids. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_projects_in_domain(self, domain_id): """List projects in the domain. :param domain_id: the driver MUST only return projects within this domain. :returns: a list of project_refs or an empty list. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def get_project(self, project_id): """Get a project by ID. :returns: project_ref :raises keystone.exception.ProjectNotFound: if project_id does not exist """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def update_project(self, project_id, project): """Update an existing project. :raises keystone.exception.ProjectNotFound: if project_id does not exist :raises keystone.exception.Conflict: if project name already exists """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def delete_project(self, project_id): """Delete an existing project. :raises keystone.exception.ProjectNotFound: if project_id does not exist """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_project_parents(self, project_id): """List all parents from a project by its ID. :param project_id: the driver will list the parents of this project. :returns: a list of project_refs or an empty list. :raises keystone.exception.ProjectNotFound: if project_id does not exist """ raise exception.NotImplemented()
@abc.abstractmethod
[docs] def list_projects_in_subtree(self, project_id): """List all projects in the subtree of a given project. :param project_id: the driver will get the subtree under this project. :returns: a list of project_refs or an empty list :raises keystone.exception.ProjectNotFound: if project_id does not exist """ raise exception.NotImplemented()
@abc.abstractmethod
[docs] def is_leaf_project(self, project_id): """Check if a project is a leaf in the hierarchy. :param project_id: the driver will check if this project is a leaf in the hierarchy. :raises keystone.exception.ProjectNotFound: if project_id does not exist """ raise exception.NotImplemented()
def _validate_default_domain(self, ref): """Validate that either the default domain or nothing is specified. Also removes the domain from the ref so that LDAP doesn't have to persist the attribute. """ ref = ref.copy() domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) self._validate_default_domain_id(domain_id) return ref def _validate_default_domain_id(self, domain_id): """Validate that the domain ID belongs to the default domain.""" if domain_id != CONF.identity.default_domain_id: raise exception.DomainNotFound(domain_id=domain_id)
[docs]class ResourceDriverV8(ResourceDriverBase): """Removed or redefined methods from V8. Move the abstract methods of any methods removed or modified in later versions of the driver from ResourceDriverBase to here. We maintain this so that legacy drivers, which will be a subclass of ResourceDriverV8, can still reference them. """ @abc.abstractmethod
[docs] def create_project(self, tenant_id, tenant): """Create a new project. :param tenant_id: This parameter can be ignored. :param dict tenant: The new project Project schema:: type: object properties: id: type: string name: type: string domain_id: type: string description: type: string enabled: type: boolean parent_id: type: string is_domain: type: boolean required: [id, name, domain_id] additionalProperties: true If project doesn't match the schema the behavior is undefined. The driver can impose requirements such as the maximum length of a field. If these requirements are not met the behavior is undefined. :raises keystone.exception.Conflict: if the project id already exists or the name already exists for the domain_id. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def get_project_by_name(self, tenant_name, domain_id): """Get a tenant by name. :returns: tenant_ref :raises keystone.exception.ProjectNotFound: if a project with the tenant_name does not exist within the domain """ raise exception.NotImplemented() # pragma: no cover # Domain management functions for backends that only allow a single # domain. Although we no longer use this, a custom legacy driver might # have made use of it, so keep it here in case.
def _set_default_domain(self, ref): """If the domain ID has not been set, set it to the default.""" if isinstance(ref, dict): if 'domain_id' not in ref: ref = ref.copy() ref['domain_id'] = CONF.identity.default_domain_id return ref elif isinstance(ref, list): return [self._set_default_domain(x) for x in ref] else: raise ValueError(_('Expected dict or list: %s') % type(ref)) # domain crud @abc.abstractmethod
[docs] def create_domain(self, domain_id, domain): """Create a new domain. :raises keystone.exception.Conflict: if the domain_id or domain name already exists """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_domains(self, hints): """List domains in the system. :param hints: filter hints which the driver should implement if at all possible. :returns: a list of domain_refs or an empty list. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_domains_from_ids(self, domain_ids): """List domains for the provided list of ids. :param domain_ids: list of ids :returns: a list of domain_refs. This method is used internally by the assignment manager to bulk read a set of domains given their ids. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def get_domain(self, domain_id): """Get a domain by ID. :returns: domain_ref :raises keystone.exception.DomainNotFound: if domain_id does not exist """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def get_domain_by_name(self, domain_name): """Get a domain by name. :returns: domain_ref :raises keystone.exception.DomainNotFound: if domain_name does not exist """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def update_domain(self, domain_id, domain): """Update an existing domain. :raises keystone.exception.DomainNotFound: if domain_id does not exist :raises keystone.exception.Conflict: if domain name already exists """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def delete_domain(self, domain_id): """Delete an existing domain. :raises keystone.exception.DomainNotFound: if domain_id does not exist """ raise exception.NotImplemented() # pragma: no cover
[docs]class ResourceDriverV9(ResourceDriverBase): """New or redefined methods from V8. Add any new V9 abstract methods (or those with modified signatures) to this class. """ @abc.abstractmethod
[docs] def create_project(self, project_id, project): """Create a new project. :param project_id: This parameter can be ignored. :param dict project: The new project Project schema:: type: object properties: id: type: string name: type: string domain_id: type: [string, null] description: type: string enabled: type: boolean parent_id: type: string is_domain: type: boolean required: [id, name, domain_id] additionalProperties: true If the project doesn't match the schema the behavior is undefined. The driver can impose requirements such as the maximum length of a field. If these requirements are not met the behavior is undefined. :raises keystone.exception.Conflict: if the project id already exists or the name already exists for the domain_id. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def get_project_by_name(self, project_name, domain_id): """Get a project by name. :returns: project_ref :raises keystone.exception.ProjectNotFound: if a project with the project_name does not exist within the domain """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def delete_projects_from_ids(self, project_ids): """Delete a given list of projects. Deletes a list of projects. Ensures no project on the list exists after it is successfully called. If an empty list is provided, the it is silently ignored. In addition, if a project ID in the list of project_ids is not found in the backend, no exception is raised, but a message is logged. """ raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
[docs] def list_projects_acting_as_domain(self, hints): """List all projects acting as domains. :param hints: filter hints which the driver should implement if at all possible. :returns: a list of project_refs or an empty list. """ raise exception.NotImplemented() # pragma: no cover
[docs]class V9ResourceWrapperForV8Driver(ResourceDriverV9): """Wrapper class to supported a V8 legacy driver. In order to support legacy drivers without having to make the manager code driver-version aware, we wrap legacy drivers so that they look like the latest version. For the various changes made in a new driver, here are the actions needed in this wrapper: Method removed from new driver - remove the call-through method from this class, since the manager will no longer be calling it. Method signature (or meaning) changed - wrap the old method in a new signature here, and munge the input and output parameters accordingly. New method added to new driver - add a method to implement the new functionality here if possible. If that is not possible, then return NotImplemented, since we do not guarantee to support new functionality with legacy drivers. This wrapper contains the following support for newer manager code: - The current manager code expects domains to be represented as projects acting as domains, something that may not be possible in a legacy driver. Hence the wrapper will map any calls for projects acting as a domain back onto the driver domain methods. The caveat for this, is that this assumes that there can not be a clash between a project_id and a domain_id, in which case it may not be able to locate the correct entry. """ @versionutils.deprecated( as_of=versionutils.deprecated.MITAKA, what='keystone.resource.ResourceDriverV8', in_favor_of='keystone.resource.ResourceDriverV9', remove_in=+2) def __init__(self, wrapped_driver): self.driver = wrapped_driver def _get_domain_from_project(self, project_ref): """Create a domain ref from a project ref. Based on the provided project ref (or partial ref), creates a domain ref, so that the result can be passed to the driver domain methods. """ domain_ref = project_ref.copy() for k in ['parent_id', 'domain_id', 'is_domain']: domain_ref.pop(k, None) return domain_ref
[docs] def get_project_by_name(self, project_name, domain_id): if domain_id is None: try: domain_ref = self.driver.get_domain_by_name(project_name) return get_project_from_domain(domain_ref) except exception.DomainNotFound: raise exception.ProjectNotFound(project_id=project_name) else: return self.driver.get_project_by_name(project_name, domain_id)
[docs] def create_project(self, project_id, project): if project['is_domain']: new_domain = self._get_domain_from_project(project) domain_ref = self.driver.create_domain(project_id, new_domain) return get_project_from_domain(domain_ref) else: return self.driver.create_project(project_id, project)
[docs] def list_projects(self, hints): """List projects and/or domains. We use the hints filter to determine whether we are listing projects, domains or both. If the filter includes domain_id==None, then we should only list domains (convert to a project acting as a domain) since regular projects always have a non-None value for domain_id. Likewise, if the filter includes domain_id==<non-None value>, then we should only list projects. If there is no domain_id filter, then we need to do a combained listing of domains and projects, converting domains to projects acting as a domain. """ domain_listing_filter = None for f in hints.filters: if (f['name'] == 'domain_id'): domain_listing_filter = f if domain_listing_filter is not None: if domain_listing_filter['value'] is not None: proj_list = self.driver.list_projects(hints) else: domains = self.driver.list_domains(hints) proj_list = [get_project_from_domain(p) for p in domains] hints.filters.remove(domain_listing_filter) return proj_list else: # No domain_id filter, so combine domains and projects. Although # we hand any remaining filters into each driver, since each filter # might need to be carried out more than once, we use copies of the # filters, allowing the original filters to be passed back up to # controller level where a final filter will occur. local_hints = copy.deepcopy(hints) proj_list = self.driver.list_projects(local_hints) local_hints = copy.deepcopy(hints) domains = self.driver.list_domains(local_hints) for domain in domains: proj_list.append(get_project_from_domain(domain)) return proj_list
[docs] def list_projects_from_ids(self, project_ids): return [self.get_project(id) for id in project_ids]
[docs] def list_project_ids_from_domain_ids(self, domain_ids): return self.driver.list_project_ids_from_domain_ids(domain_ids)
[docs] def list_projects_in_domain(self, domain_id): return self.driver.list_projects_in_domain(domain_id)
[docs] def get_project(self, project_id): try: domain_ref = self.driver.get_domain(project_id) return get_project_from_domain(domain_ref) except exception.DomainNotFound: return self.driver.get_project(project_id)
def _is_domain(self, project_id): ref = self.get_project(project_id) return ref.get('is_domain', False)
[docs] def update_project(self, project_id, project): if self._is_domain(project_id): update_domain = self._get_domain_from_project(project) domain_ref = self.driver.update_domain(project_id, update_domain) return get_project_from_domain(domain_ref) else: return self.driver.update_project(project_id, project)
[docs] def delete_project(self, project_id): if self._is_domain(project_id): try: self.driver.delete_domain(project_id) except exception.DomainNotFound: raise exception.ProjectNotFound(project_id=project_id) else: self.driver.delete_project(project_id)
[docs] def delete_projects_from_ids(self, project_ids): raise exception.NotImplemented() # pragma: no cover
[docs] def list_project_parents(self, project_id): """List a project's ancestors. The current manager expects the ancestor tree to end with the project acting as the domain (since that's now the top of the tree), but a legacy driver will not have that top project in their projects table, since it's still in the domain table. Hence we lift the algorithm for traversing up the tree from the driver to here, so that our version of get_project() is called, which will fetch the "project" from the right table. """ project = self.get_project(project_id) parents = [] examined = set() while project.get('parent_id') is not None: if project['id'] in examined: msg = _LE('Circular reference or a repeated ' 'entry found in projects hierarchy - ' '%(project_id)s.') LOG.error(msg, {'project_id': project['id']}) return examined.add(project['id']) parent_project = self.get_project(project['parent_id']) parents.append(parent_project) project = parent_project return parents
[docs] def list_projects_in_subtree(self, project_id): return self.driver.list_projects_in_subtree(project_id)
[docs] def is_leaf_project(self, project_id): return self.driver.is_leaf_project(project_id)
[docs] def list_projects_acting_as_domain(self, hints): refs = self.driver.list_domains(hints) return [get_project_from_domain(p) for p in refs]

Project Source