diff options
-rw-r--r-- | docs/source/configuration.rst | 50 | ||||
-rw-r--r-- | keystone/cli.py | 18 | ||||
-rw-r--r-- | keystone/common/sql/nova.py | 108 | ||||
-rw-r--r-- | tests/test_migrate_nova_auth.py | 136 |
4 files changed, 311 insertions, 1 deletions
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 22f3748b..a225c4ab 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -123,7 +123,7 @@ Use the following command to import your old data:: Specify db_url as the connection string that was present in your old keystone.conf file. -Step 3: Import your legacy service catalog +Step 4: Import your legacy service catalog ------------------------------------------ While the older keystone stored the service catalog in the database, the updated version configures the service catalog using a template file. @@ -139,6 +139,53 @@ To import your legacy catalog, run this command:: After executing this command, you will need to restart the keystone service to see your changes. +Migrating from Nova Auth +======================== +Migration of users, projects (aka tenants), roles and EC2 credentials +is supported for the Diablo and Essex releases of Nova. To migrate your auth +data from Nova, use the following steps: + +Step 1: Export your data from Nova +---------------------------------- +Use the following command to export your data fron Nova:: + + nova-manage export auth > /path/to/dump + +It is important to redirect the output to a file so it can be imported +in a later step. + +Step 2: db_sync your new, empty database +---------------------------------------- +Run the following command to configure the most recent schema in your new +keystone installation:: + + keystone-manage db_sync + +Step 3: Import your data to Keystone +------------------------------------ +To import your Nova auth data from a dump file created with nova-manage, +run this command:: + + keystone-manage import_nova_auth [dump_file, e.g. /path/to/dump] + +.. note:: + Users are added to Keystone with the user id from Nova as the user name. + Nova's projects are imported with the project id as the tenant name. The + password used to authenticate a user in Keystone will be the api key + (also EC2 access key) used in Nova. Users also lose any administrative + privileges they had in Nova. The necessary admin role must be explicitly + re-assigned to each user. + +.. note:: + Users in Nova's auth system have a single set of EC2 credentials that + works with all projects (tenants) that user can access. In Keystone, these + credentials are scoped to a single user/tenant pair. In order to use the + same secret keys from Nova, you must prefix each corresponding access key + with the id of the project used in Nova. For example, if you had access + to the 'Beta' project in your Nova installation with the access/secret + keys 'XXX'/'YYY', you should use 'Beta:XXX'/'YYY' in Keystone. These + credentials are active once your migration is complete. + Initializing Keystone ===================== @@ -148,6 +195,7 @@ through the normal REST api. At the moment, the following calls are supported: * ``db_sync``: Sync the database. * ``import_legacy``: Import a legacy (pre-essex) version of the db. * ``export_legacy_catalog``: Export service catalog from a legacy (pre-essex) db. +* ``import_nova_auth``: Load auth data from a dump created with keystone-manage. Generally, the following is the first step after a source installation:: diff --git a/keystone/cli.py b/keystone/cli.py index 039fc92c..383c4497 100644 --- a/keystone/cli.py +++ b/keystone/cli.py @@ -90,9 +90,27 @@ class ExportLegacyCatalog(BaseApp): print '\n'.join(migration.dump_catalog()) +class ImportNovaAuth(BaseApp): + """Import a dump of nova auth data into keystone.""" + + name = 'import_nova_auth' + + def __init__(self, *args, **kw): + super(ImportNovaAuth, self).__init__(*args, **kw) + + def main(self): + from keystone.common.sql import nova + if len(self.argv) < 2: + return self.missing_param('dump_file') + dump_file = self.argv[1] + dump_data = json.loads(open(dump_file).read()) + nova.import_auth(dump_data) + + CMDS = {'db_sync': DbSync, 'import_legacy': ImportLegacy, 'export_legacy_catalog': ExportLegacyCatalog, + 'import_nova_auth': ImportNovaAuth, } diff --git a/keystone/common/sql/nova.py b/keystone/common/sql/nova.py new file mode 100644 index 00000000..bf97669f --- /dev/null +++ b/keystone/common/sql/nova.py @@ -0,0 +1,108 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +"""Export data from Nova database and import through Identity Service.""" + +import logging +import uuid + +from keystone.contrib.ec2.backends import sql as ec2_sql +from keystone.identity.backends import sql as identity_sql + + +logger = logging.getLogger('keystone.common.sql.nova') + + +def import_auth(data): + identity_api = identity_sql.Identity() + tenant_map = _create_tenants(identity_api, data['tenants']) + user_map = _create_users(identity_api, data['users']) + _create_memberships(identity_api, data['user_tenant_list'], + user_map, tenant_map) + role_map = _create_roles(identity_api, data['roles']) + _assign_roles(identity_api, data['role_user_tenant_list'], + role_map, user_map, tenant_map) + + ec2_api = ec2_sql.Ec2() + ec2_creds = data['ec2_credentials'] + _create_ec2_creds(ec2_api, identity_api, ec2_creds, user_map) + + +def _generate_uuid(): + return uuid.uuid4().hex + + +def _create_tenants(api, tenants): + tenant_map = {} + for tenant in tenants: + tenant_dict = { + 'id': _generate_uuid(), + 'name': tenant['id'], + 'description': tenant['description'], + 'enabled': True, + } + tenant_map[tenant['id']] = tenant_dict['id'] + logger.debug('Create tenant %s' % tenant_dict) + api.create_tenant(tenant_dict['id'], tenant_dict) + return tenant_map + + +def _create_users(api, users): + user_map = {} + for user in users: + user_dict = { + 'id': _generate_uuid(), + 'name': user['id'], + 'email': '', + 'password': user['password'], + 'enabled': True, + } + user_map[user['id']] = user_dict['id'] + logger.debug('Create user %s' % user_dict) + api.create_user(user_dict['id'], user_dict) + return user_map + + +def _create_memberships(api, memberships, user_map, tenant_map): + for membership in memberships: + user_id = user_map[membership['user_id']] + tenant_id = tenant_map[membership['tenant_id']] + logger.debug('Add user %s to tenant %s' % (user_id, tenant_id)) + api.add_user_to_tenant(tenant_id, user_id) + + +def _create_roles(api, roles): + role_map = {} + for role in roles: + role_dict = { + 'id': _generate_uuid(), + 'name': role, + } + role_map[role] = role_dict['id'] + logger.debug('Create role %s' % role_dict) + api.create_role(role_dict['id'], role_dict) + return role_map + + +def _assign_roles(api, assignments, role_map, user_map, tenant_map): + for assignment in assignments: + role_id = role_map[assignment['role']] + user_id = user_map[assignment['user_id']] + tenant_id = tenant_map[assignment['tenant_id']] + logger.debug('Assign role %s to user %s on tenant %s' % + (role_id, user_id, tenant_id)) + api.add_role_to_user_and_tenant(user_id, tenant_id, role_id) + + +def _create_ec2_creds(ec2_api, identity_api, ec2_creds, user_map): + for ec2_cred in ec2_creds: + user_id = user_map[ec2_cred['user_id']] + for tenant_id in identity_api.get_tenants_for_user(user_id): + cred_dict = { + 'access': '%s:%s' % (tenant_id, ec2_cred['access_key']), + 'secret': ec2_cred['secret_key'], + 'user_id': user_id, + 'tenant_id': tenant_id, + } + logger.debug('Creating ec2 cred for user %s and tenant %s' % + (user_id, tenant_id)) + ec2_api.create_credential(None, cred_dict) diff --git a/tests/test_migrate_nova_auth.py b/tests/test_migrate_nova_auth.py new file mode 100644 index 00000000..1be59b17 --- /dev/null +++ b/tests/test_migrate_nova_auth.py @@ -0,0 +1,136 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# +# 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.common.sql import nova +from keystone.common.sql import util as sql_util +from keystone import config +from keystone.contrib.ec2.backends import sql as ec2_sql +from keystone.identity.backends import sql as identity_sql +from keystone import test + + +CONF = config.CONF + + +FIXTURE = { + 'users': [ + {'id': 'user1', 'name': 'uname1', 'password': 'acc1'}, + {'id': 'user4', 'name': 'uname4', 'password': 'acc1'}, + {'id': 'user2', 'name': 'uname2', 'password': 'acc2'}, + {'id': 'user3', 'name': 'uname3', 'password': 'acc3'}, + ], + 'roles': ['role1', 'role2', 'role3'], + 'role_user_tenant_list': [ + {'user_id': 'user1', 'role': 'role1', 'tenant_id': 'proj1'}, + {'user_id': 'user1', 'role': 'role2', 'tenant_id': 'proj1'}, + {'user_id': 'user4', 'role': 'role1', 'tenant_id': 'proj4'}, + {'user_id': 'user2', 'role': 'role1', 'tenant_id': 'proj1'}, + {'user_id': 'user2', 'role': 'role1', 'tenant_id': 'proj2'}, + {'user_id': 'user2', 'role': 'role2', 'tenant_id': 'proj2'}, + {'user_id': 'user3', 'role': 'role3', 'tenant_id': 'proj1'}, + ], + 'user_tenant_list': [ + {'tenant_id': 'proj1', 'user_id': 'user1'}, + {'tenant_id': 'proj4', 'user_id': 'user4'}, + {'tenant_id': 'proj1', 'user_id': 'user2'}, + {'tenant_id': 'proj2', 'user_id': 'user2'}, + {'tenant_id': 'proj1', 'user_id': 'user3'}, + ], + 'ec2_credentials': [ + {'access_key': 'acc1', 'secret_key': 'sec1', 'user_id': 'user1'}, + {'access_key': 'acc4', 'secret_key': 'sec4', 'user_id': 'user4'}, + {'access_key': 'acc2', 'secret_key': 'sec2', 'user_id': 'user2'}, + {'access_key': 'acc3', 'secret_key': 'sec3', 'user_id': 'user3'}, + ], + 'tenants': [ + {'description': 'desc1', 'id': 'proj1', 'name': 'pname1'}, + {'description': 'desc4', 'id': 'proj4', 'name': 'pname4'}, + {'description': 'desc2', 'id': 'proj2', 'name': 'pname2'}, + ], +} + + +class MigrateNovaAuth(test.TestCase): + def setUp(self): + super(MigrateNovaAuth, self).setUp() + CONF(config_files=[test.etcdir('keystone.conf'), + test.testsdir('test_overrides.conf'), + test.testsdir('backend_sql.conf')]) + sql_util.setup_test_database() + self.identity_api = identity_sql.Identity() + self.ec2_api = ec2_sql.Ec2() + + def test_import(self): + nova.import_auth(FIXTURE) + + users = {} + for user in ['user1', 'user2', 'user3', 'user4']: + users[user] = self.identity_api.get_user_by_name(user) + + tenants = {} + for tenant in ['proj1', 'proj2', 'proj4']: + tenants[tenant] = self.identity_api.get_tenant_by_name(tenant) + + membership_map = { + 'user1': ['proj1'], + 'user2': ['proj1', 'proj2'], + 'user3': ['proj1'], + 'user4': ['proj4'], + } + + for (old_user, old_tenants) in membership_map.iteritems(): + user = users[old_user] + membership = self.identity_api.get_tenants_for_user(user['id']) + expected = [tenants[t]['id'] for t in old_tenants] + self.assertEqual(set(expected), set(membership)) + for tenant_id in membership: + password = None + for _user in FIXTURE['users']: + if _user['id'] == old_user: + password = _user['password'] + self.identity_api.authenticate(user['id'], tenant_id, password) + + for ec2_cred in FIXTURE['ec2_credentials']: + user_id = users[ec2_cred['user_id']]['id'] + for tenant_id in self.identity_api.get_tenants_for_user(user_id): + access = '%s:%s' % (tenant_id, ec2_cred['access_key']) + cred = self.ec2_api.get_credential(access) + actual = cred['secret'] + expected = ec2_cred['secret_key'] + self.assertEqual(expected, actual) + + roles = self.identity_api.list_roles() + role_names = set([role['name'] for role in roles]) + self.assertEqual(role_names, set(['role2', 'role1', 'role3'])) + + assignment_map = { + 'user1': {'proj1': ['role1', 'role2']}, + 'user2': {'proj1': ['role1'], 'proj2': ['role1', 'role2']}, + 'user3': {'proj1': ['role3']}, + 'user4': {'proj4': ['role1']}, + } + + for (old_user, old_tenant_map) in assignment_map.iteritems(): + tenant_names = ['proj1', 'proj2', 'proj4'] + for tenant_name in tenant_names: + user = users[old_user] + tenant = tenants[tenant_name] + roles = self.identity_api.get_roles_for_user_and_tenant( + user['id'], tenant['id']) + actual = [self.identity_api.get_role(role_id)['name'] + for role_id in roles] + expected = old_tenant_map.get(tenant_name, []) + self.assertEqual(set(actual), set(expected)) |