{% blocktrans %}
+ From here you can upload the volume to the Image Service as an image.
+ This is equivalent to the cinder upload-to-image command.
+ {% endblocktrans %}
+
+
{% blocktrans %}
+ Choose "Disk Format" for the image. The volume images are created with
+ the QEMU disk image utility.
+ {% endblocktrans %}
+
+ {% if volume.status == 'in-use' %}
+
{% blocktrans %}
+ When the volume status is "in-use", you can use "Force" to upload the
+ volume to an image.
+ {% endblocktrans %}
+
+ {% endif %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html
new file mode 100644
index 0000000000..484da4cd1c
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/upload_to_image.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Upload Volume to Image" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Upload Volume to Image") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/volumes/volumes/_upload_to_image.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
index 4222c02ab2..073386dbba 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
@@ -36,6 +36,11 @@ from openstack_dashboard.dashboards.project.images import utils
from openstack_dashboard.dashboards.project.instances import tables
from openstack_dashboard.usage import quotas
+IMAGE_BACKEND_SETTINGS = getattr(settings, 'OPENSTACK_IMAGE_BACKEND', {})
+IMAGE_FORMAT_CHOICES = IMAGE_BACKEND_SETTINGS.get('image_formats', [])
+VALID_DISK_FORMATS = ('raw', 'vmdk', 'vdi', 'qcow2')
+DEFAULT_CONTAINER_FORMAT = 'bare'
+
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Volume Name"))
@@ -511,6 +516,64 @@ class UpdateForm(forms.SelfHandlingForm):
redirect=redirect)
+class UploadToImageForm(forms.SelfHandlingForm):
+ name = forms.CharField(label=_('Volume Name'),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ image_name = forms.CharField(max_length="255", label=_('Image Name'),
+ required=True)
+ disk_format = forms.ChoiceField(label=_('Disk Format'),
+ widget=forms.Select(),
+ required=False)
+ force = forms.BooleanField(label=_("Force"),
+ widget=forms.CheckboxInput(),
+ required=False)
+
+ def __init__(self, request, *args, **kwargs):
+ super(UploadToImageForm, self).__init__(request, *args, **kwargs)
+
+ # 'vhd','iso','aki','ari' and 'ami' disk formats are supported by
+ # glance, but not by qemu-img. qemu-img supports 'vpc', 'cloop', 'cow'
+ # and 'qcow' which are not supported by glance.
+ # I can only use 'raw', 'vmdk', 'vdi' or 'qcow2' so qemu-img will not
+ # have issues when processes image request from cinder.
+ disk_format_choices = [(value, name) for value, name
+ in IMAGE_FORMAT_CHOICES
+ if value in VALID_DISK_FORMATS]
+ self.fields['disk_format'].choices = disk_format_choices
+ self.fields['disk_format'].initial = 'raw'
+ if self.initial['status'] != 'in-use':
+ self.fields['force'].widget = forms.widgets.HiddenInput()
+
+ def handle(self, request, data):
+ volume_id = self.initial['id']
+
+ try:
+ # 'aki','ari','ami' container formats are supported by glance,
+ # but they need matching disk format to use.
+ # Glance usually uses 'bare' for other disk formats except
+ # amazon's. Please check the comment in CreateImageForm class
+ cinder.volume_upload_to_image(request,
+ volume_id,
+ data['force'],
+ data['image_name'],
+ DEFAULT_CONTAINER_FORMAT,
+ data['disk_format'])
+ message = _(
+ 'Successfully sent the request to upload volume to image '
+ 'for volume: "%s"') % data['name']
+ messages.info(request, message)
+
+ return True
+ except Exception:
+ error_message = _(
+ 'Unable to upload volume to image for volume: "%s"') \
+ % data['name']
+ exceptions.handle(request, error_message)
+
+ return False
+
+
class ExtendForm(forms.SelfHandlingForm):
name = forms.CharField(
label=_("Volume Name"),
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
index dbcb41a09f..27b4a016fe 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
@@ -198,6 +198,29 @@ class CreateBackup(tables.LinkAction):
volume.status == "available")
+class UploadToImage(tables.LinkAction):
+ name = "upload_to_image"
+ verbose_name = _("Upload to Image")
+ url = "horizon:project:volumes:volumes:upload_to_image"
+ classes = ("ajax-modal",)
+ icon = "cloud-upload"
+ policy_rules = (("volume", "volume:upload_to_image"),)
+
+ def get_policy_target(self, request, datum=None):
+ project_id = None
+ if datum:
+ project_id = getattr(datum, "os-vol-tenant-attr:tenant_id", None)
+
+ return {"project_id": project_id}
+
+ def allowed(self, request, volume=None):
+ has_image_service_perm = \
+ request.user.has_perm('openstack.services.image')
+
+ return volume.status in ("available", "in-use") and \
+ has_image_service_perm
+
+
class EditVolume(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Volume")
@@ -364,7 +387,7 @@ class VolumesTable(VolumesTableBase):
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
CreateSnapshot, CreateBackup, RetypeVolume,
- DeleteVolume)
+ UploadToImage, DeleteVolume)
class DetachVolume(tables.BatchAction):
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
index 32cb148a77..d4d8a46c3e 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
@@ -1005,6 +1005,47 @@ class VolumeViewTests(test.TestCase):
res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
+ @test.create_stubs({cinder: ('volume_upload_to_image',
+ 'volume_get')})
+ def test_upload_to_image(self):
+ volume = self.cinder_volumes.get(name='v2_volume')
+ loaded_resp = {'container_format': 'bare',
+ 'disk_format': 'raw',
+ 'id': '741fe2ac-aa2f-4cec-82a9-4994896b43fb',
+ 'image_id': '2faa080b-dd56-4bf0-8f0a-0d4627d8f306',
+ 'image_name': 'test',
+ 'size': '2',
+ 'status': 'uploading'}
+
+ form_data = {'id': volume.id,
+ 'name': volume.name,
+ 'image_name': 'testimage',
+ 'force': True,
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+
+ cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
+
+ cinder.volume_upload_to_image(
+ IsA(http.HttpRequest),
+ form_data['id'],
+ form_data['force'],
+ form_data['image_name'],
+ form_data['container_format'],
+ form_data['disk_format']).AndReturn(loaded_resp)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:project:volumes:volumes:upload_to_image',
+ args=[volume.id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertMessageCount(info=1)
+
+ redirect_url = VOLUME_INDEX_URL
+ self.assertRedirectsNoFollow(res, redirect_url)
+
@test.create_stubs({cinder: ('volume_get',
'volume_extend')})
def test_extend_volume(self):
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
index 8c3aed2b7a..01c31ae89e 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
@@ -40,6 +40,9 @@ urlpatterns = patterns(VIEWS_MOD,
url(r'^(?P[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
+ url(r'^(?P[^/]+)/upload_to_image/$',
+ views.UploadToImageView.as_view(),
+ name='upload_to_image'),
url(r'^(?P[^/]+)/update/$',
views.UpdateView.as_view(),
name='update'),
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
index a3c02751ee..f068e9e0f2 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
@@ -148,6 +148,40 @@ class CreateSnapshotView(forms.ModalFormView):
return {'volume_id': self.kwargs["volume_id"]}
+class UploadToImageView(forms.ModalFormView):
+ form_class = project_forms.UploadToImageForm
+ template_name = 'project/volumes/volumes/upload_to_image.html'
+ success_url = reverse_lazy("horizon:project:volumes:index")
+
+ @memoized.memoized_method
+ def get_data(self):
+ try:
+ volume_id = self.kwargs['volume_id']
+ volume = cinder.volume_get(self.request, volume_id)
+ except Exception:
+ error_message = _(
+ 'Unable to retrieve volume information for volume: "%s"') \
+ % volume_id
+ exceptions.handle(self.request,
+ error_message,
+ redirect=self.success_url)
+
+ return volume
+
+ def get_context_data(self, **kwargs):
+ context = super(UploadToImageView, self).get_context_data(**kwargs)
+ context['volume'] = self.get_data()
+
+ return context
+
+ def get_initial(self):
+ volume = self.get_data()
+
+ return {'id': self.kwargs['volume_id'],
+ 'name': volume.name,
+ 'status': volume.status}
+
+
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateForm
template_name = 'project/volumes/volumes/update.html'