Merge "Adds support for volume snapshots (volume snapshots table and ability to boot from a volume snapshot)."
This commit is contained in:
commit
2e914f9578
@ -455,3 +455,16 @@ def volume_attach(request, volume_id, instance_id, device):
|
||||
def volume_detach(request, instance_id, attachment_id):
|
||||
novaclient(request).volumes.delete_server_volume(
|
||||
instance_id, attachment_id)
|
||||
|
||||
|
||||
def volume_snapshot_list(request):
|
||||
return novaclient(request).volume_snapshots.list()
|
||||
|
||||
|
||||
def volume_snapshot_create(request, volume_id, name, description):
|
||||
return novaclient(request).volume_snapshots.create(
|
||||
volume_id, display_name=name, display_description=description)
|
||||
|
||||
|
||||
def volume_snapshot_delete(request, snapshot_id):
|
||||
novaclient(request).volume_snapshots.delete(snapshot_id)
|
||||
|
@ -99,7 +99,7 @@ class LaunchForm(forms.SelfHandlingForm):
|
||||
widget=forms.CheckboxSelectMultiple(),
|
||||
help_text=_("Launch instance in these "
|
||||
"security groups."))
|
||||
volume = forms.ChoiceField(label=_("Volume"),
|
||||
volume = forms.ChoiceField(label=_("Volume or Volume Snapshot"),
|
||||
required=False,
|
||||
help_text=_("Volume to boot from."))
|
||||
device_name = forms.CharField(label=_("Device Name"),
|
||||
@ -131,10 +131,10 @@ class LaunchForm(forms.SelfHandlingForm):
|
||||
delete_on_terminate = 1
|
||||
else:
|
||||
delete_on_terminate = 0
|
||||
dev_spec = {data['device_name']:
|
||||
("%s:::%s" % (data['volume'], delete_on_terminate))}
|
||||
dev_mapping = {data['device_name']:
|
||||
("%s::%s" % (data['volume'], delete_on_terminate))}
|
||||
else:
|
||||
dev_spec = None
|
||||
dev_mapping = None
|
||||
|
||||
api.server_create(request,
|
||||
data['name'],
|
||||
@ -143,7 +143,7 @@ class LaunchForm(forms.SelfHandlingForm):
|
||||
data.get('keypair'),
|
||||
normalize_newlines(data.get('user_data')),
|
||||
data.get('security_groups'),
|
||||
dev_spec,
|
||||
dev_mapping,
|
||||
instance_count=int(data.get('count')))
|
||||
messages.success(request,
|
||||
_('Instance "%s" launched.') % data["name"])
|
||||
|
@ -22,6 +22,7 @@ from django import http
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from keystoneclient import exceptions as keystone_exceptions
|
||||
from novaclient.v1_1 import client as nova_client, volume_snapshots
|
||||
from mox import IgnoreArg, IsA
|
||||
|
||||
from horizon import api
|
||||
@ -74,6 +75,16 @@ class ImageViewTests(test.BaseViewTests):
|
||||
volume.displayName = ''
|
||||
self.volumes = (volume,)
|
||||
|
||||
self.volume_snapshot = volume_snapshots.Snapshot(
|
||||
volume_snapshots.SnapshotManager,
|
||||
{'id': 2,
|
||||
'displayName': 'test snapshot',
|
||||
'displayDescription': 'test snapshot description',
|
||||
'size': 40,
|
||||
'status': 'available',
|
||||
'volumeId': 1})
|
||||
self.volume_snapshots = [self.volume_snapshot]
|
||||
|
||||
def test_launch_get(self):
|
||||
IMAGE_ID = 1
|
||||
|
||||
@ -111,14 +122,14 @@ class ImageViewTests(test.BaseViewTests):
|
||||
self.keypairs[0].name)
|
||||
|
||||
def test_launch_post(self):
|
||||
FLAVOR_ID = self.flavors[0].id
|
||||
IMAGE_ID = '1'
|
||||
keypair = self.keypairs[0].name
|
||||
SERVER_NAME = 'serverName'
|
||||
USER_DATA = 'userData'
|
||||
volume = self.volumes[0].id
|
||||
device_name = 'vda'
|
||||
BLOCK_DEVICE_MAPPING = {device_name: "1:::0"}
|
||||
FLAVOR_ID = unicode(self.flavors[0].id)
|
||||
IMAGE_ID = u'1'
|
||||
keypair = unicode(self.keypairs[0].name)
|
||||
SERVER_NAME = u'serverName'
|
||||
USER_DATA = u'userData'
|
||||
volume = u'%s:vol' % self.volumes[0].id
|
||||
device_name = u'vda'
|
||||
BLOCK_DEVICE_MAPPING = {device_name: u"1:vol::0"}
|
||||
|
||||
form_data = {'method': 'LaunchForm',
|
||||
'flavor': FLAVOR_ID,
|
||||
@ -130,8 +141,7 @@ class ImageViewTests(test.BaseViewTests):
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'security_groups': 'default',
|
||||
'volume': volume,
|
||||
'device_name': device_name
|
||||
}
|
||||
'device_name': device_name}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'image_get_meta')
|
||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||
@ -143,9 +153,9 @@ class ImageViewTests(test.BaseViewTests):
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.security_groups)
|
||||
api.image_get_meta(IsA(http.HttpRequest),
|
||||
IMAGE_ID).AndReturn(self.visibleImage)
|
||||
self.security_groups)
|
||||
api.image_get_meta(IsA(http.HttpRequest), IMAGE_ID).AndReturn(
|
||||
self.visibleImage)
|
||||
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes)
|
||||
api.server_create(IsA(http.HttpRequest), SERVER_NAME,
|
||||
str(IMAGE_ID), str(FLAVOR_ID),
|
||||
@ -254,9 +264,8 @@ class ImageViewTests(test.BaseViewTests):
|
||||
api.flavor_list(IgnoreArg()).AndReturn(self.flavors)
|
||||
api.keypair_list(IgnoreArg()).AndReturn(self.keypairs)
|
||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.security_groups)
|
||||
api.image_get_meta(IgnoreArg(),
|
||||
IMAGE_ID).AndReturn(self.visibleImage)
|
||||
self.security_groups)
|
||||
api.image_get_meta(IgnoreArg(), IMAGE_ID).AndReturn(self.visibleImage)
|
||||
api.volume_list(IgnoreArg()).AndReturn(self.volumes)
|
||||
|
||||
exception = keystone_exceptions.ClientException('Failed')
|
||||
@ -270,9 +279,6 @@ class ImageViewTests(test.BaseViewTests):
|
||||
None,
|
||||
instance_count=IsA(int)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
url = reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID])
|
||||
|
@ -110,17 +110,42 @@ class LaunchView(forms.ModalFormView):
|
||||
return security_group_list
|
||||
|
||||
def volume_list(self):
|
||||
volume_options = [("", _("Select Volume"))]
|
||||
|
||||
def _get_volume_select_item(volume):
|
||||
if hasattr(volume, "volumeId"):
|
||||
vol_type = "snap"
|
||||
visible_label = _("Snapshot")
|
||||
else:
|
||||
vol_type = "vol"
|
||||
visible_label = _("Volume")
|
||||
return (("%s:%s" % (volume.id, vol_type)),
|
||||
("%s - %s GB (%s)" % (volume.displayName,
|
||||
volume.size,
|
||||
visible_label)))
|
||||
|
||||
# First add volumes to the list
|
||||
try:
|
||||
volumes = [v for v in api.volume_list(self.request) \
|
||||
if v.status == api.VOLUME_STATE_AVAILABLE]
|
||||
volume_sel = [(v.id, ("%s (%s GB)" % (v.displayName, v.size))) \
|
||||
for v in volumes]
|
||||
volume_sel.insert(0, ("", "Select Volume"))
|
||||
if v.status == api.VOLUME_STATE_AVAILABLE]
|
||||
volume_options.extend(
|
||||
[_get_volume_select_item(vol) for vol in volumes])
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve list of volumes'))
|
||||
volume_sel = []
|
||||
return volume_sel
|
||||
|
||||
# Next add volume snapshots to the list
|
||||
try:
|
||||
snapshots = api.novaclient(self.request).volume_snapshots.list()
|
||||
snapshots = [s for s in snapshots \
|
||||
if s.status == api.VOLUME_STATE_AVAILABLE]
|
||||
volume_options.extend(
|
||||
[_get_volume_select_item(snap) for snap in snapshots])
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve list of volumes'))
|
||||
|
||||
return volume_options
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
|
@ -23,7 +23,7 @@ from horizon.dashboards.nova import dashboard
|
||||
|
||||
|
||||
class Snapshots(horizon.Panel):
|
||||
name = "Snapshots"
|
||||
name = "Instance Snapshots"
|
||||
slug = 'snapshots'
|
||||
|
||||
|
||||
|
@ -32,6 +32,6 @@ class DeleteSnapshot(DeleteImage):
|
||||
class SnapshotsTable(ImagesTable):
|
||||
class Meta:
|
||||
name = "snapshots"
|
||||
verbose_name = _("Snapshots")
|
||||
verbose_name = _("Instance Snapshots")
|
||||
table_actions = (DeleteSnapshot,)
|
||||
row_actions = (LaunchImage, EditImage, DeleteSnapshot)
|
||||
|
@ -32,13 +32,14 @@ from horizon import exceptions
|
||||
from horizon import tables
|
||||
from .images.tables import ImagesTable
|
||||
from .snapshots.tables import SnapshotsTable
|
||||
from .volume_snapshots.tables import VolumeSnapshotsTable
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.MultiTableView):
|
||||
table_classes = (ImagesTable, SnapshotsTable)
|
||||
table_classes = (ImagesTable, SnapshotsTable, VolumeSnapshotsTable)
|
||||
template_name = 'nova/images_and_snapshots/index.html'
|
||||
|
||||
def get_images_data(self):
|
||||
@ -59,3 +60,12 @@ class IndexView(tables.MultiTableView):
|
||||
snapshots = []
|
||||
exceptions.handle(self.request, _("Unable to retrieve snapshots."))
|
||||
return snapshots
|
||||
|
||||
def get_volume_snapshots_data(self):
|
||||
try:
|
||||
snapshots = api.volume_snapshot_list(self.request)
|
||||
except:
|
||||
snapshots = []
|
||||
exceptions.handle(self.request, _("Unable to retrieve "
|
||||
"volume snapshots."))
|
||||
return snapshots
|
||||
|
@ -0,0 +1,26 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import horizon
|
||||
from horizon.dashboards.nova import dashboard
|
||||
|
||||
|
||||
class VolumeSnapshots(horizon.Panel):
|
||||
name = "Volume Snapshots"
|
||||
slug = 'volume_snapshots'
|
||||
|
||||
|
||||
dashboard.Nova.register(VolumeSnapshots)
|
@ -0,0 +1,45 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from horizon import api
|
||||
from horizon import tables
|
||||
from ...instances_and_volumes.volumes import tables as volume_tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeleteVolumeSnapshot(tables.DeleteAction):
|
||||
data_type_singular = _("Volume Snapshot")
|
||||
data_type_plural = _("Volume Snaphots")
|
||||
classes = ('danger',)
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
api.volume_snapshot_delete(request, obj_id)
|
||||
|
||||
|
||||
class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
|
||||
volume_id = tables.Column("volumeId", verbose_name=_("Volume ID"))
|
||||
|
||||
class Meta:
|
||||
name = "volume_snapshots"
|
||||
verbose_name = _("Volume Snapshots")
|
||||
table_actions = (DeleteVolumeSnapshot,)
|
||||
row_actions = (DeleteVolumeSnapshot,)
|
@ -0,0 +1,78 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django import http
|
||||
from django.core.urlresolvers import reverse
|
||||
from novaclient.v1_1 import volume_snapshots
|
||||
from mox import IsA
|
||||
|
||||
from horizon import api
|
||||
from horizon import test
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
|
||||
|
||||
|
||||
class SnapshotsViewTests(test.BaseViewTests):
|
||||
def test_create_snapshot_get(self):
|
||||
VOLUME_ID = u'1'
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:instances_and_volumes:'
|
||||
'volumes:create_snapshot',
|
||||
args=[VOLUME_ID]))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/instances_and_volumes/'
|
||||
'volumes/create_snapshot.html')
|
||||
|
||||
def test_create_snapshot_post(self):
|
||||
VOLUME_ID = u'1'
|
||||
SNAPSHOT_NAME = u'vol snap'
|
||||
SNAPSHOT_DESCRIPTION = u'vol snap desc'
|
||||
|
||||
volume_snapshot = volume_snapshots.Snapshot(
|
||||
volume_snapshots.SnapshotManager,
|
||||
{'id': 1,
|
||||
'displayName': 'test snapshot',
|
||||
'displayDescription': 'test snapshot description',
|
||||
'size': 40,
|
||||
'status': 'available',
|
||||
'volumeId': 1})
|
||||
|
||||
formData = {'method': 'CreateSnapshotForm',
|
||||
'tenant_id': self.TEST_TENANT,
|
||||
'volume_id': VOLUME_ID,
|
||||
'name': SNAPSHOT_NAME,
|
||||
'description': SNAPSHOT_DESCRIPTION}
|
||||
|
||||
self.mox.StubOutWithMock(api, 'volume_snapshot_create')
|
||||
|
||||
api.volume_snapshot_create(
|
||||
IsA(http.HttpRequest), str(VOLUME_ID), SNAPSHOT_NAME,
|
||||
SNAPSHOT_DESCRIPTION).AndReturn(volume_snapshot)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:instances_and_volumes:volumes:'
|
||||
'create_snapshot',
|
||||
args=[VOLUME_ID]),
|
||||
formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
@ -15,6 +15,7 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from horizon import api
|
||||
from horizon import forms
|
||||
from horizon import exceptions
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
|
||||
|
||||
@ -82,3 +83,33 @@ class AttachForm(forms.SelfHandlingForm):
|
||||
_('Error attaching volume: %s') % e.message)
|
||||
return shortcuts.redirect(
|
||||
"horizon:nova:instances_and_volumes:index")
|
||||
|
||||
|
||||
class CreateSnapshotForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length="255", label=_("Snapshot Name"))
|
||||
description = forms.CharField(widget=forms.Textarea,
|
||||
label=_("Description"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateSnapshotForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# populate volume_id
|
||||
volume_id = kwargs.get('initial', {}).get('volume_id', [])
|
||||
self.fields['volume_id'] = forms.CharField(widget=forms.HiddenInput(),
|
||||
initial=volume_id)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
api.volume_snapshot_create(request,
|
||||
data['volume_id'],
|
||||
data['name'],
|
||||
data['description'])
|
||||
|
||||
message = _('Creating volume snapshot "%s"') % data['name']
|
||||
LOG.info(message)
|
||||
messages.info(request, message)
|
||||
except:
|
||||
exceptions.handle(request,
|
||||
_('Error Creating Volume Snapshot: %s'))
|
||||
|
||||
return shortcuts.redirect("horizon:nova:images_and_snapshots:index")
|
||||
|
@ -55,6 +55,15 @@ class EditAttachments(tables.LinkAction):
|
||||
return volume.status in ("available", "in-use")
|
||||
|
||||
|
||||
class CreateSnapshot(tables.LinkAction):
|
||||
name = "snapshots"
|
||||
verbose_name = _("Create Snapshot")
|
||||
url = "horizon:nova:instances_and_volumes:volumes:create_snapshot"
|
||||
|
||||
def allowed(self, request, volume=None):
|
||||
return volume.status in ("available", "in-use")
|
||||
|
||||
|
||||
def get_size(volume):
|
||||
return _("%s GB") % volume.size
|
||||
|
||||
@ -75,17 +84,11 @@ def get_attachment(volume):
|
||||
return safestring.mark_safe(", ".join(attachments))
|
||||
|
||||
|
||||
class VolumesTable(tables.DataTable):
|
||||
name = tables.Column("displayName",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:nova:instances_and_volumes:"
|
||||
"volumes:detail")
|
||||
class VolumesTableBase(tables.DataTable):
|
||||
name = tables.Column("displayName", verbose_name=_("Name"))
|
||||
description = tables.Column("displayDescription",
|
||||
verbose_name=("Description"))
|
||||
verbose_name=_("Description"))
|
||||
size = tables.Column(get_size, verbose_name=_("Size"))
|
||||
attachments = tables.Column(get_attachment,
|
||||
verbose_name=_("Attachments"),
|
||||
empty_value=_("-"))
|
||||
status = tables.Column("status", filters=(title,))
|
||||
|
||||
def sanitize_id(self, obj_id):
|
||||
@ -94,11 +97,16 @@ class VolumesTable(tables.DataTable):
|
||||
def get_object_display(self, obj):
|
||||
return obj.displayName
|
||||
|
||||
|
||||
class VolumesTable(VolumesTableBase):
|
||||
attachments = tables.Column(get_attachment,
|
||||
verbose_name=_("Attachments"))
|
||||
|
||||
class Meta:
|
||||
name = "volumes"
|
||||
verbose_name = _("Volumes")
|
||||
table_actions = (CreateVolume, DeleteVolume,)
|
||||
row_actions = (EditAttachments, DeleteVolume,)
|
||||
row_actions = (EditAttachments, CreateSnapshot, DeleteVolume)
|
||||
|
||||
|
||||
class DetachVolume(tables.BatchAction):
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import CreateView, EditAttachmentsView
|
||||
from .views import CreateView, EditAttachmentsView, CreateSnapshotView
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
@ -25,5 +25,8 @@ urlpatterns = patterns(
|
||||
url(r'^(?P<volume_id>[^/]+)/attach/$',
|
||||
EditAttachmentsView.as_view(),
|
||||
name='attach'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||
CreateSnapshotView.as_view(),
|
||||
name='create_snapshot'),
|
||||
url(r'^(?P<volume_id>[^/]+)/detail/$', 'detail', name='detail'),
|
||||
)
|
||||
|
@ -29,7 +29,7 @@ from horizon import api
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from .forms import CreateForm, AttachForm
|
||||
from .forms import CreateForm, AttachForm, CreateSnapshotForm
|
||||
from .tables import AttachmentsTable
|
||||
|
||||
|
||||
@ -63,6 +63,17 @@ class CreateView(forms.ModalFormView):
|
||||
template_name = 'nova/instances_and_volumes/volumes/create.html'
|
||||
|
||||
|
||||
class CreateSnapshotView(forms.ModalFormView):
|
||||
form_class = CreateSnapshotForm
|
||||
template_name = 'nova/instances_and_volumes/volumes/create_snapshot.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {'volume_id': kwargs['volume_id']}
|
||||
|
||||
def get_initial(self):
|
||||
return {'volume_id': self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class EditAttachmentsView(tables.DataTableView):
|
||||
table_class = AttachmentsTable
|
||||
template_name = 'nova/instances_and_volumes/volumes/attach.html'
|
||||
|
@ -13,4 +13,7 @@
|
||||
<div class="snapshots">
|
||||
{{ snapshots_table.render }}
|
||||
</div>
|
||||
<div class="volume_snapshots">
|
||||
{{ volume_snapshots_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,25 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create_snapshot volume_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}create_volume_snapshot_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Volume Snapshot" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% trans "Volumes are block devices that can be attached to instances." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn primary pull-right" type="submit" value="{% trans "Create Volume Snapshot" %}" />
|
||||
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Volume Snapshot" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create a Volume Snapshot") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/instances_and_volumes/volumes/_create_snapshot.html' %}
|
||||
{% endblock %}
|
@ -170,7 +170,9 @@ class BaseViewTests(TestCase):
|
||||
Base class for view based unit tests.
|
||||
"""
|
||||
def assertRedirectsNoFollow(self, response, expected_url):
|
||||
self.assertEqual(response._headers['location'],
|
||||
if response.status_code / 100 != 3:
|
||||
assert("The response did not return a redirect.")
|
||||
self.assertEqual(response._headers.get('location', None),
|
||||
('Location', settings.TESTSERVER + expected_url))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user