From 4e0d6938164fed823e09f54f191fb537c90fdaa1 Mon Sep 17 00:00:00 2001 From: silvacarloss Date: Tue, 25 Jul 2023 12:31:06 -0300 Subject: [PATCH] Resource locks and access rules restrictions Implement resource locks and access rules restrictions feature in the openstacksdk. Depends-On: Ib9f65a4523222f1224d51534c5061f90501b59d3 Change-Id: I45f9b06b1b41756d34f39604c82e28fd4eb102de --- .../user/proxies/shared_file_system.rst | 17 ++- .../resources/shared_file_system/index.rst | 1 + .../shared_file_system/v2/resource_locks.rst | 13 ++ openstack/proxy.py | 7 + openstack/shared_file_system/v2/_proxy.py | 141 +++++++++++++++++- .../shared_file_system/v2/resource_locks.py | 73 +++++++++ .../v2/share_access_rule.py | 17 ++- .../functional/shared_file_system/base.py | 14 +- .../shared_file_system/test_resource_lock.py | 96 ++++++++++++ .../test_share_access_rule.py | 13 ++ .../unit/shared_file_system/v2/test_proxy.py | 46 +++++- ...system-locks-support-4859ca93f93a1056.yaml | 8 + 12 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 doc/source/user/resources/shared_file_system/v2/resource_locks.rst create mode 100644 openstack/shared_file_system/v2/resource_locks.py create mode 100644 openstack/tests/functional/shared_file_system/test_resource_lock.py create mode 100644 releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index facbe03b2..2a3a756a0 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -133,7 +133,9 @@ Shared File System Share Access Rules ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Create, View, and Delete access rules for shares from the -Shared File Systems service. +Shared File Systems service. Access rules can also have their deletion +and visibility restricted during creation. A lock reason can also be +specified. The deletion restriction can be removed during the access removal. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: @@ -177,3 +179,16 @@ Shared File Systems service. :members: get_share_metadata, get_share_metadata_item, create_share_metadata, update_share_metadata, delete_share_metadata + + +Shared File System Resource Locks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create, list, update and delete locks for resources. When a resource is +locked, it means that it can be deleted only by services, admins or +the user that created the lock. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: resource_locks, get_resource_lock, update_resource_lock, + delete_resource_lock, create_resource_lock diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index e2bd0488a..1b45f4f17 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -17,3 +17,4 @@ Shared File System service resources v2/share_group v2/share_access_rule v2/share_group_snapshot + v2/resource_locks diff --git a/doc/source/user/resources/shared_file_system/v2/resource_locks.rst b/doc/source/user/resources/shared_file_system/v2/resource_locks.rst new file mode 100644 index 000000000..6040bfa5a --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/resource_locks.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.resource_locks +============================================== + +.. automodule:: openstack.shared_file_system.v2.resource_locks + +The Resource Locks Class +------------------------ + +The ``ResourceLock`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.resource_locks.ResourceLock + :members: diff --git a/openstack/proxy.py b/openstack/proxy.py index 90e06b877..c058964a9 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -632,6 +632,13 @@ class Proxy(adapter.Adapter): :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` """ + # Check for attributes whose names conflict with the parameters + # specified in the method. + conflicting_attrs = attrs.get('__conflicting_attrs', {}) + if conflicting_attrs: + for k, v in conflicting_attrs.items(): + attrs[k] = v + attrs.pop('__conflicting_attrs') conn = self._get_connection() res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 76eae7557..56c5dfb74 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -16,6 +16,7 @@ from openstack.shared_file_system.v2 import ( availability_zone as _availability_zone, ) from openstack.shared_file_system.v2 import limit as _limit +from openstack.shared_file_system.v2 import resource_locks as _resource_locks from openstack.shared_file_system.v2 import share as _share from openstack.shared_file_system.v2 import share_group as _share_group from openstack.shared_file_system.v2 import ( @@ -56,6 +57,7 @@ class Proxy(proxy.Proxy): "share_access_rule": _share_access_rule.ShareAccessRule, "share_group": _share_group.ShareGroup, "share_group_snapshot": _share_group_snapshot.ShareGroupSnapshot, + "resource_locks": _resource_locks.ResourceLock, } def availability_zones(self): @@ -354,7 +356,13 @@ class Proxy(proxy.Proxy): ) def wait_for_status( - self, res, status='active', failures=None, interval=2, wait=120 + self, + res, + status='active', + failures=None, + interval=2, + wait=120, + status_attr_name='status', ): """Wait for a resource to be in a particular status. :param res: The resource to wait on to reach the specified status. @@ -367,6 +375,8 @@ class Proxy(proxy.Proxy): checks. Default to 2. :param wait: Maximum number of seconds to wait before the change. Default to 120. + :param status_attr_name: name of the attribute to reach the desired + status. :returns: The resource is returned on success. :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition to the desired status failed to occur in specified seconds. @@ -377,7 +387,13 @@ class Proxy(proxy.Proxy): """ failures = [] if failures is None else failures return resource.wait_for_status( - self, res, status, failures, interval, wait + self, + res, + status, + failures, + interval, + wait, + attribute=status_attr_name, ) def storage_pools(self, details=True, **query): @@ -846,17 +862,25 @@ class Proxy(proxy.Proxy): _share_access_rule.ShareAccessRule, base_path=base_path, **attrs ) - def delete_access_rule(self, access_id, share_id, ignore_missing=True): + def delete_access_rule( + self, access_id, share_id, ignore_missing=True, *, unrestrict=False + ): """Deletes an access rule :param access_id: The id of the access rule to get :param share_id: The ID of the share + :param unrestrict: If Manila must attempt removing locks while deleting :rtype: ``requests.models.Response`` HTTP response from internal requests client """ res = self._get_resource(_share_access_rule.ShareAccessRule, access_id) - res.delete(self, share_id, ignore_missing=ignore_missing) + return res.delete( + self, + share_id, + ignore_missing=ignore_missing, + unrestrict=unrestrict, + ) def share_group_snapshots(self, details=True, **query): """Lists all share group snapshots. @@ -1065,3 +1089,112 @@ class Proxy(proxy.Proxy): raise exceptions.SDKException( "Some keys failed to be deleted %s" % keys_failed_to_delete ) + + def resource_locks(self, **query): + """Lists all resource locks. + + :param kwargs query: Optional query parameters to be sent to limit + the resource locks being returned. Available parameters include: + + * project_id: The project ID of the user that the lock is + created for. + * user_id: The ID of a user to filter resource locks by. + * all_projects: list locks from all projects (Admin Only) + * resource_id: The ID of the resource that the locks pertain to + filter resource locks by. + * resource_action: The action prevented by the filtered resource + locks. + * resource_type: The type of the resource that the locks pertain + to filter resource locks by. + * lock_context: The lock creator’s context to filter locks by. + * lock_reason: The lock reason that can be used to filter resource + locks. (Inexact search is also available with lock_reason~) + * created_since: Search for the list of resources that were created + after the specified date. The date is in ‘yyyy-mm-dd’ format. + * created_before: Search for the list of resources that were + created prior to the specified date. The date is in + ‘yyyy-mm-dd’ format. + * limit: The maximum number of resource locks to return. + * offset: The offset to define start point of resource lock + listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares + * with_count: Whether to show count in API response or not, + default is False. This query parameter is useful with + pagination. + + :returns: A generator of manila resource locks + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._list(_resource_locks.ResourceLock, **query) + + def get_resource_lock(self, resource_lock): + """Show details of a resource lock. + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :returns: Details of the identified resource lock. + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._get(_resource_locks.ResourceLock, resource_lock) + + def update_resource_lock(self, resource_lock, **attrs): + """Updates details of a single resource lock. + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :param dict attrs: The attributes to update on the resource lock + :returns: the updated resource lock + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + return self._update( + _resource_locks.ResourceLock, resource_lock, **attrs + ) + + def delete_resource_lock(self, resource_lock, ignore_missing=True): + """Deletes a single resource lock + + :param resource_lock: The ID of a resource lock or a + :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` instance. + :returns: Result of the ``delete`` + :rtype: ``None`` + """ + return self._delete( + _resource_locks.ResourceLock, + resource_lock, + ignore_missing=ignore_missing, + ) + + def create_resource_lock(self, **attrs): + """Locks a resource. + + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock`, comprised of the properties + on the ResourceLock class. Available parameters include: + + * ``resource_id``: ID of the resource to be locked. + * ``resource_type``: type of the resource (share, access_rule). + * ``resource_action``: action to be locked (delete, show). + * ``lock_reason``: reason why you're locking the resource + (Optional). + :returns: Details of the lock + :rtype: :class:`~openstack.shared_file_system.v2. + resource_locks.ResourceLock` + """ + + if attrs.get('resource_type'): + # The _create method has a parameter named resource_type, which + # refers to the type of resource to be created, so we need to avoid + # a conflict of parameters we are sending to the method. + attrs['__conflicting_attrs'] = { + 'resource_type': attrs.get('resource_type') + } + attrs.pop('resource_type') + return self._create(_resource_locks.ResourceLock, **attrs) diff --git a/openstack/shared_file_system/v2/resource_locks.py b/openstack/shared_file_system/v2/resource_locks.py new file mode 100644 index 000000000..2d5731e13 --- /dev/null +++ b/openstack/shared_file_system/v2/resource_locks.py @@ -0,0 +1,73 @@ +# 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 resource + + +class ResourceLock(resource.Resource): + resource_key = "resource_lock" + resources_key = "resource_locks" + base_path = "/resource-locks" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + _query_mapping = resource.QueryParameters( + "project_id", + "created_since", + "created_before", + "limit", + "offset", + "id", + "resource_id", + "resource_type", + "resource_action", + "user_id", + "lock_context", + "lock_reason", + "lock_reason~", + "sort_key", + "sort_dir", + "with_count", + "all_projects", + ) + # The resource was introduced in this microversion, so it is the minimum + # version to use it. Openstacksdk currently doesn't allow to set + # minimum microversions. + _max_microversion = '2.81' + + #: Properties + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The date and time stamp when the resource was last modified within the + #: service’s database. + updated_at = resource.Body("updated_at", type=str) + #: The ID of the user that owns the lock + user_id = resource.Body("user_id", type=str) + #: The ID of the project that owns the lock. + project_id = resource.Body("project_id", type=str) + #: The type of the resource that is locked, i.e.: share, access rule. + resource_type = resource.Body("resource_type", type=str) + #: The UUID of the resource that is locked. + resource_id = resource.Body("resource_id", type=str) + #: What action is currently locked, i.e.: deletion, visibility of fields. + resource_action = resource.Body("resource_action", type=str) + #: The reason specified while the lock was being placed. + lock_reason = resource.Body("lock_reason", type=str) + #: The context that placed the lock (user, admin or service). + lock_context = resource.Body("lock_context", type=str) diff --git a/openstack/shared_file_system/v2/share_access_rule.py b/openstack/shared_file_system/v2/share_access_rule.py index 519c679b9..66be84223 100644 --- a/openstack/shared_file_system/v2/share_access_rule.py +++ b/openstack/shared_file_system/v2/share_access_rule.py @@ -16,7 +16,7 @@ from openstack import utils class ShareAccessRule(resource.Resource): - resource_key = "share_access_rule" + resource_key = "access" resources_key = "access_list" base_path = "/share-access-rules" @@ -30,7 +30,8 @@ class ShareAccessRule(resource.Resource): _query_mapping = resource.QueryParameters("share_id") - _max_microversion = '2.45' + # Restricted access rules became available in 2.82 + _max_microversion = '2.82' #: Properties #: The access credential of the entity granted share access. @@ -56,6 +57,12 @@ class ShareAccessRule(resource.Resource): #: The date and time stamp when the resource was last updated within #: the service’s database. updated_at = resource.Body("updated_at", type=str) + #: Whether the visibility of some sensitive fields is restricted or not + lock_visibility = resource.Body("lock_visibility", type=bool) + #: Whether the deletion of the access rule should be restricted or not + lock_deletion = resource.Body("lock_deletion", type=bool) + #: Reason for placing the loc + lock_reason = resource.Body("lock_reason", type=bool) def _action(self, session, body, url, action='patch', microversion=None): headers = {'Accept': ''} @@ -75,8 +82,12 @@ class ShareAccessRule(resource.Resource): **kwargs ) - def delete(self, session, share_id, ignore_missing=True): + def delete( + self, session, share_id, ignore_missing=True, *, unrestrict=False + ): body = {"deny_access": {"access_id": self.id}} + if unrestrict: + body['deny_access']['unrestrict'] = True url = utils.urljoin("/shares", share_id, "action") response = self._action(session, body, url) try: diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index f12a48c6b..4a1d5a295 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -22,8 +22,8 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): self.require_service( 'shared-file-system', min_microversion=self.min_microversion ) - self._set_operator_cloud(shared_file_system_api_version='2.78') - self._set_user_cloud(shared_file_system_api_version='2.78') + self._set_operator_cloud(shared_file_system_api_version='2.82') + self._set_user_cloud(shared_file_system_api_version='2.82') def create_share(self, **kwargs): share = self.user_cloud.share.create_share(**kwargs) @@ -75,3 +75,13 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): ) self.assertIsNotNone(share_group.id) return share_group + + def create_resource_lock(self, **kwargs): + resource_lock = self.user_cloud.share.create_resource_lock(**kwargs) + self.addCleanup( + self.user_cloud.share.delete_resource_lock, + resource_lock.id, + ignore_missing=True, + ) + self.assertIsNotNone(resource_lock.id) + return resource_lock diff --git a/openstack/tests/functional/shared_file_system/test_resource_lock.py b/openstack/tests/functional/shared_file_system/test_resource_lock.py new file mode 100644 index 000000000..e4f2b0351 --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_resource_lock.py @@ -0,0 +1,96 @@ +# 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.shared_file_system.v2 import resource_locks as _resource_locks +from openstack.tests.functional.shared_file_system import base + + +class ResourceLocksTest(base.BaseSharedFileSystemTest): + def setUp(self): + super(ResourceLocksTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + share = self.user_cloud.shared_file_system.create_share( + name=self.SHARE_NAME, + size=2, + share_type="dhss_false", + share_protocol='NFS', + description=None, + ) + self.SHARE_ID = share.id + self.user_cloud.shared_file_system.wait_for_status( + share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + ) + access_rule = self.user_cloud.share.create_access_rule( + self.SHARE_ID, + access_level="rw", + access_type="ip", + access_to="0.0.0.0/0", + ) + self.user_cloud.shared_file_system.wait_for_status( + access_rule, + status='active', + failures=['error'], + interval=5, + wait=self._wait_for_timeout, + status_attr_name='state', + ) + self.assertIsNotNone(share) + self.assertIsNotNone(share.id) + self.ACCESS_ID = access_rule.id + share_lock = self.create_resource_lock( + resource_action='delete', + resource_type='share', + resource_id=self.SHARE_ID, + lock_reason='openstacksdk testing', + ) + access_lock = self.create_resource_lock( + resource_action='show', + resource_type='access_rule', + resource_id=self.ACCESS_ID, + lock_reason='openstacksdk testing', + ) + self.SHARE_LOCK_ID = share_lock.id + self.ACCESS_LOCK_ID = access_lock.id + + def test_get(self): + share_lock = self.user_cloud.shared_file_system.get_resource_lock( + self.SHARE_LOCK_ID + ) + access_lock = self.user_cloud.shared_file_system.get_resource_lock( + self.ACCESS_LOCK_ID + ) + assert isinstance(share_lock, _resource_locks.ResourceLock) + assert isinstance(access_lock, _resource_locks.ResourceLock) + self.assertEqual(self.SHARE_LOCK_ID, share_lock.id) + self.assertEqual(self.ACCESS_LOCK_ID, access_lock.id) + self.assertEqual('show', access_lock.resource_action) + + def test_list(self): + resource_locks = self.user_cloud.share.resource_locks() + self.assertGreater(len(list(resource_locks)), 0) + lock_attrs = ( + 'id', + 'lock_reason', + 'resource_type', + 'resource_action', + 'lock_context', + 'created_at', + 'updated_at', + ) + for lock in resource_locks: + for attribute in lock_attrs: + self.assertTrue(hasattr(lock, attribute)) diff --git a/openstack/tests/functional/shared_file_system/test_share_access_rule.py b/openstack/tests/functional/shared_file_system/test_share_access_rule.py index d8dc4d85b..7fc7817a5 100644 --- a/openstack/tests/functional/shared_file_system/test_share_access_rule.py +++ b/openstack/tests/functional/shared_file_system/test_share_access_rule.py @@ -75,3 +75,16 @@ class ShareAccessRuleTest(base.BaseSharedFileSystemTest): 'metadata', ): self.assertTrue(hasattr(rule, attribute)) + + def test_create_delete_access_rule_with_locks(self): + access_rule = self.user_cloud.share.create_access_rule( + self.SHARE_ID, + access_level="rw", + access_type="ip", + access_to="203.0.113.10", + lock_deletion=True, + lock_visibility=True, + ) + self.user_cloud.share.delete_access_rule( + access_rule['id'], self.SHARE_ID, unrestrict=True + ) 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 921318f64..a562a450b 100644 --- a/openstack/tests/unit/shared_file_system/v2/test_proxy.py +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -14,6 +14,7 @@ from unittest import mock from openstack.shared_file_system.v2 import _proxy from openstack.shared_file_system.v2 import limit +from openstack.shared_file_system.v2 import resource_locks from openstack.shared_file_system.v2 import share from openstack.shared_file_system.v2 import share_access_rule from openstack.shared_file_system.v2 import share_group @@ -130,7 +131,7 @@ class TestSharedFileSystemShare(TestSharedFileSystemProxy): self.proxy.wait_for_status(mock_resource, 'ACTIVE') mock_wait.assert_called_once_with( - self.proxy, mock_resource, 'ACTIVE', [], 2, 120 + self.proxy, mock_resource, 'ACTIVE', [], 2, 120, attribute='status' ) @@ -473,8 +474,49 @@ class TestAccessRuleProxy(test_proxy_base.TestProxyBase): "openstack.shared_file_system.v2.share_access_rule." + "ShareAccessRule.delete", self.proxy.delete_access_rule, - method_args=['access_id', 'share_id', 'ignore_missing'], + method_args=[ + 'access_id', + 'share_id', + 'ignore_missing', + ], expected_args=[self.proxy, 'share_id'], + expected_kwargs={'unrestrict': False}, + ) + + +class TestResourceLocksProxy(test_proxy_base.TestProxyBase): + def setUp(self): + super(TestResourceLocksProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_list_resource_locks(self): + self.verify_list( + self.proxy.resource_locks, resource_locks.ResourceLock + ) + + def test_resource_lock_get(self): + self.verify_get( + self.proxy.get_resource_lock, resource_locks.ResourceLock + ) + + def test_resource_lock_delete(self): + self.verify_delete( + self.proxy.delete_resource_lock, resource_locks.ResourceLock, False + ) + + def test_resource_lock_delete_ignore(self): + self.verify_delete( + self.proxy.delete_resource_lock, resource_locks.ResourceLock, True + ) + + def test_resource_lock_create(self): + self.verify_create( + self.proxy.create_resource_lock, resource_locks.ResourceLock + ) + + def test_resource_lock_update(self): + self.verify_update( + self.proxy.update_resource_lock, resource_locks.ResourceLock ) diff --git a/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml b/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml new file mode 100644 index 000000000..c6dca5601 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-locks-support-4859ca93f93a1056.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support to manipulate resource locks from the shared file system + service. + - | + Added support to restrict the visibility and deletion of the shared file + system share access rules.