Skip to content

Commit

Permalink
View and update Neutron project quotas
Browse files Browse the repository at this point in the history
Also ensure that the correct quota is displayed in the Floating IPs
allocation page (Security & Access panel).

Closes-Bug: #1109140

Change-Id: I30d207fbf149bfbcfefeaddf91af49082b7b1f53
  • Loading branch information
jpichon committed Aug 27, 2013
1 parent 065786c commit 3852d1c
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 9 deletions.
16 changes: 16 additions & 0 deletions openstack_dashboard/api/base.py
Expand Up @@ -176,6 +176,19 @@ def __setitem__(self, k, v):
def __getitem__(self, index):
return self.items[index]

def __add__(self, other):
'''Merge another QuotaSet into this one. Existing quotas are
not overriden.'''
if not isinstance(other, QuotaSet):
msg = "Can only add QuotaSet to QuotaSet, " \
"but received %s instead" % type(other)
raise ValueError(msg)

for item in other:
if self.get(item.name).limit is None:
self.items.append(item)
return self

def __len__(self):
return len(self.items)

Expand All @@ -186,6 +199,9 @@ def get(self, key, default=None):
match = [quota for quota in self.items if quota.name == key]
return match.pop() if len(match) else Quota(key, default)

def add(self, other):
return self.__add__(other)


def get_service_from_catalog(catalog, service_type):
if catalog:
Expand Down
14 changes: 14 additions & 0 deletions openstack_dashboard/api/neutron.py
Expand Up @@ -608,6 +608,11 @@ def tenant_quota_get(request, tenant_id):
return base.QuotaSet(neutronclient(request).show_quota(tenant_id)['quota'])


def tenant_quota_update(request, tenant_id, **kwargs):
quotas = {'quota': kwargs}
return neutronclient(request).update_quota(tenant_id, quotas)


def list_extensions(request):
extensions_list = neutronclient(request).list_extensions()
if 'extensions' in extensions_list:
Expand All @@ -624,3 +629,12 @@ def is_extension_supported(request, extension_alias):
return True
else:
return False


def is_quotas_extension_supported(request):
network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {})
if network_config.get('enable_quotas', False) and \
is_extension_supported(request, 'quotas'):
return True
else:
return False
2 changes: 1 addition & 1 deletion openstack_dashboard/dashboards/admin/info/tests.py
Expand Up @@ -96,7 +96,7 @@ def test_index_with_neutron_disabled(self):
api.base.is_service_enabled(IsA(http.HttpRequest), 'volume') \
.AndReturn(True)
api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \
.AndReturn(False)
.MultipleTimes().AndReturn(False)

api.nova.default_quota_get(IsA(http.HttpRequest),
self.tenant.id).AndReturn(self.quotas.nova)
Expand Down
89 changes: 89 additions & 0 deletions openstack_dashboard/dashboards/admin/projects/tests.py
Expand Up @@ -18,6 +18,7 @@

from django.core.urlresolvers import reverse # noqa
from django import http
from django.test.utils import override_settings # noqa

from mox import IsA # noqa

Expand Down Expand Up @@ -84,11 +85,14 @@ def _get_workflow_fields(self, project):

def _get_quota_info(self, quota):
cinder_quota = self.cinder_quotas.first()
neutron_quota = self.neutron_quotas.first()
quota_data = {}
for field in quotas.NOVA_QUOTA_FIELDS:
quota_data[field] = int(quota.get(field).limit)
for field in quotas.CINDER_QUOTA_FIELDS:
quota_data[field] = int(cinder_quota.get(field).limit)
for field in quotas.NEUTRON_QUOTA_FIELDS:
quota_data[field] = int(neutron_quota.get(field).limit)
return quota_data

def _get_workflow_data(self, project, quota):
Expand Down Expand Up @@ -147,6 +151,8 @@ def test_add_project_get(self):
res = self.client.get(url)

