Merge "Add Availability Zone to Volume screens"
This commit is contained in:
commit
27b5b9f3fc
@ -28,8 +28,10 @@ from django.conf import settings # noqa
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from cinderclient.v1 import client as cinder_client
|
||||
from cinderclient.v1.contrib import list_extensions as cinder_list_extensions
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon.utils.memoized import memoized # noqa
|
||||
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.api import nova
|
||||
@ -92,10 +94,12 @@ def volume_get(request, volume_id):
|
||||
|
||||
|
||||
def volume_create(request, size, name, description, volume_type,
|
||||
snapshot_id=None, metadata=None, image_id=None):
|
||||
snapshot_id=None, metadata=None, image_id=None,
|
||||
availability_zone=None):
|
||||
return cinderclient(request).volumes.create(size, display_name=name,
|
||||
display_description=description, volume_type=volume_type,
|
||||
snapshot_id=snapshot_id, metadata=metadata, imageRef=image_id)
|
||||
snapshot_id=snapshot_id, metadata=metadata, imageRef=image_id,
|
||||
availability_zone=availability_zone)
|
||||
|
||||
|
||||
def volume_delete(request, volume_id):
|
||||
@ -163,3 +167,25 @@ def tenant_absolute_limits(request):
|
||||
else:
|
||||
limits_dict[limit.name] = limit.value
|
||||
return limits_dict
|
||||
|
||||
|
||||
def availability_zone_list(request, detailed=False):
|
||||
return cinderclient(request).availability_zones.list(detailed=detailed)
|
||||
|
||||
|
||||
@memoized
|
||||
def list_extensions(request):
|
||||
return cinder_list_extensions.ListExtManager(cinderclient(request))\
|
||||
.show_all()
|
||||
|
||||
|
||||
@memoized
|
||||
def extension_supported(request, extension_name):
|
||||
"""
|
||||
This method will determine if Cinder supports a given extension name.
|
||||
"""
|
||||
extensions = list_extensions(request)
|
||||
for extension in extensions:
|
||||
if extension.name == extension_name:
|
||||
return True
|
||||
return False
|
||||
|
@ -51,6 +51,8 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
data_attrs=('size', 'name'),
|
||||
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
||||
required=False)
|
||||
availability_zone = forms.ChoiceField(label=_("Availability Zone"),
|
||||
required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateForm, self).__init__(request, *args, **kwargs)
|
||||
@ -58,6 +60,8 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
self.fields['type'].choices = [("", "")] + \
|
||||
[(type.name, type.name)
|
||||
for type in volume_types]
|
||||
self.fields['availability_zone'].choices = \
|
||||
self.availability_zones(request)
|
||||
|
||||
if ("snapshot_id" in request.GET):
|
||||
try:
|
||||
@ -137,6 +141,34 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
else:
|
||||
del self.fields['volume_source_type']
|
||||
|
||||
# Determine whether the extension for Cinder AZs is enabled
|
||||
def cinder_az_supported(self, request):
|
||||
try:
|
||||
return cinder.extension_supported(request, 'AvailabilityZones')
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to determine if '
|
||||
'availability zones extension '
|
||||
'is supported.'))
|
||||
return False
|
||||
|
||||
def availability_zones(self, request):
|
||||
zone_list = []
|
||||
if self.cinder_az_supported(request):
|
||||
try:
|
||||
zones = api.cinder.availability_zone_list(request)
|
||||
zone_list = [(zone.zoneName, zone.zoneName)
|
||||
for zone in zones if zone.zoneState['available']]
|
||||
zone_list.sort()
|
||||
except Exception:
|
||||
exceptions.handle(request, _('Unable to retrieve availability '
|
||||
'zones.'))
|
||||
if not zone_list:
|
||||
zone_list.insert(0, ("", _("No availability zones found.")))
|
||||
elif len(zone_list) > 0:
|
||||
zone_list.insert(0, ("", _("Any Availability Zone")))
|
||||
|
||||
return zone_list
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
@ -188,6 +220,8 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
|
||||
metadata = {}
|
||||
|
||||
az = data['availability_zone'] or None
|
||||
|
||||
volume = cinder.volume_create(request,
|
||||
data['size'],
|
||||
data['name'],
|
||||
@ -195,7 +229,8 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
data['type'],
|
||||
snapshot_id=snapshot_id,
|
||||
image_id=image_id,
|
||||
metadata=metadata)
|
||||
metadata=metadata,
|
||||
availability_zone=az)
|
||||
message = _('Creating volume "%s"') % data['name']
|
||||
messages.info(request, message)
|
||||
return volume
|
||||
|
@ -199,6 +199,8 @@ class VolumesTable(VolumesTableBase):
|
||||
empty_value="-")
|
||||
attachments = AttachmentColumn("attachments",
|
||||
verbose_name=_("Attached To"))
|
||||
availability_zone = tables.Column("availability_zone",
|
||||
verbose_name=_("Availability Zone"))
|
||||
|
||||
class Meta:
|
||||
name = "volumes"
|
||||
|
@ -35,12 +35,15 @@ from openstack_dashboard.usage import quotas
|
||||
class VolumeViewTests(test.TestCase):
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_type_list',),
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume(self):
|
||||
volume = self.volumes.first()
|
||||
volume_type = self.volume_types.first()
|
||||
az = self.cinder_availability_zones.first().zoneName
|
||||
usage_limit = {'maxTotalVolumeGigabytes': 250,
|
||||
'gigabytesUsed': 20,
|
||||
'volumesUsed': len(self.volumes.list()),
|
||||
@ -50,7 +53,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'method': u'CreateForm',
|
||||
'type': volume_type.name,
|
||||
'size': 50,
|
||||
'snapshot_source': ''}
|
||||
'snapshot_source': '',
|
||||
'availability_zone': az}
|
||||
|
||||
cinder.volume_type_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.volume_types.list())
|
||||
@ -66,6 +70,12 @@ class VolumeViewTests(test.TestCase):
|
||||
filters={'property-owner_id': self.tenant.id,
|
||||
'status': 'active'}) \
|
||||
.AndReturn([[], False])
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -73,7 +83,9 @@ class VolumeViewTests(test.TestCase):
|
||||
formData['type'],
|
||||
metadata={},
|
||||
snapshot_id=None,
|
||||
image_id=None).AndReturn(volume)
|
||||
image_id=None,
|
||||
availability_zone=formData['availability_zone'])\
|
||||
.AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -85,7 +97,9 @@ class VolumeViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_type_list',),
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_dropdown(self):
|
||||
@ -117,6 +131,12 @@ class VolumeViewTests(test.TestCase):
|
||||
.AndReturn([[], False])
|
||||
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
|
||||
AndReturn(usage_limit)
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -124,8 +144,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'',
|
||||
metadata={},
|
||||
snapshot_id=None,
|
||||
image_id=None).\
|
||||
AndReturn(volume)
|
||||
image_id=None,
|
||||
availability_zone=None).AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -138,7 +158,9 @@ class VolumeViewTests(test.TestCase):
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_get',
|
||||
'volume_get',
|
||||
'volume_type_list',),
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_from_snapshot(self):
|
||||
volume = self.volumes.first()
|
||||
@ -162,6 +184,12 @@ class VolumeViewTests(test.TestCase):
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\
|
||||
AndReturn(self.volumes.first())
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -169,8 +197,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'',
|
||||
metadata={},
|
||||
snapshot_id=snapshot.id,
|
||||
image_id=None).\
|
||||
AndReturn(volume)
|
||||
image_id=None,
|
||||
availability_zone=None).AndReturn(volume)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get snapshot from url
|
||||
@ -186,7 +214,9 @@ class VolumeViewTests(test.TestCase):
|
||||
'volume_snapshot_list',
|
||||
'volume_snapshot_get',
|
||||
'volume_get',
|
||||
'volume_type_list',),
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_from_snapshot_dropdown(self):
|
||||
@ -220,6 +250,12 @@ class VolumeViewTests(test.TestCase):
|
||||
AndReturn(usage_limit)
|
||||
cinder.volume_snapshot_get(IsA(http.HttpRequest),
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -227,8 +263,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'',
|
||||
metadata={},
|
||||
snapshot_id=snapshot.id,
|
||||
image_id=None).\
|
||||
AndReturn(volume)
|
||||
image_id=None,
|
||||
availability_zone=None).AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -241,7 +277,9 @@ class VolumeViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_get',
|
||||
'volume_type_list',
|
||||
'volume_get',),
|
||||
'volume_get',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_from_snapshot_invalid_size(self):
|
||||
@ -263,6 +301,12 @@ class VolumeViewTests(test.TestCase):
|
||||
str(snapshot.id)).AndReturn(snapshot)
|
||||
cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\
|
||||
AndReturn(self.volumes.first())
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
|
||||
AndReturn(usage_limit)
|
||||
|
||||
@ -278,7 +322,9 @@ class VolumeViewTests(test.TestCase):
|
||||
"snapshot size (40GB)")
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_type_list',),
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_get',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_from_image(self):
|
||||
@ -301,6 +347,12 @@ class VolumeViewTests(test.TestCase):
|
||||
AndReturn(usage_limit)
|
||||
api.glance.image_get(IsA(http.HttpRequest),
|
||||
str(image.id)).AndReturn(image)
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -308,8 +360,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'',
|
||||
metadata={},
|
||||
snapshot_id=None,
|
||||
image_id=image.id).\
|
||||
AndReturn(volume)
|
||||
image_id=image.id,
|
||||
availability_zone=None).AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -324,7 +376,9 @@ class VolumeViewTests(test.TestCase):
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_type_list',
|
||||
'volume_snapshot_list',),
|
||||
'volume_snapshot_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_get',
|
||||
'image_list_detailed'),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
@ -360,6 +414,12 @@ class VolumeViewTests(test.TestCase):
|
||||
.AndReturn(usage_limit)
|
||||
api.glance.image_get(IsA(http.HttpRequest),
|
||||
str(image.id)).AndReturn(image)
|
||||
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
|
||||
cinder.volume_create(IsA(http.HttpRequest),
|
||||
formData['size'],
|
||||
formData['name'],
|
||||
@ -367,8 +427,8 @@ class VolumeViewTests(test.TestCase):
|
||||
'',
|
||||
metadata={},
|
||||
snapshot_id=None,
|
||||
image_id=image.id).\
|
||||
AndReturn(volume)
|
||||
image_id=image.id,
|
||||
availability_zone=None).AndReturn(volume)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -379,7 +439,9 @@ class VolumeViewTests(test.TestCase):
|
||||
redirect_url = reverse('horizon:project:volumes:index')
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_type_list',),
|
||||
@test.create_stubs({cinder: ('volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_get',
|
||||
'image_list_detailed'),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
@ -400,6 +462,10 @@ class VolumeViewTests(test.TestCase):
|
||||
AndReturn(usage_limit)
|
||||
api.glance.image_get(IsA(http.HttpRequest),
|
||||
str(image.id)).AndReturn(image)
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
|
||||
AndReturn(usage_limit)
|
||||
|
||||
@ -414,7 +480,10 @@ class VolumeViewTests(test.TestCase):
|
||||
"The volume size cannot be less than the "
|
||||
"image size (20.0 GB)")
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',),
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list',
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_gb_used_over_alloted_quota(self):
|
||||
@ -441,6 +510,10 @@ class VolumeViewTests(test.TestCase):
|
||||
filters={'property-owner_id': self.tenant.id,
|
||||
'status': 'active'}) \
|
||||
.AndReturn([[], False])
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
|
||||
AndReturn(usage_limit)
|
||||
|
||||
@ -453,7 +526,10 @@ class VolumeViewTests(test.TestCase):
|
||||
' have 20GB of your quota available.']
|
||||
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',),
|
||||
@test.create_stubs({cinder: ('volume_snapshot_list',
|
||||
'volume_type_list',
|
||||
'availability_zone_list',
|
||||
'extension_supported'),
|
||||
api.glance: ('image_list_detailed',),
|
||||
quotas: ('tenant_limit_usages',)})
|
||||
def test_create_volume_number_over_alloted_quota(self):
|
||||
@ -480,6 +556,10 @@ class VolumeViewTests(test.TestCase):
|
||||
filters={'property-owner_id': self.tenant.id,
|
||||
'status': 'active'}) \
|
||||
.AndReturn([[], False])
|
||||
cinder.extension_supported(IsA(http.HttpRequest), 'AvailabilityZones')\
|
||||
.AndReturn(True)
|
||||
cinder.availability_zone_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.cinder_availability_zones.list())
|
||||
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
|
||||
AndReturn(usage_limit)
|
||||
|
||||
|
@ -12,7 +12,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from cinderclient.v1 import availability_zones
|
||||
from cinderclient.v1 import quotas
|
||||
|
||||
from openstack_dashboard.api import base
|
||||
from openstack_dashboard.usage import quotas as usage_quotas
|
||||
|
||||
@ -22,6 +24,7 @@ from openstack_dashboard.test.test_data import utils
|
||||
def data(TEST):
|
||||
TEST.cinder_quotas = utils.TestDataContainer()
|
||||
TEST.cinder_quota_usages = utils.TestDataContainer()
|
||||
TEST.cinder_availability_zones = utils.TestDataContainer()
|
||||
|
||||
# Quota Sets
|
||||
quota_data = dict(volumes='1',
|
||||
@ -44,3 +47,18 @@ def data(TEST):
|
||||
quota_usage.tally(k, v['used'])
|
||||
|
||||
TEST.cinder_quota_usages.add(quota_usage)
|
||||
|
||||
# Availability Zones
|
||||
# Cinder returns the following structure from os-availability-zone
|
||||
# {"availabilityZoneInfo":
|
||||
# [{"zoneState": {"available": true}, "zoneName": "nova"}]}
|
||||
# Note that the default zone is still "nova" even though this is cinder
|
||||
TEST.cinder_availability_zones.add(
|
||||
availability_zones.AvailabilityZone(
|
||||
availability_zones.AvailabilityZoneManager(None),
|
||||
{
|
||||
'zoneName': 'nova',
|
||||
'zoneState': {'available': True}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user