project: Split quota update into a separate workflow

This commit converts the quota update workflow tab into
a separate workflow. After this change, admin cannot set
a project's quotas in the project creation workflow,
but the admin can update its quotas just after creating a project,
so I think it is not a problem.

Because of splitting quota update from a project create/update workflow,
our unit tests has been simplified significantly. Previously we needed
tests for **combination** of create/update project and quota update,
but we now need only tests for create/update project and quota update
separately.

Part of blueprint horizon-plugin-tab-for-info-and-quotas
Change-Id: I7b95428e89ddc1c7a85a1162db29cef9a9674129
This commit is contained in:
Akihiro Motoki 2018-01-17 02:15:24 +09:00
parent 711d6f01ae
commit a257b52b85
5 changed files with 253 additions and 814 deletions

View File

@ -143,7 +143,7 @@ class UpdateProject(policy.PolicyTargetMixin, tables.LinkAction):
class ModifyQuotas(tables.LinkAction):
name = "quotas"
verbose_name = _("Modify Quotas")
url = "horizon:identity:projects:update"
url = "horizon:identity:projects:update_quotas"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (('compute', "os_compute_api:os-quota-sets:update"),)

File diff suppressed because it is too large Load Diff

View File

@ -30,4 +30,6 @@ urlpatterns = [
views.ProjectUsageView.as_view(), name='usage'),
url(r'^(?P<project_id>[^/]+)/detail/$',
views.DetailProjectView.as_view(), name='detail'),
url(r'^(?P<tenant_id>[^/]+)/update_quotas/$',
views.UpdateQuotasView.as_view(), name='update_quotas'),
]

View File

@ -154,11 +154,6 @@ class CreateProjectView(workflows.WorkflowView):
workflow_class = project_workflows.CreateProject
def get_initial(self):
if (api.keystone.is_multi_domain_enabled() and
not api.keystone.is_cloud_admin(self.request)):
self.workflow_class = project_workflows.CreateProjectNoQuota
initial = super(CreateProjectView, self).get_initial()
# Set the domain of the project
@ -166,16 +161,6 @@ class CreateProjectView(workflows.WorkflowView):
initial["domain_id"] = domain.id
initial["domain_name"] = domain.name
# get initial quota defaults
if api.keystone.is_cloud_admin(self.request):
try:
quota_defaults = quotas.get_default_quota_data(self.request)
for field in quotas.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, 'create_quotas')
return initial
@ -183,11 +168,6 @@ class UpdateProjectView(workflows.WorkflowView):
workflow_class = project_workflows.UpdateProject
def get_initial(self):
if (api.keystone.is_multi_domain_enabled() and
not api.keystone.is_cloud_admin(self.request)):
self.workflow_class = project_workflows.UpdateProjectNoQuota
initial = super(UpdateProjectView, self).get_initial()
project_id = self.kwargs['tenant_id']
@ -222,7 +202,21 @@ class UpdateProjectView(workflows.WorkflowView):
exceptions.handle(self.request,
_('Unable to retrieve project domain.'),
redirect=reverse(INDEX_URL))
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve project details.'),
redirect=reverse(INDEX_URL))
return initial
class UpdateQuotasView(workflows.WorkflowView):
workflow_class = project_workflows.UpdateQuota
def get_initial(self):
initial = super(UpdateQuotasView, self).get_initial()
project_id = self.kwargs['tenant_id']
initial['project_id'] = project_id
try:
# get initial project quota
if keystone.is_cloud_admin(self.request):
quota_data = quotas.get_tenant_quota_data(self.request,
@ -231,7 +225,7 @@ class UpdateProjectView(workflows.WorkflowView):
initial[field] = quota_data.get(field).limit
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve project details.'),
_('Unable to retrieve project quotas.'),
redirect=reverse(INDEX_URL))
return initial

View File

