Use EXTRA_TABS/EXTRA_STEPS mechanism to handle manila quotas

The previous manila-ui approach to show shared-file-system default
quotas is too hacky and fragile as the bug reveals.
This commit applies EXTRA_TABS/EXTRA_STEPS plugin interface
introduced to horizon in Rocky to show the manila quota tab
in the default quotas table and quota related forms.

monkey-patching from manila quotas has been dropped.

Note that EXTRA_STEPS feature is being reviewd in horizon
https://review.openstack.org/#/c/560679/ so workflow steps related to
manila in the project quota form and the default quota form are not
shown until the corresponding horizon feature is merged.
However, this commit will unblock manila-ui CI so I believe this is
a good compromise.

Note that manila support in the project overview panel is dropped
temporarily as further refactoring in the project overview panel
is being done in horizon side (which will land in Rocky-2).

Closes-Bug: #1759340
Co-Authored-By: Victoria Martinez de la Cruz <victoria@redhat.com>
Change-Id: I22ed7d757c5e5e902f1e85a15c33b34de6c609f1
This commit is contained in:
Akihiro Motoki 2018-03-28 06:11:02 +09:00
parent d778746ef0
commit 6b4acf19a7
11 changed files with 258 additions and 414 deletions

View File

@ -39,6 +39,14 @@ MANILA_SERVICE_TYPE = "sharev2"
SHARE_STATE_AVAILABLE = "available"
DEFAULT_QUOTA_NAME = 'default'
MANILA_QUOTA_FIELDS = {
"shares",
"share_gigabytes",
"share_snapshots",
"share_snapshot_gigabytes",
"share_networks",
}
def manilaclient(request):
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)

View File

