diff --git a/keystone/contrib/kds/cli/manage.py b/keystone/contrib/kds/cli/manage.py new file mode 100644 index 0000000000..c13de289b5 --- /dev/null +++ b/keystone/contrib/kds/cli/manage.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 sys + +from keystone.openstack.common import gettextutils + +# gettextutils.install() must run to set _ before importing any modules that +# contain static translated strings. +gettextutils.install('keystone') + +from oslo.config import cfg + +from keystone.contrib.kds.common import service +from keystone.contrib.kds.db import migration + +CONF = cfg.CONF + + +def do_db_version(): + """Print database's current migration level.""" + print(migration.db_version()) + + +def do_db_sync(): + """Place a database under migration control and upgrade, + creating first if necessary. + """ + return migration.db_sync(CONF.command.version) + + +def add_command_parsers(subparsers): + parser = subparsers.add_parser('db_version') + parser.set_defaults(func=do_db_version) + + parser = subparsers.add_parser('db_sync') + parser.set_defaults(func=do_db_sync) + parser.add_argument('version', nargs='?') + + +command_opt = cfg.SubCommandOpt('command', + title='Commands', + help='Available commands', + handler=add_command_parsers) + + +def main(): + CONF.register_cli_opt(command_opt) + service.prepare_service(sys.argv) + + try: + CONF.command.func() + except Exception as e: + sys.exit("ERROR: %s" % e) diff --git a/keystone/contrib/kds/common/exception.py b/keystone/contrib/kds/common/exception.py index 7c5b916a26..0126e3222c 100644 --- a/keystone/contrib/kds/common/exception.py +++ b/keystone/contrib/kds/common/exception.py @@ -12,6 +12,45 @@ # License for the specific language governing permissions and limitations # under the License. +_FATAL_EXCEPTION_FORMAT_ERRORS = False + class KdsException(Exception): - pass + """Base Exception class. + + To correctly use this class, inherit from it and define + a 'msg_fmt' property. That message will get printf'd + with the keyword arguments provided to the constructor. + """ + + msg_fmt = _('An unknown exception occurred') + + def __init__(self, **kwargs): + try: + self._error_string = self.msg_fmt % kwargs + + except Exception: + if _FATAL_EXCEPTION_FORMAT_ERRORS: + raise + else: + # at least get the core message out if something happened + self._error_string = self.msg_fmt + + def __str__(self): + return self._error_string + + +class BackendException(KdsException): + msg_fmt = _("Failed to load the '%(backend)s' backend because it is not " + "allowed. Allowed backends are: %(allowed)s") + + +class IntegrityError(KdsException): + msg_fmt = _('Cannot set key data for %(name)s: %(reason)s') + + +class GroupStatusChanged(IntegrityError): + + def __init__(self, **kwargs): + kwargs.setdefault('reason', "Can't change group status of a host") + super(GroupStatusChanged, self).__init__(**kwargs) diff --git a/keystone/contrib/kds/common/utils.py b/keystone/contrib/kds/common/utils.py new file mode 100644 index 0000000000..77e364970a --- /dev/null +++ b/keystone/contrib/kds/common/utils.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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.config import cfg + +from keystone.contrib.kds.common import exception + +CONF = cfg.CONF + +# NOTE(jamielennox): This class is a direct copy from nova, glance, heat and +# a bunch of other projects. It has been submitted to OSLO +# https://review.openstack.org/#/c/67002/ and should be synced when available. + + +class LazyPluggable(object): + """A pluggable backend loaded lazily based on some value.""" + + def __init__(self, pivot, config_group=None, **backends): + self.__backends = backends + self.__pivot = pivot + self.__backend = None + self.__config_group = config_group + + def __get_backend(self): + if not self.__backend: + if self.__config_group is None: + backend_name = CONF[self.__pivot] + else: + backend_name = CONF[self.__config_group][self.__pivot] + if backend_name not in self.__backends: + allowed = ', '.join(self.__backends.iterkeys()) + raise exception.BackendException(backend=backend_name, + allowed=allowed) + + backend = self.__backends[backend_name] + if isinstance(backend, tuple): + name = backend[0] + fromlist = backend[1] + else: + name = backend + fromlist = backend + + self.__backend = __import__(name=name, globals=None, + locals=None, fromlist=fromlist) + return self.__backend + + def __getattr__(self, key): + backend = self.__get_backend() + return getattr(backend, key) diff --git a/keystone/contrib/kds/db/__init__.py b/keystone/contrib/kds/db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/contrib/kds/db/api.py b/keystone/contrib/kds/db/api.py new file mode 100644 index 0000000000..a6861b4837 --- /dev/null +++ b/keystone/contrib/kds/db/api.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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.config import cfg + +from keystone.openstack.common.db import api as db_api + +CONF = cfg.CONF + +_BACKEND_MAPPING = {'sqlalchemy': 'keystone.contrib.kds.db.sqlalchemy.api', + 'kvs': 'keystone.contrib.kds.db.kvs.api'} + +IMPL = db_api.DBAPI(backend_mapping=_BACKEND_MAPPING) + + +def reset(): + global IMPL + IMPL = db_api.DBAPI(backend_mapping=_BACKEND_MAPPING) + + +def get_instance(): + """Return a DB API instance.""" + return IMPL diff --git a/keystone/contrib/kds/db/connection.py b/keystone/contrib/kds/db/connection.py new file mode 100644 index 0000000000..1f959aab44 --- /dev/null +++ b/keystone/contrib/kds/db/connection.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class Connection(object): + + @abc.abstractmethod + def set_key(self, name, key, signature, group, expiration=None): + """Set a key for a name in the database. + + If a key is set for an existing key name then a new key entry with a + new generation value is created. + + :param string name: The unique name of the key to set. + :param string key: The key data to save. + :param string signature: The signature of the key data to save. + :param bool group: Whether this is a group key or not. + :param DateTime expiration: When the key should expire + (None is never expire). + + :raises IntegrityError: If a key exists then new keys assigned to the + name must have the same 'group' setting. If the + value of group is changed an IntegrityError is + raised. + + :returns int: The generation number of this key. + """ + + @abc.abstractmethod + def get_key(self, name, generation=None, group=None): + """Get key related to kds_id. + + :param string name: The unique name of the key to fetch. + :param int generation: A specific generation of the key to retrieve. If + not specified the most recent generation is + retrieved. + :param bool group: If provided only retrieve this key if its group + value is the same. + + :returns dict: A dictionary of the key information or None if not + found. Keys will contain: + - name: Unique name of the key. + - group: If this key is a group key or not. + - key: The key data. + - signature: The signature of the key data. + - generation: The generation of this key. + - expiration: When the key expires (or None). + Expired keys can be returned. + """ diff --git a/keystone/contrib/kds/db/kvs/__init__.py b/keystone/contrib/kds/db/kvs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/contrib/kds/db/kvs/api.py b/keystone/contrib/kds/db/kvs/api.py new file mode 100644 index 0000000000..2e267cdd10 --- /dev/null +++ b/keystone/contrib/kds/db/kvs/api.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystone.contrib.kds.common import exception +from keystone.contrib.kds.db import connection + + +def get_backend(): + return KvsDbImpl() + + +class KvsDbImpl(connection.Connection): + """A simple in-memory Key Value backend. + + KVS backends are designed for use in testing and for simple debugging. + This backend should not be deployed in any production systems. + """ + + def __init__(self): + super(KvsDbImpl, self).__init__() + self.clear() + + def clear(self): + self._data = dict() + + def set_key(self, name, key, signature, group, expiration=None): + host = self._data.setdefault(name, {'latest_generation': 0, + 'keys': dict(), 'group': group}) + + if host['group'] != group: + raise exception.GroupStatusChanged(name=name) + + host['latest_generation'] += 1 + host['keys'][host['latest_generation']] = {'key': key, + 'signature': signature, + 'expiration': expiration} + + return host['latest_generation'] + + def get_key(self, name, generation=None, group=None): + response = {'name': name} + try: + host = self._data[name] + if generation is None: + generation = host['latest_generation'] + key_data = host['keys'][generation] + except KeyError: + return None + + response['generation'] = generation + response['group'] = host['group'] + + if group is not None and host['group'] != group: + return None + + response.update(key_data) + return response diff --git a/keystone/contrib/kds/db/migration.py b/keystone/contrib/kds/db/migration.py new file mode 100644 index 0000000000..ea8ab225c6 --- /dev/null +++ b/keystone/contrib/kds/db/migration.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +"""Database setup and migration commands.""" + +from oslo.config import cfg + +from keystone.contrib.kds.common import utils + +CONF = cfg.CONF +CONF.import_opt('backend', + 'keystone.openstack.common.db.api', + group='database') + +_sqlalchemy_repo = 'keystone.contrib.kds.db.sqlalchemy.migration' +IMPL = utils.LazyPluggable(pivot='backend', + config_group='database', + sqlalchemy=_sqlalchemy_repo) + +INIT_VERSION = 0 + + +def db_sync(version=None): + """Migrate the database to `version` or the most recent version.""" + return IMPL.db_sync(version=version) + + +def db_version(): + """Display the current database version.""" + return IMPL.db_version() diff --git a/keystone/contrib/kds/db/sqlalchemy/__init__.py b/keystone/contrib/kds/db/sqlalchemy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/contrib/kds/db/sqlalchemy/api.py b/keystone/contrib/kds/db/sqlalchemy/api.py new file mode 100644 index 0000000000..74049e6311 --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/api.py @@ -0,0 +1,82 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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.orm import exc + +from keystone.contrib.kds.common import exception +from keystone.contrib.kds.db import connection +from keystone.contrib.kds.db.sqlalchemy import models +from keystone.openstack.common.db.sqlalchemy import session as db_session + + +def get_backend(): + return SqlalchemyDbImpl() + + +class SqlalchemyDbImpl(connection.Connection): + + def set_key(self, name, key, signature, group, expiration=None): + session = db_session.get_session() + + with session.begin(): + q = session.query(models.Host) + q = q.filter(models.Host.name == name) + + try: + host = q.one() + except exc.NoResultFound: + host = models.Host(name=name, + latest_generation=0, + group=group) + else: + if host.group != group: + raise exception.GroupStatusChanged(name=name) + + host.latest_generation += 1 + host.keys.append(models.Key(signature=signature, + enc_key=key, + generation=host.latest_generation, + expiration=expiration)) + + session.add(host) + + return host.latest_generation + + def get_key(self, name, generation=None, group=None): + session = db_session.get_session() + + query = session.query(models.Host, models.Key) + query = query.filter(models.Host.id == models.Key.host_id) + query = query.filter(models.Host.name == name) + + if group is not None: + query = query.filter(models.Host.group == group) + + if generation is not None: + query = query.filter(models.Key.generation == generation) + else: + query = query.filter(models.Host.latest_generation == + models.Key.generation) + + try: + result = query.one() + except exc.NoResultFound: + return None + + return {'name': result.Host.name, + 'group': result.Host.group, + 'key': result.Key.enc_key, + 'signature': result.Key.signature, + 'generation': result.Key.generation, + 'expiration': result.Key.expiration} diff --git a/keystone/contrib/kds/db/sqlalchemy/migrate_repo/__init__.py b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/contrib/kds/db/sqlalchemy/migrate_repo/manage.py b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/manage.py new file mode 100644 index 0000000000..a7e6e509c5 --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/manage.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + + +if __name__ == '__main__': + main(debug=False, repository='.') diff --git a/keystone/contrib/kds/db/sqlalchemy/migrate_repo/migrate.cfg b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/migrate.cfg new file mode 100644 index 0000000000..016cd278e9 --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/migrate.cfg @@ -0,0 +1,20 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=kds + +# 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=[] diff --git a/keystone/contrib/kds/db/sqlalchemy/migrate_repo/versions/001_kds_table.py b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/versions/001_kds_table.py new file mode 100644 index 0000000000..6725221522 --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/versions/001_kds_table.py @@ -0,0 +1,78 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + host_table = sql.Table('kds_hosts', meta, + sql.Column('id', + sql.Integer(), + primary_key=True, + autoincrement=True), + sql.Column('name', + sql.Text(), + nullable=False), + sql.Column('group', + sql.Boolean(), + nullable=False, + index=True), + sql.Column('latest_generation', + sql.Integer(), + nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + + # MySQL can't put an index on an unbound TEXT type so if we do it this way + # it will make the index on the first 20 characters which will be fine. + sql.Index('name_idx', host_table.c.name, unique=True, mysql_length=20) + + host_table.create(migrate_engine, checkfirst=True) + + key_table = sql.Table('kds_keys', meta, + sql.Column('host_id', + sql.Integer(), + sql.ForeignKey('kds_hosts.id'), + primary_key=True, + autoincrement=False), + sql.Column('generation', + sql.Integer(), + primary_key=True, + autoincrement=False), + sql.Column('signature', + sql.LargeBinary(), + nullable=False), + sql.Column('enc_key', + sql.LargeBinary(), + nullable=False), + sql.Column('expiration', + sql.DateTime(), + nullable=True, + index=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + key_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + for name in ['kds_keys', 'kds_hosts']: + table = sql.Table(name, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone/contrib/kds/db/sqlalchemy/migrate_repo/versions/__init__.py b/keystone/contrib/kds/db/sqlalchemy/migrate_repo/versions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/contrib/kds/db/sqlalchemy/migration.py b/keystone/contrib/kds/db/sqlalchemy/migration.py new file mode 100644 index 0000000000..61c631033f --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/migration.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystone.openstack.common.db.sqlalchemy import migration + + +def _repo_path(): + return os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'migrate_repo') + + +def db_version_control(version=None): + return migration.db_version_control(_repo_path(), version=version) + + +def db_sync(version=None): + return migration.db_sync(_repo_path(), version=version) + + +def db_version(version=None): + return migration.db_version(_repo_path(), version) diff --git a/keystone/contrib/kds/db/sqlalchemy/models.py b/keystone/contrib/kds/db/sqlalchemy/models.py new file mode 100644 index 0000000000..32dc476dc2 --- /dev/null +++ b/keystone/contrib/kds/db/sqlalchemy/models.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 as sql +from sqlalchemy.ext.declarative import declarative_base + +from keystone.openstack.common.db.sqlalchemy import models + + +class KdsBase(models.ModelBase): + pass + + +Base = declarative_base(cls=KdsBase) + + +class Host(Base): + __tablename__ = 'kds_hosts' + + id = sql.Column(sql.Integer(), primary_key=True, autoincrement=True) + name = sql.Column(sql.Text(), index=True, unique=True, nullable=False) + group = sql.Column(sql.Boolean(), nullable=False, index=True) + latest_generation = sql.Column(sql.Integer(), nullable=False) + + +class Key(Base): + __tablename__ = 'kds_keys' + + host_id = sql.Column(sql.Integer(), + sql.ForeignKey('kds_hosts.id'), + primary_key=True, + autoincrement=False) + + generation = sql.Column(sql.Integer(), + primary_key=True, + autoincrement=False) + + signature = sql.Column(sql.LargeBinary(), + nullable=False) + + enc_key = sql.Column(sql.LargeBinary(), + nullable=False) + + expiration = sql.Column(sql.DateTime(), + nullable=True, + index=True) + + owner = sql.orm.relationship('Host', + backref=sql.orm.backref('keys', + order_by=sql.desc( + generation))) diff --git a/keystone/tests/contrib/kds/base.py b/keystone/tests/contrib/kds/base.py index 946ba609f3..4cc4e7e56e 100644 --- a/keystone/tests/contrib/kds/base.py +++ b/keystone/tests/contrib/kds/base.py @@ -23,4 +23,8 @@ class BaseTestCase(test.BaseTestCase): super(BaseTestCase, self).setUp() self.config_fixture = self.useFixture(config.Config()) self.CONF = self.config_fixture.conf + service.parse_args(args=[]) + + def config(self, *args, **kwargs): + self.config_fixture.config(*args, **kwargs) diff --git a/keystone/tests/contrib/kds/db/__init__.py b/keystone/tests/contrib/kds/db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/tests/contrib/kds/db/base.py b/keystone/tests/contrib/kds/db/base.py new file mode 100644 index 0000000000..2c06794521 --- /dev/null +++ b/keystone/tests/contrib/kds/db/base.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 keystone.contrib.kds.db import api as db_api +from keystone.tests.contrib.kds import base +from keystone.tests.contrib.kds import fixture + + +class BaseTestCase(base.BaseTestCase): + + scenarios = [('sqlitedb', {'sql_fixture': fixture.SqliteDb}), + ('kvsdb', {'sql_fixture': fixture.KvsDb})] + + def setUp(self): + super(BaseTestCase, self).setUp() + + self.useFixture(self.sql_fixture()) + + self.DB = db_api.get_instance() diff --git a/keystone/tests/contrib/kds/db/test_host_key.py b/keystone/tests/contrib/kds/db/test_host_key.py new file mode 100644 index 0000000000..0ac2234ede --- /dev/null +++ b/keystone/tests/contrib/kds/db/test_host_key.py @@ -0,0 +1,138 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 testscenarios import load_tests_apply_scenarios as load_tests # noqa + +from keystone.contrib.kds.common import exception +from keystone.tests.contrib.kds.db import base + +TEST_NAME = 'test-name' +TEST_SIG = 'test-sig' +TEST_KEY = 'test-enc' + + +class KeyDbTestCase(base.BaseTestCase): + + def test_retrieve(self): + # Set a key and expect to get the same key back. + generation = self.DB.set_key(name=TEST_NAME, + signature=TEST_SIG, + key=TEST_KEY, + group=False) + key = self.DB.get_key(TEST_NAME) + + self.assertEqual(key['name'], TEST_NAME) + self.assertEqual(key['key'], TEST_KEY) + self.assertEqual(key['signature'], TEST_SIG) + self.assertEqual(key['generation'], generation) + self.assertIs(key['group'], False) + self.assertIsNone(key['expiration']) + + def test_no_key(self): + # return None if a key is not in the database + self.assertIsNone(self.DB.get_key(TEST_NAME)) + + def test_generations(self): + another_key = 'another-key' + + # set a key and make sure that the generation is set and returned + gen1 = self.DB.set_key(name=TEST_NAME, + signature=TEST_SIG, + key=TEST_KEY, + group=False) + + key1 = self.DB.get_key(TEST_NAME) + self.assertEqual(key1['key'], TEST_KEY) + self.assertEqual(key1['generation'], gen1) + + # set a new key for the same name and make sure that the generation is + # updated + gen2 = self.DB.set_key(name=TEST_NAME, + signature='another-sig', + key=another_key, + group=False) + + key2 = self.DB.get_key(TEST_NAME) + self.assertEqual(key2['generation'], gen2) + self.assertEqual(key2['key'], another_key) + + # Check that if we ask specifically for the first key we get it back + key3 = self.DB.get_key(TEST_NAME, gen1) + self.assertEqual(key3['key'], TEST_KEY) + self.assertEqual(key3['generation'], gen1) + + def test_no_group_filter(self): + # install a non group key + generation = self.DB.set_key(name=TEST_NAME, + signature=TEST_SIG, + key=TEST_KEY, + group=False) + + # test that if i can retrieve and specify a non-group key + key1 = self.DB.get_key(TEST_NAME) + self.assertEqual(key1['key'], TEST_KEY) + self.assertEqual(key1['generation'], generation) + + key2 = self.DB.get_key(TEST_NAME, group=False) + self.assertEqual(key2['key'], TEST_KEY) + self.assertEqual(key2['generation'], generation) + + # if i ask for a group key of that name then it should fail + key3 = self.DB.get_key(TEST_NAME, group=True) + self.assertIsNone(key3) + + def test_with_group_filter(self): + # install a group key + generation = self.DB.set_key(name=TEST_NAME, + signature=TEST_SIG, + key=TEST_KEY, + group=True) + + # i should be able to ask for and retrieve a group key + key1 = self.DB.get_key(TEST_NAME) + self.assertEqual(key1['key'], TEST_KEY) + self.assertEqual(key1['generation'], generation) + + key2 = self.DB.get_key(TEST_NAME, group=True) + self.assertEqual(key2['key'], TEST_KEY) + self.assertEqual(key2['generation'], generation) + + # if i ask for that key but not a group key it will fail + key3 = self.DB.get_key(TEST_NAME, group=False) + self.assertIsNone(key3) + + def test_cant_change_group_status(self): + group_key_name = 'name1' + host_key_name = 'name2' + + # install a host and group key + self.DB.set_key(name=group_key_name, + signature=TEST_SIG, + key=TEST_KEY, + group=True) + + self.DB.set_key(name=host_key_name, + signature=TEST_SIG, + key=TEST_KEY, + group=False) + + # should not be able to change a group key to a host key + self.assertRaises(exception.IntegrityError, self.DB.set_key, + name=group_key_name, signature='xxx', key='xxx', + group=False) + + # should not be able to change a host key to a group key + self.assertRaises(exception.IntegrityError, self.DB.set_key, + name=host_key_name, signature='xxx', key='xxx', + group=True) diff --git a/keystone/tests/contrib/kds/fixture/__init__.py b/keystone/tests/contrib/kds/fixture/__init__.py index 8c6d5f866c..313ae27960 100644 --- a/keystone/tests/contrib/kds/fixture/__init__.py +++ b/keystone/tests/contrib/kds/fixture/__init__.py @@ -11,3 +11,11 @@ # 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 keystone.tests.contrib.kds.fixture import kvsdb +from keystone.tests.contrib.kds.fixture import sqlitedb + +SqliteDb = sqlitedb.SqliteDb +KvsDb = kvsdb.KvsDb + +__all__ = [SqliteDb, KvsDb] diff --git a/keystone/tests/contrib/kds/fixture/kvsdb.py b/keystone/tests/contrib/kds/fixture/kvsdb.py new file mode 100644 index 0000000000..1426b281f8 --- /dev/null +++ b/keystone/tests/contrib/kds/fixture/kvsdb.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 fixtures +from oslo.config import cfg + +from keystone.contrib.kds.db import api as db_api + +CONF = cfg.CONF + + +class KvsDb(fixtures.Fixture): + + def setUp(self): + super(KvsDb, self).setUp() + + CONF.set_override('backend', 'kvs', 'database') + + db_api.reset() diff --git a/keystone/tests/contrib/kds/fixture/sqlitedb.py b/keystone/tests/contrib/kds/fixture/sqlitedb.py new file mode 100644 index 0000000000..6606605873 --- /dev/null +++ b/keystone/tests/contrib/kds/fixture/sqlitedb.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 + +import fixtures +from oslo.config import cfg + +from keystone.contrib.kds.db import api as db_api +from keystone.contrib.kds.db.sqlalchemy import migration +from keystone.openstack.common.db import exception as db_exception +from keystone import tests +from keystone.tests.contrib.kds import paths + +CONF = cfg.CONF + + +class SqliteDb(fixtures.Fixture): + """Connect to Keystone's sqlite database. + + KDS is not designed with the intention that it should run within the same + database as keystone however there is nothing preventing that. There seems + to be issues regarding the conflicting CONF objects between keystone and + KDS that prevent the connection to separate databases for testing. + Therefore this fixture must simply bridge the gap back to the testing + database for keystone and setup the KDS tables. + """ + + def setUp(self): + super(SqliteDb, self).setUp() + + sqlite_db = os.path.abspath(paths.tmp_path('test.db')) + + CONF.set_override('connection_debug', '51', 'database') + CONF.set_override('connection', 'sqlite:///%s' % sqlite_db, 'database') + + db_api.reset() + + tests.setup_database() + + try: + migration.db_sync() + except db_exception.DbMigrationError: + migration.db_version_control(0) + migration.db_sync() diff --git a/setup.cfg b/setup.cfg index d00511a0a7..db6c64b2d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,3 +65,4 @@ msgid_bugs_address = https://bugs.launchpad.net/keystone [entry_points] console_scripts = kds-api = keystone.contrib.kds.cli.api:main + kds-manage = keystone.contrib.kds.cli.manage:main diff --git a/test-requirements.txt b/test-requirements.txt index bf77383531..b4c008187b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -28,6 +28,7 @@ discover python-subunit testrepository>=0.0.17 testtools>=0.9.32 +testscenarios>=0.4 # for python-keystoneclient # keystoneclient <0.2.1