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:
parent
73c503ea3f
commit
becf303768
@ -9,6 +9,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from openstack import exceptions
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
from openstack import utils
|
from openstack import utils
|
||||||
|
|
||||||
@ -20,7 +21,8 @@ class Backup(resource.Resource):
|
|||||||
base_path = "/backups"
|
base_path = "/backups"
|
||||||
|
|
||||||
_query_mapping = resource.QueryParameters(
|
_query_mapping = resource.QueryParameters(
|
||||||
'all_tenants', 'limit', 'marker',
|
'all_tenants', 'limit', 'marker', 'project_id',
|
||||||
|
'name', 'status', 'volume_id',
|
||||||
'sort_key', 'sort_dir')
|
'sort_key', 'sort_dir')
|
||||||
|
|
||||||
# capabilities
|
# capabilities
|
||||||
@ -80,13 +82,20 @@ class Backup(resource.Resource):
|
|||||||
:param session: openstack session
|
:param session: openstack session
|
||||||
:param volume_id: The ID of the volume to restore the backup to.
|
:param volume_id: The ID of the volume to restore the backup to.
|
||||||
:param name: The name for new volume creation to restore.
|
:param name: The name for new volume creation to restore.
|
||||||
:return:
|
:return: Updated backup instance
|
||||||
"""
|
"""
|
||||||
url = utils.urljoin(self.base_path, self.id, "restore")
|
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,
|
response = session.post(url,
|
||||||
json=body)
|
json=body)
|
||||||
self._translate_response(response)
|
self._translate_response(response, has_body=False)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from openstack import resource
|
|||||||
|
|
||||||
|
|
||||||
class Pools(resource.Resource):
|
class Pools(resource.Resource):
|
||||||
resource_key = "pool"
|
resource_key = ""
|
||||||
resources_key = "pools"
|
resources_key = "pools"
|
||||||
base_path = "/scheduler-stats/get_pools?detail=True"
|
base_path = "/scheduler-stats/get_pools?detail=True"
|
||||||
|
|
||||||
|
@ -228,6 +228,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
|
|||||||
* sort_dir: Sorts by one or more sets of attribute and sort
|
* sort_dir: Sorts by one or more sets of attribute and sort
|
||||||
direction combinations. If you omit the sort direction
|
direction combinations. If you omit the sort direction
|
||||||
in a set, default is desc.
|
in a set, default is desc.
|
||||||
|
* project_id: Project ID to query backups for.
|
||||||
|
|
||||||
:returns: A generator of backup objects.
|
:returns: A generator of backup objects.
|
||||||
"""
|
"""
|
||||||
@ -290,7 +291,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
|
|||||||
self._delete(_backup.Backup, backup,
|
self._delete(_backup.Backup, backup,
|
||||||
ignore_missing=ignore_missing)
|
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
|
"""Restore a Backup to volume
|
||||||
|
|
||||||
:param backup: The value can be the ID of a backup or a
|
:param backup: The value can be the ID of a backup or a
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from openstack import exceptions
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
from openstack import utils
|
from openstack import utils
|
||||||
|
|
||||||
@ -19,8 +20,12 @@ class Backup(resource.Resource):
|
|||||||
resources_key = "backups"
|
resources_key = "backups"
|
||||||
base_path = "/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(
|
_query_mapping = resource.QueryParameters(
|
||||||
'all_tenants', 'limit', 'marker',
|
'all_tenants', 'limit', 'marker', 'project_id',
|
||||||
|
'name', 'status', 'volume_id',
|
||||||
'sort_key', 'sort_dir')
|
'sort_key', 'sort_dir')
|
||||||
|
|
||||||
# capabilities
|
# capabilities
|
||||||
@ -58,10 +63,15 @@ class Backup(resource.Resource):
|
|||||||
is_incremental = resource.Body("is_incremental", type=bool)
|
is_incremental = resource.Body("is_incremental", type=bool)
|
||||||
#: A list of links associated with this volume. *Type: list*
|
#: A list of links associated with this volume. *Type: list*
|
||||||
links = resource.Body("links", type=list)
|
links = resource.Body("links", type=list)
|
||||||
|
#: The backup metadata. New in version 3.43
|
||||||
|
metadata = resource.Body('metadata', type=dict)
|
||||||
#: backup name
|
#: backup name
|
||||||
name = resource.Body("name")
|
name = resource.Body("name")
|
||||||
#: backup object count
|
#: backup object count
|
||||||
object_count = resource.Body("object_count", type=int)
|
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).
|
#: The size of the volume, in gibibytes (GiB).
|
||||||
size = resource.Body("size", type=int)
|
size = resource.Body("size", type=int)
|
||||||
#: The UUID of the source volume snapshot.
|
#: The UUID of the source volume snapshot.
|
||||||
@ -71,6 +81,8 @@ class Backup(resource.Resource):
|
|||||||
status = resource.Body("status")
|
status = resource.Body("status")
|
||||||
#: The date and time when the resource was updated.
|
#: The date and time when the resource was updated.
|
||||||
updated_at = resource.Body("updated_at")
|
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.
|
#: The UUID of the volume.
|
||||||
volume_id = resource.Body("volume_id")
|
volume_id = resource.Body("volume_id")
|
||||||
|
|
||||||
@ -80,13 +92,20 @@ class Backup(resource.Resource):
|
|||||||
:param session: openstack session
|
:param session: openstack session
|
||||||
:param volume_id: The ID of the volume to restore the backup to.
|
:param volume_id: The ID of the volume to restore the backup to.
|
||||||
:param name: The name for new volume creation to restore.
|
:param name: The name for new volume creation to restore.
|
||||||
:return:
|
:return: Updated backup instance
|
||||||
"""
|
"""
|
||||||
url = utils.urljoin(self.base_path, self.id, "restore")
|
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,
|
response = session.post(url,
|
||||||
json=body)
|
json=body)
|
||||||
self._translate_response(response)
|
self._translate_response(response, has_body=False)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from openstack import resource
|
|||||||
|
|
||||||
|
|
||||||
class Pools(resource.Resource):
|
class Pools(resource.Resource):
|
||||||
resource_key = "pool"
|
resource_key = ""
|
||||||
resources_key = "pools"
|
resources_key = "pools"
|
||||||
base_path = "/scheduler-stats/get_pools?detail=True"
|
base_path = "/scheduler-stats/get_pools?detail=True"
|
||||||
|
|
||||||
|
@ -12,14 +12,15 @@
|
|||||||
|
|
||||||
|
|
||||||
from openstack.block_storage.v2 import stats as _stats
|
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):
|
def setUp(self):
|
||||||
super(TestStats, self).setUp()
|
super(TestStats, self).setUp()
|
||||||
sot = self.conn.block_storage.backend_pools()
|
|
||||||
|
sot = self.operator_cloud.block_storage.backend_pools()
|
||||||
for pool in sot:
|
for pool in sot:
|
||||||
self.assertIsInstance(pool, _stats.Pools)
|
self.assertIsInstance(pool, _stats.Pools)
|
||||||
|
|
||||||
@ -35,10 +36,11 @@ class TestStats(base.BaseFunctionalTest):
|
|||||||
'allocated_capacity_gb', 'reserved_percentage',
|
'allocated_capacity_gb', 'reserved_percentage',
|
||||||
'location_info']
|
'location_info']
|
||||||
capList.sort()
|
capList.sort()
|
||||||
pools = self.conn.block_storage.backend_pools()
|
pools = self.operator_cloud.block_storage.backend_pools()
|
||||||
for pool in pools:
|
for pool in pools:
|
||||||
caps = pool.capabilities
|
caps = pool.capabilities
|
||||||
keys = caps.keys()
|
keys = list(caps.keys())
|
||||||
keys.sort()
|
|
||||||
assert isinstance(caps, dict)
|
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)
|
@ -16,6 +16,7 @@ from keystoneauth1 import adapter
|
|||||||
|
|
||||||
from openstack.tests.unit import base
|
from openstack.tests.unit import base
|
||||||
|
|
||||||
|
from openstack import exceptions
|
||||||
from openstack.block_storage.v2 import backup
|
from openstack.block_storage.v2 import backup
|
||||||
|
|
||||||
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
|
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
|
||||||
@ -42,8 +43,15 @@ class TestBackup(base.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestBackup, self).setUp()
|
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 = mock.Mock(spec=adapter.Adapter)
|
||||||
self.sess.get = mock.Mock()
|
self.sess.get = mock.Mock()
|
||||||
|
self.sess.post = mock.Mock(return_value=self.resp)
|
||||||
self.sess.default_microversion = mock.Mock(return_value='')
|
self.sess.default_microversion = mock.Mock(return_value='')
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
@ -62,8 +70,12 @@ class TestBackup(base.TestCase):
|
|||||||
"all_tenants": "all_tenants",
|
"all_tenants": "all_tenants",
|
||||||
"limit": "limit",
|
"limit": "limit",
|
||||||
"marker": "marker",
|
"marker": "marker",
|
||||||
|
"name": "name",
|
||||||
|
"project_id": "project_id",
|
||||||
"sort_dir": "sort_dir",
|
"sort_dir": "sort_dir",
|
||||||
"sort_key": "sort_key"
|
"sort_key": "sort_key",
|
||||||
|
"status": "status",
|
||||||
|
"volume_id": "volume_id"
|
||||||
},
|
},
|
||||||
sot._query_mapping._mapping
|
sot._query_mapping._mapping
|
||||||
)
|
)
|
||||||
@ -85,3 +97,39 @@ class TestBackup(base.TestCase):
|
|||||||
self.assertEqual(BACKUP["size"], sot.size)
|
self.assertEqual(BACKUP["size"], sot.size)
|
||||||
self.assertEqual(BACKUP["has_dependent_backups"],
|
self.assertEqual(BACKUP["has_dependent_backups"],
|
||||||
sot.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
|
||||||
|
)
|
||||||
|
@ -35,7 +35,7 @@ class TestBackendPools(base.TestCase):
|
|||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
sot = stats.Pools(POOLS)
|
sot = stats.Pools(POOLS)
|
||||||
self.assertEqual("pool", sot.resource_key)
|
self.assertEqual("", sot.resource_key)
|
||||||
self.assertEqual("pools", sot.resources_key)
|
self.assertEqual("pools", sot.resources_key)
|
||||||
self.assertEqual("/scheduler-stats/get_pools?detail=True",
|
self.assertEqual("/scheduler-stats/get_pools?detail=True",
|
||||||
sot.base_path)
|
sot.base_path)
|
@ -16,6 +16,7 @@ from keystoneauth1 import adapter
|
|||||||
|
|
||||||
from openstack.tests.unit import base
|
from openstack.tests.unit import base
|
||||||
|
|
||||||
|
from openstack import exceptions
|
||||||
from openstack.block_storage.v3 import backup
|
from openstack.block_storage.v3 import backup
|
||||||
|
|
||||||
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
|
FAKE_ID = "6685584b-1eac-4da6-b5c3-555430cf68ff"
|
||||||
@ -34,7 +35,10 @@ BACKUP = {
|
|||||||
"status": "available",
|
"status": "available",
|
||||||
"volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6",
|
"volume_id": "e5185058-943a-4cb4-96d9-72c184c337d6",
|
||||||
"is_incremental": True,
|
"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):
|
def setUp(self):
|
||||||
super(TestBackup, self).setUp()
|
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 = mock.Mock(spec=adapter.Adapter)
|
||||||
self.sess.get = mock.Mock()
|
self.sess.get = mock.Mock()
|
||||||
|
self.sess.post = mock.Mock(return_value=self.resp)
|
||||||
self.sess.default_microversion = mock.Mock(return_value='')
|
self.sess.default_microversion = mock.Mock(return_value='')
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
@ -62,8 +73,12 @@ class TestBackup(base.TestCase):
|
|||||||
"all_tenants": "all_tenants",
|
"all_tenants": "all_tenants",
|
||||||
"limit": "limit",
|
"limit": "limit",
|
||||||
"marker": "marker",
|
"marker": "marker",
|
||||||
|
"name": "name",
|
||||||
|
"project_id": "project_id",
|
||||||
"sort_dir": "sort_dir",
|
"sort_dir": "sort_dir",
|
||||||
"sort_key": "sort_key"
|
"sort_key": "sort_key",
|
||||||
|
"status": "status",
|
||||||
|
"volume_id": "volume_id"
|
||||||
},
|
},
|
||||||
sot._query_mapping._mapping
|
sot._query_mapping._mapping
|
||||||
)
|
)
|
||||||
@ -85,3 +100,43 @@ class TestBackup(base.TestCase):
|
|||||||
self.assertEqual(BACKUP["size"], sot.size)
|
self.assertEqual(BACKUP["size"], sot.size)
|
||||||
self.assertEqual(BACKUP["has_dependent_backups"],
|
self.assertEqual(BACKUP["has_dependent_backups"],
|
||||||
sot.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
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user