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):
|
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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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
|
||||||
|
@ -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"]}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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",)
|
||||||
|
@ -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'),
|
||||||
|
@ -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'})
|
||||||
|
@ -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