summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-04-01 21:55:14 +0000
committerGerrit Code Review <review@openstack.org>2013-04-01 21:55:14 +0000
commitd3dcc22cef5d3e037195abcd97e7473f3dd18b4e (patch)
tree95e32f54e32137575d91a87c60bd6681616c804c
parentc8067d1b700267cea74cba4836232527a612c5d8 (diff)
parent0ae598b1a7a92755fd0fcc2685816414fb6f47b0 (diff)
downloadnova-d3dcc22cef5d3e037195abcd97e7473f3dd18b4e.tar.gz
nova-d3dcc22cef5d3e037195abcd97e7473f3dd18b4e.tar.xz
nova-d3dcc22cef5d3e037195abcd97e7473f3dd18b4e.zip
Merge "Return 409 on creating/importing same name keypair"
-rw-r--r--nova/compute/api.py8
-rw-r--r--nova/db/sqlalchemy/api.py13
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/173_add_unique_constraint_to_key_pairs.py60
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_keypairs.py8
-rw-r--r--nova/tests/compute/test_compute.py3
-rw-r--r--nova/tests/test_db_api.py61
-rw-r--r--nova/tests/test_migrations.py39
7 files changed, 175 insertions, 17 deletions
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 8b75613d2..17851b150 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -2718,13 +2718,6 @@ class KeypairAPI(base.Base):
msg = _('Keypair name must be between 1 and 255 characters long')
raise exception.InvalidKeypair(explanation=msg)
- # NOTE: check for existing keypairs of same name
- try:
- self.db.key_pair_get(context, user_id, key_name)
- raise exception.KeyPairExists(key_name=key_name)
- except exception.NotFound:
- pass
-
def import_key_pair(self, context, user_id, key_name, public_key):
"""Import a key pair using an existing public key."""
self._validate_keypair_name(context, user_id, key_name)
@@ -2767,7 +2760,6 @@ class KeypairAPI(base.Base):
'public_key': public_key,
'private_key': private_key}
self.db.key_pair_create(context, keypair)
-
return keypair
def delete_key_pair(self, context, user_id, key_name):
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index eb34ee301..8d7e5682d 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -1953,10 +1953,13 @@ def instance_info_cache_delete(context, instance_uuid):
@require_context
def key_pair_create(context, values):
- key_pair_ref = models.KeyPair()
- key_pair_ref.update(values)
- key_pair_ref.save()
- return key_pair_ref
+ try:
+ key_pair_ref = models.KeyPair()
+ key_pair_ref.update(values)
+ key_pair_ref.save()
+ return key_pair_ref
+ except db_exc.DBDuplicateEntry:
+ raise exception.KeyPairExists(key_name=values['name'])
@require_context
@@ -1965,7 +1968,7 @@ def key_pair_destroy(context, user_id, name):
result = model_query(context, models.KeyPair).\
filter_by(user_id=user_id).\
filter_by(name=name).\
- delete()
+ soft_delete()
if not result:
raise exception.KeypairNotFound(user_id=user_id, name=name)
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/173_add_unique_constraint_to_key_pairs.py b/nova/db/sqlalchemy/migrate_repo/versions/173_add_unique_constraint_to_key_pairs.py
new file mode 100644
index 000000000..e7c61b36d
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/173_add_unique_constraint_to_key_pairs.py
@@ -0,0 +1,60 @@
+# Copyright 2012 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.changeset import UniqueConstraint
+from sqlalchemy import Index, MetaData, Table
+
+from nova.db.sqlalchemy import utils
+from nova.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+OLD_IDX_NAME = 'key_pair_user_id_name_idx'
+UC_NAME = 'key_pairs_uniq_name_and_user_id'
+TABLE_NAME = 'key_pairs'
+UC_COLUMNS = ['user_id', 'name', 'deleted']
+
+
+def upgrade(migrate_engine):
+ meta = MetaData(bind=migrate_engine)
+ key_pairs = Table(TABLE_NAME, meta, autoload=True)
+ utils.drop_old_duplicate_entries_from_table(migrate_engine,
+ TABLE_NAME, True,
+ *UC_COLUMNS)
+ old_idx = None
+ #Drop old index because the new UniqueConstraint can be used instead.
+ for index in key_pairs.indexes:
+ if index.name == OLD_IDX_NAME:
+ index.drop()
+ old_idx = index
+
+ #index.drop() in SQLAlchemy-migrate will issue a DROP INDEX statement to
+ #the DB but WILL NOT update the table metadata to remove the `Index`
+ #object. This can cause subsequent calls like drop or create constraint
+ #on that table to fail.The solution is to update the table metadata to
+ #reflect the now dropped column.
+ if old_idx:
+ key_pairs.indexes.remove(old_idx)
+ uc = UniqueConstraint(*(UC_COLUMNS), table=key_pairs, name=UC_NAME)
+ uc.create()
+
+
+def downgrade(migrate_engine):
+ utils.drop_unique_constraint(migrate_engine, TABLE_NAME, UC_NAME,
+ *(UC_COLUMNS))
+ meta = MetaData(bind=migrate_engine)
+ key_pairs = Table(TABLE_NAME, meta, autoload=True)
+ Index(OLD_IDX_NAME, key_pairs.c.user_id,
+ key_pairs.c.name).create(migrate_engine)
diff --git a/nova/tests/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/api/openstack/compute/contrib/test_keypairs.py
index 025845637..e338cad69 100644
--- a/nova/tests/api/openstack/compute/contrib/test_keypairs.py
+++ b/nova/tests/api/openstack/compute/contrib/test_keypairs.py
@@ -40,7 +40,7 @@ def db_key_pair_get_all_by_user(self, user_id):
def db_key_pair_create(self, keypair):
- pass
+ return keypair
def db_key_pair_destroy(context, user_id, name):
@@ -48,8 +48,8 @@ def db_key_pair_destroy(context, user_id, name):
raise Exception()
-def db_key_pair_get(context, user_id, name):
- pass
+def db_key_pair_create_duplicate(context, keypair):
+ raise exception.KeyPairExists(key_name=keypair.get('name', ''))
class KeypairsTest(test.TestCase):
@@ -205,7 +205,7 @@ class KeypairsTest(test.TestCase):
self.assertEqual(res.status_int, 413)
def test_keypair_create_duplicate(self):
- self.stubs.Set(db, "key_pair_get", db_key_pair_get)
+ self.stubs.Set(db, "key_pair_create", db_key_pair_create_duplicate)
body = {'keypair': {'name': 'create_duplicate'}}
req = webob.Request.blank('/v2/fake/os-keypairs')
req.method = 'POST'
diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py
index 097c87848..395a075c8 100644
--- a/nova/tests/compute/test_compute.py
+++ b/nova/tests/compute/test_compute.py
@@ -7315,6 +7315,9 @@ class KeypairAPITestCase(BaseTestCase):
self.ctxt, self.ctxt.user_id, '* BAD CHARACTERS! *')
def test_create_keypair_already_exists(self):
+ def db_key_pair_create_duplicate(context, keypair):
+ raise exception.KeyPairExists(key_name=keypair.get('name', ''))
+ self.stubs.Set(db, "key_pair_create", db_key_pair_create_duplicate)
self.assertRaises(exception.KeyPairExists,
self.keypair_api.create_key_pair,
self.ctxt, self.ctxt.user_id,
diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py
index ce4c8fe7f..23b8ebcb0 100644
--- a/nova/tests/test_db_api.py
+++ b/nova/tests/test_db_api.py
@@ -1066,6 +1066,67 @@ class DbApiTestCase(DbTestCase):
_compare(bw_usages[2], expected_bw_usages[2])
timeutils.clear_time_override()
+ def test_key_pair_create(self):
+ ctxt = context.get_admin_context()
+ values = {'name': 'test_keypair', 'public_key': 'test-public-key',
+ 'user_id': 'test_user_id', 'fingerprint': 'test_fingerprint'}
+ keypair = db.key_pair_create(ctxt, values)
+ self.assertNotEqual(None, keypair)
+ for name, value in values.iteritems():
+ self.assertEqual(keypair.get(name), value)
+
+ def test_key_pair_create_with_duplicate_name(self):
+ ctxt = context.get_admin_context()
+ values = {'name': 'test_keypair', 'public_key': 'test-public-key',
+ 'user_id': 'test_user_id', 'fingerprint': 'test_fingerprint'}
+ keypair = db.key_pair_create(ctxt, values)
+ self.assertRaises(exception.KeyPairExists,
+ db.key_pair_create, ctxt, values)
+
+ def test_admin_get_deleted_keypair(self):
+ # Test deleted keypair can be read by admin user.
+ ctxt = context.get_admin_context()
+ values = {'name': 'test_keypair', 'public_key': 'test-public-key',
+ 'user_id': 'test_user_id', 'fingerprint': 'test_fingerprint'}
+ keypair = db.key_pair_create(ctxt, values)
+ db.key_pair_destroy(ctxt, keypair['user_id'], keypair['name'])
+
+ # Raise exception when read_deleted is 'no'.
+ self.assertRaises(exception.KeypairNotFound, db.key_pair_get, ctxt,
+ keypair['user_id'], keypair['name'])
+ ctxt = ctxt.elevated(read_deleted='yes')
+ db_keypair = db.key_pair_get(ctxt, keypair['user_id'],
+ keypair['name'])
+ self.assertEqual(db_keypair['name'], keypair['name'])
+ self.assertEqual(db_keypair['deleted'], keypair['id'])
+
+ def test_admin_get_all_keypairs_including_deleted(self):
+ # Test all deleted/non-deleted keypairs can be read by admin user.
+ ctxt = context.get_admin_context()
+ keypair1_values = {'name': 'test_keypair1',
+ 'public_key': 'test-public-key1',
+ 'user_id': 'test_user_id',
+ 'fingerprint': 'test_fingerprint1'}
+ keypair2_values = {'name': 'test_keypair2',
+ 'public_key': 'test-public-key2',
+ 'user_id': 'test_user_id',
+ 'fingerprint': 'test_fingerprint2'}
+ keypair1 = db.key_pair_create(ctxt, keypair1_values)
+ keypair2 = db.key_pair_create(ctxt, keypair2_values)
+ db.key_pair_destroy(ctxt, keypair1['user_id'], keypair1['name'])
+ db.key_pair_destroy(ctxt, keypair2['user_id'], keypair2['name'])
+ # Returns non-deleted keypairs.
+ result = db.key_pair_get_all_by_user(ctxt, keypair1['user_id'])
+ self.assertEqual(result, [])
+ ctxt = ctxt.elevated(read_deleted='yes')
+ # Returns deleted and non-deleted keypairs.
+ db_keypairs = db.key_pair_get_all_by_user(ctxt, keypair1['user_id'])
+ expected_deleted_ids = [keypair1['id'], keypair2['id']]
+ expected_keypair_names = [keypair1['name'], keypair2['name']]
+ for keypair in db_keypairs:
+ self.assertTrue(keypair['name'] in expected_keypair_names)
+ self.assertTrue(keypair['deleted'] in expected_deleted_ids)
+
def _get_fake_aggr_values():
return {'name': 'fake_aggregate'}
diff --git a/nova/tests/test_migrations.py b/nova/tests/test_migrations.py
index 98c50aaa7..c78bb6d24 100644
--- a/nova/tests/test_migrations.py
+++ b/nova/tests/test_migrations.py
@@ -1151,6 +1151,45 @@ class TestNovaMigrations(BaseMigrationTestCase, CommonTestsMixIn):
fetchall()
self.assertEqual(1, len(uc_name2_rows))
+ # migration 173, add unique constraint to keypairs
+ def _pre_upgrade_173(self, engine):
+ created_at = [datetime.datetime.now() for x in range(0, 7)]
+ fake_keypairs = [dict(name='key1', user_id='1a',
+ created_at=created_at[0],
+ deleted=0),
+ dict(name='key1', user_id='1a',
+ created_at=created_at[1],
+ deleted=0),
+ dict(name='key1', user_id='1a',
+ created_at=created_at[2],
+ deleted=0)
+ ]
+ keypairs = get_table(engine, 'key_pairs')
+ engine.execute(keypairs.insert(), fake_keypairs)
+ return fake_keypairs
+
+ def _check_173(self, engine, data):
+ keypairs = get_table(engine, 'key_pairs')
+ # Unique constraints are not listed in table.constraints for any db.
+ # So, simply add a duplicate keypair to check if unique constraint
+ # is applied to the key_pairs table or not.
+ insert = keypairs.insert()
+ duplicate_keypair = dict(name='key4', user_id='4a',
+ created_at=datetime.datetime.now(),
+ deleted=0)
+ insert.execute(duplicate_keypair)
+ # Insert again
+ self.assertRaises(sqlalchemy.exc.IntegrityError, insert.execute,
+ duplicate_keypair)
+
+ # Get all unique records
+ rows = keypairs.select().\
+ where(keypairs.c.deleted != keypairs.c.id).\
+ execute().\
+ fetchall()
+ # Ensure the number of unique keypairs is correct
+ self.assertEqual(len(rows), 2)
+
class TestBaremetalMigrations(BaseMigrationTestCase, CommonTestsMixIn):
"""Test sqlalchemy-migrate migrations."""