Volume.Backup restore fixes

for the volume backup restoration name or id (or both) is required. Do
not force both to be set in v2.
- Add restore unit tests
- Add new properties in v3 and QP for v2 and v3
- Move test_stats that ended up in `block_store`. Heh?

Change-Id: Ie118afe0116ced3580e52941e69cbc57bea2f7f8
This commit is contained in:
Artem Goncharov 2019-06-18 09:49:33 +02:00
parent 73c503ea3f
commit becf303768
9 changed files with 156 additions and 22 deletions

View File

@ -9,6 +9,7 @@
# 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 openstack import exceptions
from openstack import resource
from openstack import utils
@ -20,7 +21,8 @@ class Backup(resource.Resource):
base_path = "/backups"
_query_mapping = resource.QueryParameters(
'all_tenants', 'limit', 'marker',
'all_tenants', 'limit', 'marker', 'project_id',
'name', 'status', 'volume_id',
'sort_key', 'sort_dir')
# capabilities
@ -80,13 +82,20 @@ class Backup(resource.Resource):
:param session: openstack session
:param volume_id: The ID of the volume to restore the backup to.
:param name: The name for new volume creation to restore.
:return:
:return: Updated backup instance
"""
url = utils.urljoin(self.base_path, self.id, "restore")
body = {"restore": {"volume_id": volume_id, "name": name}}
body = {'restore': {}}
if volume_id:
body['restore']['volume_id'] = volume_id
if name:
body['restore']['name'] = name
if not (volume_id or name):
raise exceptions.SDKException('Either of `name` or `volume_id`'
' must be specified.')
response = session.post(url,
json=body)
self._translate_response(response)
self._translate_response(response, has_body=False)
return self

View File

@ -14,7 +14,7 @@ from openstack import resource
class Pools(resource.Resource):
resource_key = "pool"
resource_key = ""
resources_key = "pools"
base_path = "/scheduler-stats/get_pools?detail=True"

View File

@ -228,6 +228,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
* sort_dir: Sorts by one or more sets of attribute and sort
direction combinations. If you omit the sort direction
in a set, default is desc.
* project_id: Project ID to query backups for.
:returns: A generator of backup objects.
"""
@ -290,7 +291,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
self._delete(_backup.Backup, backup,
ignore_missing=ignore_missing)
def restore_backup(self, backup, volume_id, name):
def restore_backup(self, backup, volume_id=None, name=None):
"""Restore a Backup to volume
:param backup: The value can be the ID of a backup or a

View File

@ -9,6 +9,7 @@
# 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 openstack import exceptions
from openstack import resource
from openstack import utils
@ -19,8 +20,12 @@ class Backup(resource.Resource):
resources_key = "backups"
base_path = "/backups"
# TODO(gtema): Starting from ~3.31(3.45) Cinder seems to support also fuzzy
# search (name~, status~, volume_id~). But this is not documented
# officially and seem to require microversion be set
_query_mapping = resource.QueryParameters(
'all_tenants', 'limit', 'marker',
'all_tenants', 'limit', 'marker', 'project_id',
'name', 'status', 'volume_id',
'sort_key', 'sort_dir')
# capabilities
@ -58,10 +63,15 @@ class Backup(resource.Resource):
is_incremental = resource.Body("is_incremental", type=bool)
#: A list of links associated with this volume. *Type: list*
links = resource.Body("links", type=list)
#: The backup metadata. New in version 3.43
metadata = resource.Body('metadata', type=dict)
#: backup name
name = resource.Body("name")
#: backup object count
object_count = resource.Body("object_count", type=int)
#: The UUID of the owning project.
#: New in version 3.18
project_id = resource.Body('os-backup-project-attr:project_id')
#: The size of the volume, in gibibytes (GiB).
size = resource.Body("size", type=int)
#: The UUID of the source volume snapshot.
@ -71,6 +81,8 @@ class Backup(resource.Resource):
status = resource.Body("status")
#: The date and time when the resource was updated.
updated_at = resource.Body("updated_at")
#: The UUID of the project owner. New in 3.56
user_id = resource.Body('user_id')
#: The UUID of the volume.
volume_id = resource.Body("volume_id")
@ -80,13 +92,20 @@ class Backup(resource.Resource):
:param session: openstack session
:param volume_id: The ID of the volume to restore the backup to.
:param name: The name for new volume creation to restore.
:return:
:return: Updated backup instance
"""
url = utils.urljoin(self.base_path, self.id, "restore")
body = {"restore": {"volume_id": volume_id, "name": name}}
body = {'restore': {}}
if volume_id:
body['restore']['volume_id'] = volume_id
if name:
body['restore']['name'] = name
if not (volume_id or name):
raise exceptions.SDKException('Either of `name` or `volume_id`'
' must be specified.')
response = session.post(url,
json=body)
self._translate_response(response)
self._translate_response(response, has_body=False)
return self

View File

@ -14,7 +14,7 @@ from openstack import resource
class Pools(resource.Resource):
resource_key = "pool"
resource_key = ""
resources_key = "pools"
base_path = "/scheduler-stats/get_pools?detail=True"

View File

