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:
Jesper Schmitz Mouridsen 2019-06-26 18:22:58 +00:00
parent 67ca5ad9f7
commit 9fa1cddf09
13 changed files with 291 additions and 25 deletions

View File

@ -123,9 +123,9 @@ class VolumeType(BaseCinderAPIResourceWrapper):
class VolumeBackup(BaseCinderAPIResourceWrapper): class VolumeBackup(BaseCinderAPIResourceWrapper):
_attrs = ['id', 'name', 'description', 'container', 'size', 'status', _attrs = ['id', 'name', 'description', 'container', 'size', 'status',
'created_at', 'volume_id', 'availability_zone'] 'created_at', 'volume_id', 'availability_zone', 'snapshot_id']
_volume = None _volume = None
_snapshot = None
@property @property
def volume(self): def volume(self):
return self._volume return self._volume
@ -134,6 +134,14 @@ class VolumeBackup(BaseCinderAPIResourceWrapper):
def volume(self, value): def volume(self, value):
self._volume = value self._volume = value
@property
def snapshot(self):
return self._snapshot
@snapshot.setter
def snapshot(self, value):
self._snapshot = value
class QosSpecs(BaseCinderAPIResourceWrapper): class QosSpecs(BaseCinderAPIResourceWrapper):
@ -630,7 +638,8 @@ def volume_backup_create(request,
container_name, container_name,
name, name,
description, description,
force=False): force=False,
snapshot_id=None):
# need to ensure the container name is not an empty # need to ensure the container name is not an empty
# string, but pass None to get the container name # string, but pass None to get the container name
# generated correctly # generated correctly
@ -639,6 +648,7 @@ def volume_backup_create(request,
container=container_name if container_name else None, container=container_name if container_name else None,
name=name, name=name,
description=description, description=description,
snapshot_id=snapshot_id,
force=force) force=force)
return VolumeBackup(backup) return VolumeBackup(backup)

View File

@ -40,20 +40,58 @@ class CreateBackupForm(forms.SelfHandlingForm):
validators=[containers_utils.no_slash_validator], validators=[containers_utils.no_slash_validator],
required=False) required=False)
volume_id = forms.CharField(widget=forms.HiddenInput()) 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): def handle(self, request, data):
try: try:
volume = api.cinder.volume_get(request, data['volume_id']) volume = api.cinder.volume_get(request, data['volume_id'])
snapshot_id = data['snapshot_id'] or None
force = False force = False
if volume.status == 'in-use': if volume.status == 'in-use':
force = True force = True
backup = api.cinder.volume_backup_create(request, backup = api.cinder.volume_backup_create(
data['volume_id'], request, data['volume_id'],
data['container_name'], data['container_name'], data['name'],
data['name'], data['description'], force=force,
data['description'], snapshot_id=snapshot_id
force=force) )
message = _('Creating volume backup "%s"') % data['name'] message = _('Creating volume backup "%s"') % data['name']
messages.info(request, message) messages.info(request, message)

View File

@ -44,6 +44,24 @@ class BackupVolumeNameColumn(tables.Column):
return reverse(self.link, args=(volume_id,)) 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): class DeleteBackup(tables.DeleteAction):
help_text = _("Deleted volume backups are not recoverable.") help_text = _("Deleted volume backups are not recoverable.")
policy_rules = (("volume", "backup:delete"),) policy_rules = (("volume", "backup:delete"),)
@ -103,6 +121,14 @@ class UpdateRow(tables.Row):
backup.volume_id) backup.volume_id)
except Exception: except Exception:
pass 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 return backup
@ -148,6 +174,9 @@ class BackupsTable(tables.DataTable):
volume_name = BackupVolumeNameColumn("name", volume_name = BackupVolumeNameColumn("name",
verbose_name=_("Volume Name"), verbose_name=_("Volume Name"),
link="horizon:project:volumes:detail") link="horizon:project:volumes:detail")
snapshot = SnapshotColumn("snapshot",
verbose_name=_("Snapshot"),
link="horizon:project:snapshots:detail")
class Meta(object): class Meta(object):
name = "volume_backups" name = "volume_backups"

View File

