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:
parent
70f4d713e2
commit
6806fcfbbe
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Add support for Cinder volume backup resources, with the
|
||||
usual methods (search/list/get/create/delete).
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
72
shade/tests/functional/test_volume_backup.py
Normal file
72
shade/tests/functional/test_volume_backup.py
Normal 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'])
|
72
shade/tests/unit/test_volume_backups.py
Normal file
72
shade/tests/unit/test_volume_backups.py
Normal 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
|
Loading…
Reference in New Issue
Block a user