self.assertTemplateUsed(res, views.WorkflowView.template_name)
self.assertContains(res, '<input type="hidden" name="subnet" '
'id="id_subnet" />', html=True)

workflow = res.context['workflow']
self.assertEqual(res.context['workflow'].name,
Expand All @@ -168,6 +174,52 @@ def test_add_project_get_domain(self):
domain_context_name=domain.name)
self.test_add_project_get()

@test.create_stubs({api.keystone: ('get_default_role',
'user_list',
'group_list',
'role_list'),
api.neutron: ('is_extension_supported',
'tenant_quota_get'),
quotas: ('get_default_quota_data',)})
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True})
def test_add_project_get_with_neutron(self):
quota = self.quotas.first()
neutron_quotas = self.neutron_quotas.first()

quotas.get_default_quota_data(IsA(http.HttpRequest)) \
.AndReturn(quota)
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'quotas') \
.MultipleTimes().AndReturn(True)
api.neutron.tenant_quota_get(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(neutron_quotas)
api.keystone.get_default_role(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(self.roles.first())
api.keystone.user_list(IsA(http.HttpRequest), domain=None) \
.AndReturn(self.users.list())
api.keystone.role_list(IsA(http.HttpRequest)) \
.AndReturn(self.roles.list())
api.keystone.group_list(IsA(http.HttpRequest), domain=None) \
.AndReturn(self.groups.list())
api.keystone.role_list(IsA(http.HttpRequest)) \
.AndReturn(self.roles.list())
self.mox.ReplayAll()

res = self.client.get(reverse('horizon:admin:projects:create'))

self.assertTemplateUsed(res, views.WorkflowView.template_name)
self.assertContains(res, '<input name="subnet" id="id_subnet" '
'value="10" type="text" />', html=True)

workflow = res.context['workflow']
self.assertEqual(res.context['workflow'].name,
workflows.CreateProject.name)

step = workflow.get_step("createprojectinfoaction")
self.assertEqual(step.action.initial['ram'], quota.get('ram').limit)
self.assertEqual(step.action.initial['subnet'],
neutron_quotas.get('subnet').limit)

@test.create_stubs({api.keystone: ('get_default_role',
'add_tenant_user_role',
'tenant_create',
Expand Down Expand Up @@ -250,6 +302,21 @@ def test_add_project_post_domain(self):
domain_context_name=domain.name)
self.test_add_project_post()

@test.create_stubs({api.neutron: ('is_extension_supported',
'tenant_quota_update')})
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True})
def test_add_project_post_with_neutron(self):
quota_data = self.neutron_quotas.first()
neutron_updated_quota = dict([(key, quota_data.get(key).limit)
for key in quotas.NEUTRON_QUOTA_FIELDS])

api.neutron.is_extension_supported(IsA(http.HttpRequest), 'quotas') \
.MultipleTimes().AndReturn(True)
api.neutron.tenant_quota_update(IsA(http.HttpRequest),
self.tenant.id,
**neutron_updated_quota)
self.test_add_project_post()

@test.create_stubs({api.keystone: ('user_list',
'role_list',
'group_list',
Expand Down Expand Up @@ -539,11 +606,14 @@ def test_add_project_missing_field_error_domain(self):
class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
def _get_quota_info(self, quota):
cinder_quota = self.cinder_quotas.first()
neutron_quota = self.neutron_quotas.first()
quota_data = {}
for field in quotas.NOVA_QUOTA_FIELDS:
quota_data[field] = int(quota.get(field).limit)
for field in quotas.CINDER_QUOTA_FIELDS:
quota_data[field] = int(cinder_quota.get(field).limit)
for field in quotas.NEUTRON_QUOTA_FIELDS:
quota_data[field] = int(neutron_quota.get(field).limit)
return quota_data

def _get_domain_id(self):
Expand Down Expand Up @@ -845,6 +915,25 @@ def test_update_project_save_domain(self):
domain_context_name=domain.name)
self.test_update_project_save()

@test.create_stubs({api.neutron: ('is_extension_supported',
'tenant_quota_get',
'tenant_quota_update')})
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True})
def test_update_project_save_with_neutron(self):
quota_data = self.neutron_quotas.first()
neutron_updated_quota = dict([(key, quota_data.get(key).limit)
for key in quotas.NEUTRON_QUOTA_FIELDS])

api.neutron.is_extension_supported(IsA(http.HttpRequest), 'quotas') \
.MultipleTimes().AndReturn(True)
api.neutron.tenant_quota_get(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(quota_data)
api.neutron.tenant_quota_update(IsA(http.HttpRequest),
self.tenant.id,
**neutron_updated_quota)
self.test_update_project_save()

@test.create_stubs({api.keystone: ('tenant_get',)})
def test_update_project_get_error(self):

Expand Down
20 changes: 20 additions & 0 deletions openstack_dashboard/dashboards/admin/projects/views.py
Expand Up @@ -109,6 +109,22 @@ def get_initial(self):
# get initial quota defaults
try:
quota_defaults = quotas.get_default_quota_data(self.request)

try:
if api.base.is_service_enabled(self.request, 'network') and \
api.neutron.is_quotas_extension_supported(self.request):
# TODO(jpichon): There is no API to access the Neutron
# default quotas (LP#1204956). For now, use the values
# from the current project.
project_id = self.request.user.project_id
quota_defaults += api.neutron.tenant_quota_get(
self.request,
tenant_id=project_id)
except Exception:
error_msg = _('Unable to retrieve default Neutron quota '
'values.')
self.add_error_to_step(error_msg, 'update_quotas')

for field in quotas.QUOTA_FIELDS:
initial[field] = quota_defaults.get(field).limit

Expand Down Expand Up @@ -138,6 +154,10 @@ def get_initial(self):
# get initial project quota
quota_data = quotas.get_tenant_quota_data(self.request,
tenant_id=project_id)
if api.base.is_service_enabled(self.request, 'network') and \
api.neutron.is_quotas_extension_supported(self.request):
quota_data += api.neutron.tenant_quota_get(self.request,
tenant_id=project_id)
for field in quotas.QUOTA_FIELDS:
initial[field] = quota_data.get(field).limit
except Exception:
Expand Down
23 changes: 23 additions & 0 deletions openstack_dashboard/dashboards/admin/projects/workflows.py
Expand Up @@ -63,6 +63,13 @@ class UpdateProjectQuotaAction(workflows.Action):
security_group_rules = forms.IntegerField(min_value=-1,
label=_("Security Group Rules"))

# Neutron
floatingip = forms.IntegerField(min_value=-1, label=_("Floating IPs"))
network = forms.IntegerField(min_value=-1, label=_("Networks"))
port = forms.IntegerField(min_value=-1, label=_("Ports"))
router = forms.IntegerField(min_value=-1, label=_("Routers"))
subnet = forms.IntegerField(min_value=-1, label=_("Subnets"))

def __init__(self, request, *args, **kwargs):
super(UpdateProjectQuotaAction, self).__init__(request,
*args,
Expand Down Expand Up @@ -418,6 +425,14 @@ def handle(self, request, data):
cinder.tenant_quota_update(request,
project_id,
**cinder_data)

if api.base.is_service_enabled(request, 'network') and \
api.neutron.is_quotas_extension_supported(request):
neutron_data = dict([(key, data[key]) for key in
quotas.NEUTRON_QUOTA_FIELDS])
api.neutron.tenant_quota_update(request,
project_id,
**neutron_data)
except Exception:
exceptions.handle(request, _('Unable to set project quotas.'))
return True
Expand Down Expand Up @@ -667,6 +682,14 @@ def handle(self, request, data):
cinder.tenant_quota_update(request,
project_id,
**cinder_data)

if api.base.is_service_enabled(request, 'network') and \
api.neutron.is_quotas_extension_supported(request):
neutron_data = dict([(key, data[key]) for key in
quotas.NEUTRON_QUOTA_FIELDS])
api.neutron.tenant_quota_update(request,
project_id,
**neutron_data)
return True
except Exception:
exceptions.handle(request, _('Modified project information and '
Expand Down
Expand Up @@ -21,6 +21,7 @@

from django.core.urlresolvers import reverse # noqa
from django import http
from django.test.utils import override_settings # noqa

from mox import IsA # noqa

Expand Down Expand Up @@ -175,3 +176,43 @@ def setUp(self):
def tearDown(self):
self.floating_ips = self._floating_ips_orig
super(FloatingIpViewTests, self).tearDown()

@test.create_stubs({api.nova: ('tenant_quota_get', 'flavor_list',
'server_list'),
api.cinder: ('tenant_quota_get', 'volume_list',
'volume_snapshot_list',),
api.network: ('floating_ip_pools_list',
'tenant_floating_ip_list'),
api.neutron: ('is_extension_supported',
'tenant_quota_get')})
@override_settings(OPENSTACK_NEUTRON_NETWORK={'enable_quotas': True})
def test_correct_quotas_displayed(self):
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]

api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.quotas.first())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([servers, False])
api.cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(self.snapshots.list())
api.cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
.AndReturn(self.cinder_quotas.first())
api.neutron.is_extension_supported(IsA(http.HttpRequest), 'quotas') \
.AndReturn(True)
api.neutron.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(self.neutron_quotas.first())
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(self.floating_ips.list())
api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \
.AndReturn(self.pools.list())
self.mox.ReplayAll()

url = reverse('%s:allocate' % NAMESPACE)
res = self.client.get(url)
self.assertEqual(res.context['usages']['floating_ips']['quota'],
self.neutron_quotas.first().get('floatingip').limit)
44 changes: 44 additions & 0 deletions openstack_dashboard/test/api_tests/base_tests.py
Expand Up @@ -169,3 +169,47 @@ def test_url_for(self):
self.request.user.services_region = "bogus_value"
with self.assertRaises(exceptions.ServiceCatalogException):
url = api_base.url_for(self.request, 'image')


class QuotaSetTests(test.TestCase):

def test_quotaset_add_with_plus(self):
quota_dict = {'foo': 1, 'bar': 10}
other_quota_dict = {'my_test': 12}
quota_set = api_base.QuotaSet(quota_dict)
other_quota_set = api_base.QuotaSet(other_quota_dict)

quota_set += other_quota_set
self.assertEqual(len(quota_set), 3)

quota_dict.update(other_quota_dict)
for q in quota_set:
self.assertEqual(q.limit, quota_dict[q.name])

def test_quotaset_add_doesnt_override_existing_quota(self):
quota_dict = {'foo': 1, 'bar': 10}
quota_set = api_base.QuotaSet(quota_dict)
other_quota_set = api_base.QuotaSet({'foo': 12})

quota_set += other_quota_set
self.assertEqual(len(quota_set), 2)

for q in quota_set:
self.assertEqual(q.limit, quota_dict[q.name])

def test_quotaset_add_method(self):
quota_dict = {'foo': 1, 'bar': 10}
other_quota_dict = {'my_test': 12}
quota_set = api_base.QuotaSet(quota_dict)
other_quota_set = api_base.QuotaSet(other_quota_dict)

quota_set.add(other_quota_set)
self.assertEqual(len(quota_set), 3)

quota_dict.update(other_quota_dict)
for q in quota_set:
self.assertEqual(q.limit, quota_dict[q.name])

def test_quotaset_add_with_wrong_type(self):
quota_set = api_base.QuotaSet({'foo': 1, 'bar': 10})
self.assertRaises(ValueError, quota_set.add, {'test': 7})

0 comments on commit 3852d1c

Please sign in to comment.