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:
Tihomir Trifonov 2012-10-25 14:48:27 +03:00
parent cd70bc9284
commit 057d891f31
15 changed files with 389 additions and 108 deletions

View File

@ -77,9 +77,11 @@ def volume_get(request, volume_id):
return volume_data 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, 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): 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): def default_quota_get(request, tenant_id):
return QuotaSet(cinderclient(request).quotas.defaults(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)

View 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

View File

@ -1,10 +1,26 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import tables from horizon import tables
from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.volumes.tables import (UpdateRow, from openstack_dashboard.dashboards.project.volumes.tables import (UpdateRow,
VolumesTable as _VolumesTable, DeleteVolume) 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): class VolumesTable(_VolumesTable):
name = tables.Column("display_name", name = tables.Column("display_name",
verbose_name=_("Name"), verbose_name=_("Name"),
@ -17,3 +33,20 @@ class VolumesTable(_VolumesTable):
row_class = UpdateRow row_class = UpdateRow
table_actions = (DeleteVolume,) table_actions = (DeleteVolume,)
row_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,)

View File

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

View File

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

View File

@ -7,7 +7,11 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{{ table.render }} <div id="volumes">
{{ volumes_table.render }}
</div>
<div id="volume-types">
{{ volume_types_table.render }}
</div>
{% endblock %} {% endblock %}

View File

@ -19,15 +19,21 @@ from django.core.urlresolvers import reverse
from mox import IsA from mox import IsA
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class VolumeTests(test.BaseAdminViewTests): 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): 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()) '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() self.mox.ReplayAll()
@ -37,3 +43,43 @@ class VolumeTests(test.BaseAdminViewTests):
volumes = res.context['volumes_table'].data volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, self.volumes.list()) 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)

View File

@ -1,8 +1,9 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from .views import IndexView, DetailView from .views import IndexView, DetailView, CreateVolumeTypeView
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'), 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'), url(r'^(?P<volume_id>[^/]+)/$', DetailView.as_view(), name='detail'),
) )

View File

@ -18,22 +18,48 @@
Admin views for managing volumes. 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 \ from openstack_dashboard.dashboards.project.volumes.views import \
IndexView as _IndexView, DetailView as _DetailView VolumeTableMixIn, DetailView as _DetailView
from .tables import VolumesTable 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): class IndexView(tables.MultiTableView, VolumeTableMixIn):
table_class = VolumesTable table_classes = (VolumesTable, VolumeTypesTable)
template_name = "admin/volumes/index.html" template_name = "admin/volumes/index.html"
def get_data(self): def get_volumes_data(self):
volumes = self._get_volumes(search_opts={'all_tenants': 1}) volumes = self._get_volumes(search_opts={'all_tenants': 1})
instances = self._get_instances() instances = self._get_instances()
self._set_id_if_nameless(volumes, instances) self._set_id_if_nameless(volumes, instances)
self._set_attachments_string(volumes, instances) self._set_attachments_string(volumes, instances)
return volumes 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): class DetailView(_DetailView):
template_name = "admin/volumes/detail.html" 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)

View File

@ -22,7 +22,7 @@ from django import http
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mox import IsA from mox import IsA
from openstack_dashboard import api from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
@ -38,12 +38,12 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html') self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html')
@test.create_stubs({cinder: ('volume_snapshot_create',)})
def test_create_snapshot_post(self): def test_create_snapshot_post(self):
volume = self.volumes.first() volume = self.volumes.first()
snapshot = self.volume_snapshots.first() snapshot = self.volume_snapshots.first()
self.mox.StubOutWithMock(api, 'volume_snapshot_create') cinder.volume_snapshot_create(IsA(http.HttpRequest),
api.volume_snapshot_create(IsA(http.HttpRequest),
volume.id, volume.id,
snapshot.display_name, snapshot.display_name,
snapshot.display_description) \ snapshot.display_description) \

View File

