From 190f4fc150ea756cc0b3cc1599a35f54cb66bfe9 Mon Sep 17 00:00:00 2001 From: Tres Henry Date: Mon, 6 Feb 2012 10:24:50 -0800 Subject: [PATCH] Adds support for volume snapshots (volume snapshots table and ability to boot from a volume snapshot). Implements blueprint snapshot-volume Change-Id: I467d177f2f461bd4ea7aec86b41063e811c6e5ba --- horizon/horizon/api/nova.py | 13 ++++ .../nova/images_and_snapshots/images/forms.py | 10 +-- .../nova/images_and_snapshots/images/tests.py | 44 ++++++----- .../nova/images_and_snapshots/images/views.py | 37 +++++++-- .../images_and_snapshots/snapshots/panel.py | 2 +- .../images_and_snapshots/snapshots/tables.py | 2 +- .../nova/images_and_snapshots/views.py | 12 ++- .../volume_snapshots/__init__.py | 0 .../volume_snapshots/panel.py | 26 +++++++ .../volume_snapshots/tables.py | 45 +++++++++++ .../volume_snapshots/tests.py | 78 +++++++++++++++++++ .../instances_and_volumes/volumes/forms.py | 31 ++++++++ .../instances_and_volumes/volumes/tables.py | 28 ++++--- .../instances_and_volumes/volumes/urls.py | 5 +- .../instances_and_volumes/volumes/views.py | 13 +++- .../nova/images_and_snapshots/index.html | 3 + .../volumes/_create_snapshot.html | 25 ++++++ .../volumes/create_snapshot.html | 11 +++ horizon/horizon/test.py | 4 +- 19 files changed, 343 insertions(+), 46 deletions(-) create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/__init__.py create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/panel.py create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/_create_snapshot.html create mode 100644 horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/create_snapshot.html diff --git a/horizon/horizon/api/nova.py b/horizon/horizon/api/nova.py index ff684b982..810f53485 100644 --- a/horizon/horizon/api/nova.py +++ b/horizon/horizon/api/nova.py @@ -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) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py index c4cf87daa..3b407fb14 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py @@ -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"]) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py index 59cce41aa..675a4b815 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py @@ -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]) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py index a8d236e76..dda1e48bb 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py @@ -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): diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/panel.py b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/panel.py index 499d1a4a4..5a6edb62b 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/panel.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/panel.py @@ -23,7 +23,7 @@ from horizon.dashboards.nova import dashboard class Snapshots(horizon.Panel): - name = "Snapshots" + name = "Instance Snapshots" slug = 'snapshots' diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py index f20953d90..37f4fe53f 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py @@ -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) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/views.py b/horizon/horizon/dashboards/nova/images_and_snapshots/views.py index 2b140e9dd..cc6588e17 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/views.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/views.py @@ -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 diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/__init__.py b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/panel.py b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/panel.py new file mode 100644 index 000000000..6aff53aaa --- /dev/null +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/panel.py @@ -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) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py new file mode 100644 index 000000000..e9532cc4c --- /dev/null +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tables.py @@ -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,) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py new file mode 100644 index 000000000..d98200718 --- /dev/null +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/volume_snapshots/tests.py @@ -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) diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py index 1610e0d0e..38e9a170e 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py @@ -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") diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py index 0b73ad167..9f10691cc 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/tables.py @@ -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): diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py index f2444adad..7a2f73f74 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/urls.py @@ -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[^/]+)/attach/$', EditAttachmentsView.as_view(), name='attach'), + url(r'^(?P[^/]+)/create_snapshot/$', + CreateSnapshotView.as_view(), + name='create_snapshot'), url(r'^(?P[^/]+)/detail/$', 'detail', name='detail'), ) diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/views.py b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/views.py index 40656433e..d6b6a0b0e 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/views.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/volumes/views.py @@ -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' diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html index 4473f15fb..8c7ce0690 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html @@ -13,4 +13,7 @@
{{ snapshots_table.render }}
+
+ {{ volume_snapshots_table.render }} +
{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/_create_snapshot.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/_create_snapshot.html new file mode 100644 index 000000000..e76f7f617 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/_create_snapshot.html @@ -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 %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "Volumes are block devices that can be attached to instances." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/create_snapshot.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/create_snapshot.html new file mode 100644 index 000000000..0c870ee03 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/volumes/create_snapshot.html @@ -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 %} diff --git a/horizon/horizon/test.py b/horizon/horizon/test.py index 9c8fe0738..3a70bbffb 100644 --- a/horizon/horizon/test.py +++ b/horizon/horizon/test.py @@ -165,7 +165,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)