diff --git a/plugins/module_utils/vendor/hcloud/_client.py b/plugins/module_utils/vendor/hcloud/_client.py index 6f1fdad..b208181 100644 --- a/plugins/module_utils/vendor/hcloud/_client.py +++ b/plugins/module_utils/vendor/hcloud/_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import time +from http import HTTPStatus from random import uniform from typing import Protocol @@ -81,7 +82,33 @@ def exponential_backoff_function( class Client: - """Base Client for accessing the Hetzner Cloud API""" + """ + Client for the Hetzner Cloud API. + + The Hetzner Cloud API reference is available at https://docs.hetzner.cloud. + + **Retry mechanism** + + The :attr:`Client.request` method will retry failed requests that match certain criteria. The + default retry interval is defined by an exponential backoff algorithm truncated to 60s + with jitter. The default maximal number of retries is 5. + + The following rules define when a request can be retried: + + - When the client returned a network timeout error. + - When the API returned an HTTP error, with the status code: + + - ``502`` Bad Gateway + - ``504`` Gateway Timeout + + - When the API returned an application error, with the code: + + - ``conflict`` + - ``rate_limit_exceeded`` + + Changes to the retry policy might occur between releases, and will not be considered + breaking changes. + """ _version = __version__ __user_agent_prefix = "hcloud-python" @@ -259,50 +286,71 @@ class Client: retries = 0 while True: - response = self._requests_session.request( - method=method, - url=url, - headers=headers, - **kwargs, - ) - - correlation_id = response.headers.get("X-Correlation-Id") - payload = {} try: - if len(response.content) > 0: - payload = response.json() - except (TypeError, ValueError) as exc: + response = self._requests_session.request( + method=method, + url=url, + headers=headers, + **kwargs, + ) + return self._read_response(response) + except APIException as exception: + if retries < self._retry_max_retries and self._retry_policy(exception): + time.sleep(self._retry_interval(retries)) + retries += 1 + continue + raise + except requests.exceptions.Timeout: + if retries < self._retry_max_retries: + time.sleep(self._retry_interval(retries)) + retries += 1 + continue + raise + + def _read_response(self, response) -> dict: + correlation_id = response.headers.get("X-Correlation-Id") + payload = {} + try: + if len(response.content) > 0: + payload = response.json() + except (TypeError, ValueError) as exc: + raise APIException( + code=response.status_code, + message=response.reason, + details={"content": response.content}, + correlation_id=correlation_id, + ) from exc + + if not response.ok: + if not payload or "error" not in payload: raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, correlation_id=correlation_id, - ) from exc - - if not response.ok: - if not payload or "error" not in payload: - raise APIException( - code=response.status_code, - message=response.reason, - details={"content": response.content}, - correlation_id=correlation_id, - ) - - error: dict = payload["error"] - - if ( - error["code"] == "rate_limit_exceeded" - and retries < self._retry_max_retries - ): - time.sleep(self._retry_interval(retries)) - retries += 1 - continue - - raise APIException( - code=error["code"], - message=error["message"], - details=error.get("details"), - correlation_id=correlation_id, ) - return payload + error: dict = payload["error"] + raise APIException( + code=error["code"], + message=error["message"], + details=error.get("details"), + correlation_id=correlation_id, + ) + + return payload + + def _retry_policy(self, exception: APIException) -> bool: + if isinstance(exception.code, str): + return exception.code in ( + "rate_limit_exceeded", + "conflict", + ) + + if isinstance(exception.code, int): + return exception.code in ( + HTTPStatus.BAD_GATEWAY, + HTTPStatus.GATEWAY_TIMEOUT, + ) + + return False diff --git a/plugins/module_utils/vendor/hcloud/_version.py b/plugins/module_utils/vendor/hcloud/_version.py index 0dbaa6b..1fc7f59 100644 --- a/plugins/module_utils/vendor/hcloud/_version.py +++ b/plugins/module_utils/vendor/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.1.1" # x-release-please-version +__version__ = "2.2.0" # x-release-please-version diff --git a/plugins/module_utils/vendor/hcloud/networks/domain.py b/plugins/module_utils/vendor/hcloud/networks/domain.py index 7dac858..3c5f02d 100644 --- a/plugins/module_utils/vendor/hcloud/networks/domain.py +++ b/plugins/module_utils/vendor/hcloud/networks/domain.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING try: @@ -92,12 +93,31 @@ class NetworkSubnet(BaseDomain): ID of the vSwitch. """ - TYPE_SERVER = "server" - """Subnet Type server, deprecated, use TYPE_CLOUD instead""" + @property + def TYPE_SERVER(self) -> str: # pylint: disable=invalid-name + """ + Used to connect cloud servers and load balancers. + + .. deprecated:: 2.2.0 + Use :attr:`NetworkSubnet.TYPE_CLOUD` instead. + """ + warnings.warn( + "The 'NetworkSubnet.TYPE_SERVER' property is deprecated, please use the `NetworkSubnet.TYPE_CLOUD` property instead.", + DeprecationWarning, + stacklevel=2, + ) + return "server" + TYPE_CLOUD = "cloud" - """Subnet Type cloud""" + """ + Used to connect cloud servers and load balancers. + """ TYPE_VSWITCH = "vswitch" - """Subnet Type vSwitch""" + """ + Used to connect cloud servers and load balancers with dedicated servers. + + See https://docs.hetzner.com/cloud/networks/connect-dedi-vswitch/ + """ __api_properties__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") __slots__ = __api_properties__ diff --git a/scripts/vendor.py b/scripts/vendor.py index ee4010a..415d220 100755 --- a/scripts/vendor.py +++ b/scripts/vendor.py @@ -22,7 +22,7 @@ from textwrap import dedent logger = logging.getLogger("vendor") HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python" -HCLOUD_VERSION = "v2.1.1" +HCLOUD_VERSION = "v2.2.0" HCLOUD_VENDOR_PATH = "plugins/module_utils/vendor/hcloud"