diff --git a/cloudpulse/common/paths.py b/cloudpulse/common/paths.py new file mode 100644 index 0000000..2d908b2 --- /dev/null +++ b/cloudpulse/common/paths.py @@ -0,0 +1,66 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright 2012 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. + +import os + +from oslo_config import cfg + +PATH_OPTS = [ + cfg.StrOpt('pybasedir', + default=os.path.abspath(os.path.join(os.path.dirname(__file__), + '../')), + help='Directory where cloudpulse python module is installed.'), + cfg.StrOpt('bindir', + default='$pybasedir/bin', + help='Directory where cloudpulse binaries are installed.'), + cfg.StrOpt('state_path', + default='$pybasedir', + help="Top-level directory for maintaining cloudpulse's state."), +] + +CONF = cfg.CONF +CONF.register_opts(PATH_OPTS) + + +def basedir_def(*args): + """Return an uninterpolated path relative to $pybasedir.""" + return os.path.join('$pybasedir', *args) + + +def bindir_def(*args): + """Return an uninterpolated path relative to $bindir.""" + return os.path.join('$bindir', *args) + + +def state_path_def(*args): + """Return an uninterpolated path relative to $state_path.""" + return os.path.join('$state_path', *args) + + +def basedir_rel(*args): + """Return a path relative to $pybasedir.""" + return os.path.join(CONF.pybasedir, *args) + + +def bindir_rel(*args): + """Return a path relative to $bindir.""" + return os.path.join(CONF.bindir, *args) + + +def state_path_rel(*args): + """Return a path relative to $state_path.""" + return os.path.join(CONF.state_path, *args) diff --git a/cloudpulse/db/sqlalchemy/__init__.py b/cloudpulse/db/sqlalchemy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudpulse/db/sqlalchemy/api.py b/cloudpulse/db/sqlalchemy/api.py new file mode 100644 index 0000000..c2070cb --- /dev/null +++ b/cloudpulse/db/sqlalchemy/api.py @@ -0,0 +1,223 @@ +# 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 cloudpulse.common import exception +from cloudpulse.common import utils +from cloudpulse.db import api +from cloudpulse.db.sqlalchemy import models +from cloudpulse.openstack.common._i18n import _ +from cloudpulse.openstack.common import log +from oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_db.sqlalchemy import session as db_session +from oslo_db.sqlalchemy import utils as db_utils +from oslo_utils import timeutils +import sqlalchemy.orm.exc + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +_FACADE = None + + +def _create_facade_lazily(): + global _FACADE + if _FACADE is None: + _FACADE = db_session.EngineFacade.from_config(CONF) + return _FACADE + + +def get_engine(): + facade = _create_facade_lazily() + return facade.get_engine() + + +def get_session(**kwargs): + facade = _create_facade_lazily() + return facade.get_session(**kwargs) + + +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 get_session() + query = session.query(model, *args) + return query + + +def add_identity_filter(query, value): + """Adds an identity filter to a query. + + Filters results by ID, if supplied value is a valid integer. + Otherwise attempts to filter results by UUID. + + :param query: Initial query to add filter to. + :param value: Value for filtering results by. + :return: Modified query. + """ + if utils.is_int_like(value): + return query.filter_by(id=value) + elif utils.is_uuid_like(value): + return query.filter_by(uuid=value) + else: + raise exception.InvalidIdentity(identity=value) + + +def _paginate_query(model, limit=None, marker=None, sort_key=None, + sort_dir=None, query=None): + if not query: + query = model_query(model) + sort_keys = ['id'] + if sort_key and sort_key not in sort_keys: + sort_keys.insert(0, sort_key) + query = db_utils.paginate_query(query, model, limit, sort_keys, + marker=marker, sort_dir=sort_dir) + return query.all() + + +class Connection(api.Connection): + """SqlAlchemy connection.""" + + def __init__(self): + pass + + def _add_tenant_filters(self, context, query): + if context.project_id: + query = query.filter_by(project_id=context.project_id) + else: + query = query.filter_by(user_id=context.user_id) + + return query + + def _add_tests_filters(self, query, filters): + if filters is None: + filters = [] + + if 'name' in filters: + query = query.filter_by(name=filters['name']) + + return query + + def get_test_list(self, context, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + # query = model_query(models.cpulse) + query = model_query(models.cpulse) + return _paginate_query(models.cpulse, limit, marker, + sort_key, sort_dir, query) + + def create_test(self, values): + # ensure defaults are present for new tests + if not values.get('uuid'): + values['uuid'] = utils.generate_uuid() + + if not values.get('id'): + values['id'] = 777 + + cpulse = models.cpulse() + cpulse.update(values) + # TODO(VINOD) + try: + cpulse.save() + except db_exc.DBDuplicateEntry: + raise exception.TestAlreadyExists(uuid=values['uuid']) + return cpulse + + def get_test_by_id(self, context, test_id): + query = model_query(models.cpulse) + query = self._add_tenant_filters(context, query) + query = query.filter_by(id=test_id) + try: + return query.one() + except sqlalchemy.orm.exc.NoResultFound: + raise exception.TestNotFound(test=test_id) + + def get_test_by_name(self, context, test_name): + query = model_query(models.cpulse) + query = self._add_tenant_filters(context, query) + query = query.filter_by(name=test_name) + try: + return query.one() + except sqlalchemy.orm.exc.MultipleResultsFound: + raise exception.Conflict('Multiple tests exist with same name.' + ' Please use the test uuid instead.') + except sqlalchemy.orm.exc.NoResultFound: + raise exception.TestNotFound(test=test_name) + + def get_test_by_uuid(self, context, uuid): + query = model_query(models.cpulse) + # query = self._add_tenant_filters(context, query) + query = query.filter_by(uuid=uuid) + try: + return query.one() + except sqlalchemy.orm.exc.NoResultFound: + raise exception.TestNotFound(test=uuid) + + def destroy_test(self, test_id): + session = get_session() + test_ref = None + with session.begin(): + query = model_query(models.cpulse, session=session) + query = add_identity_filter(query, test_id) + + try: + test_ref = query.one() + except sqlalchemy.orm.exc.NoResultFound: + raise exception.TestNotFound(test=test_id) + query.delete() + + return test_ref + + def update_test(self, test_id, values): + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing test.") + raise exception.InvalidParameterValue(err=msg) + + return self._do_update_test(test_id, values) + + def _do_update_test(self, test_id, values): + session = get_session() + with session.begin(): + query = model_query(models.cpulse, session=session) + query = add_identity_filter(query, test_id) + try: + ref = query.with_lockmode('update').one() + except sqlalchemy.orm.exc.NoResultFound: + raise exception.TestNotFound(test=test_id) + + if 'provision_state' in values: + values['provision_updated_at'] = timeutils.utcnow() + + ref.update(values) + return ref + + def create_test_lock(self, test_uuid, conductor_id): + pass + + def steal_test_lock(self, test_uuid, old_conductor_id, new_conductor_id): + pass + + def release_test_lock(self, test_uuid, conductor_id): + pass diff --git a/cloudpulse/db/sqlalchemy/models.py b/cloudpulse/db/sqlalchemy/models.py new file mode 100644 index 0000000..ee2b0ac --- /dev/null +++ b/cloudpulse/db/sqlalchemy/models.py @@ -0,0 +1,138 @@ +# 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 models for cloudpulse service +""" + +import json + +from oslo_config import cfg +from oslo_db import options as db_options +from oslo_db.sqlalchemy import models +import six.moves.urllib.parse as urlparse +from sqlalchemy import Column +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Integer +from sqlalchemy import schema +from sqlalchemy import String +from sqlalchemy import Text +from sqlalchemy.types import TypeDecorator, TEXT + +from cloudpulse.common import paths + + +sql_opts = [ + cfg.StrOpt('mysql_engine', + default='InnoDB', + help='MySQL engine to use.') +] + +_DEFAULT_SQL_CONNECTION = ('sqlite:///' + + paths.state_path_def('cloudpulse.sqlite')) + +cfg.CONF.register_opts(sql_opts, 'database') +db_options.set_defaults(cfg.CONF, _DEFAULT_SQL_CONNECTION, 'cloudpulse.sqlite') + + +def table_args(): + engine_name = urlparse.urlparse(cfg.CONF.database.connection).scheme + if engine_name == 'mysql': + return {'mysql_engine': cfg.CONF.database.mysql_engine, + 'mysql_charset': "utf8"} + return None + + +class JsonEncodedType(TypeDecorator): + """Abstract base type serialized as json-encoded string in db.""" + type = None + impl = TEXT + + def process_bind_param(self, value, dialect): + if value is None: + # Save default value according to current type to keep the + # interface the consistent. + value = self.type() + elif not isinstance(value, self.type): + raise TypeError("%s supposes to store %s objects, but %s given" + % (self.__class__.__name__, + self.type.__name__, + type(value).__name__)) + serialized_value = json.dumps(value) + return serialized_value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + +class JSONEncodedDict(JsonEncodedType): + """Represents dict serialized as json-encoded string in db.""" + type = dict + + +class JSONEncodedList(JsonEncodedType): + """Represents list serialized as json-encoded string in db.""" + type = list + + +class CpulseBase(models.TimestampMixin, + models.ModelBase): + + metadata = None + + def as_dict(self): + d = {} + for c in self.__table__.columns: + d[c.name] = self[c.name] + return d + + def save(self, session=None): + import cloudpulse.db.sqlalchemy.api as db_api + + if session is None: + session = db_api.get_session() + + super(CpulseBase, self).save(session) + +Base = declarative_base(cls=CpulseBase) + + +class cpulse(Base): + """Represents cloudpulse test""" + + __tablename__ = 'cpulse' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_test0uuid'), + table_args() + ) + id = Column(Integer, primary_key=True) + uuid = Column(String(64)) + name = Column(String(255)) + state = Column(String(255)) + result = Column(Text) + + +class CpulseLock(Base): + """Represents a cpulselock.""" + + __tablename__ = 'cpulselock' + __table_args__ = ( + schema.UniqueConstraint('test_uuid', name='uniq_testlock0test_uuid'), + table_args() + ) + id = Column(Integer, primary_key=True) + test_uuid = Column(String(36)) + conductor_id = Column(String(64)) diff --git a/requirements.txt b/requirements.txt index 95137a6..f9b5f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,18 @@ pbr>=0.6,!=0.7,<1.0 Babel>=1.3 + +jsonpatch>=1.1 +oslo.concurrency>=1.8.0,<1.9.0 # Apache-2.0 +oslo.config>=1.9.3,<1.10.0 # Apache-2.0 +oslo.context>=0.2.0,<0.3.0 # Apache-2.0 +oslo.db>=1.7.0,<1.8.0 # Apache-2.0 +oslo.messaging>=1.8.0,<1.9.0 # Apache-2.0 +oslo.serialization>=1.4.0,<1.5.0 # Apache-2.0 +oslo.utils>=1.4.0,<1.5.0 # Apache-2.0 +oslo.versionedobjects>=0.1.1,<0.2.0 +oslo.i18n>=1.5.0,<1.6.0 # Apache-2.0 +six>=1.9.0 +SQLAlchemy>=0.9.7,<=0.9.99 +taskflow>=0.7.1,<0.8.0 +