Enable volume snapshot backups
Make it possible to use horizon for backing up snapshots. * Add snapshot column to backups table * Add snapshot information to backup detail * Fix wrong template path in create_backup.html * Add ChoiceField with snapshots belonging to the volume in create backup form * Add create backup option in snapshots table * Adjust tests and test data When backing up from snapshots table the snapshot ChoiceField is preset with the choosen snapshot as the only option. Implements: blueprint volume-snapshot-backups Change-Id: I4b7707d95756501e0622460e3ddc4e3f7624f02e
This commit is contained in:
parent
67ca5ad9f7
commit
9fa1cddf09
@ -123,9 +123,9 @@ class VolumeType(BaseCinderAPIResourceWrapper):
|
||||
class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||
|
||||
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
||||
'created_at', 'volume_id', 'availability_zone']
|
||||
'created_at', 'volume_id', 'availability_zone', 'snapshot_id']
|
||||
_volume = None
|
||||
|
||||
_snapshot = None
|
||||
@property
|
||||
def volume(self):
|
||||
return self._volume
|
||||
@ -134,6 +134,14 @@ class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||
def volume(self, value):
|
||||
self._volume = value
|
||||
|
||||
@property
|
||||
def snapshot(self):
|
||||
return self._snapshot
|
||||
|
||||
@snapshot.setter
|
||||
def snapshot(self, value):
|
||||
self._snapshot = value
|
||||
|
||||
|
||||
class QosSpecs(BaseCinderAPIResourceWrapper):
|
||||
|
||||
@ -630,7 +638,8 @@ def volume_backup_create(request,
|
||||
container_name,
|
||||
name,
|
||||
description,
|
||||
force=False):
|
||||
force=False,
|
||||
snapshot_id=None):
|
||||
# need to ensure the container name is not an empty
|
||||
# string, but pass None to get the container name
|
||||
# generated correctly
|
||||
@ -639,6 +648,7 @@ def volume_backup_create(request,
|
||||
container=container_name if container_name else None,
|
||||
name=name,
|
||||
description=description,
|
||||
snapshot_id=snapshot_id,
|
||||
force=force)
|
||||
return VolumeBackup(backup)
|
||||
|
||||
|
@ -40,20 +40,58 @@ class CreateBackupForm(forms.SelfHandlingForm):
|
||||
validators=[containers_utils.no_slash_validator],
|
||||
required=False)
|
||||
volume_id = forms.CharField(widget=forms.HiddenInput())
|
||||
snapshot_id = forms.ThemableChoiceField(label=_("Backup Snapshot"),
|
||||
required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateBackupForm, self).__init__(request, *args, **kwargs)
|
||||
if kwargs['initial'].get('snapshot_id'):
|
||||
snap_id = kwargs['initial']['snapshot_id']
|
||||
try:
|
||||
snapshot = api.cinder.volume_snapshot_get(request, snap_id)
|
||||
self.fields['snapshot_id'].choices = [(snapshot.id,
|
||||
snapshot.name)]
|
||||
self.fields['snapshot_id'].initial = snap_id
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:snapshots:index')
|
||||
exceptions.handle(request, _('Unable to fetch snapshot'),
|
||||
redirect=redirect)
|
||||
else:
|
||||
try:
|
||||
sop = {'volume_id': kwargs['initial']['volume_id']}
|
||||
snapshots = api.cinder.volume_snapshot_list(request,
|
||||
search_opts=sop)
|
||||
|
||||
snapshots.sort(key=operator.attrgetter('id', 'created_at'))
|
||||
snapshotChoices = [[snapshot.id, snapshot.name]
|
||||
for snapshot in snapshots]
|
||||
if not snapshotChoices:
|
||||
snapshotChoices.insert(0, ('',
|
||||
_("No snapshot for this volume")))
|
||||
else:
|
||||
snapshotChoices.insert(
|
||||
0, ('',
|
||||
_("Select snapshot to backup (Optional)")))
|
||||
self.fields['snapshot_id'].choices = snapshotChoices
|
||||
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(request, _('Unable to fetch snapshots'),
|
||||
redirect=redirect)
|
||||
|
||||
def handle(self, request, data):
|
||||
|
||||
try:
|
||||
volume = api.cinder.volume_get(request, data['volume_id'])
|
||||
snapshot_id = data['snapshot_id'] or None
|
||||
force = False
|
||||
if volume.status == 'in-use':
|
||||
force = True
|
||||
backup = api.cinder.volume_backup_create(request,
|
||||
data['volume_id'],
|
||||
data['container_name'],
|
||||
data['name'],
|
||||
data['description'],
|
||||
force=force)
|
||||
backup = api.cinder.volume_backup_create(
|
||||
request, data['volume_id'],
|
||||
data['container_name'], data['name'],
|
||||
data['description'], force=force,
|
||||
snapshot_id=snapshot_id
|
||||
)
|
||||
|
||||
message = _('Creating volume backup "%s"') % data['name']
|
||||
messages.info(request, message)
|
||||
|
@ -44,6 +44,24 @@ class BackupVolumeNameColumn(tables.Column):
|
||||
return reverse(self.link, args=(volume_id,))
|
||||
|
||||
|
||||
class SnapshotColumn(tables.Column):
|
||||
def get_raw_data(self, backup):
|
||||
snapshot = backup.snapshot
|
||||
if snapshot:
|
||||
snapshot_name = snapshot.name
|
||||
snapshot_name = html.escape(snapshot_name)
|
||||
elif backup.snapshot_id:
|
||||
snapshot_name = _("Unknown")
|
||||
else:
|
||||
return None
|
||||
return safestring.mark_safe(snapshot_name)
|
||||
|
||||
def get_link_url(self, backup):
|
||||
if backup.snapshot:
|
||||
return reverse('horizon:project:snapshots:detail',
|
||||
args=(backup.snapshot_id,))
|
||||
|
||||
|
||||
class DeleteBackup(tables.DeleteAction):
|
||||
help_text = _("Deleted volume backups are not recoverable.")
|
||||
policy_rules = (("volume", "backup:delete"),)
|
||||
@ -103,6 +121,14 @@ class UpdateRow(tables.Row):
|
||||
backup.volume_id)
|
||||
except Exception:
|
||||
pass
|
||||
if backup.snapshot_id is not None:
|
||||
try:
|
||||
backup.snapshot = cinder.volume_snapshot_get(
|
||||
request, backup.snapshot_id)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
backup.snapshot = None
|
||||
return backup
|
||||
|
||||
|
||||
@ -148,6 +174,9 @@ class BackupsTable(tables.DataTable):
|
||||
volume_name = BackupVolumeNameColumn("name",
|
||||
verbose_name=_("Volume Name"),
|
||||
link="horizon:project:volumes:detail")
|
||||
snapshot = SnapshotColumn("snapshot",
|
||||
verbose_name=_("Snapshot"),
|
||||
link="horizon:project:snapshots:detail")
|
||||
|
||||
class Meta(object):
|
||||
name = "volume_backups"
|
||||
|
@ -32,8 +32,19 @@ class BackupOverviewTab(tabs.Tab):
|
||||
volume = cinder.volume_get(request, backup.volume_id)
|
||||
except Exception:
|
||||
volume = None
|
||||
try:
|
||||
if backup.snapshot_id:
|
||||
snapshot = cinder.volume_snapshot_get(request,
|
||||
backup.snapshot_id)
|
||||
else:
|
||||
snapshot = None
|
||||
except Exception:
|
||||
snapshot = None
|
||||
|
||||
return {'backup': backup,
|
||||
'volume': volume}
|
||||
'volume': volume,
|
||||
'snapshot': snapshot}
|
||||
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:backups:index')
|
||||
exceptions.handle(self.request,
|
||||
|
@ -9,6 +9,11 @@
|
||||
You must have one of these services activated in order to create a backup.
|
||||
{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}
|
||||
If a snapshot is specified here only the specified snapshot of the volume
|
||||
will be backed up.
|
||||
</p>
|
||||
{% endblocktrans %}
|
||||
<p>{% blocktrans %}
|
||||
If no container name is provided, a default container named volumebackups
|
||||
will be provisioned for you.
|
||||
Backups will be the same size as the volume they originate from.
|
||||
|
@ -20,6 +20,20 @@
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if backup.snapshot_id %}
|
||||
<dt>{% trans "Snapshot" %}</dt>
|
||||
{% if snapshot %}
|
||||
<dd>
|
||||
<a href="{% url 'horizon:project:snapshots:detail' backup.snapshot_id %}">
|
||||
{{ snapshot.name }}
|
||||
</a>
|
||||
</dd>
|
||||
{% elif backup.snapshot_id %}
|
||||
<dd>
|
||||
{{ backup.snapshot_id }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
<h4>{% trans "Specs" %}</h4>
|
||||
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Create Volume Backup" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/backups/_create_backup.html' %}
|
||||
{% include 'project/backups/_create_backup.html' %}
|
||||
{% endblock %}
|
||||
|
@ -27,13 +27,15 @@ INDEX_URL = reverse('horizon:project:backups:index')
|
||||
|
||||
class VolumeBackupsViewTests(test.TestCase):
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_list',
|
||||
@test.create_mocks({api.cinder: ('volume_list', 'volume_snapshot_list',
|
||||
'volume_backup_list_paged')})
|
||||
def _test_backups_index_paginated(self, marker, sort_dir, backups, url,
|
||||
has_more, has_prev):
|
||||
self.mock_volume_backup_list_paged.return_value = [backups,
|
||||
has_more, has_prev]
|
||||
self.mock_volume_list.return_value = self.cinder_volumes.list()
|
||||
self.mock_volume_snapshot_list.return_value \
|
||||
= self.cinder_volume_snapshots.list()
|
||||
|
||||
res = self.client.get(urlunquote(url))
|
||||
|
||||
@ -43,12 +45,14 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
test.IsHttpRequest(), marker=marker, sort_dir=sort_dir,
|
||||
paginate=True)
|
||||
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
|
||||
|
||||
self.mock_volume_snapshot_list.assert_called_once_with(
|
||||
test.IsHttpRequest())
|
||||
return res
|
||||
|
||||
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||
def test_backups_index_paginated(self):
|
||||
backups = self.cinder_volume_backups.list()
|
||||
expected_snapshosts = self.cinder_volume_snapshots.list()
|
||||
size = settings.API_RESULT_PAGE_SIZE
|
||||
base_url = INDEX_URL
|
||||
next = backup_tables.BackupsTable._meta.pagination_param
|
||||
@ -71,7 +75,7 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
has_more=True, has_prev=True)
|
||||
result = res.context['volume_backups_table'].data
|
||||
self.assertItemsEqual(result, expected_backups)
|
||||
|
||||
self.assertEqual(result[0].snapshot.id, expected_snapshosts[1].id)
|
||||
# get last page
|
||||
expected_backups = backups[-size:]
|
||||
marker = expected_backups[0].id
|
||||
@ -110,6 +114,7 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
self.assertItemsEqual(result, expected_backups)
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_backup_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_get')})
|
||||
def test_create_backup_available(self):
|
||||
volume = self.cinder_volumes.first()
|
||||
@ -131,7 +136,9 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=0, warning=0)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
self.mock_volume_snapshot_list.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
search_opts={'volume_id': volume.id})
|
||||
self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
|
||||
volume.id)
|
||||
self.mock_volume_backup_create.assert_called_once_with(
|
||||
@ -140,18 +147,97 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
backup.container_name,
|
||||
backup.name,
|
||||
backup.description,
|
||||
force=False)
|
||||
force=False,
|
||||
snapshot_id=None)
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_backup_create',
|
||||
'volume_get')})
|
||||
@test.create_mocks(
|
||||
{api.cinder: ('volume_backup_create', 'volume_snapshot_get',
|
||||
'volume_get')})
|
||||
def test_create_backup_from_snapshot_table(self):
|
||||
backup = self.cinder_volume_backups.list()[1]
|
||||
volume = self.cinder_volumes.list()[4]
|
||||
snapshot = self.cinder_volume_snapshots.list()[1]
|
||||
self.mock_volume_backup_create.return_value = backup
|
||||
self.mock_volume_get.return_value = volume
|
||||
self.mock_volume_snapshot_get.return_value = snapshot
|
||||
formData = {'method': 'CreateBackupForm',
|
||||
'tenant_id': self.tenant.id,
|
||||
'volume_id': volume.id,
|
||||
'container_name': backup.container_name,
|
||||
'name': backup.name,
|
||||
'snapshot_id': backup.snapshot_id,
|
||||
'description': backup.description}
|
||||
url = reverse('horizon:project:volumes:create_snapshot_backup',
|
||||
args=[backup.volume_id, backup.snapshot_id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=0, warning=0)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
|
||||
backup.volume_id)
|
||||
self.mock_volume_backup_create.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
backup.volume_id,
|
||||
backup.container_name,
|
||||
backup.name,
|
||||
backup.description,
|
||||
force=False,
|
||||
snapshot_id=backup.snapshot_id)
|
||||
|
||||
@test.create_mocks(
|
||||
{api.cinder: ('volume_backup_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_get')})
|
||||
def test_create_backup_from_snapshot_volume_table(self):
|
||||
volume = self.cinder_volumes.list()[4]
|
||||
backup = self.cinder_volume_backups.list()[1]
|
||||
snapshots = self.cinder_volume_snapshots.list()[1:3]
|
||||
self.mock_volume_backup_create.return_value = backup
|
||||
self.mock_volume_get.return_value = volume
|
||||
self.mock_volume_snapshot_list.return_value = snapshots
|
||||
formData = {'method': 'CreateBackupForm',
|
||||
'tenant_id': self.tenant.id,
|
||||
'volume_id': volume.id,
|
||||
'container_name': backup.container_name,
|
||||
'name': backup.name,
|
||||
'snapshot_id': snapshots[0].id,
|
||||
'description': backup.description}
|
||||
url = reverse('horizon:project:volumes:create_backup',
|
||||
args=[backup.volume_id])
|
||||
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.mock_volume_snapshot_list.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
search_opts={'volume_id': volume.id})
|
||||
self.assertMessageCount(error=0, warning=0)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
|
||||
backup.volume_id)
|
||||
self.mock_volume_backup_create.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
backup.volume_id,
|
||||
backup.container_name,
|
||||
backup.name,
|
||||
backup.description,
|
||||
force=False,
|
||||
snapshot_id=backup.snapshot_id)
|
||||
|
||||
@test.create_mocks(
|
||||
{api.cinder: ('volume_backup_create', 'volume_snapshot_list',
|
||||
'volume_get')})
|
||||
def test_create_backup_in_use(self):
|
||||
# The third volume in the cinder test volume data is in-use
|
||||
volume = self.cinder_volumes.list()[2]
|
||||
backup = self.cinder_volume_backups.first()
|
||||
|
||||
backup = self.cinder_volume_backups.first()
|
||||
snapshots = []
|
||||
self.mock_volume_get.return_value = volume
|
||||
self.mock_volume_backup_create.return_value = backup
|
||||
|
||||
self.mock_volume_snapshot_list.return_value = snapshots
|
||||
formData = {'method': 'CreateBackupForm',
|
||||
'tenant_id': self.tenant.id,
|
||||
'volume_id': volume.id,
|
||||
@ -160,8 +246,11 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
'description': backup.description}
|
||||
url = reverse('horizon:project:volumes:create_backup',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
res = self.client.post(url, formData)
|
||||
self.mock_volume_snapshot_list.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
search_opts={'volume_id': volume.id})
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=0, warning=0)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
@ -173,21 +262,24 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
backup.container_name,
|
||||
backup.name,
|
||||
backup.description,
|
||||
force=True)
|
||||
force=True,
|
||||
snapshot_id=None)
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_list',
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_list_paged',
|
||||
'volume_backup_delete')})
|
||||
def test_delete_volume_backup(self):
|
||||
vol_backups = self.cinder_volume_backups.list()
|
||||
volumes = self.cinder_volumes.list()
|
||||
backup = self.cinder_volume_backups.first()
|
||||
snapshots = self.cinder_volume_snapshots.list()
|
||||
|
||||
self.mock_volume_backup_list_paged.return_value = [vol_backups,
|
||||
False, False]
|
||||
self.mock_volume_list.return_value = volumes
|
||||
self.mock_volume_backup_delete.return_value = None
|
||||
|
||||
self.mock_volume_snapshot_list.return_value = snapshots
|
||||
formData = {'action':
|
||||
'volume_backups__delete__%s' % backup.id}
|
||||
res = self.client.post(INDEX_URL, formData)
|
||||
@ -198,6 +290,8 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
test.IsHttpRequest(), marker=None, sort_dir='desc',
|
||||
paginate=True)
|
||||
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
|
||||
self.mock_volume_snapshot_list.assert_called_once_with(
|
||||
test.IsHttpRequest())
|
||||
self.mock_volume_backup_delete.assert_called_once_with(
|
||||
test.IsHttpRequest(), backup.id)
|
||||
|
||||
@ -221,6 +315,31 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
self.mock_volume_get.assert_called_once_with(
|
||||
test.IsHttpRequest(), backup.volume_id)
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_backup_get',
|
||||
'volume_snapshot_get',
|
||||
'volume_get')})
|
||||
def test_volume_backup_detail_get_with_snapshot(self):
|
||||
backup = self.cinder_volume_backups.list()[1]
|
||||
volume = self.cinder_volumes.get(id=backup.volume_id)
|
||||
|
||||
self.mock_volume_backup_get.return_value = backup
|
||||
self.mock_volume_get.return_value = volume
|
||||
self.mock_volume_snapshot_get.return_value \
|
||||
= self.cinder_volume_snapshots.list()[1]
|
||||
url = reverse('horizon:project:backups:detail',
|
||||
args=[backup.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
|
||||
self.assertEqual(res.context['backup'].id, backup.id)
|
||||
self.assertEqual(res.context['snapshot'].id, backup.snapshot_id)
|
||||
self.mock_volume_backup_get.assert_called_once_with(
|
||||
test.IsHttpRequest(), backup.id)
|
||||
self.mock_volume_get.assert_called_once_with(
|
||||
test.IsHttpRequest(), backup.volume_id)
|
||||
self.mock_volume_snapshot_get.assert_called_once_with(
|
||||
test.IsHttpRequest(), backup.snapshot_id)
|
||||
|
||||
@test.create_mocks({api.cinder: ('volume_backup_get',)})
|
||||
def test_volume_backup_detail_get_with_exception(self):
|
||||
# Test to verify redirect if get volume backup fails
|
||||
|
@ -48,8 +48,11 @@ class BackupsView(tables.DataTableView, tables.PagedTableMixin,
|
||||
paginate=True)
|
||||
volumes = api.cinder.volume_list(self.request)
|
||||
volumes = dict((v.id, v) for v in volumes)
|
||||
snapshots = api.cinder.volume_snapshot_list(self.request)
|
||||
snapshots = dict((s.id, s) for s in snapshots)
|
||||
for backup in backups:
|
||||
backup.volume = volumes.get(backup.volume_id)
|
||||
backup.snapshot = snapshots.get(backup.snapshot_id)
|
||||
except Exception:
|
||||
backups = []
|
||||
exceptions.handle(self.request, _("Unable to retrieve "
|
||||
@ -73,6 +76,9 @@ class CreateBackupView(forms.ModalFormView):
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
if self.kwargs.get('snapshot_id'):
|
||||
return {"volume_id": self.kwargs["volume_id"],
|
||||
"snapshot_id": self.kwargs["snapshot_id"]}
|
||||
return {"volume_id": self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
|
@ -101,6 +101,8 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
# Can't delete snapshot if part of group snapshot
|
||||
if getattr(datum, 'group_snapshot_id', None):
|
||||
return False
|
||||
if datum.status == 'backing-up':
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@ -142,6 +144,23 @@ class CreateVolumeFromSnapshot(tables.LinkAction):
|
||||
return False
|
||||
|
||||
|
||||
class CreateBackup(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "backups"
|
||||
verbose_name = _("Create Backup")
|
||||
url = "horizon:project:volumes:create_snapshot_backup"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "backup:create"),)
|
||||
|
||||
def get_link_url(self, datum):
|
||||
snap_id = self.table.get_object_id(datum)
|
||||
url = reverse(self.url, args=(datum.volume_id, snap_id))
|
||||
return url
|
||||
|
||||
def allowed(self, request, snapshot=None):
|
||||
return (cinder.volume_backup_supported(request) and
|
||||
snapshot.status == 'available')
|
||||
|
||||
|
||||
class UpdateMetadata(tables.LinkAction):
|
||||
name = "update_metadata"
|
||||
verbose_name = _("Update Metadata")
|
||||
@ -230,7 +249,7 @@ class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
|
||||
launch_actions = (LaunchSnapshotNG,) + launch_actions
|
||||
|
||||
row_actions = ((CreateVolumeFromSnapshot,) + launch_actions +
|
||||
(EditVolumeSnapshot, DeleteVolumeSnapshot,
|
||||
(EditVolumeSnapshot, DeleteVolumeSnapshot, CreateBackup,
|
||||
UpdateMetadata))
|
||||
row_class = UpdateRow
|
||||
status_columns = ("status",)
|
||||
|
@ -42,6 +42,9 @@ urlpatterns = [
|
||||
url(r'^(?P<volume_id>[^/]+)/create_backup/$',
|
||||
backup_views.CreateBackupView.as_view(),
|
||||
name='create_backup'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_backup/(?P<snapshot_id>[^/]+)$',
|
||||
backup_views.CreateBackupView.as_view(),
|
||||
name='create_snapshot_backup'),
|
||||
url(r'^(?P<volume_id>[^/]+)/$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
|
@ -204,6 +204,7 @@ def data(TEST):
|
||||
'display_name': 'test snapshot',
|
||||
'display_description': 'volume snapshot',
|
||||
'size': 40,
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'status': 'available',
|
||||
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
|
||||
snapshot2 = vol_snaps.Snapshot(
|
||||
@ -212,6 +213,7 @@ def data(TEST):
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot description',
|
||||
'size': 80,
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
snapshot3 = vol_snaps.Snapshot(
|
||||
@ -220,6 +222,7 @@ def data(TEST):
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot description 2',
|
||||
'size': 80,
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
snapshot4 = vol_snaps.Snapshot(
|
||||
@ -228,6 +231,7 @@ def data(TEST):
|
||||
'name': '',
|
||||
'description': 'v2 volume snapshot with metadata description',
|
||||
'size': 80,
|
||||
'created_at': '2014-01-27 10:30:00',
|
||||
'status': 'available',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234',
|
||||
'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}})
|
||||
@ -269,6 +273,7 @@ def data(TEST):
|
||||
'size': 10,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'snapshot_id': None,
|
||||
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
|
||||
|
||||
volume_backup2 = vol_backups.VolumeBackup(
|
||||
@ -278,6 +283,7 @@ def data(TEST):
|
||||
'description': 'volume backup 2',
|
||||
'size': 20,
|
||||
'status': 'available',
|
||||
'snapshot_id': snapshot2.id,
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
|
||||
@ -287,6 +293,7 @@ def data(TEST):
|
||||
'name': 'backup3',
|
||||
'description': 'volume backup 3',
|
||||
'size': 20,
|
||||
'snapshot_id': None,
|
||||
'status': 'available',
|
||||
'container_name': 'volumebackups',
|
||||
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Enabled horizon to make use of cinder's feature to
|
||||
backup up snapshots of block storage volumes.
|
||||
|
Loading…
Reference in New Issue
Block a user