Allow admins to update default quotas
Add update default quotas form to edit the default quotas. Move the editable default quotas tab to a new panel called "Defaults". Implements blueprint edit-default-quota Change-Id: I6e3806226cd2f699f16b93c25e294bc67883738f
This commit is contained in:
parent
f91df5bb9c
commit
5f8e370f2d
@ -39,6 +39,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
# API static values
|
||||
VOLUME_STATE_AVAILABLE = "available"
|
||||
DEFAULT_QUOTA_NAME = 'default'
|
||||
|
||||
|
||||
def cinderclient(request):
|
||||
@ -134,6 +135,10 @@ def default_quota_get(request, tenant_id):
|
||||
return base.QuotaSet(cinderclient(request).quotas.defaults(tenant_id))
|
||||
|
||||
|
||||
def default_quota_update(request, **kwargs):
|
||||
cinderclient(request).quota_classes.update(DEFAULT_QUOTA_NAME, **kwargs)
|
||||
|
||||
|
||||
def volume_type_list(request):
|
||||
return cinderclient(request).volume_types.list()
|
||||
|
||||
|
@ -47,6 +47,7 @@ LOG = logging.getLogger(__name__)
|
||||
# API static values
|
||||
INSTANCE_ACTIVE_STATE = 'ACTIVE'
|
||||
VOLUME_STATE_AVAILABLE = "available"
|
||||
DEFAULT_QUOTA_NAME = 'default'
|
||||
|
||||
|
||||
class VNCConsole(base.APIDictWrapper):
|
||||
@ -557,6 +558,10 @@ def default_quota_get(request, tenant_id):
|
||||
return base.QuotaSet(novaclient(request).quotas.defaults(tenant_id))
|
||||
|
||||
|
||||
def default_quota_update(request, **kwargs):
|
||||
novaclient(request).quota_classes.update(DEFAULT_QUOTA_NAME, **kwargs)
|
||||
|
||||
|
||||
def usage_get(request, tenant_id, start, end):
|
||||
return NovaUsage(novaclient(request).usage.get(tenant_id, start, end))
|
||||
|
||||
|
@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup):
|
||||
slug = "admin"
|
||||
name = _("System Panel")
|
||||
panels = ('overview', 'hypervisors', 'instances', 'volumes',
|
||||
'flavors', 'images', 'networks', 'routers', 'info')
|
||||
'flavors', 'images', 'networks', 'routers', 'defaults', 'info')
|
||||
|
||||
|
||||
class IdentityPanels(horizon.PanelGroup):
|
||||
|
29
openstack_dashboard/dashboards/admin/defaults/panel.py
Normal file
29
openstack_dashboard/dashboards/admin/defaults/panel.py
Normal file
@ -0,0 +1,29 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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 django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
import horizon
|
||||
|
||||
from openstack_dashboard.dashboards.admin import dashboard
|
||||
|
||||
|
||||
class Defaults(horizon.Panel):
|
||||
name = _("Defaults")
|
||||
slug = 'defaults'
|
||||
|
||||
|
||||
dashboard.Admin.register(Defaults)
|
62
openstack_dashboard/dashboards/admin/defaults/tables.py
Normal file
62
openstack_dashboard/dashboards/admin/defaults/tables.py
Normal file
@ -0,0 +1,62 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotaFilterAction(tables.FilterAction):
|
||||
def filter(self, table, tenants, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
||||
def comp(tenant):
|
||||
if q in tenant.name.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
return filter(comp, tenants)
|
||||
|
||||
|
||||
class UpdateDefaultQuotas(tables.LinkAction):
|
||||
name = "update_defaults"
|
||||
verbose_name = _("Update Defaults")
|
||||
url = "horizon:admin:defaults:update_defaults"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
|
||||
|
||||
def get_quota_name(quota):
|
||||
QUOTA_NAMES = {'cores': _('VCPUs'), 'floating_ips': _('Floating IPs')}
|
||||
return QUOTA_NAMES.get(quota.name, quota.name.replace("_", " ").title())
|
||||
|
||||
|
||||
class QuotasTable(tables.DataTable):
|
||||
name = tables.Column(get_quota_name, verbose_name=_('Quota Name'))
|
||||
limit = tables.Column("limit", verbose_name=_('Limit'))
|
||||
|
||||
def get_object_id(self, obj):
|
||||
return obj.name
|
||||
|
||||
class Meta:
|
||||
name = "quotas"
|
||||
verbose_name = _("Quotas")
|
||||
table_actions = (QuotaFilterAction, UpdateDefaultQuotas)
|
||||
multi_select = False
|
54
openstack_dashboard/dashboards/admin/defaults/tabs.py
Normal file
54
openstack_dashboard/dashboards/admin/defaults/tabs.py
Normal file
@ -0,0 +1,54 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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 django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
from openstack_dashboard.dashboards.admin.defaults import tables
|
||||
|
||||
|
||||
class DefaultQuotasTab(tabs.TableTab):
|
||||
table_classes = (tables.QuotasTable,)
|
||||
name = _("Default Quotas")
|
||||
slug = "quotas"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
|
||||
def get_quotas_data(self):
|
||||
request = self.tab_group.request
|
||||
try:
|
||||
quota_set = quotas.get_default_quota_data(request)
|
||||
data = quota_set.items
|
||||
# There is no API to get the default system quotas in
|
||||
# Neutron (cf. LP#1204956). Remove the network-related
|
||||
# quotas from the list for now to avoid confusion
|
||||
if base.is_service_enabled(self.request, 'network'):
|
||||
data = [quota for quota in data
|
||||
if quota.name not in ['floating_ips', 'fixed_ips']]
|
||||
except Exception:
|
||||
data = []
|
||||
exceptions.handle(self.request, _('Unable to get quota info.'))
|
||||
return data
|
||||
|
||||
|
||||
class DefaultsTabs(tabs.TabGroup):
|
||||
slug = "defaults"
|
||||
tabs = (DefaultQuotasTab,)
|
||||
sticky = True
|
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Defaults" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Defaults")%}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
140
openstack_dashboard/dashboards/admin/defaults/tests.py
Normal file
140
openstack_dashboard/dashboards/admin/defaults/tests.py
Normal file
@ -0,0 +1,140 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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 django.core.urlresolvers import reverse # noqa
|
||||
from django import http
|
||||
from mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
INDEX_URL = reverse('horizon:admin:defaults:index')
|
||||
|
||||
|
||||
class ServicesViewTests(test.BaseAdminViewTests):
|
||||
def test_index(self):
|
||||
self.mox.StubOutWithMock(api.nova, 'default_quota_get')
|
||||
self.mox.StubOutWithMock(api.cinder, 'default_quota_get')
|
||||
api.nova.default_quota_get(IsA(http.HttpRequest),
|
||||
self.tenant.id).AndReturn(self.quotas.nova)
|
||||
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||
.AndReturn(self.cinder_quotas.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'admin/defaults/index.html')
|
||||
|
||||
quotas_tab = res.context['tab_group'].get_tab('quotas')
|
||||
self.assertQuerysetEqual(quotas_tab._tables['quotas'].data,
|
||||
['<Quota: (injected_file_content_bytes, 1)>',
|
||||
'<Quota: (metadata_items, 1)>',
|
||||
'<Quota: (injected_files, 1)>',
|
||||
'<Quota: (gigabytes, 1000)>',
|
||||
'<Quota: (ram, 10000)>',
|
||||
'<Quota: (instances, 10)>',
|
||||
'<Quota: (snapshots, 1)>',
|
||||
'<Quota: (volumes, 1)>',
|
||||
'<Quota: (cores, 10)>',
|
||||
'<Quota: (security_groups, 10)>',
|
||||
'<Quota: (security_group_rules, 20)>'],
|
||||
ordered=False)
|
||||
|
||||
@test.create_stubs({api.base: ('is_service_enabled',),
|
||||
api.nova: ('default_quota_get', 'service_list',),
|
||||
api.cinder: ('default_quota_get',)})
|
||||
def test_index_with_neutron_disabled(self):
|
||||
# Neutron does not have an API for getting default system
|
||||
# quotas. When not using Neutron, the floating ips quotas
|
||||
# should be in the list.
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'volume') \
|
||||
.AndReturn(True)
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \
|
||||
.MultipleTimes().AndReturn(False)
|
||||
|
||||
api.nova.default_quota_get(IsA(http.HttpRequest),
|
||||
self.tenant.id).AndReturn(self.quotas.nova)
|
||||
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||
.AndReturn(self.cinder_quotas.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'admin/defaults/index.html')
|
||||
|
||||
quotas_tab = res.context['tab_group'].get_tab('quotas')
|
||||
self.assertQuerysetEqual(quotas_tab._tables['quotas'].data,
|
||||
['<Quota: (injected_file_content_bytes, 1)>',
|
||||
'<Quota: (metadata_items, 1)>',
|
||||
'<Quota: (injected_files, 1)>',
|
||||
'<Quota: (gigabytes, 1000)>',
|
||||
'<Quota: (ram, 10000)>',
|
||||
'<Quota: (floating_ips, 1)>',
|
||||
'<Quota: (fixed_ips, 10)>',
|
||||
'<Quota: (instances, 10)>',
|
||||
'<Quota: (snapshots, 1)>',
|
||||
'<Quota: (volumes, 1)>',
|
||||
'<Quota: (cores, 10)>',
|
||||
'<Quota: (security_groups, 10)>',
|
||||
'<Quota: (security_group_rules, 20)>'],
|
||||
ordered=False)
|
||||
|
||||
|
||||
class UpdateDefaultQuotasTests(test.BaseAdminViewTests):
|
||||
def _get_quota_info(self, quota):
|
||||
quota_data = {}
|
||||
for field in (quotas.QUOTA_FIELDS + quotas.MISSING_QUOTA_FIELDS):
|
||||
if field != 'fixed_ips':
|
||||
limit = quota.get(field).limit or 10
|
||||
quota_data[field] = int(limit)
|
||||
return quota_data
|
||||
|
||||
@test.create_stubs({api.nova: ('default_quota_update', ),
|
||||
api.cinder: ('default_quota_update', ),
|
||||
quotas: ('get_default_quota_data', )})
|
||||
def test_update_default_quotas(self):
|
||||
quota = self.quotas.first()
|
||||
|
||||
# init
|
||||
quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota)
|
||||
|
||||
# update some fields
|
||||
quota[0].limit = 123
|
||||
quota[1].limit = -1
|
||||
updated_quota = self._get_quota_info(quota)
|
||||
|
||||
# handle
|
||||
nova_fields = quotas.NOVA_QUOTA_FIELDS + quotas.MISSING_QUOTA_FIELDS
|
||||
nova_updated_quota = dict([(key, updated_quota[key]) for key in
|
||||
nova_fields if key != 'fixed_ips'])
|
||||
api.nova.default_quota_update(IsA(http.HttpRequest),
|
||||
**nova_updated_quota)
|
||||
|
||||
cinder_updated_quota = dict([(key, updated_quota[key]) for key in
|
||||
quotas.CINDER_QUOTA_FIELDS])
|
||||
api.cinder.default_quota_update(IsA(http.HttpRequest),
|
||||
**cinder_updated_quota)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:defaults:update_defaults')
|
||||
res = self.client.post(url, updated_quota)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
26
openstack_dashboard/dashboards/admin/defaults/urls.py
Normal file
26
openstack_dashboard/dashboards/admin/defaults/urls.py
Normal file
@ -0,0 +1,26 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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 django.conf.urls.defaults import patterns # noqa
|
||||
from django.conf.urls.defaults import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.defaults import views
|
||||
|
||||
|
||||
urlpatterns = patterns('openstack_dashboard.dashboards.admin.defaults.views',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^update_defaults$',
|
||||
views.UpdateDefaultQuotasView.as_view(), name='update_defaults'))
|
54
openstack_dashboard/dashboards/admin/defaults/views.py
Normal file
54
openstack_dashboard/dashboards/admin/defaults/views.py
Normal file
@ -0,0 +1,54 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import tabs
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard.dashboards.admin.defaults import tabs as project_tabs
|
||||
from openstack_dashboard.dashboards.admin.defaults import workflows as \
|
||||
project_workflows
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tabs.TabbedTableView):
|
||||
tab_group_class = project_tabs.DefaultsTabs
|
||||
template_name = 'admin/defaults/index.html'
|
||||
|
||||
|
||||
class UpdateDefaultQuotasView(workflows.WorkflowView):
|
||||
workflow_class = project_workflows.UpdateDefaultQuotas
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(UpdateDefaultQuotasView, self).get_initial()
|
||||
|
||||
# get initial quota defaults
|
||||
try:
|
||||
quota_defaults = quotas.get_default_quota_data(self.request)
|
||||
for field in (quotas.QUOTA_FIELDS + quotas.MISSING_QUOTA_FIELDS):
|
||||
initial[field] = quota_defaults.get(field).limit
|
||||
|
||||
except Exception:
|
||||
error_msg = _('Unable to retrieve default quota values.')
|
||||
self.add_error_to_step(error_msg, 'update_default_quotas')
|
||||
|
||||
return initial
|
101
openstack_dashboard/dashboards/admin/defaults/workflows.py
Normal file
101
openstack_dashboard/dashboards/admin/defaults/workflows.py
Normal file
@ -0,0 +1,101 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin, Inc.
|
||||
#
|
||||
# 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 django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.api import nova
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
ALL_NOVA_QUOTA_FIELDS = quotas.NOVA_QUOTA_FIELDS + quotas.MISSING_QUOTA_FIELDS
|
||||
|
||||
|
||||
class UpdateDefaultQuotasAction(workflows.Action):
|
||||
ifcb_label = _("Injected File Content Bytes")
|
||||
ifpb_label = _("Injected File Path Bytes")
|
||||
metadata_items = forms.IntegerField(min_value=-1,
|
||||
label=_("Metadata Items"))
|
||||
cores = forms.IntegerField(min_value=-1, label=_("VCPUs"))
|
||||
instances = forms.IntegerField(min_value=-1, label=_("Instances"))
|
||||
injected_files = forms.IntegerField(min_value=-1,
|
||||
label=_("Injected Files"))
|
||||
injected_file_content_bytes = forms.IntegerField(min_value=-1,
|
||||
label=ifcb_label)
|
||||
injected_file_path_bytes = forms.IntegerField(min_value=-1,
|
||||
label=ifpb_label)
|
||||
volumes = forms.IntegerField(min_value=-1, label=_("Volumes"))
|
||||
snapshots = forms.IntegerField(min_value=-1, label=_("Snapshots"))
|
||||
gigabytes = forms.IntegerField(min_value=-1, label=_("Gigabytes"))
|
||||
ram = forms.IntegerField(min_value=-1, label=_("RAM (MB)"))
|
||||
floating_ips = forms.IntegerField(min_value=-1, label=_("Floating IPs"))
|
||||
security_groups = forms.IntegerField(min_value=-1,
|
||||
label=_("Security Groups"))
|
||||
security_group_rules = forms.IntegerField(min_value=-1,
|
||||
label=_("Security Group Rules"))
|
||||
key_pairs = forms.IntegerField(min_value=-1, label=_("Key Pairs"))
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(UpdateDefaultQuotasAction, self).__init__(request,
|
||||
*args,
|
||||
**kwargs)
|
||||
disabled_quotas = quotas.get_disabled_quotas(request)
|
||||
for field in disabled_quotas:
|
||||
if field in self.fields:
|
||||
self.fields[field].required = False
|
||||
self.fields[field].widget = forms.HiddenInput()
|
||||
|
||||
class Meta:
|
||||
name = _("Default Quotas")
|
||||
slug = 'update_default_quotas'
|
||||
help_text = _("From here you can update the default quotas "
|
||||
"(max limits).")
|
||||
|
||||
|
||||
class UpdateDefaultQuotas(workflows.Step):
|
||||
action_class = UpdateDefaultQuotasAction
|
||||
contributes = (quotas.QUOTA_FIELDS + quotas.MISSING_QUOTA_FIELDS)
|
||||
|
||||
|
||||
class UpdateDefaultQuotas(workflows.Workflow):
|
||||
slug = "update_default_quotas"
|
||||
name = _("Update Default Quotas")
|
||||
finalize_button_name = _("Update Defaults")
|
||||
success_message = _('Default quotas updated "%s".')
|
||||
failure_message = _('Unable to update default quotas "%s".')
|
||||
success_url = "horizon:admin:defaults:index"
|
||||
default_steps = (UpdateDefaultQuotas,)
|
||||
|
||||
def handle(self, request, data):
|
||||
# Update the default quotas.
|
||||
# `fixed_ips` update for quota class is not supported by novaclient
|
||||
nova_data = dict([(key, data[key]) for key in ALL_NOVA_QUOTA_FIELDS
|
||||
if key != 'fixed_ips'])
|
||||
try:
|
||||
nova.default_quota_update(request, **nova_data)
|
||||
|
||||
if base.is_service_enabled(request, 'volume'):
|
||||
cinder_data = dict([(key, data[key]) for key in
|
||||
quotas.CINDER_QUOTA_FIELDS])
|
||||
cinder.default_quota_update(request, **cinder_data)
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to update default quotas.'))
|
||||
return True
|
@ -25,9 +25,9 @@ import horizon
|
||||
from openstack_dashboard.dashboards.admin import dashboard
|
||||
|
||||
|
||||
class Quotas(horizon.Panel):
|
||||
class Info(horizon.Panel):
|
||||
name = _("System Info")
|
||||
slug = 'info'
|
||||
|
||||
|
||||
dashboard.Admin.register(Quotas)
|
||||
dashboard.Admin.register(Info)
|
||||
|
@ -11,40 +11,6 @@ from horizon.utils.filters import parse_isotime # noqa
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotaFilterAction(tables.FilterAction):
|
||||
def filter(self, table, tenants, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
||||
def comp(tenant):
|
||||
if q in tenant.name.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
return filter(comp, tenants)
|
||||
|
||||
|
||||
def get_quota_name(quota):
|
||||
if quota.name == "cores":
|
||||
return _('VCPUs')
|
||||
if quota.name == "floating_ips":
|
||||
return _('Floating IPs')
|
||||
return quota.name.replace("_", " ").title()
|
||||
|
||||
|
||||
class QuotasTable(tables.DataTable):
|
||||
name = tables.Column(get_quota_name, verbose_name=_('Quota Name'))
|
||||
limit = tables.Column("limit", verbose_name=_('Limit'))
|
||||
|
||||
def get_object_id(self, obj):
|
||||
return obj.name
|
||||
|
||||
class Meta:
|
||||
name = "quotas"
|
||||
verbose_name = _("Quotas")
|
||||
table_actions = (QuotaFilterAction,)
|
||||
multi_select = False
|
||||
|
||||
|
||||
class ServiceFilterAction(tables.FilterAction):
|
||||
def filter(self, table, services, filter_string):
|
||||
q = filter_string.lower()
|
||||
|
@ -19,37 +19,12 @@ from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.api import keystone
|
||||
from openstack_dashboard.api import nova
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
from openstack_dashboard.dashboards.admin.info import tables
|
||||
|
||||
|
||||
class DefaultQuotasTab(tabs.TableTab):
|
||||
table_classes = (tables.QuotasTable,)
|
||||
name = _("Default Quotas")
|
||||
slug = "quotas"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
|
||||
def get_quotas_data(self):
|
||||
request = self.tab_group.request
|
||||
try:
|
||||
quota_set = quotas.get_default_quota_data(request)
|
||||
data = quota_set.items
|
||||
# There is no API to get the default system quotas in
|
||||
# Neutron (cf. LP#1204956). Remove the network-related
|
||||
# quotas from the list for now to avoid confusion
|
||||
if base.is_service_enabled(self.request, 'network'):
|
||||
data = [quota for quota in data
|
||||
if quota.name not in ['floating_ips', 'fixed_ips']]
|
||||
except Exception:
|
||||
data = []
|
||||
exceptions.handle(self.request, _('Unable to get quota info.'))
|
||||
return data
|
||||
|
||||
|
||||
class ServicesTab(tabs.TableTab):
|
||||
table_classes = (tables.ServicesTable,)
|
||||
name = _("Services")
|
||||
@ -119,6 +94,5 @@ class NovaServicesTab(tabs.TableTab):
|
||||
|
||||
class SystemInfoTabs(tabs.TabGroup):
|
||||
slug = "system_info"
|
||||
tabs = (ServicesTab, NovaServicesTab, ZonesTab, HostAggregatesTab,
|
||||
DefaultQuotasTab)
|
||||
tabs = (ServicesTab, NovaServicesTab, ZonesTab, HostAggregatesTab)
|
||||
sticky = True
|
||||
|
@ -26,16 +26,10 @@ INDEX_URL = reverse('horizon:admin:info:index')
|
||||
|
||||
class SystemInfoViewTests(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.nova: ('default_quota_get',
|
||||
'service_list',
|
||||
@test.create_stubs({api.nova: ('service_list',
|
||||
'availability_zone_list',
|
||||
'aggregate_list'),
|
||||
api.cinder: ('default_quota_get',)})
|
||||
'aggregate_list')})
|
||||
def test_index(self):
|
||||
api.nova.default_quota_get(IsA(http.HttpRequest),
|
||||
self.tenant.id).AndReturn(self.quotas.nova)
|
||||
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||
.AndReturn(self.cinder_quotas.first())
|
||||
services = self.services.list()
|
||||
api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services)
|
||||
api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
|
||||
@ -61,91 +55,6 @@ class SystemInfoViewTests(test.BaseAdminViewTests):
|
||||
'<Service: metering>',
|
||||
'<Service: orchestration>'])
|
||||
|
||||
quotas_tab = res.context['tab_group'].get_tab('quotas')
|
||||
self.assertQuerysetEqual(quotas_tab._tables['quotas'].data,
|
||||
['<Quota: (injected_file_content_bytes, 1)>',
|
||||
'<Quota: (metadata_items, 1)>',
|
||||
'<Quota: (injected_files, 1)>',
|
||||
'<Quota: (gigabytes, 1000)>',
|
||||
'<Quota: (ram, 10000)>',
|
||||
'<Quota: (instances, 10)>',
|
||||
'<Quota: (snapshots, 1)>',
|
||||
'<Quota: (volumes, 1)>',
|
||||
'<Quota: (cores, 10)>',
|
||||
'<Quota: (security_groups, 10)>',
|
||||
'<Quota: (security_group_rules, 20)>'],
|
||||
ordered=False)
|
||||
|
||||
zones_tab = res.context['tab_group'].get_tab('zones')
|
||||
self.assertQuerysetEqual(zones_tab._tables['zones'].data,
|
||||
['<AvailabilityZone: nova>'])
|
||||
|
||||
aggregates_tab = res.context['tab_group'].get_tab('aggregates')
|
||||
self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data,
|
||||
['<Aggregate: 1>', '<Aggregate: 2>'])
|
||||
|
||||
@test.create_stubs({api.base: ('is_service_enabled',),
|
||||
api.nova: ('default_quota_get',
|
||||
'service_list',
|
||||
'availability_zone_list',
|
||||
'aggregate_list'),
|
||||
api.cinder: ('default_quota_get',)})
|
||||
def test_index_with_neutron_disabled(self):
|
||||
# Neutron does not have an API for getting default system
|
||||
# quotas. When not using Neutron, the floating ips quotas
|
||||
# should be in the list.
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'volume') \
|
||||
.AndReturn(True)
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \
|
||||
.MultipleTimes().AndReturn(False)
|
||||
|
||||
api.nova.default_quota_get(IsA(http.HttpRequest),
|
||||
self.tenant.id).AndReturn(self.quotas.nova)
|
||||
|
||||
api.cinder.default_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||
.AndReturn(self.cinder_quotas.first())
|
||||
services = self.services.list()
|
||||
api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services)
|
||||
api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \
|
||||
.AndReturn(self.availability_zones.list())
|
||||
api.nova.aggregate_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.aggregates.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'admin/info/index.html')
|
||||
|
||||
services_tab = res.context['tab_group'].get_tab('services')
|
||||
self.assertQuerysetEqual(services_tab._tables['services'].data,
|
||||
['<Service: compute>',
|
||||
'<Service: volume>',
|
||||
'<Service: image>',
|
||||
'<Service: identity (native backend)>',
|
||||
'<Service: object-store>',
|
||||
'<Service: network>',
|
||||
'<Service: ec2>',
|
||||
'<Service: metering>',
|
||||
'<Service: orchestration>'])
|
||||
|
||||
quotas_tab = res.context['tab_group'].get_tab('quotas')
|
||||
self.assertQuerysetEqual(quotas_tab._tables['quotas'].data,
|
||||
['<Quota: (injected_file_content_bytes, 1)>',
|
||||
'<Quota: (metadata_items, 1)>',
|
||||
'<Quota: (injected_files, 1)>',
|
||||
'<Quota: (gigabytes, 1000)>',
|
||||
'<Quota: (ram, 10000)>',
|
||||
'<Quota: (floating_ips, 1)>',
|
||||
'<Quota: (fixed_ips, 10)>',
|
||||
'<Quota: (instances, 10)>',
|
||||
'<Quota: (snapshots, 1)>',
|
||||
'<Quota: (volumes, 1)>',
|
||||
'<Quota: (cores, 10)>',
|
||||
'<Quota: (security_groups, 10)>',
|
||||
'<Quota: (security_group_rules, 20)>'],
|
||||
ordered=False)
|
||||
|
||||
zones_tab = res.context['tab_group'].get_tab('zones')
|
||||
self.assertQuerysetEqual(zones_tab._tables['zones'].data,
|
||||
['<AvailabilityZone: nova>'])
|
||||
|
@ -26,6 +26,9 @@ NOVA_QUOTA_FIELDS = ("metadata_items",
|
||||
"security_groups",
|
||||
"security_group_rules",)
|
||||
|
||||
MISSING_QUOTA_FIELDS = ("key_pairs",
|
||||
"injected_file_path_bytes",)
|
||||
|
||||
CINDER_QUOTA_FIELDS = ("volumes",
|
||||
"snapshots",
|
||||
"gigabytes",)
|
||||
|
@ -8,7 +8,7 @@ kombu>=2.4.8
|
||||
lesscpy>=0.9j
|
||||
iso8601>=0.1.4
|
||||
netaddr
|
||||
python-cinderclient>=1.0.4
|
||||
python-cinderclient>=1.0.5
|
||||
python-glanceclient>=0.9.0
|
||||
python-heatclient>=0.2.3
|
||||
python-keystoneclient>=0.3.0
|
||||
|
Loading…
Reference in New Issue
Block a user