diff --git a/openstack/block_storage/v2/backup.py b/openstack/block_storage/v2/backup.py index ee3d53163..61d53c62c 100644 --- a/openstack/block_storage/v2/backup.py +++ b/openstack/block_storage/v2/backup.py @@ -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 diff --git a/openstack/block_storage/v2/stats.py b/openstack/block_storage/v2/stats.py index dcd2e2945..8f9a44224 100644 --- a/openstack/block_storage/v2/stats.py +++ b/openstack/block_storage/v2/stats.py @@ -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" diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index 6590b2fc3..11b72952d 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -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 diff --git a/openstack/block_storage/v3/backup.py b/openstack/block_storage/v3/backup.py index ee3d53163..2212cf629 100644 --- a/openstack/block_storage/v3/backup.py +++ b/openstack/block_storage/v3/backup.py @@ -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 diff --git a/openstack/block_storage/v3/stats.py b/openstack/block_storage/v3/stats.py index dcd2e2945..8f9a44224 100644 --- a/openstack/block_storage/v3/stats.py +++ b/openstack/block_storage/v3/stats.py @@ -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" diff --git a/openstack/tests/functional/block_store/v2/test_stats.py b/openstack/tests/functional/block_storage/v2/test_stats.py similarity index 78% rename from openstack/tests/functional/block_store/v2/test_stats.py rename to openstack/tests/functional/block_storage/v2/test_stats.py index ef64d49a7..63175645f 100644 --- a/openstack/tests/functional/block_store/v2/test_stats.py +++ b/openstack/tests/functional/block_storage/v2/test_stats.py @@ -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) diff --git a/openstack/tests/unit/block_storage/v2/test_backup.py b/openstack/tests/unit/block_storage/v2/test_backup.py index 58512bfc5..a25a03753 100644 --- a/openstack/tests/unit/block_storage/v2/test_backup.py +++ b/openstack/tests/unit/block_storage/v2/test_backup.py @@ -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 + ) diff --git a/openstack/tests/unit/block_store/v2/test_stats.py b/openstack/tests/unit/block_storage/v2/test_stats.py similarity index 96% rename from openstack/tests/unit/block_store/v2/test_stats.py rename to openstack/tests/unit/block_storage/v2/test_stats.py index 729ec6370..53817a4a8 100644 --- a/openstack/tests/unit/block_store/v2/test_stats.py +++ b/openstack/tests/unit/block_storage/v2/test_stats.py @@ -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) diff --git a/openstack/tests/unit/block_storage/v3/test_backup.py b/openstack/tests/unit/block_storage/v3/test_backup.py index 4d206c452..a2b0def81 100644 --- a/openstack/tests/unit/block_storage/v3/test_backup.py +++ b/openstack/tests/unit/block_storage/v3/test_backup.py @@ -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 + )