254 lines
9.9 KiB
Python
254 lines
9.9 KiB
Python
# 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 os_resource_classes as orc
|
|
from oslo_concurrency import lockutils
|
|
from oslo_db import api as oslo_db_api
|
|
from oslo_db import exception as db_exc
|
|
from oslo_log import log as logging
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import func
|
|
|
|
from placement.db.sqlalchemy import models
|
|
from placement import db_api
|
|
from placement import exception
|
|
|
|
_RC_TBL = models.ResourceClass.__table__
|
|
_RESOURCE_CLASSES_LOCK = 'resource_classes_sync'
|
|
_RESOURCE_CLASSES_SYNCED = False
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class ResourceClass(object):
|
|
|
|
MIN_CUSTOM_RESOURCE_CLASS_ID = 10000
|
|
"""Any user-defined resource classes must have an identifier greater than
|
|
or equal to this number.
|
|
"""
|
|
|
|
# Retry count for handling possible race condition in creating resource
|
|
# class. We don't ever want to hit this, as it is simply a race when
|
|
# creating these classes, but this is just a stopgap to prevent a potential
|
|
# infinite loop.
|
|
RESOURCE_CREATE_RETRY_COUNT = 100
|
|
|
|
def __init__(self, context, id=None, name=None, updated_at=None,
|
|
created_at=None):
|
|
self._context = context
|
|
self.id = id
|
|
self.name = name
|
|
self.updated_at = updated_at
|
|
self.created_at = created_at
|
|
|
|
@staticmethod
|
|
def _from_db_object(context, target, source):
|
|
target._context = context
|
|
target.id = source['id']
|
|
target.name = source['name']
|
|
target.updated_at = source['updated_at']
|
|
target.created_at = source['created_at']
|
|
return target
|
|
|
|
@classmethod
|
|
def get_by_name(cls, context, name):
|
|
"""Return a ResourceClass object with the given string name.
|
|
|
|
:param name: String name of the resource class to find
|
|
|
|
:raises: ResourceClassNotFound if no such resource class was found
|
|
"""
|
|
rc = context.rc_cache.all_from_string(name)
|
|
obj = cls(
|
|
context,
|
|
id=rc.id,
|
|
name=rc.name,
|
|
updated_at=rc.updated_at,
|
|
created_at=rc.created_at,
|
|
)
|
|
return obj
|
|
|
|
@staticmethod
|
|
@db_api.placement_context_manager.reader
|
|
def _get_next_id(context):
|
|
"""Utility method to grab the next resource class identifier to use for
|
|
user-defined resource classes.
|
|
"""
|
|
query = context.session.query(func.max(models.ResourceClass.id))
|
|
max_id = query.one()[0]
|
|
if not max_id or max_id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
|
|
return ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID
|
|
else:
|
|
return max_id + 1
|
|
|
|
def create(self):
|
|
if self.id is not None:
|
|
raise exception.ObjectActionError(action='create',
|
|
reason='already created')
|
|
if not self.name:
|
|
raise exception.ObjectActionError(action='create',
|
|
reason='name is required')
|
|
if self.name in orc.STANDARDS:
|
|
raise exception.ResourceClassExists(resource_class=self.name)
|
|
|
|
if not self.name.startswith(orc.CUSTOM_NAMESPACE):
|
|
raise exception.ObjectActionError(
|
|
action='create',
|
|
reason='name must start with ' + orc.CUSTOM_NAMESPACE)
|
|
updates = {}
|
|
for field in ['name', 'updated_at', 'created_at']:
|
|
value = getattr(self, field, None)
|
|
if value:
|
|
updates[field] = value
|
|
|
|
# There is the possibility of a race when adding resource classes, as
|
|
# the ID is generated locally. This loop catches that exception, and
|
|
# retries until either it succeeds, or a different exception is
|
|
# encountered.
|
|
retries = self.RESOURCE_CREATE_RETRY_COUNT
|
|
while retries:
|
|
retries -= 1
|
|
try:
|
|
rc = self._create_in_db(self._context, updates)
|
|
self._from_db_object(self._context, self, rc)
|
|
break
|
|
except db_exc.DBDuplicateEntry as e:
|
|
if 'id' in e.columns:
|
|
# Race condition for ID creation; try again
|
|
continue
|
|
# The duplication is on the other unique column, 'name'. So do
|
|
# not retry; raise the exception immediately.
|
|
raise exception.ResourceClassExists(resource_class=self.name)
|
|
else:
|
|
# We have no idea how common it will be in practice for the retry
|
|
# limit to be exceeded. We set it high in the hope that we never
|
|
# hit this point, but added this log message so we know that this
|
|
# specific situation occurred.
|
|
LOG.warning("Exceeded retry limit on ID generation while "
|
|
"creating ResourceClass %(name)s",
|
|
{'name': self.name})
|
|
msg = "creating resource class %s" % self.name
|
|
raise exception.MaxDBRetriesExceeded(action=msg)
|
|
self._context.rc_cache.clear()
|
|
|
|
@staticmethod
|
|
@db_api.placement_context_manager.writer
|
|
def _create_in_db(context, updates):
|
|
next_id = ResourceClass._get_next_id(context)
|
|
rc = models.ResourceClass()
|
|
rc.update(updates)
|
|
rc.id = next_id
|
|
context.session.add(rc)
|
|
return rc
|
|
|
|
def destroy(self):
|
|
if self.id is None:
|
|
raise exception.ObjectActionError(action='destroy',
|
|
reason='ID attribute not found')
|
|
# Never delete any standard resource class.
|
|
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
|
|
raise exception.ResourceClassCannotDeleteStandard(
|
|
resource_class=self.name)
|
|
|
|
self._destroy(self._context, self.id, self.name)
|
|
self._context.rc_cache.clear()
|
|
|
|
@staticmethod
|
|
@db_api.placement_context_manager.writer
|
|
def _destroy(context, _id, name):
|
|
# Don't delete the resource class if it is referred to in the
|
|
# inventories table.
|
|
num_inv = context.session.query(models.Inventory).filter(
|
|
models.Inventory.resource_class_id == _id).count()
|
|
if num_inv:
|
|
raise exception.ResourceClassInUse(resource_class=name)
|
|
|
|
res = context.session.query(models.ResourceClass).filter(
|
|
models.ResourceClass.id == _id).delete()
|
|
if not res:
|
|
raise exception.NotFound()
|
|
|
|
def save(self):
|
|
if self.id is None:
|
|
raise exception.ObjectActionError(action='save',
|
|
reason='ID attribute not found')
|
|
updates = {}
|
|
for field in ['name', 'updated_at', 'created_at']:
|
|
value = getattr(self, field, None)
|
|
if value:
|
|
updates[field] = value
|
|
# Never update any standard resource class.
|
|
if self.id < ResourceClass.MIN_CUSTOM_RESOURCE_CLASS_ID:
|
|
raise exception.ResourceClassCannotUpdateStandard(
|
|
resource_class=self.name)
|
|
self._save(self._context, self.id, self.name, updates)
|
|
self._context.rc_cache.clear()
|
|
|
|
@staticmethod
|
|
@db_api.placement_context_manager.writer
|
|
def _save(context, id, name, updates):
|
|
db_rc = context.session.query(models.ResourceClass).filter_by(
|
|
id=id).first()
|
|
db_rc.update(updates)
|
|
try:
|
|
db_rc.save(context.session)
|
|
except db_exc.DBDuplicateEntry:
|
|
raise exception.ResourceClassExists(resource_class=name)
|
|
|
|
|
|
def ensure_sync(ctx):
|
|
global _RESOURCE_CLASSES_SYNCED
|
|
# If another thread is doing this work, wait for it to complete.
|
|
# When that thread is done _RESOURCE_CLASSES_SYNCED will be true in this
|
|
# thread and we'll simply return.
|
|
with lockutils.lock(_RESOURCE_CLASSES_LOCK):
|
|
if not _RESOURCE_CLASSES_SYNCED:
|
|
_resource_classes_sync(ctx)
|
|
_RESOURCE_CLASSES_SYNCED = True
|
|
|
|
|
|
def get_all(context):
|
|
"""Get a list of all the resource classes in the database."""
|
|
resource_classes = context.rc_cache.get_all()
|
|
return [ResourceClass(context, **rc._mapping) for rc in resource_classes]
|
|
|
|
|
|
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
|
|
@db_api.placement_context_manager.writer
|
|
def _resource_classes_sync(ctx):
|
|
# Create a set of all resource class in the os_resource_classes library.
|
|
sel = sa.select(_RC_TBL.c.name)
|
|
res = ctx.session.execute(sel).fetchall()
|
|
db_classes = [r[0] for r in res if not orc.is_custom(r[0])]
|
|
LOG.debug("Found existing resource classes in db: %s", db_classes)
|
|
# Determine those resource classes which are in os_resource_classes but not
|
|
# currently in the database, and insert them.
|
|
batch_args = [{'name': str(name), 'id': index}
|
|
for index, name in enumerate(orc.STANDARDS)
|
|
if name not in db_classes]
|
|
ins = _RC_TBL.insert()
|
|
if batch_args:
|
|
conn = ctx.session.connection()
|
|
if conn.engine.dialect.name == 'mysql':
|
|
# We need to do a literal insert of 0 to preserve the order
|
|
# of the resource class ids from the previous style of
|
|
# managing them. In some mysql settings a 0 is the same as
|
|
# "give me a default key".
|
|
conn.execute("SET SESSION SQL_MODE='NO_AUTO_VALUE_ON_ZERO'")
|
|
try:
|
|
ctx.session.execute(ins, batch_args)
|
|
LOG.debug("Synced resource_classes from os_resource_classes: %s",
|
|
batch_args)
|
|
except db_exc.DBDuplicateEntry:
|
|
pass # some other process sync'd, just ignore
|