Implement create/get/list/delete volume backups

Code should me mostly straightforward. Things are done for
backups as they are done for snapshots.

Change-Id: I450356274916e9abef80f270b8a15f94fac6692b
This commit is contained in:
Jordan Pittier 2016-10-07 16:40:09 +02:00
parent 70f4d713e2
commit 6806fcfbbe
6 changed files with 283 additions and 2 deletions

View File

@ -0,0 +1,4 @@
---
features:
- Add support for Cinder volume backup resources, with the
usual methods (search/list/get/create/delete).

View File

@ -409,6 +409,21 @@ class VolumeSnapshotList(task_manager.Task):
return client.cinder_client.volume_snapshots.list(**self.args)
class VolumeBackupList(task_manager.Task):
def main(self, client):
return client.cinder_client.backups.list(**self.args)
class VolumeBackupCreate(task_manager.Task):
def main(self, client):
return client.cinder_client.backups.create(**self.args)
class VolumeBackupDelete(task_manager.Task):
def main(self, client):
return client.cinder_client.backups.delete(**self.args)
class VolumeSnapshotDelete(task_manager.Task):
def main(self, client):
return client.cinder_client.volume_snapshots.delete(**self.args)

View File

@ -1321,6 +1321,11 @@ class OpenStackCloud(_normalize.Normalizer):
return _utils._filter_list(
volumesnapshots, name_or_id, filters)
def search_volume_backups(self, name_or_id=None, filters=None):
volume_backups = self.list_volume_backups()
return _utils._filter_list(
volume_backups, name_or_id, filters)
def search_flavors(self, name_or_id=None, filters=None, get_extra=True):
flavors = self.list_flavors(get_extra=get_extra)
return _utils._filter_list(flavors, name_or_id, filters)
@ -3520,6 +3525,63 @@ class OpenStackCloud(_normalize.Normalizer):
return _utils._get_entity(self.search_volume_snapshots, name_or_id,
filters)
def create_volume_backup(self, volume_id, name=None, description=None,
force=False, wait=True, timeout=None):
"""Create a volume backup.
:param volume_id: the id of the volume to backup.
:param name: name of the backup, one will be generated if one is
not provided
:param description: description of the backup, one will be generated
if one is not provided
:param force: If set to True the backup will be created even if the
volume is attached to an instance, if False it will not
:param wait: If true, waits for volume backup to be created.
:param timeout: Seconds to wait for volume backup creation. None is
forever.
:returns: The created volume backup object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
with _utils.shade_exceptions(
"Error creating backup of volume {volume_id}".format(
volume_id=volume_id)):
backup = self.manager.submit_task(
_tasks.VolumeBackupCreate(
volume_id=volume_id, name=name, description=description,
force=force
)
)
if wait:
backup_id = backup['id']
msg = ("Timeout waiting for the volume backup {} to be "
"available".format(backup_id))
for _ in _utils._iterate_timeout(timeout, msg):
backup = self.get_volume_backup(backup_id)
if backup['status'] == 'available':
break
if backup['status'] == 'error':
msg = ("Error in creating volume "
"backup {}, please check logs".format(backup_id))
raise OpenStackCloudException(msg)
return backup
def get_volume_backup(self, name_or_id, filters=None):
"""Get a volume backup by name or ID.
:returns: A backup ``munch.Munch`` or None if no matching backup is
found.
"""
return _utils._get_entity(self.search_volume_backups, name_or_id,
filters)
def list_volume_snapshots(self, detailed=True, search_opts=None):
"""List all volume snapshots.
@ -3532,6 +3594,59 @@ class OpenStackCloud(_normalize.Normalizer):
_tasks.VolumeSnapshotList(
detailed=detailed, search_opts=search_opts)))
def list_volume_backups(self, detailed=True, search_opts=None):
"""
List all volume backups.
:param bool detailed: Also list details for each entry
:param dict search_opts: Search options
A dictionary of meta data to use for further filtering. Example::
{
'name': 'my-volume-backup',
'status': 'available',
'volume_id': 'e126044c-7b4c-43be-a32a-c9cbbc9ddb56',
'all_tenants': 1
}
:returns: A list of volume backups ``munch.Munch``.
"""
with _utils.shade_exceptions("Error getting a list of backups"):
return self.manager.submit_task(
_tasks.VolumeBackupList(
detailed=detailed, search_opts=search_opts))
def delete_volume_backup(self, name_or_id=None, force=False, wait=False,
timeout=None):
"""Delete a volume backup.
:param name_or_id: Name or unique ID of the volume backup.
:param force: Allow delete in state other than error or available.
:param wait: If true, waits for volume backup to be deleted.
:param timeout: Seconds to wait for volume backup deletion. None is
forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
volume_backup = self.get_volume_backup(name_or_id)
if not volume_backup:
return False
with _utils.shade_exceptions("Error in deleting volume backup"):
self.manager.submit_task(
_tasks.VolumeBackupDelete(
backup=volume_backup['id'], force=force
)
)
if wait:
msg = "Timeout waiting for the volume backup to be deleted."
for count in _utils._iterate_timeout(timeout, msg):
if not self.get_volume_backup(volume_backup['id']):
break
return True
def delete_volume_snapshot(self, name_or_id=None, wait=False,
timeout=None):
"""Delete a volume snapshot.

View File

