Skip to content
Permalink
Browse files Browse the repository at this point in the history
Implement security group quotas.
Fixes LP Bug #969545

Change-Id: I60d4300aa04597e2d8b0eea31ab0303b0b3c48f9
  • Loading branch information
dprince committed Apr 19, 2012
1 parent ab7e590 commit 1f644d2
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 10 deletions.
12 changes: 12 additions & 0 deletions nova/api/ec2/cloud.py
Expand Up @@ -41,6 +41,7 @@
from nova.image import s3
from nova import log as logging
from nova import network
from nova import quota
from nova import utils
from nova import volume

Expand Down Expand Up @@ -725,6 +726,13 @@ def authorize_security_group_ingress(self, context, group_name=None,
raise exception.EC2APIError(err % values_for_rule)
postvalues.append(values_for_rule)

allowed = quota.allowed_security_group_rules(context,
security_group['id'],
1)
if allowed < 1:
msg = _("Quota exceeded, too many security group rules.")
raise exception.EC2APIError(msg)

rule_ids = []
for values_for_rule in postvalues:
security_group_rule = db.security_group_rule_create(
Expand Down Expand Up @@ -782,6 +790,10 @@ def create_security_group(self, context, group_name, group_description):
msg = _('group %s already exists')
raise exception.EC2APIError(msg % group_name)

if quota.allowed_security_groups(context, 1) < 1:
msg = _("Quota exceeded, too many security groups.")
raise exception.EC2APIError(msg)

group = {'user_id': context.user_id,
'project_id': context.project_id,
'name': group_name,
Expand Down
12 changes: 12 additions & 0 deletions nova/api/openstack/compute/contrib/security_groups.py
Expand Up @@ -31,6 +31,7 @@
from nova import exception
from nova import flags
from nova import log as logging
from nova import quota
from nova import utils


Expand Down Expand Up @@ -289,6 +290,10 @@ def create(self, req, body):
group_name = group_name.strip()
group_description = group_description.strip()

if quota.allowed_security_groups(context, 1) < 1:
msg = _("Quota exceeded, too many security groups.")
raise exc.HTTPBadRequest(explanation=msg)

LOG.audit(_("Create Security Group %s"), group_name, context=context)
self.compute_api.ensure_default_security_group(context)
if db.security_group_exists(context, context.project_id, group_name):
Expand Down Expand Up @@ -376,6 +381,13 @@ def create(self, req, body):
msg = _('This rule already exists in group %s') % parent_group_id
raise exc.HTTPBadRequest(explanation=msg)

allowed = quota.allowed_security_group_rules(context,
parent_group_id,
1)
if allowed < 1:
msg = _("Quota exceeded, too many security group rules.")
raise exc.HTTPBadRequest(explanation=msg)

security_group_rule = db.security_group_rule_create(context, values)
self.sgh.trigger_security_group_rule_create_refresh(
context, [security_group_rule['id']])
Expand Down
10 changes: 10 additions & 0 deletions nova/db/api.py
Expand Up @@ -1151,6 +1151,11 @@ def security_group_destroy(context, security_group_id):
return IMPL.security_group_destroy(context, security_group_id)


def security_group_count_by_project(context, project_id):
"""Count number of security groups in a project."""
return IMPL.security_group_count_by_project(context, project_id)


####################


Expand Down Expand Up @@ -1182,6 +1187,11 @@ def security_group_rule_get(context, security_group_rule_id):
return IMPL.security_group_rule_get(context, security_group_rule_id)


def security_group_rule_count_by_group(context, security_group_id):
"""Count rules in a given security group."""
return IMPL.security_group_rule_count_by_group(context, security_group_id)


###################


Expand Down
15 changes: 15 additions & 0 deletions nova/db/sqlalchemy/api.py
Expand Up @@ -2903,6 +2903,13 @@ def security_group_destroy(context, security_group_id):
'updated_at': literal_column('updated_at')})


@require_context
def security_group_count_by_project(context, project_id):
authorize_project_context(context, project_id)
return model_query(context, models.SecurityGroup, read_deleted="no").\
filter_by(project_id=project_id).\
count()

###################


Expand Down Expand Up @@ -2961,6 +2968,14 @@ def security_group_rule_destroy(context, security_group_rule_id):
security_group_rule.delete(session=session)


@require_context
def security_group_rule_count_by_group(context, security_group_id):
return model_query(context, models.SecurityGroupIngressRule,
read_deleted="no").\
filter_by(parent_group_id=security_group_id).\
count()

#
###################


Expand Down
36 changes: 35 additions & 1 deletion nova/quota.py
Expand Up @@ -54,6 +54,12 @@
cfg.IntOpt('quota_injected_file_path_bytes',
default=255,
help='number of bytes allowed per injected file path'),
cfg.IntOpt('quota_security_groups',
default=10,
help='number of security groups per project'),
cfg.IntOpt('quota_security_group_rules',
default=20,
help='number of security rules per security group'),
]

