commit 899c3656ebb5f7701165db44d4474bdae5643c16 Author: Michael Still Date: Sun Mar 3 03:57:22 2013 +0000 Add quotas for fixed ips. DocImpact: there is now a default quota of 10 fixed ips per tenant. This will need to be adjusted by deployers if that number does not meet their needs. Resolves bug 1125468 for essex. diff --git a/nova/db/api.py b/nova/db/api.py index 27f80f6..0b2a9d0 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -461,6 +461,12 @@ def fixed_ip_update(context, address, values): """Create a fixed ip from the values dictionary.""" return IMPL.fixed_ip_update(context, address, values) + +def fixed_ip_count_by_project(context, project_id, session=None): + """Count fixed ips used by project.""" + return IMPL.fixed_ip_count_by_project(context, project_id, + session=session) + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 55eec9e..d18d836 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1114,6 +1114,27 @@ def fixed_ip_update(context, address, values): fixed_ip_ref.save(session=session) +@require_context +def fixed_ip_count_by_project(context, project_id, session=None): + authorize_project_context(context, project_id) + + # NOTE(mikal): Yes I know this is horrible, but I couldn't + # get a query using a join working, mainly because of a failure + # to be able to express the where clause sensibly. Patches + # welcome. + session = get_session() + with session.begin(): + instance_id_query = model_query(context, models.Instance.id, + read_deleted="no", session=session).\ + filter(models.Instance.project_id == \ + project_id) + id_filter = models.FixedIp.instance_id.in_(instance_id_query) + return model_query(context, models.FixedIp, read_deleted="no", + session=session).\ + filter(id_filter).\ + count() + + ################### diff --git a/nova/network/manager.py b/nova/network/manager.py index 3485e40..0583ae5 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -1152,6 +1152,12 @@ class NetworkManager(manager.SchedulerDependentManager): def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" + LOG.debug("QUOTA: %s" % quota.allowed_fixed_ips(context, 1)) + if quota.allowed_fixed_ips(context, 1) < 1: + LOG.warn(_('Quota exceeded for %s, tried to allocate address'), + context.project_id) + raise exception.QuotaError(code='FixedAddressLimitExceeded') + # TODO(vish): when this is called by compute, we can associate compute # with a network, or a cluster of computes with a network # and use that network here with a method like @@ -1231,6 +1237,10 @@ class NetworkManager(manager.SchedulerDependentManager): {'allocated': False, 'virtual_interface_id': None}) + # Commit the reservations + if reservations: + QUOTAS.commit(context, reservations) + def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" LOG.debug(_('Leased IP |%(address)s|'), locals(), context=context) diff --git a/nova/quota.py b/nova/quota.py index 505dce0..01ec9dd 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -42,6 +42,10 @@ quota_opts = [ cfg.IntOpt('quota_floating_ips', default=10, help='number of floating ips allowed per project'), + cfg.IntOpt('quota_fixed_ips', + default=10, + help=('number of fixed ips allowed per project (this should be ' + 'at least the number of instances allowed)')), cfg.IntOpt('quota_metadata_items', default=128, help='number of metadata items allowed per instance'), @@ -74,6 +78,7 @@ def _get_default_quotas(): 'volumes': FLAGS.quota_volumes, 'gigabytes': FLAGS.quota_gigabytes, 'floating_ips': FLAGS.quota_floating_ips, + 'fixed_ips': FLAGS.quota_fixed_ips, 'metadata_items': FLAGS.quota_metadata_items, 'injected_files': FLAGS.quota_max_injected_files, 'injected_file_content_bytes': @@ -173,6 +178,18 @@ def allowed_floating_ips(context, requested_floating_ips): return min(requested_floating_ips, allowed_floating_ips) +def allowed_fixed_ips(context, requested_fixed_ips): + """Check quota and return min(requested, allowed) fixed ips.""" + project_id = context.project_id + context = context.elevated() + used_fixed_ips = db.fixed_ip_count_by_project(context, project_id) + quota = get_project_quotas(context, project_id) + allowed_fixed_ips = _get_request_allotment(requested_fixed_ips, + used_fixed_ips, + quota['fixed_ips']) + return min(requested_fixed_ips, allowed_fixed_ips) + + def allowed_security_groups(context, requested_security_groups): """Check quota and return min(requested, allowed) security groups.""" project_id = context.project_id diff --git a/nova/tests/api/openstack/compute/contrib/test_quotas.py b/nova/tests/api/openstack/compute/contrib/test_quotas.py index 8f7084a..9fc0673 100644 --- a/nova/tests/api/openstack/compute/contrib/test_quotas.py +++ b/nova/tests/api/openstack/compute/contrib/test_quotas.py @@ -27,8 +27,8 @@ from nova.tests.api.openstack import fakes 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, + 'fixed_ips': 10, 'instances': 10, 'injected_files': 5, + 'cores': 20, 'injected_file_content_bytes': 10240, 'security_groups': 10, 'security_group_rules': 20}} @@ -50,6 +50,7 @@ class QuotaSetsTest(test.TestCase): 'ram': 51200, 'volumes': 10, 'floating_ips': 10, + 'fixed_ips': 10, 'metadata_items': 128, 'gigabytes': 1000, 'injected_files': 5, @@ -89,6 +90,7 @@ class QuotaSetsTest(test.TestCase): 'volumes': 10, 'gigabytes': 1000, 'floating_ips': 10, + 'fixed_ips': 10, 'metadata_items': 128, 'injected_files': 5, 'injected_file_content_bytes': 10240, @@ -154,6 +156,7 @@ class QuotaXMLSerializerTest(test.TestCase): gigabytes=40, ram=50, floating_ips=60, + fixed_ips=10, instances=70, injected_files=80, security_groups=10, diff --git a/nova/tests/network/test_manager.py b/nova/tests/network/test_manager.py index 5008bad..4cc83fa 100644 --- a/nova/tests/network/test_manager.py +++ b/nova/tests/network/test_manager.py @@ -275,6 +275,7 @@ class FlatNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance_and_network') self.mox.StubOutWithMock(db, 'fixed_ip_update') + self.mox.StubOutWithMock(quota.QUOTAS, 'reserve') db.fixed_ip_update(mox.IgnoreArg(), mox.IgnoreArg(), @@ -432,6 +433,10 @@ class VlanNetworkTestCase(test.TestCase): db.instance_get(mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'security_groups': [{'id': 0}]}) + + quota.QUOTAS.reserve(mox.IgnoreArg(), + fixed_ips=mox.IgnoreArg()).AndReturn(None) + db.fixed_ip_associate_pool(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn('192.168.0.1') @@ -802,6 +807,8 @@ class VlanNetworkTestCase(test.TestCase): self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance_and_network') self.mox.StubOutWithMock(db, 'fixed_ip_update') + self.mox.StubOutWithMock(quota.QUOTAS, 'reserve') + self.mox.StubOutWithMock(quota.QUOTAS, 'reserve') db.fixed_ip_update(mox.IgnoreArg(), mox.IgnoreArg(),