diff --git a/doc/source/sdk/guides/sfsturbo.rst b/doc/source/sdk/guides/sfsturbo.rst index 45c745e55..617319a31 100644 --- a/doc/source/sdk/guides/sfsturbo.rst +++ b/doc/source/sdk/guides/sfsturbo.rst @@ -17,7 +17,7 @@ This interface is used to create a share (scalable file system). :class:`~otcextensions.sdk.sfsturbo.v1.share.Share`. .. literalinclude:: ../examples/sfsturbo/create_share.py - :lines: 16-36 + :lines: 16-43 Delete Share ^^^^^^^^^^^^ diff --git a/examples/sfsturbo/create_share.py b/examples/sfsturbo/create_share.py index f8d5a2f5a..75039f14c 100644 --- a/examples/sfsturbo/create_share.py +++ b/examples/sfsturbo/create_share.py @@ -25,11 +25,18 @@ "name": "test_share_1", "share_proto": "NFS", "share_type": "STANDARD", + # For an HPC file system, size must be a multiple of 1.2 TiB (e.g. 3686) + # and hpc_bw one of "20M", "40M", "125M", "250M", "500M", "1000M". "size": 100, "availability_zone": "eu-de-01", "vpc_id": "vpc_uuid", "subnet_id": "subnet_uuid", "security_group_id": "security_group_uuid", + # Optional metadata. crypt_key_id creates an encrypted file system. To + # deploy an HPC file system set expand_type to "hpc" and provide hpc_bw + # (size must then be a multiple of 1.2 TiB, e.g. 3686); use "bandwidth" + # for an enhanced file system. + "metadata": {"crypt_key_id": "kms_key_uuid"}, } share = conn.sfsturbo.create_share(**attrs) diff --git a/otcextensions/sdk/cce/v3/_base.py b/otcextensions/sdk/cce/v3/_base.py index 88da413c4..289e1a9b6 100644 --- a/otcextensions/sdk/cce/v3/_base.py +++ b/otcextensions/sdk/cce/v3/_base.py @@ -141,11 +141,16 @@ def create(self, session, prepend_key=False, base_path=None): # Overriden here to override prepend_key default value return super(Resource, self).create(session, prepend_key, base_path) - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **params): """Delete the remote resource based on this instance. :param session: The session to use for making this request. :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict params: Additional arguments (e.g. ``params``, + ``microversion``, ``base_path``) as passed down by the calling + :class:`~openstack.proxy.Proxy`. Their exact set differs between + openstacksdk releases, so they are accepted generically and + forwarded as-is to ``_raw_delete``. :return: This :class:`Resource` instance. :raises: :exc:`~openstack.exceptions.MethodNotSupported` if @@ -154,7 +159,7 @@ def delete(self, session, error_message=None): the resource was not found. """ - response = self._raw_delete(session) + response = self._raw_delete(session, **params) kwargs = {} if error_message: kwargs["error_message"] = error_message diff --git a/otcextensions/sdk/rds/v3/_base.py b/otcextensions/sdk/rds/v3/_base.py index 7491ee7da..2de785252 100644 --- a/otcextensions/sdk/rds/v3/_base.py +++ b/otcextensions/sdk/rds/v3/_base.py @@ -151,9 +151,19 @@ def find(cls, session, name_or_id, ignore_missing=True, **params): "No %s found for %s" % (cls.__name__, name_or_id) ) - def delete(self, session, error_message=None): + def delete(self, session, error_message=None, **params): + """Delete the remote resource based on this instance. - response = self._raw_delete(session) + :param session: The session to use for making this request. + :type session: :class:`~keystoneauth1.adapter.Adapter` + :param dict params: Additional arguments (e.g. ``params``, + ``microversion``, ``base_path``) as passed down by the calling + :class:`~openstack.proxy.Proxy`. Their exact set differs between + openstacksdk releases, so they are accepted generically and + forwarded as-is to ``_raw_delete``. + """ + + response = self._raw_delete(session, **params) kwargs = {} if error_message: kwargs["error_message"] = error_message diff --git a/otcextensions/sdk/sfsturbo/v1/share.py b/otcextensions/sdk/sfsturbo/v1/share.py index 961475d4f..0f0783f35 100644 --- a/otcextensions/sdk/sfsturbo/v1/share.py +++ b/otcextensions/sdk/sfsturbo/v1/share.py @@ -17,6 +17,24 @@ LOG = _log.setup_logging(__name__) +class Metadata(resource.Resource): + + # Properties + #: The extension type. ``bandwidth`` creates an enhanced file system; + #: ``hpc`` creates an HPC file system (in which case ``hpc_bw`` is + #: mandatory). + #: *Type: str* + expand_type = resource.Body("expand_type") + #: The HPC file system bandwidth, e.g. ``250M``. Mandatory when + #: ``expand_type`` is ``hpc``. + #: *Type: str* + hpc_bw = resource.Body("hpc_bw") + #: The ID of a KMS professional key, used to create an encrypted file + #: system. + #: *Type: str* + crypt_key_id = resource.Body("crypt_key_id") + + class Share(resource.Resource): base_path = "/sfs-turbo/shares" @@ -52,6 +70,10 @@ class Share(resource.Resource): #: For an enhanced file system, bandwidth is returned for this field. #: *Type: str* expand_type = resource.Body("expand_type") + #: Specifies the metadata used to create the file system, carrying + #: ``expand_type``, ``hpc_bw`` and ``crypt_key_id``. Sent only on creation. + #: *Type: :class:`Metadata`* + metadata = resource.Body("metadata", type=Metadata) #: Specifies the mount point of the SFS Turbo file system. #: *Type: str* export_location = resource.Body("export_location") diff --git a/otcextensions/tests/functional/privatenat.py b/otcextensions/tests/functional/privatenat.py index 955a76d4a..7ecca6f4c 100644 --- a/otcextensions/tests/functional/privatenat.py +++ b/otcextensions/tests/functional/privatenat.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import time import uuid from openstack import exceptions as sdk_exceptions @@ -17,6 +18,25 @@ class PrivateNatEnvironmentMixin(object): + def _wait_for_subnet_active(self, subnet, timeout=60, retries=3): + """Wait for a VPC subnet to become ACTIVE. + + The OTC VPC subnet API is occasionally read-after-write + inconsistent: a GET right after creation can 404 even though a + previous GET already returned the resource. Retry the wait a + few times before giving up so such transient 404s don't fail + the test. + """ + for attempt in range(retries): + try: + return resource.wait_for_status( + self.conn.vpc, subnet, "ACTIVE", None, 2, timeout + ) + except sdk_exceptions.ResourceNotFound: + if attempt == retries - 1: + raise + time.sleep(2) + def _delete_private_transit_ip(self, transit_ip_id): self.conn.natv3.delete_private_transit_ip(transit_ip_id, ignore_missing=True) @@ -43,7 +63,7 @@ def _create_private_nat_subnet(self, vpc, prefix, suffix): gateway_ip=gw[:-2] + ".1", dns_list=["100.125.4.25", "100.125.129.199"], ) - resource.wait_for_status(self.conn.vpc, subnet, "ACTIVE", None, 2, 60) + self._wait_for_subnet_active(subnet) return subnet def _delete_private_nat_subnet(self, subnet): @@ -52,7 +72,7 @@ def _delete_private_nat_subnet(self, subnet): except sdk_exceptions.ResourceNotFound: return - resource.wait_for_status(self.conn.vpc, subnet, "ACTIVE", None, 2, 60) + self._wait_for_subnet_active(subnet) self.conn.vpc.delete_subnet(subnet, ignore_missing=True) resource.wait_for_delete(self.conn.vpc, subnet, 2, 120) diff --git a/otcextensions/tests/functional/sdk/swr/__init__.py b/otcextensions/tests/functional/sdk/swr/__init__.py index 42bdc6f0f..cbc1ff9ec 100644 --- a/otcextensions/tests/functional/sdk/swr/__init__.py +++ b/otcextensions/tests/functional/sdk/swr/__init__.py @@ -9,6 +9,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import time + +from openstack import exceptions as sdk_exceptions from otcextensions.tests.functional import base @@ -17,3 +20,19 @@ class TestSwr(base.BaseFunctionalTest): def setUp(self): super(TestSwr, self).setUp() self.client = self.conn.swr + + def _create_organization(self, namespace, retries=3, delay=5): + """Create an SWR organization, retrying on transient server errors. + + The SWR "create namespace" API occasionally returns a transient + 5xx (e.g. SVCSTG.SWR.5000059) under load; retry a few times + before giving up. + """ + for attempt in range(retries): + try: + return self.client.create_organization(namespace=namespace) + except sdk_exceptions.HttpException as e: + status = e.response.status_code if e.response is not None else None + if not status or status < 500 or attempt == retries - 1: + raise + time.sleep(delay) diff --git a/otcextensions/tests/functional/sdk/swr/v2/test_organization.py b/otcextensions/tests/functional/sdk/swr/v2/test_organization.py index 40cf3fb82..488f6632c 100644 --- a/otcextensions/tests/functional/sdk/swr/v2/test_organization.py +++ b/otcextensions/tests/functional/sdk/swr/v2/test_organization.py @@ -22,7 +22,7 @@ def setUp(self): super(TestOrganization, self).setUp() self.org_name = "sdk-swr-org-" + uuid.uuid4().hex - self.org = self.client.create_organization(namespace=self.org_name) + self.org = self._create_organization(self.org_name) if os.getenv("OS_SWR_PERMISSIONS_RUN"): self.permission = [ { diff --git a/otcextensions/tests/functional/sdk/swr/v2/test_repository.py b/otcextensions/tests/functional/sdk/swr/v2/test_repository.py index 7d1739470..aebc748e3 100644 --- a/otcextensions/tests/functional/sdk/swr/v2/test_repository.py +++ b/otcextensions/tests/functional/sdk/swr/v2/test_repository.py @@ -22,7 +22,7 @@ def setUp(self): self.org_name = "sdk-swr-org-" + uuid.uuid4().hex self.repo_name = "sdk-swr-repo-" + uuid.uuid4().hex - self.org = self.client.create_organization(namespace=self.org_name) + self.org = self._create_organization(self.org_name) self.repo = self.client.create_repository( namespace=self.org_name, repository=self.repo_name, diff --git a/otcextensions/tests/unit/sdk/sfsturbo/v1/test_share.py b/otcextensions/tests/unit/sdk/sfsturbo/v1/test_share.py index 9b2c58287..e675cb333 100644 --- a/otcextensions/tests/unit/sdk/sfsturbo/v1/test_share.py +++ b/otcextensions/tests/unit/sdk/sfsturbo/v1/test_share.py @@ -39,6 +39,21 @@ "vpc_id": "vpc_id", } +METADATA_EXAMPLE = { + "expand_type": "hpc", + "hpc_bw": "250M", + "crypt_key_id": "crypt_key_id", +} + + +class TestMetadata(base.TestCase): + + def test_make_it(self): + sot = _share.Metadata(**METADATA_EXAMPLE) + self.assertEqual(METADATA_EXAMPLE["expand_type"], sot.expand_type) + self.assertEqual(METADATA_EXAMPLE["hpc_bw"], sot.hpc_bw) + self.assertEqual(METADATA_EXAMPLE["crypt_key_id"], sot.crypt_key_id) + class TestShare(base.TestCase): @@ -114,3 +129,25 @@ def test_change_security_group(self): "sfs-turbo/shares/%s/action" % EXAMPLE["id"], json={"change_security_group": {"security_group_id": "secgroup-uuid"}}, ) + + def test_create_body_includes_metadata(self): + metadata = { + "expand_type": "hpc", + "hpc_bw": "250M", + "crypt_key_id": "kms-key-uuid", + } + sot = _share.Share( + name="hpc-test", + share_proto="NFS", + share_type="PERFORMANCE", + size=3686, + availability_zone="eu-de-01", + vpc_id="vpc-uuid", + subnet_id="subnet-uuid", + security_group_id="secgroup-uuid", + metadata=metadata, + ) + + request = sot._prepare_request(requires_id=False, prepend_key=True) + + self.assertEqual(metadata, request.body["share"]["metadata"])