FLAGS = flags.FLAGS
Expand All @@ -62,7 +68,7 @@

quota_resources = ['metadata_items', 'injected_file_content_bytes',
'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances',
'injected_files', 'cores']
'injected_files', 'cores', 'security_groups', 'security_group_rules']


def _get_default_quotas():
Expand All @@ -77,6 +83,8 @@ def _get_default_quotas():
'injected_files': FLAGS.quota_injected_files,
'injected_file_content_bytes':
FLAGS.quota_injected_file_content_bytes,
'security_groups': FLAGS.quota_security_groups,
'security_group_rules': FLAGS.quota_security_group_rules,
}
# -1 in the quota flags means unlimited
return defaults
Expand Down Expand Up @@ -170,6 +178,32 @@ def allowed_floating_ips(context, requested_floating_ips):
return min(requested_floating_ips, allowed_floating_ips)


def allowed_security_groups(context, requested_security_groups):
"""Check quota and return min(requested, allowed) security groups."""
project_id = context.project_id
context = context.elevated()
used_sec_groups = db.security_group_count_by_project(context, project_id)
quota = get_project_quotas(context, project_id)
allowed_sec_groups = _get_request_allotment(requested_security_groups,
used_sec_groups,
quota['security_groups'])
return min(requested_security_groups, allowed_sec_groups)


def allowed_security_group_rules(context, security_group_id,
requested_rules):
"""Check quota and return min(requested, allowed) sec group rules."""
project_id = context.project_id
context = context.elevated()
used_rules = db.security_group_rule_count_by_group(context,
security_group_id)
quota = get_project_quotas(context, project_id)
allowed_rules = _get_request_allotment(requested_rules,
used_rules,
quota['security_group_rules'])
return min(requested_rules, allowed_rules)


def _calculate_simple_quota(context, resource, requested):
"""Check quota for resource; return min(requested, allowed)."""
quota = get_project_quotas(context, context.project_id)
Expand Down
25 changes: 25 additions & 0 deletions nova/tests/api/ec2/test_cloud.py
Expand Up @@ -271,6 +271,18 @@ def test_create_delete_security_group(self):
delete = self.cloud.delete_security_group
self.assertTrue(delete(self.context, 'testgrp'))

def test_security_group_quota_limit(self):
self.flags(quota_security_groups=10)
for i in range(1, 10):
name = 'test name %i' % i
descript = 'test description %i' % i
create = self.cloud.create_security_group
result = create(self.context, name, descript)

# 11'th group should fail
self.assertRaises(exception.EC2APIError,
create, self.context, 'foo', 'bar')

def test_delete_security_group_by_id(self):
sec = db.security_group_create(self.context,
{'project_id': self.context.project_id,
Expand Down Expand Up @@ -436,6 +448,19 @@ def test_authorize_security_group_ingress_already_exists(self):
self.assertRaises(exception.EC2APIError, authz, self.context,
group_name=sec['name'], **kwargs)

def test_security_group_ingress_quota_limit(self):
self.flags(quota_security_group_rules=20)
kwargs = {'project_id': self.context.project_id, 'name': 'test'}
sec_group = db.security_group_create(self.context, kwargs)
authz = self.cloud.authorize_security_group_ingress
for i in range(100, 120):
kwargs = {'to_port': i, 'from_port': i, 'ip_protocol': 'tcp'}
authz(self.context, group_id=sec_group['id'], **kwargs)

kwargs = {'to_port': 121, 'from_port': 121, 'ip_protocol': 'tcp'}
self.assertRaises(exception.EC2APIError, authz, self.context,
group_id=sec_group['id'], **kwargs)

def _test_authorize_security_group_no_ports_with_source_group(self, proto):
kwargs = {'project_id': self.context.project_id, 'name': 'test'}
sec = db.security_group_create(self.context, kwargs)
Expand Down
26 changes: 22 additions & 4 deletions nova/tests/api/openstack/compute/contrib/test_quota_classes.py
Expand Up @@ -26,7 +26,8 @@ def quota_set(class_name):
return {'quota_class_set': {'id': class_name, 'metadata_items': 128,
'volumes': 10, 'gigabytes': 1000, 'ram': 51200,
'floating_ips': 10, 'instances': 10, 'injected_files': 5,
'cores': 20, 'injected_file_content_bytes': 10240}}
'cores': 20, 'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_group_rules': 20}}


class QuotaClassSetsTest(test.TestCase):
Expand All @@ -45,7 +46,10 @@ def test_format_quota_set(self):
'metadata_items': 128,
'gigabytes': 1000,
'injected_files': 5,
'injected_file_content_bytes': 10240}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
}

