395 lines
13 KiB
Python
395 lines
13 KiB
Python
# -*- encoding: utf-8 -*-
|
|
#
|
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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.
|
|
|
|
"""SQLAlchemy storage backend."""
|
|
|
|
from oslo.config import cfg
|
|
|
|
# TODO(deva): import MultipleResultsFound and handle it appropriately
|
|
from sqlalchemy.orm.exc import NoResultFound
|
|
from sqlalchemy.orm import subqueryload
|
|
|
|
from tuskar.common import exception
|
|
from tuskar.db import api
|
|
from tuskar.db.sqlalchemy import models
|
|
from tuskar.openstack.common.db import exception as db_exception
|
|
from tuskar.openstack.common.db.sqlalchemy import session as db_session
|
|
from tuskar.openstack.common import log
|
|
|
|
|
|
CONF = cfg.CONF
|
|
CONF.import_opt('connection',
|
|
'tuskar.openstack.common.db.sqlalchemy.session',
|
|
group='database')
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
def get_backend():
|
|
"""The backend is this module itself."""
|
|
return Connection()
|
|
|
|
|
|
def model_query(model, *args, **kwargs):
|
|
"""Query helper for simpler session usage.
|
|
|
|
:param session: if present, the session to use
|
|
"""
|
|
|
|
session = kwargs.get('session') or db_session.get_session()
|
|
query = session.query(model, *args)
|
|
return query
|
|
|
|
|
|
class Connection(api.Connection):
|
|
"""SqlAlchemy connection."""
|
|
|
|
def __init__(self):
|
|
|
|
# The superclass __init__ is abstract and prevents the class
|
|
# from being instantiated unless we explicitly remove that
|
|
# here.
|
|
pass
|
|
|
|
@staticmethod
|
|
def get_overcloud_roles():
|
|
"""Returns all overcloud roles known to Tuskar.
|
|
|
|
:return: list of roles; empty list if none are found
|
|
:rtype: list of tuskar.db.sqlalchemy.models.OvercloudRole
|
|
"""
|
|
|
|
session = db_session.get_session()
|
|
roles = session.query(models.OvercloudRole).all()
|
|
session.close()
|
|
return roles
|
|
|
|
@staticmethod
|
|
def get_overcloud_role_by_id(role_id):
|
|
"""Single overcloud role query.
|
|
|
|
:return: role if one exists with the given ID
|
|
:rtype: tuskar.db.sqlalchemy.models.OvercloudRole
|
|
|
|
:raises: tuskar.common.exception.OvercloudRoleNotFound: if no
|
|
role with the given ID exists
|
|
"""
|
|
|
|
session = db_session.get_session()
|
|
try:
|
|
query = session.query(models.OvercloudRole).filter_by(
|
|
id=role_id)
|
|
result = query.one()
|
|
|
|
except NoResultFound:
|
|
raise exception.OvercloudRoleNotFound()
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def create_overcloud_role(overcloud_role):
|
|
"""Creates a new overcloud role in the database.
|
|
|
|
:param overcloud_role: role instance to save
|
|
:type overcloud_role: tuskar.db.sqlalchemy.models.OvercloudRole
|
|
|
|
:return: the role instance that was saved with its
|
|
ID populated
|
|
:rtype: tuskar.db.sqlalchemy.models.OvercloudRole
|
|
|
|
:raises: tuskar.common.exception.OvercloudRoleExists: if a role
|
|
with the given name exists
|
|
"""
|
|
session = db_session.get_session()
|
|
session.begin()
|
|
|
|
try:
|
|
session.add(overcloud_role)
|
|
session.commit()
|
|
return overcloud_role
|
|
|
|
except db_exception.DBDuplicateEntry:
|
|
raise exception.OvercloudRoleExists(name=overcloud_role.name)
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
def update_overcloud_role(self, updated):
|
|
"""Updates the given overcloud role.
|
|
|
|
:param updated: role instance containing changed values
|
|
:type updated: tuskar.db.sqlalchemy.models.OvercloudRole
|
|
|
|
:return: the role instance that was saved
|
|
:rtype: tuskar.db.sqlalchemy.models.OvercloudRole
|
|
|
|
:raises: tuskar.common.exception.OvercloudRoleNotFound if there
|
|
is no role with the given ID
|
|
"""
|
|
existing = self.get_overcloud_role_by_id(updated.id)
|
|
|
|
for a in ('name', 'description', 'image_name', 'flavor_id'):
|
|
if getattr(updated, a) is not None:
|
|
setattr(existing, a, getattr(updated, a))
|
|
|
|
return self.create_overcloud_role(existing)
|
|
|
|
def delete_overcloud_role_by_id(self, role_id):
|
|
"""Deletes an overcloud role from the database.
|
|
|
|
:param role_id: database ID of the role
|
|
:type role_id: int
|
|
|
|
:raises: tuskar.common.exception.OvercloudRoleNotFound if there
|
|
is no role with the given ID
|
|
"""
|
|
role = self.get_overcloud_role_by_id(role_id)
|
|
|
|
session = db_session.get_session()
|
|
session.begin()
|
|
|
|
try:
|
|
session.delete(role)
|
|
session.commit()
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
@staticmethod
|
|
def get_overclouds():
|
|
"""Returns all overcloud instances from the database.
|
|
|
|
:return: list of overcloud instances; empty list if none are found
|
|
:rtype: list of tuskar.db.sqlalchemy.models.Overcloud
|
|
"""
|
|
|
|
session = db_session.get_session()
|
|
overclouds = session.query(models.Overcloud).\
|
|
options(subqueryload(models.Overcloud.attributes)).\
|
|
options(subqueryload(models.Overcloud.counts)).\
|
|
all()
|
|
session.close()
|
|
return overclouds
|
|
|
|
@staticmethod
|
|
def get_overcloud_by_id(overcloud_id):
|
|
"""Returns a specific overcloud instance.
|
|
|
|
:return: overcloud if one exists with the given ID
|
|
:rtype: tuskar.db.sqlalchemy.models.Overcloud
|
|
|
|
:raises: tuskar.common.exception.OvercloudNotFound: if no
|
|
overcloud with the given ID exists
|
|
"""
|
|
|
|
session = db_session.get_session()
|
|
try:
|
|
query = session.query(models.Overcloud).\
|
|
options(subqueryload(models.Overcloud.attributes)).\
|
|
options(subqueryload(models.Overcloud.counts)).\
|
|
filter_by(id=overcloud_id)
|
|
result = query.one()
|
|
|
|
except NoResultFound:
|
|
raise exception.OvercloudNotFound()
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
return result
|
|
|
|
def create_overcloud(self, overcloud):
|
|
"""Creates a new overcloud instance to the database.
|
|
|
|
:param overcloud: overcloud instance to save
|
|
:type overcloud: tuskar.db.sqlalchemy.models.Overcloud
|
|
|
|
:return: the overcloud instance that was saved with its
|
|
ID populated
|
|
:rtype: tuskar.db.sqlalchemy.models.Overcloud
|
|
|
|
:raises: tuskar.common.exception.OvercloudExists: if an overcloud
|
|
role with the given name exists
|
|
"""
|
|
session = db_session.get_session()
|
|
session.begin()
|
|
|
|
try:
|
|
session.add(overcloud)
|
|
session.commit()
|
|
|
|
# Reload from the database to load all of the joined table data
|
|
overcloud = self.get_overcloud_by_id(overcloud.id)
|
|
return overcloud
|
|
|
|
except db_exception.DBDuplicateEntry as e:
|
|
if 'name' in e.columns:
|
|
raise exception.OvercloudExists(name=overcloud.name)
|
|
else:
|
|
raise exception.DuplicateAttribute()
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
def update_overcloud(self, updated):
|
|
"""Updates the configuration of an existing overcloud.
|
|
|
|
The specified parameter is an instance of the domain model with
|
|
the changes to be made. Updating follows the given rules:
|
|
- The updated overcloud must include the ID of the overcloud
|
|
being updated.
|
|
- Any direct attributes on the overcloud that are *not* being changed
|
|
should have their values set to None.
|
|
- For attributes and counts, only differences are specified according
|
|
to the following rules:
|
|
- New items are specified in the updated object's lists
|
|
- Updated items are specified in the updated object's lists with
|
|
the new value and existing key
|
|
- Removed items are specified in the updated object's lists with
|
|
a value of None (zero in the case of a count).
|
|
- Unchanged items are *not* specified.
|
|
|
|
:param updated: overcloud instance containing changed values
|
|
:type updated: tuskar.db.sqlalchemy.models.Overcloud
|
|
|
|
:return: the overcloud instance that was saved
|
|
:rtype: tuskar.db.sqlalchemy.models.Overcloud
|
|
|
|
:raises: tuskar.common.exception.OvercloudNotFound if there
|
|
is no overcloud with the given ID
|
|
"""
|
|
|
|
existing = self.get_overcloud_by_id(updated.id)
|
|
|
|
session = db_session.get_session()
|
|
session.begin()
|
|
|
|
try:
|
|
# First class attributes on the overcloud
|
|
for name in ('stack_id', 'name', 'description'):
|
|
new_value = getattr(updated, name)
|
|
if new_value is not None:
|
|
setattr(existing, name, new_value)
|
|
|
|
self._update_overcloud_attributes(existing, session, updated)
|
|
self._update_overcloud_counts(existing, session, updated)
|
|
|
|
# Save the modified object
|
|
session.add(existing)
|
|
session.commit()
|
|
|
|
return existing
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
@staticmethod
|
|
def _update_overcloud_attributes(existing, session, updated):
|
|
if updated.attributes is not None:
|
|
existing_keys = [a.key for a in existing.attributes]
|
|
existing_attributes_by_key = \
|
|
dict((a.key, a) for a in existing.attributes)
|
|
|
|
delete_keys = []
|
|
for a in updated.attributes:
|
|
|
|
# Deleted
|
|
if a.value is None:
|
|
delete_keys.append(a.key)
|
|
continue
|
|
|
|
# Updated
|
|
if a.key in existing_keys:
|
|
updating = existing_attributes_by_key[a.key]
|
|
updating.value = a.value
|
|
session.add(updating)
|
|
continue
|
|
|
|
# Added
|
|
if a.key not in existing_keys:
|
|
existing_attributes_by_key[a.key] = a
|
|
a.overcloud_id = updated.id
|
|
existing.attributes.append(a)
|
|
session.add(a)
|
|
continue
|
|
|
|
# Purge deleted attributes
|
|
for a in existing.attributes:
|
|
if a.key in delete_keys:
|
|
existing.attributes.remove(a)
|
|
session.delete(a)
|
|
|
|
@staticmethod
|
|
def _update_overcloud_counts(existing, session, updated):
|
|
if updated.counts is not None:
|
|
existing_count_role_ids = [c.overcloud_role_id
|
|
for c in existing.counts]
|
|
existing_counts_by_role_id = \
|
|
dict((c.overcloud_role_id, c) for c in existing.counts)
|
|
|
|
delete_role_ids = []
|
|
for c in updated.counts:
|
|
|
|
# Deleted
|
|
if c.num_nodes == 0:
|
|
delete_role_ids.append(c.overcloud_role_id)
|
|
continue
|
|
|
|
# Updated
|
|
if c.overcloud_role_id in existing_count_role_ids:
|
|
updating = \
|
|
existing_counts_by_role_id[c.overcloud_role_id]
|
|
updating.num_nodes = c.num_nodes
|
|
session.add(updating)
|
|
continue
|
|
|
|
# New
|
|
if c.overcloud_role_id not in existing_count_role_ids:
|
|
existing_counts_by_role_id[c.overcloud_role_id] = c
|
|
c.overcloud_id = updated.id
|
|
existing.counts.append(c)
|
|
session.add(c)
|
|
continue
|
|
|
|
# Purge deleted counts
|
|
for c in existing.counts:
|
|
if c.overcloud_role_id in delete_role_ids:
|
|
existing.counts.remove(c)
|
|
session.delete(c)
|
|
|
|
def delete_overcloud_by_id(self, overcloud_id):
|
|
"""Deletes a overcloud from the database.
|
|
|
|
:param overcloud_id: database ID of the overcloud
|
|
:type overcloud_id: int
|
|
|
|
:raises: tuskar.common.exception.OvercloudNotFound if there
|
|
is no overcloud with the given ID
|
|
"""
|
|
overcloud = self.get_overcloud_by_id(overcloud_id)
|
|
|
|
session = db_session.get_session()
|
|
session.begin()
|
|
|
|
try:
|
|
session.delete(overcloud)
|
|
session.commit()
|
|
|
|
finally:
|
|
session.close()
|