diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index f2d7d14a23cc..f063749b4454 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -82,6 +82,7 @@ from nova.i18n import _ from nova import objects from nova.objects import flavor as flavor_obj from nova.objects import instance as instance_obj +from nova.objects import keypair as keypair_obj from nova.objects import request_spec from nova import quota from nova import rpc @@ -728,6 +729,7 @@ class DbCommands(object): flavor_obj.migrate_flavor_reset_autoincrement, instance_obj.migrate_instance_keypairs, request_spec.migrate_instances_add_request_spec, + keypair_obj.migrate_keypairs_to_api_db, ) def __init__(self): diff --git a/nova/objects/keypair.py b/nova/objects/keypair.py index bb02b05a52af..3cd305ae25a6 100644 --- a/nova/objects/keypair.py +++ b/nova/objects/keypair.py @@ -13,18 +13,22 @@ # under the License. from oslo_db import exception as db_exc +from oslo_log import log as logging from oslo_utils import versionutils from nova import db from nova.db.sqlalchemy import api as db_api from nova.db.sqlalchemy import api_models +from nova.db.sqlalchemy import models as main_models from nova import exception +from nova.i18n import _LE from nova import objects from nova.objects import base from nova.objects import fields KEYPAIR_TYPE_SSH = 'ssh' KEYPAIR_TYPE_X509 = 'x509' +LOG = logging.getLogger(__name__) @db_api.api_context_manager.reader @@ -155,6 +159,9 @@ class KeyPair(base.NovaPersistentObject, base.NovaObject, except exception.KeypairNotFound: pass + self._create() + + def _create(self): updates = self.obj_get_changes() db_keypair = self._create_in_db(self._context, updates) self._from_db_object(self._context, self, db_keypair) @@ -198,3 +205,49 @@ class KeyPairList(base.ObjectListBase, base.NovaObject): def get_count_by_user(cls, context, user_id): return (cls._get_count_from_db(context, user_id) + db.key_pair_count_by_user(context, user_id)) + + +@db_api.main_context_manager.reader +def _count_unmigrated_instances(context): + return context.session.query(main_models.InstanceExtra).\ + filter_by(keypairs=None).\ + filter_by(deleted=0).\ + count() + + +@db_api.main_context_manager.reader +def _get_main_keypairs(context, limit): + return context.session.query(main_models.KeyPair).\ + filter_by(deleted=0).\ + limit(limit).\ + all() + + +def migrate_keypairs_to_api_db(context, count): + bad_instances = _count_unmigrated_instances(context) + if bad_instances: + LOG.error(_LE('Some instances are still missing keypair ' + 'information. Unable to run keypair migration ' + 'at this time.')) + return 0, 0 + + main_keypairs = _get_main_keypairs(context, count) + done = 0 + for db_keypair in main_keypairs: + kp = objects.KeyPair(context=context, + user_id=db_keypair.user_id, + name=db_keypair.name, + fingerprint=db_keypair.fingerprint, + public_key=db_keypair.public_key, + type=db_keypair.type) + try: + kp._create() + except exception.KeyPairExists: + # NOTE(danms): If this got created somehow in the API DB, + # then it's newer and we just continue on to destroy the + # old one in the cell DB. + pass + db_api.key_pair_destroy(context, db_keypair.user_id, db_keypair.name) + done += 1 + + return len(main_keypairs), done diff --git a/nova/tests/functional/db/test_keypair.py b/nova/tests/functional/db/test_keypair.py index a0adc1ac26f0..dd840852fecd 100644 --- a/nova/tests/functional/db/test_keypair.py +++ b/nova/tests/functional/db/test_keypair.py @@ -134,3 +134,46 @@ class KeyPairObjectTestCase(test.NoDBTestCase): count = objects.KeyPairList.get_count_by_user(self.context, self.context.user_id) self.assertEqual(2, count) + + def test_migrate_keypairs(self): + self._api_kp(name='apikey') + self._main_kp(name='mainkey1') + self._main_kp(name='mainkey2') + self._main_kp(name='mainkey3') + total, done = keypair.migrate_keypairs_to_api_db(self.context, 2) + self.assertEqual(2, total) + self.assertEqual(2, done) + + # NOTE(danms): This only fetches from the API DB + api_keys = objects.KeyPairList._get_from_db(self.context, + self.context.user_id) + self.assertEqual(3, len(api_keys)) + + # NOTE(danms): This only fetches from the main DB + main_keys = db_api.key_pair_get_all_by_user(self.context, + self.context.user_id) + self.assertEqual(1, len(main_keys)) + + self.assertEqual((1, 1), + keypair.migrate_keypairs_to_api_db(self.context, 100)) + self.assertEqual((0, 0), + keypair.migrate_keypairs_to_api_db(self.context, 100)) + + def test_migrate_keypairs_bails_on_unmigrated_instances(self): + objects.Instance(context=self.context, user_id=self.context.user_id, + project_id=self.context.project_id).create() + self._api_kp(name='apikey') + self._main_kp(name='mainkey1') + total, done = keypair.migrate_keypairs_to_api_db(self.context, 100) + self.assertEqual(0, total) + self.assertEqual(0, done) + + def test_migrate_keypairs_skips_existing(self): + self._api_kp(name='mykey') + self._main_kp(name='mykey') + total, done = keypair.migrate_keypairs_to_api_db(self.context, 100) + self.assertEqual(1, total) + self.assertEqual(1, done) + total, done = keypair.migrate_keypairs_to_api_db(self.context, 100) + self.assertEqual(0, total) + self.assertEqual(0, done) diff --git a/releasenotes/notes/keypairs-moved-to-api-9cde30acac6f76b6.yaml b/releasenotes/notes/keypairs-moved-to-api-9cde30acac6f76b6.yaml new file mode 100644 index 000000000000..68e8afcf333f --- /dev/null +++ b/releasenotes/notes/keypairs-moved-to-api-9cde30acac6f76b6.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Keypairs have been moved to the API database, using an online + data migration. During the first phase of the migration, instances + will be given local storage of their key, after which keypairs will + be moved to the API database.