nova/nova/objects/flavor.py

652 lines
26 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_utils import versionutils
from sqlalchemy import or_
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import asc
from sqlalchemy.sql import true
import nova.conf
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 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
OPTIONAL_FIELDS = ['extra_specs', 'projects']
# Remove these fields in version 2.0 of the object.
DEPRECATED_FIELDS = ['deleted', 'deleted_at']
# Non-joined fields which can be updated.
MUTABLE_FIELDS = set(['description'])
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
# TODO(berrange): Remove NovaObjectDictCompat
# TODO(mriedem): Remove NovaPersistentObject in version 2.0
@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.2: Added description field. Note: this field should not be
# persisted with the embedded instance.flavor.
VERSION = '1.2'
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(),
'description': fields.StringField(nullable=True)
}
def __init__(self, *args, **kwargs):
super(Flavor, self).__init__(*args, **kwargs)
self._orig_extra_specs = {}
self._orig_projects = []
def obj_make_compatible(self, primitive, target_version):
super(Flavor, self).obj_make_compatible(primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 2) and 'description' in primitive:
del primitive['description']
@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 _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 _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 _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):
self.projects = self._get_projects_from_db(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):
db_flavor = cls._flavor_get_from_db(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):
db_flavor = cls._flavor_get_by_name_from_db(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):
db_flavor = cls._flavor_get_by_flavor_id_from_db(context,
flavor_id)
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):
self._flavor_add_project(self._context, self.id, 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):
self._flavor_del_project(self._context, self.id, 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)
@base.remotable
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason='already created')
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
"""
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:
self._flavor_extra_specs_add(self._context, self.id, to_add)
for key in to_delete:
self._flavor_extra_specs_del(self._context, self.id, key)
self.obj_reset_changes(['extra_specs'])
# NOTE(mriedem): This method is not remotable since we only expect the API
# to be able to make updates to a flavor.
@db_api.api_context_manager.writer
def _save(self, context, values):
db_flavor = context.session.query(api_models.Flavors).\
filter_by(id=self.id).first()
if not db_flavor:
raise exception.FlavorNotFound(flavor_id=self.id)
db_flavor.update(values)
db_flavor.save(context.session)
# Refresh ourselves from the DB object so we get the new updated_at.
self._from_db_object(context, self, db_flavor)
self.obj_reset_changes()
def save(self):
updates = self.obj_get_changes()
projects = updates.pop('projects', None)
extra_specs = updates.pop('extra_specs', None)
if updates:
# Only allowed to update from the whitelist of mutable fields.
if set(updates.keys()) - MUTABLE_FIELDS:
raise exception.ObjectActionError(
action='save', reason='read-only fields were changed')
self._save(self._context, updates)
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 or
updates):
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.
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)
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):
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)
return base.obj_make_list(context, cls(context), objects.Flavor,
api_db_flavors,
expected_attrs=['extra_specs'])