Added support for volume types
As cinder already supports volume types, it's time to be added in Horizon. The types are added in admin panel. A volume can either have a *valid* type, or None. There are two minor issues: 1. When a type is deleted, if there is a volume with this type, the type is returned as the id, instead of the name. Which is good as consistency, but maybe a type should not be deleted if used by a volume? 2. If no vol type is passed(None) value, the type is being assigned as 'None', and returned as a string('None') 3. In the create volume type form, the 'Description' is empty at the moment, I couldn't find any help info to add for volume types... Implements blueprint volume-types Tested with n-cinder and n-vol services. Since Folsom both support volume-types. I'm not sure if it is possible a grizzly Dashboard to work with essex nova, which could be a problem. Dashboard doesn't seem to provide permissions based on tables, so if it needs to check if a service is available to show/hide volume types, this will need some more effors to implement permissions on table/action level. Or maybe this needs to be added as a standalone panel? Patch set 2: implemented suggestions, added one more thing: When creating a volume from a snapshot, set the volume type initial as the type of the original volume from which the snapshot is being created. Patch Set 3: Updated the description of volume types, based on the notes from: https://etherpad.openstack.org/grizzly-cinder-volumetypes https://etherpad.openstack.org/cinder-usecases Any feedback on the description is welcome. Change-Id: Ib0c136c5c8cd9fbd34ce1dd346260f404c96f667
This commit is contained in:
parent
cd70bc9284
commit
057d891f31
@ -77,9 +77,11 @@ def volume_get(request, volume_id):
|
||||
return volume_data
|
||||
|
||||
|
||||
def volume_create(request, size, name, description, snapshot_id=None):
|
||||
def volume_create(request, size, name, description, volume_type,
|
||||
snapshot_id=None):
|
||||
return cinderclient(request).volumes.create(size, display_name=name,
|
||||
display_description=description, snapshot_id=snapshot_id)
|
||||
display_description=description, volume_type=volume_type,
|
||||
snapshot_id=snapshot_id)
|
||||
|
||||
|
||||
def volume_delete(request, volume_id):
|
||||
@ -113,3 +115,15 @@ def tenant_quota_update(request, tenant_id, **kwargs):
|
||||
|
||||
def default_quota_get(request, tenant_id):
|
||||
return QuotaSet(cinderclient(request).quotas.defaults(tenant_id))
|
||||
|
||||
|
||||
def volume_type_list(request):
|
||||
return cinderclient(request).volume_types.list()
|
||||
|
||||
|
||||
def volume_type_create(request, name):
|
||||
return cinderclient(request).volume_types.create(name)
|
||||
|
||||
|
||||
def volume_type_delete(request, volume_type_id):
|
||||
return cinderclient(request).volume_types.delete(volume_type_id)
|
||||
|
44
openstack_dashboard/dashboards/admin/volumes/forms.py
Normal file
44
openstack_dashboard/dashboards/admin/volumes/forms.py
Normal file
@ -0,0 +1,44 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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 messages
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
|
||||
class CreateVolumeType(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label=_("Name"))
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
# Remove any new lines in the public key
|
||||
volume_type = cinder.volume_type_create(request,
|
||||
data['name'])
|
||||
messages.success(request, _('Successfully created volume type: %s')
|
||||
% data['name'])
|
||||
return volume_type
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Unable to create volume type.'))
|
||||
return False
|
@ -1,10 +1,26 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.dashboards.project.volumes.tables import (UpdateRow,
|
||||
VolumesTable as _VolumesTable, DeleteVolume)
|
||||
|
||||
|
||||
class CreateVolumeType(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Volume Type")
|
||||
url = "horizon:admin:volumes:create_type"
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
|
||||
|
||||
class DeleteVolumeType(tables.DeleteAction):
|
||||
data_type_singular = _("Volume Type")
|
||||
data_type_plural = _("Volume Types")
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
cinder.volume_type_delete(request, obj_id)
|
||||
|
||||
|
||||
class VolumesTable(_VolumesTable):
|
||||
name = tables.Column("display_name",
|
||||
verbose_name=_("Name"),
|
||||
@ -17,3 +33,20 @@ class VolumesTable(_VolumesTable):
|
||||
row_class = UpdateRow
|
||||
table_actions = (DeleteVolume,)
|
||||
row_actions = (DeleteVolume,)
|
||||
|
||||
|
||||
class VolumeTypesTable(tables.DataTable):
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"))
|
||||
|
||||
def get_object_display(self, vol_type):
|
||||
return vol_type.name
|
||||
|
||||
def get_object_id(self, vol_type):
|
||||
return str(vol_type.id)
|
||||
|
||||
class Meta:
|
||||
name = "volume_types"
|
||||
verbose_name = _("Volume Types")
|
||||
table_actions = (CreateVolumeType, DeleteVolumeType,)
|
||||
row_actions = (DeleteVolumeType,)
|
||||
|
@ -0,0 +1,29 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:admin:volumes:create_type %}{% endblock %}
|
||||
|
||||
{% block modal_id %}create_volume_type_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Volume Type" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% blocktrans %}
|
||||
The volume type defines the characteristics of a volume.
|
||||
It usually maps to a set of capabilities of the storage back-end driver to be used for this volume.
|
||||
Examples: "Performance", "SSD", "Backup", etc.
|
||||
{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Volume Type" %}" />
|
||||
<a href="{% url horizon:admin:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Volume Type" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create a Volume Type") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/volumes/_create_volume_type.html' %}
|
||||
{% endblock %}
|
@ -7,7 +7,11 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
<div id="volumes">
|
||||
{{ volumes_table.render }}
|
||||
</div>
|
||||
|
||||
<div id="volume-types">
|
||||
{{ volume_types_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -19,15 +19,21 @@ from django.core.urlresolvers import reverse
|
||||
from mox import IsA
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class VolumeTests(test.BaseAdminViewTests):
|
||||
@test.create_stubs({api: ('server_list', 'volume_list',)})
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
cinder: ('volume_list',
|
||||
'volume_type_list',)})
|
||||
def test_index(self):
|
||||
api.volume_list(IsA(http.HttpRequest), search_opts={
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts={
|
||||
'all_tenants': 1}).AndReturn(self.volumes.list())
|
||||
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -37,3 +43,43 @@ class VolumeTests(test.BaseAdminViewTests):
|
||||
volumes = res.context['volumes_table'].data
|
||||
|
||||
self.assertItemsEqual(volumes, self.volumes.list())
|
||||
|
||||
@test.create_stubs({cinder: ('volume_type_create',)})
|
||||
def test_create_volume_type(self):
|
||||
formData = {'name': 'volume type 1'}
|
||||
cinder.volume_type_create(IsA(http.HttpRequest),
|
||||
formData['name']).\
|
||||
AndReturn(self.volume_types.first())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse('horizon:admin:volumes:create_type'),
|
||||
formData)
|
||||
|
||||
redirect = reverse('horizon:admin:volumes:index')
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, redirect)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
cinder: ('volume_list',
|
||||
'volume_type_list',
|
||||
'volume_type_delete',)})
|
||||
def test_delete_volume_type(self):
|
||||
volume_type = self.volume_types.first()
|
||||
formData = {'action': 'volume_types__delete__%s' % volume_type.id}
|
||||
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts={
|
||||
'all_tenants': 1}).AndReturn(self.volumes.list())
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
cinder.volume_type_delete(IsA(http.HttpRequest),
|
||||
str(volume_type.id))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse('horizon:admin:volumes:index'),
|
||||
formData)
|
||||
|
||||
redirect = reverse('horizon:admin:volumes:index')
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, redirect)
|
||||
|
@ -1,8 +1,9 @@
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import IndexView, DetailView
|
||||
from .views import IndexView, DetailView, CreateVolumeTypeView
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', IndexView.as_view(), name='index'),
|
||||
url(r'^create_type$', CreateVolumeTypeView.as_view(), name='create_type'),
|
||||
url(r'^(?P<volume_id>[^/]+)/$', DetailView.as_view(), name='detail'),
|
||||
)
|
||||
|
@ -18,22 +18,48 @@
|
||||
Admin views for managing volumes.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from openstack_dashboard.dashboards.project.volumes.views import \
|
||||
IndexView as _IndexView, DetailView as _DetailView
|
||||
from .tables import VolumesTable
|
||||
VolumeTableMixIn, DetailView as _DetailView
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
from .tables import VolumesTable, VolumeTypesTable
|
||||
from .forms import CreateVolumeType
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
|
||||
class IndexView(_IndexView):
|
||||
table_class = VolumesTable
|
||||
class IndexView(tables.MultiTableView, VolumeTableMixIn):
|
||||
table_classes = (VolumesTable, VolumeTypesTable)
|
||||
template_name = "admin/volumes/index.html"
|
||||
|
||||
def get_data(self):
|
||||
def get_volumes_data(self):
|
||||
volumes = self._get_volumes(search_opts={'all_tenants': 1})
|
||||
instances = self._get_instances()
|
||||
self._set_id_if_nameless(volumes, instances)
|
||||
self._set_attachments_string(volumes, instances)
|
||||
return volumes
|
||||
|
||||
def get_volume_types_data(self):
|
||||
try:
|
||||
volume_types = cinder.volume_type_list(self.request)
|
||||
except:
|
||||
volume_types = []
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve volume types"))
|
||||
return volume_types
|
||||
|
||||
|
||||
class DetailView(_DetailView):
|
||||
template_name = "admin/volumes/detail.html"
|
||||
|
||||
|
||||
class CreateVolumeTypeView(forms.ModalFormView):
|
||||
form_class = CreateVolumeType
|
||||
template_name = 'admin/volumes/create_volume_type.html'
|
||||
success_url = 'horizon:admin:volumes:index'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(self.success_url)
|
||||
|
@ -22,7 +22,7 @@ from django import http
|
||||
from django.core.urlresolvers import reverse
|
||||
from mox import IsA
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
@ -38,12 +38,12 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
|
||||
self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html')
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_create',)})
|
||||
def test_create_snapshot_post(self):
|
||||
volume = self.volumes.first()
|
||||
snapshot = self.volume_snapshots.first()
|
||||
|
||||
self.mox.StubOutWithMock(api, 'volume_snapshot_create')
|
||||
api.volume_snapshot_create(IsA(http.HttpRequest),
|
||||
cinder.volume_snapshot_create(IsA(http.HttpRequest),
|
||||
volume.id,
|
||||
snapshot.display_name,
|
||||
snapshot.display_description) \
|
||||
|
@ -28,6 +28,8 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label=_("Volume Name"))
|
||||
description = forms.CharField(widget=forms.Textarea,
|
||||
label=_("Description"), required=False)
|
||||
type = forms.ChoiceField(label=_("Type"),
|
||||
required=False)
|
||||
size = forms.IntegerField(min_value=1, label=_("Size (GB)"))
|
||||
snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"),
|
||||
widget=SelectWidget(
|
||||
@ -40,6 +42,10 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateForm, self).__init__(request, *args, **kwargs)
|
||||
volume_types = cinder.volume_type_list(request)
|
||||
self.fields['type'].choices = [("", "")] + \
|
||||
[(type.name, type.name)
|
||||
for type in volume_types]
|
||||
if ("snapshot_id" in request.GET):
|
||||
try:
|
||||
snapshot = self.get_snapshot(request,
|
||||
@ -48,6 +54,13 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
self.fields['size'].initial = snapshot.size
|
||||
self.fields['snapshot_source'].choices = ((snapshot.id,
|
||||
snapshot),)
|
||||
try:
|
||||
# Set the volume type from the original volume
|
||||
orig_volume = cinder.volume_get(request,
|
||||
snapshot.volume_id)
|
||||
self.fields['type'].initial = orig_volume.volume_type
|
||||
except:
|
||||
pass
|
||||
self.fields['size'].help_text = _('Volume size must be equal '
|
||||
'to or greater than the snapshot size (%sGB)'
|
||||
% snapshot.size)
|
||||
@ -56,7 +69,7 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
_('Unable to load the specified snapshot.'))
|
||||
else:
|
||||
try:
|
||||
snapshots = api.volume_snapshot_list(request)
|
||||
snapshots = cinder.volume_snapshot_list(request)
|
||||
if snapshots:
|
||||
choices = [('', _("Choose a snapshot"))] + \
|
||||
[(s.id, s) for s in snapshots]
|
||||
@ -102,19 +115,22 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
' volumes.')
|
||||
raise ValidationError(error_message)
|
||||
|
||||
volume = api.volume_create(request,
|
||||
volume = cinder.volume_create(request,
|
||||
data['size'],
|
||||
data['name'],
|
||||
data['description'],
|
||||
data['type'],
|
||||
snapshot_id=snapshot_id)
|
||||
message = 'Creating volume "%s"' % data['name']
|
||||
messages.info(request, message)
|
||||
return volume
|
||||
except ValidationError, e:
|
||||
return self.api_error(e.messages[0])
|
||||
self.api_error(e.messages[0])
|
||||
return False
|
||||
except:
|
||||
exceptions.handle(request, ignore=True)
|
||||
return self.api_error(_("Unable to create volume."))
|
||||
self.api_error(_("Unable to create volume."))
|
||||
return False
|
||||
|
||||
@memoized
|
||||
def get_snapshot(self, request, id):
|
||||
@ -176,7 +192,8 @@ class AttachForm(forms.SelfHandlingForm):
|
||||
data['volume_id'],
|
||||
data['instance'],
|
||||
data.get('device', ''))
|
||||
vol_name = api.volume_get(request, data['volume_id']).display_name
|
||||
vol_name = cinder.volume_get(request,
|
||||
data['volume_id']).display_name
|
||||
|
||||
message = _('Attaching volume %(vol)s to instance '
|
||||
'%(inst)s on %(dev)s.') % {"vol": vol_name,
|
||||
@ -206,7 +223,7 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
snapshot = api.volume_snapshot_create(request,
|
||||
snapshot = cinder.volume_snapshot_create(request,
|
||||
data['volume_id'],
|
||||
data['name'],
|
||||
data['description'])
|
||||
|
@ -26,6 +26,7 @@ from horizon import exceptions
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -41,7 +42,7 @@ class DeleteVolume(tables.DeleteAction):
|
||||
obj = self.table.get_object_by_id(obj_id)
|
||||
name = self.table.get_object_display(obj)
|
||||
try:
|
||||
api.volume_delete(request, obj_id)
|
||||
cinder.volume_delete(request, obj_id)
|
||||
except:
|
||||
msg = _('Unable to delete volume "%s". One or more snapshots '
|
||||
'depend on it.')
|
||||
@ -85,7 +86,7 @@ class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, volume_id):
|
||||
volume = api.volume_get(request, volume_id)
|
||||
volume = cinder.volume_get(request, volume_id)
|
||||
return volume
|
||||
|
||||
|
||||
@ -133,6 +134,10 @@ class AttachmentColumn(tables.Column):
|
||||
return safestring.mark_safe(", ".join(attachments))
|
||||
|
||||
|
||||
def get_volume_type(volume):
|
||||
return volume.volume_type if volume.volume_type != "None" else None
|
||||
|
||||
|
||||
class VolumesTableBase(tables.DataTable):
|
||||
STATUS_CHOICES = (
|
||||
("in-use", True),
|
||||
@ -163,6 +168,9 @@ class VolumesTable(VolumesTableBase):
|
||||
name = tables.Column("display_name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:detail")
|
||||
volume_type = tables.Column(get_volume_type,
|
||||
verbose_name=_("Type"),
|
||||
empty_value="-")
|
||||
attachments = AttachmentColumn("attachments",
|
||||
verbose_name=_("Attached To"))
|
||||
|
||||
|
@ -32,24 +32,31 @@ from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
class VolumeViewTests(test.TestCase):
|
||||
@test.create_stubs({api: ('volume_create',
|
||||
'volume_snapshot_list'),
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_type_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_volume(self):
|
||||
volume = self.volumes.first()
|
||||
volume_type = self.volume_types.first()
|
||||
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
|
||||
formData = {'name': u'A Volume I Am Making',
|
||||
'description': u'This is a volume I am making for a test.',
|
||||
'method': u'CreateForm',
|
||||
'size': 50, 'snapshot_source': ''}
|
||||
'type': volume_type.name,
|
||||
'size': 50,
|
||||
'snapshot_source': ''}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
api.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_snapshots.list())
|
||||
api.volume_create(IsA(http.HttpRequest),
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
formData['type'],
|
||||
snapshot_id=None).AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
@ -60,9 +67,11 @@ class VolumeViewTests(test.TestCase):
|
||||
redirect_url = reverse('horizon:project:volumes:index')
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({api: ('volume_create',
|
||||
'volume_snapshot_list'),
|
||||
cinder: ('volume_snapshot_get',),
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_snapshot_get',
|
||||
'volume_get',
|
||||
'volume_type_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_volume_from_snapshot(self):
|
||||
volume = self.volumes.first()
|
||||
@ -71,28 +80,38 @@ class VolumeViewTests(test.TestCase):
|
||||
formData = {'name': u'A Volume I Am Making',
|
||||
'description': u'This is a volume I am making for a test.',
|
||||
'method': u'CreateForm',
|
||||
'size': 50, 'snapshot_source': snapshot.id}
|
||||
'size': 50,
|
||||
'type': '',
|
||||
'snapshot_source': snapshot.id}
|
||||
|
||||
# first call- with url param
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
cinder.volume_snapshot_get(IsA(http.HttpRequest),
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
api.volume_create(IsA(http.HttpRequest),
|
||||
cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\
|
||||
AndReturn(self.volumes.first())
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
'',
|
||||
snapshot_id=snapshot.id).\
|
||||
AndReturn(volume)
|
||||
# second call- with dropdown
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
api.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_snapshots.list())
|
||||
cinder.volume_snapshot_get(IsA(http.HttpRequest),
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
api.volume_create(IsA(http.HttpRequest),
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
formData['description'],
|
||||
'',
|
||||
snapshot_id=snapshot.id).\
|
||||
AndReturn(volume)
|
||||
|
||||
@ -114,7 +133,9 @@ class VolumeViewTests(test.TestCase):
|
||||
redirect_url = reverse('horizon:project:volumes:index')
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_get',),
|
||||
@test.create_stubs({cinder: ('volume_snapshot_get',
|
||||
'volume_type_list',
|
||||
'volume_get',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_volume_from_snapshot_invalid_size(self):
|
||||
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
|
||||
@ -124,9 +145,13 @@ class VolumeViewTests(test.TestCase):
|
||||
'method': u'CreateForm',
|
||||
'size': 20, 'snapshot_source': snapshot.id}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
cinder.volume_snapshot_get(IsA(http.HttpRequest),
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\
|
||||
AndReturn(self.volumes.first())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
@ -140,7 +165,7 @@ class VolumeViewTests(test.TestCase):
|
||||
"The volume size cannot be less than the "
|
||||
"snapshot size (40GB)")
|
||||
|
||||
@test.create_stubs({api: ('volume_snapshot_list',),
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_volume_gb_used_over_alloted_quota(self):
|
||||
usage = {'gigabytes': {'available': 100, 'used': 20}}
|
||||
@ -149,8 +174,10 @@ class VolumeViewTests(test.TestCase):
|
||||
'method': u'CreateForm',
|
||||
'size': 5000}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
api.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_snapshots.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
|
||||
@ -163,7 +190,7 @@ class VolumeViewTests(test.TestCase):
|
||||
' have 100GB of your quota available.']
|
||||
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
|
||||
|
||||
@test.create_stubs({api: ('volume_snapshot_list',),
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_create_volume_number_over_alloted_quota(self):
|
||||
usage = {'gigabytes': {'available': 100, 'used': 20},
|
||||
@ -173,8 +200,10 @@ class VolumeViewTests(test.TestCase):
|
||||
'method': u'CreateForm',
|
||||
'size': 10}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
api.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_snapshots.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
|
||||
|
||||
@ -187,21 +216,23 @@ class VolumeViewTests(test.TestCase):
|
||||
' volumes.']
|
||||
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
|
||||
|
||||
@test.create_stubs({api: ('volume_list',
|
||||
'volume_delete',
|
||||
'server_list')})
|
||||
@test.create_stubs({cinder: ('volume_list',
|
||||
'volume_delete',),
|
||||
api.nova: ('server_list',)})
|
||||
def test_delete_volume(self):
|
||||
volume = self.volumes.first()
|
||||
formData = {'action':
|
||||
'volumes__delete__%s' % volume.id}
|
||||
|
||||
api.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(self.volumes.list())
|
||||
api.volume_delete(IsA(http.HttpRequest), volume.id)
|
||||
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
|
||||
api.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
cinder.volume_delete(IsA(http.HttpRequest), volume.id)
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(self.volumes.list())
|
||||
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -209,9 +240,9 @@ class VolumeViewTests(test.TestCase):
|
||||
res = self.client.post(url, formData, follow=True)
|
||||
self.assertMessageCount(res, count=0)
|
||||
|
||||
@test.create_stubs({api: ('volume_list',
|
||||
'volume_delete',
|
||||
'server_list')})
|
||||
@test.create_stubs({cinder: ('volume_list',
|
||||
'volume_delete',),
|
||||
api.nova: ('server_list',)})
|
||||
def test_delete_volume_error_existing_snapshot(self):
|
||||
volume = self.volumes.first()
|
||||
formData = {'action':
|
||||
@ -219,14 +250,16 @@ class VolumeViewTests(test.TestCase):
|
||||
exc = self.exceptions.cinder.__class__(400,
|
||||
"error: dependent snapshots")
|
||||
|
||||
api.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(self.volumes.list())
|
||||
api.volume_delete(IsA(http.HttpRequest), volume.id). \
|
||||
cinder.volume_delete(IsA(http.HttpRequest), volume.id).\
|
||||
AndRaise(exc)
|
||||
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
|
||||
api.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn(self.volumes.list())
|
||||
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
|
||||
api.nova.server_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.servers.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -238,12 +271,12 @@ class VolumeViewTests(test.TestCase):
|
||||
u'One or more snapshots depend on it.' %
|
||||
volume.display_name)
|
||||
|
||||
@test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)})
|
||||
@test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)})
|
||||
def test_edit_attachments(self):
|
||||
volume = self.volumes.first()
|
||||
servers = self.servers.list()
|
||||
|
||||
api.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
|
||||
cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -258,7 +291,7 @@ class VolumeViewTests(test.TestCase):
|
||||
self.assertTrue(isinstance(form.fields['device'].widget,
|
||||
widgets.TextInput))
|
||||
|
||||
@test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)})
|
||||
@test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)})
|
||||
def test_edit_attachments_cannot_set_mount_point(self):
|
||||
PREV = settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point']
|
||||
settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False
|
||||
@ -266,7 +299,7 @@ class VolumeViewTests(test.TestCase):
|
||||
volume = self.volumes.first()
|
||||
servers = self.servers.list()
|
||||
|
||||
api.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
|
||||
cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -278,13 +311,13 @@ class VolumeViewTests(test.TestCase):
|
||||
widgets.HiddenInput))
|
||||
settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = PREV
|
||||
|
||||
@test.create_stubs({api: ('volume_get',),
|
||||
@test.create_stubs({cinder: ('volume_get',),
|
||||
api.nova: ('server_get', 'server_list',)})
|
||||
def test_edit_attachments_attached_volume(self):
|
||||
server = self.servers.first()
|
||||
volume = self.volumes.list()[0]
|
||||
|
||||
api.volume_get(IsA(http.HttpRequest), volume.id) \
|
||||
cinder.volume_get(IsA(http.HttpRequest), volume.id) \
|
||||
.AndReturn(volume)
|
||||
api.nova.server_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.servers.list())
|
||||
|
@ -30,6 +30,7 @@ from horizon import tables
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.usage import quotas
|
||||
from .forms import CreateForm, AttachForm, CreateSnapshotForm
|
||||
from .tables import AttachmentsTable, VolumesTable
|
||||
@ -39,22 +40,18 @@ from .tabs import VolumeDetailTabs
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = VolumesTable
|
||||
template_name = 'project/volumes/index.html'
|
||||
|
||||
class VolumeTableMixIn(object):
|
||||
def _get_volumes(self, search_opts=None):
|
||||
try:
|
||||
return api.volume_list(self.request, search_opts=search_opts)
|
||||
return cinder.volume_list(self.request, search_opts=search_opts)
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume list.'))
|
||||
|
||||
def _get_instances(self):
|
||||
try:
|
||||
return api.server_list(self.request)
|
||||
return api.nova.server_list(self.request)
|
||||
except:
|
||||
instance_list = []
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve volume/instance "
|
||||
"attachment information"))
|
||||
@ -73,6 +70,11 @@ class IndexView(tables.DataTableView):
|
||||
server_id = att.get('server_id', None)
|
||||
att['instance'] = instances.get(server_id, None)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView, VolumeTableMixIn):
|
||||
table_class = VolumesTable
|
||||
template_name = 'project/volumes/index.html'
|
||||
|
||||
def get_data(self):
|
||||
volumes = self._get_volumes()
|
||||
instances = self._get_instances()
|
||||
@ -124,7 +126,7 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
|
||||
if not hasattr(self, "_object"):
|
||||
volume_id = self.kwargs['volume_id']
|
||||
try:
|
||||
self._object = api.volume_get(self.request, volume_id)
|
||||
self._object = cinder.volume_get(self.request, volume_id)
|
||||
except:
|
||||
self._object = None
|
||||
exceptions.handle(self.request,
|
||||
|
@ -14,7 +14,8 @@
|
||||
|
||||
import json
|
||||
|
||||
from novaclient.v1_1 import (flavors, keypairs, servers, volumes, quotas,
|
||||
from novaclient.v1_1 import (flavors, keypairs, servers, volumes,
|
||||
volume_types, quotas,
|
||||
floating_ips, usage, certs,
|
||||
volume_snapshots as vol_snaps,
|
||||
security_group_rules as rules,
|
||||
@ -145,6 +146,7 @@ def data(TEST):
|
||||
TEST.usages = TestDataContainer()
|
||||
TEST.certs = TestDataContainer()
|
||||
TEST.volume_snapshots = TestDataContainer()
|
||||
TEST.volume_types = TestDataContainer()
|
||||
|
||||
# Volumes
|
||||
volume = volumes.Volume(volumes.VolumeManager(None),
|
||||
@ -154,6 +156,7 @@ def data(TEST):
|
||||
size=40,
|
||||
display_name='Volume name',
|
||||
created_at='2012-04-01 10:30:00',
|
||||
volume_type=None,
|
||||
attachments=[]))
|
||||
nameless_volume = volumes.Volume(volumes.VolumeManager(None),
|
||||
dict(id="3b189ac8-9166-ac7f-90c9-16c8bf9e01ac",
|
||||
@ -164,6 +167,7 @@ def data(TEST):
|
||||
display_description='',
|
||||
device="/dev/hda",
|
||||
created_at='2010-11-21 18:34:25',
|
||||
volume_type='vol_type_1',
|
||||
attachments=[{"id": "1", "server_id": '1',
|
||||
"device": "/dev/hda"}]))
|
||||
attached_volume = volumes.Volume(volumes.VolumeManager(None),
|
||||
@ -175,12 +179,21 @@ def data(TEST):
|
||||
display_description='',
|
||||
device="/dev/hdk",
|
||||
created_at='2011-05-01 11:54:33',
|
||||
volume_type='vol_type_2',
|
||||
attachments=[{"id": "2", "server_id": '1',
|
||||
"device": "/dev/hdk"}]))
|
||||
TEST.volumes.add(volume)
|
||||
TEST.volumes.add(nameless_volume)
|
||||
TEST.volumes.add(attached_volume)
|
||||
|
||||
vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': 1,
|
||||
'name': 'vol_type_1'})
|
||||
vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||
{'id': 2,
|
||||
'name': 'vol_type_2'})
|
||||
TEST.volume_types.add(vol_type1, vol_type2)
|
||||
|
||||
# Flavors
|
||||
flavor_1 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "1",
|
||||
|
Loading…
Reference in New Issue
Block a user