@ -12,14 +12,15 @@
from openstack.block_storage.v2 import stats as _stats
from openstack.tests.functional import base
from openstack.tests.functional.block_storage.v2 import base
class TestStats(base.BaseFunctionalTest):
class TestStats(base.BaseBlockStorageTest):
def setUp(self):
super(TestStats, self).setUp()
sot = self.conn.block_storage.backend_pools()
sot = self.operator_cloud.block_storage.backend_pools()
for pool in sot:
self.assertIsInstance(pool, _stats.Pools)
@ -35,10 +36,11 @@ class TestStats(base.BaseFunctionalTest):
'allocated_capacity_gb', 'reserved_percentage',
'location_info']
capList.sort()
pools = self.conn.block_storage.backend_pools()
pools = self.operator_cloud.block_storage.backend_pools()
for pool in pools:
caps = pool.capabilities
keys = caps.keys()
keys.sort()
keys = list(caps.keys())
assert isinstance(caps, dict)
self.assertListEqual(keys, capList)
# Check that we have at minimum listed capabilities
for cap in sorted(capList):
self.assertIn(cap, keys)

View File

@ -16,6 +16,7 @@ from keystoneauth1 import adapter
from openstack.tests.unit import base
from openstack import exceptions
from openstack.block_storage.v2 import backup
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
@ -42,8 +43,15 @@ class TestBackup(base.TestCase):
def setUp(self):
super(TestBackup, self).setUp()
self.resp = mock.Mock()
self.resp.body = None
self.resp.json = mock.Mock(return_value=self.resp.body)
self.resp.headers = {}
self.resp.status_code = 202
self.sess = mock.Mock(spec=adapter.Adapter)
self.sess.get = mock.Mock()
self.sess.post = mock.Mock(return_value=self.resp)
self.sess.default_microversion = mock.Mock(return_value='')
def test_basic(self):
@ -62,8 +70,12 @@ class TestBackup(base.TestCase):
"all_tenants": "all_tenants",
"limit": "limit",
"marker": "marker",
"name": "name",
"project_id": "project_id",
"sort_dir": "sort_dir",
"sort_key": "sort_key"
"sort_key": "sort_key",
"status": "status",
"volume_id": "volume_id"
},
sot._query_mapping._mapping
)
@ -85,3 +97,39 @@ class TestBackup(base.TestCase):
self.assertEqual(BACKUP["size"], sot.size)
self.assertEqual(BACKUP["has_dependent_backups"],
sot.has_dependent_backups)
def test_restore(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"volume_id": "vol", "name": "name"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_name(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, name='name'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"name": "name"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_vol_id(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, volume_id='vol'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"volume_id": "vol"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_no_params(self):
sot = backup.Backup(**BACKUP)
self.assertRaises(
exceptions.SDKException,
sot.restore,
self.sess
)

View File

@ -35,7 +35,7 @@ class TestBackendPools(base.TestCase):
def test_basic(self):
sot = stats.Pools(POOLS)
self.assertEqual("pool", sot.resource_key)
self.assertEqual("", sot.resource_key)
self.assertEqual("pools", sot.resources_key)
self.assertEqual("/scheduler-stats/get_pools?detail=True",
sot.base_path)

View File

@ -16,6 +16,7 @@ from keystoneauth1 import adapter
from openstack.tests.unit import base
from openstack import exceptions
from openstack.block_storage.v3 import backup
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
@ -34,7 +35,10 @@ BACKUP = {
"status": "available",
"volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6",
"is_incremental": True,
"has_dependent_backups": False
"has_dependent_backups": False,
"os-backup-project-attr:project_id": "2c67a14be9314c5dae2ee6c4ec90cf0b",
"user_id": "515ba0dd59f84f25a6a084a45d8d93b2",
"metadata": {"key": "value"}
}
@ -42,8 +46,15 @@ class TestBackup(base.TestCase):
def setUp(self):
super(TestBackup, self).setUp()
self.resp = mock.Mock()
self.resp.body = None
self.resp.json = mock.Mock(return_value=self.resp.body)
self.resp.headers = {}
self.resp.status_code = 202
self.sess = mock.Mock(spec=adapter.Adapter)
self.sess.get = mock.Mock()
self.sess.post = mock.Mock(return_value=self.resp)
self.sess.default_microversion = mock.Mock(return_value='')
def test_basic(self):
@ -62,8 +73,12 @@ class TestBackup(base.TestCase):
"all_tenants": "all_tenants",
"limit": "limit",
"marker": "marker",
"name": "name",
"project_id": "project_id",
"sort_dir": "sort_dir",
"sort_key": "sort_key"
"sort_key": "sort_key",
"status": "status",
"volume_id": "volume_id"
},
sot._query_mapping._mapping
)
@ -85,3 +100,43 @@ class TestBackup(base.TestCase):
self.assertEqual(BACKUP["size"], sot.size)
self.assertEqual(BACKUP["has_dependent_backups"],
sot.has_dependent_backups)
self.assertEqual(BACKUP['os-backup-project-attr:project_id'],
sot.project_id)
self.assertEqual(BACKUP['metadata'], sot.metadata)
self.assertEqual(BACKUP['user_id'], sot.user_id)
def test_restore(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, 'vol', 'name'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"volume_id": "vol", "name": "name"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_name(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, name='name'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"name": "name"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_vol_id(self):
sot = backup.Backup(**BACKUP)
self.assertEqual(sot, sot.restore(self.sess, volume_id='vol'))
url = 'backups/%s/restore' % FAKE_ID
body = {"restore": {"volume_id": "vol"}}
self.sess.post.assert_called_with(url, json=body)
def test_restore_no_params(self):
sot = backup.Backup(**BACKUP)
self.assertRaises(
exceptions.SDKException,
sot.restore,
self.sess
)