1. Simplify the exposed api to reduce race conditions which could occur if we allowed flow details, task details and log books to be deleted at different times; reduce this down to just being able to save and delete from logbooks (and only save/update for flow and task details to) to reduce the problem cases. 2. Introduce a alembic migration with a proper schema so that the initial database can be created in the first place, adjust its exposed fields and relations to be defined by said schema. 3. Use oslo db models instead of our own. Change-Id: I78bbedf87d506d3f39157198638937c933235b7b
247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
|
# Copyright (C) 2013 Rackspace Hosting All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""Implementation of a SQLAlchemy storage backend."""
|
|
|
|
import logging
|
|
import sys
|
|
|
|
from sqlalchemy import exceptions as sql_exc
|
|
|
|
from taskflow import exceptions as exc
|
|
from taskflow.openstack.common.db.sqlalchemy import session as db_session
|
|
from taskflow.persistence.backends.sqlalchemy import models
|
|
from taskflow.persistence import flowdetail
|
|
from taskflow.persistence import logbook
|
|
from taskflow.persistence import taskdetail
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def get_backend():
|
|
"""The backend is this module itself."""
|
|
return sys.modules[__name__]
|
|
|
|
|
|
def _convert_fd_to_external(fd):
|
|
fd_c = flowdetail.FlowDetail(fd.name, uuid=fd.uuid, backend='sqlalchemy')
|
|
fd_c.meta = fd.meta
|
|
fd_c.state = fd.state
|
|
for td in fd.taskdetails:
|
|
fd_c.add(_convert_td_to_external(td))
|
|
return fd_c
|
|
|
|
|
|
def _convert_fd_to_internal(fd, lb_uuid):
|
|
fd_m = models.FlowDetail(name=fd.name, uuid=fd.uuid, parent_uuid=lb_uuid,
|
|
meta=fd.meta, state=fd.state)
|
|
fd_m.taskdetails = []
|
|
for td in fd:
|
|
fd_m.taskdetails.append(_convert_td_to_internal(td, fd_m.uuid))
|
|
return fd_m
|
|
|
|
|
|
def _convert_td_to_internal(td, parent_uuid):
|
|
return models.TaskDetail(name=td.name, uuid=td.uuid,
|
|
state=td.state, results=td.results,
|
|
exception=td.exception, meta=td.meta,
|
|
stacktrace=td.stacktrace,
|
|
version=td.version, parent_uuid=parent_uuid)
|
|
|
|
|
|
def _convert_td_to_external(td):
|
|
# Convert from sqlalchemy model -> external model, this allows us
|
|
# to change the internal sqlalchemy model easily by forcing a defined
|
|
# interface (that isn't the sqlalchemy model itself).
|
|
td_c = taskdetail.TaskDetail(td.name, uuid=td.uuid, backend='sqlalchemy')
|
|
td_c.state = td.state
|
|
td_c.results = td.results
|
|
td_c.exception = td.exception
|
|
td_c.stacktrace = td.stacktrace
|
|
td_c.meta = td.meta
|
|
td_c.version = td.version
|
|
return td_c
|
|
|
|
|
|
def _convert_lb_to_external(lb_m):
|
|
"""Don't expose the internal sqlalchemy ORM model to the external api."""
|
|
lb_c = logbook.LogBook(lb_m.name, lb_m.uuid,
|
|
updated_at=lb_m.updated_at,
|
|
created_at=lb_m.created_at,
|
|
backend='sqlalchemy')
|
|
lb_c.meta = lb_m.meta
|
|
for fd_m in lb_m.flowdetails:
|
|
lb_c.add(_convert_fd_to_external(fd_m))
|
|
return lb_c
|
|
|
|
|
|
def _convert_lb_to_internal(lb_c):
|
|
"""Don't expose the external model to the sqlalchemy ORM model."""
|
|
lb_m = models.LogBook(uuid=lb_c.uuid, meta=lb_c.meta, name=lb_c.name)
|
|
lb_m.flowdetails = []
|
|
for fd_c in lb_c:
|
|
lb_m.flowdetails.append(_convert_fd_to_internal(fd_c, lb_c.uuid))
|
|
return lb_m
|
|
|
|
|
|
def _logbook_get_model(lb_id, session):
|
|
entry = session.query(models.LogBook).filter_by(uuid=lb_id).first()
|
|
if entry is None:
|
|
raise exc.NotFound("No logbook found with id: %s" % lb_id)
|
|
return entry
|
|
|
|
|
|
def _flow_details_get_model(f_id, session):
|
|
entry = session.query(models.FlowDetail).filter_by(uuid=f_id).first()
|
|
if entry is None:
|
|
raise exc.NotFound("No flow details found with id: %s" % f_id)
|
|
return entry
|
|
|
|
|
|
def _task_details_get_model(t_id, session):
|
|
entry = session.query(models.TaskDetail).filter_by(uuid=t_id).first()
|
|
if entry is None:
|
|
raise exc.NotFound("No task details found with id: %s" % t_id)
|
|
return entry
|
|
|
|
|
|
def _taskdetails_merge(td_m, td):
|
|
if td_m.state != td.state:
|
|
td_m.state = td.state
|
|
if td_m.results != td.results:
|
|
td_m.results = td.results
|
|
if td_m.exception != td.exception:
|
|
td_m.exception = td.exception
|
|
if td_m.stacktrace != td.stacktrace:
|
|
td_m.stacktrace = td.stacktrace
|
|
if td_m.meta != td.meta:
|
|
td_m.meta = td.meta
|
|
return td_m
|
|
|
|
|
|
def clear_all():
|
|
session = db_session.get_session()
|
|
with session.begin():
|
|
# NOTE(harlowja): due to how we have our relationship setup and
|
|
# cascading deletes are enabled, this will cause all associated task
|
|
# details and flow details to automatically be purged.
|
|
try:
|
|
return session.query(models.LogBook).delete()
|
|
except sql_exc.DBAPIError as e:
|
|
raise exc.StorageError("Failed clearing all entries: %s" % e, e)
|
|
|
|
|
|
def taskdetails_save(td):
|
|
# Must already exist since a tasks details has a strong connection to
|
|
# a flow details, and tasks details can not be saved on there own since
|
|
# they *must* have a connection to an existing flow details.
|
|
session = db_session.get_session()
|
|
with session.begin():
|
|
td_m = _task_details_get_model(td.uuid, session=session)
|
|
td_m = _taskdetails_merge(td_m, td)
|
|
td_m = session.merge(td_m)
|
|
return _convert_td_to_external(td_m)
|
|
|
|
|
|
def flowdetails_save(fd):
|
|
# Must already exist since a flow details has a strong connection to
|
|
# a logbook, and flow details can not be saved on there own since they
|
|
# *must* have a connection to an existing logbook.
|
|
session = db_session.get_session()
|
|
with session.begin():
|
|
fd_m = _flow_details_get_model(fd.uuid, session=session)
|
|
if fd_m.meta != fd.meta:
|
|
fd_m.meta = fd.meta
|
|
if fd_m.state != fd.state:
|
|
fd_m.state = fd.state
|
|
for td in fd:
|
|
updated = False
|
|
for td_m in fd_m.taskdetails:
|
|
if td_m.uuid == td.uuid:
|
|
updated = True
|
|
td_m = _taskdetails_merge(td_m, td)
|
|
break
|
|
if not updated:
|
|
fd_m.taskdetails.append(_convert_td_to_internal(td, fd_m.uuid))
|
|
fd_m = session.merge(fd_m)
|
|
return _convert_fd_to_external(fd_m)
|
|
|
|
|
|
def logbook_destroy(lb_id):
|
|
session = db_session.get_session()
|
|
with session.begin():
|
|
try:
|
|
lb = _logbook_get_model(lb_id, session=session)
|
|
session.delete(lb)
|
|
except sql_exc.DBAPIError as e:
|
|
raise exc.StorageError("Failed destroying"
|
|
" logbook %s: %s" % (lb_id, e), e)
|
|
|
|
|
|
def logbook_save(lb):
|
|
session = db_session.get_session()
|
|
with session.begin():
|
|
try:
|
|
lb_m = _logbook_get_model(lb.uuid, session=session)
|
|
# NOTE(harlowja): Merge them (note that this doesn't provide 100%
|
|
# correct update semantics due to how databases have MVCC). This
|
|
# is where a stored procedure or a better backing store would
|
|
# handle this better (something more suited to this type of data).
|
|
for fd in lb:
|
|
existing_fd = False
|
|
for fd_m in lb_m.flowdetails:
|
|
if fd_m.uuid == fd.uuid:
|
|
existing_fd = True
|
|
if fd_m.meta != fd.meta:
|
|
fd_m.meta = fd.meta
|
|
if fd_m.state != fd.state:
|
|
fd_m.state = fd.state
|
|
for td in fd:
|
|
existing_td = False
|
|
for td_m in fd_m.taskdetails:
|
|
if td_m.uuid == td.uuid:
|
|
existing_td = True
|
|
td_m = _taskdetails_merge(td_m, td)
|
|
break
|
|
if not existing_td:
|
|
td_m = _convert_td_to_internal(td, fd_m.uuid)
|
|
fd_m.taskdetails.append(td_m)
|
|
if not existing_fd:
|
|
lb_m.flowdetails.append(_convert_fd_to_internal(fd,
|
|
lb_m.uuid))
|
|
except exc.NotFound:
|
|
lb_m = _convert_lb_to_internal(lb)
|
|
try:
|
|
lb_m = session.merge(lb_m)
|
|
return _convert_lb_to_external(lb_m)
|
|
except sql_exc.DBAPIError as e:
|
|
raise exc.StorageError("Failed saving"
|
|
" logbook %s: %s" % (lb.uuid, e), e)
|
|
|
|
|
|
def logbook_get(lb_id):
|
|
session = db_session.get_session()
|
|
try:
|
|
lb_m = _logbook_get_model(lb_id, session=session)
|
|
return _convert_lb_to_external(lb_m)
|
|
except sql_exc.DBAPIError as e:
|
|
raise exc.StorageError("Failed getting"
|
|
" logbook %s: %s" % (lb_id, e), e)
|