diff --git a/doc/source/user/proxies/block_storage_v3.rst b/doc/source/user/proxies/block_storage_v3.rst index 5fc796549..65b5bb919 100644 --- a/doc/source/user/proxies/block_storage_v3.rst +++ b/doc/source/user/proxies/block_storage_v3.rst @@ -71,6 +71,14 @@ Group Operations :members: create_group, create_group_from_source, delete_group, update_group, get_group, find_group, groups, reset_group_state +Group Snapshot Operations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v3._proxy.Proxy + :noindex: + :members: create_group_snapshot, delete_group_snapshot, get_group_snapshot, + find_group_snapshot, group_snapshots, reset_group_snapshot_state + Group Type Operations ^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/block_storage/v3/_proxy.py b/openstack/block_storage/v3/_proxy.py index bbdb1dbb4..0da1d97e9 100644 --- a/openstack/block_storage/v3/_proxy.py +++ b/openstack/block_storage/v3/_proxy.py @@ -16,6 +16,7 @@ from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import capabilities as _capabilities from openstack.block_storage.v3 import extension as _extension from openstack.block_storage.v3 import group as _group +from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack.block_storage.v3 import group_type as _group_type from openstack.block_storage.v3 import limits as _limits from openstack.block_storage.v3 import quota_set as _quota_set @@ -1103,6 +1104,91 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): return self._list(availability_zone.AvailabilityZone) + # ====== GROUP SNAPSHOT ====== + def get_group_snapshot(self, group_snapshot_id): + """Get a group snapshot + + :param group_snapshot_id: The ID of the group snapshot to get. + + :returns: A GroupSnapshot instance. + :rtype: :class:`~openstack.block_storage.v3.group_snapshot` + """ + return self._get(_group_snapshot.GroupSnapshot, group_snapshot_id) + + def find_group_snapshot(self, name_or_id, ignore_missing=True): + """Find a single group snapshot + + :param name_or_id: The name or ID of a group snapshot. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised + when the group snapshot does not exist. + + :returns: One :class:`~openstack.block_storage.v3.group_snapshot` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._find( + _group_snapshot.GroupSnapshot, name_or_id, + ignore_missing=ignore_missing) + + def group_snapshots(self, details=True, **query): + """Retrieve a generator of group snapshots + + :param bool details: When ``True``, returns + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + objects with additional attributes filled. + :param kwargs query: Optional query parameters to be sent to limit + the group snapshots being returned. + :returns: A generator of group snapshtos. + """ + base_path = '/group_snapshots' + if details: + base_path = '/group_snapshots/detail' + + return self._list( + _group_snapshot.GroupSnapshot, + base_path=base_path, + **query, + ) + + def create_group_snapshot(self, **attrs): + """Create a group snapshot + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + comprised of the properties on the GroupSnapshot class. + + :returns: The results of group snapshot creation. + :rtype: :class:`~openstack.block_storage.v3.group_snapshot`. + """ + return self._create(_group_snapshot.GroupSnapshot, **attrs) + + def reset_group_snapshot_state(self, group_snapshot, state): + """Reset group snapshot status + + :param group_snapshot: The + :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot` + to set the state. + :param state: The state of the group snapshot to be set. + + :returns: None + """ + resource = self._get_resource( + _group_snapshot.GroupSnapshot, group_snapshot) + resource.reset_state(self, state) + + def delete_group_snapshot(self, group_snapshot, ignore_missing=True): + """Delete a group snapshot + + :param group_snapshot: The :class:`~openstack.block_storage.v3. + group_snapshot.GroupSnapshot` to delete. + + :returns: None + """ + self._delete( + _group_snapshot.GroupSnapshot, group_snapshot, + ignore_missing=ignore_missing) + # ====== GROUP TYPE ====== def get_group_type(self, group_type): """Get a specific group type @@ -1180,7 +1266,7 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): When set to ``True``, no exception will be set when attempting to delete a nonexistent zone. - :returns: ''None'' + :returns: None """ self._delete( _group_type.GroupType, group_type, ignore_missing=ignore_missing) diff --git a/openstack/block_storage/v3/group_snapshot.py b/openstack/block_storage/v3/group_snapshot.py new file mode 100644 index 000000000..cb72541b5 --- /dev/null +++ b/openstack/block_storage/v3/group_snapshot.py @@ -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. + +from openstack import exceptions +from openstack import resource +from openstack import utils + + +class GroupSnapshot(resource.Resource): + resource_key = "group_snapshot" + resources_key = "group_snapshots" + base_path = "/group_snapshots" + + # capabilities + allow_fetch = True + allow_create = True + allow_delete = True + allow_commit = False + allow_list = True + + #: Properties + #: The date and time when the resource was created. + created_at = resource.Body("created_at") + #: The group snapshot description. + description = resource.Body("description") + #: The UUID of the source group. + group_id = resource.Body("group_id") + #: The group type ID. + group_type_id = resource.Body("group_type_id") + #: The ID of the group snapshot. + id = resource.Body("id") + #: The group snapshot name. + name = resource.Body("name") + #: The UUID of the volume group snapshot project. + project_id = resource.Body("project_id") + #: The status of the generic group snapshot. + status = resource.Body("status") + + # Pagination support was added in microversion 3.29 + _max_microversion = '3.29' + + def _action(self, session, body, microversion=None): + """Preform aggregate actions given the message body.""" + url = utils.urljoin(self.base_path, self.id, 'action') + headers = {'Accept': ''} + # TODO(stephenfin): This logic belongs in openstack.resource I suspect + if microversion is None: + if session.default_microversion: + microversion = session.default_microversion + else: + microversion = utils.maximum_supported_microversion( + session, self._max_microversion, + ) + response = session.post( + url, json=body, headers=headers, microversion=microversion, + ) + exceptions.raise_from_response(response) + return response + + def reset_state(self, session, state): + """Resets the status for a group snapshot.""" + body = {'reset_status': {'status': state}} + return self._action(session, body) diff --git a/openstack/tests/functional/block_storage/v3/test_group.py b/openstack/tests/functional/block_storage/v3/test_group.py index a3929d610..7312641bf 100644 --- a/openstack/tests/functional/block_storage/v3/test_group.py +++ b/openstack/tests/functional/block_storage/v3/test_group.py @@ -11,7 +11,9 @@ # under the License. from openstack.block_storage.v3 import group as _group +from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack.block_storage.v3 import group_type as _group_type +from openstack.block_storage.v3 import volume as _volume from openstack.tests.functional.block_storage.v3 import base @@ -151,3 +153,65 @@ class TestGroup(base.BaseBlockStorageTest): group = self.conn.block_storage.get_group(self.group.id) self.assertEqual(group_name, group.name) self.assertEqual(group_description, group.description) + + def test_group_snapshot(self): + # group snapshots require a volume + # no need for a teardown as the deletion of the group (with the + # 'delete_volumes' flag) will handle this but we do need to wait for + # the thing to be created + volume_name = self.getUniqueString() + self.volume = self.conn.block_storage.create_volume( + name=volume_name, + volume_type=self.volume_type.id, + group_id=self.group.id, + size=1, + ) + self.conn.block_storage.wait_for_status( + self.volume, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.assertIsInstance(self.volume, _volume.Volume) + + group_snapshot_name = self.getUniqueString() + self.group_snapshot = self.conn.block_storage.create_group_snapshot( + name=group_snapshot_name, + group_id=self.group.id, + ) + self.conn.block_storage.wait_for_status( + self.group_snapshot, + status='available', + failures=['error'], + interval=2, + wait=self._wait_for_timeout, + ) + self.assertIsInstance( + self.group_snapshot, + _group_snapshot.GroupSnapshot, + ) + + # get + group_snapshot = self.conn.block_storage.get_group_snapshot( + self.group_snapshot.id, + ) + self.assertEqual(self.group_snapshot.name, group_snapshot.name) + + # find + group_snapshot = self.conn.block_storage.find_group_snapshot( + self.group_snapshot.name, + ) + self.assertEqual(self.group_snapshot.id, group_snapshot.id) + + # list + group_snapshots = self.conn.block_storage.group_snapshots() + # other tests may have created group snapshot and there can be defaults + # so we don't assert that this is the *only* group snapshot present + self.assertIn(self.group_snapshot.id, {g.id for g in group_snapshots}) + + # update (not supported) + + # delete + self.conn.block_storage.delete_group_snapshot(self.group_snapshot) + self.conn.block_storage.wait_for_delete(self.group_snapshot) diff --git a/openstack/tests/unit/block_storage/v3/test_group_snapshot.py b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py new file mode 100644 index 000000000..8005bd6da --- /dev/null +++ b/openstack/tests/unit/block_storage/v3/test_group_snapshot.py @@ -0,0 +1,51 @@ +# 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 openstack.block_storage.v3 import group_snapshot +from openstack.tests.unit import base + +GROUP_SNAPSHOT = { + "id": "6f519a48-3183-46cf-a32f-41815f813986", + "group_id": "6f519a48-3183-46cf-a32f-41815f814444", + "status": "available", + "created_at": "2015-09-16T09:28:52.000000", + "name": "my_group_snapshot1", + "description": "my first group snapshot", + "group_type_id": "7270c56e-6354-4528-8e8b-f54dee2232c8", + "project_id": "7ccf4863071f44aeb8f141f65780c51b", +} + + +class TestGroupSnapshot(base.TestCase): + def test_basic(self): + resource = group_snapshot.GroupSnapshot() + self.assertEqual("group_snapshot", resource.resource_key) + self.assertEqual("group_snapshots", resource.resources_key) + self.assertEqual("/group_snapshots", resource.base_path) + self.assertTrue(resource.allow_create) + self.assertTrue(resource.allow_fetch) + self.assertTrue(resource.allow_delete) + self.assertTrue(resource.allow_list) + self.assertFalse(resource.allow_commit) + + def test_make_resource(self): + resource = group_snapshot.GroupSnapshot(**GROUP_SNAPSHOT) + self.assertEqual(GROUP_SNAPSHOT["created_at"], resource.created_at) + self.assertEqual(GROUP_SNAPSHOT["description"], resource.description) + self.assertEqual(GROUP_SNAPSHOT["group_id"], resource.group_id) + self.assertEqual( + GROUP_SNAPSHOT["group_type_id"], resource.group_type_id + ) + self.assertEqual(GROUP_SNAPSHOT["id"], resource.id) + self.assertEqual(GROUP_SNAPSHOT["name"], resource.name) + self.assertEqual(GROUP_SNAPSHOT["project_id"], resource.project_id) + self.assertEqual(GROUP_SNAPSHOT["status"], resource.status) diff --git a/openstack/tests/unit/block_storage/v3/test_proxy.py b/openstack/tests/unit/block_storage/v3/test_proxy.py index 2ff15b8fc..46fa3b0ff 100644 --- a/openstack/tests/unit/block_storage/v3/test_proxy.py +++ b/openstack/tests/unit/block_storage/v3/test_proxy.py @@ -17,6 +17,7 @@ from openstack.block_storage.v3 import backup from openstack.block_storage.v3 import capabilities from openstack.block_storage.v3 import extension from openstack.block_storage.v3 import group +from openstack.block_storage.v3 import group_snapshot from openstack.block_storage.v3 import group_type from openstack.block_storage.v3 import limits from openstack.block_storage.v3 import quota_set @@ -181,6 +182,55 @@ class TestGroup(TestVolumeProxy): self._verify(self.proxy.reset_group_state, group.Group) +class TestGroupSnapshot(TestVolumeProxy): + def test_group_snapshot_get(self): + self.verify_get( + self.proxy.get_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshot_find(self): + self.verify_find( + self.proxy.find_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshots(self): + self.verify_list( + self.proxy.group_snapshots, + group_snapshot.GroupSnapshot, + expected_kwargs={}, + ) + + def test_group_snapshots__detailed(self): + self.verify_list( + self.proxy.group_snapshots, + group_snapshot.GroupSnapshot, + method_kwargs={'details': True, 'query': 1}, + expected_kwargs={ + 'query': 1, + 'base_path': '/group_snapshots/detail', + }, + ) + + def test_group_snapshot_create(self): + self.verify_create( + self.proxy.create_group_snapshot, group_snapshot.GroupSnapshot + ) + + def test_group_snapshot_delete(self): + self.verify_delete( + self.proxy.delete_group_snapshot, + group_snapshot.GroupSnapshot, + False, + ) + + def test_group_snapshot_delete_ignore(self): + self.verify_delete( + self.proxy.delete_group_snapshot, + group_snapshot.GroupSnapshot, + True, + ) + + class TestGroupType(TestVolumeProxy): def test_group_type_get(self): self.verify_get(self.proxy.get_group_type, group_type.GroupType) diff --git a/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml b/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml new file mode 100644 index 000000000..eb4e0f266 --- /dev/null +++ b/releasenotes/notes/add-block-storage-group-snapshots-954cc869227317c3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for group snapshots to the block storage service.