From 6a38b650c001ec8e6da435856c37a28737401aaf Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Tue, 13 Mar 2012 17:13:02 -0500 Subject: Implement quota classes. Allows entire classes of quotas to be associated with projects, which makes it easier to set specific quotas across multiple projects. TODO: * (?) Adding a mapping between projects and quota classes Change-Id: I6b6477481187d16af225d33c1989430e4071d5a8 --- nova/db/api.py | 33 +++++++++ nova/db/sqlalchemy/api.py | 83 ++++++++++++++++++++++ .../migrate_repo/versions/083_quota_class.py | 61 ++++++++++++++++ nova/db/sqlalchemy/models.py | 25 ++++++- 4 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py (limited to 'nova/db') diff --git a/nova/db/api.py b/nova/db/api.py index 2a38d026f..782bbdcdb 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -927,6 +927,39 @@ def quota_destroy_all_by_project(context, project_id): ################### +def quota_class_create(context, class_name, resource, limit): + """Create a quota class for the given name and resource.""" + return IMPL.quota_class_create(context, class_name, resource, limit) + + +def quota_class_get(context, class_name, resource): + """Retrieve a quota class or raise if it does not exist.""" + return IMPL.quota_class_get(context, class_name, resource) + + +def quota_class_get_all_by_name(context, class_name): + """Retrieve all quotas associated with a given quota class.""" + return IMPL.quota_class_get_all_by_name(context, class_name) + + +def quota_class_update(context, class_name, resource, limit): + """Update a quota class or raise if it does not exist.""" + return IMPL.quota_class_update(context, class_name, resource, limit) + + +def quota_class_destroy(context, class_name, resource): + """Destroy the quota class or raise if it does not exist.""" + return IMPL.quota_class_destroy(context, class_name, resource) + + +def quota_class_destroy_all_by_name(context, class_name): + """Destroy all quotas associated with a given quota class.""" + return IMPL.quota_class_destroy_all_by_name(context, class_name) + + +################### + + def volume_allocate_iscsi_target(context, volume_id, host): """Atomically allocate a free iscsi_target from the pool.""" return IMPL.volume_allocate_iscsi_target(context, volume_id, host) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9eda43941..8b966336c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -89,6 +89,15 @@ def authorize_user_context(context, user_id): raise exception.NotAuthorized() +def authorize_quota_class_context(context, class_name): + """Ensures a request has permission to access the given quota class.""" + if is_user_context(context): + if not context.quota_class: + raise exception.NotAuthorized() + elif context.quota_class != class_name: + raise exception.NotAuthorized() + + def require_admin_context(f): """Decorator to require admin request context. @@ -2291,6 +2300,80 @@ def quota_destroy_all_by_project(context, project_id): ################### +@require_context +def quota_class_get(context, class_name, resource, session=None): + result = model_query(context, models.QuotaClass, session=session, + read_deleted="no").\ + filter_by(class_name=class_name).\ + filter_by(resource=resource).\ + first() + + if not result: + raise exception.QuotaClassNotFound(class_name=class_name) + + return result + + +@require_context +def quota_class_get_all_by_name(context, class_name): + authorize_quota_class_context(context, class_name) + + rows = model_query(context, models.QuotaClass, read_deleted="no").\ + filter_by(class_name=class_name).\ + all() + + result = {'class_name': class_name} + for row in rows: + result[row.resource] = row.hard_limit + + return result + + +@require_admin_context +def quota_class_create(context, class_name, resource, limit): + quota_class_ref = models.QuotaClass() + quota_class_ref.class_name = class_name + quota_class_ref.resource = resource + quota_class_ref.hard_limit = limit + quota_class_ref.save() + return quota_class_ref + + +@require_admin_context +def quota_class_update(context, class_name, resource, limit): + session = get_session() + with session.begin(): + quota_class_ref = quota_class_get(context, class_name, resource, + session=session) + quota_class_ref.hard_limit = limit + quota_class_ref.save(session=session) + + +@require_admin_context +def quota_class_destroy(context, class_name, resource): + session = get_session() + with session.begin(): + quota_class_ref = quota_class_get(context, class_name, resource, + session=session) + quota_class_ref.delete(session=session) + + +@require_admin_context +def quota_class_destroy_all_by_name(context, class_name): + session = get_session() + with session.begin(): + quota_classes = model_query(context, models.QuotaClass, + session=session, read_deleted="no").\ + filter_by(class_name=class_name).\ + all() + + for quota_class_ref in quota_classes: + quota_class_ref.delete(session=session) + + +################### + + @require_admin_context def volume_allocate_iscsi_target(context, volume_id, host): session = get_session() diff --git a/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py b/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py new file mode 100644 index 000000000..37d9695d7 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py @@ -0,0 +1,61 @@ +# Copyright 2012 OpenStack LLC. +# +# 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. + +from sqlalchemy import Boolean, Column, DateTime +from sqlalchemy import MetaData, Integer, String, Table + +from nova import log as logging + +LOG = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + # New table + quota_classes = Table('quota_classes', meta, + Column('created_at', DateTime(timezone=False)), + Column('updated_at', DateTime(timezone=False)), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('id', Integer(), primary_key=True), + Column('class_name', + String(length=255, convert_unicode=True, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False), index=True), + Column('resource', + String(length=255, convert_unicode=True, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)), + Column('hard_limit', Integer(), nullable=True), + ) + + try: + quota_classes.create() + except Exception: + LOG.error(_("Table |%s| not created!"), repr(quota_classes)) + raise + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + quota_classes = Table('quota_classes', meta, autoload=True) + try: + quota_classes.drop() + except Exception: + LOG.error(_("quota_classes table not dropped")) + raise diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 3865cd5d0..634f04dbc 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -419,9 +419,11 @@ class VolumeTypeExtraSpecs(BASE, NovaBase): class Quota(BASE, NovaBase): """Represents a single quota override for a project. - If there is no row for a given project id and resource, then - the default for the deployment is used. If the row is present - but the hard limit is Null, then the resource is unlimited. + If there is no row for a given project id and resource, then the + default for the quota class is used. If there is no row for a + given quota class and resource, then the default for the + deployment is used. If the row is present but the hard limit is + Null, then the resource is unlimited. """ __tablename__ = 'quotas' @@ -433,6 +435,23 @@ class Quota(BASE, NovaBase): hard_limit = Column(Integer, nullable=True) +class QuotaClass(BASE, NovaBase): + """Represents a single quota override for a quota class. + + If there is no row for a given quota class and resource, then the + default for the deployment is used. If the row is present but the + hard limit is Null, then the resource is unlimited. + """ + + __tablename__ = 'quota_classes' + id = Column(Integer, primary_key=True) + + class_name = Column(String(255), index=True) + + resource = Column(String(255)) + hard_limit = Column(Integer, nullable=True) + + class Snapshot(BASE, NovaBase): """Represents a block storage device that can be attached to a vm.""" __tablename__ = 'snapshots' -- cgit