summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Prince <dprince@redhat.com>2012-04-22 19:34:38 -0400
committerDan Prince <dprince@redhat.com>2012-05-03 14:12:16 -0400
commit6a53bf90d0c1ea547a1c920c45e0eeef7ddfba2e (patch)
tree60726cbcc244ee2d9ed9072aeab1ef1ff2b81efa
parent8fc533699d8ae02da951e0443853159bed80199f (diff)
downloadnova-6a53bf90d0c1ea547a1c920c45e0eeef7ddfba2e.tar.gz
nova-6a53bf90d0c1ea547a1c920c45e0eeef7ddfba2e.tar.xz
nova-6a53bf90d0c1ea547a1c920c45e0eeef7ddfba2e.zip
Implement key pair quotas.
Fixes LP Bug #987058. Change-Id: Ibefcdc448cb60754d5358fd08d74f7d279c8b16e
-rw-r--r--nova/api/ec2/cloud.py10
-rw-r--r--nova/api/openstack/compute/contrib/keypairs.py6
-rw-r--r--nova/db/api.py5
-rw-r--r--nova/db/sqlalchemy/api.py7
-rw-r--r--nova/quota.py20
-rw-r--r--nova/tests/api/ec2/test_cloud.py29
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_keypairs.py46
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_quota_classes.py10
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_quotas.py15
-rw-r--r--nova/tests/test_quota.py22
10 files changed, 165 insertions, 5 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 8c6a1fdc3..28ed0279f 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -73,6 +73,11 @@ def _gen_key(context, user_id, key_name):
raise exception.KeyPairExists(key_name=key_name)
except exception.NotFound:
pass
+
+ if quota.allowed_key_pairs(context, 1) < 1:
+ msg = _("Quota exceeded, too many key pairs.")
+ raise exception.EC2APIError(msg)
+
private_key, public_key, fingerprint = crypto.generate_key_pair()
key = {}
key['user_id'] = user_id
@@ -395,6 +400,11 @@ class CloudController(object):
raise exception.KeyPairExists(key_name=key_name)
except exception.NotFound:
pass
+
+ if quota.allowed_key_pairs(context, 1) < 1:
+ msg = _("Quota exceeded, too many key pairs.")
+ raise exception.EC2APIError(msg)
+
public_key = base64.b64decode(public_key_material)
fingerprint = crypto.generate_fingerprint(public_key)
key = {}
diff --git a/nova/api/openstack/compute/contrib/keypairs.py b/nova/api/openstack/compute/contrib/keypairs.py
index 7dbbf14f3..5a764978c 100644
--- a/nova/api/openstack/compute/contrib/keypairs.py
+++ b/nova/api/openstack/compute/contrib/keypairs.py
@@ -28,6 +28,7 @@ from nova.api.openstack import extensions
from nova import crypto
from nova import db
from nova import exception
+from nova import quota
authorize = extensions.extension_authorizer('compute', 'keypairs')
@@ -105,6 +106,11 @@ class KeypairController(object):
keypair = {'user_id': context.user_id,
'name': name}
+ if quota.allowed_key_pairs(context, 1) < 1:
+ msg = _("Quota exceeded, too many key pairs.")
+ raise webob.exc.HTTPRequestEntityTooLarge(
+ explanation=msg,
+ headers={'Retry-After': 0})
# import if public_key is sent
if 'public_key' in params:
try:
diff --git a/nova/db/api.py b/nova/db/api.py
index 5de921667..1d4103c2d 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -703,6 +703,11 @@ def key_pair_get_all_by_user(context, user_id):
return IMPL.key_pair_get_all_by_user(context, user_id)
+def key_pair_count_by_user(context, user_id):
+ """Count number of key pairs for the given user ID."""
+ return IMPL.key_pair_count_by_user(context, user_id)
+
+
####################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 1d7509aef..105bce004 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -1816,6 +1816,13 @@ def key_pair_get_all_by_user(context, user_id):
all()
+def key_pair_count_by_user(context, user_id):
+ authorize_user_context(context, user_id)
+ return model_query(context, models.KeyPair, read_deleted="no").\
+ filter_by(user_id=user_id).\
+ count()
+
+
###################
diff --git a/nova/quota.py b/nova/quota.py
index 9f43f722c..2fdecd1e2 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -60,6 +60,9 @@ quota_opts = [
cfg.IntOpt('quota_security_group_rules',
default=20,
help='number of security rules per security group'),
+ cfg.IntOpt('quota_key_pairs',
+ default=100,
+ help='number of key pairs per user'),
]
FLAGS = flags.FLAGS
@@ -68,7 +71,8 @@ FLAGS.register_opts(quota_opts)
quota_resources = ['metadata_items', 'injected_file_content_bytes',
'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances',
- 'injected_files', 'cores', 'security_groups', 'security_group_rules']
+ 'injected_files', 'cores', 'security_groups', 'security_group_rules',
+ 'key_pairs']
def _get_default_quotas():
@@ -85,6 +89,7 @@ def _get_default_quotas():
FLAGS.quota_injected_file_content_bytes,
'security_groups': FLAGS.quota_security_groups,
'security_group_rules': FLAGS.quota_security_group_rules,
+ 'key_pairs': FLAGS.quota_key_pairs,
}
# -1 in the quota flags means unlimited
return defaults
@@ -204,6 +209,19 @@ def allowed_security_group_rules(context, security_group_id,
return min(requested_rules, allowed_rules)
+def allowed_key_pairs(context, requested_key_pairs):
+ """Check quota and return min(requested, allowed) key pairs."""
+ user_id = context.user_id
+ project_id = context.project_id
+ context = context.elevated()
+ used_key_pairs = db.key_pair_count_by_user(context, user_id)
+ quota = get_project_quotas(context, project_id)
+ allowed_key_pairs = _get_request_allotment(requested_key_pairs,
+ used_key_pairs,
+ quota['key_pairs'])
+ return min(requested_key_pairs, allowed_key_pairs)
+
+
def _calculate_simple_quota(context, resource, requested):
"""Check quota for resource; return min(requested, allowed)."""
quota = get_project_quotas(context, context.project_id)
diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py
index 7b6fb34c2..bc7cb02e4 100644
--- a/nova/tests/api/ec2/test_cloud.py
+++ b/nova/tests/api/ec2/test_cloud.py
@@ -1540,6 +1540,21 @@ class CloudTestCase(test.TestCase):
self.assertEqual(dummypub, keydata['public_key'])
self.assertEqual(dummyfprint, keydata['fingerprint'])
+ def test_import_key_pair_quota_limit(self):
+ self.flags(quota_key_pairs=0)
+ pubkey_path = os.path.join(os.path.dirname(__file__), 'public_key')
+ f = open(pubkey_path + '/dummy.pub', 'r')
+ dummypub = f.readline().rstrip()
+ f.close
+ f = open(pubkey_path + '/dummy.fingerprint', 'r')
+ dummyfprint = f.readline().rstrip()
+ f.close
+ key_name = 'testimportkey'
+ public_key_material = base64.b64encode(dummypub)
+ self.assertRaises(exception.EC2APIError,
+ self.cloud.import_key_pair, self.context, key_name,
+ public_key_material)
+
def test_create_key_pair(self):
good_names = ('a', 'a' * 255, string.ascii_letters + ' -_')
bad_names = ('', 'a' * 256, '*', '/')
@@ -1555,6 +1570,20 @@ class CloudTestCase(test.TestCase):
self.context,
key_name)
+ def test_create_key_pair_quota_limit(self):
+ self.flags(quota_key_pairs=10)
+ for i in range(0, 10):
+ key_name = 'key_%i' % i
+ result = self.cloud.create_key_pair(self.context,
+ key_name)
+ self.assertEqual(result['keyName'], key_name)
+
+ # 11'th group should fail
+ self.assertRaises(exception.EC2APIError,
+ self.cloud.create_key_pair,
+ self.context,
+ 'foo')
+
def test_delete_key_pair(self):
self._create_key('test')
self.cloud.delete_key_pair(self.context, 'test')
diff --git a/nova/tests/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/api/openstack/compute/contrib/test_keypairs.py
index fa962dd15..fd6fcbdb6 100644
--- a/nova/tests/api/openstack/compute/contrib/test_keypairs.py
+++ b/nova/tests/api/openstack/compute/contrib/test_keypairs.py
@@ -118,6 +118,22 @@ class KeypairsTest(test.TestCase):
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 400)
+ def test_keypair_create_quota_limit(self):
+
+ def db_key_pair_count_by_user_max(self, user_id):
+ return 100
+
+ self.stubs.Set(db, "key_pair_count_by_user",
+ db_key_pair_count_by_user_max)
+
+ req = webob.Request.blank('/v2/fake/os-keypairs')
+ req.method = 'POST'
+ req.headers['Content-Type'] = 'application/json'
+ body = {'keypair': {'name': 'foo'}}
+ req.body = json.dumps(body)
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 413)
+
def test_keypair_import(self):
body = {
'keypair': {
@@ -145,6 +161,36 @@ class KeypairsTest(test.TestCase):
self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
self.assertFalse('private_key' in res_dict['keypair'])
+ def test_keypair_import_quota_limit(self):
+
+ def db_key_pair_count_by_user_max(self, user_id):
+ return 100
+
+ self.stubs.Set(db, "key_pair_count_by_user",
+ db_key_pair_count_by_user_max)
+
+ body = {
+ 'keypair': {
+ 'name': 'create_test',
+ 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA'
+ 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4'
+ 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y'
+ 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi'
+ 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj'
+ 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1'
+ 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc'
+ 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB'
+ 'bHkXa6OciiJDvkRzJXzf',
+ },
+ }
+
+ req = webob.Request.blank('/v2/fake/os-keypairs')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers['Content-Type'] = 'application/json'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 413)
+
def test_keypair_create_duplicate(self):
self.stubs.Set(db, "key_pair_get", db_key_pair_get)
body = {'keypair': {'name': 'create_duplicate'}}
diff --git a/nova/tests/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py
index d922152e0..e67a8d76d 100644
--- a/nova/tests/api/openstack/compute/contrib/test_quota_classes.py
+++ b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py
@@ -27,7 +27,8 @@ def quota_set(class_name):
'volumes': 10, 'gigabytes': 1000, 'ram': 51200,
'floating_ips': 10, 'instances': 10, 'injected_files': 5,
'cores': 20, 'injected_file_content_bytes': 10240,
- 'security_groups': 10, 'security_group_rules': 20}}
+ 'security_groups': 10, 'security_group_rules': 20,
+ 'key_pairs': 100}}
class QuotaClassSetsTest(test.TestCase):
@@ -49,6 +50,7 @@ class QuotaClassSetsTest(test.TestCase):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
+ 'key_pairs': 100,
}
quota_set = self.controller._format_quota_set('test_class',
@@ -67,6 +69,7 @@ class QuotaClassSetsTest(test.TestCase):
self.assertEqual(qs['injected_file_content_bytes'], 10240)
self.assertEqual(qs['security_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20)
+ self.assertEqual(qs['key_pairs'], 100)
def test_quotas_show_as_admin(self):
req = fakes.HTTPRequest.blank(
@@ -90,6 +93,7 @@ class QuotaClassSetsTest(test.TestCase):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
+ 'key_pairs': 100,
}}
req = fakes.HTTPRequest.blank(
@@ -107,6 +111,7 @@ class QuotaClassSetsTest(test.TestCase):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
+ 'key_pairs': 100,
}}
req = fakes.HTTPRequest.blank(
@@ -134,6 +139,7 @@ class QuotaTemplateXMLSerializerTest(test.TestCase):
injected_files=80,
security_groups=10,
security_group_rules=20,
+ key_pairs=100,
cores=90))
text = self.serializer.serialize(exemplar)
@@ -160,6 +166,7 @@ class QuotaTemplateXMLSerializerTest(test.TestCase):
injected_files='80',
security_groups='10',
security_group_rules='20',
+ key_pairs='100',
cores='90'))
intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
'<quota_class_set>'
@@ -175,6 +182,7 @@ class QuotaTemplateXMLSerializerTest(test.TestCase):
'<cores>90</cores>'
'<security_groups>10</security_groups>'
'<security_group_rules>20</security_group_rules>'
+ '<key_pairs>100</key_pairs>'
'</quota_class_set>')
result = self.deserializer.deserialize(intext)['body']
diff --git a/nova/tests/api/openstack/compute/contrib/test_quotas.py b/nova/tests/api/openstack/compute/contrib/test_quotas.py
index b603ae684..2bda31595 100644
--- a/nova/tests/api/openstack/compute/contrib/test_quotas.py
+++ b/nova/tests/api/openstack/compute/contrib/test_quotas.py
@@ -29,7 +29,8 @@ def quota_set(id):
'gigabytes': 1000, 'ram': 51200, 'floating_ips': 10,
'instances': 10, 'injected_files': 5, 'cores': 20,
'injected_file_content_bytes': 10240,
- 'security_groups': 10, 'security_group_rules': 20}}
+ 'security_groups': 10, 'security_group_rules': 20,
+ 'key_pairs': 100}}
class QuotaSetsTest(test.TestCase):
@@ -51,6 +52,7 @@ class QuotaSetsTest(test.TestCase):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
+ 'key_pairs': 100,
}
quota_set = self.controller._format_quota_set('1234', raw_quota_set)
@@ -68,6 +70,7 @@ class QuotaSetsTest(test.TestCase):
self.assertEqual(qs['injected_file_content_bytes'], 10240)
self.assertEqual(qs['security_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20)
+ self.assertEqual(qs['key_pairs'], 100)
def test_quotas_defaults(self):
uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults'
@@ -88,6 +91,7 @@ class QuotaSetsTest(test.TestCase):
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
+ 'key_pairs': 100,
}}
self.assertEqual(res_dict, expected)
@@ -111,7 +115,8 @@ class QuotaSetsTest(test.TestCase):
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240,
'security_groups': 10,
- 'security_group_rules': 20}}
+ 'security_group_rules': 20,
+ 'key_pairs': 100}}
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
@@ -126,7 +131,8 @@ class QuotaSetsTest(test.TestCase):
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240,
'security_groups': 10,
- 'security_group_rules': 20}}
+ 'security_group_rules': 20,
+ 'key_pairs': 100}}
req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
@@ -164,6 +170,7 @@ class QuotaXMLSerializerTest(test.TestCase):
injected_files=80,
security_groups=10,
security_group_rules=20,
+ key_pairs=100,
cores=90))
text = self.serializer.serialize(exemplar)
@@ -189,6 +196,7 @@ class QuotaXMLSerializerTest(test.TestCase):
injected_files='80',
security_groups='10',
security_group_rules='20',
+ key_pairs='100',
cores='90'))
intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
'<quota_set>'
@@ -203,6 +211,7 @@ class QuotaXMLSerializerTest(test.TestCase):
'<injected_files>80</injected_files>'
'<security_groups>10</security_groups>'
'<security_group_rules>20</security_group_rules>'
+ '<key_pairs>100</key_pairs>'
'<cores>90</cores>'
'</quota_set>')
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 06aaa26db..65ee7471a 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -43,6 +43,7 @@ class GetQuotaTestCase(test.TestCase):
quota_floating_ips=10,
quota_security_groups=10,
quota_security_group_rules=20,
+ quota_key_pairs=10,
quota_metadata_items=128,
quota_injected_files=5,
quota_injected_file_content_bytes=10 * 1024)
@@ -61,6 +62,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=5,
quota_security_groups=10,
quota_security_group_rules=20,
+ quota_key_pairs=10,
metadata_items=64,
injected_files=2,
injected_file_content_bytes=5 * 1024,
@@ -84,6 +86,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=2,
security_groups=5,
security_group_rules=10,
+ key_pairs=5,
metadata_items=32,
injected_files=1,
injected_file_content_bytes=2 * 1024,
@@ -105,6 +108,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=10,
security_groups=10,
security_group_rules=20,
+ key_pairs=10,
metadata_items=128,
injected_files=5,
injected_file_content_bytes=10 * 1024,
@@ -119,6 +123,7 @@ class GetQuotaTestCase(test.TestCase):
quota_floating_ips=-1,
quota_security_groups=-1,
quota_security_group_rules=-1,
+ quota_key_pairs=-1,
quota_metadata_items=-1,
quota_injected_files=-1,
quota_injected_file_content_bytes=-1)
@@ -132,6 +137,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=-1,
security_groups=-1,
security_group_rules=-1,
+ key_pairs=-1,
metadata_items=-1,
injected_files=-1,
injected_file_content_bytes=-1,
@@ -149,6 +155,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=10,
security_groups=10,
security_group_rules=20,
+ key_pairs=10,
metadata_items=128,
injected_files=5,
injected_file_content_bytes=10 * 1024,
@@ -166,6 +173,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=5,
security_groups=10,
security_group_rules=20,
+ key_pairs=10,
metadata_items=64,
injected_files=2,
injected_file_content_bytes=5 * 1024,
@@ -184,6 +192,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=10,
security_groups=10,
security_group_rules=20,
+ key_pairs=10,
metadata_items=128,
injected_files=5,
injected_file_content_bytes=10 * 1024,
@@ -202,6 +211,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=2,
security_groups=5,
security_group_rules=10,
+ key_pairs=5,
metadata_items=32,
injected_files=1,
injected_file_content_bytes=2 * 1024,
@@ -221,6 +231,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=5,
security_groups=10,
security_group_rules=20,
+ key_pairs=10,
metadata_items=64,
injected_files=2,
injected_file_content_bytes=5 * 1024,
@@ -240,6 +251,7 @@ class GetQuotaTestCase(test.TestCase):
floating_ips=2,
security_groups=5,
security_group_rules=10,
+ key_pairs=5,
metadata_items=32,
injected_files=1,
injected_file_content_bytes=2 * 1024,
@@ -438,6 +450,16 @@ class QuotaTestCase(test.TestCase):
security_groups = quota.allowed_security_groups(self.context, 101)
self.assertEqual(security_groups, 101)
+ def test_unlimited_key_pairs(self):
+ self.flags(quota_key_pairs=10)
+ key_pairs = quota.allowed_key_pairs(self.context, 100)
+ self.assertEqual(key_pairs, 10)
+ db.quota_create(self.context, self.project_id, 'key_pairs', -1)
+ key_pairs = quota.allowed_key_pairs(self.context, 100)
+ self.assertEqual(key_pairs, 100)
+ key_pairs = quota.allowed_key_pairs(self.context, 101)
+ self.assertEqual(key_pairs, 101)
+
def test_unlimited_security_group_rules(self):
def fake_security_group_rule_count_by_group(context, sec_group_id):