@ -29,13 +29,16 @@ class TestCase(testtools.TestCase):
"""Test case base class for all tests."""
# A way to adjust slow test classes
TIMEOUT_SCALING_FACTOR = 1.0
def setUp(self):
"""Run before each test method to initialize test environment."""
super(TestCase, self).setUp()
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
test_timeout = int(os.environ.get('OS_TEST_TIMEOUT', 0))
try:
test_timeout = int(test_timeout)
test_timeout = int(test_timeout * self.TIMEOUT_SCALING_FACTOR)
except ValueError:
# If timeout value is invalid do not set a timeout.
test_timeout = 0

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# 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 shade.tests.functional import base
class TestVolume(base.BaseFunctionalTestCase):
# Creating a volume backup is incredibly slow.
TIMEOUT_SCALING_FACTOR = 1.5
def setUp(self):
super(TestVolume, self).setUp()
if not self.demo_cloud.has_service('volume'):
self.skipTest('volume service not supported by cloud')
def test_create_get_delete_volume_backup(self):
volume = self.demo_cloud.create_volume(
display_name=self.getUniqueString(), size=1)
self.addCleanup(self.demo_cloud.delete_volume, volume['id'])
backup_name_1 = self.getUniqueString()
backup_desc_1 = self.getUniqueString()
backup = self.demo_cloud.create_volume_backup(
volume_id=volume['id'], name=backup_name_1,
description=backup_desc_1, wait=True)
self.assertEqual(backup_name_1, backup['name'])
backup = self.demo_cloud.get_volume_backup(backup['id'])
self.assertEqual("available", backup['status'])
self.assertEqual(backup_desc_1, backup['description'])
self.demo_cloud.delete_volume_backup(backup['id'], wait=True)
self.assertIsNone(self.demo_cloud.get_volume_backup(backup['id']))
def test_list_volume_backups(self):
vol1 = self.demo_cloud.create_volume(
display_name=self.getUniqueString(), size=1)
self.addCleanup(self.demo_cloud.delete_volume, vol1['id'])
# We create 2 volumes to create 2 backups. We could have created 2
# backups from the same volume but taking 2 successive backups seems
# to be race-condition prone. And I didn't want to use an ugly sleep()
# here.
vol2 = self.demo_cloud.create_volume(
display_name=self.getUniqueString(), size=1)
self.addCleanup(self.demo_cloud.delete_volume, vol2['id'])
backup_name_1 = self.getUniqueString()
backup = self.demo_cloud.create_volume_backup(
volume_id=vol1['id'], name=backup_name_1)
self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id'])
backup = self.demo_cloud.create_volume_backup(volume_id=vol2['id'])
self.addCleanup(self.demo_cloud.delete_volume_backup, backup['id'])
backups = self.demo_cloud.list_volume_backups()
self.assertEqual(2, len(backups))
backups = self.demo_cloud.list_volume_backups(
search_opts={"name": backup_name_1})
self.assertEqual(1, len(backups))
self.assertEqual(backup_name_1, backups[0]['name'])

View File

@ -0,0 +1,72 @@
# 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 mock
import shade
from shade.tests.unit import base
class TestVolumeBackups(base.TestCase):
@mock.patch.object(shade.OpenStackCloud, 'list_volume_backups')
@mock.patch("shade._utils._filter_list")
def test_search_volume_backups(self, m_filter_list, m_list_volume_backups):
result = self.cloud.search_volume_backups(
mock.sentinel.name_or_id, mock.sentinel.filter)
m_list_volume_backups.assert_called_once_with()
m_filter_list.assert_called_once_with(
m_list_volume_backups.return_value, mock.sentinel.name_or_id,
mock.sentinel.filter)
self.assertIs(m_filter_list.return_value, result)
@mock.patch("shade._utils._get_entity")
def test_get_volume_backup(self, m_get_entity):
result = self.cloud.get_volume_backup(
mock.sentinel.name_or_id, mock.sentinel.filter)
self.assertIs(m_get_entity.return_value, result)
m_get_entity.assert_called_once_with(
self.cloud.search_volume_backups, mock.sentinel.name_or_id,
mock.sentinel.filter)
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
def test_list_volume_backups(self, m_cinder_client):
backup_id = '6ff16bdf-44d5-4bf9-b0f3-687549c76414'
m_cinder_client.backups.list.return_value = [
{'id': backup_id}
]
result = self.cloud.list_volume_backups(
mock.sentinel.detailed, mock.sentinel.search_opts)
m_cinder_client.backups.list.assert_called_once_with(
detailed=mock.sentinel.detailed,
search_opts=mock.sentinel.search_opts)
self.assertEqual(backup_id, result[0]['id'])
@mock.patch.object(shade.OpenStackCloud, 'cinder_client')
@mock.patch("shade._utils._iterate_timeout")
@mock.patch.object(shade.OpenStackCloud, 'get_volume_backup')
def test_delete_volume_backup(self, m_get_volume_backup,
m_iterate_timeout, m_cinder_client):
m_get_volume_backup.side_effect = [{'id': 42}, True, False]
self.cloud.delete_volume_backup(
mock.sentinel.name_or_id, mock.sentinel.force, mock.sentinel.wait,
mock.sentinel.timeout)
m_iterate_timeout.assert_called_once_with(
mock.sentinel.timeout, mock.ANY)
m_cinder_client.backups.delete.assert_called_once_with(
backup=42, force=mock.sentinel.force)
# We expect 3 calls, the last return_value is False which breaks the
# wait loop.
m_get_volume_backup.call_args_list = [mock.call(42)] * 3