792 lines
30 KiB
Python
792 lines
30 KiB
Python
# Copyright 2013 Red Hat, Inc
|
|
#
|
|
# 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 oslo_db import exception as db_exc
|
|
from oslo_db.sqlalchemy import utils as sqlalchemyutils
|
|
from oslo_log import log as logging
|
|
import six
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import joinedload
|
|
from sqlalchemy.sql.expression import asc
|
|
from sqlalchemy.sql import func
|
|
from sqlalchemy.sql import text
|
|
from sqlalchemy.sql import true
|
|
|
|
import nova.conf
|
|
from nova import db
|
|
from nova.db.sqlalchemy import api as db_api
|
|
from nova.db.sqlalchemy.api import require_context
|
|
from nova.db.sqlalchemy import api_models
|
|
from nova.db.sqlalchemy import models as main_models
|
|
from nova import exception
|
|
from nova.notifications.objects import base as notification
|
|
from nova.notifications.objects import flavor as flavor_notification
|
|
from nova import objects
|
|
from nova.objects import base
|
|
from nova.objects import fields
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
OPTIONAL_FIELDS = ['extra_specs', 'projects']
|
|
DEPRECATED_FIELDS = ['deleted', 'deleted_at']
|
|
|
|
CONF = nova.conf.CONF
|
|
|
|
|
|
def _dict_with_extra_specs(flavor_model):
|
|
extra_specs = {x['key']: x['value']
|
|
for x in flavor_model['extra_specs']}
|
|
return dict(flavor_model, extra_specs=extra_specs)
|
|
|
|
|
|
# NOTE(danms): There are some issues with the oslo_db context manager
|
|
# decorators with static methods. We pull these out for now and can
|
|
# move them back into the actual staticmethods on the object when those
|
|
# issues are resolved.
|
|
@db_api.api_context_manager.reader
|
|
def _get_projects_from_db(context, flavorid):
|
|
db_flavor = context.session.query(api_models.Flavors).\
|
|
filter_by(flavorid=flavorid).\
|
|
options(joinedload('projects')).\
|
|
first()
|
|
if not db_flavor:
|
|
raise exception.FlavorNotFound(flavor_id=flavorid)
|
|
return [x['project_id'] for x in db_flavor['projects']]
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_add_project(context, flavor_id, project_id):
|
|
project = api_models.FlavorProjects()
|
|
project.update({'flavor_id': flavor_id,
|
|
'project_id': project_id})
|
|
try:
|
|
project.save(context.session)
|
|
except db_exc.DBDuplicateEntry:
|
|
raise exception.FlavorAccessExists(flavor_id=flavor_id,
|
|
project_id=project_id)
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_del_project(context, flavor_id, project_id):
|
|
result = context.session.query(api_models.FlavorProjects).\
|
|
filter_by(project_id=project_id).\
|
|
filter_by(flavor_id=flavor_id).\
|
|
delete()
|
|
if result == 0:
|
|
raise exception.FlavorAccessNotFound(flavor_id=flavor_id,
|
|
project_id=project_id)
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_extra_specs_add(context, flavor_id, specs, max_retries=10):
|
|
writer = db_api.api_context_manager.writer
|
|
for attempt in range(max_retries):
|
|
try:
|
|
spec_refs = context.session.query(
|
|
api_models.FlavorExtraSpecs).\
|
|
filter_by(flavor_id=flavor_id).\
|
|
filter(api_models.FlavorExtraSpecs.key.in_(
|
|
specs.keys())).\
|
|
all()
|
|
|
|
existing_keys = set()
|
|
for spec_ref in spec_refs:
|
|
key = spec_ref["key"]
|
|
existing_keys.add(key)
|
|
with writer.savepoint.using(context):
|
|
spec_ref.update({"value": specs[key]})
|
|
|
|
for key, value in specs.items():
|
|
if key in existing_keys:
|
|
continue
|
|
spec_ref = api_models.FlavorExtraSpecs()
|
|
with writer.savepoint.using(context):
|
|
spec_ref.update({"key": key, "value": value,
|
|
"flavor_id": flavor_id})
|
|
context.session.add(spec_ref)
|
|
|
|
return specs
|
|
except db_exc.DBDuplicateEntry:
|
|
# a concurrent transaction has been committed,
|
|
# try again unless this was the last attempt
|
|
if attempt == max_retries - 1:
|
|
raise exception.FlavorExtraSpecUpdateCreateFailed(
|
|
id=flavor_id, retries=max_retries)
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_extra_specs_del(context, flavor_id, key):
|
|
result = context.session.query(api_models.FlavorExtraSpecs).\
|
|
filter_by(flavor_id=flavor_id).\
|
|
filter_by(key=key).\
|
|
delete()
|
|
if result == 0:
|
|
raise exception.FlavorExtraSpecsNotFound(
|
|
extra_specs_key=key, flavor_id=flavor_id)
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_create(context, values):
|
|
specs = values.get('extra_specs')
|
|
db_specs = []
|
|
if specs:
|
|
for k, v in specs.items():
|
|
db_spec = api_models.FlavorExtraSpecs()
|
|
db_spec['key'] = k
|
|
db_spec['value'] = v
|
|
db_specs.append(db_spec)
|
|
|
|
projects = values.get('projects')
|
|
db_projects = []
|
|
if projects:
|
|
for project in set(projects):
|
|
db_project = api_models.FlavorProjects()
|
|
db_project['project_id'] = project
|
|
db_projects.append(db_project)
|
|
|
|
values['extra_specs'] = db_specs
|
|
values['projects'] = db_projects
|
|
db_flavor = api_models.Flavors()
|
|
db_flavor.update(values)
|
|
|
|
try:
|
|
db_flavor.save(context.session)
|
|
except db_exc.DBDuplicateEntry as e:
|
|
if 'flavorid' in e.columns:
|
|
raise exception.FlavorIdExists(flavor_id=values['flavorid'])
|
|
raise exception.FlavorExists(name=values['name'])
|
|
except Exception as e:
|
|
raise db_exc.DBError(e)
|
|
|
|
return _dict_with_extra_specs(db_flavor)
|
|
|
|
|
|
@db_api.api_context_manager.writer
|
|
def _flavor_destroy(context, flavor_id=None, flavorid=None):
|
|
query = context.session.query(api_models.Flavors)
|
|
|
|
if flavor_id is not None:
|
|
query = query.filter(api_models.Flavors.id == flavor_id)
|
|
else:
|
|
query = query.filter(api_models.Flavors.flavorid == flavorid)
|
|
result = query.first()
|
|
|
|
if not result:
|
|
raise exception.FlavorNotFound(flavor_id=(flavor_id or flavorid))
|
|
|
|
context.session.query(api_models.FlavorProjects).\
|
|
filter_by(flavor_id=result.id).delete()
|
|
context.session.query(api_models.FlavorExtraSpecs).\
|
|
filter_by(flavor_id=result.id).delete()
|
|
context.session.delete(result)
|
|
return result
|
|
|
|
|
|
@db_api.pick_context_manager_reader
|
|
def _ensure_migrated(context):
|
|
result = context.session.query(main_models.InstanceTypes).\
|
|
filter_by(deleted=0).count()
|
|
if result:
|
|
LOG.warning('Main database contains %(count)i unmigrated flavors',
|
|
{'count': result})
|
|
return result == 0
|
|
|
|
|
|
# TODO(berrange): Remove NovaObjectDictCompat
|
|
@base.NovaObjectRegistry.register
|
|
class Flavor(base.NovaPersistentObject, base.NovaObject,
|
|
base.NovaObjectDictCompat):
|
|
# Version 1.0: Initial version
|
|
# Version 1.1: Added save_projects(), save_extra_specs(), removed
|
|
# remotable from save()
|
|
VERSION = '1.1'
|
|
|
|
fields = {
|
|
'id': fields.IntegerField(),
|
|
'name': fields.StringField(nullable=True),
|
|
'memory_mb': fields.IntegerField(),
|
|
'vcpus': fields.IntegerField(),
|
|
'root_gb': fields.IntegerField(),
|
|
'ephemeral_gb': fields.IntegerField(),
|
|
'flavorid': fields.StringField(),
|
|
'swap': fields.IntegerField(),
|
|
'rxtx_factor': fields.FloatField(nullable=True, default=1.0),
|
|
'vcpu_weight': fields.IntegerField(nullable=True),
|
|
'disabled': fields.BooleanField(),
|
|
'is_public': fields.BooleanField(),
|
|
'extra_specs': fields.DictOfStringsField(),
|
|
'projects': fields.ListOfStringsField(),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Flavor, self).__init__(*args, **kwargs)
|
|
self._orig_extra_specs = {}
|
|
self._orig_projects = []
|
|
self._in_api = False
|
|
|
|
@property
|
|
def in_api(self):
|
|
if self._in_api:
|
|
return True
|
|
else:
|
|
try:
|
|
if 'id' in self:
|
|
self._flavor_get_from_db(self._context, self.id)
|
|
else:
|
|
flavor = self._flavor_get_by_flavor_id_from_db(
|
|
self._context,
|
|
self.flavorid)
|
|
# Fix us up so we can use our real id
|
|
self.id = flavor['id']
|
|
self._in_api = True
|
|
except exception.FlavorNotFound:
|
|
pass
|
|
return self._in_api
|
|
|
|
@staticmethod
|
|
def _from_db_object(context, flavor, db_flavor, expected_attrs=None):
|
|
if expected_attrs is None:
|
|
expected_attrs = []
|
|
flavor._context = context
|
|
for name, field in flavor.fields.items():
|
|
if name in OPTIONAL_FIELDS:
|
|
continue
|
|
if name in DEPRECATED_FIELDS and name not in db_flavor:
|
|
continue
|
|
value = db_flavor[name]
|
|
if isinstance(field, fields.IntegerField):
|
|
value = value if value is not None else 0
|
|
flavor[name] = value
|
|
|
|
# NOTE(danms): This is to support processing the API flavor
|
|
# model, which does not have these deprecated fields. When we
|
|
# remove compatibility with the old InstanceType model, we can
|
|
# remove this as well.
|
|
if any(f not in db_flavor for f in DEPRECATED_FIELDS):
|
|
flavor.deleted_at = None
|
|
flavor.deleted = False
|
|
|
|
if 'extra_specs' in expected_attrs:
|
|
flavor.extra_specs = db_flavor['extra_specs']
|
|
|
|
if 'projects' in expected_attrs:
|
|
if 'projects' in db_flavor:
|
|
flavor['projects'] = [x['project_id']
|
|
for x in db_flavor['projects']]
|
|
else:
|
|
flavor._load_projects()
|
|
|
|
flavor.obj_reset_changes()
|
|
return flavor
|
|
|
|
@staticmethod
|
|
@db_api.api_context_manager.reader
|
|
def _flavor_get_query_from_db(context):
|
|
query = context.session.query(api_models.Flavors).\
|
|
options(joinedload('extra_specs'))
|
|
if not context.is_admin:
|
|
the_filter = [api_models.Flavors.is_public == true()]
|
|
the_filter.extend([
|
|
api_models.Flavors.projects.any(project_id=context.project_id)
|
|
])
|
|
query = query.filter(or_(*the_filter))
|
|
return query
|
|
|
|
@staticmethod
|
|
@require_context
|
|
def _flavor_get_from_db(context, id):
|
|
"""Returns a dict describing specific flavor."""
|
|
result = Flavor._flavor_get_query_from_db(context).\
|
|
filter_by(id=id).\
|
|
first()
|
|
if not result:
|
|
raise exception.FlavorNotFound(flavor_id=id)
|
|
return db_api._dict_with_extra_specs(result)
|
|
|
|
@staticmethod
|
|
@require_context
|
|
def _flavor_get_by_name_from_db(context, name):
|
|
"""Returns a dict describing specific flavor."""
|
|
result = Flavor._flavor_get_query_from_db(context).\
|
|
filter_by(name=name).\
|
|
first()
|
|
if not result:
|
|
raise exception.FlavorNotFoundByName(flavor_name=name)
|
|
return db_api._dict_with_extra_specs(result)
|
|
|
|
@staticmethod
|
|
@require_context
|
|
def _flavor_get_by_flavor_id_from_db(context, flavor_id):
|
|
"""Returns a dict describing specific flavor_id."""
|
|
result = Flavor._flavor_get_query_from_db(context).\
|
|
filter_by(flavorid=flavor_id).\
|
|
order_by(asc(api_models.Flavors.id)).\
|
|
first()
|
|
if not result:
|
|
raise exception.FlavorNotFound(flavor_id=flavor_id)
|
|
return db_api._dict_with_extra_specs(result)
|
|
|
|
@staticmethod
|
|
def _get_projects_from_db(context, flavorid):
|
|
return _get_projects_from_db(context, flavorid)
|
|
|
|
@base.remotable
|
|
def _load_projects(self):
|
|
try:
|
|
self.projects = self._get_projects_from_db(self._context,
|
|
self.flavorid)
|
|
except exception.FlavorNotFound:
|
|
self.projects = [x['project_id'] for x in
|
|
db.flavor_access_get_by_flavor_id(self._context,
|
|
self.flavorid)]
|
|
self.obj_reset_changes(['projects'])
|
|
|
|
def obj_load_attr(self, attrname):
|
|
# NOTE(danms): Only projects could be lazy-loaded right now
|
|
if attrname != 'projects':
|
|
raise exception.ObjectActionError(
|
|
action='obj_load_attr', reason='unable to load %s' % attrname)
|
|
|
|
self._load_projects()
|
|
|
|
def obj_reset_changes(self, fields=None, recursive=False):
|
|
super(Flavor, self).obj_reset_changes(fields=fields,
|
|
recursive=recursive)
|
|
if fields is None or 'extra_specs' in fields:
|
|
self._orig_extra_specs = (dict(self.extra_specs)
|
|
if self.obj_attr_is_set('extra_specs')
|
|
else {})
|
|
if fields is None or 'projects' in fields:
|
|
self._orig_projects = (list(self.projects)
|
|
if self.obj_attr_is_set('projects')
|
|
else [])
|
|
|
|
def obj_what_changed(self):
|
|
changes = super(Flavor, self).obj_what_changed()
|
|
if ('extra_specs' in self and
|
|
self.extra_specs != self._orig_extra_specs):
|
|
changes.add('extra_specs')
|
|
if 'projects' in self and self.projects != self._orig_projects:
|
|
changes.add('projects')
|
|
return changes
|
|
|
|
@classmethod
|
|
def _obj_from_primitive(cls, context, objver, primitive):
|
|
self = super(Flavor, cls)._obj_from_primitive(context, objver,
|
|
primitive)
|
|
changes = self.obj_what_changed()
|
|
if 'extra_specs' not in changes:
|
|
# This call left extra_specs "clean" so update our tracker
|
|
self._orig_extra_specs = (dict(self.extra_specs)
|
|
if self.obj_attr_is_set('extra_specs')
|
|
else {})
|
|
if 'projects' not in changes:
|
|
# This call left projects "clean" so update our tracker
|
|
self._orig_projects = (list(self.projects)
|
|
if self.obj_attr_is_set('projects')
|
|
else [])
|
|
return self
|
|
|
|
@base.remotable_classmethod
|
|
def get_by_id(cls, context, id):
|
|
try:
|
|
db_flavor = cls._flavor_get_from_db(context, id)
|
|
except exception.FlavorNotFound:
|
|
db_flavor = db.flavor_get(context, id)
|
|
return cls._from_db_object(context, cls(context), db_flavor,
|
|
expected_attrs=['extra_specs'])
|
|
|
|
@base.remotable_classmethod
|
|
def get_by_name(cls, context, name):
|
|
try:
|
|
db_flavor = cls._flavor_get_by_name_from_db(context, name)
|
|
except exception.FlavorNotFoundByName:
|
|
db_flavor = db.flavor_get_by_name(context, name)
|
|
return cls._from_db_object(context, cls(context), db_flavor,
|
|
expected_attrs=['extra_specs'])
|
|
|
|
@base.remotable_classmethod
|
|
def get_by_flavor_id(cls, context, flavor_id, read_deleted=None):
|
|
try:
|
|
db_flavor = cls._flavor_get_by_flavor_id_from_db(context,
|
|
flavor_id)
|
|
except exception.FlavorNotFound:
|
|
db_flavor = db.flavor_get_by_flavor_id(context, flavor_id,
|
|
read_deleted)
|
|
return cls._from_db_object(context, cls(context), db_flavor,
|
|
expected_attrs=['extra_specs'])
|
|
|
|
@staticmethod
|
|
def _flavor_add_project(context, flavor_id, project_id):
|
|
return _flavor_add_project(context, flavor_id, project_id)
|
|
|
|
@staticmethod
|
|
def _flavor_del_project(context, flavor_id, project_id):
|
|
return _flavor_del_project(context, flavor_id, project_id)
|
|
|
|
def _add_access(self, project_id):
|
|
if self.in_api:
|
|
self._flavor_add_project(self._context, self.id, project_id)
|
|
else:
|
|
db.flavor_access_add(self._context, self.flavorid, project_id)
|
|
|
|
@base.remotable
|
|
def add_access(self, project_id):
|
|
if 'projects' in self.obj_what_changed():
|
|
raise exception.ObjectActionError(action='add_access',
|
|
reason='projects modified')
|
|
self._add_access(project_id)
|
|
self._load_projects()
|
|
self._send_notification(fields.NotificationAction.UPDATE)
|
|
|
|
def _remove_access(self, project_id):
|
|
if self.in_api:
|
|
self._flavor_del_project(self._context, self.id, project_id)
|
|
else:
|
|
db.flavor_access_remove(self._context, self.flavorid, project_id)
|
|
|
|
@base.remotable
|
|
def remove_access(self, project_id):
|
|
if 'projects' in self.obj_what_changed():
|
|
raise exception.ObjectActionError(action='remove_access',
|
|
reason='projects modified')
|
|
self._remove_access(project_id)
|
|
self._load_projects()
|
|
self._send_notification(fields.NotificationAction.UPDATE)
|
|
|
|
@staticmethod
|
|
def _flavor_create(context, updates):
|
|
return _flavor_create(context, updates)
|
|
|
|
@staticmethod
|
|
def _ensure_migrated(context):
|
|
return _ensure_migrated(context)
|
|
|
|
@base.remotable
|
|
def create(self):
|
|
if self.obj_attr_is_set('id'):
|
|
raise exception.ObjectActionError(action='create',
|
|
reason='already created')
|
|
|
|
# NOTE(danms): Once we have made it past a point where we know
|
|
# all flavors have been migrated, we can remove this. Ideally
|
|
# in Ocata with a blocker migration to be sure.
|
|
if not self._ensure_migrated(self._context):
|
|
raise exception.ObjectActionError(
|
|
action='create',
|
|
reason='main database still contains flavors')
|
|
|
|
updates = self.obj_get_changes()
|
|
expected_attrs = []
|
|
for attr in OPTIONAL_FIELDS:
|
|
if attr in updates:
|
|
expected_attrs.append(attr)
|
|
db_flavor = self._flavor_create(self._context, updates)
|
|
self._from_db_object(self._context, self, db_flavor,
|
|
expected_attrs=expected_attrs)
|
|
self._send_notification(fields.NotificationAction.CREATE)
|
|
|
|
@base.remotable
|
|
def save_projects(self, to_add=None, to_delete=None):
|
|
"""Add or delete projects.
|
|
|
|
:param:to_add: A list of projects to add
|
|
:param:to_delete: A list of projects to remove
|
|
"""
|
|
|
|
to_add = to_add if to_add is not None else []
|
|
to_delete = to_delete if to_delete is not None else []
|
|
|
|
for project_id in to_add:
|
|
self._add_access(project_id)
|
|
for project_id in to_delete:
|
|
self._remove_access(project_id)
|
|
self.obj_reset_changes(['projects'])
|
|
|
|
@staticmethod
|
|
def _flavor_extra_specs_add(context, flavor_id, specs, max_retries=10):
|
|
return _flavor_extra_specs_add(context, flavor_id, specs, max_retries)
|
|
|
|
@staticmethod
|
|
def _flavor_extra_specs_del(context, flavor_id, key):
|
|
return _flavor_extra_specs_del(context, flavor_id, key)
|
|
|
|
@base.remotable
|
|
def save_extra_specs(self, to_add=None, to_delete=None):
|
|
"""Add or delete extra_specs.
|
|
|
|
:param:to_add: A dict of new keys to add/update
|
|
:param:to_delete: A list of keys to remove
|
|
"""
|
|
|
|
if self.in_api:
|
|
add_fn = self._flavor_extra_specs_add
|
|
del_fn = self._flavor_extra_specs_del
|
|
ident = self.id
|
|
else:
|
|
add_fn = db.flavor_extra_specs_update_or_create
|
|
del_fn = db.flavor_extra_specs_delete
|
|
ident = self.flavorid
|
|
|
|
to_add = to_add if to_add is not None else {}
|
|
to_delete = to_delete if to_delete is not None else []
|
|
|
|
if to_add:
|
|
add_fn(self._context, ident, to_add)
|
|
|
|
for key in to_delete:
|
|
del_fn(self._context, ident, key)
|
|
self.obj_reset_changes(['extra_specs'])
|
|
|
|
def save(self):
|
|
updates = self.obj_get_changes()
|
|
projects = updates.pop('projects', None)
|
|
extra_specs = updates.pop('extra_specs', None)
|
|
if updates:
|
|
raise exception.ObjectActionError(
|
|
action='save', reason='read-only fields were changed')
|
|
|
|
if extra_specs is not None:
|
|
deleted_keys = (set(self._orig_extra_specs.keys()) -
|
|
set(extra_specs.keys()))
|
|
added_keys = self.extra_specs
|
|
else:
|
|
added_keys = deleted_keys = None
|
|
|
|
if projects is not None:
|
|
deleted_projects = set(self._orig_projects) - set(projects)
|
|
added_projects = set(projects) - set(self._orig_projects)
|
|
else:
|
|
added_projects = deleted_projects = None
|
|
|
|
# NOTE(danms): The first remotable method we call will reset
|
|
# our of the original values for projects and extra_specs. Thus,
|
|
# we collect the added/deleted lists for both above and /then/
|
|
# call these methods to update them.
|
|
|
|
if added_keys or deleted_keys:
|
|
self.save_extra_specs(self.extra_specs, deleted_keys)
|
|
|
|
if added_projects or deleted_projects:
|
|
self.save_projects(added_projects, deleted_projects)
|
|
|
|
if added_keys or deleted_keys or added_projects or deleted_projects:
|
|
self._send_notification(fields.NotificationAction.UPDATE)
|
|
|
|
@staticmethod
|
|
def _flavor_destroy(context, flavor_id=None, flavorid=None):
|
|
return _flavor_destroy(context, flavor_id=flavor_id, flavorid=flavorid)
|
|
|
|
@base.remotable
|
|
def destroy(self):
|
|
# NOTE(danms): Historically the only way to delete a flavor
|
|
# is via name, which is not very precise. We need to be able to
|
|
# support the light construction of a flavor object and subsequent
|
|
# delete request with only our name filled out. However, if we have
|
|
# our id property, we should instead delete with that since it's
|
|
# far more specific.
|
|
try:
|
|
if 'id' in self:
|
|
db_flavor = self._flavor_destroy(self._context,
|
|
flavor_id=self.id)
|
|
else:
|
|
db_flavor = self._flavor_destroy(self._context,
|
|
flavorid=self.flavorid)
|
|
self._from_db_object(self._context, self, db_flavor)
|
|
self._send_notification(fields.NotificationAction.DELETE)
|
|
except exception.FlavorNotFound:
|
|
db.flavor_destroy(self._context, self.flavorid)
|
|
|
|
def _send_notification(self, action):
|
|
# NOTE(danms): Instead of making the below notification
|
|
# lazy-load projects (which is a problem for instance-bound
|
|
# flavors and compute-cell operations), just load them here.
|
|
if 'projects' not in self:
|
|
# If the flavor is deleted we can't lazy-load projects.
|
|
# FlavorPayload will orphan the flavor which will make the
|
|
# NotificationPayloadBase set projects=None in the notification
|
|
# payload.
|
|
if action != fields.NotificationAction.DELETE:
|
|
self._load_projects()
|
|
notification_type = flavor_notification.FlavorNotification
|
|
payload_type = flavor_notification.FlavorPayload
|
|
|
|
payload = payload_type(self)
|
|
notification_type(
|
|
publisher=notification.NotificationPublisher(
|
|
host=CONF.host, source=fields.NotificationSource.API),
|
|
event_type=notification.EventType(object="flavor",
|
|
action=action),
|
|
priority=fields.NotificationPriority.INFO,
|
|
payload=payload).emit(self._context)
|
|
|
|
|
|
@db_api.api_context_manager.reader
|
|
def _flavor_get_all_from_db(context, inactive, filters, sort_key, sort_dir,
|
|
limit, marker):
|
|
"""Returns all flavors.
|
|
"""
|
|
filters = filters or {}
|
|
|
|
query = Flavor._flavor_get_query_from_db(context)
|
|
|
|
if 'min_memory_mb' in filters:
|
|
query = query.filter(
|
|
api_models.Flavors.memory_mb >= filters['min_memory_mb'])
|
|
|
|
if 'min_root_gb' in filters:
|
|
query = query.filter(
|
|
api_models.Flavors.root_gb >= filters['min_root_gb'])
|
|
|
|
if 'disabled' in filters:
|
|
query = query.filter(
|
|
api_models.Flavors.disabled == filters['disabled'])
|
|
|
|
if 'is_public' in filters and filters['is_public'] is not None:
|
|
the_filter = [api_models.Flavors.is_public == filters['is_public']]
|
|
if filters['is_public'] and context.project_id is not None:
|
|
the_filter.extend([api_models.Flavors.projects.any(
|
|
project_id=context.project_id)])
|
|
if len(the_filter) > 1:
|
|
query = query.filter(or_(*the_filter))
|
|
else:
|
|
query = query.filter(the_filter[0])
|
|
marker_row = None
|
|
if marker is not None:
|
|
marker_row = Flavor._flavor_get_query_from_db(context).\
|
|
filter_by(flavorid=marker).\
|
|
first()
|
|
if not marker_row:
|
|
raise exception.MarkerNotFound(marker=marker)
|
|
|
|
query = sqlalchemyutils.paginate_query(query, api_models.Flavors,
|
|
limit,
|
|
[sort_key, 'id'],
|
|
marker=marker_row,
|
|
sort_dir=sort_dir)
|
|
return [_dict_with_extra_specs(i) for i in query.all()]
|
|
|
|
|
|
@base.NovaObjectRegistry.register
|
|
class FlavorList(base.ObjectListBase, base.NovaObject):
|
|
VERSION = '1.1'
|
|
|
|
fields = {
|
|
'objects': fields.ListOfObjectsField('Flavor'),
|
|
}
|
|
|
|
@base.remotable_classmethod
|
|
def get_all(cls, context, inactive=False, filters=None,
|
|
sort_key='flavorid', sort_dir='asc', limit=None, marker=None):
|
|
try:
|
|
api_db_flavors = _flavor_get_all_from_db(context,
|
|
inactive=inactive,
|
|
filters=filters,
|
|
sort_key=sort_key,
|
|
sort_dir=sort_dir,
|
|
limit=limit,
|
|
marker=marker)
|
|
# NOTE(danms): If we were asked for a marker and found it in
|
|
# results from the API DB, we must continue our pagination with
|
|
# just the limit (if any) to the main DB.
|
|
marker = None
|
|
except exception.MarkerNotFound:
|
|
api_db_flavors = []
|
|
|
|
if limit is not None:
|
|
limit_more = limit - len(api_db_flavors)
|
|
else:
|
|
limit_more = None
|
|
|
|
if limit_more is None or limit_more > 0:
|
|
db_flavors = db.flavor_get_all(context, inactive=inactive,
|
|
filters=filters, sort_key=sort_key,
|
|
sort_dir=sort_dir, limit=limit_more,
|
|
marker=marker)
|
|
else:
|
|
db_flavors = []
|
|
return base.obj_make_list(context, cls(context), objects.Flavor,
|
|
api_db_flavors + db_flavors,
|
|
expected_attrs=['extra_specs'])
|
|
|
|
|
|
@db_api.pick_context_manager_reader
|
|
def _get_main_db_flavor_ids(context, limit):
|
|
# NOTE(danms): We don't need this imported at runtime, so
|
|
# keep it separate here
|
|
from nova.db.sqlalchemy import models
|
|
return [x[0] for x in context.session.query(models.InstanceTypes.id).
|
|
filter_by(deleted=0).
|
|
limit(limit)]
|
|
|
|
|
|
@db_api.pick_context_manager_writer
|
|
def _destroy_flavor_hard(context, name):
|
|
# NOTE(danms): We don't need this imported at runtime, so
|
|
# keep it separate here
|
|
from nova.db.sqlalchemy import models
|
|
context.session.query(models.InstanceTypes).filter_by(name=name).delete()
|
|
|
|
|
|
def migrate_flavors(ctxt, count, hard_delete=False):
|
|
main_db_ids = _get_main_db_flavor_ids(ctxt, count)
|
|
if not main_db_ids:
|
|
return 0, 0
|
|
|
|
count_all = len(main_db_ids)
|
|
count_hit = 0
|
|
|
|
for flavor_id in main_db_ids:
|
|
try:
|
|
flavor = Flavor.get_by_id(ctxt, flavor_id)
|
|
flavor_values = {field: getattr(flavor, field)
|
|
for field in flavor.fields}
|
|
flavor._flavor_create(ctxt, flavor_values)
|
|
count_hit += 1
|
|
if hard_delete:
|
|
_destroy_flavor_hard(ctxt, flavor.name)
|
|
else:
|
|
db.flavor_destroy(ctxt, flavor.flavorid)
|
|
except exception.FlavorNotFound:
|
|
LOG.warning('Flavor id %(id)i disappeared during migration',
|
|
{'id': flavor_id})
|
|
except (exception.FlavorExists, exception.FlavorIdExists) as e:
|
|
LOG.error(six.text_type(e))
|
|
|
|
return count_all, count_hit
|
|
|
|
|
|
def _adjust_autoincrement(context, value):
|
|
engine = db_api.get_api_engine()
|
|
if engine.name == 'postgresql':
|
|
# NOTE(danms): If we migrated some flavors in the above function,
|
|
# then we will have confused postgres' sequence for the autoincrement
|
|
# primary key. MySQL does not care about this, but since postgres does,
|
|
# we need to reset this to avoid a failure on the next flavor creation.
|
|
engine.execute(
|
|
text('ALTER SEQUENCE flavors_id_seq RESTART WITH %i;' % (
|
|
value)))
|
|
|
|
|
|
@db_api.api_context_manager.reader
|
|
def _get_max_flavor_id(context):
|
|
max_id = context.session.query(func.max(api_models.Flavors.id)).one()[0]
|
|
return max_id or 0
|
|
|
|
|
|
def migrate_flavor_reset_autoincrement(ctxt, count):
|
|
max_id = _get_max_flavor_id(ctxt)
|
|
_adjust_autoincrement(ctxt, max_id + 1)
|
|
return 0, 0
|