quota_set = self.controller._format_quota_set('test_class',
raw_quota_set)
Expand All @@ -61,6 +65,8 @@ def test_format_quota_set(self):
self.assertEqual(qs['metadata_items'], 128)
self.assertEqual(qs['injected_files'], 5)
self.assertEqual(qs['injected_file_content_bytes'], 10240)
self.assertEqual(qs['security_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20)

def test_quotas_show_as_admin(self):
req = fakes.HTTPRequest.blank(
Expand All @@ -81,7 +87,10 @@ def test_quotas_update_as_admin(self):
'ram': 51200, 'volumes': 10,
'gigabytes': 1000, 'floating_ips': 10,
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
}}

req = fakes.HTTPRequest.blank(
'/v2/fake4/os-quota-class-sets/test_class',
Expand All @@ -95,7 +104,10 @@ def test_quotas_update_as_user(self):
'ram': 51200, 'volumes': 10,
'gigabytes': 1000, 'floating_ips': 10,
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
}}

req = fakes.HTTPRequest.blank(
'/v2/fake4/os-quota-class-sets/test_class')
Expand All @@ -120,6 +132,8 @@ def test_serializer(self):
floating_ips=60,
instances=70,
injected_files=80,
security_groups=10,
security_group_rules=20,
cores=90))
text = self.serializer.serialize(exemplar)

Expand All @@ -144,6 +158,8 @@ def test_deserializer(self):
floating_ips='60',
instances='70',
injected_files='80',
security_groups='10',
security_group_rules='20',
cores='90'))
intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
'<quota_class_set>'
Expand All @@ -157,6 +173,8 @@ def test_deserializer(self):
'<instances>70</instances>'
'<injected_files>80</injected_files>'
'<cores>90</cores>'
'<security_groups>10</security_groups>'
'<security_group_rules>20</security_group_rules>'
'</quota_class_set>')

result = self.deserializer.deserialize(intext)['body']
Expand Down
29 changes: 24 additions & 5 deletions nova/tests/api/openstack/compute/contrib/test_quotas.py
Expand Up @@ -28,7 +28,8 @@ def quota_set(id):
return {'quota_set': {'id': id, 'metadata_items': 128, 'volumes': 10,
'gigabytes': 1000, 'ram': 51200, 'floating_ips': 10,
'instances': 10, 'injected_files': 5, 'cores': 20,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10, 'security_group_rules': 20}}


class QuotaSetsTest(test.TestCase):
Expand All @@ -47,7 +48,10 @@ def test_format_quota_set(self):
'metadata_items': 128,
'gigabytes': 1000,
'injected_files': 5,
'injected_file_content_bytes': 10240}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
}

quota_set = self.controller._format_quota_set('1234', raw_quota_set)
qs = quota_set['quota_set']
Expand All @@ -62,6 +66,8 @@ def test_format_quota_set(self):
self.assertEqual(qs['metadata_items'], 128)
self.assertEqual(qs['injected_files'], 5)
self.assertEqual(qs['injected_file_content_bytes'], 10240)
self.assertEqual(qs['security_groups'], 10)
self.assertEqual(qs['security_group_rules'], 20)

def test_quotas_defaults(self):
uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults'
Expand All @@ -79,7 +85,10 @@ def test_quotas_defaults(self):
'floating_ips': 10,
'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20,
}}

self.assertEqual(res_dict, expected)

Expand All @@ -100,7 +109,9 @@ def test_quotas_update_as_admin(self):
'ram': 51200, 'volumes': 10,
'gigabytes': 1000, 'floating_ips': 10,
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20}}

req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me',
use_admin_context=True)
Expand All @@ -113,7 +124,9 @@ def test_quotas_update_as_user(self):
'ram': 51200, 'volumes': 10,
'gigabytes': 1000, 'floating_ips': 10,
'metadata_items': 128, 'injected_files': 5,
'injected_file_content_bytes': 10240}}
'injected_file_content_bytes': 10240,
'security_groups': 10,
'security_group_rules': 20}}

req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me')
self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
Expand Down Expand Up @@ -149,6 +162,8 @@ def test_serializer(self):
floating_ips=60,
instances=70,
injected_files=80,
security_groups=10,
security_group_rules=20,
cores=90))
text = self.serializer.serialize(exemplar)

Expand All @@ -172,6 +187,8 @@ def test_deserializer(self):
floating_ips='60',
instances='70',
injected_files='80',
security_groups='10',
security_group_rules='20',
cores='90'))
intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
'<quota_set>'
Expand All @@ -184,6 +201,8 @@ def test_deserializer(self):
'<floating_ips>60</floating_ips>'
'<instances>70</instances>'
'<injected_files>80</injected_files>'
'<security_groups>10</security_groups>'
'<security_group_rules>20</security_group_rules>'
'<cores>90</cores>'
'</quota_set>')

Expand Down

0 comments on commit 1f644d2

Please sign in to comment.