diff --git a/adjutant_ui/api/adjutant.py b/adjutant_ui/api/adjutant.py index ee9f853..0b1f32b 100644 --- a/adjutant_ui/api/adjutant.py +++ b/adjutant_ui/api/adjutant.py @@ -17,10 +17,12 @@ import json import logging import requests from six.moves.urllib.parse import urljoin +import six from django.conf import settings from horizon.utils import functions as utils +from horizon.utils import memoized from openstack_dashboard.api import base @@ -37,6 +39,68 @@ TASK = collections.namedtuple('Task', 'created_on', 'approved_on', 'page', '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 = {: [], } +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 = {: [], } +# 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): # 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'] for task in resp['tasks']: tasklist.append(task_obj_get(request, task=task, page=page)) + return tasklist, prev, more except Exception as e: LOG.error(e) raise - return tasklist, prev, more - def task_get(request, task_id): # Get a single task @@ -364,3 +427,190 @@ def task_revalidate(request, task_id): data.update(action_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) diff --git a/adjutant_ui/content/quota/__init__.py b/adjutant_ui/content/quota/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adjutant_ui/content/quota/forms.py b/adjutant_ui/content/quota/forms.py new file mode 100644 index 0000000..f36a203 --- /dev/null +++ b/adjutant_ui/content/quota/forms.py @@ -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 diff --git a/adjutant_ui/content/quota/panel.py b/adjutant_ui/content/quota/panel.py new file mode 100644 index 0000000..2639d65 --- /dev/null +++ b/adjutant_ui/content/quota/panel.py @@ -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"),) diff --git a/adjutant_ui/content/quota/tables.py b/adjutant_ui/content/quota/tables.py new file mode 100644 index 0000000..d0e98e2 --- /dev/null +++ b/adjutant_ui/content/quota/tables.py @@ -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 diff --git a/adjutant_ui/content/quota/templates/quota/_index_help.html b/adjutant_ui/content/quota/templates/quota/_index_help.html new file mode 100644 index 0000000..0d69bb3 --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/_index_help.html @@ -0,0 +1,18 @@ +{% load i18n %} + +
+

{% blocktrans trimmed %} + Your current quotas are avaliable here, and can be changed to suit your needs. + {% endblocktrans %}

+ +

{% 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 %}

+ +

{% blocktrans trimmed %} + If your proposed change needed approval, you will be emailed on completion. + {% endblocktrans %}

+
diff --git a/adjutant_ui/content/quota/templates/quota/_update.html b/adjutant_ui/content/quota/templates/quota/_update.html new file mode 100644 index 0000000..bff6752 --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/_update.html @@ -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 %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+ {% trans 'Display all quotas' %} + {{ change_size_table.render }} + +
+{% endblock %} diff --git a/adjutant_ui/content/quota/templates/quota/index.html b/adjutant_ui/content/quota/templates/quota/index.html new file mode 100644 index 0000000..6777d53 --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/index.html @@ -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 %} +
+ {% include 'management/quota/_index_help.html' %} +
+ {{ region_overview_table.render }} +
+ +
+ {{ quota_tasks_table.render }} +
+
+
+ +
+ {{ size_overview_table.render }} +
+
+ +{% endblock %} diff --git a/adjutant_ui/content/quota/templates/quota/region_detail.html b/adjutant_ui/content/quota/templates/quota/region_detail.html new file mode 100644 index 0000000..cd9212d --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/region_detail.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %} {% trans "Quota Details" %} {% endblock %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/adjutant_ui/content/quota/templates/quota/size_detail.html b/adjutant_ui/content/quota/templates/quota/size_detail.html new file mode 100644 index 0000000..87aa1b9 --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/size_detail.html @@ -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 %} diff --git a/adjutant_ui/content/quota/templates/quota/update.html b/adjutant_ui/content/quota/templates/quota/update.html new file mode 100644 index 0000000..941aeef --- /dev/null +++ b/adjutant_ui/content/quota/templates/quota/update.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Quota" %}{% endblock %} +{% block main %} + {% include 'management/quota/_update.html' %} +{% endblock %} diff --git a/adjutant_ui/content/quota/urls.py b/adjutant_ui/content/quota/urls.py new file mode 100644 index 0000000..f484727 --- /dev/null +++ b/adjutant_ui/content/quota/urls.py @@ -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[^/]+)$', views.RegionDetailView.as_view(), + name='region_detail'), + url(r'^(?P[^/]+)/update$', + views.RegionUpdateView.as_view(), name='update'), + url(r'^size/(?P[^/]+)$', views.QuotaSizeView.as_view(), + name='size_detail'), +] diff --git a/adjutant_ui/content/quota/views.py b/adjutant_ui/content/quota/views.py new file mode 100644 index 0000000..7038cb0 --- /dev/null +++ b/adjutant_ui/content/quota/views.py @@ -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) diff --git a/adjutant_ui/enabled/_6080_management_quota.py b/adjutant_ui/enabled/_6080_management_quota.py new file mode 100644 index 0000000..92540ff --- /dev/null +++ b/adjutant_ui/enabled/_6080_management_quota.py @@ -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'