@ -0,0 +1,51 @@
# 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 _
from horizon import tables
from openstack_dashboard.dashboards.admin.defaults import tables as default_tbl
MANILA_QUOTA_NAMES = {
'shares': _('Shares'),
'gigabytes': _('Share gigabytes'),
'snapshots': _('Share snapshots'),
'snapshot_gigabytes': _('Share snapshot gigabytes'),
'share_networks': _('Shares Networks'),
}
def get_quota_name(quota):
return MANILA_QUOTA_NAMES.get(quota.name,
quota.name.replace("_", " ").title())
class UpdateDefaultShareQuotas(default_tbl.UpdateDefaultQuotas):
name = 'update_share_defaults'
step = 'update_default_share_quotas'
class ShareQuotasTable(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(object):
name = "share_quotas"
verbose_name = _("Shared Quotas")
table_actions = (default_tbl.QuotaFilterAction,
UpdateDefaultShareQuotas)
multi_select = False

View File

@ -0,0 +1,42 @@
# 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 _
from horizon import exceptions
from horizon import tabs
from openstack_dashboard import api
from manila_ui.api import manila as api_manila
from manila_ui.dashboards.admin.defaults import tables
class ShareQuotasTab(tabs.TableTab):
table_classes = (tables.ShareQuotasTable,)
name = _("Share Quotas")
slug = "shared_quotas"
template_name = ("horizon/common/_detail_table.html")
def get_share_quotas_data(self):
request = self.tab_group.request
tenant_id = request.user.tenant_id
try:
data = api_manila.default_quota_get(request, tenant_id)
except Exception:
data = []
exceptions.handle(self.request,
_('Unable to get manila default quota.'))
return data
def allowed(self, request):
return api.base.is_service_enabled(request, 'share')

View File

@ -0,0 +1,80 @@
# 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 _
from horizon import exceptions
from horizon import forms
from horizon import workflows
from openstack_dashboard.api import base
from manila_ui.api import manila as api_manila
class UpdateDefaultShareQuotasAction(workflows.Action):
shares = forms.IntegerField(min_value=-1, label=_("Shares"))
share_gigabytes = forms.IntegerField(
min_value=-1, label=_("Share gigabytes"))
share_snapshots = forms.IntegerField(
min_value=-1, label=_("Share snapshots"))
share_snapshot_gigabytes = forms.IntegerField(
min_value=-1, label=_("Share snapshot gigabytes"))
share_networks = forms.IntegerField(
min_value=-1, label=_("Share Networks"))
def __init__(self, request, context, *args, **kwargs):
super(UpdateDefaultShareQuotasAction, self).__init__(
request, context, *args, **kwargs)
disabled_quotas = context['disabled_quotas']
for field in disabled_quotas:
if field in self.fields:
self.fields[field].required = False
self.fields[field].widget = forms.HiddenInput()
def handle(self, request, data):
try:
if base.is_service_enabled(request, 'share'):
manila_data = dict([(key, data[key]) for key in
api_manila.MANILA_QUOTA_FIELDS])
api_manila.default_quota_update(request, **manila_data)
return True
except Exception:
exceptions.handle(request,
_('Unable to update default quotas.'))
return False
class Meta(object):
name = _("Share")
slug = 'update_default_share_quotas'
help_text = _("From here you can update the default share quotas "
"(max limits).")
class UpdateDefaultShareQuotasStep(workflows.Step):
action_class = UpdateDefaultShareQuotasAction
contributes = api_manila.MANILA_QUOTA_FIELDS
depends_on = ('disabled_quotas',)
def prepare_action_context(self, request, context):
try:
quota_defaults = api_manila.default_quota_get(
request, request.user.tenant_id)
for field in api_manila.MANILA_QUOTA_FIELDS:
context[field] = quota_defaults.get(field).limit
except Exception:
exceptions.handle(request,
_('Unable to retrieve default share quotas.'))
return context
def allowed(self, request):
return base.is_service_enabled(request, 'share')

View File

@ -0,0 +1,54 @@
# 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 _
from horizon import forms
from horizon import workflows
from openstack_dashboard.api import base
from openstack_dashboard.dashboards.identity.projects \
import workflows as project_workflows
from manila_ui.api import manila as api_manila
class ShareQuotaAction(project_workflows.CommonQuotaAction):
shares = forms.IntegerField(min_value=-1, label=_("Shares"))
share_gigabytes = forms.IntegerField(
min_value=-1, label=_("Share gigabytes"))
share_snapshots = forms.IntegerField(
min_value=-1, label=_("Share snapshots"))
share_snapshot_gigabytes = forms.IntegerField(
min_value=-1, label=_("Share snapshot gigabytes"))
share_networks = forms.IntegerField(
min_value=-1, label=_("Share Networks"))
_quota_fields = api_manila.MANILA_QUOTA_FIELDS
def _tenant_quota_update(self, request, project_id, data):
api_manila.tenant_quota_update(request, project_id, **data)
class Meta(object):
name = _("Share")
slug = 'update_share_quotas'
help_text = _("Set maximum quotas for the project.")
permissions = ('openstack.roles.admin', 'openstack.services.share')
class UpdateShareQuota(workflows.Step):
action_class = ShareQuotaAction
depends_on = ("project_id", "disabled_quotas")
contributes = api_manila.MANILA_QUOTA_FIELDS
def allowed(self, request):
return base.is_service_enabled(request, 'share')

View File

@ -10,343 +10,21 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
import sys
from django.utils.translation import ugettext_lazy as _
import horizon
from horizon import exceptions
from openstack_dashboard.api import base
from openstack_dashboard.usage import base as usage
from manila_ui.api import manila
from openstack_dashboard.api import base
from openstack_dashboard.dashboards.admin.defaults import tables as quota_tbl
from openstack_dashboard.dashboards.admin.defaults import views \
as default_views
from openstack_dashboard.dashboards.admin.defaults import workflows \
as default_workflows
from openstack_dashboard.dashboards.identity.projects import views \
as project_views
from openstack_dashboard.dashboards.identity.projects import workflows \
as project_workflows
from openstack_dashboard.dashboards.project.overview import views \
as overview_views
from openstack_dashboard.usage import base as usage_base
from openstack_dashboard.usage import quotas
def wrap(orig_func):
"""decorator to wrap an existing function
Modified post from http://downgra.de/2009/05/16/python-monkey-patching/
to work with functions
e.g.
@wrap(quotas.tenant_limit_usages)
def tenant_limit_usages(orig, self):
limits = orig(request)
limits['disksUsed'] = 100
return limits
the first parameter of the new function is the the original,
overwritten function ('orig').
"""
def outer(new_func):
@functools.wraps(orig_func)
def wrapper(*args, **kwargs):
return new_func(orig_func, *args, **kwargs)
# Replace the original function in the module with the wrapper
orig_module = sys.modules[orig_func.__module__]
setattr(orig_module, orig_func.__name__, wrapper)
return wrapper
return outer
# All public methods in openstack_dashboard.usage.quotas are hardcoded to
# only look at a fixed set of services (nova, cinder, etc.). In order to
# incorporate manila quotas and usage, all public functions must be
# monkey-patched to add that information in.
MANILA_QUOTA_FIELDS = {
"shares",
"share_gigabytes",
"share_snapshots",
"share_snapshot_gigabytes",
"share_networks",
}
MANILA_QUOTA_NAMES = {
'shares': _('Shares'),
'share_gigabytes': _('Share gigabytes'),
'share_snapshots': _('Share snapshots'),
'share_snapshot_gigabytes': _('Share snapshot gigabytes'),
'share_networks': _('Shares Networks'),
}
quotas.QUOTA_FIELDS = quotas.QUOTA_FIELDS | MANILA_QUOTA_FIELDS
def _get_manila_disabled_quotas(request):
disabled_quotas = []
if not base.is_service_enabled(request, 'share'):
disabled_quotas.extend(MANILA_QUOTA_FIELDS)
return disabled_quotas
def _get_manila_quota_data(request, method_name, disabled_quotas=None,
tenant_id=None):
if not tenant_id:
tenant_id = request.user.tenant_id
if disabled_quotas is None:
disabled_quotas = _get_manila_disabled_quotas(request)
if 'shares' not in disabled_quotas:
manila_quotas = getattr(manila, method_name)(request, tenant_id)
for quota in manila_quotas:
if quota.name == 'gigabytes':
quota.name = 'share_gigabytes'
elif quota.name == 'snapshots':
quota.name = 'share_snapshots'
elif quota.name == 'snapshot_gigabytes':
quota.name = 'share_snapshot_gigabytes'
return manila_quotas
else:
return None
@wrap(quotas.get_default_quota_data)
def get_default_quota_data(f, request, disabled_quotas=None, tenant_id=None):
qs = f(request, disabled_quotas, tenant_id)
manila_quota = _get_manila_quota_data(request, "default_quota_get",
disabled_quotas=disabled_quotas,
tenant_id=tenant_id)
if manila_quota:
qs.add(manila_quota)
return qs
@wrap(quotas.get_tenant_quota_data)
def get_tenant_quota_data(f, request, disabled_quotas=None, tenant_id=None):
qs = f(request, disabled_quotas, tenant_id)
manila_quota = _get_manila_quota_data(request, "tenant_quota_get",
disabled_quotas=disabled_quotas,
tenant_id=tenant_id)
if manila_quota:
qs.add(manila_quota)
return qs
@wrap(quotas.get_disabled_quotas)
def get_disabled_quotas(f, request):
disabled_quotas = f(request)
disabled_quotas.update(_get_manila_disabled_quotas(request))
return disabled_quotas
@wrap(quotas.tenant_quota_usages)
def tenant_quota_usages(f, request, tenant_id=None, targets=None):
usages = f(request, tenant_id, targets)
if 'shares' not in _get_manila_disabled_quotas(request):
shares = manila.share_list(request)
snapshots = manila.share_snapshot_list(request)
sn_l = manila.share_network_list(request)
gig_s = sum([int(v.size) for v in shares])
gig_ss = sum([int(v.size) for v in snapshots])
usages.tally('shares', len(shares))
usages.tally('share_gigabytes', gig_s)
usages.tally('share_snapshots', len(snapshots))
usages.tally('share_snapshot_gigabytes', gig_ss)
usages.tally('share_networks', len(sn_l))
return usages
@wrap(quotas.tenant_limit_usages)
def tenant_limit_usages(f, request):
limits = f(request)
if base.is_service_enabled(request, 'share'):
try:
limits.update(manila.tenant_absolute_limits(request))
shares = manila.share_list(request)
snapshots = manila.share_snapshot_list(request)
share_networks = manila.share_network_list(request)
total_s_size = sum([getattr(share, 'size', 0) for share in shares])
total_ss_size = sum([getattr(ss, 'size', 0) for ss in snapshots])
limits['totalSharesUsed'] = len(shares)
limits['totalShareGigabytesUsed'] = total_s_size
limits['totalSnapshotsUsed'] = len(snapshots)
limits['totalSnapshotGigabytesUsed'] = total_ss_size
limits['totalShareNetworksUsed'] = len(share_networks)
except Exception:
msg = _("Unable to retrieve share limit information.")
horizon.exceptions.handle(request, msg)
return limits
@wrap(quota_tbl.get_quota_name)
def get_quota_name(f, quota):
if quota.name in MANILA_QUOTA_NAMES:
return MANILA_QUOTA_NAMES.get(quota.name)
else:
return f(quota)
#
# Add manila fields to Admin/Defaults/Update Defaults
# Add extra pie charts to project/compute overview
#
class ManilaUpdateDefaultQuotaAction(
default_workflows.UpdateDefaultQuotasAction):
shares = horizon.forms.IntegerField(min_value=-1, label=_("Shares"))
share_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share gigabytes"))
share_snapshots = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshots"))
share_snapshot_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshot gigabytes"))
share_networks = horizon.forms.IntegerField(
min_value=-1, label=_("Share Networks"))
class Meta(object):
name = _("Default Quotas")
slug = 'update_default_quotas'
help_text = _("From here you can update the default quotas "
"(max limits).")
class ManilaUpdateDefaultQuotasStep(default_workflows.UpdateDefaultQuotasStep):
action_class = ManilaUpdateDefaultQuotaAction
contributes = quotas.QUOTA_FIELDS
class ManilaUpdateDefaultQuotas(default_workflows.UpdateDefaultQuotas):
default_steps = (ManilaUpdateDefaultQuotasStep,)
def handle(self, request, data):
try:
super(ManilaUpdateDefaultQuotas, self).handle(request, data)
if base.is_service_enabled(request, 'share'):
manila_data = dict([(key, data[key]) for key in
MANILA_QUOTA_FIELDS])
manila.default_quota_update(request, **manila_data)
except Exception:
horizon.exceptions.handle(request,
_('Unable to update default quotas.'))
return True
default_views.UpdateDefaultQuotasView.workflow_class = (
ManilaUpdateDefaultQuotas)
#
# Add manila fields to Identity/Projects/Modify Quotas
#
class ManilaUpdateProjectQuotaAction(
project_workflows.UpdateProjectQuotaAction):
shares = horizon.forms.IntegerField(min_value=-1, label=_("Shares"))
share_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share gigabytes"))
share_snapshots = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshots"))
share_snapshot_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshot gigabytes"))
share_networks = horizon.forms.IntegerField(
min_value=-1, label=_("Share Networks"))
class Meta(object):
name = _("Quota")
slug = 'update_quotas'
help_text = _("Set maximum quotas for the project.")
project_workflows.UpdateProjectQuota.action_class = (
ManilaUpdateProjectQuotaAction)
project_workflows.UpdateProjectQuota.contributes = quotas.QUOTA_FIELDS
class ManilaUpdateProject(project_workflows.UpdateProject):
def handle(self, request, data):
try:
super(ManilaUpdateProject, self).handle(request, data)
if base.is_service_enabled(request, 'share'):
manila_data = dict([(key, data[key]) for key in
MANILA_QUOTA_FIELDS])
manila.tenant_quota_update(request,
data['project_id'],
**manila_data)
except Exception:
horizon.exceptions.handle(request,
_('Modified project information and '
'members, but unable to modify '
'project quotas.'))
return True
project_views.UpdateProjectView.workflow_class = ManilaUpdateProject
#
# Add manila fields to Identity/Projects/Create Project
#
class ManilaCreateProjectQuotaAction(
project_workflows.CreateProjectQuotaAction):
shares = horizon.forms.IntegerField(min_value=-1, label=_("Shares"))
share_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share gigabytes"))
share_snapshots = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshots"))
share_snapshot_gigabytes = horizon.forms.IntegerField(
min_value=-1, label=_("Share snapshot gigabytes"))
share_networks = horizon.forms.IntegerField(
min_value=-1, label=_("Share Networks"))
class Meta(object):
name = _("Quota")
slug = 'create_quotas'
help_text = _("Set maximum quotas for the project.")
project_workflows.CreateProjectQuota.action_class = (
ManilaCreateProjectQuotaAction)
project_workflows.CreateProjectQuota.contributes = quotas.QUOTA_FIELDS
class ManilaCreateProject(project_workflows.CreateProject):
def handle(self, request, data):
try:
super(ManilaCreateProject, self).handle(request, data)
if base.is_service_enabled(request, 'share'):
manila_data = dict([(key, data[key]) for key in
MANILA_QUOTA_FIELDS])
manila.tenant_quota_update(request,
self.object.id,
**manila_data)
except Exception:
horizon.exceptions.handle(request,
_('Unable to set project quotas.'))
return True
project_views.CreateProjectView.workflow_class = ManilaCreateProject
#
# Add extra pie charts to Project/Compute Overview
#
class ManilaUsage(usage_base.ProjectUsage):
class ManilaUsage(usage.ProjectUsage):
def get_manila_limits(self):
"""Get share limits if manila is enabled."""
@ -356,7 +34,7 @@ class ManilaUsage(usage_base.ProjectUsage):
self.limits.update(manila.tenant_absolute_limits(self.request))
except Exception:
msg = _("Unable to retrieve share limit information.")
horizon.exceptions.handle(self.request, msg)
exceptions.handle(self.request, msg)
return
def get_limits(self):
@ -387,7 +65,3 @@ def get_context_data(self, **kwargs):
'text': False,
})
return context
overview_views.ProjectOverview.get_context_data = get_context_data
overview_views.ProjectOverview.usage_class = ManilaUsage

