From c3e77fc61e8082f281ce3f37cef95b30b9180668 Mon Sep 17 00:00:00 2001 From: Reynaldo Bontje Date: Wed, 29 Mar 2023 14:36:54 -0500 Subject: [PATCH] Add resize/extend share actions. Adds the resize/extend actions from the Share actions API. Includes the resize_share method in the proxy. Change-Id: I9c852360b2e71f6e0a2cfd45c0a77690220379cd --- doc/source/user/guides/shared_file_system.rst | 13 +++ .../user/proxies/shared_file_system.rst | 2 +- examples/shared_file_system/shares.py | 33 +++++++ openstack/shared_file_system/v2/_proxy.py | 35 ++++++++ openstack/shared_file_system/v2/share.py | 54 ++++++++++- .../shared_file_system/test_share.py | 90 +++++++++++++++++++ .../unit/shared_file_system/v2/test_proxy.py | 24 +++++ .../unit/shared_file_system/v2/test_share.py | 62 ++++++++++++- ...-system-share-resize-ddd650c2e32fed34.yaml | 4 + 9 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 examples/shared_file_system/shares.py create mode 100644 releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml diff --git a/doc/source/user/guides/shared_file_system.rst b/doc/source/user/guides/shared_file_system.rst index 1aee6a36f..86c25a6b0 100644 --- a/doc/source/user/guides/shared_file_system.rst +++ b/doc/source/user/guides/shared_file_system.rst @@ -57,3 +57,16 @@ Force-deletes a share instance. .. literalinclude:: ../examples/shared_file_system/share_instances.py :pyobject: delete_share_instance + + +Resize Share +------------ + +Shared File System shares can be resized (extended or shrunk) to a given +size. For details on resizing shares, refer to the +`Manila docs `_. + +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: resize_share +.. literalinclude:: ../examples/shared_file_system/shares.py + :pyobject: resize_shares_without_shrink diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 6d4a34ac9..53de3bdb2 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -33,7 +33,7 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: shares, get_share, delete_share, update_share, create_share, - revert_share_to_snapshot + revert_share_to_snapshot, resize_share Shared File System Storage Pools diff --git a/examples/shared_file_system/shares.py b/examples/shared_file_system/shares.py new file mode 100644 index 000000000..9328b5048 --- /dev/null +++ b/examples/shared_file_system/shares.py @@ -0,0 +1,33 @@ +# 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. + + +def resize_share(conn, share_id, share_size): + # Be explicit about not wanting to use force if the share + # will be extended. + use_force = False + print('Resize the share to the given size:') + conn.share.resize_share(share_id, share_size, use_force) + + +def resize_shares_without_shrink(conn, min_size): + # Sometimes, extending shares without shrinking + # them (effectively setting a min size) is desirable. + + # Get list of shares from the connection. + shares = conn.share.shares() + + # Loop over the shares: + for share in shares: + # Extend shares smaller than min_size to min_size, + # but don't shrink shares larger than min_size. + conn.share.resize_share(share.id, min_size, no_shrink=True) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index eae2b0994..a4f6492c0 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -158,6 +158,41 @@ class Proxy(proxy.Proxy): res = self._get(_share.Share, share_id) res.revert_to_snapshot(self, snapshot_id) + def resize_share( + self, + share_id, + new_size, + no_shrink=False, + no_extend=False, + force=False + ): + """Resizes a share, extending/shrinking the share as needed. + + :param share_id: The ID of the share to resize + :param new_size: The new size of the share in GiBs. If new_size is + the same as the current size, then nothing is done. + :param bool no_shrink: If set to True, the given share is not shrunk, + even if shrinking the share is required to get the share to the + given size. This could be useful for extending shares to a minimum + size, while not shrinking shares to the given size. This defaults + to False. + :param bool no_extend: If set to True, the given share is not + extended, even if extending the share is required to get the share + to the given size. This could be useful for shrinking shares to a + maximum size, while not extending smaller shares to that maximum + size. This defaults to False. + :param bool force: Whether or not force should be used, + in the case where the share should be extended. + :returns: ``None`` + """ + + res = self._get(_share.Share, share_id) + + if new_size > res.size and no_extend is not True: + res.extend_share(self, new_size, force) + elif new_size < res.size and no_shrink is not True: + res.shrink_share(self, new_size) + def wait_for_status(self, res, status='active', failures=None, interval=2, wait=120): """Wait for a resource to be in a particular status. diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py index 75cc636e2..526e60475 100644 --- a/openstack/shared_file_system/v2/share.py +++ b/openstack/shared_file_system/v2/share.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource from openstack import utils @@ -102,12 +103,59 @@ class Share(resource.Resource): #: Display description for updating description display_description = resource.Body("display_description", type=str) - def _action(self, session, body): + def _action(self, session, body, action='patch', microversion=None): + """Perform share instance actions given the message body""" url = utils.urljoin(self.base_path, self.id, 'action') headers = {'Accept': ''} - session.post( - url, json=body, headers=headers) + + if microversion is None: + microversion = \ + self._get_microversion(session, action=action) + + response = session.post( + url, + json=body, + headers=headers, + microversion=microversion) + + exceptions.raise_from_response(response) + return response + + def extend_share(self, session, new_size, force=False): + """Extend the share size. + + :param float new_size: The new size of the share + in GiB. + :param bool force: Whether or not to use force, bypassing + the scheduler. Requires admin privileges. Defaults to False. + :returns: The result of the action. + :rtype: ``None`` + """ + + extend_body = {"new_size": new_size} + + if force is True: + extend_body['force'] = True + + body = {"extend": extend_body} + self._action(session, body) + + def shrink_share(self, session, new_size): + """Shrink the share size. + + :param float new_size: The new size of the share + in GiB. + :returns: ``None`` + """ + + body = {"shrink": {'new_size': new_size}} + self._action(session, body) def revert_to_snapshot(self, session, snapshot_id): + """Revert the share to the given snapshot. + + :param str snapshot_id: The id of the snapshot to revert to. + :returns: ``None`` + """ body = {"revert": {"snapshot_id": snapshot_id}} self._action(session, body) diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py index a134140cd..4c9f309f3 100644 --- a/openstack/tests/functional/shared_file_system/test_share.py +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -24,6 +24,7 @@ class ShareTest(base.BaseSharedFileSystemTest): name=self.SHARE_NAME, size=2, share_type="dhss_false", share_protocol='NFS', description=None) self.SHARE_ID = my_share.id + self.SHARE_SIZE = my_share.size my_share_snapshot = self.create_share_snapshot( share_id=self.SHARE_ID ) @@ -60,3 +61,92 @@ class ShareTest(base.BaseSharedFileSystemTest): interval=5, wait=self._wait_for_timeout) self.assertIsNotNone(get_reverted_share.id) + + def test_resize_share_larger(self): + larger_size = 3 + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(larger_size, get_resized_share.size) + + def test_resize_share_smaller(self): + # Resize to 3 GiB + smaller_size = 1 + + self.user_cloud.share.resize_share( + self.SHARE_ID, smaller_size) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(smaller_size, get_resized_share.size) + + def test_resize_share_larger_no_extend(self): + larger_size = 3 + + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size, no_extend=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + + # Assert that no change was made. + self.assertEqual(self.SHARE_SIZE, get_resized_share.size) + + def test_resize_share_smaller_no_shrink(self): + smaller_size = 1 + + self.user_cloud.share.resize_share( + self.SHARE_ID, smaller_size, no_shrink=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + + # Assert that no change was made. + self.assertEqual(self.SHARE_SIZE, get_resized_share.size) + + def test_resize_share_with_force(self): + # Resize to 3 GiB + larger_size = 3 + self.user_cloud.share.resize_share( + self.SHARE_ID, larger_size, force=True) + + get_resized_share = self.user_cloud.share.get_share( + self.SHARE_ID) + + self.user_cloud.share.wait_for_status( + get_resized_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertEqual(larger_size, get_resized_share.size) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py index 364fe5a2e..9c2a00be3 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -62,6 +62,30 @@ class TestSharedFileSystemShare(TestSharedFileSystemProxy): def test_share_update(self): self.verify_update(self.proxy.update_share, share.Share) + def test_share_resize_extend(self): + mock_share = share.Share(size=10, id='fakeId') + self.proxy._get = mock.Mock(return_value=mock_share) + + self._verify( + "openstack.shared_file_system.v2.share." + + "Share.extend_share", + self.proxy.resize_share, + method_args=['fakeId', 20], + expected_args=[self.proxy, 20, False], + ) + + def test_share_resize_shrink(self): + mock_share = share.Share(size=30, id='fakeId') + self.proxy._get = mock.Mock(return_value=mock_share) + + self._verify( + "openstack.shared_file_system.v2.share." + + "Share.shrink_share", + self.proxy.resize_share, + method_args=['fakeId', 20], + expected_args=[self.proxy, 20], + ) + def test_share_instances(self): self.verify_list(self.proxy.share_instances, share_instance.ShareInstance) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py index 44e8fff3c..e19d5b51e 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_share.py +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -10,13 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from keystoneauth1 import adapter + from openstack.shared_file_system.v2 import share from openstack.tests.unit import base + IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' EXAMPLE = { "id": IDENTIFIER, - "size": 1, + "size": 2, "availability_zone": "manila-zone-1", "created_at": "2021-02-11T17:38:00.000000", "status": "available", @@ -109,3 +114,58 @@ class TestShares(base.TestCase): self.assertEqual(EXAMPLE['share_server_id'], shares_resource.share_server_id) self.assertEqual(EXAMPLE['host'], shares_resource.host) + + +class TestShareActions(TestShares): + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.status_code = 202 + self.resp.json = mock.Mock(return_value=self.resp.body) + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = '3.0' + self.sess.post = mock.Mock(return_value=self.resp) + self.sess._get_connection = mock.Mock(return_value=self.cloud) + + def test_shrink_share(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.shrink_share(self.sess, new_size=1)) + + url = f'shares/{IDENTIFIER}/action' + body = {"shrink": {"new_size": 1}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) + + def test_extend_share(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.extend_share(self.sess, new_size=3)) + + url = f'shares/{IDENTIFIER}/action' + body = {"extend": {"new_size": 3}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) + + def test_revert_to_snapshot(self): + sot = share.Share(**EXAMPLE) + microversion = sot._get_microversion(self.sess, action='patch') + + self.assertIsNone(sot.revert_to_snapshot(self.sess, "fake_id")) + + url = f'shares/{IDENTIFIER}/action' + body = {"revert": {"snapshot_id": "fake_id"}} + headers = {'Accept': ''} + + self.sess.post.assert_called_with( + url, json=body, headers=headers, + microversion=microversion) diff --git a/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml b/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml new file mode 100644 index 000000000..b1adea626 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-share-resize-ddd650c2e32fed34.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for shrink/extend share actions.