From 7229381c38e071d7709d1cdc970ae7f954b061a1 Mon Sep 17 00:00:00 2001 From: wangxiyuan Date: Thu, 30 Nov 2017 15:11:40 +0800 Subject: [PATCH] Add db operation for unified limit This patch adds the db operation part for unified limit Co-Authored-By: Colleen Murphy Change-Id: Ifb2bb54b35ea0d1573cdb9cdab77dfdeb8f22446 bp: unified-limits --- keystone/common/sql/core.py | 2 + keystone/exception.py | 13 ++ keystone/limit/__init__.py | 0 keystone/limit/backends/__init__.py | 0 keystone/limit/backends/base.py | 162 ++++++++++++++++++++ keystone/limit/backends/sql.py | 223 ++++++++++++++++++++++++++++ 6 files changed, 400 insertions(+) create mode 100644 keystone/limit/__init__.py create mode 100644 keystone/limit/backends/__init__.py create mode 100644 keystone/limit/backends/base.py create mode 100644 keystone/limit/backends/sql.py diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py index e34afd04ca..caf023612f 100644 --- a/keystone/common/sql/core.py +++ b/keystone/common/sql/core.py @@ -328,6 +328,8 @@ class _WontMatch(Exception): won't match any value in the column in the table. """ + if value is None: + return col = col_attr.property.columns[0] if isinstance(col.type, sql.types.Boolean): # The column is a Boolean, we should have already validated input. diff --git a/keystone/exception.py b/keystone/exception.py index 72ab0cd22f..d6fae251d1 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -456,6 +456,19 @@ class PublicIDNotFound(NotFound): message_format = "%(id)s" +class RegisteredLimitNotFound(NotFound): + message_format = _("Could not find registered limit for %(id)s.") + + +class LimitNotFound(NotFound): + message_format = _("Could not find limit for %(id)s.") + + +class NoLimitReference(Forbidden): + message_format = _("Unable to create a limit that doesn't have a " + "corresponding registered limit") + + class DomainConfigNotFound(NotFound): message_format = _('Could not find %(group_or_option)s in domain ' 'configuration for domain %(domain_id)s.') diff --git a/keystone/limit/__init__.py b/keystone/limit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/limit/backends/__init__.py b/keystone/limit/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/limit/backends/base.py b/keystone/limit/backends/base.py new file mode 100644 index 0000000000..54444ffce0 --- /dev/null +++ b/keystone/limit/backends/base.py @@ -0,0 +1,162 @@ +# Copyright 2017 SUSE Linux Gmbh +# Copyright 2017 Huawei +# +# 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 + +from oslo_log import log +import six + +from keystone import exception + + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class UnifiedLimitDriverBase(object): + + @abc.abstractmethod + def create_registered_limits(self, registered_limits): + """Create new registered limits. + + :param registered_limits: a list of dictionaries representing limits to + create. + + :returns: all the registered limits. + :raises keystone.exception.Conflict: If a duplicate registered limit + exists. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_registered_limits(self, registered_limits): + """Update existing registered limits. + + :param registered_limits: a list of dictionaries representing limits to + update. + + :returns: all the registered limits. + :raises keystone.exception.RegisteredLimitNotFound: If registered limit + doesn't exist. + :raises keystone.exception.Conflict: If update to a duplicate + registered limit. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_registered_limits(self, hints): + """List all registered limits. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: a list of dictionaries or an empty registered limit. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_registered_limit(self, registered_limit_id): + """Get a registered limit. + + :param registered_limit_id: the registered limit id to get. + + :returns: a dictionary representing a registered limit reference. + :raises keystone.exception.RegisteredLimitNotFound: If registered limit + doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_registered_limit(self, registered_limit_id): + """Delete an existing registered limit. + + :param registered_limit_id: the registered limit id to delete. + + :raises keystone.exception.RegisteredLimitNotFound: If registered limit + doesn't exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_limits(self, limits): + """Create new limits. + + :param limits: a list of dictionaries representing limits to create. + + :returns: all the limits. + :raises keystone.exception.Conflict: If a duplicate limit exists. + :raises keystone.exception.NoLimitReference: If no reference registered + limit exists. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_limits(self, limits): + """Update existing limits. + + :param limits: a list of dictionaries representing limits to update. + + :returns: all the limits. + :raises keystone.exception.LimitNotFound: If limit doesn't + exist. + :raises keystone.exception.Conflict: If update to a duplicate limit. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_limits(self, hints): + """List all limits. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: a list of dictionaries or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_limit(self, limit_id): + """Get a limit. + + :param limit_id: the limit id to get. + + :returns: a dictionary representing a limit reference. + :raises keystone.exception.LimitNotFound: If limit doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_limit(self, limit_id): + """Delete an existing limit. + + :param limit_id: the limit id to delete. + + :raises keystone.exception.LimitNotFound: If limit doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone/limit/backends/sql.py b/keystone/limit/backends/sql.py new file mode 100644 index 0000000000..d612070c6e --- /dev/null +++ b/keystone/limit/backends/sql.py @@ -0,0 +1,223 @@ +# Copyright 2017 SUSE Linux Gmbh +# Copyright 2017 Huawei +# +# 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 copy + +from oslo_db import exception as db_exception +import sqlalchemy + +from keystone.common import driver_hints +from keystone.common import sql +from keystone import exception +from keystone.i18n import _ +from keystone.limit.backends import base + + +class RegisteredLimitModel(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'registered_limit' + attributes = [ + 'id', + 'service_id', + 'region_id', + 'resource_name', + 'default_limit' + ] + + id = sql.Column(sql.String(length=64), primary_key=True) + service_id = sql.Column(sql.String(255), + sql.ForeignKey('service.id')) + region_id = sql.Column(sql.String(64), + sql.ForeignKey('region.id'), nullable=True) + resource_name = sql.Column(sql.String(255)) + default_limit = sql.Column(sql.Integer, nullable=False) + + __table_args__ = ( + sqlalchemy.UniqueConstraint('service_id', + 'region_id', + 'resource_name'),) + + +class LimitModel(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'limit' + attributes = [ + 'id', + 'project_id', + 'service_id', + 'region_id', + 'resource_name', + 'resource_limit' + ] + + id = sql.Column(sql.String(length=64), primary_key=True) + project_id = sql.Column(sql.String(64), + sql.ForeignKey('project.id')) + service_id = sql.Column(sql.String(255)) + region_id = sql.Column(sql.String(64), nullable=True) + resource_name = sql.Column(sql.String(255)) + resource_limit = sql.Column(sql.Integer, nullable=False) + + __table_args__ = ( + sqlalchemy.ForeignKeyConstraint(['service_id', + 'region_id', + 'resource_name'], + ['registered_limit.service_id', + 'registered_limit.region_id', + 'registered_limit.resource_name']), + sqlalchemy.UniqueConstraint('project_id', + 'service_id', + 'region_id', + 'resource_name'),) + + +class UnifiedLimit(base.UnifiedLimitDriverBase): + + def _check_unified_limit_without_region(self, unified_limit, + is_registered_limit=True): + hints = driver_hints.Hints() + hints.add_filter('service_id', unified_limit['service_id']) + hints.add_filter('resource_name', unified_limit['resource_name']) + hints.add_filter('region_id', None) + if not is_registered_limit: + # For limit, we should ensure: + # 1. there is no duplicate entry. + # 2. there is a registered limit reference. + reference_hints = copy.deepcopy(hints) + hints.add_filter('project_id', unified_limit['project_id']) + with sql.session_for_read() as session: + unified_limits = session.query(LimitModel) + unified_limits = sql.filter_limit_query(LimitModel, + unified_limits, + hints) + with sql.session_for_read() as session: + registered_limits = session.query(RegisteredLimitModel) + registered_limits = sql.filter_limit_query( + RegisteredLimitModel, registered_limits, reference_hints) + if not registered_limits.all(): + raise exception.NoLimitReference + else: + # For registered limit, we should just ensure that there is no + # duplicate entry. + with sql.session_for_read() as session: + unified_limits = session.query(RegisteredLimitModel) + unified_limits = sql.filter_limit_query(RegisteredLimitModel, + unified_limits, + hints) + if unified_limits.all(): + msg = _('Duplicate entry') + limit_type = 'registered_limit' if is_registered_limit else 'limit' + raise exception.Conflict(type=limit_type, details=msg) + + @sql.handle_conflicts(conflict_type='registered_limit') + def create_registered_limits(self, registered_limits): + with sql.session_for_write() as session: + return_ref = [] + for registered_limit in registered_limits: + if registered_limit.get('region_id') is None: + self._check_unified_limit_without_region(registered_limit) + ref = RegisteredLimitModel.from_dict(registered_limit) + session.add(ref) + return_ref.append(ref.to_dict()) + return return_ref + + @sql.handle_conflicts(conflict_type='registered_limit') + def update_registered_limits(self, registered_limits): + with sql.session_for_write() as session: + for registered_limit in registered_limits: + ref = self._get_registered_limit(session, + registered_limit['id']) + old_dict = ref.to_dict() + old_dict.update(registered_limit) + new_registered_limit = RegisteredLimitModel.from_dict(old_dict) + for attr in RegisteredLimitModel.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_registered_limit, attr)) + + @driver_hints.truncated + def list_registered_limits(self, hints): + with sql.session_for_read() as session: + registered_limits = session.query(RegisteredLimitModel) + registered_limits = sql.filter_limit_query(RegisteredLimitModel, + registered_limits, + hints) + return [s.to_dict() for s in registered_limits] + + def _get_registered_limit(self, session, registered_limit_id): + ref = session.query(RegisteredLimitModel).get(registered_limit_id) + if ref is None: + raise exception.RegisteredLimitNotFound(id=registered_limit_id) + return ref + + def get_registered_limit(self, registered_limit_id): + with sql.session_for_read() as session: + return self._get_registered_limit( + session, registered_limit_id).to_dict() + + def delete_registered_limit(self, registered_limit_id): + with sql.session_for_write() as session: + ref = self._get_registered_limit(session, + registered_limit_id) + session.delete(ref) + + @sql.handle_conflicts(conflict_type='limit') + def create_limits(self, limits): + try: + with sql.session_for_write() as session: + return_ref = [] + for limit in limits: + if limit.get('region_id') is None: + self._check_unified_limit_without_region( + limits, is_registered_limit=False) + ref = LimitModel.from_dict(limit) + session.add(ref) + return_ref.append(ref.to_dict()) + return return_ref + except db_exception.DBReferenceError: + raise exception.NoLimitReference() + + @sql.handle_conflicts(conflict_type='limit') + def update_limits(self, limits): + with sql.session_for_write() as session: + for limit in limits: + ref = self._get_limit(session, limit['id']) + old_dict = ref.to_dict() + old_dict['resource_limit'] = limit['resource_limit'] + new_limit = LimitModel.from_dict(old_dict) + ref.resource_limit = new_limit.resource_limit + + @driver_hints.truncated + def list_limits(self, hints): + with sql.session_for_read() as session: + limits = session.query(LimitModel) + limits = sql.filter_limit_query(LimitModel, + limits, + hints) + return [s.to_dict() for s in limits] + + def _get_limit(self, session, limit_id): + ref = session.query(LimitModel).get(limit_id) + if ref is None: + raise exception.LimitNotFound(id=limit_id) + return ref + + def get_limit(self, limit_id): + with sql.session_for_read() as session: + return self._get_limit(session, + limit_id).to_dict() + + def delete_limit(self, limit_id): + with sql.session_for_write() as session: + ref = self._get_limit(session, + limit_id) + session.delete(ref)