Merge "Upload volume to image service"
This commit is contained in:
commit
9416662e42
@ -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)
|
||||
|
@ -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"]],
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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"),
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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'),
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user