@ -28,6 +28,8 @@ class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Volume Name")) name = forms.CharField(max_length="255", label=_("Volume Name"))
description = forms.CharField(widget=forms.Textarea, description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False) label=_("Description"), required=False)
type = forms.ChoiceField(label=_("Type"),
required=False)
size = forms.IntegerField(min_value=1, label=_("Size (GB)")) size = forms.IntegerField(min_value=1, label=_("Size (GB)"))
snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"), snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"),
widget=SelectWidget( widget=SelectWidget(
@ -40,6 +42,10 @@ class CreateForm(forms.SelfHandlingForm):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(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): if ("snapshot_id" in request.GET):
try: try:
snapshot = self.get_snapshot(request, snapshot = self.get_snapshot(request,
@ -48,6 +54,13 @@ class CreateForm(forms.SelfHandlingForm):
self.fields['size'].initial = snapshot.size self.fields['size'].initial = snapshot.size
self.fields['snapshot_source'].choices = ((snapshot.id, self.fields['snapshot_source'].choices = ((snapshot.id,
snapshot),) 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 ' self.fields['size'].help_text = _('Volume size must be equal '
'to or greater than the snapshot size (%sGB)' 'to or greater than the snapshot size (%sGB)'
% snapshot.size) % snapshot.size)
@ -56,7 +69,7 @@ class CreateForm(forms.SelfHandlingForm):
_('Unable to load the specified snapshot.')) _('Unable to load the specified snapshot.'))
else: else:
try: try:
snapshots = api.volume_snapshot_list(request) snapshots = cinder.volume_snapshot_list(request)
if snapshots: if snapshots:
choices = [('', _("Choose a snapshot"))] + \ choices = [('', _("Choose a snapshot"))] + \
[(s.id, s) for s in snapshots] [(s.id, s) for s in snapshots]
@ -102,19 +115,22 @@ class CreateForm(forms.SelfHandlingForm):
' volumes.') ' volumes.')
raise ValidationError(error_message) raise ValidationError(error_message)
volume = api.volume_create(request, volume = cinder.volume_create(request,
data['size'], data['size'],
data['name'], data['name'],
data['description'], data['description'],
data['type'],
snapshot_id=snapshot_id) snapshot_id=snapshot_id)
message = 'Creating volume "%s"' % data['name'] message = 'Creating volume "%s"' % data['name']
messages.info(request, message) messages.info(request, message)
return volume return volume
except ValidationError, e: except ValidationError, e:
return self.api_error(e.messages[0]) self.api_error(e.messages[0])
return False
except: except:
exceptions.handle(request, ignore=True) exceptions.handle(request, ignore=True)
return self.api_error(_("Unable to create volume.")) self.api_error(_("Unable to create volume."))
return False
@memoized @memoized
def get_snapshot(self, request, id): def get_snapshot(self, request, id):
@ -176,7 +192,8 @@ class AttachForm(forms.SelfHandlingForm):
data['volume_id'], data['volume_id'],
data['instance'], data['instance'],
data.get('device', '')) 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 ' message = _('Attaching volume %(vol)s to instance '
'%(inst)s on %(dev)s.') % {"vol": vol_name, '%(inst)s on %(dev)s.') % {"vol": vol_name,
@ -206,7 +223,7 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
def handle(self, request, data): def handle(self, request, data):
try: try:
snapshot = api.volume_snapshot_create(request, snapshot = cinder.volume_snapshot_create(request,
data['volume_id'], data['volume_id'],
data['name'], data['name'],
data['description']) data['description'])

View File

@ -26,6 +26,7 @@ from horizon import exceptions
from horizon import tables from horizon import tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -41,7 +42,7 @@ class DeleteVolume(tables.DeleteAction):
obj = self.table.get_object_by_id(obj_id) obj = self.table.get_object_by_id(obj_id)
name = self.table.get_object_display(obj) name = self.table.get_object_display(obj)
try: try:
api.volume_delete(request, obj_id) cinder.volume_delete(request, obj_id)
except: except:
msg = _('Unable to delete volume "%s". One or more snapshots ' msg = _('Unable to delete volume "%s". One or more snapshots '
'depend on it.') 'depend on it.')
@ -85,7 +86,7 @@ class UpdateRow(tables.Row):
ajax = True ajax = True
def get_data(self, request, volume_id): def get_data(self, request, volume_id):
volume = api.volume_get(request, volume_id) volume = cinder.volume_get(request, volume_id)
return volume return volume
@ -133,6 +134,10 @@ class AttachmentColumn(tables.Column):
return safestring.mark_safe(", ".join(attachments)) 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): class VolumesTableBase(tables.DataTable):
STATUS_CHOICES = ( STATUS_CHOICES = (
("in-use", True), ("in-use", True),
@ -163,6 +168,9 @@ class VolumesTable(VolumesTableBase):
name = tables.Column("display_name", name = tables.Column("display_name",
verbose_name=_("Name"), verbose_name=_("Name"),
link="horizon:project:volumes:detail") link="horizon:project:volumes:detail")
volume_type = tables.Column(get_volume_type,
verbose_name=_("Type"),
empty_value="-")
attachments = AttachmentColumn("attachments", attachments = AttachmentColumn("attachments",
verbose_name=_("Attached To")) verbose_name=_("Attached To"))

View File

@ -32,24 +32,31 @@ from openstack_dashboard.usage import quotas
class VolumeViewTests(test.TestCase): class VolumeViewTests(test.TestCase):
@test.create_stubs({api: ('volume_create', @test.create_stubs({cinder: ('volume_create',
'volume_snapshot_list'), 'volume_snapshot_list',
'volume_type_list',),
quotas: ('tenant_quota_usages',)}) quotas: ('tenant_quota_usages',)})
def test_create_volume(self): def test_create_volume(self):
volume = self.volumes.first() volume = self.volumes.first()
volume_type = self.volume_types.first()
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
formData = {'name': u'A Volume I Am Making', formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.', 'description': u'This is a volume I am making for a test.',
'method': u'CreateForm', '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) 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()) AndReturn(self.volume_snapshots.list())
api.volume_create(IsA(http.HttpRequest), cinder.volume_create(IsA(http.HttpRequest),
formData['size'], formData['size'],
formData['name'], formData['name'],
formData['description'], formData['description'],
formData['type'],
snapshot_id=None).AndReturn(volume) snapshot_id=None).AndReturn(volume)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -60,9 +67,11 @@ class VolumeViewTests(test.TestCase):
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url) self.assertRedirectsNoFollow(res, redirect_url)
@test.create_stubs({api: ('volume_create', @test.create_stubs({cinder: ('volume_create',
'volume_snapshot_list'), 'volume_snapshot_list',
cinder: ('volume_snapshot_get',), 'volume_snapshot_get',
'volume_get',
'volume_type_list',),
quotas: ('tenant_quota_usages',)}) quotas: ('tenant_quota_usages',)})
def test_create_volume_from_snapshot(self): def test_create_volume_from_snapshot(self):
volume = self.volumes.first() volume = self.volumes.first()
@ -71,28 +80,38 @@ class VolumeViewTests(test.TestCase):
formData = {'name': u'A Volume I Am Making', formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.', 'description': u'This is a volume I am making for a test.',
'method': u'CreateForm', 'method': u'CreateForm',
'size': 50, 'snapshot_source': snapshot.id} 'size': 50,
'type': '',
'snapshot_source': snapshot.id}
# first call- with url param # 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) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
cinder.volume_snapshot_get(IsA(http.HttpRequest), cinder.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot) 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['size'],
formData['name'], formData['name'],
formData['description'], formData['description'],
'',
snapshot_id=snapshot.id).\ snapshot_id=snapshot.id).\
AndReturn(volume) AndReturn(volume)
# second call- with dropdown # 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) 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()) AndReturn(self.volume_snapshots.list())
cinder.volume_snapshot_get(IsA(http.HttpRequest), cinder.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot) str(snapshot.id)).AndReturn(snapshot)
api.volume_create(IsA(http.HttpRequest), cinder.volume_create(IsA(http.HttpRequest),
formData['size'], formData['size'],
formData['name'], formData['name'],
formData['description'], formData['description'],
'',
snapshot_id=snapshot.id).\ snapshot_id=snapshot.id).\
AndReturn(volume) AndReturn(volume)
@ -114,7 +133,9 @@ class VolumeViewTests(test.TestCase):
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url) 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',)}) quotas: ('tenant_quota_usages',)})
def test_create_volume_from_snapshot_invalid_size(self): def test_create_volume_from_snapshot_invalid_size(self):
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
@ -124,9 +145,13 @@ class VolumeViewTests(test.TestCase):
'method': u'CreateForm', 'method': u'CreateForm',
'size': 20, 'snapshot_source': snapshot.id} '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) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
cinder.volume_snapshot_get(IsA(http.HttpRequest), cinder.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot) 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) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -140,7 +165,7 @@ class VolumeViewTests(test.TestCase):
"The volume size cannot be less than the " "The volume size cannot be less than the "
"snapshot size (40GB)") "snapshot size (40GB)")
@test.create_stubs({api: ('volume_snapshot_list',), @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',),
quotas: ('tenant_quota_usages',)}) quotas: ('tenant_quota_usages',)})
def test_create_volume_gb_used_over_alloted_quota(self): def test_create_volume_gb_used_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20}} usage = {'gigabytes': {'available': 100, 'used': 20}}
@ -149,8 +174,10 @@ class VolumeViewTests(test.TestCase):
'method': u'CreateForm', 'method': u'CreateForm',
'size': 5000} 'size': 5000}
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) 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()) AndReturn(self.volume_snapshots.list())
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
@ -163,7 +190,7 @@ class VolumeViewTests(test.TestCase):
' have 100GB of your quota available.'] ' have 100GB of your quota available.']
self.assertEqual(res.context['form'].errors['__all__'], expected_error) 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',)}) quotas: ('tenant_quota_usages',)})
def test_create_volume_number_over_alloted_quota(self): def test_create_volume_number_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20}, usage = {'gigabytes': {'available': 100, 'used': 20},
@ -173,8 +200,10 @@ class VolumeViewTests(test.TestCase):
'method': u'CreateForm', 'method': u'CreateForm',
'size': 10} 'size': 10}
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) 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()) AndReturn(self.volume_snapshots.list())
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
@ -187,21 +216,23 @@ class VolumeViewTests(test.TestCase):
' volumes.'] ' volumes.']
self.assertEqual(res.context['form'].errors['__all__'], expected_error) self.assertEqual(res.context['form'].errors['__all__'], expected_error)
@test.create_stubs({api: ('volume_list', @test.create_stubs({cinder: ('volume_list',
'volume_delete', 'volume_delete',),
'server_list')}) api.nova: ('server_list',)})
def test_delete_volume(self): def test_delete_volume(self):
volume = self.volumes.first() volume = self.volumes.first()
formData = {'action': formData = {'action':
'volumes__delete__%s' % volume.id} '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()) AndReturn(self.volumes.list())
api.volume_delete(IsA(http.HttpRequest), volume.id) cinder.volume_delete(IsA(http.HttpRequest), volume.id)
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.nova.server_list(IsA(http.HttpRequest)).\
api.volume_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn(self.servers.list())
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn(self.volumes.list()) 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() self.mox.ReplayAll()
@ -209,9 +240,9 @@ class VolumeViewTests(test.TestCase):
res = self.client.post(url, formData, follow=True) res = self.client.post(url, formData, follow=True)
self.assertMessageCount(res, count=0) self.assertMessageCount(res, count=0)
@test.create_stubs({api: ('volume_list', @test.create_stubs({cinder: ('volume_list',
'volume_delete', 'volume_delete',),
'server_list')}) api.nova: ('server_list',)})
def test_delete_volume_error_existing_snapshot(self): def test_delete_volume_error_existing_snapshot(self):
volume = self.volumes.first() volume = self.volumes.first()
formData = {'action': formData = {'action':
@ -219,14 +250,16 @@ class VolumeViewTests(test.TestCase):
exc = self.exceptions.cinder.__class__(400, exc = self.exceptions.cinder.__class__(400,
"error: dependent snapshots") "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()) AndReturn(self.volumes.list())
api.volume_delete(IsA(http.HttpRequest), volume.id). \ cinder.volume_delete(IsA(http.HttpRequest), volume.id).\
AndRaise(exc) AndRaise(exc)
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) api.nova.server_list(IsA(http.HttpRequest)).\
api.volume_list(IsA(http.HttpRequest), search_opts=None).\ AndReturn(self.servers.list())
cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn(self.volumes.list()) 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() self.mox.ReplayAll()
@ -238,12 +271,12 @@ class VolumeViewTests(test.TestCase):
u'One or more snapshots depend on it.' % u'One or more snapshots depend on it.' %
volume.display_name) 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): def test_edit_attachments(self):
volume = self.volumes.first() volume = self.volumes.first()
servers = self.servers.list() 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) api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -258,7 +291,7 @@ class VolumeViewTests(test.TestCase):
self.assertTrue(isinstance(form.fields['device'].widget, self.assertTrue(isinstance(form.fields['device'].widget,
widgets.TextInput)) 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): def test_edit_attachments_cannot_set_mount_point(self):
PREV = settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] PREV = settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point']
settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False
@ -266,7 +299,7 @@ class VolumeViewTests(test.TestCase):
volume = self.volumes.first() volume = self.volumes.first()
servers = self.servers.list() 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) api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -278,13 +311,13 @@ class VolumeViewTests(test.TestCase):
widgets.HiddenInput)) widgets.HiddenInput))
settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = PREV 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',)}) api.nova: ('server_get', 'server_list',)})
def test_edit_attachments_attached_volume(self): def test_edit_attachments_attached_volume(self):
server = self.servers.first() server = self.servers.first()
volume = self.volumes.list()[0] volume = self.volumes.list()[0]
api.volume_get(IsA(http.HttpRequest), volume.id) \ cinder.volume_get(IsA(http.HttpRequest), volume.id) \
.AndReturn(volume) .AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)) \ api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn(self.servers.list()) .AndReturn(self.servers.list())

