Rollup of volume fixes.
* Standardizes volume attachment wording. * Defensive coding for missing values in attachment dict. Fixes bug 1004412 and bug 1004337. * Adds polling to Snapshots and Volume Snapshots tables. Fixes bug 1005805. * Removes an erroneous link on the Volume Snapshots table. Fixes bug 1005806. * Correts the "type" property on the Images and Snapshots tables. Fixes bug 1011910. This seems to restore all supported functionality related to volumes using devstack master. Change-Id: Ie9b7aec06fa1bb7628cd854fb49c02aab14451ea
This commit is contained in:
parent
ede50840aa
commit
155bfb72c1
@ -485,9 +485,14 @@ def volume_get(request, volume_id):
|
||||
volume_data = cinderclient(request).volumes.get(volume_id)
|
||||
|
||||
for attachment in volume_data.attachments:
|
||||
instance = server_get(request, attachment['server_id'])
|
||||
attachment[u'instance_name'] = instance.name
|
||||
|
||||
if "server_id" in attachment:
|
||||
instance = server_get(request, attachment['server_id'])
|
||||
attachment['instance_name'] = instance.name
|
||||
else:
|
||||
# Nova volume can occasionally send back error'd attachments
|
||||
# the lack a server_id property; to work around that we'll
|
||||
# give the attached instance a generic name.
|
||||
attachment['instance_name'] = _("Unknown instance")
|
||||
return volume_data
|
||||
|
||||
|
||||
@ -516,9 +521,12 @@ def volume_attach(request, volume_id, instance_id, device):
|
||||
device)
|
||||
|
||||
|
||||
def volume_detach(request, instance_id, attachment_id):
|
||||
novaclient(request).volumes.delete_server_volume(
|
||||
instance_id, attachment_id)
|
||||
def volume_detach(request, instance_id, att_id):
|
||||
novaclient(request).volumes.delete_server_volume(instance_id, att_id)
|
||||
|
||||
|
||||
def volume_snapshot_get(request, snapshot_id):
|
||||
return cinderclient(request).volume_snapshots.get(snapshot_id)
|
||||
|
||||
|
||||
def volume_snapshot_list(request):
|
||||
|
@ -24,9 +24,9 @@ from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.core import validators
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
|
||||
|
||||
@ -46,10 +46,9 @@ class CreateKeypair(forms.SelfHandlingForm):
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:keypairs:download',
|
||||
keypair_name=data['name'])
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in CreateKeyPair")
|
||||
messages.error(request,
|
||||
_('Error Creating Keypair: %s') % e.message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Unable to create keypair.'))
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
||||
|
||||
@ -66,8 +65,7 @@ class ImportKeypair(forms.SelfHandlingForm):
|
||||
% data['name'])
|
||||
return shortcuts.redirect(
|
||||
'horizon:nova:access_and_security:index')
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in ImportKeypair")
|
||||
messages.error(request,
|
||||
_('Error Importing Keypair: %s') % e.message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Unable to import keypair.'))
|
||||
return shortcuts.redirect(request.build_absolute_uri())
|
||||
|
@ -77,7 +77,7 @@ class EditImage(tables.LinkAction):
|
||||
|
||||
|
||||
def get_image_type(image):
|
||||
return getattr(image.properties, "image_type", "Image")
|
||||
return getattr(image, "properties", {}).get("image_type", _("Image"))
|
||||
|
||||
|
||||
def get_format(image):
|
||||
|
@ -21,7 +21,7 @@ from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
from ..images.tables import ImagesTable, EditImage, DeleteImage
|
||||
from ..images.tables import ImagesTable, EditImage, DeleteImage, UpdateRow
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -55,3 +55,5 @@ class SnapshotsTable(ImagesTable):
|
||||
table_actions = (DeleteSnapshot,)
|
||||
row_actions = (LaunchSnapshot, EditImage, DeleteSnapshot)
|
||||
pagination_param = "snapshot_marker"
|
||||
row_class = UpdateRow
|
||||
status_columns = ["status"]
|
||||
|
@ -34,11 +34,23 @@ class DeleteVolumeSnapshot(tables.DeleteAction):
|
||||
api.volume_snapshot_delete(request, obj_id)
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, snapshot_id):
|
||||
snapshot = api.nova.volume_snapshot_get(request, snapshot_id)
|
||||
return snapshot
|
||||
|
||||
|
||||
class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
|
||||
volume_id = tables.Column("volume_id", verbose_name=_("Volume ID"))
|
||||
name = tables.Column("display_name", verbose_name=_("Name"))
|
||||
volume_id = tables.Column("volume_id",
|
||||
verbose_name=_("Volume ID"))
|
||||
|
||||
class Meta:
|
||||
name = "volume_snapshots"
|
||||
verbose_name = _("Volume Snapshots")
|
||||
table_actions = (DeleteVolumeSnapshot,)
|
||||
row_actions = (DeleteVolumeSnapshot,)
|
||||
row_class = UpdateRow
|
||||
status_columns = ("status",)
|
||||
|
@ -88,7 +88,7 @@ class InstancesAndVolumesViewTest(test.TestCase):
|
||||
self.assertContains(res, ">80GB<", 1, 200)
|
||||
self.assertContains(res, ">In-Use<", 1, 200)
|
||||
self.assertContains(res, ">server_1<", 2, 200)
|
||||
self.assertContains(res, "(/dev/hdn)", 1, 200)
|
||||
self.assertContains(res, "on /dev/hdn", 1, 200)
|
||||
|
||||
def test_index_server_list_exception(self):
|
||||
self.mox.StubOutWithMock(api, 'server_list')
|
||||
|
@ -25,10 +25,8 @@ Views for Instances and Volumes.
|
||||
import re
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.datastructures import SortedDict
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import exceptions
|
||||
@ -45,24 +43,28 @@ class IndexView(tables.MultiTableView):
|
||||
template_name = 'nova/instances_and_volumes/index.html'
|
||||
|
||||
def get_instances_data(self):
|
||||
# Gather our instances
|
||||
try:
|
||||
instances = self._get_instances()
|
||||
except:
|
||||
instances = []
|
||||
exceptions.handle(self.request, _('Unable to retrieve instances.'))
|
||||
# Gather our flavors and correlate our instances to them
|
||||
if instances:
|
||||
if not hasattr(self, "_instances"):
|
||||
# Gather our instances
|
||||
try:
|
||||
flavors = api.flavor_list(self.request)
|
||||
full_flavors = SortedDict([(str(flavor.id), flavor) for \
|
||||
flavor in flavors])
|
||||
for instance in instances:
|
||||
instance.full_flavor = full_flavors[instance.flavor["id"]]
|
||||
instances = self._get_instances()
|
||||
except:
|
||||
msg = _('Unable to retrieve instance size information.')
|
||||
exceptions.handle(self.request, msg)
|
||||
return instances
|
||||
instances = []
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve instances.'))
|
||||
# Gather our flavors and correlate our instances to them
|
||||
if instances:
|
||||
try:
|
||||
flavors = api.flavor_list(self.request)
|
||||
full_flavors = SortedDict([(str(flavor.id), flavor) for \
|
||||
flavor in flavors])
|
||||
for instance in instances:
|
||||
flavor_id = instance.flavor["id"]
|
||||
instance.full_flavor = full_flavors[flavor_id]
|
||||
except:
|
||||
msg = _('Unable to retrieve instance size information.')
|
||||
exceptions.handle(self.request, msg)
|
||||
self._instances = instances
|
||||
return self._instances
|
||||
|
||||
def get_volumes_data(self):
|
||||
# Gather our volumes
|
||||
@ -83,11 +85,12 @@ class IndexView(tables.MultiTableView):
|
||||
volume.display_description = truncated_string + u'...'
|
||||
|
||||
for att in volume.attachments:
|
||||
att['instance'] = instances[att['server_id']]
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
server_id = att.get('server_id', None)
|
||||
att['instance'] = instances.get(server_id, None)
|
||||
except:
|
||||
volumes = []
|
||||
LOG.exception("ClientException in volume index")
|
||||
messages.error(self.request, _('Unable to fetch volumes: %s') % e)
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume list.'))
|
||||
return volumes
|
||||
|
||||
def _get_instances(self):
|
||||
|
@ -7,8 +7,6 @@
|
||||
Views for managing Nova volumes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -16,12 +14,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import api
|
||||
from horizon import forms
|
||||
from horizon import exceptions
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
from ..instances.tables import ACTIVE_STATES
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label="Volume Name")
|
||||
@ -34,12 +29,10 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
api.volume_create(request, data['size'], data['name'],
|
||||
data['description'])
|
||||
message = 'Creating volume "%s"' % data['name']
|
||||
LOG.info(message)
|
||||
messages.info(request, message)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in CreateVolume")
|
||||
messages.error(request,
|
||||
_('Error Creating Volume: %s') % e.message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_("Unable to create volume."))
|
||||
return shortcuts.redirect("horizon:nova:instances_and_volumes:index")
|
||||
|
||||
|
||||
@ -76,6 +69,12 @@ class AttachForm(forms.SelfHandlingForm):
|
||||
self.fields['instance'].choices = instances
|
||||
|
||||
def handle(self, request, data):
|
||||
instance_choices = dict(self.fields['instance'].choices)
|
||||
instance_name = instance_choices.get(data['instance'],
|
||||
_("Unknown instance (None)"))
|
||||
# The name of the instance in the choices list has the ID appended to
|
||||
# it, so let's slice that off...
|
||||
instance_name = instance_name.rsplit(" (")[0]
|
||||
try:
|
||||
api.volume_attach(request,
|
||||
data['volume_id'],
|
||||
@ -83,16 +82,14 @@ class AttachForm(forms.SelfHandlingForm):
|
||||
data['device'])
|
||||
vol_name = api.volume_get(request, data['volume_id']).display_name
|
||||
|
||||
message = (_('Attaching volume %(vol)s to instance '
|
||||
'%(inst)s at %(dev)s') %
|
||||
{"vol": vol_name, "inst": data['instance'],
|
||||
"dev": data['device']})
|
||||
LOG.info(message)
|
||||
message = _('Attaching volume %(vol)s to instance '
|
||||
'%(inst)s on %(dev)s.') % {"vol": vol_name,
|
||||
"inst": instance_name,
|
||||
"dev": data['device']}
|
||||
messages.info(request, message)
|
||||
except novaclient_exceptions.ClientException, e:
|
||||
LOG.exception("ClientException in AttachVolume")
|
||||
messages.error(request,
|
||||
_('Error attaching volume: %s') % e.message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Unable to attach volume.'))
|
||||
return shortcuts.redirect(
|
||||
"horizon:nova:instances_and_volumes:index")
|
||||
|
||||
@ -118,10 +115,9 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
|
||||
data['description'])
|
||||
|
||||
message = _('Creating volume snapshot "%s"') % data['name']
|
||||
LOG.info(message)
|
||||
messages.info(request, message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Error Creating Volume Snapshot: %(exc)s'))
|
||||
_('Unable to create volume snapshot.'))
|
||||
|
||||
return shortcuts.redirect("horizon:nova:images_and_snapshots:index")
|
||||
|
@ -16,12 +16,14 @@
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from django.template.defaultfilters import title
|
||||
from django.utils import safestring
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import api
|
||||
from horizon import exceptions
|
||||
from horizon import tables
|
||||
|
||||
|
||||
@ -83,21 +85,44 @@ def get_size(volume):
|
||||
return _("%sGB") % volume.size
|
||||
|
||||
|
||||
def get_attachment(volume):
|
||||
attachments = []
|
||||
link = '<a href="%(url)s">%(name)s</a> (%(dev)s)'
|
||||
# Filter out "empty" attachments which the client returns...
|
||||
for attachment in [att for att in volume.attachments if att]:
|
||||
url = reverse("%s:instances:detail" % URL_PREFIX,
|
||||
args=(attachment["server_id"],))
|
||||
# TODO(jake): Make "instance" the instance name
|
||||
vals = {"url": url,
|
||||
"name": attachment["instance"].name
|
||||
if "instance" in attachment else None,
|
||||
"instance": attachment["server_id"],
|
||||
"dev": attachment["device"]}
|
||||
attachments.append(link % vals)
|
||||
return safestring.mark_safe(", ".join(attachments))
|
||||
def get_attachment_name(request, attachment):
|
||||
server_id = attachment.get("server_id", None)
|
||||
if "instance" in attachment:
|
||||
name = attachment["instance"].name
|
||||
else:
|
||||
try:
|
||||
server = api.nova.server_get(request, server_id)
|
||||
name = server.name
|
||||
except:
|
||||
name = None
|
||||
exceptions.handle(request, _("Unable to retrieve "
|
||||
"attachment information."))
|
||||
try:
|
||||
url = reverse("%s:instances:detail" % URL_PREFIX, args=(server_id,))
|
||||
instance = '<a href="%s">%s</a>' % (url, name)
|
||||
except NoReverseMatch:
|
||||
instance = name
|
||||
return instance
|
||||
|
||||
|
||||
class AttachmentColumn(tables.Column):
|
||||
"""
|
||||
Customized column class that does complex processing on the attachments
|
||||
for a volume instance.
|
||||
"""
|
||||
def get_raw_data(self, volume):
|
||||
request = self.table._meta.request
|
||||
link = _('Attached to %(instance)s on %(dev)s')
|
||||
attachments = []
|
||||
# Filter out "empty" attachments which the client returns...
|
||||
for attachment in [att for att in volume.attachments if att]:
|
||||
# When a volume is first attached it may return the server_id
|
||||
# without the server name...
|
||||
instance = get_attachment_name(request, attachment)
|
||||
vals = {"instance": instance,
|
||||
"dev": attachment["device"]}
|
||||
attachments.append(link % vals)
|
||||
return safestring.mark_safe(", ".join(attachments))
|
||||
|
||||
|
||||
class VolumesTableBase(tables.DataTable):
|
||||
@ -126,7 +151,7 @@ class VolumesTable(VolumesTableBase):
|
||||
name = tables.Column("display_name",
|
||||
verbose_name=_("Name"),
|
||||
link="%s:volumes:detail" % URL_PREFIX)
|
||||
attachments = tables.Column(get_attachment,
|
||||
attachments = AttachmentColumn("attachments",
|
||||
verbose_name=_("Attached To"))
|
||||
|
||||
class Meta:
|
||||
@ -141,30 +166,42 @@ class VolumesTable(VolumesTableBase):
|
||||
class DetachVolume(tables.BatchAction):
|
||||
name = "detach"
|
||||
action_present = _("Detach")
|
||||
action_past = _("Detached")
|
||||
action_past = _("Detaching") # This action is asynchronous.
|
||||
data_type_singular = _("Volume")
|
||||
data_type_plural = _("Volumes")
|
||||
classes = ('btn-danger', 'btn-detach')
|
||||
|
||||
def action(self, request, obj_id):
|
||||
instance_id = self.table.get_object_by_id(obj_id)['server_id']
|
||||
api.volume_detach(request, instance_id, obj_id)
|
||||
attachment = self.table.get_object_by_id(obj_id)
|
||||
api.volume_detach(request, attachment.get('server_id', None), obj_id)
|
||||
|
||||
def get_success_url(self, request):
|
||||
return reverse('%s:index' % URL_PREFIX)
|
||||
|
||||
|
||||
class AttachedInstanceColumn(tables.Column):
|
||||
"""
|
||||
Customized column class that does complex processing on the attachments
|
||||
for a volume instance.
|
||||
"""
|
||||
def get_raw_data(self, attachment):
|
||||
request = self.table._meta.request
|
||||
return safestring.mark_safe(get_attachment_name(request, attachment))
|
||||
|
||||
|
||||
class AttachmentsTable(tables.DataTable):
|
||||
instance = tables.Column("instance_name", verbose_name=_("Instance Name"))
|
||||
instance = AttachedInstanceColumn(get_attachment_name,
|
||||
verbose_name=_("Instance"))
|
||||
device = tables.Column("device")
|
||||
|
||||
def get_object_id(self, obj):
|
||||
return obj['id']
|
||||
|
||||
def get_object_display(self, obj):
|
||||
vals = {"dev": obj['device'],
|
||||
"instance": obj['server_id']}
|
||||
return "Attachment %(dev)s on %(instance)s" % vals
|
||||
def get_object_display(self, attachment):
|
||||
instance_name = get_attachment_name(self._meta.request, attachment)
|
||||
vals = {"dev": attachment['device'],
|
||||
"instance_name": strip_tags(instance_name)}
|
||||
return _("%(dev)s on instance %(instance_name)s") % vals
|
||||
|
||||
def get_object_by_id(self, obj_id):
|
||||
for obj in self.data:
|
||||
|
@ -48,6 +48,7 @@ class VolumeViewTests(test.TestCase):
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_edit_attachments_attached_volume(self):
|
||||
server = self.servers.first()
|
||||
servers = deepcopy(self.servers)
|
||||
active_server = deepcopy(self.servers.first())
|
||||
active_server.status = 'ACTIVE'
|
||||
@ -57,12 +58,14 @@ class VolumeViewTests(test.TestCase):
|
||||
volume = deepcopy(self.volumes.first())
|
||||
volume.id = "2"
|
||||
volume.status = "in-use"
|
||||
volume.attachments = [{"id": "1", "server_id": "3",
|
||||
volume.attachments = [{"id": "1", "server_id": server.id,
|
||||
"device": "/dev/hdn"}]
|
||||
volumes.add(volume)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'volume_get')
|
||||
self.mox.StubOutWithMock(api.nova, 'server_list')
|
||||
self.mox.StubOutWithMock(api.nova, 'server_get')
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
|
||||
api.volume_get(IsA(http.HttpRequest), volume.id) \
|
||||
.AndReturn(volume)
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers.list())
|
||||
@ -104,7 +107,6 @@ class VolumeViewTests(test.TestCase):
|
||||
self.assertContains(res, "<dd>40 GB</dd>", 1, 200)
|
||||
self.assertContains(res, "<dd>04/01/12 at 10:30:00</dd>", 1, 200)
|
||||
self.assertContains(res, "<a href=\"/nova/instances_and_volumes/"
|
||||
"instances/1/detail\"><strong>server_1</strong> "
|
||||
"(1)</a>", 1, 200)
|
||||
"instances/1/detail\">server_1</a>", 1, 200)
|
||||
|
||||
self.assertNoMessages()
|
||||
|
@ -83,11 +83,9 @@
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
{% for volume in instance.volumes %}
|
||||
<dt>{% trans "Attached On" %} {{ volume.device }}</dt>
|
||||
<dt>{% trans "Attached To" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url horizon:nova:instances_and_volumes:volumes:detail volume.volumeId %}">
|
||||
<strong>{{ volume.name }}</strong> ({{ volume.id }})
|
||||
</a>
|
||||
<a href="{% url horizon:nova:instances_and_volumes:volumes:detail volume.volumeId %}">{{ volume.name }}</a><span> {% trans "on" %} {{ volume.device }}</span>
|
||||
</dd>
|
||||
{% empty %}
|
||||
<dt>{% trans "Volume" %}</dt>
|
||||
|
@ -38,7 +38,7 @@
|
||||
<dt>{% trans "Attached To" %}</dt>
|
||||
<dd>
|
||||
{% url horizon:nova:instances_and_volumes:instances:detail attachment.server_id as instance_url%}
|
||||
<a href="{{ instance_url }}"><strong>{{ attachment.instance.name }}</strong> ({{ attachment.instance.id }})</a>
|
||||
<a href="{{ instance_url }}">{{ attachment.instance.name }}</a>
|
||||
<span> {% trans "on" %} {{ attachment.device }}</span>
|
||||
</dd>
|
||||
{% empty %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user