diff --git a/doc/source/user/proxies/shared_file_system.rst b/doc/source/user/proxies/shared_file_system.rst index 3e8ae7388..759d19a39 100644 --- a/doc/source/user/proxies/shared_file_system.rst +++ b/doc/source/user/proxies/shared_file_system.rst @@ -22,3 +22,14 @@ service. .. autoclass:: openstack.shared_file_system.v2._proxy.Proxy :noindex: :members: availability_zones + + +Shared File System Shares +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Interact with Shares supported by the Shared File Systems +service. + +.. autoclass:: openstack.shared_file_system.v2._proxy.Proxy + :noindex: + :members: shares, get_share, delete_share, update_share, create_share diff --git a/doc/source/user/resources/shared_file_system/index.rst b/doc/source/user/resources/shared_file_system/index.rst index 1be3611bd..4d31d2ed7 100644 --- a/doc/source/user/resources/shared_file_system/index.rst +++ b/doc/source/user/resources/shared_file_system/index.rst @@ -5,3 +5,4 @@ Shared File System service resources :maxdepth: 1 v2/availability_zone + v2/share diff --git a/doc/source/user/resources/shared_file_system/v2/share.rst b/doc/source/user/resources/shared_file_system/v2/share.rst new file mode 100644 index 000000000..bac5e9602 --- /dev/null +++ b/doc/source/user/resources/shared_file_system/v2/share.rst @@ -0,0 +1,13 @@ +openstack.shared_file_system.v2.share +===================================== + +.. automodule:: openstack.shared_file_system.v2.share + +The Share Class +--------------- + +The ``Share`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.shared_file_system.v2.share.Share + :members: diff --git a/openstack/shared_file_system/v2/_proxy.py b/openstack/shared_file_system/v2/_proxy.py index 0f6a688c4..2f27d0d80 100644 --- a/openstack/shared_file_system/v2/_proxy.py +++ b/openstack/shared_file_system/v2/_proxy.py @@ -11,7 +11,10 @@ # under the License. from openstack import proxy -from openstack.shared_file_system.v2 import availability_zone +from openstack import resource +from openstack.shared_file_system.v2 import ( + availability_zone as _availability_zone) +from openstack.shared_file_system.v2 import share as _share class Proxy(proxy.Proxy): @@ -23,4 +26,123 @@ class Proxy(proxy.Proxy): :rtype: :class:`~openstack.shared_file_system.v2. \availability_zone.AvailabilityZone` """ - return self._list(availability_zone.AvailabilityZone) + return self._list(_availability_zone.AvailabilityZone) + + def shares(self, details=True, **query): + """Lists all shares with details + + :param kwargs query: Optional query parameters to be sent to limit + the shares being returned. Available parameters include: + + * status: Filters by a share status + * share_server_id: The UUID of the share server. + * metadata: One or more metadata key and value pairs as a url + encoded dictionary of strings. + * extra_specs: The extra specifications as a set of one or more + key-value pairs. + * share_type_id: The UUID of a share type to query resources by. + * name: The user defined name of the resource to filter resources + by. + * snapshot_id: The UUID of the share’s base snapshot to filter + the request based on. + * host: The host name of the resource to query with. + * share_network_id: The UUID of the share network to filter + resources by. + * project_id: The ID of the project that owns the resource. + * is_public: A boolean query parameter that, when set to true, + allows retrieving public resources that belong to + all projects. + * share_group_id: The UUID of a share group to filter resource. + * export_location_id: The export location UUID that can be used + to filter shares or share instances. + * export_location_path: The export location path that can be used + to filter shares or share instances. + * name~: The name pattern that can be used to filter shares, share + snapshots, share networks or share groups. + * description~: The description pattern that can be used to filter + shares, share snapshots, share networks or share groups. + * with_count: Whether to show count in API response or not, + default is False. + * limit: The maximum number of shares to return. + * offset: The offset to define start point of share or share group + listing. + * sort_key: The key to sort a list of shares. + * sort_dir: The direction to sort a list of shares. A valid value + is asc, or desc. + + :returns: Details of shares resources + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + base_path = '/shares/detail' if details else None + return self._list(_share.Share, base_path=base_path, **query) + + def get_share(self, share_id): + """Lists details of a single share + + :param share: The ID of the share to get + :returns: Details of the identified share + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._get(_share.Share, share_id) + + def delete_share(self, share, ignore_missing=True): + """Deletes a single share + + :param share: The ID of the share to delete + :returns: Result of the ``delete`` + :rtype: ``None`` + """ + self._delete(_share.Share, share, + ignore_missing=ignore_missing) + + def update_share(self, share_id, **attrs): + """Updates details of a single share. + + :param share: The ID of the share to update + :pram dict attrs: The attributes to update on the share + :returns: the updated share + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._update(_share.Share, share_id, **attrs) + + def create_share(self, **attrs): + """Creates a share from attributes + + :returns: Details of the new share + :param dict attrs: Attributes which will be used to create + a :class:`~openstack.shared_file_system.v2. + shares.Shares`,comprised of the properties + on the Shares class. 'size' and 'share' + are required to create a share. + :rtype: :class:`~openstack.shared_file_system.v2. + share.Share` + """ + return self._create(_share.Share, **attrs) + + def wait_for_status(self, res, status='active', failures=None, + interval=2, wait=120): + """Wait for a resource to be in a particular status. + :param res: The resource to wait on to reach the specified status. + The resource must have a ``status`` attribute. + :type resource: A :class:`~openstack.resource.Resource` object. + :param status: Desired status. + :param failures: Statuses that would be interpreted as failures. + :type failures: :py:class:`list` + :param interval: Number of seconds to wait before to consecutive + checks. Default to 2. + :param wait: Maximum number of seconds to wait before the change. + Default to 120. + :returns: The resource is returned on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition + to the desired status failed to occur in specified seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource + has transited to one of the failure statuses. + :raises: :class:`~AttributeError` if the resource does not have a + ``status`` attribute. + """ + failures = [] if failures is None else failures + return resource.wait_for_status( + self, res, status, failures, interval, wait) diff --git a/openstack/shared_file_system/v2/share.py b/openstack/shared_file_system/v2/share.py new file mode 100644 index 000000000..d2bd95d48 --- /dev/null +++ b/openstack/shared_file_system/v2/share.py @@ -0,0 +1,102 @@ +# 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 Share(resource.Resource): + resource_key = "share" + resources_key = "shares" + base_path = "/shares" + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_head = False + + #: Properties + #: The share instance access rules status. A valid value is active, + #: error, or syncing. + access_rules_status = resource.Body("access_rules_status", type=str) + #: The availability zone. + availability_zone = resource.Body("availability_zone", type=str) + #: The date and time stamp when the resource was created within the + #: service’s database. + created_at = resource.Body("created_at", type=str) + #: The user defined description of the resource. + description = resource.Body("description", type=str) + #: The share host name. + host = resource.Body("host", type=str) + #: The level of visibility for the share. + is_public = resource.Body("is_public", type=bool) + #: Whether or not this share supports snapshots that can be + #: cloned into new shares. + is_creating_new_share_from_snapshot_supported = resource.Body( + "create_share_from_snapshot_support", type=bool) + #: Whether the share's snapshots can be mounted directly and access + #: controlled independently or not. + is_mounting_snapshot_supported = resource.Body( + "mount_snapshot_support", type=bool) + #: Whether the share can be reverted to its latest snapshot or not. + is_reverting_to_snapshot_supported = resource.Body( + "revert_to_snapshot_support", type=bool) + #: An extra specification that filters back ends by whether the share + #: supports snapshots or not. + is_snapshot_supported = resource.Body( + "snapshot_support", type=bool) + #: Indicates whether the share has replicas or not. + is_replicated = resource.Body("has_replicas", type=bool) + #: One or more metadata key and value pairs as a dictionary of strings. + metadata = resource.Body("metadata", type=dict) + #: The progress of the share creation. + progress = resource.Body("progress", type=str) + #: The ID of the project that owns the resource. + project_id = resource.Body("project_id", type=str) + #: The share replication type. Valid values are none, readable, + #: writable and dr. + replication_type = resource.Body("replication_type", type=str) + #: The UUID of the share group that this shares belongs to. + share_group_id = resource.Body("share_group_id", type=str) + #: The share network ID. + share_network_id = resource.Body("share_network_id", type=str) + #: The Shared File Systems protocol. A valid value is NFS, + #: CIFS, GlusterFS, HDFS, CephFS, MAPRFS + share_protocol = resource.Body("share_proto", type=str) + #: The UUID of the share server. + share_server_id = resource.Body("share_server_id", type=str) + #: The UUID of the share type. In minor versions, this parameter is a + #: share type name, as a string. + share_type = resource.Body("share_type", type=str) + #: Name of the share type. + share_type_name = resource.Body("share_type_name", type=str) + #: The share size, in GiBs. + size = resource.Body("size", type=int) + #: The UUID of the snapshot that was used to create the + #: share. + snapshot_id = resource.Body("snapshot_id", type=str) + #: The ID of the group snapshot instance that was used to create + #: this share. + source_share_group_snapshot_member_id = resource.Body( + "source_share_group_snapshot_member_id", type=str) + #: The share status + status = resource.Body("status", type=str) + #: For the share migration, the migration task state. + task_state = resource.Body("task_state", type=str) + #: ID of the user that the share was created by. + user_id = resource.Body("user_id", type=str) + #: Display name for updating name + display_name = resource.Body("display_name", type=str) + #: Display description for updating description + display_description = resource.Body("display_description", type=str) diff --git a/openstack/tests/functional/shared_file_system/base.py b/openstack/tests/functional/shared_file_system/base.py index 3714dcebe..864116ee1 100644 --- a/openstack/tests/functional/shared_file_system/base.py +++ b/openstack/tests/functional/shared_file_system/base.py @@ -21,3 +21,10 @@ class BaseSharedFileSystemTest(base.BaseFunctionalTest): super(BaseSharedFileSystemTest, self).setUp() self.require_service('shared-file-system', min_microversion=self.min_microversion) + + def create_share(self, **kwargs): + share = self.conn.share.create_share(**kwargs) + self.addCleanup(self.conn.share.delete_share, share.id, + ignore_missing=True) + self.assertIsNotNone(share.id) + return share diff --git a/openstack/tests/functional/shared_file_system/test_share.py b/openstack/tests/functional/shared_file_system/test_share.py new file mode 100644 index 000000000..16bae406c --- /dev/null +++ b/openstack/tests/functional/shared_file_system/test_share.py @@ -0,0 +1,60 @@ +# 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 share as _share +from openstack.tests.functional.shared_file_system import base + + +class ShareTest(base.BaseSharedFileSystemTest): + + def setUp(self): + super(ShareTest, self).setUp() + + self.SHARE_NAME = self.getUniqueString() + my_share = self.conn.shared_file_system.create_share( + name=self.SHARE_NAME, size=2, share_type="dhss_false", + share_protocol='NFS', description=None) + self.conn.shared_file_system.wait_for_status( + my_share, + status='available', + failures=['error'], + interval=5, + wait=self._wait_for_timeout) + self.assertIsNotNone(my_share) + self.assertIsNotNone(my_share.id) + self.SHARE_ID = my_share.id + + def tearDown(self): + sot = self.conn.shared_file_system.delete_share( + self.SHARE_ID, + ignore_missing=True) + self.assertIsNone(sot) + super(ShareTest, self).tearDown() + + def test_get(self): + sot = self.conn.shared_file_system.get_share(self.SHARE_ID) + assert isinstance(sot, _share.Share) + self.assertEqual(self.SHARE_ID, sot.id) + + def test_list_share(self): + shares = self.conn.shared_file_system.shares(details=False) + self.assertGreater(len(list(shares)), 0) + for share in shares: + for attribute in ('id', 'name', 'created_at', 'updated_at'): + self.assertTrue(hasattr(share, attribute)) + + def test_update(self): + updated_share = self.conn.shared_file_system.update_share( + self.SHARE_ID, display_description='updated share') + get_updated_share = self.conn.shared_file_system.get_share( + updated_share.id) + self.assertEqual('updated share', get_updated_share.description) diff --git a/openstack/tests/unit/shared_file_system/v2/test_proxy.py b/openstack/tests/unit/shared_file_system/v2/test_proxy.py new file mode 100644 index 000000000..1a1e8340a --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_proxy.py @@ -0,0 +1,64 @@ +# 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 unittest import mock + +from openstack.shared_file_system.v2 import _proxy +from openstack.shared_file_system.v2 import share +from openstack.tests.unit import test_proxy_base + + +class TestSharedFileSystemProxy(test_proxy_base.TestProxyBase): + + def setUp(self): + super(TestSharedFileSystemProxy, self).setUp() + self.proxy = _proxy.Proxy(self.session) + + def test_shares(self): + self.verify_list(self.proxy.shares, share.Share) + + def test_shares_detailed(self): + self.verify_list(self.proxy.shares, share.Share, + method_kwargs={"details": True, "query": 1}, + expected_kwargs={"query": 1}) + + def test_shares_not_detailed(self): + self.verify_list(self.proxy.shares, share.Share, + method_kwargs={"details": False, "query": 1}, + expected_kwargs={"query": 1}) + + def test_share_get(self): + self.verify_get(self.proxy.get_share, share.Share) + + def test_share_delete(self): + self.verify_delete( + self.proxy.delete_share, share.Share, False) + + def test_share_delete_ignore(self): + self.verify_delete( + self.proxy.delete_share, share.Share, True) + + def test_share_create(self): + self.verify_create(self.proxy.create_share, share.Share) + + def test_share_update(self): + self.verify_update(self.proxy.update_share, share.Share) + + @mock.patch("openstack.resource.wait_for_status") + def test_wait_for(self, mock_wait): + mock_resource = mock.Mock() + mock_wait.return_value = mock_resource + + self.proxy.wait_for_status(mock_resource, 'ACTIVE') + + mock_wait.assert_called_once_with(self.proxy, mock_resource, + 'ACTIVE', [], 2, 120) diff --git a/openstack/tests/unit/shared_file_system/v2/test_share.py b/openstack/tests/unit/shared_file_system/v2/test_share.py new file mode 100644 index 000000000..44e8fff3c --- /dev/null +++ b/openstack/tests/unit/shared_file_system/v2/test_share.py @@ -0,0 +1,111 @@ +# 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 share +from openstack.tests.unit import base + +IDENTIFIER = '08a87d37-5ca2-4308-86c5-cba06d8d796c' +EXAMPLE = { + "id": IDENTIFIER, + "size": 1, + "availability_zone": "manila-zone-1", + "created_at": "2021-02-11T17:38:00.000000", + "status": "available", + "name": None, + "description": None, + "project_id": "d19444eb73af4b37bc0794532ef6fc50", + "snapshot_id": None, + "share_network_id": None, + "share_protocol": "NFS", + "metadata": {}, + "share_type": "cbb18bb7-cc97-477a-b64b-ed7c7f2a1c67", + "volume_type": "default", + "is_public": False, + "is_snapshot_supported": True, + "task_state": None, + "share_type_name": "default", + "access_rules_status": "active", + "replication_type": None, + "is_replicated": False, + "user_id": "6c262cab98de42c2afc4cfccbefc50c7", + "is_creating_new_share_from_snapshot_supported": True, + "is_reverting_to_snapshot_supported": True, + "share_group_id": None, + "source_share_group_snapshot_member_id": None, + "is_mounting_snapshot_supported": True, + "progress": "100%", + "share_server_id": None, + "host": "new@denver#lvm-single-pool" +} + + +class TestShares(base.TestCase): + + def test_basic(self): + shares_resource = share.Share() + self.assertEqual('shares', shares_resource.resources_key) + self.assertEqual('/shares', shares_resource.base_path) + self.assertTrue(shares_resource.allow_list) + self.assertTrue(shares_resource.allow_create) + self.assertTrue(shares_resource.allow_fetch) + self.assertTrue(shares_resource.allow_commit) + self.assertTrue(shares_resource.allow_delete) + + def test_make_shares(self): + shares_resource = share.Share(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], shares_resource.id) + self.assertEqual(EXAMPLE['size'], shares_resource.size) + self.assertEqual(EXAMPLE['availability_zone'], + shares_resource.availability_zone) + self.assertEqual(EXAMPLE['created_at'], shares_resource.created_at) + self.assertEqual(EXAMPLE['status'], shares_resource.status) + self.assertEqual(EXAMPLE['name'], shares_resource.name) + self.assertEqual(EXAMPLE['description'], + shares_resource.description) + self.assertEqual(EXAMPLE['project_id'], shares_resource.project_id) + self.assertEqual(EXAMPLE['snapshot_id'], shares_resource.snapshot_id) + self.assertEqual(EXAMPLE['share_network_id'], + shares_resource.share_network_id) + self.assertEqual(EXAMPLE['share_protocol'], + shares_resource.share_protocol) + self.assertEqual(EXAMPLE['metadata'], shares_resource.metadata) + self.assertEqual(EXAMPLE['share_type'], shares_resource.share_type) + self.assertEqual(EXAMPLE['is_public'], shares_resource.is_public) + self.assertEqual(EXAMPLE['is_snapshot_supported'], + shares_resource.is_snapshot_supported) + self.assertEqual(EXAMPLE['task_state'], shares_resource.task_state) + self.assertEqual(EXAMPLE['share_type_name'], + shares_resource.share_type_name) + self.assertEqual(EXAMPLE['access_rules_status'], + shares_resource.access_rules_status) + self.assertEqual(EXAMPLE['replication_type'], + shares_resource.replication_type) + self.assertEqual(EXAMPLE['is_replicated'], + shares_resource.is_replicated) + self.assertEqual(EXAMPLE['user_id'], shares_resource.user_id) + self.assertEqual(EXAMPLE[ + 'is_creating_new_share_from_snapshot_supported'], + (shares_resource.is_creating_new_share_from_snapshot_supported)) + self.assertEqual(EXAMPLE['is_reverting_to_snapshot_supported'], + shares_resource.is_reverting_to_snapshot_supported) + self.assertEqual(EXAMPLE['share_group_id'], + shares_resource.share_group_id) + self.assertEqual(EXAMPLE[ + 'source_share_group_snapshot_member_id'], + shares_resource.source_share_group_snapshot_member_id) + self.assertEqual(EXAMPLE['is_mounting_snapshot_supported'], + shares_resource.is_mounting_snapshot_supported) + self.assertEqual(EXAMPLE['progress'], + shares_resource.progress) + self.assertEqual(EXAMPLE['share_server_id'], + shares_resource.share_server_id) + self.assertEqual(EXAMPLE['host'], shares_resource.host) diff --git a/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml b/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml new file mode 100644 index 000000000..e7a1bcb01 --- /dev/null +++ b/releasenotes/notes/add-shared-file-system-shares-e9f356a318045607.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support to create, update, list, get, and delete shares + (from shared file system service).