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):
|
def volume_detach(request, instance_id, attachment_id):
|
||||||
novaclient(request).volumes.delete_server_volume(
|
novaclient(request).volumes.delete_server_volume(
|
||||||
instance_id, attachment_id)
|
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(),
|
widget=forms.CheckboxSelectMultiple(),
|
||||||
help_text=_("Launch instance in these "
|
help_text=_("Launch instance in these "
|
||||||
"security groups."))
|
"security groups."))
|
||||||
volume = forms.ChoiceField(label=_("Volume"),
|
volume = forms.ChoiceField(label=_("Volume or Volume Snapshot"),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("Volume to boot from."))
|
help_text=_("Volume to boot from."))
|
||||||
device_name = forms.CharField(label=_("Device Name"),
|
device_name = forms.CharField(label=_("Device Name"),
|
||||||
@ -131,10 +131,10 @@ class LaunchForm(forms.SelfHandlingForm):
|
|||||||
delete_on_terminate = 1
|
delete_on_terminate = 1
|
||||||
else:
|
else:
|
||||||
delete_on_terminate = 0
|
delete_on_terminate = 0
|
||||||
dev_spec = {data['device_name']:
|
dev_mapping = {data['device_name']:
|
||||||
("%s:::%s" % (data['volume'], delete_on_terminate))}
|
("%s::%s" % (data['volume'], delete_on_terminate))}
|
||||||
else:
|
else:
|
||||||
dev_spec = None
|
dev_mapping = None
|
||||||
|
|
||||||
api.server_create(request,
|
api.server_create(request,
|
||||||
data['name'],
|
data['name'],
|
||||||
@ -143,7 +143,7 @@ class LaunchForm(forms.SelfHandlingForm):
|
|||||||
data.get('keypair'),
|
data.get('keypair'),
|
||||||
normalize_newlines(data.get('user_data')),
|
normalize_newlines(data.get('user_data')),
|
||||||
data.get('security_groups'),
|
data.get('security_groups'),
|
||||||
dev_spec,
|
dev_mapping,
|
||||||
instance_count=int(data.get('count')))
|
instance_count=int(data.get('count')))
|
||||||
messages.success(request,
|
messages.success(request,
|
||||||
_('Instance "%s" launched.') % data["name"])
|
_('Instance "%s" launched.') % data["name"])
|
||||||
|
@ -22,6 +22,7 @@ from django import http
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from keystoneclient import exceptions as keystone_exceptions
|
from keystoneclient import exceptions as keystone_exceptions
|
||||||
|
from novaclient.v1_1 import client as nova_client, volume_snapshots
|
||||||
from mox import IgnoreArg, IsA
|
from mox import IgnoreArg, IsA
|
||||||
|
|
||||||
from horizon import api
|
from horizon import api
|
||||||
@ -74,6 +75,16 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
volume.displayName = ''
|
volume.displayName = ''
|
||||||
self.volumes = (volume,)
|
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):
|
def test_launch_get(self):
|
||||||
IMAGE_ID = 1
|
IMAGE_ID = 1
|
||||||
|
|
||||||
@ -111,14 +122,14 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
self.keypairs[0].name)
|
self.keypairs[0].name)
|
||||||
|
|
||||||
def test_launch_post(self):
|
def test_launch_post(self):
|
||||||
FLAVOR_ID = self.flavors[0].id
|
FLAVOR_ID = unicode(self.flavors[0].id)
|
||||||
IMAGE_ID = '1'
|
IMAGE_ID = u'1'
|
||||||
keypair = self.keypairs[0].name
|
keypair = unicode(self.keypairs[0].name)
|
||||||
SERVER_NAME = 'serverName'
|
SERVER_NAME = u'serverName'
|
||||||
USER_DATA = 'userData'
|
USER_DATA = u'userData'
|
||||||
volume = self.volumes[0].id
|
volume = u'%s:vol' % self.volumes[0].id
|
||||||
device_name = 'vda'
|
device_name = u'vda'
|
||||||
BLOCK_DEVICE_MAPPING = {device_name: "1:::0"}
|
BLOCK_DEVICE_MAPPING = {device_name: u"1:vol::0"}
|
||||||
|
|
||||||
form_data = {'method': 'LaunchForm',
|
form_data = {'method': 'LaunchForm',
|
||||||
'flavor': FLAVOR_ID,
|
'flavor': FLAVOR_ID,
|
||||||
@ -130,8 +141,7 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
'tenant_id': self.TEST_TENANT,
|
'tenant_id': self.TEST_TENANT,
|
||||||
'security_groups': 'default',
|
'security_groups': 'default',
|
||||||
'volume': volume,
|
'volume': volume,
|
||||||
'device_name': device_name
|
'device_name': device_name}
|
||||||
}
|
|
||||||
|
|
||||||
self.mox.StubOutWithMock(api, 'image_get_meta')
|
self.mox.StubOutWithMock(api, 'image_get_meta')
|
||||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||||
@ -144,8 +154,8 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||||
self.security_groups)
|
self.security_groups)
|
||||||
api.image_get_meta(IsA(http.HttpRequest),
|
api.image_get_meta(IsA(http.HttpRequest), IMAGE_ID).AndReturn(
|
||||||
IMAGE_ID).AndReturn(self.visibleImage)
|
self.visibleImage)
|
||||||
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes)
|
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes)
|
||||||
api.server_create(IsA(http.HttpRequest), SERVER_NAME,
|
api.server_create(IsA(http.HttpRequest), SERVER_NAME,
|
||||||
str(IMAGE_ID), str(FLAVOR_ID),
|
str(IMAGE_ID), str(FLAVOR_ID),
|
||||||
@ -255,8 +265,7 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
api.keypair_list(IgnoreArg()).AndReturn(self.keypairs)
|
api.keypair_list(IgnoreArg()).AndReturn(self.keypairs)
|
||||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||||
self.security_groups)
|
self.security_groups)
|
||||||
api.image_get_meta(IgnoreArg(),
|
api.image_get_meta(IgnoreArg(), IMAGE_ID).AndReturn(self.visibleImage)
|
||||||
IMAGE_ID).AndReturn(self.visibleImage)
|
|
||||||
api.volume_list(IgnoreArg()).AndReturn(self.volumes)
|
api.volume_list(IgnoreArg()).AndReturn(self.volumes)
|
||||||
|
|
||||||
exception = keystone_exceptions.ClientException('Failed')
|
exception = keystone_exceptions.ClientException('Failed')
|
||||||
@ -270,9 +279,6 @@ class ImageViewTests(test.BaseViewTests):
|
|||||||
None,
|
None,
|
||||||
instance_count=IsA(int)).AndRaise(exception)
|
instance_count=IsA(int)).AndRaise(exception)
|
||||||
|
|
||||||
self.mox.StubOutWithMock(messages, 'error')
|
|
||||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
url = reverse('horizon:nova:images_and_snapshots:images:launch',
|
url = reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||||
args=[IMAGE_ID])
|
args=[IMAGE_ID])
|
||||||
|
@ -110,17 +110,42 @@ class LaunchView(forms.ModalFormView):
|
|||||||
return security_group_list
|
return security_group_list
|
||||||
|
|
||||||
def volume_list(self):
|
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:
|
try:
|
||||||
volumes = [v for v in api.volume_list(self.request) \
|
volumes = [v for v in api.volume_list(self.request) \
|
||||||
if v.status == api.VOLUME_STATE_AVAILABLE]
|
if v.status == api.VOLUME_STATE_AVAILABLE]
|
||||||
volume_sel = [(v.id, ("%s (%s GB)" % (v.displayName, v.size))) \
|
volume_options.extend(
|
||||||
for v in volumes]
|
[_get_volume_select_item(vol) for vol in volumes])
|
||||||
volume_sel.insert(0, ("", "Select Volume"))
|
|
||||||
except:
|
except:
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve list of volumes'))
|
_('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):
|
class UpdateView(forms.ModalFormView):
|
||||||
|
@ -23,7 +23,7 @@ from horizon.dashboards.nova import dashboard
|
|||||||
|
|
||||||
|
|
||||||
class Snapshots(horizon.Panel):
|
class Snapshots(horizon.Panel):
|
||||||
name = "Snapshots"
|
name = "Instance Snapshots"
|
||||||
slug = 'snapshots'
|
slug = 'snapshots'
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,6 @@ class DeleteSnapshot(DeleteImage):
|
|||||||
class SnapshotsTable(ImagesTable):
|
class SnapshotsTable(ImagesTable):
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "snapshots"
|
name = "snapshots"
|
||||||
verbose_name = _("Snapshots")
|
verbose_name = _("Instance Snapshots")
|
||||||
table_actions = (DeleteSnapshot,)
|
table_actions = (DeleteSnapshot,)
|
||||||
row_actions = (LaunchImage, EditImage, DeleteSnapshot)
|
row_actions = (LaunchImage, EditImage, DeleteSnapshot)
|
||||||
|
@ -32,13 +32,14 @@ from horizon import exceptions
|
|||||||
from horizon import tables
|
from horizon import tables
|
||||||
from .images.tables import ImagesTable
|
from .images.tables import ImagesTable
|
||||||
from .snapshots.tables import SnapshotsTable
|
from .snapshots.tables import SnapshotsTable
|
||||||
|
from .volume_snapshots.tables import VolumeSnapshotsTable
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IndexView(tables.MultiTableView):
|
class IndexView(tables.MultiTableView):
|
||||||
table_classes = (ImagesTable, SnapshotsTable)
|
table_classes = (ImagesTable, SnapshotsTable, VolumeSnapshotsTable)
|
||||||
template_name = 'nova/images_and_snapshots/index.html'
|
template_name = 'nova/images_and_snapshots/index.html'
|
||||||
|
|
||||||
def get_images_data(self):
|
def get_images_data(self):
|
||||||
@ -59,3 +60,12 @@ class IndexView(tables.MultiTableView):
|
|||||||
snapshots = []
|
snapshots = []
|
||||||
exceptions.handle(self.request, _("Unable to retrieve snapshots."))
|
exceptions.handle(self.request, _("Unable to retrieve snapshots."))
|
||||||
return 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 api
|
||||||
from horizon import forms
|
from horizon import forms
|
||||||
|
from horizon import exceptions
|
||||||
from novaclient import exceptions as novaclient_exceptions
|
from novaclient import exceptions as novaclient_exceptions
|
||||||
|
|
||||||
|
|
||||||
@ -82,3 +83,33 @@ class AttachForm(forms.SelfHandlingForm):
|
|||||||
_('Error attaching volume: %s') % e.message)
|
_('Error attaching volume: %s') % e.message)
|
||||||
return shortcuts.redirect(
|
return shortcuts.redirect(
|
||||||
"horizon:nova:instances_and_volumes:index")
|
"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")
|
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):
|
def get_size(volume):
|
||||||
return _("%s GB") % volume.size
|
return _("%s GB") % volume.size
|
||||||
|
|
||||||
@ -75,17 +84,11 @@ def get_attachment(volume):
|
|||||||
return safestring.mark_safe(", ".join(attachments))
|
return safestring.mark_safe(", ".join(attachments))
|
||||||
|
|
||||||
|
|
||||||
class VolumesTable(tables.DataTable):
|
class VolumesTableBase(tables.DataTable):
|
||||||
name = tables.Column("displayName",
|
name = tables.Column("displayName", verbose_name=_("Name"))
|
||||||
verbose_name=_("Name"),
|
|
||||||
link="horizon:nova:instances_and_volumes:"
|
|
||||||
"volumes:detail")
|
|
||||||
description = tables.Column("displayDescription",
|
description = tables.Column("displayDescription",
|
||||||
verbose_name=("Description"))
|
verbose_name=_("Description"))
|
||||||
size = tables.Column(get_size, verbose_name=_("Size"))
|
size = tables.Column(get_size, verbose_name=_("Size"))
|
||||||
attachments = tables.Column(get_attachment,
|
|
||||||
verbose_name=_("Attachments"),
|
|
||||||
empty_value=_("-"))
|
|
||||||
status = tables.Column("status", filters=(title,))
|
status = tables.Column("status", filters=(title,))
|
||||||
|
|
||||||
def sanitize_id(self, obj_id):
|
def sanitize_id(self, obj_id):
|
||||||
@ -94,11 +97,16 @@ class VolumesTable(tables.DataTable):
|
|||||||
def get_object_display(self, obj):
|
def get_object_display(self, obj):
|
||||||
return obj.displayName
|
return obj.displayName
|
||||||
|
|
||||||
|
|
||||||
|
class VolumesTable(VolumesTableBase):
|
||||||
|
attachments = tables.Column(get_attachment,
|
||||||
|
verbose_name=_("Attachments"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "volumes"
|
name = "volumes"
|
||||||
verbose_name = _("Volumes")
|
verbose_name = _("Volumes")
|
||||||
table_actions = (CreateVolume, DeleteVolume,)
|
table_actions = (CreateVolume, DeleteVolume,)
|
||||||
row_actions = (EditAttachments, DeleteVolume,)
|
row_actions = (EditAttachments, CreateSnapshot, DeleteVolume)
|
||||||
|
|
||||||
|
|
||||||
class DetachVolume(tables.BatchAction):
|
class DetachVolume(tables.BatchAction):
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
|
||||||
from .views import CreateView, EditAttachmentsView
|
from .views import CreateView, EditAttachmentsView, CreateSnapshotView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
urlpatterns = patterns(
|
||||||
@ -25,5 +25,8 @@ urlpatterns = patterns(
|
|||||||
url(r'^(?P<volume_id>[^/]+)/attach/$',
|
url(r'^(?P<volume_id>[^/]+)/attach/$',
|
||||||
EditAttachmentsView.as_view(),
|
EditAttachmentsView.as_view(),
|
||||||
name='attach'),
|
name='attach'),
|
||||||
|
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||||
|
CreateSnapshotView.as_view(),
|
||||||
|
name='create_snapshot'),
|
||||||
url(r'^(?P<volume_id>[^/]+)/detail/$', 'detail', name='detail'),
|
url(r'^(?P<volume_id>[^/]+)/detail/$', 'detail', name='detail'),
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,7 @@ from horizon import api
|
|||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import forms
|
from horizon import forms
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
from .forms import CreateForm, AttachForm
|
from .forms import CreateForm, AttachForm, CreateSnapshotForm
|
||||||
from .tables import AttachmentsTable
|
from .tables import AttachmentsTable
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +63,17 @@ class CreateView(forms.ModalFormView):
|
|||||||
template_name = 'nova/instances_and_volumes/volumes/create.html'
|
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):
|
class EditAttachmentsView(tables.DataTableView):
|
||||||
table_class = AttachmentsTable
|
table_class = AttachmentsTable
|
||||||
template_name = 'nova/instances_and_volumes/volumes/attach.html'
|
template_name = 'nova/instances_and_volumes/volumes/attach.html'
|
||||||
|
@ -13,4 +13,7 @@
|
|||||||
<div class="snapshots">
|
<div class="snapshots">
|
||||||
{{ snapshots_table.render }}
|
{{ snapshots_table.render }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="volume_snapshots">
|
||||||
|
{{ volume_snapshots_table.render }}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% 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.
|
Base class for view based unit tests.
|
||||||
"""
|
"""
|
||||||
def assertRedirectsNoFollow(self, response, expected_url):
|
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))
|
('Location', settings.TESTSERVER + expected_url))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user