Merge "Support editing volume type access"
This commit is contained in:
commit
98ac7666a0
|
@ -1027,3 +1027,17 @@ def is_volume_service_enabled(request):
|
||||||
base.is_service_enabled(request, 'volume') or
|
base.is_service_enabled(request, 'volume') or
|
||||||
base.is_service_enabled(request, 'volumev2')
|
base.is_service_enabled(request, 'volumev2')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def volume_type_access_list(request, volume_type):
|
||||||
|
return cinderclient(request).volume_type_access.list(volume_type)
|
||||||
|
|
||||||
|
|
||||||
|
def volume_type_add_project_access(request, volume_type, project_id):
|
||||||
|
return cinderclient(request).volume_type_access.add_project_access(
|
||||||
|
volume_type, project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def volume_type_remove_project_access(request, volume_type, project_id):
|
||||||
|
return cinderclient(request).volume_type_access.remove_project_access(
|
||||||
|
volume_type, project_id)
|
||||||
|
|
|
@ -19,6 +19,7 @@ from horizon import forms
|
||||||
from horizon import messages
|
from horizon import messages
|
||||||
|
|
||||||
from openstack_dashboard.api import cinder
|
from openstack_dashboard.api import cinder
|
||||||
|
from openstack_dashboard.api import keystone
|
||||||
|
|
||||||
|
|
||||||
class CreateVolumeType(forms.SelfHandlingForm):
|
class CreateVolumeType(forms.SelfHandlingForm):
|
||||||
|
@ -303,3 +304,67 @@ class EditVolumeType(forms.SelfHandlingForm):
|
||||||
|
|
||||||
exceptions.handle(request, error_message,
|
exceptions.handle(request, error_message,
|
||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
|
|
||||||
|
|
||||||
|
class EditTypeAccessForm(forms.SelfHandlingForm):
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super(EditTypeAccessForm, self).__init__(request, *args, **kwargs)
|
||||||
|
err_msg = _('Unable to retrieve volume type access list.')
|
||||||
|
|
||||||
|
self.fields["member"] = forms.MultipleChoiceField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.ThemableCheckboxSelectMultiple())
|
||||||
|
# Get list of available projects.
|
||||||
|
try:
|
||||||
|
all_projects, has_more = keystone.tenant_list(request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, err_msg)
|
||||||
|
projects_list = [(project.id, project.name)
|
||||||
|
for project in all_projects]
|
||||||
|
|
||||||
|
self.fields["member"].choices = projects_list
|
||||||
|
|
||||||
|
volume_type_id = self.initial.get('volume_type_id')
|
||||||
|
volume_type_access = []
|
||||||
|
try:
|
||||||
|
if volume_type_id:
|
||||||
|
volume_type = cinder.volume_type_get(request,
|
||||||
|
volume_type_id)
|
||||||
|
if not volume_type.is_public:
|
||||||
|
volume_type_access = [
|
||||||
|
project.project_id for project in
|
||||||
|
cinder.volume_type_access_list(request,
|
||||||
|
volume_type_id)]
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, err_msg)
|
||||||
|
|
||||||
|
self.fields["member"].initial = volume_type_access
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
type_id = self.initial['volume_type_id']
|
||||||
|
current_projects = self.fields["member"].initial
|
||||||
|
|
||||||
|
removed_projects = current_projects
|
||||||
|
for p in data['member']:
|
||||||
|
if p not in current_projects:
|
||||||
|
# Newly added project access
|
||||||
|
try:
|
||||||
|
cinder.volume_type_add_project_access(request, type_id, p)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request,
|
||||||
|
_('Failed to add project %(project)s to '
|
||||||
|
'volume type access.') %
|
||||||
|
{'project': p})
|
||||||
|
else:
|
||||||
|
removed_projects.remove(p)
|
||||||
|
for p in removed_projects:
|
||||||
|
try:
|
||||||
|
cinder.volume_type_remove_project_access(request, type_id, p)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _('Failed to remove project '
|
||||||
|
'%(project)s from volume type '
|
||||||
|
'access.') % {'project': p})
|
||||||
|
messages.success(request,
|
||||||
|
_('Modified volume type access: %s') % type_id)
|
||||||
|
return True
|
||||||
|
|
|
@ -40,6 +40,19 @@ class EditVolumeType(tables.LinkAction):
|
||||||
policy_rules = (("volume", "volume_extension:types_manage"),)
|
policy_rules = (("volume", "volume_extension:types_manage"),)
|
||||||
|
|
||||||
|
|
||||||
|
class EditAccess(tables.LinkAction):
|
||||||
|
name = "edit_access"
|
||||||
|
verbose_name = _("Edit Access")
|
||||||
|
url = "horizon:admin:volume_types:edit_access"
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "pencil"
|
||||||
|
policy_rules = (("volume", "volume_extension:types_manage"),)
|
||||||
|
|
||||||
|
def allowed(self, request, volume_type=None):
|
||||||
|
if volume_type is not None:
|
||||||
|
return not getattr(volume_type, 'is_public', True)
|
||||||
|
|
||||||
|
|
||||||
class ViewVolumeTypeExtras(tables.LinkAction):
|
class ViewVolumeTypeExtras(tables.LinkAction):
|
||||||
name = "extras"
|
name = "extras"
|
||||||
verbose_name = _("View Extra Specs")
|
verbose_name = _("View Extra Specs")
|
||||||
|
@ -248,6 +261,7 @@ class VolumeTypesTable(tables.DataTable):
|
||||||
ManageQosSpecAssociation,
|
ManageQosSpecAssociation,
|
||||||
EditVolumeType,
|
EditVolumeType,
|
||||||
UpdateVolumeTypeEncryption,
|
UpdateVolumeTypeEncryption,
|
||||||
|
EditAccess,
|
||||||
DeleteVolumeTypeEncryption,
|
DeleteVolumeTypeEncryption,
|
||||||
DeleteVolumeType,
|
DeleteVolumeType,
|
||||||
UpdateMetadata)
|
UpdateMetadata)
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "horizon/common/_modal_form_add_members.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
<div class="left">
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<p>{% trans "Select the projects where the volume types will be used. If no projects are selected, then volume type will be only visible by users with the admin role." %}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'admin/volume_types/_update_access.html' %}
|
||||||
|
{% endblock %}
|
|
@ -72,8 +72,7 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
reverse('horizon:admin:volume_types:create_type'),
|
reverse('horizon:admin:volume_types:create_type'),
|
||||||
formData)
|
formData)
|
||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
self.assertRedirectsNoFollow(res, redirect)
|
|
||||||
|
|
||||||
@test.create_stubs({cinder: ('volume_type_get',
|
@test.create_stubs({cinder: ('volume_type_get',
|
||||||
'volume_type_update')})
|
'volume_type_update')})
|
||||||
|
@ -96,8 +95,7 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
args=[volume_type.id])
|
args=[volume_type.id])
|
||||||
res = self.client.post(url, formData)
|
res = self.client.post(url, formData)
|
||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
self.assertRedirectsNoFollow(res, redirect)
|
|
||||||
|
|
||||||
def test_update_volume_type_public_true(self):
|
def test_update_volume_type_public_true(self):
|
||||||
self._test_update_volume_type(True)
|
self._test_update_volume_type(True)
|
||||||
|
@ -129,13 +127,10 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
volume_type.id)
|
volume_type.id)
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
res = self.client.post(
|
res = self.client.post(INDEX_URL, formData)
|
||||||
reverse('horizon:admin:volume_types:index'),
|
|
||||||
formData)
|
|
||||||
|
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
|
||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
self.assertRedirectsNoFollow(res, redirect)
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('server_list',),
|
@test.create_stubs({api.nova: ('server_list',),
|
||||||
cinder: ('volume_list',
|
cinder: ('volume_list',
|
||||||
|
@ -162,13 +157,10 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
.AndRaise(exceptions.BadRequest())
|
.AndRaise(exceptions.BadRequest())
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
res = self.client.post(
|
res = self.client.post(INDEX_URL, formData)
|
||||||
reverse('horizon:admin:volume_types:index'),
|
|
||||||
formData)
|
|
||||||
|
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
|
||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
self.assertRedirectsNoFollow(res, redirect)
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
@test.create_stubs({cinder: ('volume_encryption_type_create',
|
@test.create_stubs({cinder: ('volume_encryption_type_create',
|
||||||
'volume_type_list',)})
|
'volume_type_list',)})
|
||||||
|
@ -293,13 +285,10 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
volume_type.id)
|
volume_type.id)
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
res = self.client.post(
|
res = self.client.post(INDEX_URL, formData)
|
||||||
reverse('horizon:admin:volume_types:index'),
|
|
||||||
formData)
|
|
||||||
|
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
|
||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
self.assertRedirectsNoFollow(res, redirect)
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
@test.create_stubs({cinder: ('volume_encryption_type_update',
|
@test.create_stubs({cinder: ('volume_encryption_type_update',
|
||||||
'volume_encryption_type_get',
|
'volume_encryption_type_get',
|
||||||
|
@ -335,3 +324,43 @@ class VolumeTypeTests(test.BaseAdminViewTests):
|
||||||
self.assertTemplateUsed(
|
self.assertTemplateUsed(
|
||||||
res,
|
res,
|
||||||
'admin/volume_types/update_volume_type_encryption.html')
|
'admin/volume_types/update_volume_type_encryption.html')
|
||||||
|
|
||||||
|
@test.create_stubs({cinder: ('volume_type_get',
|
||||||
|
'volume_type_access_list',
|
||||||
|
'volume_type_add_project_access',
|
||||||
|
'volume_type_remove_project_access'),
|
||||||
|
keystone: ('tenant_list',)})
|
||||||
|
def _test_edit_volume_type_access(self, exception=False):
|
||||||
|
volume_type = self.cinder_volume_types.list()[2]
|
||||||
|
volume_type.id = u'1'
|
||||||
|
keystone.tenant_list(
|
||||||
|
IsA(http.HttpRequest)).AndReturn([self.tenants.list(), False])
|
||||||
|
type_access = self.cinder_type_access.list()
|
||||||
|
formData = {'member': [u'3'],
|
||||||
|
'volume_type_id': volume_type.id}
|
||||||
|
volume_type = cinder.volume_type_get(
|
||||||
|
IsA(http.HttpRequest), volume_type.id).AndReturn(volume_type)
|
||||||
|
cinder.volume_type_access_list(
|
||||||
|
IsA(http.HttpRequest), volume_type.id).AndReturn(type_access)
|
||||||
|
cinder.volume_type_add_project_access(
|
||||||
|
IsA(http.HttpRequest), volume_type.id, u'3')
|
||||||
|
if exception:
|
||||||
|
cinder.volume_type_remove_project_access(
|
||||||
|
IsA(http.HttpRequest), volume_type.id, u'1')\
|
||||||
|
.AndRaise(exceptions.BadRequest())
|
||||||
|
else:
|
||||||
|
cinder.volume_type_remove_project_access(
|
||||||
|
IsA(http.HttpRequest), volume_type.id, u'1')
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:volume_types:edit_access',
|
||||||
|
args=[volume_type.id])
|
||||||
|
res = self.client.post(url, formData)
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
def test_edit_volume_type_access(self):
|
||||||
|
self._test_edit_volume_type_access()
|
||||||
|
|
||||||
|
def test_edit_volume_type_access_exception(self):
|
||||||
|
self._test_edit_volume_type_access(exception=True)
|
||||||
|
|
|
@ -49,4 +49,6 @@ urlpatterns = [
|
||||||
name='type_encryption_detail'),
|
name='type_encryption_detail'),
|
||||||
url(r'^qos_specs/',
|
url(r'^qos_specs/',
|
||||||
include(qos_specs_urls, namespace='qos_specs')),
|
include(qos_specs_urls, namespace='qos_specs')),
|
||||||
|
url(r'^(?P<volume_type_id>[^/]+)/edit_access/$',
|
||||||
|
views.EditAccessView.as_view(), name='edit_access'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -87,6 +87,9 @@ class VolumeTypesView(tables.MultiTableView, volumes_views.VolumeTableMixIn):
|
||||||
return qos_specs
|
return qos_specs
|
||||||
|
|
||||||
|
|
||||||
|
INDEX_URL = 'horizon:admin:volume_types:index'
|
||||||
|
|
||||||
|
|
||||||
class CreateVolumeTypeView(forms.ModalFormView):
|
class CreateVolumeTypeView(forms.ModalFormView):
|
||||||
form_class = volume_types_forms.CreateVolumeType
|
form_class = volume_types_forms.CreateVolumeType
|
||||||
modal_id = "create_volume_type_modal"
|
modal_id = "create_volume_type_modal"
|
||||||
|
@ -119,7 +122,7 @@ class VolumeTypeEncryptionDetailView(views.HorizonTemplateView):
|
||||||
self.name = volume_type.name
|
self.name = volume_type.name
|
||||||
self._volume_type_encryption.name = self.name
|
self._volume_type_encryption.name = self.name
|
||||||
except Exception:
|
except Exception:
|
||||||
redirect = reverse('horizon:admin:volume_types:index')
|
redirect = reverse(INDEX_URL)
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve volume type encryption'
|
_('Unable to retrieve volume type encryption'
|
||||||
' details.'),
|
' details.'),
|
||||||
|
@ -136,7 +139,7 @@ class CreateVolumeTypeEncryptionView(forms.ModalFormView):
|
||||||
template_name = "admin/volume_types/create_volume_type_encryption.html"
|
template_name = "admin/volume_types/create_volume_type_encryption.html"
|
||||||
submit_label = _("Create Volume Type Encryption")
|
submit_label = _("Create Volume Type Encryption")
|
||||||
submit_url = "horizon:admin:volume_types:create_type_encryption"
|
submit_url = "horizon:admin:volume_types:create_type_encryption"
|
||||||
success_url = reverse_lazy('horizon:admin:volume_types:index')
|
success_url = reverse_lazy(INDEX_URL)
|
||||||
page_title = _("Create an Encrypted Volume Type")
|
page_title = _("Create an Encrypted Volume Type")
|
||||||
|
|
||||||
@memoized.memoized_method
|
@memoized.memoized_method
|
||||||
|
@ -204,7 +207,7 @@ def _get_volume_type_name(request, kwargs):
|
||||||
return volume_type.name
|
return volume_type.name
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = _('Unable to retrieve volume type name.')
|
msg = _('Unable to retrieve volume type name.')
|
||||||
url = reverse('horizon:admin:volume_types:index')
|
url = reverse(INDEX_URL)
|
||||||
exceptions.handle(request, msg, redirect=url)
|
exceptions.handle(request, msg, redirect=url)
|
||||||
|
|
||||||
|
|
||||||
|
@ -374,3 +377,23 @@ class ManageQosSpecAssociationView(forms.ModalFormView):
|
||||||
'cur_qos_spec_id': cur_qos_spec_id,
|
'cur_qos_spec_id': cur_qos_spec_id,
|
||||||
'cur_qos_spec_name': cur_qos_spec_name,
|
'cur_qos_spec_name': cur_qos_spec_name,
|
||||||
'qos_specs': self.get_qos_specs()}
|
'qos_specs': self.get_qos_specs()}
|
||||||
|
|
||||||
|
|
||||||
|
class EditAccessView(forms.ModalFormView):
|
||||||
|
form_class = volume_types_forms.EditTypeAccessForm
|
||||||
|
template_name = 'admin/volume_types/update_access.html'
|
||||||
|
submit_label = _("Save")
|
||||||
|
submit_url = "horizon:admin:volume_types:edit_access"
|
||||||
|
success_url = reverse_lazy('horizon:admin:volume_types:index')
|
||||||
|
cancel_url = reverse_lazy('horizon:admin:volume_types:index')
|
||||||
|
page_title = _("Edit Volume Type Access")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(EditAccessView, self).get_context_data(**kwargs)
|
||||||
|
context['volume_type_id'] = self.kwargs["volume_type_id"]
|
||||||
|
args = (self.kwargs['volume_type_id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {'volume_type_id': self.kwargs['volume_type_id']}
|
||||||
|
|
|
@ -23,6 +23,7 @@ from cinderclient.v2 import volume_backups as vol_backups
|
||||||
from cinderclient.v2 import volume_encryption_types as vol_enc_types
|
from cinderclient.v2 import volume_encryption_types as vol_enc_types
|
||||||
from cinderclient.v2 import volume_snapshots as vol_snaps
|
from cinderclient.v2 import volume_snapshots as vol_snaps
|
||||||
from cinderclient.v2 import volume_transfers
|
from cinderclient.v2 import volume_transfers
|
||||||
|
from cinderclient.v2 import volume_type_access
|
||||||
from cinderclient.v2 import volume_types
|
from cinderclient.v2 import volume_types
|
||||||
from cinderclient.v2 import volumes
|
from cinderclient.v2 import volumes
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ def data(TEST):
|
||||||
TEST.cinder_volume_backups = utils.TestDataContainer()
|
TEST.cinder_volume_backups = utils.TestDataContainer()
|
||||||
TEST.cinder_volume_encryption_types = utils.TestDataContainer()
|
TEST.cinder_volume_encryption_types = utils.TestDataContainer()
|
||||||
TEST.cinder_volume_types = utils.TestDataContainer()
|
TEST.cinder_volume_types = utils.TestDataContainer()
|
||||||
|
TEST.cinder_type_access = utils.TestDataContainer()
|
||||||
TEST.cinder_volume_encryption = utils.TestDataContainer()
|
TEST.cinder_volume_encryption = utils.TestDataContainer()
|
||||||
TEST.cinder_bootable_volumes = utils.TestDataContainer()
|
TEST.cinder_bootable_volumes = utils.TestDataContainer()
|
||||||
TEST.cinder_qos_specs = utils.TestDataContainer()
|
TEST.cinder_qos_specs = utils.TestDataContainer()
|
||||||
|
@ -157,7 +159,16 @@ def data(TEST):
|
||||||
{'id': u'2',
|
{'id': u'2',
|
||||||
'name': u'vol_type_2',
|
'name': u'vol_type_2',
|
||||||
'description': 'type 2 description'})
|
'description': 'type 2 description'})
|
||||||
TEST.cinder_volume_types.add(vol_type1, vol_type2)
|
vol_type3 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||||
|
{'id': u'3',
|
||||||
|
'name': u'vol_type_3',
|
||||||
|
'is_public': False,
|
||||||
|
'description': 'type 3 description'})
|
||||||
|
TEST.cinder_volume_types.add(vol_type1, vol_type2, vol_type3)
|
||||||
|
vol_type_access1 = volume_type_access.VolumeTypeAccess(
|
||||||
|
volume_type_access.VolumeTypeAccessManager(None),
|
||||||
|
{'volume_type_id': u'1', 'project_id': u'1'})
|
||||||
|
TEST.cinder_type_access.add(vol_type_access1)
|
||||||
|
|
||||||
# Volumes - Cinder v2
|
# Volumes - Cinder v2
|
||||||
volume_v2 = volumes.Volume(
|
volume_v2 = volumes.Volume(
|
||||||
|
|
Loading…
Reference in New Issue