Merge "Upload volume to image service"

This commit is contained in:
Jenkins 2014-09-01 05:44:06 +00:00 committed by Gerrit Code Review
commit 9416662e42
9 changed files with 228 additions and 1 deletions

View File

@ -245,6 +245,15 @@ def volume_reset_state(request, volume_id, state):
return cinderclient(request).volumes.reset_state(volume_id, state)
def volume_upload_to_image(request, volume_id, force, image_name,
container_format, disk_format):
return cinderclient(request).volumes.upload_to_image(volume_id,
force,
image_name,
container_format,
disk_format)
def volume_snapshot_get(request, snapshot_id):
snapshot = cinderclient(request).volume_snapshots.get(snapshot_id)
return VolumeSnapshot(snapshot)

View File

@ -20,6 +20,7 @@
"volume:get_all_snapshots": [],
"volume:extend": [],
"volume:retype": [],
"volume:upload_to_image": [],
"volume_extension:types_manage": [["rule:admin_api"]],
"volume_extension:types_extra_specs": [["rule:admin_api"]],

View File

@ -0,0 +1,42 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:volumes:upload_to_image' volume.id %}{% endblock %}
{% block modal_id %}update_volume_modal{% endblock %}
{% block modal-header %}{% trans "Upload Volume to Image" %}{% 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 %}
From here you can upload the volume to the Image Service as an image.
This is equivalent to the <tt>cinder upload-to-image</tt> command.
{% endblocktrans %}
</p>
<p>{% blocktrans %}
Choose "Disk Format" for the image. The volume images are created with
the QEMU disk image utility.
{% endblocktrans %}
</p>
{% if volume.status == 'in-use' %}
<p>{% blocktrans %}
When the volume status is "in-use", you can use "Force" to upload the
volume to an image.
{% endblocktrans %}
</p>
{% endif %}
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Upload" %}" />
<a href="{% url 'horizon:project:volumes:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

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

View File

@ -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"),

View File

@ -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):

View File

@ -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):

View File

@ -40,6 +40,9 @@ urlpatterns = patterns(VIEWS_MOD,
url(r'^(?P<volume_id>[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
url(r'^(?P<volume_id>[^/]+)/upload_to_image/$',
views.UploadToImageView.as_view(),
name='upload_to_image'),
url(r'^(?P<volume_id>[^/]+)/update/$',
views.UpdateView.as_view(),
name='update'),

View File

@ -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'