View File

@ -18,3 +18,19 @@ PANEL_GROUP = 'share'
PANEL_GROUP_NAME = 'Share'
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'admin'
EXTRA_TABS = {
'openstack_dashboard.dashboards.admin.defaults.tabs.DefaultsTabs': (
'manila_ui.dashboards.admin.defaults.tabs.ShareQuotasTab',
),
}
EXTRA_STEPS = {
'openstack_dashboard.dashboards.identity.projects.workflows.UpdateQuota': (
'manila_ui.dashboards.identity.projects.workflows.UpdateShareQuota',
),
'openstack_dashboard.dashboards.admin.defaults.workflows.'
'UpdateDefaultQuotas': (
'manila_ui.dashboards.admin.defaults.workflows.'
'UpdateDefaultShareQuotasStep',
),
}

View File

@ -12,14 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
from django.core.urlresolvers import reverse
from django.utils import translation
import mock
from openstack_dashboard.api import base
from openstack_dashboard.usage import quotas
from manila_ui.api import manila as api_manila
from manila_ui.dashboards.project import shares
from manila_ui.tests import helpers as test
@ -85,80 +81,3 @@ class PieChartsTests(test.TestCase):
"text": chart["text"]},
expected_charts.pop(name, "NotFound")
)
@ddt.ddt
class QuotaTests(test.TestCase):
def test_get_disabled_quotas(self):
self.mock_object(
base, "is_service_enabled", mock.Mock(return_value=False))
result_quotas = quotas.get_disabled_quotas(self.request)
expected_quotas = set(quotas.QUOTA_FIELDS)
self.assertItemsEqual(result_quotas, expected_quotas)
@ddt.data(
shares.ManilaUpdateDefaultQuotaAction,
shares.ManilaUpdateProjectQuotaAction,
shares.ManilaCreateProjectQuotaAction,
)
def test_manila_quota_action(self, class_ref):
self.mock_object(
quotas, 'get_disabled_quotas', mock.Mock(return_value=[]))
class_instance = class_ref(self.request, 'foo')
expected_fields = set([
'shares', 'share_gigabytes', 'share_snapshots',
'share_snapshot_gigabytes', 'share_networks',
])
# NOTE(vponomaryov): iterate over reversed list of visible fields
# because manila's fields are at the end always.
for vf in reversed(class_instance.visible_fields()):
if expected_fields and vf.name in expected_fields:
self.assertEqual(-1, vf.field.min_value)
self.assertIsInstance(
vf.field, shares.horizon.forms.IntegerField)
expected_fields.remove(vf.name)
self.assertSetEqual(set([]), expected_fields)
self.assertTrue(quotas.get_disabled_quotas.called)
@ddt.data('default_quota_get', 'tenant_quota_get')
def test__get_manila_quota_data(self, method_name):
fake_quotas = [
type('Fake', (object, ), {'name': name})
for name in ('gigabytes', 'snapshots', 'snapshot_gigabytes')
]
self.mock_object(
api_manila, method_name, mock.Mock(return_value=fake_quotas))
self.mock_object(
shares, '_get_manila_disabled_quotas',
mock.Mock(return_value=[]))
result = shares._get_manila_quota_data(
self.request, method_name)
expected = [
'share_gigabytes',
'share_snapshot_gigabytes',
'share_snapshots',
]
self.assertEqual(3, len(result))
self.assertEqual(
expected,
sorted([element.name for element in result]))
getattr(api_manila, method_name).assert_called_once_with(
self.request, self.request.user.tenant_id)
shares._get_manila_disabled_quotas.asssert_called_once_with(
self.request)
def test_manila_quota_fields(self):
expected_fields = (
"shares",
"share_gigabytes",
"share_snapshots",
"share_snapshot_gigabytes",
"share_networks",
)
for ef in expected_fields:
self.assertIn(ef, shares.quotas.QUOTA_FIELDS)