Quota Update Panel
Allows viewing of current quota value, quota sizes and percentage of use of current quota. Allows changing of each region's quota. Change-Id: Ia9f254ffb905b4e8971d84f85aefad164e8a3438
This commit is contained in:
parent
502e6db24d
commit
519c30d185
@ -17,10 +17,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from six.moves.urllib.parse import urljoin
|
from six.moves.urllib.parse import urljoin
|
||||||
|
import six
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from horizon.utils import functions as utils
|
from horizon.utils import functions as utils
|
||||||
|
from horizon.utils import memoized
|
||||||
|
|
||||||
from openstack_dashboard.api import base
|
from openstack_dashboard.api import base
|
||||||
|
|
||||||
@ -37,6 +39,68 @@ TASK = collections.namedtuple('Task',
|
|||||||
'created_on', 'approved_on', 'page',
|
'created_on', 'approved_on', 'page',
|
||||||
'completed_on', 'actions', 'status'])
|
'completed_on', 'actions', 'status'])
|
||||||
|
|
||||||
|
QUOTA_SIZE = collections.namedtuple('QuotaSize',
|
||||||
|
['id', 'name', 'cinder',
|
||||||
|
'nova', 'neutron'])
|
||||||
|
|
||||||
|
REGION_QUOTA = collections.namedtuple('RegionQuota',
|
||||||
|
['id', 'region',
|
||||||
|
'quota_size', 'preapproved_quotas'])
|
||||||
|
|
||||||
|
REGION_QUOTA_VALUE = collections.namedtuple('RegionQuotaValue',
|
||||||
|
['id', 'name',
|
||||||
|
'service', 'current_quota',
|
||||||
|
'current_usage', 'percent',
|
||||||
|
'size_blob', 'important'])
|
||||||
|
|
||||||
|
SIZE_QUOTA_VALUE = collections.namedtuple('SizeQuotaValue',
|
||||||
|
['id', 'name', 'service',
|
||||||
|
'value', 'current_quota',
|
||||||
|
'current_usage', 'percent'])
|
||||||
|
|
||||||
|
QUOTA_TASK = collections.namedtuple(
|
||||||
|
'QuotaTask',
|
||||||
|
['id', 'regions', 'size', 'user', 'created', 'valid', 'status'])
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(amelia): A list of quota names that we consider to be the most
|
||||||
|
# relevant to customers to be shown initially on the update page.
|
||||||
|
# These can be overriden in the local_settings file:
|
||||||
|
# IMPORTANT_QUOTAS = {<service>: [<quota_name>], }
|
||||||
|
DEFAULT_IMPORTANT_QUOTAS = {
|
||||||
|
'nova': [
|
||||||
|
'instances', 'cores', 'ram',
|
||||||
|
],
|
||||||
|
'cinder': [
|
||||||
|
'volumes', 'snapshots', 'gigabytes',
|
||||||
|
],
|
||||||
|
'neutron': [
|
||||||
|
'network', 'floatingip', 'router', 'security_group',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(adriant): Quotas that should be hidden by default.
|
||||||
|
# Can be overriden in the local_settings file by setting:
|
||||||
|
# HIDDEN_QUOTAS = {<service>: [<quota_name>], }
|
||||||
|
# or disabled entirely with: HIDDEN_QUOTAS = {}
|
||||||
|
DEFAULT_HIDDEN_QUOTAS = {
|
||||||
|
# these values have long since been deprecated from Nova
|
||||||
|
'nova': [
|
||||||
|
'security_groups', 'security_group_rules',
|
||||||
|
'floating_ips', 'fixed_ips',
|
||||||
|
],
|
||||||
|
# these by default have no limit
|
||||||
|
'cinder': [
|
||||||
|
'per_volume_gigabytes', 'volumes_lvmdriver-1',
|
||||||
|
'gigabytes_lvmdriver-1', 'snapshots_lvmdriver-1',
|
||||||
|
|
||||||
|
],
|
||||||
|
'neutron': [
|
||||||
|
'subnetpool',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_endpoint_url(request):
|
def _get_endpoint_url(request):
|
||||||
# If the request is made by an anonymous user, this endpoint request fails.
|
# If the request is made by an anonymous user, this endpoint request fails.
|
||||||
@ -287,12 +351,11 @@ def task_list(request, filters={}, page=1):
|
|||||||
more = resp['has_more']
|
more = resp['has_more']
|
||||||
for task in resp['tasks']:
|
for task in resp['tasks']:
|
||||||
tasklist.append(task_obj_get(request, task=task, page=page))
|
tasklist.append(task_obj_get(request, task=task, page=page))
|
||||||
|
return tasklist, prev, more
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(e)
|
LOG.error(e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return tasklist, prev, more
|
|
||||||
|
|
||||||
|
|
||||||
def task_get(request, task_id):
|
def task_get(request, task_id):
|
||||||
# Get a single task
|
# Get a single task
|
||||||
@ -364,3 +427,190 @@ def task_revalidate(request, task_id):
|
|||||||
data.update(action_data)
|
data.update(action_data)
|
||||||
|
|
||||||
return task_update(request, task_id, json.dumps(data))
|
return task_update(request, task_id, json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
# Quota management functions
|
||||||
|
def _is_quota_hidden(service, resource):
|
||||||
|
hidden_quotas = getattr(settings, 'HIDDEN_QUOTAS', None)
|
||||||
|
if hidden_quotas is None:
|
||||||
|
hidden_quotas = DEFAULT_HIDDEN_QUOTAS
|
||||||
|
return service in hidden_quotas and resource in hidden_quotas[service]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_quota_important(service, resource):
|
||||||
|
important_quotas = getattr(settings, 'IMPORTANT_QUOTAS', None)
|
||||||
|
if important_quotas is None:
|
||||||
|
important_quotas = DEFAULT_IMPORTANT_QUOTAS
|
||||||
|
return (
|
||||||
|
service in important_quotas and resource in important_quotas[service])
|
||||||
|
|
||||||
|
|
||||||
|
@memoized.memoized_method
|
||||||
|
def _get_quota_information(request, regions=None):
|
||||||
|
headers = {'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': request.user.token.id}
|
||||||
|
params = {}
|
||||||
|
if regions:
|
||||||
|
params = {'regions': regions}
|
||||||
|
try:
|
||||||
|
return get(request, 'openstack/quotas/',
|
||||||
|
params=params, headers=headers).json()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def quota_sizes_get(request, region=None):
|
||||||
|
# Gets the list of quota sizes, and a json blob defining what they
|
||||||
|
# have for each of the services
|
||||||
|
# Region param is useless here, but nedded for memoized decorator to work
|
||||||
|
quota_sizes_dict = {}
|
||||||
|
|
||||||
|
resp = _get_quota_information(request, regions=region)
|
||||||
|
|
||||||
|
for size_name, size in six.iteritems(resp['quota_sizes']):
|
||||||
|
quota_sizes_dict[size_name] = QUOTA_SIZE(
|
||||||
|
id=size_name,
|
||||||
|
name=size_name,
|
||||||
|
cinder=json.dumps(size['cinder'], indent=1),
|
||||||
|
nova=json.dumps(size['nova'], indent=1),
|
||||||
|
neutron=json.dumps(size['neutron'], indent=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
quota_sizes = []
|
||||||
|
for size in resp['quota_size_order']:
|
||||||
|
quota_sizes.append(quota_sizes_dict[size])
|
||||||
|
|
||||||
|
return quota_sizes
|
||||||
|
|
||||||
|
|
||||||
|
def size_details_get(request, size, region=None):
|
||||||
|
""" Gets the current details of the size as well as the current region's
|
||||||
|
quota
|
||||||
|
"""
|
||||||
|
quota_details = []
|
||||||
|
|
||||||
|
if not region:
|
||||||
|
region = request.user.services_region
|
||||||
|
resp = _get_quota_information(request, regions=region)
|
||||||
|
|
||||||
|
data = resp['quota_sizes'][size]
|
||||||
|
region_data = resp['regions'][0]['current_quota']
|
||||||
|
for service, values in six.iteritems(data):
|
||||||
|
for resource, value in six.iteritems(values):
|
||||||
|
if _is_quota_hidden(service, resource):
|
||||||
|
continue
|
||||||
|
|
||||||
|
usage = resp['regions'][0]['current_usage'][service].get(
|
||||||
|
resource)
|
||||||
|
try:
|
||||||
|
percent = float(usage)/value
|
||||||
|
except TypeError:
|
||||||
|
percent = '-'
|
||||||
|
|
||||||
|
quota_details.append(
|
||||||
|
SIZE_QUOTA_VALUE(
|
||||||
|
id=resource,
|
||||||
|
name=resource,
|
||||||
|
service=service,
|
||||||
|
value=value,
|
||||||
|
current_quota=region_data[service][resource],
|
||||||
|
current_usage=usage,
|
||||||
|
percent=percent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return quota_details
|
||||||
|
|
||||||
|
|
||||||
|
def quota_details_get(request, region):
|
||||||
|
quota_details = []
|
||||||
|
|
||||||
|
resp = _get_quota_information(request, regions=region)
|
||||||
|
|
||||||
|
data = resp['regions'][0]['current_quota']
|
||||||
|
|
||||||
|
for service, values in six.iteritems(data):
|
||||||
|
for name, value in six.iteritems(values):
|
||||||
|
if _is_quota_hidden(service, name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if value < 0:
|
||||||
|
value = 'No Limit'
|
||||||
|
usage = resp['regions'][0]['current_usage'][service].get(name)
|
||||||
|
try:
|
||||||
|
percent = float(usage)/value
|
||||||
|
except TypeError:
|
||||||
|
percent = '-'
|
||||||
|
|
||||||
|
size_blob = {}
|
||||||
|
for size_name, size_data in resp['quota_sizes'].iteritems():
|
||||||
|
size_blob[size_name] = size_data[service].get(name, '-')
|
||||||
|
|
||||||
|
if name != 'id':
|
||||||
|
quota_details.append(
|
||||||
|
REGION_QUOTA_VALUE(
|
||||||
|
id=name,
|
||||||
|
name=name,
|
||||||
|
service=service,
|
||||||
|
current_quota=value,
|
||||||
|
current_usage=usage,
|
||||||
|
percent=percent,
|
||||||
|
size_blob=size_blob,
|
||||||
|
important=_is_quota_important(service, name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return quota_details
|
||||||
|
|
||||||
|
|
||||||
|
def region_quotas_get(request, region=None):
|
||||||
|
quota_details = []
|
||||||
|
|
||||||
|
resp = _get_quota_information(request, regions=region)
|
||||||
|
|
||||||
|
data = resp['regions']
|
||||||
|
for region_values in data:
|
||||||
|
quota_details.append(
|
||||||
|
REGION_QUOTA(
|
||||||
|
id=region_values['region'],
|
||||||
|
region=region_values['region'],
|
||||||
|
quota_size=region_values['current_quota_size'],
|
||||||
|
preapproved_quotas=', '.join(region_values[
|
||||||
|
'quota_change_options'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return quota_details
|
||||||
|
|
||||||
|
|
||||||
|
def quota_tasks_get(request, region=None):
|
||||||
|
# Region param only used to help with memoized decorator
|
||||||
|
quota_tasks = []
|
||||||
|
|
||||||
|
resp = _get_quota_information(request, regions=region)
|
||||||
|
|
||||||
|
for task in resp['active_quota_tasks']:
|
||||||
|
quota_tasks.append(
|
||||||
|
QUOTA_TASK(
|
||||||
|
id=task['id'],
|
||||||
|
regions=', '.join(task['regions']),
|
||||||
|
size=task['size'],
|
||||||
|
user=task['request_user'],
|
||||||
|
created=task['task_created'].split("T")[0],
|
||||||
|
valid=task['valid'],
|
||||||
|
status=task['status'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return quota_tasks
|
||||||
|
|
||||||
|
|
||||||
|
def update_quotas(request, size, regions=[]):
|
||||||
|
headers = {'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': request.user.token.id}
|
||||||
|
data = {
|
||||||
|
'size': size,
|
||||||
|
}
|
||||||
|
if regions:
|
||||||
|
data['regions'] = regions
|
||||||
|
|
||||||
|
return post(request, 'openstack/quotas/',
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=headers)
|
||||||
|
0
adjutant_ui/content/quota/__init__.py
Normal file
0
adjutant_ui/content/quota/__init__.py
Normal file
66
adjutant_ui/content/quota/forms.py
Normal file
66
adjutant_ui/content/quota/forms.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Copyright 2016 Catalyst IT Ltd
|
||||||
|
#
|
||||||
|
# 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.core.urlresolvers import reverse # noqa
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import messages
|
||||||
|
|
||||||
|
from adjutant_ui.api import adjutant
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateQuotaForm(forms.SelfHandlingForm):
|
||||||
|
region = forms.CharField(label=_("Region"))
|
||||||
|
region.widget.attrs['readonly'] = True
|
||||||
|
size = forms.ChoiceField(label=_("Size"))
|
||||||
|
size.widget.attrs['onchange'] = 'updateSizeTable()'
|
||||||
|
|
||||||
|
failure_url = 'horizon:management:quota:index'
|
||||||
|
submit_url = 'horizon:management:quota:update'
|
||||||
|
success_url = "horizon:management:quota:index"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
size_choices = kwargs.pop('size_choices')
|
||||||
|
super(UpdateQuotaForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['size'].choices = size_choices
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
response = adjutant.update_quotas(request, data['size'],
|
||||||
|
regions=[data['region']])
|
||||||
|
if response.status_code == 200:
|
||||||
|
messages.success(request, _('Quota updated sucessfully.'))
|
||||||
|
elif response.status_code == 202:
|
||||||
|
messages.success(request, _('Task created but requires '
|
||||||
|
'admin approval.'))
|
||||||
|
elif response.status_code == 400:
|
||||||
|
messages.error(request, _('Failed to update quota. You may'
|
||||||
|
' have usage over the new values '
|
||||||
|
'that you are attempting to update'
|
||||||
|
' the quota to.'))
|
||||||
|
else:
|
||||||
|
messages.error(request, _('Failed to update quota.'))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
msg = _('Failed to update quota.')
|
||||||
|
url = reverse('horizon:management:quota:index')
|
||||||
|
exceptions.handle(request, msg, redirect=url)
|
||||||
|
return False
|
23
adjutant_ui/content/quota/panel.py
Normal file
23
adjutant_ui/content/quota/panel.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Copyright (c) 2016 Catalyst IT Ltd.
|
||||||
|
#
|
||||||
|
# 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 _
|
||||||
|
|
||||||
|
import horizon
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaPanel(horizon.Panel):
|
||||||
|
name = _("Quota Managment")
|
||||||
|
slug = 'quota'
|
||||||
|
policy_rules = (('identity', "identity:project_mod_or_admin"),)
|
189
adjutant_ui/content/quota/tables.py
Normal file
189
adjutant_ui/content/quota/tables.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# Copyright 2016 Catalyst IT Ltd
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ungettext_lazy
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.admin.defaults.tables import get_quota_name
|
||||||
|
|
||||||
|
from adjutant_ui.api import adjutant
|
||||||
|
|
||||||
|
|
||||||
|
def to_caps(value):
|
||||||
|
return value.title()
|
||||||
|
|
||||||
|
|
||||||
|
def display_as_percent(value):
|
||||||
|
if value == "-":
|
||||||
|
return value
|
||||||
|
return '{:.1%}'.format(value)
|
||||||
|
|
||||||
|
|
||||||
|
def service_name(value):
|
||||||
|
# Takes service names and returns a 'nice' name of where they
|
||||||
|
# are from
|
||||||
|
service_name_dict = {'cinder': 'Volume Storage',
|
||||||
|
'neutron': 'Networking',
|
||||||
|
'nova': 'Compute'}
|
||||||
|
return service_name_dict.get(value, value)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateQuota(tables.LinkAction):
|
||||||
|
name = "update"
|
||||||
|
verbose_name = _("Update Quota")
|
||||||
|
url = "horizon:management:quota:update"
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "edit"
|
||||||
|
|
||||||
|
|
||||||
|
class CancelQuotaTask(tables.DeleteAction):
|
||||||
|
help_text = _("This will cancel the selected quota update.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_present(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u"Cancel Quota Update",
|
||||||
|
u"Cancel Quota Updates",
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_past(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u"Cancelled Quota Update",
|
||||||
|
u"Cancelled Quota Updates",
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, request, obj_id):
|
||||||
|
result = adjutant.task_cancel(request, obj_id)
|
||||||
|
if not result or result.status_code != 200:
|
||||||
|
exception = exceptions.NotAvailable()
|
||||||
|
exception._safe_message = False
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
def allowed(self, request, task=None):
|
||||||
|
if task:
|
||||||
|
return task.status == "Awaiting Approval"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ViewRegion(tables.LinkAction):
|
||||||
|
name = "view_region"
|
||||||
|
verbose_name = _("View Region")
|
||||||
|
url = "horizon:management:quota:region_detail"
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSize(tables.LinkAction):
|
||||||
|
name = "view_size"
|
||||||
|
verbose_name = _("View Size")
|
||||||
|
url = "horizon:management:quota:size_detail"
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateQuotaRow(tables.Row):
|
||||||
|
def load_cells(self, resource=None):
|
||||||
|
super(UpdateQuotaRow, self).load_cells(resource)
|
||||||
|
resource = self.datum
|
||||||
|
if resource.important is False:
|
||||||
|
self.attrs['hide'] = True
|
||||||
|
self.attrs['style'] = 'display: none;'
|
||||||
|
|
||||||
|
self.attrs['size_blob'] = json.dumps(self.datum.size_blob)
|
||||||
|
|
||||||
|
|
||||||
|
class RegionQuotaDetailTable(tables.DataTable):
|
||||||
|
service = tables.Column("service", verbose_name=_("Service"),
|
||||||
|
filters=(service_name, ))
|
||||||
|
name = tables.Column(get_quota_name, verbose_name=_("Resource Name"),)
|
||||||
|
value = tables.Column("current_quota", verbose_name=_("Resource Quota"), )
|
||||||
|
usage = tables.Column("current_usage", verbose_name=_("Current Usage"))
|
||||||
|
percent = tables.Column("percent", verbose_name=_("Percentage of Use"),
|
||||||
|
filters=(display_as_percent, ))
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaDetailUsageTable(tables.DataTable):
|
||||||
|
service = tables.Column("service", verbose_name=_("Service"),
|
||||||
|
filters=(service_name, ))
|
||||||
|
name = tables.Column(get_quota_name, verbose_name=_("Resource Name"),)
|
||||||
|
value = tables.Column("value", verbose_name=_("Quota Value"), )
|
||||||
|
current_quota = tables.Column("current_quota",
|
||||||
|
verbose_name=_("Current Quota "
|
||||||
|
"(Current Region)"), )
|
||||||
|
|
||||||
|
|
||||||
|
class RegionOverviewTable(tables.DataTable):
|
||||||
|
region = tables.Column("region", verbose_name=_("Region Name"),
|
||||||
|
link=("horizon:management:quota:region_detail"))
|
||||||
|
quota_size = tables.Column("quota_size",
|
||||||
|
verbose_name=_("Current Quota Size"),
|
||||||
|
filters=(to_caps, ))
|
||||||
|
preapproved_quotas = tables.Column(
|
||||||
|
"preapproved_quotas", filters=(to_caps, ),
|
||||||
|
verbose_name=_("Preapproved Quota Sizes"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "region_overview"
|
||||||
|
row_actions = (UpdateQuota, ViewRegion)
|
||||||
|
verbose_name = _("Current Quotas")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaTasksTable(tables.DataTable):
|
||||||
|
quota_size = tables.Column(
|
||||||
|
"size",
|
||||||
|
verbose_name=_("Proposed Size"),
|
||||||
|
filters=(to_caps, ))
|
||||||
|
regions = tables.Column("regions", verbose_name=_("For Regions"))
|
||||||
|
user = tables.Column("user", verbose_name=_("Requested By"))
|
||||||
|
created = tables.Column("created", verbose_name=_("Requested On"))
|
||||||
|
valid = tables.Column("valid", verbose_name=_("Valid"))
|
||||||
|
stats = tables.Column("status", verbose_name=_("Status"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "quota_tasks"
|
||||||
|
row_actions = (CancelQuotaTask, )
|
||||||
|
verbose_name = _("Previous Quota Changes")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
class SizeOverviewTable(tables.DataTable):
|
||||||
|
id = tables.Column("id", hidden=True)
|
||||||
|
size = tables.Column("name", verbose_name=_("Size Name"),
|
||||||
|
filters=(to_caps, ))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "size_overview"
|
||||||
|
row_actions = (ViewSize, )
|
||||||
|
verbose_name = _("Quota Sizes")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSizeDisplayTable(tables.DataTable):
|
||||||
|
service = tables.Column("service", verbose_name=_("Service"),
|
||||||
|
filters=(service_name, ),
|
||||||
|
hidden=True)
|
||||||
|
name = tables.Column(get_quota_name, verbose_name=_("Resource"),)
|
||||||
|
current_quota = tables.Column("current_quota",
|
||||||
|
verbose_name=_("Current Quota"), )
|
||||||
|
usage = tables.Column("current_usage", verbose_name=_("Current Usage"))
|
||||||
|
value = tables.Column("value", verbose_name=_("New Quota Value"), )
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = 'change_size'
|
||||||
|
row_class = UpdateQuotaRow
|
18
adjutant_ui/content/quota/templates/quota/_index_help.html
Normal file
18
adjutant_ui/content/quota/templates/quota/_index_help.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class=quota-help>
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
Your current quotas are avaliable here, and can be changed to suit your needs.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
Certain types of quota changes, such as changing your quota more than once
|
||||||
|
in a given period, or changing your quota by large amounts will require admin
|
||||||
|
approval. The period and the quota sizes themselves is configured by your
|
||||||
|
admin, with the default period being 30 days.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
If your proposed change needed approval, you will be emailed on completion.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
</div>
|
62
adjutant_ui/content/quota/templates/quota/_update.html
Normal file
62
adjutant_ui/content/quota/templates/quota/_update.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block form_id %}update_quota_form{% endblock %}
|
||||||
|
{% block form_action %}{% url 'horizon:management:quota:update' region.id %}{% endblock %}
|
||||||
|
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
<div class="left">
|
||||||
|
<fieldset>
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<a id="toggle_link" onclick='toggleDisplayTable()'>{% trans 'Display all quotas' %}</a>
|
||||||
|
{{ change_size_table.render }}
|
||||||
|
<script>
|
||||||
|
var select = document.getElementById('id_size')
|
||||||
|
var toggle_link = document.getElementById('toggle_link')
|
||||||
|
var show_all = false;
|
||||||
|
|
||||||
|
var table = document.getElementById("change_size");
|
||||||
|
var count_headings = table.getElementsByClassName("table_count");
|
||||||
|
|
||||||
|
for (i=0; i<count_headings.length; i++){
|
||||||
|
count_headings[i].parentElement.parentElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSizeTable(){
|
||||||
|
var current_size = select.options[select.selectedIndex].value;
|
||||||
|
var rows = document.getElementById("change_size").tBodies[0].rows;
|
||||||
|
for (i=1; i < rows.length; i++){
|
||||||
|
var cell = rows[i].cells[4];
|
||||||
|
var value_dict = $.parseJSON(rows[i].getAttribute('size_blob'));
|
||||||
|
cell.innerHTML = value_dict[current_size]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDisplayTable(){
|
||||||
|
show_all = !show_all;
|
||||||
|
|
||||||
|
if(show_all){
|
||||||
|
toggle_link.innerHTML = "{% trans 'Hide some rows' %}";
|
||||||
|
}else{
|
||||||
|
toggle_link.innerHTML = "{% trans 'Display all quotas' %}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var tr = document.getElementById("change_size").tBodies[0].rows;
|
||||||
|
for (i=0; i<tr.length; i++){
|
||||||
|
if (tr[i].getAttribute('hide') == ""){
|
||||||
|
if (show_all){
|
||||||
|
tr[i].style.display = '';
|
||||||
|
} else {
|
||||||
|
tr[i].style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSizeTable()
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
27
adjutant_ui/content/quota/templates/quota/index.html
Normal file
27
adjutant_ui/content/quota/templates/quota/index.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Quotas" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Quotas") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="left">
|
||||||
|
{% include 'management/quota/_index_help.html' %}
|
||||||
|
<div id="region-overview">
|
||||||
|
{{ region_overview_table.render }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="quota-tasks-overview">
|
||||||
|
{{ quota_tasks_table.render }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
|
||||||
|
<div id="size-overview">
|
||||||
|
{{ size_overview_table.render }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %} {% trans "Quota Details" %} {% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{{ table.render }}
|
||||||
|
{% endblock %}
|
11
adjutant_ui/content/quota/templates/quota/size_detail.html
Normal file
11
adjutant_ui/content/quota/templates/quota/size_detail.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %} {{title}} - Quota Details{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=title %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{{ table.render }}
|
||||||
|
{% endblock %}
|
6
adjutant_ui/content/quota/templates/quota/update.html
Normal file
6
adjutant_ui/content/quota/templates/quota/update.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Update Quota" %}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
{% include 'management/quota/_update.html' %}
|
||||||
|
{% endblock %}
|
28
adjutant_ui/content/quota/urls.py
Normal file
28
adjutant_ui/content/quota/urls.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Copyright (c) 2016 Catalyst IT Ltd.
|
||||||
|
#
|
||||||
|
# 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 import url
|
||||||
|
|
||||||
|
from adjutant_ui.content.quota import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^region/(?P<region>[^/]+)$', views.RegionDetailView.as_view(),
|
||||||
|
name='region_detail'),
|
||||||
|
url(r'^(?P<region>[^/]+)/update$',
|
||||||
|
views.RegionUpdateView.as_view(), name='update'),
|
||||||
|
url(r'^size/(?P<size>[^/]+)$', views.QuotaSizeView.as_view(),
|
||||||
|
name='size_detail'),
|
||||||
|
]
|
159
adjutant_ui/content/quota/views.py
Normal file
159
adjutant_ui/content/quota/views.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Copyright (c) 2016 Catalyst IT Ltd.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import tables as horizon_tables
|
||||||
|
|
||||||
|
from adjutant_ui.content.quota import forms as quota_forms
|
||||||
|
from adjutant_ui.content.quota import tables as quota_tables
|
||||||
|
from adjutant_ui.api import adjutant
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(horizon_tables.MultiTableView):
|
||||||
|
page_title = _("Quota Management")
|
||||||
|
table_classes = (quota_tables.RegionOverviewTable,
|
||||||
|
quota_tables.SizeOverviewTable,
|
||||||
|
quota_tables.QuotaTasksTable)
|
||||||
|
template_name = 'management/quota/index.html'
|
||||||
|
|
||||||
|
def get_region_overview_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.region_quotas_get(self.request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota sizes.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_size_overview_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.quota_sizes_get(self.request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota sizes.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_quota_tasks_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.quota_tasks_get(self.request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota tasks.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class RegionDetailView(horizon_tables.DataTableView):
|
||||||
|
table_class = quota_tables.RegionQuotaDetailTable
|
||||||
|
template_name = 'management/quota/region_detail.html'
|
||||||
|
page_title = _("'{{ region }}' Quota Details")
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.quota_details_get(self.request,
|
||||||
|
self.kwargs['region'])
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota sizes.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RegionDetailView, self).get_context_data(**kwargs)
|
||||||
|
context['region'] = self.kwargs['region']
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaSizeView(horizon_tables.DataTableView):
|
||||||
|
table_class = quota_tables.QuotaDetailUsageTable
|
||||||
|
template_name = 'management/quota/size_detail.html'
|
||||||
|
page_title = _("'{{ size }}' Quota Details")
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.size_details_get(self.request,
|
||||||
|
size=self.kwargs['size'])
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota size.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
# request.user.services_region
|
||||||
|
context = super(QuotaSizeView, self).get_context_data(**kwargs)
|
||||||
|
context['title'] = _("%s - Quota Details") \
|
||||||
|
% self.kwargs['size'].title()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class RegionUpdateView(forms.ModalFormView, horizon_tables.MultiTableView):
|
||||||
|
form_class = quota_forms.UpdateQuotaForm
|
||||||
|
table_classes = (quota_tables.ChangeSizeDisplayTable, )
|
||||||
|
submit_url = 'horizon:management:quota:update'
|
||||||
|
context_object_name = 'ticket'
|
||||||
|
template_name = 'management/quota/update.html'
|
||||||
|
success_url = reverse_lazy("horizon:management:quota:index")
|
||||||
|
page_title = _("Update Quota")
|
||||||
|
|
||||||
|
def get_change_size_data(self):
|
||||||
|
try:
|
||||||
|
return adjutant.quota_details_get(self.request,
|
||||||
|
region=self.kwargs['region'])
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request, _('Failed to list quota sizes.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return adjutant.region_quotas_get(self.request,
|
||||||
|
region=self.kwargs['region'])[0]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RegionUpdateView, self).get_context_data(**kwargs)
|
||||||
|
context['region'] = self.get_object()
|
||||||
|
args = (self.kwargs['region'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
context['form'] = self.get_form()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super(RegionUpdateView, self).get_form_kwargs()
|
||||||
|
sizes = adjutant.quota_sizes_get(
|
||||||
|
self.request, region=self.kwargs['region'])
|
||||||
|
kwargs['size_choices'] = []
|
||||||
|
|
||||||
|
region = self.get_object()
|
||||||
|
for size in sizes:
|
||||||
|
if region.quota_size == size.name:
|
||||||
|
continue
|
||||||
|
if size.name not in region.preapproved_quotas:
|
||||||
|
kwargs['size_choices'].append(
|
||||||
|
[size.id, "%s (requires approval)" % size.name.title()])
|
||||||
|
else:
|
||||||
|
kwargs['size_choices'].append([size.id, size.name.title()])
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
region = self.get_object()
|
||||||
|
data = {'id': region.id,
|
||||||
|
'region': region.region,
|
||||||
|
'quota_size': region.quota_size,
|
||||||
|
'preapproved_quotas': region.preapproved_quotas
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# NOTE(amelia): The multitableview overides the form view post
|
||||||
|
# this reinstates it.
|
||||||
|
form = self.get_form()
|
||||||
|
if form.is_valid():
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
9
adjutant_ui/enabled/_6080_management_quota.py
Normal file
9
adjutant_ui/enabled/_6080_management_quota.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'quota'
|
||||||
|
# The slug of the dashboard the PANEL associated with. Required.
|
||||||
|
PANEL_DASHBOARD = 'management'
|
||||||
|
|
||||||
|
PANEL_GROUP = 'default'
|
||||||
|
|
||||||
|
# Python panel class of the PANEL to be added.
|
||||||
|
ADD_PANEL = 'adjutant_ui.content.quota.panel.QuotaPanel'
|
Loading…
x
Reference in New Issue
Block a user