Introduce database functionality into KDS
Add database setup and initial testing into Key Distribution Server. Also adds a migration script. Implements: bp key-distribution-server Change-Id: I22e914ef1ca6ec8217d131c073261b309d9f637e
This commit is contained in:
parent
51602dcc25
commit
365e65dfd3
65
keystone/contrib/kds/cli/manage.py
Normal file
65
keystone/contrib/kds/cli/manage.py
Normal file
@ -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)
|
@ -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)
|
||||
|
61
keystone/contrib/kds/common/utils.py
Normal file
61
keystone/contrib/kds/common/utils.py
Normal file
@ -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)
|
0
keystone/contrib/kds/db/__init__.py
Normal file
0
keystone/contrib/kds/db/__init__.py
Normal file
34
keystone/contrib/kds/db/api.py
Normal file
34
keystone/contrib/kds/db/api.py
Normal file
@ -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
|
65
keystone/contrib/kds/db/connection.py
Normal file
65
keystone/contrib/kds/db/connection.py
Normal file
@ -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.
|
||||
"""
|
0
keystone/contrib/kds/db/kvs/__init__.py
Normal file
0
keystone/contrib/kds/db/kvs/__init__.py
Normal file
68
keystone/contrib/kds/db/kvs/api.py
Normal file
68
keystone/contrib/kds/db/kvs/api.py
Normal file
@ -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
|
41
keystone/contrib/kds/db/migration.py
Normal file
41
keystone/contrib/kds/db/migration.py
Normal file
@ -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()
|
0
keystone/contrib/kds/db/sqlalchemy/__init__.py
Normal file
0
keystone/contrib/kds/db/sqlalchemy/__init__.py
Normal file
82
keystone/contrib/kds/db/sqlalchemy/api.py
Normal file
82
keystone/contrib/kds/db/sqlalchemy/api.py
Normal file
@ -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}
|
19
keystone/contrib/kds/db/sqlalchemy/migrate_repo/manage.py
Normal file
19
keystone/contrib/kds/db/sqlalchemy/migrate_repo/manage.py
Normal file
@ -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='.')
|
20
keystone/contrib/kds/db/sqlalchemy/migrate_repo/migrate.cfg
Normal file
20
keystone/contrib/kds/db/sqlalchemy/migrate_repo/migrate.cfg
Normal file
@ -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=[]
|
@ -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)
|
34
keystone/contrib/kds/db/sqlalchemy/migration.py
Normal file
34
keystone/contrib/kds/db/sqlalchemy/migration.py
Normal file
@ -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)
|
62
keystone/contrib/kds/db/sqlalchemy/models.py
Normal file
62
keystone/contrib/kds/db/sqlalchemy/models.py
Normal file
@ -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)))
|
@ -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)
|
||||
|
0
keystone/tests/contrib/kds/db/__init__.py
Normal file
0
keystone/tests/contrib/kds/db/__init__.py
Normal file
30
keystone/tests/contrib/kds/db/base.py
Normal file
30
keystone/tests/contrib/kds/db/base.py
Normal file
@ -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()
|
138
keystone/tests/contrib/kds/db/test_host_key.py
Normal file
138
keystone/tests/contrib/kds/db/test_host_key.py
Normal file
@ -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)
|
@ -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]
|
||||
|
30
keystone/tests/contrib/kds/fixture/kvsdb.py
Normal file
30
keystone/tests/contrib/kds/fixture/kvsdb.py
Normal file
@ -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()
|
56
keystone/tests/contrib/kds/fixture/sqlitedb.py
Normal file
56
keystone/tests/contrib/kds/fixture/sqlitedb.py
Normal file
@ -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()
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user