diff --git a/.gitignore b/.gitignore index 7df8843294..5103ec26e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vagrant/ *.pyc .idea +reddwarf_test.sqlite diff --git a/bin/reddwarf-server b/bin/reddwarf-server index ac55ee5e94..9f9c04d5fb 100755 --- a/bin/reddwarf-server +++ b/bin/reddwarf-server @@ -36,7 +36,7 @@ if os.path.exists(os.path.join(possible_topdir, 'reddwarf', '__init__.py')): from reddwarf import version from reddwarf.common import config from reddwarf.common import wsgi - +from reddwarf.db import db_api def create_options(parser): """Sets up the CLI and config-file options @@ -60,6 +60,7 @@ if __name__ == '__main__': (options, args) = config.parse_options(oparser) try: conf, app = config.Config.load_paste_app('reddwarf', options, args) + db_api.configure_db(conf) server = wsgi.Server() server.start(app, options.get('port', conf['bind_port']), conf['bind_host']) diff --git a/development/development_enviroment.sh b/development/development_enviroment.sh index db91ba1b81..7610c1ce35 100644 --- a/development/development_enviroment.sh +++ b/development/development_enviroment.sh @@ -33,8 +33,7 @@ curl -d '{"auth":{"passwordCredentials":{"username": "reddwarf", "password": "RE # now get a list of instances, which connects over python-novaclient to nova # NOTE THIS AUTH TOKEN NEEDS TO BE CHANGED # Also note that keystone uses the tenant id now and _not_ the name -#curl -H"content-type:application/xml" -H"X-Auth-Project-Id:$REDDWARF_TENANT" -H"X-Auth-User:reddwarf" \ -# -H'X-Auth-Key:2a2c89c6a7284d32bcb94b4e56f0411c' http://0.0.0.0:8779/v2/$REDDWARF_TENANT/instances +# curl -H'X-Auth-Token:AUTH_TOKEN' http://0.0.0.0:8779/v0.1/$REDDWARF_TENANT/instances # Also, you should start up the api node like this # bin/reddwarf-api-os-database --flagfile=etc/nova/nova.conf.template diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py index 42b54ffd89..0908d46bac 100644 --- a/reddwarf/common/exception.py +++ b/reddwarf/common/exception.py @@ -33,9 +33,9 @@ class ReddwarfError(openstack_exception.OpenstackException): super(ReddwarfError, self).__init__(**kwargs) -class ThisIsATestError(ReddwarfError): +class DBConstraintError(ReddwarfError): - message = _("Data Missing") + message = _("Failed to save %(model_name)s because: %(error)s") diff --git a/reddwarf/common/utils.py b/reddwarf/common/utils.py index 9c11fde4be..75348a2c34 100644 --- a/reddwarf/common/utils.py +++ b/reddwarf/common/utils.py @@ -16,12 +16,33 @@ # under the License. """I totally stole most of this from melange, thx guys!!!""" +import datetime +import inspect +import re +import uuid + +from reddwarf.openstack.common import utils as openstack_utils + + +import_class = openstack_utils.import_class +import_object = openstack_utils.import_object +bool_from_string = openstack_utils.bool_from_string + + def stringify_keys(dictionary): if dictionary is None: return None return dict((str(key), value) for key, value in dictionary.iteritems()) +def generate_uuid(): + return str(uuid.uuid4()) + + +def utcnow(): + return datetime.datetime.utcnow() + + class cached_property(object): """A decorator that converts a function into a lazy property. @@ -50,3 +71,42 @@ class cached_property(object): value = self.func(obj) setattr(obj, self.__name__, value) return value + +class MethodInspector(object): + + def __init__(self, func): + self._func = func + + @cached_property + def required_args(self): + return self.args[0:self.required_args_count] + + @cached_property + def optional_args(self): + keys = self.args[self.required_args_count: len(self.args)] + return zip(keys, self.defaults) + + @cached_property + def defaults(self): + return self.argspec.defaults or () + + @cached_property + def required_args_count(self): + return len(self.args) - len(self.defaults) + + @cached_property + def args(self): + args = self.argspec.args + if inspect.ismethod(self._func): + args.pop(0) + return args + + @cached_property + def argspec(self): + return inspect.getargspec(self._func) + + def __str__(self): + optionals = ["[{0}=<{0}>]".format(k) for k, v in self.optional_args] + required = ["{0}=<{0}>".format(arg) for arg in self.required_args] + args_str = ' '.join(required + optionals) + return "%s %s" % (self._func.__name__, args_str) diff --git a/reddwarf/database/models.py b/reddwarf/database/models.py index a352d9dfc8..b685bfd45f 100644 --- a/reddwarf/database/models.py +++ b/reddwarf/database/models.py @@ -19,7 +19,12 @@ import logging import netaddr + +from reddwarf import db + from reddwarf.common import config +from reddwarf.common import exception +from reddwarf.common import utils from novaclient.v1_1.client import Client LOG = logging.getLogger('reddwarf.database.models') @@ -41,6 +46,30 @@ class ModelBase(object): data_fields = self._data_fields + self._auto_generated_attrs return dict([(field, self[field]) for field in data_fields]) + def is_valid(self): + self.errors = {} +# self._validate_columns_type() +# self._before_validate() +# self._validate() + return self.errors == {} + + def __setitem__(self, key, value): + setattr(self, key, value) + + def __getitem__(self, key): + return getattr(self, key) + + def __eq__(self, other): + if not hasattr(other, 'id'): + return False + return type(other) == type(self) and other.id == self.id + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return self.id.__hash__() + class RemoteModelBase(ModelBase): @@ -89,4 +118,49 @@ class Instances(Instance): class DatabaseModelBase(ModelBase): - _auto_generated_attrs = ["id", "created_at", "updated_at"] \ No newline at end of file + _auto_generated_attrs = ['id'] + + @classmethod + def create(cls, **values): + values['id'] = utils.generate_uuid() + print values +# values['created_at'] = utils.utcnow() + instance = cls(**values).save() +# instance._notify_fields("create") + return instance + + def save(self): + if not self.is_valid(): + raise InvalidModelError(self.errors) +# self._convert_columns_to_proper_type() +# self._before_save() + self['updated_at'] = utils.utcnow() + LOG.debug("Saving %s: %s" % (self.__class__.__name__, self.__dict__)) + return db.db_api.save(self) + + def __init__(self, **kwargs): + self.merge_attributes(kwargs) + + def merge_attributes(self, values): + """dict.update() behaviour.""" + for k, v in values.iteritems(): + self[k] = v + + + +class DBInstance(DatabaseModelBase): + _data_fields = ['name', 'status'] + +def persisted_models(): + return { + 'instance': DBInstance, + } + +class InvalidModelError(exception.ReddwarfError): + + message = _("The following values are invalid: %(errors)s") + + def __init__(self, errors, message=None): + super(InvalidModelError, self).__init__(message, errors=errors) + + diff --git a/reddwarf/database/service.py b/reddwarf/database/service.py index 22c5fd5f96..b87d4e6c3c 100644 --- a/reddwarf/database/service.py +++ b/reddwarf/database/service.py @@ -50,6 +50,8 @@ class InstanceController(BaseController): def index(self, req, tenant_id): """Return all instances.""" servers = models.Instances(req.headers["X-Auth-Token"]).data() + # Test to check that the db code works. this will eventually be removed + models.DBInstance.create(name='foo', status='status_foo') return wsgi.Result(views.InstancesView(servers).data(), 201) def show(self, req, tenant_id, id): diff --git a/reddwarf/db/__init__.py b/reddwarf/db/__init__.py index d65c689a83..623910d763 100644 --- a/reddwarf/db/__init__.py +++ b/reddwarf/db/__init__.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. +# Copyright 2010-2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,3 +14,86 @@ # 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 optparse + +from reddwarf.common import utils +from reddwarf.common import config + + +db_api = utils.import_object(config.Config.get("db_api_implementation", + "reddwarf.db.sqlalchemy.api")) + + +class Query(object): + """Mimics sqlalchemy query object. + + This class allows us to store query conditions and use them with + bulk updates and deletes just like sqlalchemy query object. + Using this class makes the models independent of sqlalchemy + + """ + def __init__(self, model, query_func, **conditions): + self._query_func = query_func + self._model = model + self._conditions = conditions + + def all(self): + return db_api.list(self._query_func, self._model, **self._conditions) + + def count(self): + return db_api.count(self._query_func, self._model, **self._conditions) + + def __iter__(self): + return iter(self.all()) + + def update(self, **values): + db_api.update_all(self._query_func, self._model, self._conditions, + values) + + def delete(self): + db_api.delete_all(self._query_func, self._model, **self._conditions) + + def limit(self, limit=200, marker=None, marker_column=None): + return db_api.find_all_by_limit(self._query_func, + self._model, + self._conditions, + limit=limit, + marker=marker, + marker_column=marker_column) + + def paginated_collection(self, limit=200, marker=None, marker_column=None): + collection = self.limit(int(limit) + 1, marker, marker_column) + if len(collection) > int(limit): + return (collection[0:-1], collection[-2]['id']) + return (collection, None) + + +class Queryable(object): + + def __getattr__(self, item): + return lambda model, **conditions: Query( + model, query_func=getattr(db_api, item), **conditions) + +db_query = Queryable() + + +def add_options(parser): + """Adds any configuration options that the db layer might have. + + :param parser: An optparse.OptionParser object + :retval None + + """ + help_text = ("The following configuration options are specific to the " + "Reddwarf database.") + + group = optparse.OptionGroup(parser, + "Registry Database Options", + help_text) + group.add_option('--sql-connection', + metavar="CONNECTION", + default=None, + help="A valid SQLAlchemy connection string for the " + "registry database. Default: %default") + parser.add_option_group(group) diff --git a/reddwarf/db/sqlalchemy/__init__.py b/reddwarf/db/sqlalchemy/__init__.py new file mode 100644 index 0000000000..b60695702c --- /dev/null +++ b/reddwarf/db/sqlalchemy/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# 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. diff --git a/reddwarf/db/sqlalchemy/api.py b/reddwarf/db/sqlalchemy/api.py new file mode 100644 index 0000000000..393a3614ec --- /dev/null +++ b/reddwarf/db/sqlalchemy/api.py @@ -0,0 +1,144 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +import sqlalchemy.exc +from sqlalchemy import and_ +from sqlalchemy import or_ +from sqlalchemy.orm import aliased + +from reddwarf.common import exception +from reddwarf.common import utils +from reddwarf.db.sqlalchemy import migration +from reddwarf.db.sqlalchemy import mappers +from reddwarf.db.sqlalchemy import session + + +def list(query_func, *args, **kwargs): + return query_func(*args, **kwargs).all() + + +def count(query, *args, **kwargs): + return query(*args, **kwargs).count() + + +def find_all(model, **conditions): + return _query_by(model, **conditions) + + +def find_all_by_limit(query_func, model, conditions, limit, marker=None, + marker_column=None): + return _limits(query_func, model, conditions, limit, marker, + marker_column).all() + + +def find_by(model, **kwargs): + return _query_by(model, **kwargs).first() + + +def save(model): + try: + db_session = session.get_session() + model = db_session.merge(model) + db_session.flush() + return model + except sqlalchemy.exc.IntegrityError as error: + raise exception.DBConstraintError(model_name=model.__class__.__name__, + error=str(error.orig)) + + +def delete(model): + db_session = session.get_session() + model = db_session.merge(model) + db_session.delete(model) + db_session.flush() + + +def delete_all(query_func, model, **conditions): + query_func(model, **conditions).delete() + + +def update(model, **values): + for k, v in values.iteritems(): + model[k] = v + + +def update_all(query_func, model, conditions, values): + query_func(model, **conditions).update(values) + + +def configure_db(options, *plugins): + session.configure_db(options) + configure_db_for_plugins(options, *plugins) + + +def configure_db_for_plugins(options, *plugins): + for plugin in plugins: + session.configure_db(options, models_mapper=plugin.mapper) + + +def drop_db(options): + session.drop_db(options) + + +def clean_db(): + session.clean_db() + + +def db_sync(options, version=None, repo_path=None): + migration.db_sync(options, version, repo_path) + + +def db_upgrade(options, version=None, repo_path=None): + migration.upgrade(options, version, repo_path) + + +def db_downgrade(options, version, repo_path=None): + migration.downgrade(options, version, repo_path) + + +def db_reset(options, *plugins): + drop_db(options) + db_sync(options) + db_reset_for_plugins(options, *plugins) + configure_db(options) + + +def db_reset_for_plugins(options, *plugins): + for plugin in plugins: + repo_path = plugin.migrate_repo_path() + if repo_path: + db_sync(options, repo_path=repo_path) + configure_db(options, *plugins) + + +def _base_query(cls): + return session.get_session().query(cls) + + +def _query_by(cls, **conditions): + query = _base_query(cls) + if conditions: + query = query.filter_by(**conditions) + return query + + +def _limits(query_func, model, conditions, limit, marker, marker_column=None): + query = query_func(model, **conditions) + marker_column = marker_column or model.id + if marker: + query = query.filter(marker_column > marker) + return query.order_by(marker_column).limit(limit) diff --git a/reddwarf/db/sqlalchemy/mappers.py b/reddwarf/db/sqlalchemy/mappers.py new file mode 100644 index 0000000000..c916b1fd19 --- /dev/null +++ b/reddwarf/db/sqlalchemy/mappers.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +from sqlalchemy import MetaData +from sqlalchemy import Table +from sqlalchemy import orm +from sqlalchemy.orm import exc as orm_exc + + +def map(engine, models): + meta = MetaData() + meta.bind = engine + if mapping_exists(models['instance']): + return + + orm.mapper(models['instance'], Table('instances', meta, autoload=True)) + +def mapping_exists(model): + try: + orm.class_mapper(model) + return True + except orm_exc.UnmappedClassError: + return False diff --git a/reddwarf/db/sqlalchemy/migrate_repo/README b/reddwarf/db/sqlalchemy/migrate_repo/README new file mode 100644 index 0000000000..6218f8cac4 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/README @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff --git a/reddwarf/db/sqlalchemy/migrate_repo/__init__.py b/reddwarf/db/sqlalchemy/migrate_repo/__init__.py new file mode 100644 index 0000000000..d65c689a83 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. diff --git a/reddwarf/db/sqlalchemy/migrate_repo/manage.py b/reddwarf/db/sqlalchemy/migrate_repo/manage.py new file mode 100644 index 0000000000..84bf4d6cb9 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +from migrate.versioning.shell import main + +main(debug='False', repository='.') diff --git a/reddwarf/db/sqlalchemy/migrate_repo/migrate.cfg b/reddwarf/db/sqlalchemy/migrate_repo/migrate.cfg new file mode 100644 index 0000000000..3089120144 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/migrate.cfg @@ -0,0 +1,21 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=Reddwarf Migrations + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] + +required_dbs=['mysql','postgres','sqlite'] diff --git a/reddwarf/db/sqlalchemy/migrate_repo/schema.py b/reddwarf/db/sqlalchemy/migrate_repo/schema.py new file mode 100644 index 0000000000..6970dac698 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/schema.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +"""Various conveniences used for migration scripts.""" + +import logging +import sqlalchemy.types + + +logger = logging.getLogger('reddwarf.db.sqlalchemy.migrate_repo.schema') + + +String = lambda length: sqlalchemy.types.String( + length=length, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False) + + +Text = lambda: sqlalchemy.types.Text( + length=None, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False) + + +Boolean = lambda: sqlalchemy.types.Boolean(create_constraint=True, name=None) + + +DateTime = lambda: sqlalchemy.types.DateTime(timezone=False) + + +Integer = lambda: sqlalchemy.types.Integer() + + +BigInteger = lambda: sqlalchemy.types.BigInteger() + + +def create_tables(tables): + for table in tables: + logger.info("creating table %(table)s" % locals()) + table.create() + + +def drop_tables(tables): + for table in tables: + logger.info("dropping table %(table)s" % locals()) + table.drop() + + +def Table(name, metadata, *args, **kwargs): + return sqlalchemy.schema.Table(name, metadata, *args, + mysql_engine='INNODB', **kwargs) diff --git a/reddwarf/db/sqlalchemy/migrate_repo/versions/001_base_schema.py b/reddwarf/db/sqlalchemy/migrate_repo/versions/001_base_schema.py new file mode 100644 index 0000000000..018f50febf --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/versions/001_base_schema.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +from sqlalchemy import ForeignKey +from sqlalchemy.schema import Column +from sqlalchemy.schema import MetaData +from sqlalchemy.schema import UniqueConstraint + +from reddwarf.db.sqlalchemy.migrate_repo.schema import Boolean +from reddwarf.db.sqlalchemy.migrate_repo.schema import create_tables +from reddwarf.db.sqlalchemy.migrate_repo.schema import DateTime +from reddwarf.db.sqlalchemy.migrate_repo.schema import drop_tables +from reddwarf.db.sqlalchemy.migrate_repo.schema import Integer +from reddwarf.db.sqlalchemy.migrate_repo.schema import BigInteger +from reddwarf.db.sqlalchemy.migrate_repo.schema import String +from reddwarf.db.sqlalchemy.migrate_repo.schema import Table + + +meta = MetaData() + +instances = Table('instances', meta, + Column('id', String(36), primary_key=True, nullable=False), + Column('name', String(255)), + Column('status', String(255))) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + create_tables([instances,]) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + drop_tables([instances,]) diff --git a/reddwarf/db/sqlalchemy/migrate_repo/versions/__init__.py b/reddwarf/db/sqlalchemy/migrate_repo/versions/__init__.py new file mode 100644 index 0000000000..e5e8b3743b --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/versions/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +# template repository default versions module diff --git a/reddwarf/db/sqlalchemy/migration.py b/reddwarf/db/sqlalchemy/migration.py new file mode 100644 index 0000000000..ca0ecdc8d4 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migration.py @@ -0,0 +1,134 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +import logging +import os + +from migrate.versioning import api as versioning_api +# See LP bug #719834. sqlalchemy-migrate changed location of +# exceptions.py after 0.6.0. +try: + from migrate.versioning import exceptions as versioning_exceptions +except ImportError: + from migrate import exceptions as versioning_exceptions + +from reddwarf.common import exception + + +logger = logging.getLogger('reddwarf.db.sqlalchemy.migration') + + +def db_version(options, repo_path=None): + """Return the database's current migration number. + + :param options: options dict + :retval version number + + """ + repo_path = get_migrate_repo_path(repo_path) + sql_connection = options['sql_connection'] + try: + return versioning_api.db_version(sql_connection, repo_path) + except versioning_exceptions.DatabaseNotControlledError: + msg = ("database '%(sql_connection)s' is not under migration control" + % locals()) + raise exception.DatabaseMigrationError(msg) + + +def upgrade(options, version=None, repo_path=None): + """Upgrade the database's current migration level. + + :param options: options dict + :param version: version to upgrade (defaults to latest) + :retval version number + + """ + db_version(options, repo_path) # Ensure db is under migration control + repo_path = get_migrate_repo_path(repo_path) + sql_connection = options['sql_connection'] + version_str = version or 'latest' + logger.info("Upgrading %(sql_connection)s to version %(version_str)s" % + locals()) + return versioning_api.upgrade(sql_connection, repo_path, version) + + +def downgrade(options, version, repo_path=None): + """Downgrade the database's current migration level. + + :param options: options dict + :param version: version to downgrade to + :retval version number + + """ + db_version(options, repo_path) # Ensure db is under migration control + repo_path = get_migrate_repo_path(repo_path) + sql_connection = options['sql_connection'] + logger.info("Downgrading %(sql_connection)s to version %(version)s" % + locals()) + return versioning_api.downgrade(sql_connection, repo_path, version) + + +def version_control(options, repo_path=None): + """Place a database under migration control. + + :param options: options dict + + """ + sql_connection = options['sql_connection'] + try: + _version_control(options) + except versioning_exceptions.DatabaseAlreadyControlledError: + msg = ("database '%(sql_connection)s' is already under migration " + "control" % locals()) + raise exception.DatabaseMigrationError(msg) + + +def _version_control(options, repo_path): + """Place a database under migration control. + + :param options: options dict + + """ + repo_path = get_migrate_repo_path(repo_path) + sql_connection = options['sql_connection'] + return versioning_api.version_control(sql_connection, repo_path) + + +def db_sync(options, version=None, repo_path=None): + """Place a database under migration control and perform an upgrade. + + :param options: options dict + :param repo_path: used for plugin db migrations, defaults to main repo + :retval version number + + """ + try: + _version_control(options, repo_path) + except versioning_exceptions.DatabaseAlreadyControlledError: + pass + + upgrade(options, version=version, repo_path=repo_path) + + +def get_migrate_repo_path(repo_path=None): + """Get the path for the migrate repository.""" + + default_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'migrate_repo') + repo_path = repo_path or default_path + assert os.path.exists(repo_path) + return repo_path diff --git a/reddwarf/db/sqlalchemy/session.py b/reddwarf/db/sqlalchemy/session.py new file mode 100644 index 0000000000..17c3641f1e --- /dev/null +++ b/reddwarf/db/sqlalchemy/session.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +import contextlib +import logging +from sqlalchemy import create_engine +from sqlalchemy import MetaData +from sqlalchemy.orm import sessionmaker + +from reddwarf import database +from reddwarf.common import config +from reddwarf.db.sqlalchemy import mappers + +_ENGINE = None +_MAKER = None + + +LOG = logging.getLogger('reddwarf.db.sqlalchemy.session') + + +def configure_db(options, models_mapper=None): + configure_sqlalchemy_log(options) + global _ENGINE + if not _ENGINE: + _ENGINE = _create_engine(options) + if models_mapper: + models_mapper.map(_ENGINE) + else: + mappers.map(_ENGINE, database.models.persisted_models()) + + +def configure_sqlalchemy_log(options): + debug = config.get_option(options, 'debug', type='bool', default=False) + verbose = config.get_option(options, 'verbose', type='bool', default=False) + logger = logging.getLogger('sqlalchemy.engine') + if debug: + logger.setLevel(logging.DEBUG) + elif verbose: + logger.setLevel(logging.INFO) + + +def _create_engine(options): + engine_args = { + "pool_recycle": config.get_option(options, + 'sql_idle_timeout', + type='int', + default=3600), + "echo": config.get_option(options, + 'sql_query_log', + type='bool', + default=False), + } + LOG.info("Creating SQLAlchemy engine with args: %s" % engine_args) + return create_engine(options['sql_connection'], **engine_args) + + +def get_session(autocommit=True, expire_on_commit=False): + """Helper method to grab session.""" + + global _MAKER, _ENGINE + if not _MAKER: + assert _ENGINE + _MAKER = sessionmaker(bind=_ENGINE, + autocommit=autocommit, + expire_on_commit=expire_on_commit) + return _MAKER() + + +def raw_query(model, autocommit=True, expire_on_commit=False): + return get_session(autocommit, expire_on_commit).query(model) + + +def clean_db(): + global _ENGINE + meta = MetaData() + meta.reflect(bind=_ENGINE) + with contextlib.closing(_ENGINE.connect()) as con: + trans = con.begin() + for table in reversed(meta.sorted_tables): + if table.name != "migrate_version": + con.execute(table.delete()) + trans.commit() + + +def drop_db(options): + meta = MetaData() + engine = _create_engine(options) + meta.bind = engine + meta.reflect() + meta.drop_all()