diff --git a/plugins/module_utils/vendor/hcloud/__init__.py b/plugins/module_utils/vendor/hcloud/__init__.py index 6b69b16..2cf9921 100644 --- a/plugins/module_utils/vendor/hcloud/__init__.py +++ b/plugins/module_utils/vendor/hcloud/__init__.py @@ -5,3 +5,4 @@ from ._exceptions import ( # noqa pylint: disable=C0414 APIException as APIException, HCloudException as HCloudException, ) +from ._version import __version__ # noqa diff --git a/plugins/module_utils/vendor/hcloud/_client.py b/plugins/module_utils/vendor/hcloud/_client.py index 257d361..842c5d0 100644 --- a/plugins/module_utils/vendor/hcloud/_client.py +++ b/plugins/module_utils/vendor/hcloud/_client.py @@ -1,15 +1,15 @@ from __future__ import annotations import time -from typing import NoReturn +from typing import Protocol try: import requests except ImportError: requests = None -from ._version import VERSION from ._exceptions import APIException +from ._version import __version__ from .actions import ActionsClient from .certificates import CertificatesClient from .datacenters import DatacentersClient @@ -29,10 +29,19 @@ from .ssh_keys import SSHKeysClient from .volumes import VolumesClient +class PollIntervalFunction(Protocol): + def __call__(self, retries: int) -> float: + """ + Return a interval in seconds to wait between each API call. + + :param retries: Number of calls already made. + """ + + class Client: """Base Client for accessing the Hetzner Cloud API""" - _version = VERSION + _version = __version__ _retry_wait_time = 0.5 __user_agent_prefix = "hcloud-python" @@ -42,7 +51,8 @@ class Client: api_endpoint: str = "https://api.hetzner.cloud/v1", application_name: str | None = None, application_version: str | None = None, - poll_interval: int = 1, + poll_interval: int | float | PollIntervalFunction = 1.0, + poll_max_retries: int = 120, timeout: float | tuple[float, float] | None = None, ): """Create a new Client instance @@ -51,7 +61,11 @@ class Client: :param api_endpoint: Hetzner Cloud API endpoint :param application_name: Your application name :param application_version: Your application _version - :param poll_interval: Interval for polling information from Hetzner Cloud API in seconds + :param poll_interval: + Interval in seconds to use when polling actions from the API. + You may pass a function to compute a custom poll interval. + :param poll_max_retries: + Max retries before timeout when polling actions from the API. :param timeout: Requests timeout in seconds """ self.token = token @@ -60,7 +74,12 @@ class Client: self._application_version = application_version self._requests_session = requests.Session() self._requests_timeout = timeout - self.poll_interval = poll_interval + + if isinstance(poll_interval, (int, float)): + self._poll_interval_func = lambda _: poll_interval # Constant poll interval + else: + self._poll_interval_func = poll_interval + self._poll_max_retries = poll_max_retries self.datacenters = DatacentersClient(self) """DatacentersClient Instance @@ -174,32 +193,18 @@ class Client: } return headers - def _raise_exception_from_response(self, response) -> NoReturn: - raise APIException( - code=response.status_code, - message=response.reason, - details={"content": response.content}, - ) - - def _raise_exception_from_content(self, content: dict) -> NoReturn: - raise APIException( - code=content["error"]["code"], - message=content["error"]["message"], - details=content["error"]["details"], - ) - def request( # type: ignore[no-untyped-def] self, method: str, url: str, - tries: int = 1, + *, + _tries: int = 1, **kwargs, ) -> dict: """Perform a request to the Hetzner Cloud API, wrapper around requests.request :param method: HTTP Method to perform the Request :param url: URL of the Endpoint - :param tries: Tries of the request (used internally, should not be set by the user) :param timeout: Requests timeout in seconds :return: Response """ @@ -213,24 +218,40 @@ class Client: **kwargs, ) - content = response.content + correlation_id = response.headers.get("X-Correlation-Id") + payload = {} try: - if len(content) > 0: - content = response.json() - except (TypeError, ValueError): - self._raise_exception_from_response(response) + 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 content: - assert isinstance(content, dict) - if content["error"]["code"] == "rate_limit_exceeded" and tries < 5: - time.sleep(tries * self._retry_wait_time) - tries = tries + 1 - return self.request(method, url, tries, **kwargs) + 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, + ) - self._raise_exception_from_content(content) - else: - self._raise_exception_from_response(response) + error: dict = payload["error"] - # TODO: return an empty dict instead of an empty string when content == "". - return content # type: ignore[return-value] + if error["code"] == "rate_limit_exceeded" and _tries < 5: + time.sleep(_tries * self._retry_wait_time) + _tries = _tries + 1 + return self.request(method, url, _tries=_tries, **kwargs) + + raise APIException( + code=error["code"], + message=error["message"], + details=error.get("details"), + correlation_id=correlation_id, + ) + + return payload diff --git a/plugins/module_utils/vendor/hcloud/_exceptions.py b/plugins/module_utils/vendor/hcloud/_exceptions.py index 877083f..c884a9a 100644 --- a/plugins/module_utils/vendor/hcloud/_exceptions.py +++ b/plugins/module_utils/vendor/hcloud/_exceptions.py @@ -10,8 +10,22 @@ class HCloudException(Exception): class APIException(HCloudException): """There was an error while performing an API Request""" - def __init__(self, code: int | str, message: str | None, details: Any): - super().__init__(code if message is None and isinstance(code, str) else message) + def __init__( + self, + code: int | str, + message: str, + details: Any, + *, + correlation_id: str | None = None, + ): + extras = [str(code)] + if correlation_id is not None: + extras.append(correlation_id) + + error = f"{message} ({', '.join(extras)})" + + super().__init__(error) self.code = code self.message = message self.details = details + self.correlation_id = correlation_id diff --git a/plugins/module_utils/vendor/hcloud/_version.py b/plugins/module_utils/vendor/hcloud/_version.py index d350d71..bf6f151 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 = "1.35.0" # x-release-please-version +__version__ = "2.0.1" # x-release-please-version diff --git a/plugins/module_utils/vendor/hcloud/actions/client.py b/plugins/module_utils/vendor/hcloud/actions/client.py index a188f62..85fc007 100644 --- a/plugins/module_utils/vendor/hcloud/actions/client.py +++ b/plugins/module_utils/vendor/hcloud/actions/client.py @@ -16,20 +16,24 @@ class BoundAction(BoundModelBase, Action): model = Action - def wait_until_finished(self, max_retries: int = 100) -> None: - """Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks) + def wait_until_finished(self, max_retries: int | None = None) -> None: + """Wait until the specific action has status=finished. - :param max_retries: int - Specify how many retries will be performed before an ActionTimeoutException will be raised - :raises: ActionFailedException when action is finished with status=="error" - :raises: ActionTimeoutException when Action is still in "running" state after max_retries reloads. + :param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised. + :raises: ActionFailedException when action is finished with status==error + :raises: ActionTimeoutException when Action is still in status==running after max_retries is reached. """ + if max_retries is None: + # pylint: disable=protected-access + max_retries = self._client._client._poll_max_retries + + retries = 0 while self.status == Action.STATUS_RUNNING: - if max_retries > 0: + if retries < max_retries: self.reload() + retries += 1 # pylint: disable=protected-access - time.sleep(self._client._client.poll_interval) - max_retries = max_retries - 1 + time.sleep(self._client._client._poll_interval_func(retries)) else: raise ActionTimeoutException(action=self) diff --git a/plugins/module_utils/vendor/hcloud/hcloud.py b/plugins/module_utils/vendor/hcloud/hcloud.py deleted file mode 100644 index 9de1cfe..0000000 --- a/plugins/module_utils/vendor/hcloud/hcloud.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import warnings - -warnings.warn( - "The 'hcloud.hcloud' module is deprecated, please import from the 'hcloud' module instead (e.g. 'from hcloud import Client').", - DeprecationWarning, - stacklevel=2, -) - -# pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import -from ._client import * # noqa diff --git a/plugins/module_utils/vendor/hcloud/isos/client.py b/plugins/module_utils/vendor/hcloud/isos/client.py index cc46af7..fa7b1a8 100644 --- a/plugins/module_utils/vendor/hcloud/isos/client.py +++ b/plugins/module_utils/vendor/hcloud/isos/client.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple -from warnings import warn from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Iso @@ -37,7 +36,6 @@ class IsosClient(ClientEntityBase): self, name: str | None = None, architecture: list[str] | None = None, - include_wildcard_architecture: bool | None = None, include_architecture_wildcard: bool | None = None, page: int | None = None, per_page: int | None = None, @@ -48,8 +46,6 @@ class IsosClient(ClientEntityBase): Can be used to filter ISOs by their name. :param architecture: List[str] (optional) Can be used to filter ISOs by their architecture. Choices: x86 arm - :param include_wildcard_architecture: bool (optional) - Deprecated, please use `include_architecture_wildcard` instead. :param include_architecture_wildcard: bool (optional) Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by architecture and also want custom ISOs. @@ -59,14 +55,6 @@ class IsosClient(ClientEntityBase): Specifies how many results are returned by page :return: (List[:class:`BoundIso `], :class:`Meta `) """ - - if include_wildcard_architecture is not None: - warn( - "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", - DeprecationWarning, - ) - include_architecture_wildcard = include_wildcard_architecture - params: dict[str, Any] = {} if name is not None: params["name"] = name @@ -87,7 +75,6 @@ class IsosClient(ClientEntityBase): self, name: str | None = None, architecture: list[str] | None = None, - include_wildcard_architecture: bool | None = None, include_architecture_wildcard: bool | None = None, ) -> list[BoundIso]: """Get all ISOs @@ -96,21 +83,11 @@ class IsosClient(ClientEntityBase): Can be used to filter ISOs by their name. :param architecture: List[str] (optional) Can be used to filter ISOs by their architecture. Choices: x86 arm - :param include_wildcard_architecture: bool (optional) - Deprecated, please use `include_architecture_wildcard` instead. :param include_architecture_wildcard: bool (optional) Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by architecture and also want custom ISOs. :return: List[:class:`BoundIso `] """ - - if include_wildcard_architecture is not None: - warn( - "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", - DeprecationWarning, - ) - include_architecture_wildcard = include_wildcard_architecture - return self._iter_pages( self.get_list, name=name, diff --git a/plugins/module_utils/vendor/hcloud/primary_ips/client.py b/plugins/module_utils/vendor/hcloud/primary_ips/client.py index ece8d88..41ef3bf 100644 --- a/plugins/module_utils/vendor/hcloud/primary_ips/client.py +++ b/plugins/module_utils/vendor/hcloud/primary_ips/client.py @@ -189,9 +189,8 @@ class PrimaryIPsClient(ClientEntityBase): def create( self, type: str, - # TODO: Make the datacenter argument optional - datacenter: Datacenter | BoundDatacenter | None, name: str, + datacenter: Datacenter | BoundDatacenter | None = None, assignee_type: str | None = "server", assignee_id: int | None = None, auto_delete: bool | None = False, @@ -199,23 +198,21 @@ class PrimaryIPsClient(ClientEntityBase): ) -> CreatePrimaryIPResponse: """Creates a new Primary IP assigned to a server. - :param type: str - Primary IP type Choices: ipv4, ipv6 - :param assignee_type: str - :param assignee_id: int (optional) - :param datacenter: Datacenter - :param labels: Dict[str, str] (optional) - User-defined labels (key-value pairs) + :param type: str Primary IP type Choices: ipv4, ipv6 :param name: str + :param datacenter: Datacenter (optional) + :param assignee_type: str (optional) + :param assignee_id: int (optional) :param auto_delete: bool (optional) + :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`CreatePrimaryIPResponse ` """ data: dict[str, Any] = { + "name": name, "type": type, "assignee_type": assignee_type, "auto_delete": auto_delete, - "name": name, } if datacenter is not None: data["datacenter"] = datacenter.id_or_name diff --git a/plugins/module_utils/vendor/hcloud/servers/client.py b/plugins/module_utils/vendor/hcloud/servers/client.py index b959b9d..0d65e79 100644 --- a/plugins/module_utils/vendor/hcloud/servers/client.py +++ b/plugins/module_utils/vendor/hcloud/servers/client.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple @@ -336,15 +335,14 @@ class BoundServer(BoundModelBase, Server): def rebuild( self, image: Image | BoundImage, - *, - return_response: bool = False, - ) -> RebuildResponse | BoundAction: + # pylint: disable=unused-argument + **kwargs: Any, + ) -> RebuildResponse: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param image: Image to use for the rebuilt server - :param return_response: Whether to return the full response or only the action. """ - return self._client.rebuild(self, image, return_response=return_response) + return self._client.rebuild(self, image) def change_type( self, @@ -1009,14 +1007,13 @@ class ServersClient(ClientEntityBase): self, server: Server | BoundServer, image: Image | BoundImage, - *, - return_response: bool = False, - ) -> RebuildResponse | BoundAction: + # pylint: disable=unused-argument + **kwargs: Any, + ) -> RebuildResponse: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param server: Server to rebuild :param image: Image to use for the rebuilt server - :param return_response: Whether to return the full response or only the action. """ data: dict[str, Any] = {"image": image.id_or_name} response = self._client.request( @@ -1025,22 +1022,11 @@ class ServersClient(ClientEntityBase): json=data, ) - rebuild_response = RebuildResponse( + return RebuildResponse( action=BoundAction(self._client.actions, response["action"]), root_password=response.get("root_password"), ) - if not return_response: - warnings.warn( - "Returning only the 'action' is deprecated, please set the " - "'return_response' keyword argument to 'True' to return the full " - "rebuild response and update your code accordingly.", - DeprecationWarning, - stacklevel=2, - ) - return rebuild_response.action - return rebuild_response - def enable_backup(self, server: Server | BoundServer) -> BoundAction: """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. diff --git a/plugins/modules/iso_info.py b/plugins/modules/iso_info.py index aab33ca..1ba5193 100644 --- a/plugins/modules/iso_info.py +++ b/plugins/modules/iso_info.py @@ -172,7 +172,7 @@ class AnsibleHCloudIsoInfo(AnsibleHCloud): else: self.hcloud_iso_info = self.client.isos.get_all( architecture=self.module.params.get("architecture"), - include_wildcard_architecture=self.module.params.get("include_wildcard_architecture"), + include_architecture_wildcard=self.module.params.get("include_wildcard_architecture"), ) except HCloudException as exception: diff --git a/plugins/modules/primary_ip.py b/plugins/modules/primary_ip.py index 08bcea4..a257325 100644 --- a/plugins/modules/primary_ip.py +++ b/plugins/modules/primary_ip.py @@ -214,7 +214,6 @@ class AnsibleHCloudPrimaryIP(AnsibleHCloud): "type": self.module.params.get("type"), "name": self.module.params.get("name"), "auto_delete": self.module.params.get("auto_delete"), - "datacenter": None, # TODO: https://github.com/hetznercloud/hcloud-python/pull/363 } if self.module.params.get("datacenter") is not None: diff --git a/plugins/modules/server.py b/plugins/modules/server.py index 350415b..765b358 100644 --- a/plugins/modules/server.py +++ b/plugins/modules/server.py @@ -868,7 +868,7 @@ class AnsibleHCloudServer(AnsibleHCloud): if not self.module.check_mode: image = self._get_image(self.hcloud_server.server_type) # When we rebuild the server progress takes some more time. - resp = self.client.servers.rebuild(self.hcloud_server, image, return_response=True) + resp = self.client.servers.rebuild(self.hcloud_server, image) resp.action.wait_until_finished(1000) self._mark_as_changed() diff --git a/scripts/vendor.py b/scripts/vendor.py index 4fa881d..6ff12ad 100755 --- a/scripts/vendor.py +++ b/scripts/vendor.py @@ -22,26 +22,15 @@ from textwrap import dedent logger = logging.getLogger("vendor") HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python" -HCLOUD_VERSION = "v1.35.0" +HCLOUD_VERSION = "v2.0.1" HCLOUD_VENDOR_PATH = "plugins/module_utils/vendor/hcloud" def apply_code_modifications(source_path: Path): - # The ansible galaxy-importer consider __version___.py to be an invalid filename in module_utils/ - # Move the __version__.py file to _version.py - move(source_path / "__version__.py", source_path / "_version.py") - for file in source_path.rglob("*.py"): content = file.read_text() content_orig = content - # Move the __version__.py file to _version.py - content = re.sub( - r"from .__version__ import VERSION", - r"from ._version import VERSION", - content, - ) - # Wrap requests imports content = re.sub( r"import requests", diff --git a/tests/integration/targets/floating_ip/tasks/test.yml b/tests/integration/targets/floating_ip/tasks/test.yml index 8f27e4f..6b4b58d 100644 --- a/tests/integration/targets/floating_ip/tasks/test.yml +++ b/tests/integration/targets/floating_ip/tasks/test.yml @@ -102,7 +102,7 @@ that: - result is failed - result.failure.code == "invalid_input" - - result.msg == "invalid input in fields 'server', 'home_location'" + - result.failure.message == "invalid input in fields 'server', 'home_location'" - name: test create Floating IP with check mode hetzner.hcloud.floating_ip: @@ -373,7 +373,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "Floating IP deletion is protected" + - result.failure.message == "Floating IP deletion is protected" - name: test update Floating IP delete protection hetzner.hcloud.floating_ip: @@ -462,7 +462,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "Floating IP deletion is protected" + - result.failure.message == "Floating IP deletion is protected" - name: test update Floating IP delete protection hetzner.hcloud.floating_ip: diff --git a/tests/integration/targets/load_balancer/tasks/test.yml b/tests/integration/targets/load_balancer/tasks/test.yml index 3b7de5b..4593f93 100644 --- a/tests/integration/targets/load_balancer/tasks/test.yml +++ b/tests/integration/targets/load_balancer/tasks/test.yml @@ -121,7 +121,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "load balancer deletion is protected" + - result.failure.message == "load balancer deletion is protected" - name: Test update delete_protection hetzner.hcloud.load_balancer: diff --git a/tests/integration/targets/network/tasks/test.yml b/tests/integration/targets/network/tasks/test.yml index b814ceb..7651b5e 100644 --- a/tests/integration/targets/network/tasks/test.yml +++ b/tests/integration/targets/network/tasks/test.yml @@ -100,7 +100,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "network deletion is protected" + - result.failure.message == "network deletion is protected" - name: Test update delete protection hetzner.hcloud.network: diff --git a/tests/integration/targets/primary_ip/tasks/test.yml b/tests/integration/targets/primary_ip/tasks/test.yml index 1c43846..970631d 100644 --- a/tests/integration/targets/primary_ip/tasks/test.yml +++ b/tests/integration/targets/primary_ip/tasks/test.yml @@ -121,7 +121,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "Primary IP deletion is protected" + - result.failure.message == "Primary IP deletion is protected" - name: Test update delete protection hetzner.hcloud.primary_ip: diff --git a/tests/integration/targets/server/tasks/test_basic.yml b/tests/integration/targets/server/tasks/test_basic.yml index b80a0d2..571a75c 100644 --- a/tests/integration/targets/server/tasks/test_basic.yml +++ b/tests/integration/targets/server/tasks/test_basic.yml @@ -316,7 +316,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "server deletion is protected" + - result.failure.message == "server deletion is protected" - name: test rebuild server fails if it is protected hetzner.hcloud.server: @@ -330,7 +330,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "server rebuild is protected" + - result.failure.message == "server rebuild is protected" - name: test remove server protection hetzner.hcloud.server: @@ -588,7 +588,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "server deletion is protected" + - result.failure.message == "server deletion is protected" - name: remove protection from server hetzner.hcloud.server: diff --git a/tests/integration/targets/server/tasks/test_firewalls.yml b/tests/integration/targets/server/tasks/test_firewalls.yml index 661012c..efcba3f 100644 --- a/tests/integration/targets/server/tasks/test_firewalls.yml +++ b/tests/integration/targets/server/tasks/test_firewalls.yml @@ -14,7 +14,7 @@ that: - result is failed - result.failure.code == "not_found" - - result.msg == "firewall not-existing was not found" + - result.failure.message == "firewall not-existing was not found" - name: setup create firewalls hetzner.hcloud.firewall: diff --git a/tests/integration/targets/server/tasks/test_validation.yml b/tests/integration/targets/server/tasks/test_validation.yml index 545117b..9d9a7e0 100644 --- a/tests/integration/targets/server/tasks/test_validation.yml +++ b/tests/integration/targets/server/tasks/test_validation.yml @@ -35,7 +35,7 @@ that: - result is failed - result.failure.code == "not_found" - - result.msg == "server_type not-existing-server-type was not found" + - result.failure.message == "server_type not-existing-server-type was not found" - name: test create server with not existing image hetzner.hcloud.server: @@ -50,4 +50,4 @@ that: - result is failed - result.failure.code == "not_found" - - result.msg == "Image my-not-existing-image-20.04 was not found" + - result.failure.message == "Image my-not-existing-image-20.04 was not found" diff --git a/tests/integration/targets/ssh_key/tasks/test.yml b/tests/integration/targets/ssh_key/tasks/test.yml index 3403080..29a797d 100644 --- a/tests/integration/targets/ssh_key/tasks/test.yml +++ b/tests/integration/targets/ssh_key/tasks/test.yml @@ -134,7 +134,7 @@ that: - result is failed - result.failure.code == "uniqueness_error" - - result.msg == "SSH key with the same fingerprint already exists" + - result.failure.message == "SSH key with the same fingerprint already exists" - name: test delete ssh key hetzner.hcloud.ssh_key: diff --git a/tests/integration/targets/volume/tasks/test.yml b/tests/integration/targets/volume/tasks/test.yml index 55ed807..e116886 100644 --- a/tests/integration/targets/volume/tasks/test.yml +++ b/tests/integration/targets/volume/tasks/test.yml @@ -209,7 +209,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "volume deletion is protected" + - result.failure.message == "volume deletion is protected" - name: test update Volume delete protection hetzner.hcloud.volume: @@ -256,7 +256,7 @@ that: - result is failed - result.failure.code == "protected" - - result.msg == "volume deletion is protected" + - result.failure.message == "volume deletion is protected" - name: test update Volume delete protection hetzner.hcloud.volume: diff --git a/tests/unit/module_utils/test_hcloud.py b/tests/unit/module_utils/test_hcloud.py index 2f5e750..196ca87 100644 --- a/tests/unit/module_utils/test_hcloud.py +++ b/tests/unit/module_utils/test_hcloud.py @@ -35,7 +35,7 @@ def test_hcloud_fail_json_hcloud(module): hcloud.fail_json_hcloud(exception) module.fail_json.assert_called_with( - msg="invalid input in fields 'server', 'home_location'", + msg="invalid input in fields 'server', 'home_location' (invalid_input)", exception=traceback.format_exc(), failure={ "message": "invalid input in fields 'server', 'home_location'",