@ -32,8 +32,19 @@ class BackupOverviewTab(tabs.Tab):
volume = cinder.volume_get(request, backup.volume_id) volume = cinder.volume_get(request, backup.volume_id)
except Exception: except Exception:
volume = None 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, return {'backup': backup,
'volume': volume} 'volume': volume,
'snapshot': snapshot}
except Exception: except Exception:
redirect = reverse('horizon:project:backups:index') redirect = reverse('horizon:project:backups:index')
exceptions.handle(self.request, exceptions.handle(self.request,

View File

@ -9,6 +9,11 @@
You must have one of these services activated in order to create a backup. You must have one of these services activated in order to create a backup.
{% endblocktrans %}</p> {% endblocktrans %}</p>
<p>{% blocktrans %} <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 If no container name is provided, a default container named volumebackups
will be provisioned for you. will be provisioned for you.
Backups will be the same size as the volume they originate from. Backups will be the same size as the volume they originate from.

View File

@ -20,6 +20,20 @@
</a> </a>
</dd> </dd>
{% endif %} {% 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> </dl>
<h4>{% trans "Specs" %}</h4> <h4>{% trans "Specs" %}</h4>

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Create Volume Backup" %}{% endblock %} {% block title %}{% trans "Create Volume Backup" %}{% endblock %}
{% block main %} {% block main %}
{% include 'project/volumes/backups/_create_backup.html' %} {% include 'project/backups/_create_backup.html' %}
{% endblock %} {% endblock %}

View File

@ -27,13 +27,15 @@ INDEX_URL = reverse('horizon:project:backups:index')
class VolumeBackupsViewTests(test.TestCase): 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')}) 'volume_backup_list_paged')})
def _test_backups_index_paginated(self, marker, sort_dir, backups, url, def _test_backups_index_paginated(self, marker, sort_dir, backups, url,
has_more, has_prev): has_more, has_prev):
self.mock_volume_backup_list_paged.return_value = [backups, self.mock_volume_backup_list_paged.return_value = [backups,
has_more, has_prev] has_more, has_prev]
self.mock_volume_list.return_value = self.cinder_volumes.list() 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)) res = self.client.get(urlunquote(url))
@ -43,12 +45,14 @@ class VolumeBackupsViewTests(test.TestCase):
test.IsHttpRequest(), marker=marker, sort_dir=sort_dir, test.IsHttpRequest(), marker=marker, sort_dir=sort_dir,
paginate=True) paginate=True)
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest()) self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
self.mock_volume_snapshot_list.assert_called_once_with(
test.IsHttpRequest())
return res return res
@override_settings(API_RESULT_PAGE_SIZE=1) @override_settings(API_RESULT_PAGE_SIZE=1)
def test_backups_index_paginated(self): def test_backups_index_paginated(self):
backups = self.cinder_volume_backups.list() backups = self.cinder_volume_backups.list()
expected_snapshosts = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL base_url = INDEX_URL
next = backup_tables.BackupsTable._meta.pagination_param next = backup_tables.BackupsTable._meta.pagination_param
@ -71,7 +75,7 @@ class VolumeBackupsViewTests(test.TestCase):
has_more=True, has_prev=True) has_more=True, has_prev=True)
result = res.context['volume_backups_table'].data result = res.context['volume_backups_table'].data
self.assertItemsEqual(result, expected_backups) self.assertItemsEqual(result, expected_backups)
self.assertEqual(result[0].snapshot.id, expected_snapshosts[1].id)
# get last page # get last page
expected_backups = backups[-size:] expected_backups = backups[-size:]
marker = expected_backups[0].id marker = expected_backups[0].id
@ -110,6 +114,7 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertItemsEqual(result, expected_backups) self.assertItemsEqual(result, expected_backups)
@test.create_mocks({api.cinder: ('volume_backup_create', @test.create_mocks({api.cinder: ('volume_backup_create',
'volume_snapshot_list',
'volume_get')}) 'volume_get')})
def test_create_backup_available(self): def test_create_backup_available(self):
volume = self.cinder_volumes.first() volume = self.cinder_volumes.first()
@ -131,7 +136,9 @@ class VolumeBackupsViewTests(test.TestCase):
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
self.assertMessageCount(error=0, warning=0) self.assertMessageCount(error=0, warning=0)
self.assertRedirectsNoFollow(res, INDEX_URL) 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(), self.mock_volume_get.assert_called_once_with(test.IsHttpRequest(),
volume.id) volume.id)
self.mock_volume_backup_create.assert_called_once_with( self.mock_volume_backup_create.assert_called_once_with(
@ -140,18 +147,97 @@ class VolumeBackupsViewTests(test.TestCase):
backup.container_name, backup.container_name,
backup.name, backup.name,
backup.description, backup.description,
force=False) force=False,
snapshot_id=None)
@test.create_mocks({api.cinder: ('volume_backup_create', @test.create_mocks(
'volume_get')}) {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): def test_create_backup_in_use(self):
# The third volume in the cinder test volume data is in-use # The third volume in the cinder test volume data is in-use
volume = self.cinder_volumes.list()[2] 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_get.return_value = volume
self.mock_volume_backup_create.return_value = backup self.mock_volume_backup_create.return_value = backup
self.mock_volume_snapshot_list.return_value = snapshots
formData = {'method': 'CreateBackupForm', formData = {'method': 'CreateBackupForm',
'tenant_id': self.tenant.id, 'tenant_id': self.tenant.id,
'volume_id': volume.id, 'volume_id': volume.id,
@ -160,8 +246,11 @@ class VolumeBackupsViewTests(test.TestCase):
'description': backup.description} 'description': backup.description}
url = reverse('horizon:project:volumes:create_backup', url = reverse('horizon:project:volumes:create_backup',
args=[volume.id]) 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.assertNoFormErrors(res)
self.assertMessageCount(error=0, warning=0) self.assertMessageCount(error=0, warning=0)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@ -173,21 +262,24 @@ class VolumeBackupsViewTests(test.TestCase):
backup.container_name, backup.container_name,
backup.name, backup.name,
backup.description, backup.description,
force=True) force=True,
snapshot_id=None)
@test.create_mocks({api.cinder: ('volume_list', @test.create_mocks({api.cinder: ('volume_list',
'volume_snapshot_list',
'volume_backup_list_paged', 'volume_backup_list_paged',
'volume_backup_delete')}) 'volume_backup_delete')})
def test_delete_volume_backup(self): def test_delete_volume_backup(self):
vol_backups = self.cinder_volume_backups.list() vol_backups = self.cinder_volume_backups.list()
volumes = self.cinder_volumes.list() volumes = self.cinder_volumes.list()
backup = self.cinder_volume_backups.first() backup = self.cinder_volume_backups.first()
snapshots = self.cinder_volume_snapshots.list()
self.mock_volume_backup_list_paged.return_value = [vol_backups, self.mock_volume_backup_list_paged.return_value = [vol_backups,
False, False] False, False]
self.mock_volume_list.return_value = volumes self.mock_volume_list.return_value = volumes
self.mock_volume_backup_delete.return_value = None self.mock_volume_backup_delete.return_value = None
self.mock_volume_snapshot_list.return_value = snapshots
formData = {'action': formData = {'action':
'volume_backups__delete__%s' % backup.id} 'volume_backups__delete__%s' % backup.id}
res = self.client.post(INDEX_URL, formData) res = self.client.post(INDEX_URL, formData)
@ -198,6 +290,8 @@ class VolumeBackupsViewTests(test.TestCase):
test.IsHttpRequest(), marker=None, sort_dir='desc', test.IsHttpRequest(), marker=None, sort_dir='desc',
paginate=True) paginate=True)
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest()) 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( self.mock_volume_backup_delete.assert_called_once_with(
test.IsHttpRequest(), backup.id) test.IsHttpRequest(), backup.id)
@ -221,6 +315,31 @@ class VolumeBackupsViewTests(test.TestCase):
self.mock_volume_get.assert_called_once_with( self.mock_volume_get.assert_called_once_with(
test.IsHttpRequest(), backup.volume_id) 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',)}) @test.create_mocks({api.cinder: ('volume_backup_get',)})
def test_volume_backup_detail_get_with_exception(self): def test_volume_backup_detail_get_with_exception(self):
# Test to verify redirect if get volume backup fails # Test to verify redirect if get volume backup fails

View File

@ -48,8 +48,11 @@ class BackupsView(tables.DataTableView, tables.PagedTableMixin,
paginate=True) paginate=True)
volumes = api.cinder.volume_list(self.request) volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes) 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: for backup in backups:
backup.volume = volumes.get(backup.volume_id) backup.volume = volumes.get(backup.volume_id)
backup.snapshot = snapshots.get(backup.snapshot_id)
except Exception: except Exception:
backups = [] backups = []
exceptions.handle(self.request, _("Unable to retrieve " exceptions.handle(self.request, _("Unable to retrieve "
@ -73,6 +76,9 @@ class CreateBackupView(forms.ModalFormView):
return context return context
def get_initial(self): 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"]} return {"volume_id": self.kwargs["volume_id"]}

View File

@ -101,6 +101,8 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
# Can't delete snapshot if part of group snapshot # Can't delete snapshot if part of group snapshot
if getattr(datum, 'group_snapshot_id', None): if getattr(datum, 'group_snapshot_id', None):
return False return False
if datum.status == 'backing-up':
return False
return True return True
@ -142,6 +144,23 @@ class CreateVolumeFromSnapshot(tables.LinkAction):
return False 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): class UpdateMetadata(tables.LinkAction):
name = "update_metadata" name = "update_metadata"
verbose_name = _("Update Metadata") verbose_name = _("Update Metadata")
@ -230,7 +249,7 @@ class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
launch_actions = (LaunchSnapshotNG,) + launch_actions launch_actions = (LaunchSnapshotNG,) + launch_actions
row_actions = ((CreateVolumeFromSnapshot,) + launch_actions + row_actions = ((CreateVolumeFromSnapshot,) + launch_actions +
(EditVolumeSnapshot, DeleteVolumeSnapshot, (EditVolumeSnapshot, DeleteVolumeSnapshot, CreateBackup,
UpdateMetadata)) UpdateMetadata))
row_class = UpdateRow row_class = UpdateRow
status_columns = ("status",) status_columns = ("status",)

View File

@ -42,6 +42,9 @@ urlpatterns = [
url(r'^(?P<volume_id>[^/]+)/create_backup/$', url(r'^(?P<volume_id>[^/]+)/create_backup/$',
backup_views.CreateBackupView.as_view(), backup_views.CreateBackupView.as_view(),
name='create_backup'), 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>[^/]+)/$', url(r'^(?P<volume_id>[^/]+)/$',
views.DetailView.as_view(), views.DetailView.as_view(),
name='detail'), name='detail'),

View File

@ -204,6 +204,7 @@ def data(TEST):
'display_name': 'test snapshot', 'display_name': 'test snapshot',
'display_description': 'volume snapshot', 'display_description': 'volume snapshot',
'size': 40, 'size': 40,
'created_at': '2014-01-27 10:30:00',
'status': 'available', 'status': 'available',
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'}) 'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
snapshot2 = vol_snaps.Snapshot( snapshot2 = vol_snaps.Snapshot(
@ -212,6 +213,7 @@ def data(TEST):
'name': '', 'name': '',
'description': 'v2 volume snapshot description', 'description': 'v2 volume snapshot description',
'size': 80, 'size': 80,
'created_at': '2014-01-27 10:30:00',
'status': 'available', 'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
snapshot3 = vol_snaps.Snapshot( snapshot3 = vol_snaps.Snapshot(
@ -220,6 +222,7 @@ def data(TEST):
'name': '', 'name': '',
'description': 'v2 volume snapshot description 2', 'description': 'v2 volume snapshot description 2',
'size': 80, 'size': 80,
'created_at': '2014-01-27 10:30:00',
'status': 'available', 'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
snapshot4 = vol_snaps.Snapshot( snapshot4 = vol_snaps.Snapshot(
@ -228,6 +231,7 @@ def data(TEST):
'name': '', 'name': '',
'description': 'v2 volume snapshot with metadata description', 'description': 'v2 volume snapshot with metadata description',
'size': 80, 'size': 80,
'created_at': '2014-01-27 10:30:00',
'status': 'available', 'status': 'available',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234', 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234',
'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}}) 'metadata': {'snapshot_meta_key': 'snapshot_meta_value'}})
@ -269,6 +273,7 @@ def data(TEST):
'size': 10, 'size': 10,
'status': 'available', 'status': 'available',
'container_name': 'volumebackups', 'container_name': 'volumebackups',
'snapshot_id': None,
'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'}) 'volume_id': '11023e92-8008-4c8b-8059-7f2293ff3887'})
volume_backup2 = vol_backups.VolumeBackup( volume_backup2 = vol_backups.VolumeBackup(
@ -278,6 +283,7 @@ def data(TEST):
'description': 'volume backup 2', 'description': 'volume backup 2',
'size': 20, 'size': 20,
'status': 'available', 'status': 'available',
'snapshot_id': snapshot2.id,
'container_name': 'volumebackups', 'container_name': 'volumebackups',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})
@ -287,6 +293,7 @@ def data(TEST):
'name': 'backup3', 'name': 'backup3',
'description': 'volume backup 3', 'description': 'volume backup 3',
'size': 20, 'size': 20,
'snapshot_id': None,
'status': 'available', 'status': 'available',
'container_name': 'volumebackups', 'container_name': 'volumebackups',
'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'}) 'volume_id': '31023e92-8008-4c8b-8059-7f2293ff1234'})

View File

@ -0,0 +1,5 @@
---
features:
- Enabled horizon to make use of cinder's feature to
backup up snapshots of block storage volumes.