@ -49,19 +49,19 @@ COMMON_HORIZONTAL_TEMPLATE = "identity/projects/_common_horizontal_form.html"
class ProjectQuotaAction(workflows.Action):
ifcb_label = _("Injected File Content (Bytes)")
ifpb_label = _("Length of Injected File Path")
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_content_bytes = forms.IntegerField(
min_value=-1,
label=_("Injected File Content (Bytes)"))
key_pairs = forms.IntegerField(min_value=-1, label=_("Key Pairs"))
injected_file_path_bytes = forms.IntegerField(min_value=-1,
label=ifpb_label)
injected_file_path_bytes = forms.IntegerField(
min_value=-1,
label=_("Length of Injected File Path"))
volumes = forms.IntegerField(min_value=-1, label=_("Volumes"))
snapshots = forms.IntegerField(min_value=-1, label=_("Volume Snapshots"))
gigabytes = forms.IntegerField(
@ -89,10 +89,8 @@ class ProjectQuotaAction(workflows.Action):
self.fields[field].required = False
self.fields[field].widget = forms.HiddenInput()
class UpdateProjectQuotaAction(ProjectQuotaAction):
def clean(self):
cleaned_data = super(UpdateProjectQuotaAction, self).clean()
cleaned_data = super(ProjectQuotaAction, self).clean()
usages = quotas.tenant_quota_usages(
self.request, tenant_id=self.initial['project_id'])
# Validate the quota values before updating quotas.
@ -118,23 +116,8 @@ class UpdateProjectQuotaAction(ProjectQuotaAction):
permissions = ('openstack.roles.admin', 'openstack.services.compute')
class CreateProjectQuotaAction(ProjectQuotaAction):
class Meta(object):
name = _("Quotas")
slug = 'create_quotas'
help_text = _("Set maximum quotas for the project.")
permissions = ('openstack.roles.admin', 'openstack.services.compute')
class UpdateProjectQuota(workflows.Step):
action_class = UpdateProjectQuotaAction
template_name = COMMON_HORIZONTAL_TEMPLATE
depends_on = ("project_id",)
contributes = quotas.QUOTA_FIELDS
class CreateProjectQuota(workflows.Step):
action_class = CreateProjectQuotaAction
action_class = ProjectQuotaAction
template_name = COMMON_HORIZONTAL_TEMPLATE
depends_on = ("project_id",)
contributes = quotas.QUOTA_FIELDS
@ -398,33 +381,7 @@ class UpdateProjectGroups(workflows.UpdateMembersStep):
return context
class CommonQuotaWorkflow(workflows.Workflow):
def _update_project_quota(self, request, data, project_id):
disabled_quotas = quotas.get_disabled_quotas(request)
# Update the project quotas.
if api.base.is_service_enabled(request, 'compute'):
nova_data = {key: data[key] for key in
quotas.NOVA_QUOTA_FIELDS - disabled_quotas}
if nova_data:
nova.tenant_quota_update(request, project_id, **nova_data)
if cinder.is_volume_service_enabled(request):
cinder_data = {key: data[key] for key in
quotas.CINDER_QUOTA_FIELDS - disabled_quotas}
if cinder_data:
cinder.tenant_quota_update(request, project_id, **cinder_data)
if (api.base.is_service_enabled(request, 'network') and
api.neutron.is_quotas_extension_supported(request)):
neutron_data = {key: data[key] for key in
quotas.NEUTRON_QUOTA_FIELDS - disabled_quotas}
if neutron_data:
api.neutron.tenant_quota_update(request, project_id,
**neutron_data)
class CreateProject(CommonQuotaWorkflow):
class CreateProject(workflows.Workflow):
slug = "create_project"
name = _("Create Project")
finalize_button_name = _("Create Project")
@ -432,16 +389,14 @@ class CreateProject(CommonQuotaWorkflow):
failure_message = _('Unable to create project "%s".')
success_url = "horizon:identity:projects:index"
default_steps = (CreateProjectInfo,
UpdateProjectMembers,
CreateProjectQuota)
UpdateProjectMembers)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
if PROJECT_GROUP_ENABLED:
self.default_steps = (CreateProjectInfo,
UpdateProjectMembers,
UpdateProjectGroups,
CreateProjectQuota)
UpdateProjectGroups)
super(CreateProject, self).__init__(request=request,
context_seed=context_seed,
entry_point=entry_point,
@ -545,13 +500,6 @@ class CreateProject(CommonQuotaWorkflow):
'and update project quotas.')
% groups_to_add)
def _update_project_quota(self, request, data, project_id):
try:
super(CreateProject, self)._update_project_quota(
request, data, project_id)
except Exception:
exceptions.handle(request, _('Unable to set project quotas.'))
def handle(self, request, data):
project = self._create_project(request, data)
if not project:
@ -560,33 +508,9 @@ class CreateProject(CommonQuotaWorkflow):
self._update_project_members(request, data, project_id)
if PROJECT_GROUP_ENABLED:
self._update_project_groups(request, data, project_id)
if keystone.is_cloud_admin(request):
self._update_project_quota(request, data, project_id)
return True
class CreateProjectNoQuota(CreateProject):
slug = "create_project"
name = _("Create Project")
finalize_button_name = _("Create Project")
success_message = _('Created new project "%s".')
failure_message = _('Unable to create project "%s".')
success_url = "horizon:identity:projects:index"
default_steps = (CreateProjectInfo, UpdateProjectMembers)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
if PROJECT_GROUP_ENABLED:
self.default_steps = (CreateProjectInfo,
UpdateProjectMembers,
UpdateProjectGroups,)
super(CreateProject, self).__init__(request=request,
context_seed=context_seed,
entry_point=entry_point,
*args,
**kwargs)
class UpdateProjectInfoAction(CreateProjectInfoAction):
enabled = forms.BooleanField(required=False, label=_("Enabled"))
domain_name = forms.CharField(label=_("Domain Name"),
@ -636,7 +560,7 @@ class UpdateProjectInfo(workflows.Step):
self.contributes += tuple(EXTRA_INFO.keys())
class UpdateProject(CommonQuotaWorkflow):
class UpdateProject(workflows.Workflow):
slug = "update_project"
name = _("Edit Project")
finalize_button_name = _("Save")
@ -644,16 +568,14 @@ class UpdateProject(CommonQuotaWorkflow):
failure_message = _('Unable to modify project "%s".')
success_url = "horizon:identity:projects:index"
default_steps = (UpdateProjectInfo,
UpdateProjectMembers,
UpdateProjectQuota)
UpdateProjectMembers)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
if PROJECT_GROUP_ENABLED:
self.default_steps = (UpdateProjectInfo,
UpdateProjectMembers,
UpdateProjectGroups,
UpdateProjectQuota)
UpdateProjectGroups)
super(UpdateProject, self).__init__(request=request,
context_seed=context_seed,
@ -911,17 +833,6 @@ class UpdateProject(CommonQuotaWorkflow):
% groups_to_modify)
return False
def _update_project_quota(self, request, data, project_id):
try:
super(UpdateProject, self)._update_project_quota(
request, data, project_id)
return True
except Exception:
exceptions.handle(request, _('Modified project information and '
'members, but unable to modify '
'project quotas.'))
return False
def handle(self, request, data):
# FIXME(gabriel): This should be refactored to use Python's built-in
# sets and do this all in a single "roles to add" and "roles to remove"
@ -945,32 +856,57 @@ class UpdateProject(CommonQuotaWorkflow):
if not ret:
return False
if api.keystone.is_cloud_admin(request):
ret = self._update_project_quota(request, data, project_id)
if not ret:
return False
return True
class UpdateProjectNoQuota(UpdateProject):
slug = "update_project"
name = _("Edit Project")
class UpdateQuota(workflows.Workflow):
slug = "update_quotas"
name = _("Edit Quotas")
finalize_button_name = _("Save")
success_message = _('Modified project "%s".')
failure_message = _('Unable to modify project "%s".')
success_message = _('Modified quotas of project "%s".')
failure_message = _('Unable to modify quotas of project "%s".')
success_url = "horizon:identity:projects:index"
default_steps = (UpdateProjectInfo, UpdateProjectMembers)
default_steps = (UpdateProjectQuota,)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
if PROJECT_GROUP_ENABLED:
self.default_steps = (UpdateProjectInfo,
UpdateProjectMembers,
UpdateProjectGroups)
def format_status_message(self, message):
if "%s" in message:
return message % self.context.get('name', 'unknown project')
else:
return message
super(UpdateProject, self).__init__(request=request,
context_seed=context_seed,
entry_point=entry_point,
*args,
**kwargs)
def _update_project_quota(self, request, data, project_id):
disabled_quotas = quotas.get_disabled_quotas(request)
if api.base.is_service_enabled(request, 'compute'):
nova_data = {key: data[key] for key in
quotas.NOVA_QUOTA_FIELDS - disabled_quotas}
if nova_data:
nova.tenant_quota_update(request, project_id, **nova_data)
if cinder.is_volume_service_enabled(request):
cinder_data = {key: data[key] for key in
quotas.CINDER_QUOTA_FIELDS - disabled_quotas}
if cinder_data:
cinder.tenant_quota_update(request, project_id, **cinder_data)
if (api.base.is_service_enabled(request, 'network') and
api.neutron.is_quotas_extension_supported(request)):
neutron_data = {key: data[key] for key in
quotas.NEUTRON_QUOTA_FIELDS - disabled_quotas}
if neutron_data:
api.neutron.tenant_quota_update(request, project_id,
**neutron_data)
def handle(self, request, data):
project_id = data['project_id']
if not api.keystone.is_cloud_admin(request):
return True
try:
self._update_project_quota(request, data, project_id)
return True
except Exception:
exceptions.handle(request,
_('Modified project information and '
'members, but unable to modify '
'project quotas.'))
return False