View File

@ -30,6 +30,7 @@ from horizon import tables
from horizon import tabs from horizon import tabs
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
from .forms import CreateForm, AttachForm, CreateSnapshotForm from .forms import CreateForm, AttachForm, CreateSnapshotForm
from .tables import AttachmentsTable, VolumesTable from .tables import AttachmentsTable, VolumesTable
@ -39,22 +40,18 @@ from .tabs import VolumeDetailTabs
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView): class VolumeTableMixIn(object):
table_class = VolumesTable
template_name = 'project/volumes/index.html'
def _get_volumes(self, search_opts=None): def _get_volumes(self, search_opts=None):
try: try:
return api.volume_list(self.request, search_opts=search_opts) return cinder.volume_list(self.request, search_opts=search_opts)
except: except:
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve volume list.')) _('Unable to retrieve volume list.'))
def _get_instances(self): def _get_instances(self):
try: try:
return api.server_list(self.request) return api.nova.server_list(self.request)
except: except:
instance_list = []
exceptions.handle(self.request, exceptions.handle(self.request,
_("Unable to retrieve volume/instance " _("Unable to retrieve volume/instance "
"attachment information")) "attachment information"))
@ -73,6 +70,11 @@ class IndexView(tables.DataTableView):
server_id = att.get('server_id', None) server_id = att.get('server_id', None)
att['instance'] = instances.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): def get_data(self):
volumes = self._get_volumes() volumes = self._get_volumes()
instances = self._get_instances() instances = self._get_instances()
@ -124,7 +126,7 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
if not hasattr(self, "_object"): if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id'] volume_id = self.kwargs['volume_id']
try: try:
self._object = api.volume_get(self.request, volume_id) self._object = cinder.volume_get(self.request, volume_id)
except: except:
self._object = None self._object = None
exceptions.handle(self.request, exceptions.handle(self.request,

View File

@ -14,7 +14,8 @@
import json 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, floating_ips, usage, certs,
volume_snapshots as vol_snaps, volume_snapshots as vol_snaps,
security_group_rules as rules, security_group_rules as rules,
@ -145,6 +146,7 @@ def data(TEST):
TEST.usages = TestDataContainer() TEST.usages = TestDataContainer()
TEST.certs = TestDataContainer() TEST.certs = TestDataContainer()
TEST.volume_snapshots = TestDataContainer() TEST.volume_snapshots = TestDataContainer()
TEST.volume_types = TestDataContainer()
# Volumes # Volumes
volume = volumes.Volume(volumes.VolumeManager(None), volume = volumes.Volume(volumes.VolumeManager(None),
@ -154,6 +156,7 @@ def data(TEST):
size=40, size=40,
display_name='Volume name', display_name='Volume name',
created_at='2012-04-01 10:30:00', created_at='2012-04-01 10:30:00',
volume_type=None,
attachments=[])) attachments=[]))
nameless_volume = volumes.Volume(volumes.VolumeManager(None), nameless_volume = volumes.Volume(volumes.VolumeManager(None),
dict(id="3b189ac8-9166-ac7f-90c9-16c8bf9e01ac", dict(id="3b189ac8-9166-ac7f-90c9-16c8bf9e01ac",
@ -164,6 +167,7 @@ def data(TEST):
display_description='', display_description='',
device="/dev/hda", device="/dev/hda",
created_at='2010-11-21 18:34:25', created_at='2010-11-21 18:34:25',
volume_type='vol_type_1',
attachments=[{"id": "1", "server_id": '1', attachments=[{"id": "1", "server_id": '1',
"device": "/dev/hda"}])) "device": "/dev/hda"}]))
attached_volume = volumes.Volume(volumes.VolumeManager(None), attached_volume = volumes.Volume(volumes.VolumeManager(None),
@ -175,12 +179,21 @@ def data(TEST):
display_description='', display_description='',
device="/dev/hdk", device="/dev/hdk",
created_at='2011-05-01 11:54:33', created_at='2011-05-01 11:54:33',
volume_type='vol_type_2',
attachments=[{"id": "2", "server_id": '1', attachments=[{"id": "2", "server_id": '1',
"device": "/dev/hdk"}])) "device": "/dev/hdk"}]))
TEST.volumes.add(volume) TEST.volumes.add(volume)
TEST.volumes.add(nameless_volume) TEST.volumes.add(nameless_volume)
TEST.volumes.add(attached_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 # Flavors
flavor_1 = flavors.Flavor(flavors.FlavorManager(None), flavor_1 = flavors.Flavor(flavors.FlavorManager(None),
{'id': "1", {'id': "1",