diff --git a/.gitignore b/.gitignore index d6a37e5c..cdfb360a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.venv +venv # Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv # Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv @@ -55,7 +57,6 @@ flycheck_*.el # network security /network-security.data - ### Git ### # Created by git for backups. To disable backups in Git: # $ git config --global mergetool.keepBackup false @@ -387,3 +388,4 @@ $RECYCLE.BIN/ # End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv cloud-config-hcloud.ini +tests/integration/inventory diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ba96d66..0dbf0850 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,7 @@ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +exclude: ^plugins/module_utils/vendor/hcloud/.*$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 @@ -52,3 +53,11 @@ repos: entry: antsibull-changelog lint pass_filenames: false files: ^changelogs/.*$ + + - id: check-hcloud-vendor + name: check hcloud vendor + description: Ensure the hcloud vendored files are in sync + language: python + entry: python3 scripts/vendor.py + pass_filenames: false + files: ^scripts/vendor.py$ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e44dbb84 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +SHELL := bash +.PHONY: vendor clean + +vendor: + python3 scripts/vendor.py + +clean: + git clean -xdf \ + -e tests/integration/cloud-config-hcloud.ini diff --git a/changelogs/fragments/vendor-hcloud-python-dependency.yml b/changelogs/fragments/vendor-hcloud-python-dependency.yml new file mode 100644 index 00000000..36194ab4 --- /dev/null +++ b/changelogs/fragments/vendor-hcloud-python-dependency.yml @@ -0,0 +1,12 @@ +release_summary: | + This release bundles the hcloud dependency in the collection, this allows us to ship + new features or bug fixes without having to release new major versions and require the + users to upgrade their version of the hcloud dependency. +minor_changes: + - Bundle hcloud python dependency inside the collection. + - > + python-dateutil >= 2.7.5 is now required by the collection. If you already have the + hcloud package installed, this dependency should also be installed. + - > + requests >= 2.20 is now required by the collection. If you already have the hcloud + package installed, this dependency should also be installed. diff --git a/plugins/doc_fragments/hcloud.py b/plugins/doc_fragments/hcloud.py index 80f0d8ea..6426ee0c 100644 --- a/plugins/doc_fragments/hcloud.py +++ b/plugins/doc_fragments/hcloud.py @@ -17,7 +17,8 @@ class ModuleDocFragment: default: https://api.hetzner.cloud/v1 type: str requirements: - - hcloud-python >= 1.20.0 + - python-dateutil >= 2.7.5 + - requests >=2.20 seealso: - name: Documentation for Hetzner Cloud API description: Complete reference for the Hetzner Cloud API. diff --git a/plugins/inventory/hcloud.py b/plugins/inventory/hcloud.py index dadf7952..856e6133 100644 --- a/plugins/inventory/hcloud.py +++ b/plugins/inventory/hcloud.py @@ -121,13 +121,11 @@ from ansible.module_utils.common.text.converters import to_native from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.release import __version__ - -try: - from hcloud import APIException, hcloud - - HAS_HCLOUD = True -except ImportError: - HAS_HCLOUD = False +from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import ( + HAS_DATEUTIL, + HAS_REQUESTS, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor import hcloud class InventoryModule(BaseInventoryPlugin, Constructable): @@ -159,7 +157,7 @@ def _test_hcloud_token(self): # We test the API Token against the location API, because this is the API with the smallest result # and not controllable from the customer. self.client.locations.get_all() - except APIException: + except hcloud.APIException: raise AnsibleError("Invalid Hetzner Cloud API Token.") def _get_servers(self): @@ -177,7 +175,7 @@ def _filter_servers(self): self.network = self.client.networks.get_by_name(network) if self.network is None: self.network = self.client.networks.get_by_id(network) - except APIException: + except hcloud.APIException: raise AnsibleError("The given network is not found.") tmp = [] @@ -322,8 +320,10 @@ def verify_file(self, path): def parse(self, inventory, loader, path, cache=True): super().parse(inventory, loader, path, cache) - if not HAS_HCLOUD: - raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.") + if not HAS_REQUESTS: + raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires requests.") + if not HAS_DATEUTIL: + raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires python-dateutil.") self._read_config_data(path) self._configure_hcloud_client() diff --git a/plugins/module_utils/hcloud.py b/plugins/module_utils/hcloud.py index cb99555e..69082f54 100644 --- a/plugins/module_utils/hcloud.py +++ b/plugins/module_utils/hcloud.py @@ -5,13 +5,20 @@ from ansible.module_utils.ansible_release import __version__ from ansible.module_utils.basic import env_fallback, missing_required_lib +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor import hcloud + +HAS_REQUESTS = True +HAS_DATEUTIL = True try: - import hcloud + import requests # pylint: disable=unused-import +except ImportError: + HAS_REQUESTS = False - HAS_HCLOUD = True +try: + import dateutil # pylint: disable=unused-import except ImportError: - HAS_HCLOUD = False + HAS_DATEUTIL = False class Hcloud: @@ -19,8 +26,10 @@ def __init__(self, module, represent): self.module = module self.represent = represent self.result = {"changed": False, self.represent: None} - if not HAS_HCLOUD: - module.fail_json(msg=missing_required_lib("hcloud-python")) + if not HAS_REQUESTS: + module.fail_json(msg=missing_required_lib("requests")) + if not HAS_DATEUTIL: + module.fail_json(msg=missing_required_lib("python-dateutil")) self._build_client() def _build_client(self): diff --git a/plugins/module_utils/vendor/__init__.py b/plugins/module_utils/vendor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/__init__.py b/plugins/module_utils/vendor/hcloud/__init__.py new file mode 100644 index 00000000..592ff64d --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/__init__.py @@ -0,0 +1,2 @@ +from ._exceptions import APIException, HCloudException # noqa +from .hcloud import Client # noqa diff --git a/plugins/module_utils/vendor/hcloud/_exceptions.py b/plugins/module_utils/vendor/hcloud/_exceptions.py new file mode 100644 index 00000000..36fee5de --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/_exceptions.py @@ -0,0 +1,14 @@ +class HCloudException(Exception): + """There was an error while using the hcloud library""" + + +class APIException(HCloudException): + """There was an error while performing an API Request""" + + def __init__(self, code, message, details): + self.code = code + self.message = message + self.details = details + + def __str__(self): + return self.message diff --git a/plugins/module_utils/vendor/hcloud/_version.py b/plugins/module_utils/vendor/hcloud/_version.py new file mode 100644 index 00000000..0c992210 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/_version.py @@ -0,0 +1 @@ +VERSION = "1.24.0" # x-release-please-version diff --git a/plugins/module_utils/vendor/hcloud/actions/__init__.py b/plugins/module_utils/vendor/hcloud/actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/actions/client.py b/plugins/module_utils/vendor/hcloud/actions/client.py new file mode 100644 index 00000000..2186b656 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/actions/client.py @@ -0,0 +1,90 @@ +import time + +from ..core.client import BoundModelBase, ClientEntityBase +from .domain import Action, ActionFailedException, ActionTimeoutException + + +class BoundAction(BoundModelBase): + model = Action + + def wait_until_finished(self, max_retries=100): + """Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks) + + :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. + """ + while self.status == Action.STATUS_RUNNING: + if max_retries > 0: + self.reload() + time.sleep(self._client._client.poll_interval) + max_retries = max_retries - 1 + else: + raise ActionTimeoutException(action=self) + + if self.status == Action.STATUS_ERROR: + raise ActionFailedException(action=self) + + +class ActionsClient(ClientEntityBase): + results_list_attribute_name = "actions" + + def get_by_id(self, id): + # type: (int) -> BoundAction + """Get a specific action by its ID. + + :param id: int + :return: :class:`BoundAction ` + """ + + response = self._client.request(url=f"/actions/{id}", method="GET") + return BoundAction(self, response["action"]) + + def get_list( + self, + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundAction]] + """Get a list of actions from this account + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/actions", method="GET", params=params) + actions = [ + BoundAction(self, action_data) for action_data in response["actions"] + ] + return self._add_meta_to_result(actions, response) + + def get_all(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Get all actions of the account + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :return: List[:class:`BoundAction `] + """ + return super().get_all(status=status, sort=sort) diff --git a/plugins/module_utils/vendor/hcloud/actions/domain.py b/plugins/module_utils/vendor/hcloud/actions/domain.py new file mode 100644 index 00000000..7972ba6a --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/actions/domain.py @@ -0,0 +1,75 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from .._exceptions import HCloudException +from ..core.domain import BaseDomain + + +class Action(BaseDomain): + """Action Domain + + :param id: int ID of an action + :param command: Command executed in the action + :param status: Status of the action + :param progress: Progress of action in percent + :param started: Point in time when the action was started + :param datetime,None finished: Point in time when the action was finished. Only set if the action is finished otherwise None + :param resources: Resources the action relates to + :param error: Error message for the action if error occurred, otherwise None. + """ + + STATUS_RUNNING = "running" + """Action Status running""" + STATUS_SUCCESS = "success" + """Action Status success""" + STATUS_ERROR = "error" + """Action Status error""" + + __slots__ = ( + "id", + "command", + "status", + "progress", + "resources", + "error", + "started", + "finished", + ) + + def __init__( + self, + id, + command=None, + status=None, + progress=None, + started=None, + finished=None, + resources=None, + error=None, + ): + self.id = id + self.command = command + + self.status = status + self.progress = progress + self.started = isoparse(started) if started else None + self.finished = isoparse(finished) if finished else None + self.resources = resources + self.error = error + + +class ActionException(HCloudException): + """A generic action exception""" + + def __init__(self, action): + self.action = action + + +class ActionFailedException(ActionException): + """The Action you were waiting for failed""" + + +class ActionTimeoutException(ActionException): + """The Action you were waiting for timed out""" diff --git a/plugins/module_utils/vendor/hcloud/certificates/__init__.py b/plugins/module_utils/vendor/hcloud/certificates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/certificates/client.py b/plugins/module_utils/vendor/hcloud/certificates/client.py new file mode 100644 index 00000000..1ab18bdc --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/certificates/client.py @@ -0,0 +1,316 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import ( + Certificate, + CreateManagedCertificateResponse, + ManagedCertificateError, + ManagedCertificateStatus, +) + + +class BoundCertificate(BoundModelBase): + model = Certificate + + def __init__(self, client, data, complete=True): + status = data.get("status") + if status is not None: + error_data = status.get("error") + error = None + if error_data: + error = ManagedCertificateError( + code=error_data["code"], message=error_data["message"] + ) + data["status"] = ManagedCertificateStatus( + issuance=status["issuance"], renewal=status["renewal"], error=error + ) + super().__init__(client, data, complete) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + """Returns all action objects for a Certificate. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Certificate. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]]) -> BoundCertificate + """Updates an certificate. You can update an certificate name and the certificate labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate + """ + return self._client.update(self, name, labels) + + def delete(self): + # type: () -> bool + """Deletes a certificate. + :return: boolean + """ + return self._client.delete(self) + + def retry_issuance(self): + # type: () -> BoundAction + """Retry a failed Certificate issuance or renewal. + :return: BoundAction + """ + return self._client.retry_issuance(self) + + +class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "certificates" + + def get_by_id(self, id): + # type: (int) -> BoundCertificate + """Get a specific certificate by its ID. + + :param id: int + :return: :class:`BoundCertificate ` + """ + response = self._client.request(url=f"/certificates/{id}", method="GET") + return BoundCertificate(self, response["certificate"]) + + def get_list( + self, + name=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundCertificate], Meta] + """Get a list of certificates + + :param name: str (optional) + Can be used to filter certificates by their name. + :param label_selector: str (optional) + Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundCertificate `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + + if label_selector is not None: + params["label_selector"] = label_selector + + if page is not None: + params["page"] = page + + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/certificates", method="GET", params=params + ) + + certificates = [ + BoundCertificate(self, certificate_data) + for certificate_data in response["certificates"] + ] + + return self._add_meta_to_result(certificates, response) + + def get_all(self, name=None, label_selector=None): + # type: (Optional[str], Optional[str]) -> List[BoundCertificate] + """Get all certificates + + :param name: str (optional) + Can be used to filter certificates by their name. + :param label_selector: str (optional) + Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. + :return: List[:class:`BoundCertificate `] + """ + return super().get_all(name=name, label_selector=label_selector) + + def get_by_name(self, name): + # type: (str) -> BoundCertificate + """Get certificate by name + + :param name: str + Used to get certificate by name. + :return: :class:`BoundCertificate ` + """ + return super().get_by_name(name) + + def create(self, name, certificate, private_key, labels=None): + # type: (str, str, str, Optional[Dict[str, str]]) -> BoundCertificate + """Creates a new Certificate with the given name, certificate and private_key. This methods allows only creating + custom uploaded certificates. If you want to create a managed certificate use :func:`~hcloud.certificates.client.CertificatesClient.create_managed` + + :param name: str + :param certificate: str + Certificate and chain in PEM format, in order so that each record directly certifies the one preceding + :param private_key: str + Certificate key in PEM format + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate ` + """ + data = { + "name": name, + "certificate": certificate, + "private_key": private_key, + "type": Certificate.TYPE_UPLOADED, + } + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/certificates", method="POST", json=data) + return BoundCertificate(self, response["certificate"]) + + def create_managed(self, name, domain_names, labels=None): + # type: (str, List[str], Optional[Dict[str, str]]) -> CreateManagedCertificateResponse + """Creates a new managed Certificate with the given name and domain names. This methods allows only creating + managed certificates for domains that are using the Hetzner DNS service. If you want to create a custom uploaded certificate use :func:`~hcloud.certificates.client.CertificatesClient.create` + + :param name: str + :param domain_names: List[str] + Domains and subdomains that should be contained in the Certificate + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate ` + """ + data = { + "name": name, + "type": Certificate.TYPE_MANAGED, + "domain_names": domain_names, + } + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/certificates", method="POST", json=data) + return CreateManagedCertificateResponse( + certificate=BoundCertificate(self, response["certificate"]), + action=BoundAction(self._client.actions, response["action"]), + ) + + def update(self, certificate, name=None, labels=None): + # type: (Certificate, Optional[str], Optional[Dict[str, str]]) -> BoundCertificate + """Updates a Certificate. You can update a certificate name and labels. + + :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundCertificate ` + """ + data = {} + if name is not None: + data["name"] = name + if labels is not None: + data["labels"] = labels + response = self._client.request( + url=f"/certificates/{certificate.id}", + method="PUT", + json=data, + ) + return BoundCertificate(self, response["certificate"]) + + def delete(self, certificate): + # type: (Certificate) -> bool + self._client.request( + url=f"/certificates/{certificate.id}", + method="DELETE", + ) + """Deletes a certificate. + + :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` + :return: True + """ + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def get_actions_list( + self, certificate, status=None, sort=None, page=None, per_page=None + ): + # type: (Certificate, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/certificates/{certificate_id}/actions".format( + certificate_id=certificate.id + ), + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions(self, certificate, status=None, sort=None): + # type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return super().get_actions(certificate, status=status, sort=sort) + + def retry_issuance(self, certificate): + # type: (Certificate) -> BoundAction + """Returns all action objects for a Certificate. + + :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/certificates/{certificate_id}/actions/retry".format( + certificate_id=certificate.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/certificates/domain.py b/plugins/module_utils/vendor/hcloud/certificates/domain.py new file mode 100644 index 00000000..de03f52c --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/certificates/domain.py @@ -0,0 +1,120 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Certificate(BaseDomain, DomainIdentityMixin): + """Certificate Domain + + :param id: int ID of Certificate + :param name: str Name of Certificate + :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding + :param not_valid_before: datetime + Point in time when the Certificate becomes valid + :param not_valid_after: datetime + Point in time when the Certificate becomes invalid + :param domain_names: List[str] List of domains and subdomains covered by this certificate + :param fingerprint: str Fingerprint of the Certificate + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the certificate was created + :param type: str Type of Certificate + :param status: ManagedCertificateStatus Current status of a type managed Certificate, always none for type uploaded Certificates + """ + + __slots__ = ( + "id", + "name", + "certificate", + "not_valid_before", + "not_valid_after", + "domain_names", + "fingerprint", + "created", + "labels", + "type", + "status", + ) + TYPE_UPLOADED = "uploaded" + TYPE_MANAGED = "managed" + + def __init__( + self, + id=None, + name=None, + certificate=None, + not_valid_before=None, + not_valid_after=None, + domain_names=None, + fingerprint=None, + created=None, + labels=None, + type=None, + status=None, + ): + self.id = id + self.name = name + self.type = type + self.certificate = certificate + self.domain_names = domain_names + self.fingerprint = fingerprint + self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None + self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None + self.created = isoparse(created) if created else None + self.labels = labels + self.status = status + + +class ManagedCertificateStatus(BaseDomain): + """ManagedCertificateStatus Domain + + :param issuance: str + Status of the issuance process of the Certificate + :param renewal: str + Status of the renewal process of the Certificate + :param error: ManagedCertificateError + If issuance or renewal reports failure, this property contains information about what happened + """ + + def __init__(self, issuance=None, renewal=None, error=None): + self.issuance = issuance + self.renewal = renewal + self.error = error + + +class ManagedCertificateError(BaseDomain): + """ManagedCertificateError Domain + + :param code: str + Error code identifying the error + :param message: + Message detailing the error + """ + + def __init__(self, code=None, message=None): + self.code = code + self.message = message + + +class CreateManagedCertificateResponse(BaseDomain): + """Create Managed Certificate Response Domain + + :param certificate: :class:`BoundCertificate ` + The created server + :param action: :class:`BoundAction ` + Shows the progress of the certificate creation + """ + + __slots__ = ("certificate", "action") + + def __init__( + self, + certificate, # type: BoundCertificate + action, # type: BoundAction + ): + self.certificate = certificate + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/core/__init__.py b/plugins/module_utils/vendor/hcloud/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/core/client.py b/plugins/module_utils/vendor/hcloud/core/client.py new file mode 100644 index 00000000..10a2115f --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/core/client.py @@ -0,0 +1,127 @@ +from .domain import add_meta_to_result + + +class ClientEntityBase: + max_per_page = 50 + results_list_attribute_name = None + + def __init__(self, client): + """ + :param client: Client + :return self + """ + self._client = client + + def _is_list_attribute_implemented(self): + if self.results_list_attribute_name is None: + raise NotImplementedError( + "in order to get results list, 'results_list_attribute_name' attribute of {} has to be specified".format( + self.__class__.__name__ + ) + ) + + def _add_meta_to_result( + self, + results, # type: List[BoundModelBase] + response, # type: json + ): + # type: (...) -> PageResult + self._is_list_attribute_implemented() + return add_meta_to_result(results, response, self.results_list_attribute_name) + + def _get_all( + self, + list_function, # type: function + results_list_attribute_name, # type: str + *args, + **kwargs + ): + # type (...) -> List[BoundModelBase] + + page = 1 + + results = [] + + while page: + page_result = list_function( + page=page, per_page=self.max_per_page, *args, **kwargs + ) + result = getattr(page_result, results_list_attribute_name) + if result: + results.extend(result) + meta = page_result.meta + if ( + meta + and meta.pagination + and meta.pagination.next_page + and meta.pagination.next_page + ): + page = meta.pagination.next_page + else: + page = None + + return results + + def get_all(self, *args, **kwargs): + # type: (...) -> List[BoundModelBase] + self._is_list_attribute_implemented() + return self._get_all( + self.get_list, self.results_list_attribute_name, *args, **kwargs + ) + + def get_actions(self, *args, **kwargs): + # type: (...) -> List[BoundModelBase] + if not hasattr(self, "get_actions_list"): + raise ValueError("this endpoint does not support get_actions method") + + return self._get_all(self.get_actions_list, "actions", *args, **kwargs) + + +class GetEntityByNameMixin: + """ + Use as a mixin for ClientEntityBase classes + """ + + def get_by_name(self, name): + # type: (str) -> BoundModelBase + self._is_list_attribute_implemented() + response = self.get_list(name=name) + entities = getattr(response, self.results_list_attribute_name) + entity = entities[0] if entities else None + return entity + + +class BoundModelBase: + """Bound Model Base""" + + model = None + + def __init__(self, client, data={}, complete=True): + """ + :param client: + The client for the specific model to use + :param data: + The data of the model + :param complete: bool + False if not all attributes of the model fetched + """ + self._client = client + self.complete = complete + self.data_model = self.model.from_dict(data) + + def __getattr__(self, name): + """Allow magical access to the properties of the model + :param name: str + :return: + """ + value = getattr(self.data_model, name) + if not value and not self.complete: + self.reload() + value = getattr(self.data_model, name) + return value + + def reload(self): + """Reloads the model and tries to get all data from the APIx""" + bound_model = self._client.get_by_id(self.data_model.id) + self.data_model = bound_model.data_model + self.complete = True diff --git a/plugins/module_utils/vendor/hcloud/core/domain.py b/plugins/module_utils/vendor/hcloud/core/domain.py new file mode 100644 index 00000000..8d99f63a --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/core/domain.py @@ -0,0 +1,75 @@ +from collections import namedtuple + + +class BaseDomain: + __slots__ = () + + @classmethod + def from_dict(cls, data): + supported_data = {k: v for k, v in data.items() if k in cls.__slots__} + return cls(**supported_data) + + +class DomainIdentityMixin: + __slots__ = () + + @property + def id_or_name(self): + if self.id is not None: + return self.id + elif self.name is not None: + return self.name + else: + raise ValueError("id or name must be set") + + +class Pagination(BaseDomain): + __slots__ = ( + "page", + "per_page", + "previous_page", + "next_page", + "last_page", + "total_entries", + ) + + def __init__( + self, + page, + per_page, + previous_page=None, + next_page=None, + last_page=None, + total_entries=None, + ): + self.page = page + self.per_page = per_page + self.previous_page = previous_page + self.next_page = next_page + self.last_page = last_page + self.total_entries = total_entries + + +class Meta(BaseDomain): + __slots__ = ("pagination",) + + def __init__(self, pagination=None): + self.pagination = pagination + + @classmethod + def parse_meta(cls, json_content): + meta = None + if json_content and "meta" in json_content: + meta = cls() + pagination_json = json_content["meta"].get("pagination") + if pagination_json: + pagination = Pagination(**pagination_json) + meta.pagination = pagination + return meta + + +def add_meta_to_result(result, json_content, attr_name): + # type: (List[BoundModelBase], json, string) -> PageResult + class_name = f"PageResults{attr_name.capitalize()}" + PageResults = namedtuple(class_name, [attr_name, "meta"]) + return PageResults(**{attr_name: result, "meta": Meta.parse_meta(json_content)}) diff --git a/plugins/module_utils/vendor/hcloud/datacenters/__init__.py b/plugins/module_utils/vendor/hcloud/datacenters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/datacenters/client.py b/plugins/module_utils/vendor/hcloud/datacenters/client.py new file mode 100644 index 00000000..0ef212b4 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/datacenters/client.py @@ -0,0 +1,111 @@ +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..locations.client import BoundLocation +from ..server_types.client import BoundServerType +from .domain import Datacenter, DatacenterServerTypes + + +class BoundDatacenter(BoundModelBase): + model = Datacenter + + def __init__(self, client, data): + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + server_types = data.get("server_types") + if server_types is not None: + available = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["available"] + ] + supported = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["supported"] + ] + available_for_migration = [ + BoundServerType( + client._client.server_types, {"id": server_type}, complete=False + ) + for server_type in server_types["available_for_migration"] + ] + data["server_types"] = DatacenterServerTypes( + available=available, + supported=supported, + available_for_migration=available_for_migration, + ) + + super().__init__(client, data) + + +class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "datacenters" + + def get_by_id(self, id): + # type: (int) -> BoundDatacenter + """Get a specific datacenter by its ID. + + :param id: int + :return: :class:`BoundDatacenter ` + """ + response = self._client.request(url=f"/datacenters/{id}", method="GET") + return BoundDatacenter(self, response["datacenter"]) + + def get_list( + self, + name=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundDatacenter], Meta] + """Get a list of datacenters + + :param name: str (optional) + Can be used to filter datacenters by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundDatacenter `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + + if page is not None: + params["page"] = page + + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/datacenters", method="GET", params=params) + + datacenters = [ + BoundDatacenter(self, datacenter_data) + for datacenter_data in response["datacenters"] + ] + + return self._add_meta_to_result(datacenters, response) + + def get_all(self, name=None): + # type: (Optional[str]) -> List[BoundDatacenter] + """Get all datacenters + + :param name: str (optional) + Can be used to filter datacenters by their name. + :return: List[:class:`BoundDatacenter `] + """ + return super().get_all(name=name) + + def get_by_name(self, name): + # type: (str) -> BoundDatacenter + """Get datacenter by name + + :param name: str + Used to get datacenter by name. + :return: :class:`BoundDatacenter ` + """ + return super().get_by_name(name) diff --git a/plugins/module_utils/vendor/hcloud/datacenters/domain.py b/plugins/module_utils/vendor/hcloud/datacenters/domain.py new file mode 100644 index 00000000..984cc854 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/datacenters/domain.py @@ -0,0 +1,42 @@ +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Datacenter(BaseDomain, DomainIdentityMixin): + """Datacenter Domain + + :param id: int ID of Datacenter + :param name: str Name of Datacenter + :param description: str Description of Datacenter + :param location: :class:`BoundLocation ` + :param server_types: :class:`DatacenterServerTypes ` + """ + + __slots__ = ("id", "name", "description", "location", "server_types") + + def __init__( + self, id=None, name=None, description=None, location=None, server_types=None + ): + self.id = id + self.name = name + self.description = description + self.location = location + self.server_types = server_types + + +class DatacenterServerTypes: + """DatacenterServerTypes Domain + + :param available: List[:class:`BoundServerTypes `] + All available server types for this datacenter + :param supported: List[:class:`BoundServerTypes `] + All supported server types for this datacenter + :param available_for_migration: List[:class:`BoundServerTypes `] + All available for migration (change type) server types for this datacenter + """ + + __slots__ = ("available", "supported", "available_for_migration") + + def __init__(self, available, supported, available_for_migration): + self.available = available + self.supported = supported + self.available_for_migration = available_for_migration diff --git a/plugins/module_utils/vendor/hcloud/deprecation/__init__.py b/plugins/module_utils/vendor/hcloud/deprecation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/deprecation/domain.py b/plugins/module_utils/vendor/hcloud/deprecation/domain.py new file mode 100644 index 00000000..26d4aa73 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/deprecation/domain.py @@ -0,0 +1,34 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class DeprecationInfo(BaseDomain): + """Describes if, when & how the resources was deprecated. If this field is set to ``None`` the resource is not + deprecated. If it has a value, it is considered deprecated. + + :param announced: datetime + Date of when the deprecation was announced. + :param unavailable_after: datetime + After the time in this field, the resource will not be available from the general listing endpoint of the + resource type, and it can not be used in new resources. For example, if this is an image, you can not create + new servers with this image after the mentioned date. + """ + + __slots__ = ( + "announced", + "unavailable_after", + ) + + def __init__( + self, + announced=None, + unavailable_after=None, + ): + self.announced = isoparse(announced) if announced else None + self.unavailable_after = ( + isoparse(unavailable_after) if unavailable_after else None + ) diff --git a/plugins/module_utils/vendor/hcloud/firewalls/__init__.py b/plugins/module_utils/vendor/hcloud/firewalls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/firewalls/client.py b/plugins/module_utils/vendor/hcloud/firewalls/client.py new file mode 100644 index 00000000..5ff716d3 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/firewalls/client.py @@ -0,0 +1,416 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import ( + CreateFirewallResponse, + Firewall, + FirewallResource, + FirewallResourceLabelSelector, + FirewallRule, +) + + +class BoundFirewall(BoundModelBase): + model = Firewall + + def __init__(self, client, data, complete=True): + rules = data.get("rules", []) + if rules: + rules = [ + FirewallRule( + direction=rule["direction"], + source_ips=rule["source_ips"], + destination_ips=rule["destination_ips"], + protocol=rule["protocol"], + port=rule["port"], + description=rule["description"], + ) + for rule in rules + ] + data["rules"] = rules + + applied_to = data.get("applied_to", []) + if applied_to: + from ..servers.client import BoundServer + + ats = [] + for a in applied_to: + if a["type"] == FirewallResource.TYPE_SERVER: + ats.append( + FirewallResource( + type=a["type"], + server=BoundServer( + client._client.servers, a["server"], complete=False + ), + ) + ) + elif a["type"] == FirewallResource.TYPE_LABEL_SELECTOR: + ats.append( + FirewallResource( + type=a["type"], + label_selector=FirewallResourceLabelSelector( + selector=a["label_selector"]["selector"] + ), + ) + ) + data["applied_to"] = ats + + super().__init__(client, data, complete) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] + """Returns all action objects for a Firewall. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Firewall. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFirewall + """Updates the name or labels of a Firewall. + + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundFirewall ` + """ + return self._client.update(self, labels, name) + + def delete(self): + # type: () -> bool + """Deletes a Firewall. + + :return: boolean + """ + return self._client.delete(self) + + def set_rules(self, rules): + # type: (List[FirewallRule]) -> List[BoundAction] + """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. + :param rules: List[:class:`FirewallRule `] + :return: List[:class:`BoundAction `] + """ + + return self._client.set_rules(self, rules) + + def apply_to_resources(self, resources): + # type: (List[FirewallResource]) -> List[BoundAction] + """Applies one Firewall to multiple resources. + :param resources: List[:class:`FirewallResource `] + :return: List[:class:`BoundAction `] + """ + return self._client.apply_to_resources(self, resources) + + def remove_from_resources(self, resources): + # type: (List[FirewallResource]) -> List[BoundAction] + """Removes one Firewall from multiple resources. + :param resources: List[:class:`FirewallResource `] + :return: List[:class:`BoundAction `] + """ + return self._client.remove_from_resources(self, resources) + + +class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "firewalls" + + def get_actions_list( + self, + firewall, # type: Firewall + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a Firewall. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url=f"/firewalls/{firewall.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions( + self, + firewall, # type: Firewall + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + ): + # type: (...) -> List[BoundAction] + """Returns all action objects for a Firewall. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction `] + """ + return super().get_actions(firewall, status=status, sort=sort) + + def get_by_id(self, id): + # type: (int) -> BoundFirewall + """Returns a specific Firewall object. + + :param id: int + :return: :class:`BoundFirewall ` + """ + response = self._client.request(url=f"/firewalls/{id}", method="GET") + return BoundFirewall(self, response["firewall"]) + + def get_list( + self, + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + name=None, # type: Optional[str] + sort=None, # type: Optional[List[str]] + ): + # type: (...) -> PageResults[List[BoundFirewall]] + """Get a list of floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: (List[:class:`BoundFirewall `], :class:`Meta `) + """ + params = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if sort is not None: + params["sort"] = sort + response = self._client.request(url="/firewalls", method="GET", params=params) + firewalls = [ + BoundFirewall(self, firewall_data) + for firewall_data in response["firewalls"] + ] + + return self._add_meta_to_result(firewalls, response) + + def get_all(self, label_selector=None, name=None, sort=None): + # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall] + """Get all floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. + :param name: str (optional) + Can be used to filter networks by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: List[:class:`BoundFirewall `] + """ + return super().get_all(label_selector=label_selector, name=name, sort=sort) + + def get_by_name(self, name): + # type: (str) -> BoundFirewall + """Get Firewall by name + + :param name: str + Used to get Firewall by name. + :return: :class:`BoundFirewall ` + """ + return super().get_by_name(name) + + def create( + self, + name, # type: str + rules=None, # type: Optional[List[FirewallRule]] + labels=None, # type: Optional[str] + resources=None, # type: Optional[List[FirewallResource]] + ): + # type: (...) -> CreateFirewallResponse + """Creates a new Firewall. + + :param name: str + Firewall Name + :param rules: List[:class:`FirewallRule `] (optional) + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param resources: List[:class:`FirewallResource `] (optional) + :return: :class:`CreateFirewallResponse ` + """ + + data = {"name": name} + if labels is not None: + data["labels"] = labels + + if rules is not None: + data.update({"rules": []}) + for rule in rules: + data["rules"].append(rule.to_payload()) + if resources is not None: + data.update({"apply_to": []}) + for resource in resources: + data["apply_to"].append(resource.to_payload()) + response = self._client.request(url="/firewalls", json=data, method="POST") + + actions = [] + if response.get("actions") is not None: + actions = [ + BoundAction(self._client.actions, _) for _ in response["actions"] + ] + + result = CreateFirewallResponse( + firewall=BoundFirewall(self, response["firewall"]), actions=actions + ) + return result + + def update(self, firewall, labels=None, name=None): + # type: (Firewall, Optional[Dict[str, str]], Optional[str]) -> BoundFirewall + """Updates the description or labels of a Firewall. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundFirewall ` + """ + data = {} + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/firewalls/{firewall.id}", + method="PUT", + json=data, + ) + return BoundFirewall(self, response["firewall"]) + + def delete(self, firewall): + # type: (Firewall) -> bool + """Deletes a Firewall. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :return: boolean + """ + self._client.request( + url=f"/firewalls/{firewall.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def set_rules(self, firewall, rules): + # type: (Firewall, List[FirewallRule]) -> List[BoundAction] + """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param rules: List[:class:`FirewallRule `] + :return: List[:class:`BoundAction `] + """ + data = {"rules": []} + for rule in rules: + data["rules"].append(rule.to_payload()) + response = self._client.request( + url="/firewalls/{firewall_id}/actions/set_rules".format( + firewall_id=firewall.id + ), + method="POST", + json=data, + ) + return [BoundAction(self._client.actions, _) for _ in response["actions"]] + + def apply_to_resources(self, firewall, resources): + # type: (Firewall, List[FirewallResource]) -> List[BoundAction] + """Applies one Firewall to multiple resources. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param resources: List[:class:`FirewallResource `] + :return: List[:class:`BoundAction `] + """ + data = {"apply_to": []} + for resource in resources: + data["apply_to"].append(resource.to_payload()) + response = self._client.request( + url="/firewalls/{firewall_id}/actions/apply_to_resources".format( + firewall_id=firewall.id + ), + method="POST", + json=data, + ) + return [BoundAction(self._client.actions, _) for _ in response["actions"]] + + def remove_from_resources(self, firewall, resources): + # type: (Firewall, List[FirewallResource]) -> List[BoundAction] + """Removes one Firewall from multiple resources. + + :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` + :param resources: List[:class:`FirewallResource `] + :return: List[:class:`BoundAction `] + """ + data = {"remove_from": []} + for resource in resources: + data["remove_from"].append(resource.to_payload()) + response = self._client.request( + url="/firewalls/{firewall_id}/actions/remove_from_resources".format( + firewall_id=firewall.id + ), + method="POST", + json=data, + ) + return [BoundAction(self._client.actions, _) for _ in response["actions"]] diff --git a/plugins/module_utils/vendor/hcloud/firewalls/domain.py b/plugins/module_utils/vendor/hcloud/firewalls/domain.py new file mode 100644 index 00000000..37e5cd53 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/firewalls/domain.py @@ -0,0 +1,180 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class Firewall(BaseDomain): + """Firewall Domain + + :param id: int + ID of the Firewall + :param name: str + Name of the Firewall + :param labels: dict + User-defined labels (key-value pairs) + :param rules: List[:class:`FirewallRule `] + Rules of the Firewall + :param applied_to: List[:class:`FirewallResource `] + Resources currently using the Firewall + :param created: datetime + Point in time when the image was created + """ + + __slots__ = ("id", "name", "labels", "rules", "applied_to", "created") + + def __init__( + self, id=None, name=None, labels=None, rules=None, applied_to=None, created=None + ): + self.id = id + self.name = name + self.rules = rules + self.applied_to = applied_to + self.labels = labels + self.created = isoparse(created) if created else None + + +class FirewallRule: + """Firewall Rule Domain + + :param direction: str + The Firewall which was created + :param port: str + Port to which traffic will be allowed, only applicable for protocols TCP and UDP, specify port ranges by using + - as a indicator, Sample: 80-85 means all ports between 80 & 85 (80, 82, 83, 84, 85) + :param protocol: str + Select traffic direction on which rule should be applied. Use source_ips for direction in and destination_ips for direction out. + :param source_ips: List[str] + List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. + :param destination_ips: List[str] + List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. + :param description: str + Short description of the firewall rule + """ + + __slots__ = ( + "direction", + "port", + "protocol", + "source_ips", + "destination_ips", + "description", + ) + + DIRECTION_IN = "in" + """Firewall Rule Direction In""" + DIRECTION_OUT = "out" + """Firewall Rule Direction Out""" + + PROTOCOL_UDP = "udp" + """Firewall Rule Protocol UDP""" + PROTOCOL_ICMP = "icmp" + """Firewall Rule Protocol ICMP""" + PROTOCOL_TCP = "tcp" + """Firewall Rule Protocol TCP""" + PROTOCOL_ESP = "esp" + """Firewall Rule Protocol ESP""" + PROTOCOL_GRE = "gre" + """Firewall Rule Protocol GRE""" + + def __init__( + self, + direction, # type: str + protocol, # type: str + source_ips, # type: List[str] + port=None, # type: Optional[str] + destination_ips=None, # type: Optional[List[str]] + description=None, # type: Optional[str] + ): + self.direction = direction + self.port = port + self.protocol = protocol + self.source_ips = source_ips + self.destination_ips = destination_ips or [] + self.description = description + + def to_payload(self): + payload = { + "direction": self.direction, + "protocol": self.protocol, + "source_ips": self.source_ips, + } + if len(self.destination_ips) > 0: + payload.update({"destination_ips": self.destination_ips}) + if self.port is not None: + payload.update({"port": self.port}) + if self.description is not None: + payload.update({"description": self.description}) + return payload + + +class FirewallResource: + """Firewall Used By Domain + + :param type: str + Type of resource referenced + :param server: Optional[Server] + Server the Firewall is applied to + :param label_selector: Optional[FirewallResourceLabelSelector] + Label Selector for Servers the Firewall should be applied to + """ + + __slots__ = ("type", "server", "label_selector") + + TYPE_SERVER = "server" + """Firewall Used By Type Server""" + TYPE_LABEL_SELECTOR = "label_selector" + """Firewall Used By Type label_selector""" + + def __init__( + self, + type, # type: str + server=None, # type: Optional[Server] + label_selector=None, # type: Optional[FirewallResourceLabelSelector] + ): + self.type = type + self.server = server + self.label_selector = label_selector + + def to_payload(self): + payload = {"type": self.type} + if self.server is not None: + payload.update({"server": {"id": self.server.id}}) + + if self.label_selector is not None: + payload.update( + {"label_selector": {"selector": self.label_selector.selector}} + ) + return payload + + +class FirewallResourceLabelSelector(BaseDomain): + """FirewallResourceLabelSelector Domain + + :param selector: str Target label selector + """ + + def __init__(self, selector=None): + self.selector = selector + + +class CreateFirewallResponse(BaseDomain): + """Create Firewall Response Domain + + :param firewall: :class:`BoundFirewall ` + The Firewall which was created + :param actions: List[:class:`BoundAction `] + The Action which shows the progress of the Firewall Creation + """ + + __slots__ = ("firewall", "actions") + + def __init__( + self, + firewall, # type: BoundFirewall + actions, # type: BoundAction + ): + self.firewall = firewall + self.actions = actions diff --git a/plugins/module_utils/vendor/hcloud/floating_ips/__init__.py b/plugins/module_utils/vendor/hcloud/floating_ips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/floating_ips/client.py b/plugins/module_utils/vendor/hcloud/floating_ips/client.py new file mode 100644 index 00000000..d5d7a16e --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/floating_ips/client.py @@ -0,0 +1,422 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from ..locations.client import BoundLocation +from .domain import CreateFloatingIPResponse, FloatingIP + + +class BoundFloatingIP(BoundModelBase): + model = FloatingIP + + def __init__(self, client, data, complete=True): + from ..servers.client import BoundServer + + server = data.get("server") + if server is not None: + data["server"] = BoundServer( + client._client.servers, {"id": server}, complete=False + ) + + home_location = data.get("home_location") + if home_location is not None: + data["home_location"] = BoundLocation( + client._client.locations, home_location + ) + + super().__init__(client, data, complete) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] + """Returns all action objects for a Floating IP. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Floating IP. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def update(self, description=None, labels=None, name=None): + # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP + """Updates the description or labels of a Floating IP. + + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundFloatingIP ` + """ + return self._client.update(self, description, labels, name) + + def delete(self): + # type: () -> bool + """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. + + :return: boolean + """ + return self._client.delete(self) + + def change_protection(self, delete=None): + # type: (Optional[bool]) -> BoundAction + """Changes the protection configuration of the Floating IP. + + :param delete: boolean + If true, prevents the Floating IP from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + def assign(self, server): + # type: (Server) -> BoundAction + """Assigns a Floating IP to a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + Server the Floating IP shall be assigned to + :return: :class:`BoundAction ` + """ + return self._client.assign(self, server) + + def unassign(self): + # type: () -> BoundAction + """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :return: :class:`BoundAction ` + """ + return self._client.unassign(self) + + def change_dns_ptr(self, ip, dns_ptr): + # type: (str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + +class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "floating_ips" + + def get_actions_list( + self, + floating_ip, # type: FloatingIP + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url="/floating_ips/{floating_ip_id}/actions".format( + floating_ip_id=floating_ip.id + ), + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions( + self, + floating_ip, # type: FloatingIP + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + ): + # type: (...) -> List[BoundAction] + """Returns all action objects for a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + + :return: List[:class:`BoundAction `] + """ + return super().get_actions(floating_ip, status=status, sort=sort) + + def get_by_id(self, id): + # type: (int) -> BoundFloatingIP + """Returns a specific Floating IP object. + + :param id: int + :return: :class:`BoundFloatingIP ` + """ + response = self._client.request(url=f"/floating_ips/{id}", method="GET") + return BoundFloatingIP(self, response["floating_ip"]) + + def get_list( + self, + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + name=None, # type: Optional[str] + ): + # type: (...) -> PageResults[List[BoundFloatingIP]] + """Get a list of floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :return: (List[:class:`BoundFloatingIP `], :class:`Meta `) + """ + params = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + + response = self._client.request( + url="/floating_ips", method="GET", params=params + ) + floating_ips = [ + BoundFloatingIP(self, floating_ip_data) + for floating_ip_data in response["floating_ips"] + ] + + return self._add_meta_to_result(floating_ips, response) + + def get_all(self, label_selector=None, name=None): + # type: (Optional[str], Optional[str]) -> List[BoundFloatingIP] + """Get all floating ips from this account + + :param label_selector: str (optional) + Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. + :param name: str (optional) + Can be used to filter networks by their name. + :return: List[:class:`BoundFloatingIP `] + """ + return super().get_all(label_selector=label_selector, name=name) + + def get_by_name(self, name): + # type: (str) -> BoundFloatingIP + """Get Floating IP by name + + :param name: str + Used to get Floating IP by name. + :return: :class:`BoundFloatingIP ` + """ + return super().get_by_name(name) + + def create( + self, + type, # type: str + description=None, # type: Optional[str] + labels=None, # type: Optional[str] + home_location=None, # type: Optional[Location] + server=None, # type: Optional[Server] + name=None, # type: Optional[str] + ): + # type: (...) -> CreateFloatingIPResponse + """Creates a new Floating IP assigned to a server. + + :param type: str + Floating IP type Choices: ipv4, ipv6 + :param description: str (optional) + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param home_location: :class:`BoundLocation ` or :class:`Location ` ( + Home location (routing is optimized for that location). Only optional if server argument is passed. + :param server: :class:`BoundServer ` or :class:`Server ` + Server to assign the Floating IP to + :param name: str (optional) + :return: :class:`CreateFloatingIPResponse ` + """ + + data = {"type": type} + if description is not None: + data["description"] = description + if labels is not None: + data["labels"] = labels + if home_location is not None: + data["home_location"] = home_location.id_or_name + if server is not None: + data["server"] = server.id + if name is not None: + data["name"] = name + + response = self._client.request(url="/floating_ips", json=data, method="POST") + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreateFloatingIPResponse( + floating_ip=BoundFloatingIP(self, response["floating_ip"]), action=action + ) + return result + + def update(self, floating_ip, description=None, labels=None, name=None): + # type: (FloatingIP, Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP + """Updates the description or labels of a Floating IP. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundFloatingIP ` + """ + data = {} + if description is not None: + data["description"] = description + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/floating_ips/{floating_ip.id}", + method="PUT", + json=data, + ) + return BoundFloatingIP(self, response["floating_ip"]) + + def delete(self, floating_ip): + # type: (FloatingIP) -> bool + """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :return: boolean + """ + self._client.request( + url=f"/floating_ips/{floating_ip.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def change_protection(self, floating_ip, delete=None): + # type: (FloatingIP, Optional[bool]) -> BoundAction + """Changes the protection configuration of the Floating IP. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param delete: boolean + If true, prevents the Floating IP from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/floating_ips/{floating_ip_id}/actions/change_protection".format( + floating_ip_id=floating_ip.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def assign(self, floating_ip, server): + # type: (FloatingIP, Server) -> BoundAction + """Assigns a Floating IP to a server. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param server: :class:`BoundServer ` or :class:`Server ` + Server the Floating IP shall be assigned to + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/floating_ips/{floating_ip_id}/actions/assign".format( + floating_ip_id=floating_ip.id + ), + method="POST", + json={"server": server.id}, + ) + return BoundAction(self._client.actions, response["action"]) + + def unassign(self, floating_ip): + # type: (FloatingIP) -> BoundAction + """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/floating_ips/{floating_ip_id}/actions/unassign".format( + floating_ip_id=floating_ip.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr(self, floating_ip, ip, dns_ptr): + # type: (FloatingIP, str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to this Floating IP. + + :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/floating_ips/{floating_ip_id}/actions/change_dns_ptr".format( + floating_ip_id=floating_ip.id + ), + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/floating_ips/domain.py b/plugins/module_utils/vendor/hcloud/floating_ips/domain.py new file mode 100644 index 00000000..a1ccfdac --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/floating_ips/domain.py @@ -0,0 +1,99 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class FloatingIP(BaseDomain): + """Floating IP Domain + + :param id: int + ID of the Floating IP + :param description: str, None + Description of the Floating IP + :param ip: str + IP address of the Floating IP + :param type: str + Type of Floating IP. Choices: `ipv4`, `ipv6` + :param server: :class:`BoundServer `, None + Server the Floating IP is assigned to, None if it is not assigned at all + :param dns_ptr: List[Dict] + Array of reverse DNS entries + :param home_location: :class:`BoundLocation ` + Location the Floating IP was created in. Routing is optimized for this location. + :param blocked: boolean + Whether the IP is blocked + :param protection: dict + Protection configuration for the Floating IP + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the Floating IP was created + :param name: str + Name of the Floating IP + """ + + __slots__ = ( + "id", + "type", + "description", + "ip", + "server", + "dns_ptr", + "home_location", + "blocked", + "protection", + "labels", + "name", + "created", + ) + + def __init__( + self, + id=None, + type=None, + description=None, + ip=None, + server=None, + dns_ptr=None, + home_location=None, + blocked=None, + protection=None, + labels=None, + created=None, + name=None, + ): + self.id = id + self.type = type + self.description = description + self.ip = ip + self.server = server + self.dns_ptr = dns_ptr + self.home_location = home_location + self.blocked = blocked + self.protection = protection + self.labels = labels + self.created = isoparse(created) if created else None + self.name = name + + +class CreateFloatingIPResponse(BaseDomain): + """Create Floating IP Response Domain + + :param floating_ip: :class:`BoundFloatingIP ` + The Floating IP which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the Floating IP Creation + """ + + __slots__ = ("floating_ip", "action") + + def __init__( + self, + floating_ip, # type: BoundFloatingIP + action, # type: BoundAction + ): + self.floating_ip = floating_ip + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/hcloud.py b/plugins/module_utils/vendor/hcloud/hcloud.py new file mode 100644 index 00000000..5dc4f1e4 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/hcloud.py @@ -0,0 +1,225 @@ +import time +from typing import Optional, Union + +try: + import requests +except ImportError: + requests = None + +from ._version import VERSION +from ._exceptions import APIException +from .actions.client import ActionsClient +from .certificates.client import CertificatesClient +from .datacenters.client import DatacentersClient +from .firewalls.client import FirewallsClient +from .floating_ips.client import FloatingIPsClient +from .images.client import ImagesClient +from .isos.client import IsosClient +from .load_balancer_types.client import LoadBalancerTypesClient +from .load_balancers.client import LoadBalancersClient +from .locations.client import LocationsClient +from .networks.client import NetworksClient +from .placement_groups.client import PlacementGroupsClient +from .primary_ips.client import PrimaryIPsClient +from .server_types.client import ServerTypesClient +from .servers.client import ServersClient +from .ssh_keys.client import SSHKeysClient +from .volumes.client import VolumesClient + + +class Client: + """Base Client for accessing the Hetzner Cloud API""" + + _version = VERSION + _retry_wait_time = 0.5 + __user_agent_prefix = "hcloud-python" + + def __init__( + self, + token: str, + api_endpoint: str = "https://api.hetzner.cloud/v1", + application_name: Optional[str] = None, + application_version: Optional[str] = None, + poll_interval: int = 1, + ): + """Create an new Client instance + + :param token: Hetzner Cloud API token + :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 + """ + self.token = token + self._api_endpoint = api_endpoint + self._application_name = application_name + self._application_version = application_version + self._requests_session = requests.Session() + self.poll_interval = poll_interval + + self.datacenters = DatacentersClient(self) + """DatacentersClient Instance + + :type: :class:`DatacentersClient ` + """ + self.locations = LocationsClient(self) + """LocationsClient Instance + + :type: :class:`LocationsClient ` + """ + self.servers = ServersClient(self) + """ServersClient Instance + + :type: :class:`ServersClient ` + """ + self.server_types = ServerTypesClient(self) + """ServerTypesClient Instance + + :type: :class:`ServerTypesClient ` + """ + self.volumes = VolumesClient(self) + """VolumesClient Instance + + :type: :class:`VolumesClient ` + """ + self.actions = ActionsClient(self) + """ActionsClient Instance + + :type: :class:`ActionsClient ` + """ + self.images = ImagesClient(self) + """ImagesClient Instance + + :type: :class:`ImagesClient ` + """ + self.isos = IsosClient(self) + """ImagesClient Instance + + :type: :class:`IsosClient ` + """ + self.ssh_keys = SSHKeysClient(self) + """SSHKeysClient Instance + + :type: :class:`SSHKeysClient ` + """ + self.floating_ips = FloatingIPsClient(self) + """FloatingIPsClient Instance + + :type: :class:`FloatingIPsClient ` + """ + self.primary_ips = PrimaryIPsClient(self) + """PrimaryIPsClient Instance + + :type: :class:`PrimaryIPsClient ` + """ + self.networks = NetworksClient(self) + """NetworksClient Instance + + :type: :class:`NetworksClient ` + """ + self.certificates = CertificatesClient(self) + """CertificatesClient Instance + + :type: :class:`CertificatesClient ` + """ + + self.load_balancers = LoadBalancersClient(self) + """LoadBalancersClient Instance + + :type: :class:`LoadBalancersClient ` + """ + + self.load_balancer_types = LoadBalancerTypesClient(self) + """LoadBalancerTypesClient Instance + + :type: :class:`LoadBalancerTypesClient ` + """ + + self.firewalls = FirewallsClient(self) + """FirewallsClient Instance + + :type: :class:`FirewallsClient ` + """ + + self.placement_groups = PlacementGroupsClient(self) + """PlacementGroupsClient Instance + + :type: :class:`PlacementGroupsClient ` + """ + + def _get_user_agent(self) -> str: + """Get the user agent of the hcloud-python instance with the user application name (if specified) + + :return: The user agent of this hcloud-python instance + """ + user_agents = [] + for name, version in [ + (self._application_name, self._application_version), + (self.__user_agent_prefix, self._version), + ]: + if name is not None: + user_agents.append(name if version is None else f"{name}/{version}") + + return " ".join(user_agents) + + def _get_headers(self) -> dict: + headers = { + "User-Agent": self._get_user_agent(), + "Authorization": f"Bearer {self.token}", + } + return headers + + def _raise_exception_from_response(self, response): + raise APIException( + code=response.status_code, + message=response.reason, + details={"content": response.content}, + ) + + def _raise_exception_from_content(self, content: dict): + raise APIException( + code=content["error"]["code"], + message=content["error"]["message"], + details=content["error"]["details"], + ) + + def request( + self, + method: str, + url: str, + tries: int = 1, + **kwargs, + ) -> Union[bytes, 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) + :return: Response + """ + response = self._requests_session.request( + method=method, + url=self._api_endpoint + url, + headers=self._get_headers(), + **kwargs, + ) + + content = response.content + try: + if len(content) > 0: + content = response.json() + except (TypeError, ValueError): + self._raise_exception_from_response(response) + + if not response.ok: + if content: + 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) + else: + self._raise_exception_from_content(content) + else: + self._raise_exception_from_response(response) + + return content diff --git a/plugins/module_utils/vendor/hcloud/helpers/__init__.py b/plugins/module_utils/vendor/hcloud/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/helpers/labels.py b/plugins/module_utils/vendor/hcloud/helpers/labels.py new file mode 100644 index 00000000..515982f0 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/helpers/labels.py @@ -0,0 +1,39 @@ +import re +from typing import Dict + + +class LabelValidator: + KEY_REGEX = re.compile( + r"^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$" + ) + VALUE_REGEX = re.compile( + r"^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)" + ) + + @staticmethod + def validate(labels: Dict[str, str]) -> bool: + """Validates Labels. If you want to know which key/value pair of the dict is not correctly formatted + use :func:`~hcloud.helpers.labels.validate_verbose`. + + :return: bool + """ + for k, v in labels.items(): + if LabelValidator.KEY_REGEX.match(k) is None: + return False + if LabelValidator.VALUE_REGEX.match(v) is None: + return False + return True + + @staticmethod + def validate_verbose(labels: Dict[str, str]) -> (bool, str): + """Validates Labels and returns the corresponding error message if something is wrong. Returns True, + if everything is fine. + + :return: bool, str + """ + for k, v in labels.items(): + if LabelValidator.KEY_REGEX.match(k) is None: + return False, f"label key {k} is not correctly formatted" + if LabelValidator.VALUE_REGEX.match(v) is None: + return False, f"label value {v} (key: {k}) is not correctly formatted" + return True, "" diff --git a/plugins/module_utils/vendor/hcloud/images/__init__.py b/plugins/module_utils/vendor/hcloud/images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/images/client.py b/plugins/module_utils/vendor/hcloud/images/client.py new file mode 100644 index 00000000..30c10af6 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/images/client.py @@ -0,0 +1,354 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import Image + + +class BoundImage(BoundModelBase): + model = Image + + def __init__(self, client, data): + from ..servers.client import BoundServer + + created_from = data.get("created_from") + if created_from is not None: + data["created_from"] = BoundServer( + client._client.servers, created_from, complete=False + ) + bound_to = data.get("bound_to") + if bound_to is not None: + data["bound_to"] = BoundServer( + client._client.servers, {"id": bound_to}, complete=False + ) + + super().__init__(client, data) + + def get_actions_list(self, sort=None, page=None, per_page=None, status=None): + # type: (Optional[List[str]], Optional[int], Optional[int], Optional[List[str]]) -> PageResult[BoundAction, Meta] + """Returns a list of action objects for the image. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list( + self, sort=sort, page=page, per_page=per_page, status=status + ) + + def get_actions(self, sort=None, status=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for the image. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status=status, sort=sort) + + def update(self, description=None, type=None, labels=None): + # type: (Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage + """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. + + :param description: str (optional) + New description of Image + :param type: str (optional) + Destination image type to convert to + Choices: snapshot + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundImage ` + """ + return self._client.update(self, description, type, labels) + + def delete(self): + # type: () -> bool + """Deletes an Image. Only images of type snapshot and backup can be deleted. + + :return: bool + """ + return self._client.delete(self) + + def change_protection(self, delete=None): + # type: (Optional[bool]) -> BoundAction + """Changes the protection configuration of the image. Can only be used on snapshots. + + :param delete: bool + If true, prevents the snapshot from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + +class ImagesClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "images" + + def get_actions_list( + self, + image, # type: Image + sort=None, # type: Optional[List[str]] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + status=None, # type: Optional[List[str]] + ): + # type: (...) -> PageResults[List[BoundAction], Meta] + """Returns a list of action objects for an image. + + :param image: :class:`BoundImage ` or :class:`Image ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if sort is not None: + params["sort"] = sort + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + response = self._client.request( + url=f"/images/{image.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions( + self, + image, # type: Image + sort=None, # type: Optional[List[str]] + status=None, # type: Optional[List[str]] + ): + # type: (...) -> List[BoundAction] + """Returns all action objects for an image. + + :param image: :class:`BoundImage ` or :class:`Image ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) + :return: List[:class:`BoundAction `] + """ + return super().get_actions(image, sort=sort, status=status) + + def get_by_id(self, id): + # type: (int) -> BoundImage + """Get a specific Image + + :param id: int + :return: :class:`BoundImage PageResults[List[BoundImage]] + """Get all images + + :param name: str (optional) + Can be used to filter images by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param bound_to: List[str] (optional) + Server Id linked to the image. Only available for images of type backup + :param type: List[str] (optional) + Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm + :param status: List[str] (optional) + Can be used to filter images by their status. The response will only contain images matching the status. + :param sort: List[str] (optional) + Choices: id id:asc id:desc name name:asc name:desc created created:asc created:desc + :param include_deprecated: bool (optional) + Include deprecated images in the response. Default: False + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundImage `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if bound_to is not None: + params["bound_to"] = bound_to + if type is not None: + params["type"] = type + if architecture is not None: + params["architecture"] = architecture + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if status is not None: + params["status"] = per_page + if include_deprecated is not None: + params["include_deprecated"] = include_deprecated + response = self._client.request(url="/images", method="GET", params=params) + images = [BoundImage(self, image_data) for image_data in response["images"]] + + return self._add_meta_to_result(images, response) + + def get_all( + self, + name=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + bound_to=None, # type: Optional[List[str]] + type=None, # type: Optional[List[str]] + architecture=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + status=None, # type: Optional[List[str]] + include_deprecated=None, # type: Optional[bool] + ): + # type: (...) -> List[BoundImage] + """Get all images + + :param name: str (optional) + Can be used to filter images by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param bound_to: List[str] (optional) + Server Id linked to the image. Only available for images of type backup + :param type: List[str] (optional) + Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm + :param status: List[str] (optional) + Can be used to filter images by their status. The response will only contain images matching the status. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :param include_deprecated: bool (optional) + Include deprecated images in the response. Default: False + :return: List[:class:`BoundImage `] + """ + return super().get_all( + name=name, + label_selector=label_selector, + bound_to=bound_to, + type=type, + architecture=architecture, + sort=sort, + status=status, + include_deprecated=include_deprecated, + ) + + def get_by_name(self, name): + # type: (str) -> BoundImage + """Get image by name + + Deprecated: Use get_by_name_and_architecture instead. + + :param name: str + Used to get image by name. + :return: :class:`BoundImage ` + """ + return super().get_by_name(name) + + def get_by_name_and_architecture(self, name, architecture): + # type: (str, str) -> BoundImage + """Get image by name + + :param name: str + Used to identify the image. + :param architecture: str + Used to identify the image. + :return: :class:`BoundImage ` + """ + response = self.get_list(name=name, architecture=[architecture]) + entities = getattr(response, self.results_list_attribute_name) + entity = entities[0] if entities else None + return entity + + def update(self, image, description=None, type=None, labels=None): + # type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage + """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. + + :param image: :class:`BoundImage ` or :class:`Image ` + :param description: str (optional) + New description of Image + :param type: str (optional) + Destination image type to convert to + Choices: snapshot + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundImage ` + """ + data = {} + if description is not None: + data.update({"description": description}) + if type is not None: + data.update({"type": type}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/images/{image.id}", method="PUT", json=data + ) + return BoundImage(self, response["image"]) + + def delete(self, image): + # type: (Image) -> bool + """Deletes an Image. Only images of type snapshot and backup can be deleted. + + :param :class:`BoundImage ` or :class:`Image ` + :return: bool + """ + self._client.request(url=f"/images/{image.id}", method="DELETE") + # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised + return True + + def change_protection(self, image, delete=None): + # type: (Image, Optional[bool]) -> BoundAction + """Changes the protection configuration of the image. Can only be used on snapshots. + + :param image: :class:`BoundImage ` or :class:`Image ` + :param delete: bool + If true, prevents the snapshot from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/images/{image_id}/actions/change_protection".format( + image_id=image.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/images/domain.py b/plugins/module_utils/vendor/hcloud/images/domain.py new file mode 100644 index 00000000..746acfd2 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/images/domain.py @@ -0,0 +1,124 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Image(BaseDomain, DomainIdentityMixin): + """Image Domain + + :param id: int + ID of the image + :param type: str + Type of the image Choices: `system`, `snapshot`, `backup`, `app` + :param status: str + Whether the image can be used or if it’s still being created Choices: `available`, `creating` + :param name: str, None + Unique identifier of the image. This value is only set for system images. + :param description: str + Description of the image + :param image_size: number, None + Size of the image file in our storage in GB. For snapshot images this is the value relevant for calculating costs for the image. + :param disk_size: number + Size of the disk contained in the image in GB. + :param created: datetime + Point in time when the image was created + :param created_from: :class:`BoundServer `, None + Information about the server the image was created from + :param bound_to: :class:`BoundServer `, None + ID of server the image is bound to. Only set for images of type `backup`. + :param os_flavor: str + Flavor of operating system contained in the image Choices: `ubuntu`, `centos`, `debian`, `fedora`, `unknown` + :param os_version: str, None + Operating system version + :param architecture: str + CPU Architecture that the image is compatible with. Choices: `x86`, `arm` + :param rapid_deploy: bool + Indicates that rapid deploy of the image is available + :param protection: dict + Protection configuration for the image + :param deprecated: datetime, None + Point in time when the image is considered to be deprecated (in ISO-8601 format) + :param labels: Dict + User-defined labels (key-value pairs) + """ + + __slots__ = ( + "id", + "name", + "type", + "description", + "image_size", + "disk_size", + "bound_to", + "os_flavor", + "os_version", + "architecture", + "rapid_deploy", + "created_from", + "status", + "protection", + "labels", + "created", + "deprecated", + ) + + def __init__( + self, + id=None, + name=None, + type=None, + created=None, + description=None, + image_size=None, + disk_size=None, + deprecated=None, + bound_to=None, + os_flavor=None, + os_version=None, + architecture=None, + rapid_deploy=None, + created_from=None, + protection=None, + labels=None, + status=None, + ): + self.id = id + self.name = name + self.type = type + self.created = isoparse(created) if created else None + self.description = description + self.image_size = image_size + self.disk_size = disk_size + self.deprecated = isoparse(deprecated) if deprecated else None + self.bound_to = bound_to + self.os_flavor = os_flavor + self.os_version = os_version + self.architecture = architecture + self.rapid_deploy = rapid_deploy + self.created_from = created_from + self.protection = protection + self.labels = labels + self.status = status + + +class CreateImageResponse(BaseDomain): + """Create Image Response Domain + + :param image: :class:`BoundImage ` + The Image which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the Floating IP Creation + """ + + __slots__ = ("action", "image") + + def __init__( + self, + action, # type: BoundAction + image, # type: BoundImage + ): + self.action = action + self.image = image diff --git a/plugins/module_utils/vendor/hcloud/isos/__init__.py b/plugins/module_utils/vendor/hcloud/isos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/isos/client.py b/plugins/module_utils/vendor/hcloud/isos/client.py new file mode 100644 index 00000000..4ca95282 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/isos/client.py @@ -0,0 +1,118 @@ +from warnings import warn + +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import Iso + + +class BoundIso(BoundModelBase): + model = Iso + + +class IsosClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "isos" + + def get_by_id(self, id): + # type: (int) -> BoundIso + """Get a specific ISO by its id + + :param id: int + :return: :class:`BoundIso ` + """ + response = self._client.request(url=f"/isos/{id}", method="GET") + return BoundIso(self, response["iso"]) + + def get_list( + self, + name=None, # type: Optional[str] + architecture=None, # type: Optional[List[str]] + include_wildcard_architecture=None, # type: Optional[bool] + include_architecture_wildcard=None, # type: Optional[bool] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundIso], Meta] + """Get a list of ISOs + + :param name: str (optional) + 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. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + 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 = {} + if name is not None: + params["name"] = name + if architecture is not None: + params["architecture"] = architecture + if include_architecture_wildcard is not None: + params["include_architecture_wildcard"] = include_architecture_wildcard + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/isos", method="GET", params=params) + isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] + return self._add_meta_to_result(isos, response) + + def get_all( + self, + name=None, # type: Optional[str] + architecture=None, # type: Optional[List[str]] + include_wildcard_architecture=None, # type: Optional[bool] + include_architecture_wildcard=None, # type: Optional[bool] + ): + # type: (...) -> List[BoundIso] + """Get all ISOs + + :param name: str (optional) + 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 super().get_all( + name=name, + architecture=architecture, + include_architecture_wildcard=include_architecture_wildcard, + ) + + def get_by_name(self, name): + # type: (str) -> BoundIso + """Get iso by name + + :param name: str + Used to get iso by name. + :return: :class:`BoundIso ` + """ + return super().get_by_name(name) diff --git a/plugins/module_utils/vendor/hcloud/isos/domain.py b/plugins/module_utils/vendor/hcloud/isos/domain.py new file mode 100644 index 00000000..44311292 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/isos/domain.py @@ -0,0 +1,42 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Iso(BaseDomain, DomainIdentityMixin): + """Iso Domain + + :param id: int + ID of the ISO + :param name: str, None + Unique identifier of the ISO. Only set for public ISOs + :param description: str + Description of the ISO + :param type: str + Type of the ISO. Choices: `public`, `private` + :param architecture: str, None + CPU Architecture that the ISO is compatible with. None means that the compatibility is unknown. Choices: `x86`, `arm` + :param deprecated: datetime, None + ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. + """ + + __slots__ = ("id", "name", "type", "architecture", "description", "deprecated") + + def __init__( + self, + id=None, + name=None, + type=None, + architecture=None, + description=None, + deprecated=None, + ): + self.id = id + self.name = name + self.type = type + self.architecture = architecture + self.description = description + self.deprecated = isoparse(deprecated) if deprecated else None diff --git a/plugins/module_utils/vendor/hcloud/load_balancer_types/__init__.py b/plugins/module_utils/vendor/hcloud/load_balancer_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py b/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py new file mode 100644 index 00000000..405e2b50 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/load_balancer_types/client.py @@ -0,0 +1,74 @@ +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import LoadBalancerType + + +class BoundLoadBalancerType(BoundModelBase): + model = LoadBalancerType + + +class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "load_balancer_types" + + def get_by_id(self, id): + # type: (int) -> load_balancer_types.client.BoundLoadBalancerType + """Returns a specific Load Balancer Type. + + :param id: int + :return: :class:`BoundLoadBalancerType ` + """ + response = self._client.request( + url="/load_balancer_types/{load_balancer_type_id}".format( + load_balancer_type_id=id + ), + method="GET", + ) + return BoundLoadBalancerType(self, response["load_balancer_type"]) + + def get_list(self, name=None, page=None, per_page=None): + # type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundLoadBalancerType], Meta] + """Get a list of Load Balancer types + + :param name: str (optional) + Can be used to filter Load Balancer type by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLoadBalancerType `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/load_balancer_types", method="GET", params=params + ) + load_balancer_types = [ + BoundLoadBalancerType(self, load_balancer_type_data) + for load_balancer_type_data in response["load_balancer_types"] + ] + return self._add_meta_to_result(load_balancer_types, response) + + def get_all(self, name=None): + # type: (Optional[str]) -> List[BoundLoadBalancerType] + """Get all Load Balancer types + + :param name: str (optional) + Can be used to filter Load Balancer type by their name. + :return: List[:class:`BoundLoadBalancerType `] + """ + return super().get_all(name=name) + + def get_by_name(self, name): + # type: (str) -> BoundLoadBalancerType + """Get Load Balancer type by name + + :param name: str + Used to get Load Balancer type by name. + :return: :class:`BoundLoadBalancerType ` + """ + return super().get_by_name(name) diff --git a/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py b/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py new file mode 100644 index 00000000..7bdb1c05 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/load_balancer_types/domain.py @@ -0,0 +1,55 @@ +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class LoadBalancerType(BaseDomain, DomainIdentityMixin): + """LoadBalancerType Domain + + :param id: int + ID of the Load Balancer type + :param name: str + Name of the Load Balancer type + :param description: str + Description of the Load Balancer type + :param max_connections: int + Max amount of connections the Load Balancer can handle + :param max_services: int + Max amount of services the Load Balancer can handle + :param max_targets: int + Max amount of targets the Load Balancer can handle + :param max_assigned_certificates: int + Max amount of certificates the Load Balancer can serve + :param prices: Dict + Prices in different locations + + """ + + __slots__ = ( + "id", + "name", + "description", + "max_connections", + "max_services", + "max_targets", + "max_assigned_certificates", + "prices", + ) + + def __init__( + self, + id=None, + name=None, + description=None, + max_connections=None, + max_services=None, + max_targets=None, + max_assigned_certificates=None, + prices=None, + ): + self.id = id + self.name = name + self.description = description + self.max_connections = max_connections + self.max_services = max_services + self.max_targets = max_targets + self.max_assigned_certificates = max_assigned_certificates + self.prices = prices diff --git a/plugins/module_utils/vendor/hcloud/load_balancers/__init__.py b/plugins/module_utils/vendor/hcloud/load_balancers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/load_balancers/client.py b/plugins/module_utils/vendor/hcloud/load_balancers/client.py new file mode 100644 index 00000000..25cc1343 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/load_balancers/client.py @@ -0,0 +1,900 @@ +from ..actions.client import BoundAction +from ..certificates.client import BoundCertificate +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from ..load_balancer_types.client import BoundLoadBalancerType +from ..locations.client import BoundLocation +from ..networks.client import BoundNetwork +from ..servers.client import BoundServer +from .domain import ( + CreateLoadBalancerResponse, + IPv4Address, + IPv6Network, + LoadBalancer, + LoadBalancerAlgorithm, + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, + LoadBalancerService, + LoadBalancerServiceHttp, + LoadBalancerTarget, + LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, + PrivateNet, + PublicNetwork, +) + + +class BoundLoadBalancer(BoundModelBase): + model = LoadBalancer + + def __init__(self, client, data, complete=True): + algorithm = data.get("algorithm") + if algorithm: + data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) + + public_net = data.get("public_net") + if public_net: + ipv4_address = IPv4Address.from_dict(public_net["ipv4"]) + ipv6_network = IPv6Network.from_dict(public_net["ipv6"]) + data["public_net"] = PublicNetwork( + ipv4=ipv4_address, ipv6=ipv6_network, enabled=public_net["enabled"] + ) + + private_nets = data.get("private_net") + if private_nets: + private_nets = [ + PrivateNet( + network=BoundNetwork( + client._client.networks, + {"id": private_net["network"]}, + complete=False, + ), + ip=private_net["ip"], + ) + for private_net in private_nets + ] + data["private_net"] = private_nets + + targets = data.get("targets") + if targets: + tmp_targets = [] + for target in targets: + tmp_target = LoadBalancerTarget(type=target["type"]) + if target["type"] == "server": + tmp_target.server = BoundServer( + client._client.servers, data=target["server"], complete=False + ) + tmp_target.use_private_ip = target["use_private_ip"] + elif target["type"] == "label_selector": + tmp_target.label_selector = LoadBalancerTargetLabelSelector( + selector=target["label_selector"]["selector"] + ) + tmp_target.use_private_ip = target["use_private_ip"] + elif target["type"] == "ip": + tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) + tmp_targets.append(tmp_target) + data["targets"] = tmp_targets + + services = data.get("services") + if services: + tmp_services = [] + for service in services: + tmp_service = LoadBalancerService( + protocol=service["protocol"], + listen_port=service["listen_port"], + destination_port=service["destination_port"], + proxyprotocol=service["proxyprotocol"], + ) + if service["protocol"] != "tcp": + tmp_service.http = LoadBalancerServiceHttp( + sticky_sessions=service["http"]["sticky_sessions"], + redirect_http=service["http"]["redirect_http"], + cookie_name=service["http"]["cookie_name"], + cookie_lifetime=service["http"]["cookie_lifetime"], + ) + tmp_service.http.certificates = [ + BoundCertificate( + client._client.certificates, + {"id": certificate}, + complete=False, + ) + for certificate in service["http"]["certificates"] + ] + + tmp_service.health_check = LoadBalancerHealthCheck( + protocol=service["health_check"]["protocol"], + port=service["health_check"]["port"], + interval=service["health_check"]["interval"], + retries=service["health_check"]["retries"], + timeout=service["health_check"]["timeout"], + ) + if tmp_service.health_check.protocol != "tcp": + tmp_service.health_check.http = LoadBalancerHealtCheckHttp( + domain=service["health_check"]["http"]["domain"], + path=service["health_check"]["http"]["path"], + response=service["health_check"]["http"]["response"], + tls=service["health_check"]["http"]["tls"], + status_codes=service["health_check"]["http"]["status_codes"], + ) + tmp_services.append(tmp_service) + data["services"] = tmp_services + + load_balancer_type = data.get("load_balancer_type") + if load_balancer_type is not None: + data["load_balancer_type"] = BoundLoadBalancerType( + client._client.load_balancer_types, load_balancer_type + ) + + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + super().__init__(client, data, complete) + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer + """Updates a Load Balancer. You can update a Load Balancers name and a Load Balancers labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundLoadBalancer ` + """ + return self._client.update(self, name, labels) + + def delete(self): + # type: () -> BoundAction + """Deletes a Load Balancer. + + :return: boolean + """ + return self._client.delete(self) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + """Returns all action objects for a Load Balancer. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Load Balancer. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def add_service(self, service): + # type: (LoadBalancerService) -> List[BoundAction] + """Adds a service to a Load Balancer. + + :param service: :class:`LoadBalancerService ` + The LoadBalancerService you want to add to the Load Balancer + :return: :class:`BoundAction ` + """ + return self._client.add_service(self, service=service) + + def update_service(self, service): + # type: (LoadBalancerService) -> List[BoundAction] + """Updates a service of an Load Balancer. + + :param service: :class:`LoadBalancerService ` + The LoadBalancerService you want to update + :return: :class:`BoundAction ` + """ + return self._client.update_service(self, service=service) + + def delete_service(self, service): + # type: (LoadBalancerService) -> List[BoundAction] + """Deletes a service from a Load Balancer. + + :param service: :class:`LoadBalancerService ` + The LoadBalancerService you want to delete from the Load Balancer + :return: :class:`BoundAction ` + """ + return self._client.delete_service(self, service) + + def add_target(self, target): + # type: (LoadBalancerTarget) -> List[BoundAction] + """Adds a target to a Load Balancer. + + :param target: :class:`LoadBalancerTarget ` + The LoadBalancerTarget you want to add to the Load Balancer + :return: :class:`BoundAction ` + """ + return self._client.add_target(self, target) + + def remove_target(self, target): + # type: (LoadBalancerTarget) -> List[BoundAction] + """Removes a target from a Load Balancer. + + :param target: :class:`LoadBalancerTarget ` + The LoadBalancerTarget you want to remove from the Load Balancer + :return: :class:`BoundAction ` + """ + return self._client.remove_target(self, target) + + def change_algorithm(self, algorithm): + # type: (LoadBalancerAlgorithm) -> List[BoundAction] + """Changes the algorithm used by the Load Balancer + + :param algorithm: :class:`LoadBalancerAlgorithm ` + The LoadBalancerAlgorithm you want to use + :return: :class:`BoundAction ` + """ + return self._client.change_algorithm(self, algorithm) + + def change_dns_ptr(self, ip, dns_ptr): + # type: (str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + def change_protection(self, delete): + # type: (LoadBalancerService) -> List[BoundAction] + """Changes the protection configuration of a Load Balancer. + + :param delete: boolean + If True, prevents the Load Balancer from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + def attach_to_network(self, network, ip=None): + # type: (Union[Network,BoundNetwork],Optional[str]) -> BoundAction + """Attaches a Load Balancer to a Network + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param ip: str + IP to request to be assigned to this Load Balancer + :return: :class:`BoundAction ` + """ + return self._client.attach_to_network(self, network, ip) + + def detach_from_network(self, network): + # type: ( Union[Network,BoundNetwork]) -> BoundAction + """Detaches a Load Balancer from a Network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + return self._client.detach_from_network(self, network) + + def enable_public_interface(self): + # type: () -> BoundAction + """Enables the public interface of a Load Balancer. + + :return: :class:`BoundAction ` + """ + return self._client.enable_public_interface(self) + + def disable_public_interface(self): + # type: () -> BoundAction + """Disables the public interface of a Load Balancer. + + :return: :class:`BoundAction ` + """ + return self._client.disable_public_interface(self) + + def change_type(self, load_balancer_type): + # type: (Union[LoadBalancerType,BoundLoadBalancerType]) -> BoundAction + """Changes the type of a Load Balancer. + + :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` + Load Balancer type the Load Balancer should migrate to + :return: :class:`BoundAction ` + """ + return self._client.change_type(self, load_balancer_type) + + +class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "load_balancers" + + def get_by_id(self, id): + # type: (int) -> BoundLoadBalancer + """Get a specific Load Balancer + + :param id: int + :return: :class:`BoundLoadBalancer ` + """ + response = self._client.request( + url=f"/load_balancers/{id}", + method="GET", + ) + return BoundLoadBalancer(self, response["load_balancer"]) + + def get_list( + self, + name=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundLoadBalancer], Meta] + """Get a list of Load Balancers from this account + + :param name: str (optional) + Can be used to filter Load Balancers by their name. + :param label_selector: str (optional) + Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLoadBalancer `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/load_balancers", method="GET", params=params + ) + + ass_load_balancers = [ + BoundLoadBalancer(self, load_balancer_data) + for load_balancer_data in response["load_balancers"] + ] + return self._add_meta_to_result(ass_load_balancers, response) + + def get_all(self, name=None, label_selector=None): + # type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer] + """Get all Load Balancers from this account + + :param name: str (optional) + Can be used to filter Load Balancers by their name. + :param label_selector: str (optional) + Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. + :return: List[:class:`BoundLoadBalancer `] + """ + return super().get_all(name=name, label_selector=label_selector) + + def get_by_name(self, name): + # type: (str) -> BoundLoadBalancer + """Get Load Balancer by name + + :param name: str + Used to get Load Balancer by name. + :return: :class:`BoundLoadBalancer ` + """ + return super().get_by_name(name) + + def create( + self, + name, # type: str + load_balancer_type, # type: LoadBalancerType + algorithm=None, # type: Optional[LoadBalancerAlgorithm] + services=None, # type: Optional[List[LoadBalancerService]] + targets=None, # type: Optional[List[LoadBalancerTarget]] + labels=None, # type: Optional[Dict[str, str]] + location=None, # type: Optional[Location] + network_zone=None, # type: Optional[str] + public_interface=None, # type: Optional[bool] + network=None, # type: Optional[Union[Network,BoundNetwork]] + ): + # type: (...) -> CreateLoadBalancerResponse + """Creates a Load Balancer . + + :param name: str + Name of the Load Balancer + :param load_balancer_type: LoadBalancerType + Type of the Load Balancer + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param location: Location + Location of the Load Balancer + :param network_zone: str + Network Zone of the Load Balancer + :param algorithm: LoadBalancerAlgorithm (optional) + The algorithm the Load Balancer is currently using + :param services: LoadBalancerService + The services the Load Balancer is currently serving + :param targets: LoadBalancerTarget + The targets the Load Balancer is currently serving + :param public_interface: bool + Enable or disable the public interface of the Load Balancer + :param network: Network + Adds the Load Balancer to a Network + :return: :class:`CreateLoadBalancerResponse ` + """ + data = {"name": name, "load_balancer_type": load_balancer_type.id_or_name} + if network is not None: + data["network"] = network.id + if public_interface is not None: + data["public_interface"] = public_interface + if labels is not None: + data["labels"] = labels + if algorithm is not None: + data["algorithm"] = {"type": algorithm.type} + if services is not None: + service_list = [] + for service in services: + service_list.append(self.get_service_parameters(service)) + data["services"] = service_list + + if targets is not None: + target_list = [] + for target in targets: + target_data = { + "type": target.type, + "use_private_ip": target.use_private_ip, + } + if target.type == "server": + target_data["server"] = {"id": target.server.id} + elif target.type == "label_selector": + target_data["label_selector"] = { + "selector": target.label_selector.selector + } + elif target.type == "ip": + target_data["ip"] = {"ip": target.ip.ip} + target_list.append(target_data) + + data["targets"] = target_list + + if network_zone is not None: + data["network_zone"] = network_zone + if location is not None: + data["location"] = location.id_or_name + + response = self._client.request(url="/load_balancers", method="POST", json=data) + + return CreateLoadBalancerResponse( + load_balancer=BoundLoadBalancer(self, response["load_balancer"]), + action=BoundAction(self._client.actions, response["action"]), + ) + + def update(self, load_balancer, name=None, labels=None): + # type:(LoadBalancer, Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer + """Updates a LoadBalancer. You can update a LoadBalancer’s name and a LoadBalancer’s labels. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundLoadBalancer ` + """ + data = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url="/load_balancers/{load_balancer_id}".format( + load_balancer_id=load_balancer.id + ), + method="PUT", + json=data, + ) + return BoundLoadBalancer(self, response["load_balancer"]) + + def delete(self, load_balancer): + # type: (LoadBalancer) -> BoundAction + """Deletes a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :return: boolean + """ + self._client.request( + url="/load_balancers/{load_balancer_id}".format( + load_balancer_id=load_balancer.id + ), + method="DELETE", + ) + return True + + def get_actions_list( + self, load_balancer, status=None, sort=None, page=None, per_page=None + ): + # type: (LoadBalancer, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions".format( + load_balancer_id=load_balancer.id + ), + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions(self, load_balancer, status=None, sort=None): + # type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return super().get_actions(load_balancer, status=status, sort=sort) + + def add_service(self, load_balancer, service): + # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] + """Adds a service to a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param service: :class:`LoadBalancerService ` + The LoadBalancerService you want to add to the Load Balancer + :return: :class:`BoundAction ` + """ + data = self.get_service_parameters(service) + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/add_service".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def get_service_parameters(self, service): + data = {} + if service.protocol is not None: + data["protocol"] = service.protocol + if service.listen_port is not None: + data["listen_port"] = service.listen_port + if service.destination_port is not None: + data["destination_port"] = service.destination_port + if service.proxyprotocol is not None: + data["proxyprotocol"] = service.proxyprotocol + if service.http is not None: + data["http"] = {} + if service.http.cookie_name is not None: + data["http"]["cookie_name"] = service.http.cookie_name + if service.http.cookie_lifetime is not None: + data["http"]["cookie_lifetime"] = service.http.cookie_lifetime + if service.http.redirect_http is not None: + data["http"]["redirect_http"] = service.http.redirect_http + if service.http.sticky_sessions is not None: + data["http"]["sticky_sessions"] = service.http.sticky_sessions + certificate_ids = [] + for certificate in service.http.certificates: + certificate_ids.append(certificate.id) + data["http"]["certificates"] = certificate_ids + if service.health_check is not None: + data["health_check"] = { + "protocol": service.health_check.protocol, + "port": service.health_check.port, + "interval": service.health_check.interval, + "timeout": service.health_check.timeout, + "retries": service.health_check.retries, + } + data["health_check"] = {} + if service.health_check.protocol is not None: + data["health_check"]["protocol"] = service.health_check.protocol + if service.health_check.port is not None: + data["health_check"]["port"] = service.health_check.port + if service.health_check.interval is not None: + data["health_check"]["interval"] = service.health_check.interval + if service.health_check.timeout is not None: + data["health_check"]["timeout"] = service.health_check.timeout + if service.health_check.retries is not None: + data["health_check"]["retries"] = service.health_check.retries + if service.health_check.http is not None: + data["health_check"]["http"] = {} + if service.health_check.http.domain is not None: + data["health_check"]["http"][ + "domain" + ] = service.health_check.http.domain + if service.health_check.http.path is not None: + data["health_check"]["http"][ + "path" + ] = service.health_check.http.path + if service.health_check.http.response is not None: + data["health_check"]["http"][ + "response" + ] = service.health_check.http.response + if service.health_check.http.status_codes is not None: + data["health_check"]["http"][ + "status_codes" + ] = service.health_check.http.status_codes + if service.health_check.http.tls is not None: + data["health_check"]["http"]["tls"] = service.health_check.http.tls + return data + + def update_service(self, load_balancer, service): + # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] + """Updates a service of an Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param service: :class:`LoadBalancerService ` + The LoadBalancerService with updated values within for the Load Balancer + :return: :class:`BoundAction ` + """ + data = self.get_service_parameters(service) + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/update_service".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_service(self, load_balancer, service): + # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] + """Deletes a service from a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param service: :class:`LoadBalancerService ` + The LoadBalancerService you want to delete from the Load Balancer + :return: :class:`BoundAction ` + """ + data = {"listen_port": service.listen_port} + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/delete_service".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_target(self, load_balancer, target): + # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] + """Adds a target to a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param target: :class:`LoadBalancerTarget ` + The LoadBalancerTarget you want to add to the Load Balancer + :return: :class:`BoundAction ` + """ + data = {"type": target.type, "use_private_ip": target.use_private_ip} + if target.type == "server": + data["server"] = {"id": target.server.id} + elif target.type == "label_selector": + data["label_selector"] = {"selector": target.label_selector.selector} + elif target.type == "ip": + data["ip"] = {"ip": target.ip.ip} + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/add_target".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def remove_target(self, load_balancer, target): + # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] + """Removes a target from a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param target: :class:`LoadBalancerTarget ` + The LoadBalancerTarget you want to remove from the Load Balancer + :return: :class:`BoundAction ` + """ + data = {"type": target.type} + if target.type == "server": + data["server"] = {"id": target.server.id} + elif target.type == "label_selector": + data["label_selector"] = {"selector": target.label_selector.selector} + elif target.type == "ip": + data["ip"] = {"ip": target.ip.ip} + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/remove_target".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_algorithm(self, load_balancer, algorithm): + # type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction + """Changes the algorithm used by the Load Balancer + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + :param algorithm: :class:`LoadBalancerAlgorithm ` + The LoadBalancerSubnet you want to add to the Load Balancer + :return: :class:`BoundAction ` + """ + data = {"type": algorithm.type} + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/change_algorithm".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr(self, load_balancer, ip, dns_ptr): + # type: (Union[LoadBalancer, BoundLoadBalancer], str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/change_dns_ptr".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection(self, load_balancer, delete=None): + # type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction + """Changes the protection configuration of a Load Balancer. + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + :param delete: boolean + If True, prevents the Load Balancer from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/change_protection".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def attach_to_network( + self, + load_balancer, # type: Union[LoadBalancer, BoundLoadBalancer] + network, # type: Union[Network, BoundNetwork] + ip=None, # type: Optional[str] + ): + """Attach a Load Balancer to a Network. + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param ip: str + IP to request to be assigned to this Load Balancer + :return: :class:`BoundAction ` + """ + data = {"network": network.id} + if ip is not None: + data.update({"ip": ip}) + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/attach_to_network".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_from_network(self, load_balancer, network): + # type: (Union[LoadBalancer, BoundLoadBalancer], Union[Network,BoundNetwork]) -> BoundAction + """Detaches a Load Balancer from a Network. + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + :param network: :class:`BoundNetwork ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + data = {"network": network.id} + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/detach_from_network".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def enable_public_interface(self, load_balancer): + # type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction + """Enables the public interface of a Load Balancer. + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + + :return: :class:`BoundAction ` + """ + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/enable_public_interface".format( + load_balancer_id=load_balancer.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def disable_public_interface(self, load_balancer): + # type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction + """Disables the public interface of a Load Balancer. + + :param load_balancer: :class:` ` or :class:`LoadBalancer ` + + :return: :class:`BoundAction ` + """ + + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/disable_public_interface".format( + load_balancer_id=load_balancer.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_type(self, load_balancer, load_balancer_type): + # type: ([LoadBalancer, BoundLoadBalancer], [LoadBalancerType, BoundLoadBalancerType]) ->BoundAction + """Changes the type of a Load Balancer. + + :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` + :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` + Load Balancer type the Load Balancer should migrate to + :return: :class:`BoundAction ` + """ + data = {"load_balancer_type": load_balancer_type.id_or_name} + response = self._client.request( + url="/load_balancers/{load_balancer_id}/actions/change_type".format( + load_balancer_id=load_balancer.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/load_balancers/domain.py b/plugins/module_utils/vendor/hcloud/load_balancers/domain.py new file mode 100644 index 00000000..f1769bf8 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/load_balancers/domain.py @@ -0,0 +1,370 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class LoadBalancer(BaseDomain): + """LoadBalancer Domain + + :param id: int + ID of the Load Balancer + :param name: str + Name of the Load Balancer (must be unique per project) + :param created: datetime + Point in time when the Load Balancer was created + :param protection: dict + Protection configuration for the Load Balancer + :param labels: dict + User-defined labels (key-value pairs) + :param location: Location + Location of the Load Balancer + :param public_net: :class:`PublicNetwork ` + Public network information. + :param private_net: List[:class:`PrivateNet ` + :param ipv6: :class:`IPv6Network ` + :param enabled: boolean + """ + + __slots__ = ("ipv4", "ipv6", "enabled") + + def __init__( + self, + ipv4, # type: IPv4Address + ipv6, # type: IPv6Network + enabled, # type: bool + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enabled = enabled + + +class IPv4Address(BaseDomain): + """IPv4 Address Domain + + :param ip: str + The IPv4 Address + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip, # type: str + dns_ptr, # type: str + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class IPv6Network(BaseDomain): + """IPv6 Network Domain + + :param ip: str + The IPv6 Network as CIDR Notation + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip, # type: str + dns_ptr, # type: str + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class PrivateNet(BaseDomain): + """PrivateNet Domain + + :param network: :class:`BoundNetwork ` + The Network the LoadBalancer is attached to + :param ip: str + The main IP Address of the LoadBalancer in the Network + """ + + __slots__ = ("network", "ip") + + def __init__( + self, + network, # type: BoundNetwork + ip, # type: str + ): + self.network = network + self.ip = ip + + +class CreateLoadBalancerResponse(BaseDomain): + """Create Load Balancer Response Domain + + :param load_balancer: :class:`BoundLoadBalancer ` + The created Load Balancer + :param action: :class:`BoundAction ` + Shows the progress of the Load Balancer creation + """ + + __slots__ = ("load_balancer", "action") + + def __init__( + self, + load_balancer, # type: BoundLoadBalancer + action, # type: BoundAction + ): + self.load_balancer = load_balancer + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/locations/__init__.py b/plugins/module_utils/vendor/hcloud/locations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/locations/client.py b/plugins/module_utils/vendor/hcloud/locations/client.py new file mode 100644 index 00000000..6171ed67 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/locations/client.py @@ -0,0 +1,67 @@ +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import Location + + +class BoundLocation(BoundModelBase): + model = Location + + +class LocationsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "locations" + + def get_by_id(self, id): + # type: (int) -> locations.client.BoundLocation + """Get a specific location by its ID. + + :param id: int + :return: :class:`BoundLocation ` + """ + response = self._client.request(url=f"/locations/{id}", method="GET") + return BoundLocation(self, response["location"]) + + def get_list(self, name=None, page=None, per_page=None): + # type: (Optional[str], Optional[int], Optional[int]) -> PageResult[List[BoundLocation], Meta] + """Get a list of locations + + :param name: str (optional) + Can be used to filter locations by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundLocation `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/locations", method="GET", params=params) + locations = [ + BoundLocation(self, location_data) + for location_data in response["locations"] + ] + return self._add_meta_to_result(locations, response) + + def get_all(self, name=None): + # type: (Optional[str]) -> List[BoundLocation] + """Get all locations + + :param name: str (optional) + Can be used to filter locations by their name. + :return: List[:class:`BoundLocation `] + """ + return super().get_all(name=name) + + def get_by_name(self, name): + # type: (str) -> BoundLocation + """Get location by name + + :param name: str + Used to get location by name. + :return: :class:`BoundLocation ` + """ + return super().get_by_name(name) diff --git a/plugins/module_utils/vendor/hcloud/locations/domain.py b/plugins/module_utils/vendor/hcloud/locations/domain.py new file mode 100644 index 00000000..6ca16d6f --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/locations/domain.py @@ -0,0 +1,54 @@ +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Location(BaseDomain, DomainIdentityMixin): + """Location Domain + + :param id: int + ID of location + :param name: str + Name of location + :param description: str + Description of location + :param country: str + ISO 3166-1 alpha-2 code of the country the location resides in + :param city: str + City the location is closest to + :param latitude: float + Latitude of the city closest to the location + :param longitude: float + Longitude of the city closest to the location + :param network_zone: str + Name of network zone this location resides in + """ + + __slots__ = ( + "id", + "name", + "description", + "country", + "city", + "latitude", + "longitude", + "network_zone", + ) + + def __init__( + self, + id=None, + name=None, + description=None, + country=None, + city=None, + latitude=None, + longitude=None, + network_zone=None, + ): + self.id = id + self.name = name + self.description = description + self.country = country + self.city = city + self.latitude = latitude + self.longitude = longitude + self.network_zone = network_zone diff --git a/plugins/module_utils/vendor/hcloud/networks/__init__.py b/plugins/module_utils/vendor/hcloud/networks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/networks/client.py b/plugins/module_utils/vendor/hcloud/networks/client.py new file mode 100644 index 00000000..17152052 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/networks/client.py @@ -0,0 +1,499 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import Network, NetworkRoute, NetworkSubnet + + +class BoundNetwork(BoundModelBase): + model = Network + + def __init__(self, client, data, complete=True): + subnets = data.get("subnets", []) + if subnets is not None: + subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] + data["subnets"] = subnets + + routes = data.get("routes", []) + if routes is not None: + routes = [NetworkRoute.from_dict(route) for route in routes] + data["routes"] = routes + + from ..servers.client import BoundServer + + servers = data.get("servers", []) + if servers is not None: + servers = [ + BoundServer(client._client.servers, {"id": server}, complete=False) + for server in servers + ] + data["servers"] = servers + + super().__init__(client, data, complete) + + def update( + self, + name=None, # type: Optional[str] + expose_routes_to_vswitch=None, # type: Optional[bool] + labels=None, # type: Optional[Dict[str, str]] + ): # type: (...) -> BoundNetwork + """Updates a network. You can update a network’s name and a networks’s labels. + + :param name: str (optional) + New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork ` + """ + return self._client.update( + self, + name=name, + expose_routes_to_vswitch=expose_routes_to_vswitch, + labels=labels, + ) + + def delete(self): + # type: () -> BoundAction + """Deletes a network. + + :return: boolean + """ + return self._client.delete(self) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + """Returns all action objects for a network. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a network. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def add_subnet(self, subnet): + # type: (NetworkSubnet) -> List[BoundAction] + """Adds a subnet entry to a network. + + :param subnet: :class:`NetworkSubnet ` + The NetworkSubnet you want to add to the Network + :return: :class:`BoundAction ` + """ + return self._client.add_subnet(self, subnet=subnet) + + def delete_subnet(self, subnet): + # type: (NetworkSubnet) -> List[BoundAction] + """Removes a subnet entry from a network + + :param subnet: :class:`NetworkSubnet ` + The NetworkSubnet you want to remove from the Network + :return: :class:`BoundAction ` + """ + return self._client.delete_subnet(self, subnet=subnet) + + def add_route(self, route): + # type: (NetworkRoute) -> List[BoundAction] + """Adds a route entry to a network. + + :param route: :class:`NetworkRoute ` + The NetworkRoute you want to add to the Network + :return: :class:`BoundAction ` + """ + return self._client.add_route(self, route=route) + + def delete_route(self, route): + # type: (NetworkRoute) -> List[BoundAction] + """Removes a route entry to a network. + + :param route: :class:`NetworkRoute ` + The NetworkRoute you want to remove from the Network + :return: :class:`BoundAction ` + """ + return self._client.delete_route(self, route=route) + + def change_ip_range(self, ip_range): + # type: (str) -> List[BoundAction] + """Changes the IP range of a network. + + :param ip_range: str + The new prefix for the whole network. + :return: :class:`BoundAction ` + """ + return self._client.change_ip_range(self, ip_range=ip_range) + + def change_protection(self, delete=None): + # type: (Optional[bool]) -> BoundAction + """Changes the protection configuration of a network. + + :param delete: boolean + If True, prevents the network from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete=delete) + + +class NetworksClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "networks" + + def get_by_id(self, id): + # type: (int) -> BoundNetwork + """Get a specific network + + :param id: int + :return: :class:`BoundNetwork + """ + response = self._client.request(url=f"/networks/{id}", method="GET") + return BoundNetwork(self, response["network"]) + + def get_list( + self, + name=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundNetwork], Meta] + """Get a list of networks from this account + + :param name: str (optional) + Can be used to filter networks by their name. + :param label_selector: str (optional) + Can be used to filter networks by labels. The response will only contain networks matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundNetwork `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/networks", method="GET", params=params) + + ass_networks = [ + BoundNetwork(self, network_data) for network_data in response["networks"] + ] + return self._add_meta_to_result(ass_networks, response) + + def get_all(self, name=None, label_selector=None): + # type: (Optional[str], Optional[str]) -> List[BoundNetwork] + """Get all networks from this account + + :param name: str (optional) + Can be used to filter networks by their name. + :param label_selector: str (optional) + Can be used to filter networks by labels. The response will only contain networks matching the label selector. + :return: List[:class:`BoundNetwork `] + """ + return super().get_all(name=name, label_selector=label_selector) + + def get_by_name(self, name): + # type: (str) -> BoundNetwork + """Get network by name + + :param name: str + Used to get network by name. + :return: :class:`BoundNetwork ` + """ + return super().get_by_name(name) + + def create( + self, + name, # type: str + ip_range, # type: str + subnets=None, # type: Optional[List[NetworkSubnet]] + routes=None, # type: Optional[List[NetworkRoute]] + expose_routes_to_vswitch=None, # type: Optional[bool] + labels=None, # type: Optional[Dict[str, str]] + ): + """Creates a network with range ip_range. + + :param name: str + Name of the network + :param ip_range: str + IP range of the whole network which must span all included subnets and route destinations + :param subnets: List[:class:`NetworkSubnet `] + Array of subnets allocated + :param routes: List[:class:`NetworkRoute `] + Array of routes set in this network + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork ` + """ + data = {"name": name, "ip_range": ip_range} + if subnets is not None: + data_subnets = [] + for subnet in subnets: + data_subnet = { + "type": subnet.type, + "ip_range": subnet.ip_range, + "network_zone": subnet.network_zone, + } + if subnet.vswitch_id is not None: + data_subnet["vswitch_id"] = subnet.vswitch_id + + data_subnets.append(data_subnet) + data["subnets"] = data_subnets + + if routes is not None: + data["routes"] = [ + {"destination": route.destination, "gateway": route.gateway} + for route in routes + ] + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + + if labels is not None: + data["labels"] = labels + + response = self._client.request(url="/networks", method="POST", json=data) + + return BoundNetwork(self, response["network"]) + + def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None): + # type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork + """Updates a network. You can update a network’s name and a network’s labels. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param name: str (optional) + New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundNetwork ` + """ + data = {} + if name is not None: + data.update({"name": name}) + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + + if labels is not None: + data.update({"labels": labels}) + + response = self._client.request( + url=f"/networks/{network.id}", + method="PUT", + json=data, + ) + return BoundNetwork(self, response["network"]) + + def delete(self, network): + # type: (Network) -> BoundAction + """Deletes a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :return: boolean + """ + self._client.request(url=f"/networks/{network.id}", method="DELETE") + return True + + def get_actions_list( + self, network, status=None, sort=None, page=None, per_page=None + ): + # type: (Network, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/networks/{network.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions(self, network, status=None, sort=None): + # type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return super().get_actions(network, status=status, sort=sort) + + def add_subnet(self, network, subnet): + # type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] + """Adds a subnet entry to a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param subnet: :class:`NetworkSubnet ` + The NetworkSubnet you want to add to the Network + :return: :class:`BoundAction ` + """ + data = {"type": subnet.type, "network_zone": subnet.network_zone} + if subnet.ip_range is not None: + data["ip_range"] = subnet.ip_range + if subnet.vswitch_id is not None: + data["vswitch_id"] = subnet.vswitch_id + + response = self._client.request( + url="/networks/{network_id}/actions/add_subnet".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_subnet(self, network, subnet): + # type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] + """Removes a subnet entry from a network + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param subnet: :class:`NetworkSubnet ` + The NetworkSubnet you want to remove from the Network + :return: :class:`BoundAction ` + """ + data = {"ip_range": subnet.ip_range} + + response = self._client.request( + url="/networks/{network_id}/actions/delete_subnet".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_route(self, network, route): + # type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] + """Adds a route entry to a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param route: :class:`NetworkRoute ` + The NetworkRoute you want to add to the Network + :return: :class:`BoundAction ` + """ + data = {"destination": route.destination, "gateway": route.gateway} + + response = self._client.request( + url="/networks/{network_id}/actions/add_route".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def delete_route(self, network, route): + # type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] + """Removes a route entry to a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param route: :class:`NetworkRoute ` + The NetworkRoute you want to remove from the Network + :return: :class:`BoundAction ` + """ + data = {"destination": route.destination, "gateway": route.gateway} + + response = self._client.request( + url="/networks/{network_id}/actions/delete_route".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_ip_range(self, network, ip_range): + # type: (Union[Network, BoundNetwork], str) -> List[BoundAction] + """Changes the IP range of a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param ip_range: str + The new prefix for the whole network. + :return: :class:`BoundAction ` + """ + data = {"ip_range": ip_range} + + response = self._client.request( + url="/networks/{network_id}/actions/change_ip_range".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection(self, network, delete=None): + # type: (Union[Network, BoundNetwork], Optional[bool]) -> BoundAction + """Changes the protection configuration of a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param delete: boolean + If True, prevents the network from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/networks/{network_id}/actions/change_protection".format( + network_id=network.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/networks/domain.py b/plugins/module_utils/vendor/hcloud/networks/domain.py new file mode 100644 index 00000000..32285a39 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/networks/domain.py @@ -0,0 +1,136 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class Network(BaseDomain): + """Network Domain + + :param id: int + ID of the network + :param name: str + Name of the network + :param ip_range: str + IPv4 prefix of the whole network + :param subnets: List[:class:`NetworkSubnet `] + Subnets allocated in this network + :param routes: List[:class:`NetworkRoute `] + Routes set in this network + :param expose_routes_to_vswitch: bool + Indicates if the routes from this network should be exposed to the vSwitch connection. + :param servers: List[:class:`BoundServer `] + Servers attached to this network + :param protection: dict + Protection configuration for the network + :param labels: dict + User-defined labels (key-value pairs) + """ + + __slots__ = ( + "id", + "name", + "ip_range", + "subnets", + "routes", + "expose_routes_to_vswitch", + "servers", + "protection", + "labels", + "created", + ) + + def __init__( + self, + id, + name=None, + created=None, + ip_range=None, + subnets=None, + routes=None, + expose_routes_to_vswitch=None, + servers=None, + protection=None, + labels=None, + ): + self.id = id + self.name = name + self.created = isoparse(created) if created else None + self.ip_range = ip_range + self.subnets = subnets + self.routes = routes + self.expose_routes_to_vswitch = expose_routes_to_vswitch + self.servers = servers + self.protection = protection + self.labels = labels + + +class NetworkSubnet(BaseDomain): + """Network Subnet Domain + + :param type: str + Type of sub network. + :param ip_range: str + Range to allocate IPs from. + :param network_zone: str + Name of network zone. + :param gateway: str + Gateway for the route. + :param vswitch_id: int + ID of the vSwitch. + """ + + TYPE_SERVER = "server" + """Subnet Type server, deprecated, use TYPE_CLOUD instead""" + TYPE_CLOUD = "cloud" + """Subnet Type cloud""" + TYPE_VSWITCH = "vswitch" + """Subnet Type vSwitch""" + __slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") + + def __init__( + self, ip_range, type=None, network_zone=None, gateway=None, vswitch_id=None + ): + self.type = type + self.ip_range = ip_range + self.network_zone = network_zone + self.gateway = gateway + self.vswitch_id = vswitch_id + + +class NetworkRoute(BaseDomain): + """Network Route Domain + + :param destination: str + Destination network or host of this route. + :param gateway: str + Gateway for the route. + """ + + __slots__ = ("destination", "gateway") + + def __init__(self, destination, gateway): + self.destination = destination + self.gateway = gateway + + +class CreateNetworkResponse(BaseDomain): + """Create Network Response Domain + + :param network: :class:`BoundNetwork ` + The network which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the network Creation + """ + + __slots__ = ("network", "action") + + def __init__( + self, + network, # type: BoundNetwork + action, # type: BoundAction + ): + self.network = network + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/placement_groups/__init__.py b/plugins/module_utils/vendor/hcloud/placement_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/placement_groups/client.py b/plugins/module_utils/vendor/hcloud/placement_groups/client.py new file mode 100644 index 00000000..c5cb7c0b --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/placement_groups/client.py @@ -0,0 +1,194 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import CreatePlacementGroupResponse, PlacementGroup + + +class BoundPlacementGroup(BoundModelBase): + model = PlacementGroup + + def update(self, labels=None, name=None): + # type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup + """Updates the name or labels of a Placement Group + + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str, (optional) + New Name to set + :return: :class:`BoundPlacementGroup ` + """ + return self._client.update(self, labels, name) + + def delete(self): + # type: () -> bool + """Deletes a Placement Group + + :return: boolean + """ + return self._client.delete(self) + + +class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "placement_groups" + + def get_by_id(self, id): + # type: (int) -> BoundPlacementGroup + """Returns a specific Placement Group object + + :param id: int + :return: :class:`BoundPlacementGroup ` + """ + response = self._client.request( + url=f"/placement_groups/{id}", + method="GET", + ) + return BoundPlacementGroup(self, response["placement_group"]) + + def get_list( + self, + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + name=None, # type: Optional[str] + sort=None, # type: Optional[List[str]] + type=None, # type: Optional[str] + ): + # type: (...) -> PageResults[List[BoundPlacementGroup]] + """Get a list of Placement Groups + + :param label_selector: str (optional) + Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter Placement Groups by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: (List[:class:`BoundPlacementGroup `], :class:`Meta `) + """ + + params = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if sort is not None: + params["sort"] = sort + if type is not None: + params["type"] = type + response = self._client.request( + url="/placement_groups", method="GET", params=params + ) + placement_groups = [ + BoundPlacementGroup(self, placement_group_data) + for placement_group_data in response["placement_groups"] + ] + + return self._add_meta_to_result(placement_groups, response) + + def get_all(self, label_selector=None, name=None, sort=None): + # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup] + """Get all Placement Groups + + :param label_selector: str (optional) + Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. + :param name: str (optional) + Can be used to filter Placement Groups by their name. + :param sort: List[str] (optional) + Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) + :return: List[:class:`BoundPlacementGroup `] + """ + return super().get_all(label_selector=label_selector, name=name, sort=sort) + + def get_by_name(self, name): + # type: (str) -> BoundPlacementGroup + """Get Placement Group by name + + :param name: str + Used to get Placement Group by name + :return: class:`BoundPlacementGroup ` + """ + return super().get_by_name(name) + + def create( + self, + name, # type: str + type, # type: str + labels=None, # type: Optional[Dict[str, str]] + ): + # type: (...) -> CreatePlacementGroupResponse + """Creates a new Placement Group. + + :param name: str + Placement Group Name + :param type: str + Type of the Placement Group + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + + :return: :class:`CreatePlacementGroupResponse ` + """ + data = {"name": name, "type": type} + if labels is not None: + data["labels"] = labels + response = self._client.request( + url="/placement_groups", json=data, method="POST" + ) + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.action, response["action"]) + + result = CreatePlacementGroupResponse( + placement_group=BoundPlacementGroup(self, response["placement_group"]), + action=action, + ) + return result + + def update(self, placement_group, labels=None, name=None): + # type: (PlacementGroup, Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup + """Updates the description or labels of a Placement Group. + + :param placement_group: :class:`BoundPlacementGroup ` or :class:`PlacementGroup ` + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundPlacementGroup ` + """ + + data = {} + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url="/placement_groups/{placement_group_id}".format( + placement_group_id=placement_group.id + ), + method="PUT", + json=data, + ) + return BoundPlacementGroup(self, response["placement_group"]) + + def delete(self, placement_group): + # type: (PlacementGroup) -> bool + """Deletes a Placement Group. + + :param placement_group: :class:`BoundPlacementGroup ` or :class:`PlacementGroup ` + :return: boolean + """ + self._client.request( + url="/placement_groups/{placement_group_id}".format( + placement_group_id=placement_group.id + ), + method="DELETE", + ) + return True diff --git a/plugins/module_utils/vendor/hcloud/placement_groups/domain.py b/plugins/module_utils/vendor/hcloud/placement_groups/domain.py new file mode 100644 index 00000000..e02443c0 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/placement_groups/domain.py @@ -0,0 +1,61 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class PlacementGroup(BaseDomain): + """Placement Group Domain + + :param id: int + ID of the Placement Group + :param name: str + Name of the Placement Group + :param labels: dict + User-defined labels (key-value pairs) + :param servers: List[ int ] + List of server IDs assigned to the Placement Group + :param type: str + Type of the Placement Group + :param created: datetime + Point in time when the image was created + """ + + __slots__ = ("id", "name", "labels", "servers", "type", "created") + + """Placement Group type spread + spreads all servers in the group on different vhosts + """ + TYPE_SPREAD = "spread" + + def __init__( + self, id=None, name=None, labels=None, servers=None, type=None, created=None + ): + self.id = id + self.name = name + self.labels = labels + self.servers = servers + self.type = type + self.created = isoparse(created) if created else None + + +class CreatePlacementGroupResponse(BaseDomain): + """Create Placement Group Response Domain + + :param placement_group: :class:`BoundPlacementGroup ` + The Placement Group which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the Placement Group Creation + """ + + __slots__ = ("placement_group", "action") + + def __init__( + self, + placement_group, # type: BoundPlacementGroup + action, # type: BoundAction + ): + self.placement_group = placement_group + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/primary_ips/__init__.py b/plugins/module_utils/vendor/hcloud/primary_ips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/primary_ips/client.py b/plugins/module_utils/vendor/hcloud/primary_ips/client.py new file mode 100644 index 00000000..e8c8d672 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/primary_ips/client.py @@ -0,0 +1,329 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import CreatePrimaryIPResponse, PrimaryIP + + +class BoundPrimaryIP(BoundModelBase): + model = PrimaryIP + + def __init__(self, client, data, complete=True): + from ..datacenters.client import BoundDatacenter + + datacenter = data.get("datacenter", {}) + if datacenter: + data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) + + super().__init__(client, data, complete) + + def update(self, auto_delete=None, labels=None, name=None): + # type: (Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP + """Updates the description or labels of a Primary IP. + + :param auto_delete: bool (optional) + Auto delete IP when assignee gets deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundPrimaryIP ` + """ + return self._client.update( + self, auto_delete=auto_delete, labels=labels, name=name + ) + + def delete(self): + # type: () -> bool + """Deletes a Primary IP. If it is currently assigned to a server it will automatically get unassigned. + + :return: boolean + """ + return self._client.delete(self) + + def change_protection(self, delete=None): + # type: (Optional[bool]) -> BoundAction + """Changes the protection configuration of the Primary IP. + + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + def assign(self, assignee_id, assignee_type): + # type: (int,str) -> BoundAction + """Assigns a Primary IP to a assignee. + + :param assignee_id: int` + Id of an assignee the Primary IP shall be assigned to + :param assignee_type: string` + Assignee type (e.g server) the Primary IP shall be assigned to + :return: :class:`BoundAction ` + """ + return self._client.assign(self, assignee_id, assignee_type) + + def unassign(self): + # type: () -> BoundAction + """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :return: :class:`BoundAction ` + """ + return self._client.unassign(self) + + def change_dns_ptr(self, ip, dns_ptr): + # type: (str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to this Primary IP. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + +class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "primary_ips" + + def get_by_id(self, id): + # type: (int) -> BoundPrimaryIP + """Returns a specific Primary IP object. + + :param id: int + :return: :class:`BoundPrimaryIP ` + """ + response = self._client.request(url=f"/primary_ips/{id}", method="GET") + return BoundPrimaryIP(self, response["primary_ip"]) + + def get_list( + self, + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + name=None, # type: Optional[str] + ip=None, # type: Optional[ip] + ): + # type: (...) -> PageResults[List[BoundPrimaryIP]] + """Get a list of primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selectorable values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :param ip: str (optional) + Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip. + :return: (List[:class:`BoundPrimaryIP `], :class:`Meta `) + """ + params = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if ip is not None: + params["ip"] = ip + + response = self._client.request(url="/primary_ips", method="GET", params=params) + primary_ips = [ + BoundPrimaryIP(self, primary_ip_data) + for primary_ip_data in response["primary_ips"] + ] + + return self._add_meta_to_result(primary_ips, response) + + def get_all(self, label_selector=None, name=None): + # type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP] + """Get all primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selector.able values. + :param name: str (optional) + Can be used to filter networks by their name. + :return: List[:class:`BoundPrimaryIP `] + """ + return super().get_all(label_selector=label_selector, name=name) + + def get_by_name(self, name): + # type: (str) -> BoundPrimaryIP + """Get Primary IP by name + + :param name: str + Used to get Primary IP by name. + :return: :class:`BoundPrimaryIP ` + """ + return super().get_by_name(name) + + def create( + self, + type, # type: str + datacenter, # type: Datacenter + name, # type: str + assignee_type="server", # type: Optional[str] + assignee_id=None, # type: Optional[int] + auto_delete=False, # type: Optional[bool] + labels=None, # type: Optional[dict] + ): + # type: (...) -> 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 name: str + :param auto_delete: bool (optional) + :return: :class:`CreatePrimaryIPResponse ` + """ + + data = { + "type": type, + "assignee_type": assignee_type, + "auto_delete": auto_delete, + "datacenter": datacenter.id_or_name, + "name": name, + } + if assignee_id: + data["assignee_id"] = assignee_id + if labels is not None: + data["labels"] = labels + + response = self._client.request(url="/primary_ips", json=data, method="POST") + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreatePrimaryIPResponse( + primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action + ) + return result + + def update(self, primary_ip, auto_delete=None, labels=None, name=None): + # type: (PrimaryIP, Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP + """Updates the name, auto_delete or labels of a Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param auto_delete: bool (optional) + Delete this Primary IP when the resource it is assigned to is deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundPrimaryIP ` + """ + data = {} + if auto_delete is not None: + data["auto_delete"] = auto_delete + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url=f"/primary_ips/{primary_ip.id}", + method="PUT", + json=data, + ) + return BoundPrimaryIP(self, response["primary_ip"]) + + def delete(self, primary_ip): + # type: (PrimaryIP) -> bool + """Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :return: boolean + """ + self._client.request( + url=f"/primary_ips/{primary_ip.id}", + method="DELETE", + ) + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True + + def change_protection(self, primary_ip, delete=None): + # type: (PrimaryIP, Optional[bool]) -> BoundAction + """Changes the protection configuration of the Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/change_protection".format( + primary_ip_id=primary_ip.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def assign(self, primary_ip, assignee_id, assignee_type="server"): + # type: (PrimaryIP, int, str) -> BoundAction + """Assigns a Primary IP to a assignee_id. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param assignee_id: int + Assignee the Primary IP shall be assigned to + :param assignee_type: str + Assignee the Primary IP shall be assigned to + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/assign".format( + primary_ip_id=primary_ip.id + ), + method="POST", + json={"assignee_id": assignee_id, "assignee_type": assignee_type}, + ) + return BoundAction(self._client.actions, response["action"]) + + def unassign(self, primary_ip): + # type: (PrimaryIP) -> BoundAction + """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/unassign".format( + primary_ip_id=primary_ip.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr(self, primary_ip, ip, dns_ptr): + # type: (PrimaryIP, str, str) -> BoundAction + """Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/change_dns_ptr".format( + primary_ip_id=primary_ip.id + ), + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/primary_ips/domain.py b/plugins/module_utils/vendor/hcloud/primary_ips/domain.py new file mode 100644 index 00000000..2db5122b --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/primary_ips/domain.py @@ -0,0 +1,104 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class PrimaryIP(BaseDomain): + """Primary IP Domain + + :param id: int + ID of the Primary IP + :param ip: str + IP address of the Primary IP + :param type: str + Type of Primary IP. Choices: `ipv4`, `ipv6` + :param dns_ptr: List[Dict] + Array of reverse DNS entries + :param datacenter: :class:`Datacenter ` + Datacenter the Primary IP was created in. + :param blocked: boolean + Whether the IP is blocked + :param protection: dict + Protection configuration for the Primary IP + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the Primary IP was created + :param name: str + Name of the Primary IP + :param assignee_id: int + Assignee ID the Primary IP is assigned to + :param assignee_type: str + Assignee Type of entity the Primary IP is assigned to + :param auto_delete: bool + Delete the Primary IP when the Assignee it is assigned to is deleted. + """ + + __slots__ = ( + "id", + "ip", + "type", + "dns_ptr", + "datacenter", + "blocked", + "protection", + "labels", + "created", + "name", + "assignee_id", + "assignee_type", + "auto_delete", + ) + + def __init__( + self, + id=None, + type=None, + ip=None, + dns_ptr=None, + datacenter=None, + blocked=None, + protection=None, + labels=None, + created=None, + name=None, + assignee_id=None, + assignee_type=None, + auto_delete=None, + ): + self.id = id + self.type = type + self.ip = ip + self.dns_ptr = dns_ptr + self.datacenter = datacenter + self.blocked = blocked + self.protection = protection + self.labels = labels + self.created = isoparse(created) if created else None + self.name = name + self.assignee_id = assignee_id + self.assignee_type = assignee_type + self.auto_delete = auto_delete + + +class CreatePrimaryIPResponse(BaseDomain): + """Create Primary IP Response Domain + + :param primary_ip: :class:`BoundPrimaryIP ` + The Primary IP which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the Primary IP Creation + """ + + __slots__ = ("primary_ip", "action") + + def __init__( + self, + primary_ip, # type: BoundPrimaryIP + action, # type: BoundAction + ): + self.primary_ip = primary_ip + self.action = action diff --git a/plugins/module_utils/vendor/hcloud/server_types/__init__.py b/plugins/module_utils/vendor/hcloud/server_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/server_types/client.py b/plugins/module_utils/vendor/hcloud/server_types/client.py new file mode 100644 index 00000000..d572e7b4 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/server_types/client.py @@ -0,0 +1,69 @@ +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import ServerType + + +class BoundServerType(BoundModelBase): + model = ServerType + + +class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "server_types" + + def get_by_id(self, id): + # type: (int) -> BoundServerType + """Returns a specific Server Type. + + :param id: int + :return: :class:`BoundServerType ` + """ + response = self._client.request(url=f"/server_types/{id}", method="GET") + return BoundServerType(self, response["server_type"]) + + def get_list(self, name=None, page=None, per_page=None): + # type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundServerType], Meta] + """Get a list of Server types + + :param name: str (optional) + Can be used to filter server type by their name. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundServerType `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url="/server_types", method="GET", params=params + ) + server_types = [ + BoundServerType(self, server_type_data) + for server_type_data in response["server_types"] + ] + return self._add_meta_to_result(server_types, response) + + def get_all(self, name=None): + # type: (Optional[str]) -> List[BoundServerType] + """Get all Server types + + :param name: str (optional) + Can be used to filter server type by their name. + :return: List[:class:`BoundServerType `] + """ + return super().get_all(name=name) + + def get_by_name(self, name): + # type: (str) -> BoundServerType + """Get Server type by name + + :param name: str + Used to get Server type by name. + :return: :class:`BoundServerType ` + """ + return super().get_by_name(name) diff --git a/plugins/module_utils/vendor/hcloud/server_types/domain.py b/plugins/module_utils/vendor/hcloud/server_types/domain.py new file mode 100644 index 00000000..bd2e26c3 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/server_types/domain.py @@ -0,0 +1,83 @@ +from ..core.domain import BaseDomain, DomainIdentityMixin +from ..deprecation.domain import DeprecationInfo + + +class ServerType(BaseDomain, DomainIdentityMixin): + """ServerType Domain + + :param id: int + ID of the server type + :param name: str + Unique identifier of the server type + :param description: str + Description of the server type + :param cores: int + Number of cpu cores a server of this type will have + :param memory: int + Memory a server of this type will have in GB + :param disk: int + Disk size a server of this type will have in GB + :param prices: Dict + Prices in different locations + :param storage_type: str + Type of server boot drive. Local has higher speed. Network has better availability. Choices: `local`, `network` + :param cpu_type: string + Type of cpu. Choices: `shared`, `dedicated` + :param architecture: string + Architecture of cpu. Choices: `x86`, `arm` + :param deprecated: bool + True if server type is deprecated. This field is deprecated. Use `deprecation` instead. + :param deprecation: :class:`DeprecationInfo `, None + Describes if, when & how the resources was deprecated. If this field is set to None the resource is not + deprecated. If it has a value, it is considered deprecated. + :param included_traffic: int + Free traffic per month in bytes + """ + + __slots__ = ( + "id", + "name", + "description", + "cores", + "memory", + "disk", + "prices", + "storage_type", + "cpu_type", + "architecture", + "deprecated", + "deprecation", + "included_traffic", + ) + + def __init__( + self, + id=None, + name=None, + description=None, + cores=None, + memory=None, + disk=None, + prices=None, + storage_type=None, + cpu_type=None, + architecture=None, + deprecated=None, + deprecation=None, + included_traffic=None, + ): + self.id = id + self.name = name + self.description = description + self.cores = cores + self.memory = memory + self.disk = disk + self.prices = prices + self.storage_type = storage_type + self.cpu_type = cpu_type + self.architecture = architecture + self.deprecated = deprecated + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) + self.included_traffic = included_traffic diff --git a/plugins/module_utils/vendor/hcloud/servers/__init__.py b/plugins/module_utils/vendor/hcloud/servers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/servers/client.py b/plugins/module_utils/vendor/hcloud/servers/client.py new file mode 100644 index 00000000..b88c69b0 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/servers/client.py @@ -0,0 +1,1089 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from ..datacenters.client import BoundDatacenter +from ..firewalls.client import BoundFirewall +from ..floating_ips.client import BoundFloatingIP +from ..images.client import BoundImage +from ..images.domain import CreateImageResponse +from ..isos.client import BoundIso +from ..networks.client import BoundNetwork # noqa +from ..networks.domain import Network # noqa +from ..placement_groups.client import BoundPlacementGroup +from ..primary_ips.client import BoundPrimaryIP +from ..server_types.client import BoundServerType +from ..volumes.client import BoundVolume +from .domain import ( + CreateServerResponse, + EnableRescueResponse, + IPv4Address, + IPv6Network, + PrivateNet, + PublicNetwork, + PublicNetworkFirewall, + RequestConsoleResponse, + ResetPasswordResponse, + Server, +) + + +class BoundServer(BoundModelBase): + model = Server + + def __init__(self, client, data, complete=True): + datacenter = data.get("datacenter") + if datacenter is not None: + data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) + + volumes = data.get("volumes", []) + if volumes: + volumes = [ + BoundVolume(client._client.volumes, {"id": volume}, complete=False) + for volume in volumes + ] + data["volumes"] = volumes + + image = data.get("image", None) + if image is not None: + data["image"] = BoundImage(client._client.images, image) + + iso = data.get("iso", None) + if iso is not None: + data["iso"] = BoundIso(client._client.isos, iso) + + server_type = data.get("server_type") + if server_type is not None: + data["server_type"] = BoundServerType( + client._client.server_types, server_type + ) + + public_net = data.get("public_net") + if public_net: + ipv4_address = ( + IPv4Address.from_dict(public_net["ipv4"]) + if public_net["ipv4"] is not None + else None + ) + ipv4_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv4"]["id"]}, + complete=False, + ) + if public_net["ipv4"] is not None + else None + ) + ipv6_network = ( + IPv6Network.from_dict(public_net["ipv6"]) + if public_net["ipv6"] is not None + else None + ) + ipv6_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv6"]["id"]}, + complete=False, + ) + if public_net["ipv6"] is not None + else None + ) + floating_ips = [ + BoundFloatingIP( + client._client.floating_ips, {"id": floating_ip}, complete=False + ) + for floating_ip in public_net["floating_ips"] + ] + firewalls = [ + PublicNetworkFirewall( + BoundFirewall( + client._client.firewalls, {"id": firewall["id"]}, complete=False + ), + status=firewall["status"], + ) + for firewall in public_net.get("firewalls", []) + ] + data["public_net"] = PublicNetwork( + ipv4=ipv4_address, + ipv6=ipv6_network, + primary_ipv4=ipv4_primary_ip, + primary_ipv6=ipv6_primary_ip, + floating_ips=floating_ips, + firewalls=firewalls, + ) + + private_nets = data.get("private_net") + if private_nets: + private_nets = [ + PrivateNet( + network=BoundNetwork( + client._client.networks, + {"id": private_net["network"]}, + complete=False, + ), + ip=private_net["ip"], + alias_ips=private_net["alias_ips"], + mac_address=private_net["mac_address"], + ) + for private_net in private_nets + ] + data["private_net"] = private_nets + + placement_group = data.get("placement_group") + if placement_group: + placement_group = BoundPlacementGroup( + client._client.placement_groups, placement_group + ) + data["placement_group"] = placement_group + + super().__init__(client, data, complete) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + """Returns all action objects for a server. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a server. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]]) -> BoundServer + """Updates a server. You can update a server’s name and a server’s labels. + + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundServer ` + """ + return self._client.update(self, name, labels) + + def delete(self): + # type: () -> BoundAction + """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. + + :return: :class:`BoundAction ` + """ + return self._client.delete(self) + + def power_off(self): + # type: () -> BoundAction + """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop + + :return: :class:`BoundAction ` + """ + return self._client.power_off(self) + + def power_on(self): + # type: () -> BoundAction + """Starts a server by turning its power on. + + :return: :class:`BoundAction ` + """ + return self._client.power_on(self) + + def reboot(self): + # type: () -> BoundAction + """Reboots a server gracefully by sending an ACPI request. + + :return: :class:`BoundAction ` + """ + return self._client.reboot(self) + + def reset(self): + # type: () -> BoundAction + """Cuts power to a server and starts it again. + + :return: :class:`BoundAction ` + """ + return self._client.reset(self) + + def shutdown(self): + # type: () -> BoundAction + """Shuts down a server gracefully by sending an ACPI shutdown request. + + :return: :class:`BoundAction ` + """ + return self._client.shutdown(self) + + def reset_password(self): + # type: () -> ResetPasswordResponse + """Resets the root password. Only works for Linux systems that are running the qemu guest agent. + + :return: :class:`ResetPasswordResponse ` + """ + return self._client.reset_password(self) + + def enable_rescue(self, type=None, ssh_keys=None): + # type: (str, Optional[List[str]]) -> EnableRescueResponse + """Enable the Hetzner Rescue System for this server. + + :param type: str + Type of rescue system to boot (default: linux64) + Choices: linux64, linux32, freebsd64 + :param ssh_keys: List[str] + Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. + :return: :class:`EnableRescueResponse ` + """ + return self._client.enable_rescue(self, type=type, ssh_keys=ssh_keys) + + def disable_rescue(self): + # type: () -> BoundAction + """Disables the Hetzner Rescue System for a server. + + :return: :class:`BoundAction ` + """ + return self._client.disable_rescue(self) + + def create_image(self, description=None, type=None, labels=None): + # type: (str, str, Optional[Dict[str, str]]) -> CreateImageResponse + """Creates an image (snapshot) from a server by copying the contents of its disks. + + :param description: str (optional) + Description of the image. If you do not set this we auto-generate one for you. + :param type: str (optional) + Type of image to create (default: snapshot) + Choices: snapshot, backup + :param labels: Dict[str, str] + User-defined labels (key-value pairs) + :return: :class:`CreateImageResponse ` + """ + return self._client.create_image(self, description, type, labels) + + def rebuild(self, image): + # type: (Image) -> BoundAction + """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. + + :param image: :class:`BoundImage ` or :class:`Image ` + :return: :class:`BoundAction ` + """ + return self._client.rebuild(self, image) + + def change_type(self, server_type, upgrade_disk): + # type: (BoundServerType, bool) -> BoundAction + """Changes the type (Cores, RAM and disk sizes) of a server. + + :param server_type: :class:`BoundServerType ` or :class:`ServerType ` + Server type the server should migrate to + :param upgrade_disk: boolean + If false, do not upgrade the disk. This allows downgrading the server type later. + :return: :class:`BoundAction ` + """ + return self._client.change_type(self, server_type, upgrade_disk) + + def enable_backup(self): + # type: () -> BoundAction + """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. + + :return: :class:`BoundAction ` + """ + return self._client.enable_backup(self) + + def disable_backup(self): + # type: () -> BoundAction + """Disables the automatic backup option and deletes all existing Backups for a Server. + + :return: :class:`BoundAction ` + """ + return self._client.disable_backup(self) + + def attach_iso(self, iso): + # type: (Iso) -> BoundAction + """Attaches an ISO to a server. + + :param iso: :class:`BoundIso ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + return self._client.attach_iso(self, iso) + + def detach_iso(self): + # type: () -> BoundAction + """Detaches an ISO from a server. + + :return: :class:`BoundAction ` + """ + return self._client.detach_iso(self) + + def change_dns_ptr(self, ip, dns_ptr): + # type: (str, Optional[str]) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + def change_protection(self, delete=None, rebuild=None): + # type: (Optional[bool], Optional[bool]) -> BoundAction + """Changes the protection configuration of the server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param delete: boolean + If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) + :param rebuild: boolean + If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete, rebuild) + + def request_console(self): + # type: () -> RequestConsoleResponse + """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. + + :return: :class:`RequestConsoleResponse ` + """ + return self._client.request_console(self) + + def attach_to_network(self, network, ip=None, alias_ips=None): + # type: (Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction + """Attaches a server to a network + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param ip: str + IP to request to be assigned to this server + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction ` + """ + return self._client.attach_to_network(self, network, ip, alias_ips) + + def detach_from_network(self, network): + # type: ( Union[Network,BoundNetwork]) -> BoundAction + """Detaches a server from a network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + return self._client.detach_from_network(self, network) + + def change_alias_ips(self, network, alias_ips): + # type: (Union[Network,BoundNetwork], List[str]) -> BoundAction + """Changes the alias IPs of an already attached network. + + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction ` + """ + return self._client.change_alias_ips(self, network, alias_ips) + + def add_to_placement_group(self, placement_group): + # type: (Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction + """Adds a server to a placement group. + + :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + return self._client.add_to_placement_group(self, placement_group) + + def remove_from_placement_group(self): + # type: () -> BoundAction + """Removes a server from a placement group. + + :return: :class:`BoundAction ` + """ + return self._client.remove_from_placement_group(self) + + +class ServersClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "servers" + + def get_by_id(self, id): + # type: (int) -> BoundServer + """Get a specific server + + :param id: int + :return: :class:`BoundServer ` + """ + response = self._client.request(url=f"/servers/{id}", method="GET") + return BoundServer(self, response["server"]) + + def get_list( + self, + name=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + status=None, # type: Optional[List[str]] + ): + # type: (...) -> PageResults[List[BoundServer], Meta] + """Get a list of servers from this account + + :param name: str (optional) + Can be used to filter servers by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param status: List[str] (optional) + Can be used to filter servers by their status. The response will only contain servers matching the status. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundServer `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/servers", method="GET", params=params) + + ass_servers = [ + BoundServer(self, server_data) for server_data in response["servers"] + ] + return self._add_meta_to_result(ass_servers, response) + + def get_all(self, name=None, label_selector=None, status=None): + # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundServer] + """Get all servers from this account + + :param name: str (optional) + Can be used to filter servers by their name. + :param label_selector: str (optional) + Can be used to filter servers by labels. The response will only contain servers matching the label selector. + :param status: List[str] (optional) + Can be used to filter servers by their status. The response will only contain servers matching the status. + :return: List[:class:`BoundServer `] + """ + return super().get_all(name=name, label_selector=label_selector, status=status) + + def get_by_name(self, name): + # type: (str) -> BoundServer + """Get server by name + + :param name: str + Used to get server by name. + :return: :class:`BoundServer ` + """ + return super().get_by_name(name) + + def create( + self, + name, # type: str + server_type, # type: ServerType + image, # type: Image + ssh_keys=None, # type: Optional[List[SSHKey]] + volumes=None, # type: Optional[List[Volume]] + firewalls=None, # type: Optional[List[Firewall]] + networks=None, # type: Optional[List[Network]] + user_data=None, # type: Optional[str] + labels=None, # type: Optional[Dict[str, str]] + location=None, # type: Optional[Location] + datacenter=None, # type: Optional[Datacenter] + start_after_create=True, # type: Optional[bool] + automount=None, # type: Optional[bool] + placement_group=None, # type: Optional[PlacementGroup] + public_net=None, # type: Optional[ServerCreatePublicNetwork] + ): + # type: (...) -> CreateServerResponse + """Creates a new server. Returns preliminary information about the server as well as an action that covers progress of creation. + + :param name: str + Name of the server to create (must be unique per project and a valid hostname as per RFC 1123) + :param server_type: :class:`BoundServerType ` or :class:`ServerType ` + Server type this server should be created with + :param image: :class:`BoundImage ` or :class:`Image ` + Image the server is created from + :param ssh_keys: List[:class:`BoundSSHKey ` or :class:`SSHKey `] (optional) + SSH keys which should be injected into the server at creation time + :param volumes: List[:class:`BoundVolume ` or :class:`Volume `] (optional) + Volumes which should be attached to the server at the creation time. Volumes must be in the same location. + :param networks: List[:class:`BoundNetwork ` or :class:`Network `] (optional) + Networks which should be attached to the server at the creation time. + :param user_data: str (optional) + Cloud-Init user data to use during server creation. This field is limited to 32KiB. + :param labels: Dict[str,str] (optional) + User-defined labels (key-value pairs) + :param location: :class:`BoundLocation ` or :class:`Location ` + :param datacenter: :class:`BoundDatacenter ` or :class:`Datacenter ` + :param start_after_create: boolean (optional) + Start Server right after creation. Defaults to True. + :param automount: boolean (optional) + Auto mount volumes after attach. + :param placement_group: :class:`BoundPlacementGroup ` or :class:`Location ` + Placement Group where server should be added during creation + :param public_net: :class:`ServerCreatePublicNetwork ` + Options to configure the public network of a server on creation + :return: :class:`CreateServerResponse ` + """ + data = { + "name": name, + "server_type": server_type.id_or_name, + "start_after_create": start_after_create, + "image": image.id_or_name, + } + + if location is not None: + data["location"] = location.id_or_name + if datacenter is not None: + data["datacenter"] = datacenter.id_or_name + if ssh_keys is not None: + data["ssh_keys"] = [ssh_key.id_or_name for ssh_key in ssh_keys] + if volumes is not None: + data["volumes"] = [volume.id for volume in volumes] + if networks is not None: + data["networks"] = [network.id for network in networks] + if firewalls is not None: + data["firewalls"] = [{"firewall": firewall.id} for firewall in firewalls] + if user_data is not None: + data["user_data"] = user_data + if labels is not None: + data["labels"] = labels + if automount is not None: + data["automount"] = automount + if placement_group is not None: + data["placement_group"] = placement_group.id + + if public_net is not None: + pn = { + "enable_ipv4": public_net.enable_ipv4, + "enable_ipv6": public_net.enable_ipv6, + } + if public_net.ipv4 is not None: + pn.update({"ipv4": public_net.ipv4.id}) + if public_net.ipv6 is not None: + pn.update({"ipv6": public_net.ipv6.id}) + data["public_net"] = pn + + response = self._client.request(url="/servers", method="POST", json=data) + + result = CreateServerResponse( + server=BoundServer(self, response["server"]), + action=BoundAction(self._client.actions, response["action"]), + next_actions=[ + BoundAction(self._client.actions, action) + for action in response["next_actions"] + ], + root_password=response["root_password"], + ) + return result + + def get_actions_list( + self, server, status=None, sort=None, page=None, per_page=None + ): + # type: (Server, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/servers/{server.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions(self, server, status=None, sort=None): + # type: (Server, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return super().get_actions(server, status=status, sort=sort) + + def update(self, server, name=None, labels=None): + # type:(Server, Optional[str], Optional[Dict[str, str]]) -> BoundServer + """Updates a server. You can update a server’s name and a server’s labels. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param name: str (optional) + New name to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundServer ` + """ + data = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/servers/{server.id}", + method="PUT", + json=data, + ) + return BoundServer(self, response["server"]) + + def delete(self, server): + # type: (Server) -> BoundAction + """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request(url=f"/servers/{server.id}", method="DELETE") + return BoundAction(self._client.actions, response["action"]) + + def power_off(self, server): + # type: (Server) -> Action + """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/poweroff", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def power_on(self, server): + # type: (servers.domain.Server) -> actions.domain.Action + """Starts a server by turning its power on. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/poweron", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reboot(self, server): + # type: (servers.domain.Server) -> actions.domain.Action + """Reboots a server gracefully by sending an ACPI request. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/reboot", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reset(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Cuts power to a server and starts it again. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/reset", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def shutdown(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Shuts down a server gracefully by sending an ACPI shutdown request. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/shutdown", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def reset_password(self, server): + # type: (servers.domain.Server) -> ResetPasswordResponse + """Resets the root password. Only works for Linux systems that are running the qemu guest agent. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`ResetPasswordResponse ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/reset_password".format( + server_id=server.id + ), + method="POST", + ) + return ResetPasswordResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response["root_password"], + ) + + def change_type(self, server, server_type, upgrade_disk): + # type: (servers.domain.Server, BoundServerType, bool) -> actions.domainAction + """Changes the type (Cores, RAM and disk sizes) of a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param server_type: :class:`BoundServerType ` or :class:`ServerType ` + Server type the server should migrate to + :param upgrade_disk: boolean + If false, do not upgrade the disk. This allows downgrading the server type later. + :return: :class:`BoundAction ` + """ + data = {"server_type": server_type.id_or_name, "upgrade_disk": upgrade_disk} + response = self._client.request( + url=f"/servers/{server.id}/actions/change_type", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def enable_rescue(self, server, type=None, ssh_keys=None): + # type: (servers.domain.Server, str, Optional[List[str]]) -> EnableRescueResponse + """Enable the Hetzner Rescue System for this server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param type: str + Type of rescue system to boot (default: linux64) + Choices: linux64, linux32, freebsd64 + :param ssh_keys: List[str] + Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. + :return: :class:`EnableRescueResponse ` + """ + data = {"type": type} + if ssh_keys is not None: + data.update({"ssh_keys": ssh_keys}) + + response = self._client.request( + url="/servers/{server_id}/actions/enable_rescue".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return EnableRescueResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response["root_password"], + ) + + def disable_rescue(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Disables the Hetzner Rescue System for a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/disable_rescue".format( + server_id=server.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def create_image(self, server, description=None, type=None, labels=None): + # type: (servers.domain.Server, str, str, Optional[Dict[str, str]]) -> CreateImageResponse + """Creates an image (snapshot) from a server by copying the contents of its disks. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param description: str (optional) + Description of the image. If you do not set this we auto-generate one for you. + :param type: str (optional) + Type of image to create (default: snapshot) + Choices: snapshot, backup + :param labels: Dict[str, str] + User-defined labels (key-value pairs) + :return: :class:`CreateImageResponse ` + """ + data = {} + if description is not None: + data.update({"description": description}) + + if type is not None: + data.update({"type": type}) + + if labels is not None: + data.update({"labels": labels}) + + response = self._client.request( + url=f"/servers/{server.id}/actions/create_image", + method="POST", + json=data, + ) + return CreateImageResponse( + action=BoundAction(self._client.actions, response["action"]), + image=BoundImage(self._client.images, response["image"]), + ) + + def rebuild(self, server, image): + # type: (servers.domain.Server, Image) -> actions.domainAction + """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param image: :class:`BoundImage ` or :class:`Image ` + :return: :class:`BoundAction ` + """ + data = {"image": image.id_or_name} + response = self._client.request( + url=f"/servers/{server.id}/actions/rebuild", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def enable_backup(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/enable_backup".format( + server_id=server.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def disable_backup(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Disables the automatic backup option and deletes all existing Backups for a Server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/disable_backup".format( + server_id=server.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def attach_iso(self, server, iso): + # type: (servers.domain.Server, Iso) -> actions.domainAction + """Attaches an ISO to a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param iso: :class:`BoundIso ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + data = {"iso": iso.id_or_name} + response = self._client.request( + url=f"/servers/{server.id}/actions/attach_iso", + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_iso(self, server): + # type: (servers.domain.Server) -> actions.domainAction + """Detaches an ISO from a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url=f"/servers/{server.id}/actions/detach_iso", + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr(self, server, ip, dns_ptr): + # type: (servers.domain.Server, str, str) -> actions.domainAction + """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + data = {"ip": ip, "dns_ptr": dns_ptr} + response = self._client.request( + url="/servers/{server_id}/actions/change_dns_ptr".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_protection(self, server, delete=None, rebuild=None): + # type: (servers.domain.Server, Optional[bool], Optional[bool]) -> actions.domainAction + """Changes the protection configuration of the server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param delete: boolean + If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) + :param rebuild: boolean + If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + if rebuild is not None: + data.update({"rebuild": rebuild}) + + response = self._client.request( + url="/servers/{server_id}/actions/change_protection".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def request_console(self, server): + # type: (servers.domain.Server) -> RequestConsoleResponse + """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`RequestConsoleResponse ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/request_console".format( + server_id=server.id + ), + method="POST", + ) + return RequestConsoleResponse( + action=BoundAction(self._client.actions, response["action"]), + wss_url=response["wss_url"], + password=response["password"], + ) + + def attach_to_network(self, server, network, ip=None, alias_ips=None): + # type: (Union[Server,BoundServer], Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction + """Attaches a server to a network + + :param server: :class:`BoundServer ` or :class:`Server ` + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param ip: str + IP to request to be assigned to this server + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction ` + """ + data = {"network": network.id} + if ip is not None: + data.update({"ip": ip}) + if alias_ips is not None: + data.update({"alias_ips": alias_ips}) + response = self._client.request( + url="/servers/{server_id}/actions/attach_to_network".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def detach_from_network(self, server, network): + # type: (Union[Server,BoundServer], Union[Network,BoundNetwork]) -> BoundAction + """Detaches a server from a network. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param network: :class:`BoundNetwork ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + data = {"network": network.id} + response = self._client.request( + url="/servers/{server_id}/actions/detach_from_network".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def change_alias_ips(self, server, network, alias_ips): + # type: (Union[Server,BoundServer], Union[Network,BoundNetwork], List[str]) -> BoundAction + """Changes the alias IPs of an already attached network. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param network: :class:`BoundNetwork ` or :class:`Network ` + :param alias_ips: List[str] + New alias IPs to set for this server. + :return: :class:`BoundAction ` + """ + data = {"network": network.id, "alias_ips": alias_ips} + response = self._client.request( + url="/servers/{server_id}/actions/change_alias_ips".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def add_to_placement_group(self, server, placement_group): + # type: (Union[Server,BoundServer], Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction + """Adds a server to a placement group. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` + :return: :class:`BoundAction ` + """ + data = {"placement_group": str(placement_group.id)} + response = self._client.request( + url="/servers/{server_id}/actions/add_to_placement_group".format( + server_id=server.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) + + def remove_from_placement_group(self, server): + # type: (Union[Server,BoundServer]) -> BoundAction + """Removes a server from a placement group. + + :param server: :class:`BoundServer ` or :class:`Server ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/servers/{server_id}/actions/remove_from_placement_group".format( + server_id=server.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/servers/domain.py b/plugins/module_utils/vendor/hcloud/servers/domain.py new file mode 100644 index 00000000..e8d537af --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/servers/domain.py @@ -0,0 +1,395 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain + + +class Server(BaseDomain): + """Server Domain + + :param id: int + ID of the server + :param name: str + Name of the server (must be unique per project and a valid hostname as per RFC 1123) + :param status: str + Status of the server Choices: `running`, `initializing`, `starting`, `stopping`, `off`, `deleting`, `migrating`, `rebuilding`, `unknown` + :param created: datetime + Point in time when the server was created + :param public_net: :class:`PublicNetwork ` + Public network information. + :param server_type: :class:`BoundServerType ` + :param datacenter: :class:`BoundDatacenter ` + :param image: :class:`BoundImage `, None + :param iso: :class:`BoundIso `, None + :param rescue_enabled: bool + True if rescue mode is enabled: Server will then boot into rescue system on next reboot. + :param locked: bool + True if server has been locked and is not available to user. + :param backup_window: str, None + Time window (UTC) in which the backup will run, or None if the backups are not enabled + :param outgoing_traffic: int, None + Outbound Traffic for the current billing period in bytes + :param ingoing_traffic: int, None + Inbound Traffic for the current billing period in bytes + :param included_traffic: int + Free Traffic for the current billing period in bytes + :param primary_disk_size: int + Size of the primary Disk + :param protection: dict + Protection configuration for the server + :param labels: dict + User-defined labels (key-value pairs) + :param volumes: List[:class:`BoundVolume `] + Volumes assigned to this server. + :param private_net: List[:class:`PrivateNet `] + Private networks information. + """ + + STATUS_RUNNING = "running" + """Server Status running""" + STATUS_INIT = "initializing" + """Server Status initializing""" + STATUS_STARTING = "starting" + """Server Status starting""" + STATUS_STOPPING = "stopping" + """Server Status stopping""" + STATUS_OFF = "off" + """Server Status off""" + STATUS_DELETING = "deleting" + """Server Status deleting""" + STATUS_MIGRATING = "migrating" + """Server Status migrating""" + STATUS_REBUILDING = "rebuilding" + """Server Status rebuilding""" + STATUS_UNKNOWN = "unknown" + """Server Status unknown""" + __slots__ = ( + "id", + "name", + "status", + "public_net", + "server_type", + "datacenter", + "image", + "iso", + "rescue_enabled", + "locked", + "backup_window", + "outgoing_traffic", + "ingoing_traffic", + "included_traffic", + "protection", + "labels", + "volumes", + "private_net", + "created", + "primary_disk_size", + "placement_group", + ) + + def __init__( + self, + id, + name=None, + status=None, + created=None, + public_net=None, + server_type=None, + datacenter=None, + image=None, + iso=None, + rescue_enabled=None, + locked=None, + backup_window=None, + outgoing_traffic=None, + ingoing_traffic=None, + included_traffic=None, + protection=None, + labels=None, + volumes=None, + private_net=None, + primary_disk_size=None, + placement_group=None, + ): + self.id = id + self.name = name + self.status = status + self.created = isoparse(created) if created else None + self.public_net = public_net + self.server_type = server_type + self.datacenter = datacenter + self.image = image + self.iso = iso + self.rescue_enabled = rescue_enabled + self.locked = locked + self.backup_window = backup_window + self.outgoing_traffic = outgoing_traffic + self.ingoing_traffic = ingoing_traffic + self.included_traffic = included_traffic + self.protection = protection + self.labels = labels + self.volumes = volumes + self.private_net = private_net + self.primary_disk_size = primary_disk_size + self.placement_group = placement_group + + +class CreateServerResponse(BaseDomain): + """Create Server Response Domain + + :param server: :class:`BoundServer ` + The created server + :param action: :class:`BoundAction ` + Shows the progress of the server creation + :param next_actions: List[:class:`BoundAction `] + Additional actions like a `start_server` action after the server creation + :param root_password: str, None + The root password of the server if no SSH-Key was given on server creation + """ + + __slots__ = ("server", "action", "next_actions", "root_password") + + def __init__( + self, + server, # type: BoundServer + action, # type: BoundAction + next_actions, # type: List[Action] + root_password, # type: str + ): + self.server = server + self.action = action + self.next_actions = next_actions + self.root_password = root_password + + +class ResetPasswordResponse(BaseDomain): + """Reset Password Response Domain + + :param action: :class:`BoundAction ` + Shows the progress of the server passwort reset action + :param root_password: str + The root password of the server + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action, # type: BoundAction + root_password, # type: str + ): + self.action = action + self.root_password = root_password + + +class EnableRescueResponse(BaseDomain): + """Enable Rescue Response Domain + + :param action: :class:`BoundAction ` + Shows the progress of the server enable rescue action + :param root_password: str + The root password of the server in the rescue mode + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action, # type: BoundAction + root_password, # type: str + ): + self.action = action + self.root_password = root_password + + +class RequestConsoleResponse(BaseDomain): + """Request Console Response Domain + + :param action: :class:`BoundAction ` + Shows the progress of the server request console action + :param wss_url: str + URL of websocket proxy to use. This includes a token which is valid for a limited time only. + :param password: str + VNC password to use for this connection. This password only works in combination with a wss_url with valid token. + """ + + __slots__ = ("action", "wss_url", "password") + + def __init__( + self, + action, # type: BoundAction + wss_url, # type: str + password, # type: str + ): + self.action = action + self.wss_url = wss_url + self.password = password + + +class PublicNetwork(BaseDomain): + """Public Network Domain + + :param ipv4: :class:`IPv4Address ` + :param ipv6: :class:`IPv6Network ` + :param floating_ips: List[:class:`BoundFloatingIP `] + :param primary_ipv4: :class:`BoundPrimaryIP ` + :param primary_ipv6: :class:`BoundPrimaryIP ` + :param firewalls: List[:class:`PublicNetworkFirewall `] + """ + + __slots__ = ( + "ipv4", + "ipv6", + "floating_ips", + "firewalls", + "primary_ipv4", + "primary_ipv6", + ) + + def __init__( + self, + ipv4, # type: IPv4Address + ipv6, # type: IPv6Network + floating_ips, # type: List[BoundFloatingIP] + primary_ipv4, # type: BoundPrimaryIP + primary_ipv6, # type: BoundPrimaryIP + firewalls=None, # type: List[PublicNetworkFirewall] + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.floating_ips = floating_ips + self.firewalls = firewalls + self.primary_ipv4 = primary_ipv4 + self.primary_ipv6 = primary_ipv6 + + +class PublicNetworkFirewall(BaseDomain): + """Public Network Domain + + :param firewall: :class:`BoundFirewall ` + :param status: str + """ + + __slots__ = ("firewall", "status") + + STATUS_APPLIED = "applied" + """Public Network Firewall Status applied""" + STATUS_PENDING = "pending" + """Public Network Firewall Status pending""" + + def __init__( + self, + firewall, # type: BoundFirewall + status, # type: str + ): + self.firewall = firewall + self.status = status + + +class IPv4Address(BaseDomain): + """IPv4 Address Domain + + :param ip: str + The IPv4 Address + :param blocked: bool + Determine if the IP is blocked + :param dns_ptr: str + DNS PTR for the ip + """ + + __slots__ = ("ip", "blocked", "dns_ptr") + + def __init__( + self, + ip, # type: str + blocked, # type: bool + dns_ptr, # type: str + ): + self.ip = ip + self.blocked = blocked + self.dns_ptr = dns_ptr + + +class IPv6Network(BaseDomain): + """IPv6 Network Domain + + :param ip: str + The IPv6 Network as CIDR Notation + :param blocked: bool + Determine if the Network is blocked + :param dns_ptr: dict + DNS PTR Records for the Network as Dict + :param network: str + The network without the network mask + :param network_mask: str + The network mask + """ + + __slots__ = ("ip", "blocked", "dns_ptr", "network", "network_mask") + + def __init__( + self, + ip, # type: str + blocked, # type: bool + dns_ptr, # type: list + ): + self.ip = ip + self.blocked = blocked + self.dns_ptr = dns_ptr + ip_parts = self.ip.split("/") # 2001:db8::/64 to 2001:db8:: and 64 + self.network = ip_parts[0] + self.network_mask = ip_parts[1] + + +class PrivateNet(BaseDomain): + """PrivateNet Domain + + :param network: :class:`BoundNetwork ` + The network the server is attached to + :param ip: str + The main IP Address of the server in the Network + :param alias_ips: List[str] + The alias ips for a server + :param mac_address: str + The mac address of the interface on the server + """ + + __slots__ = ("network", "ip", "alias_ips", "mac_address") + + def __init__( + self, + network, # type: BoundNetwork + ip, # type: str + alias_ips, # type: List[str] + mac_address, # type: str + ): + self.network = network + self.ip = ip + self.alias_ips = alias_ips + self.mac_address = mac_address + + +class ServerCreatePublicNetwork(BaseDomain): + """Server Create Public Network Domain + + :param ipv4: Optional[:class:`PrimaryIP `] + :param ipv6: Optional[:class:`PrimaryIP `] + :param enable_ipv4: bool + :param enable_ipv6: bool + """ + + __slots__ = ("ipv4", "ipv6", "enable_ipv4", "enable_ipv6") + + def __init__( + self, + ipv4=None, # type: hcloud.primary_ips.domain.PrimaryIP + ipv6=None, # type: hcloud.primary_ips.domain.PrimaryIP + enable_ipv4=True, # type: bool + enable_ipv6=True, # type: bool + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enable_ipv4 = enable_ipv4 + self.enable_ipv6 = enable_ipv6 diff --git a/plugins/module_utils/vendor/hcloud/ssh_keys/__init__.py b/plugins/module_utils/vendor/hcloud/ssh_keys/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/ssh_keys/client.py b/plugins/module_utils/vendor/hcloud/ssh_keys/client.py new file mode 100644 index 00000000..e9a94023 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/ssh_keys/client.py @@ -0,0 +1,170 @@ +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import SSHKey + + +class BoundSSHKey(BoundModelBase): + model = SSHKey + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey + """Updates an SSH key. You can update an SSH key name and an SSH key labels. + + :param description: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey + """ + return self._client.update(self, name, labels) + + def delete(self): + # type: () -> bool + """Deletes an SSH key. It cannot be used anymore. + :return: boolean + """ + return self._client.delete(self) + + +class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "ssh_keys" + + def get_by_id(self, id): + # type: (int) -> BoundSSHKey + """Get a specific SSH Key by its ID + + :param id: int + :return: :class:`BoundSSHKey ` + """ + response = self._client.request(url=f"/ssh_keys/{id}", method="GET") + return BoundSSHKey(self, response["ssh_key"]) + + def get_list( + self, + name=None, # type: Optional[str] + fingerprint=None, # type: Optional[str] + label_selector=None, # type: Optional[str] + page=None, # type: Optional[int] + per_page=None, # type: Optional[int] + ): + # type: (...) -> PageResults[List[BoundSSHKey], Meta] + """Get a list of SSH keys from the account + + :param name: str (optional) + Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. + :param fingerprint: str (optional) + Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. + :param label_selector: str (optional) + Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundSSHKey `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if fingerprint is not None: + params["fingerprint"] = fingerprint + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/ssh_keys", method="GET", params=params) + + ass_ssh_keys = [ + BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] + ] + return self._add_meta_to_result(ass_ssh_keys, response) + + def get_all(self, name=None, fingerprint=None, label_selector=None): + # type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey] + """Get all SSH keys from the account + + :param name: str (optional) + Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. + :param fingerprint: str (optional) + Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. + :param label_selector: str (optional) + Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. + :return: List[:class:`BoundSSHKey `] + """ + return super().get_all( + name=name, fingerprint=fingerprint, label_selector=label_selector + ) + + def get_by_name(self, name): + # type: (str) -> SSHKeysClient + """Get ssh key by name + + :param name: str + Used to get ssh key by name. + :return: :class:`BoundSSHKey ` + """ + return super().get_by_name(name) + + def get_by_fingerprint(self, fingerprint): + # type: (str) -> BoundSSHKey + """Get ssh key by fingerprint + + :param fingerprint: str + Used to get ssh key by fingerprint. + :return: :class:`BoundSSHKey ` + """ + response = self.get_list(fingerprint=fingerprint) + sshkeys = response.ssh_keys + return sshkeys[0] if sshkeys else None + + def create(self, name, public_key, labels=None): + # type: (str, str, Optional[Dict[str, str]]) -> BoundSSHKey + """Creates a new SSH key with the given name and public_key. + + :param name: str + :param public_key: str + Public Key of the SSH Key you want create + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey ` + """ + data = {"name": name, "public_key": public_key} + if labels is not None: + data["labels"] = labels + response = self._client.request(url="/ssh_keys", method="POST", json=data) + return BoundSSHKey(self, response["ssh_key"]) + + def update(self, ssh_key, name=None, labels=None): + # type: (SSHKey, Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey + """Updates an SSH key. You can update an SSH key name and an SSH key labels. + + :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` + :param name: str (optional) + New Description to set + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundSSHKey ` + """ + data = {} + if name is not None: + data["name"] = name + if labels is not None: + data["labels"] = labels + response = self._client.request( + url=f"/ssh_keys/{ssh_key.id}", + method="PUT", + json=data, + ) + return BoundSSHKey(self, response["ssh_key"]) + + def delete(self, ssh_key): + # type: (SSHKey) -> bool + self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE") + """Deletes an SSH key. It cannot be used anymore. + + :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` + :return: True + """ + # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised + return True diff --git a/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py b/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py new file mode 100644 index 00000000..cf6e198b --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/ssh_keys/domain.py @@ -0,0 +1,42 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class SSHKey(BaseDomain, DomainIdentityMixin): + """SSHKey Domain + + :param id: int + ID of the SSH key + :param name: str + Name of the SSH key (must be unique per project) + :param fingerprint: str + Fingerprint of public key + :param public_key: str + Public Key + :param labels: Dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the SSH Key was created + """ + + __slots__ = ("id", "name", "fingerprint", "public_key", "labels", "created") + + def __init__( + self, + id=None, + name=None, + fingerprint=None, + public_key=None, + labels=None, + created=None, + ): + self.id = id + self.name = name + self.fingerprint = fingerprint + self.public_key = public_key + self.labels = labels + self.created = isoparse(created) if created else None diff --git a/plugins/module_utils/vendor/hcloud/volumes/__init__.py b/plugins/module_utils/vendor/hcloud/volumes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/vendor/hcloud/volumes/client.py b/plugins/module_utils/vendor/hcloud/volumes/client.py new file mode 100644 index 00000000..f7f6bef5 --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/volumes/client.py @@ -0,0 +1,395 @@ +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from ..locations.client import BoundLocation +from .domain import CreateVolumeResponse, Volume + + +class BoundVolume(BoundModelBase): + model = Volume + + def __init__(self, client, data, complete=True): + location = data.get("location") + if location is not None: + data["location"] = BoundLocation(client._client.locations, location) + + from ..servers.client import BoundServer + + server = data.get("server") + if server is not None: + data["server"] = BoundServer( + client._client.servers, {"id": server}, complete=False + ) + super().__init__(client, data, complete) + + def get_actions_list(self, status=None, sort=None, page=None, per_page=None): + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + """Returns all action objects for a volume. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + return self._client.get_actions_list(self, status, sort, page, per_page) + + def get_actions(self, status=None, sort=None): + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a volume. + + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort:List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return self._client.get_actions(self, status, sort) + + def update(self, name=None, labels=None): + # type: (Optional[str], Optional[Dict[str, str]]) -> BoundAction + """Updates the volume properties. + + :param name: str (optional) + New volume name + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundAction ` + """ + return self._client.update(self, name, labels) + + def delete(self): + # type: () -> BoundAction + """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. + + :return: boolean + """ + return self._client.delete(self) + + def attach(self, server, automount=None): + # type: (Union[Server, BoundServer], Optional[bool]) -> BoundAction + """Attaches a volume to a server. Works only if the server is in the same location as the volume. + + :param server: :class:`BoundServer ` or :class:`Server ` + :param automount: boolean + :return: :class:`BoundAction ` + """ + return self._client.attach(self, server, automount) + + def detach(self): + # type: () -> BoundAction + """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. + + :return: :class:`BoundAction ` + """ + return self._client.detach(self) + + def resize(self, size): + # type: (int) -> BoundAction + """Changes the size of a volume. Note that downsizing a volume is not possible. + + :param size: int + New volume size in GB (must be greater than current size) + :return: :class:`BoundAction ` + """ + return self._client.resize(self, size) + + def change_protection(self, delete=None): + # type: (Optional[bool]) -> BoundAction + """Changes the protection configuration of a volume. + + :param delete: boolean + If True, prevents the volume from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + +class VolumesClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "volumes" + + def get_by_id(self, id): + # type: (int) -> volumes.client.BoundVolume + """Get a specific volume by its id + + :param id: int + :return: :class:`BoundVolume ` + """ + response = self._client.request(url=f"/volumes/{id}", method="GET") + return BoundVolume(self, response["volume"]) + + def get_list( + self, name=None, label_selector=None, page=None, per_page=None, status=None + ): + # type: (Optional[str], Optional[str], Optional[int], Optional[int], Optional[List[str]]) -> PageResults[List[BoundVolume], Meta] + """Get a list of volumes from this account + + :param name: str (optional) + Can be used to filter volumes by their name. + :param label_selector: str (optional) + Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. + :param status: List[str] (optional) + Can be used to filter volumes by their status. The response will only contain volumes matching the status. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundVolume `], :class:`Meta `) + """ + params = {} + if name is not None: + params["name"] = name + if label_selector is not None: + params["label_selector"] = label_selector + if status is not None: + params["status"] = status + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request(url="/volumes", method="GET", params=params) + volumes = [ + BoundVolume(self, volume_data) for volume_data in response["volumes"] + ] + return self._add_meta_to_result(volumes, response) + + def get_all(self, label_selector=None, status=None): + # type: (Optional[str], Optional[List[str]]) -> List[BoundVolume] + """Get all volumes from this account + + :param label_selector: + Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. + :param status: List[str] (optional) + Can be used to filter volumes by their status. The response will only contain volumes matching the status. + :return: List[:class:`BoundVolume `] + """ + return super().get_all(label_selector=label_selector, status=status) + + def get_by_name(self, name): + # type: (str) -> BoundVolume + """Get volume by name + + :param name: str + Used to get volume by name. + :return: :class:`BoundVolume ` + """ + return super().get_by_name(name) + + def create( + self, + size, # type: int + name, # type: str + labels=None, # type: Optional[str] + location=None, # type: Optional[Location] + server=None, # type: Optional[Server], + automount=None, # type: Optional[bool], + format=None, # type: Optional[str], + ): + # type: (...) -> CreateVolumeResponse + """Creates a new volume attached to a server. + + :param size: int + Size of the volume in GB + :param name: str + Name of the volume + :param labels: Dict[str,str] (optional) + User-defined labels (key-value pairs) + :param location: :class:`BoundLocation ` or :class:`Location ` + :param server: :class:`BoundServer ` or :class:`Server ` + :param automount: boolean (optional) + Auto mount volumes after attach. + :param format: str (optional) + Format volume after creation. One of: xfs, ext4 + :return: :class:`CreateVolumeResponse ` + """ + + if size <= 0: + raise ValueError("size must be greater than 0") + + if not (bool(location) ^ bool(server)): + raise ValueError("only one of server or location must be provided") + + data = {"name": name, "size": size} + if labels is not None: + data["labels"] = labels + if location is not None: + data["location"] = location.id_or_name + + if server is not None: + data["server"] = server.id + if automount is not None: + data["automount"] = automount + if format is not None: + data["format"] = format + + response = self._client.request(url="/volumes", json=data, method="POST") + + result = CreateVolumeResponse( + volume=BoundVolume(self, response["volume"]), + action=BoundAction(self._client.actions, response["action"]), + next_actions=[ + BoundAction(self._client.actions, action) + for action in response["next_actions"] + ], + ) + return result + + def get_actions_list( + self, volume, status=None, sort=None, page=None, per_page=None + ): + # type: (Volume, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + """Returns all action objects for a volume. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort: List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :return: (List[:class:`BoundAction `], :class:`Meta `) + """ + params = {} + if status is not None: + params["status"] = status + if sort is not None: + params["sort"] = sort + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + + response = self._client.request( + url=f"/volumes/{volume.id}/actions", + method="GET", + params=params, + ) + actions = [ + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] + ] + return add_meta_to_result(actions, response, "actions") + + def get_actions(self, volume, status=None, sort=None): + # type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + """Returns all action objects for a volume. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param status: List[str] (optional) + Response will have only actions with specified statuses. Choices: `running` `success` `error` + :param sort:List[str] (optional) + Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` + :return: List[:class:`BoundAction `] + """ + return super().get_actions(volume, status=status, sort=sort) + + def update(self, volume, name=None, labels=None): + # type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume + """Updates the volume properties. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param name: str (optional) + New volume name + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :return: :class:`BoundAction ` + """ + data = {} + if name is not None: + data.update({"name": name}) + if labels is not None: + data.update({"labels": labels}) + response = self._client.request( + url=f"/volumes/{volume.id}", + method="PUT", + json=data, + ) + return BoundVolume(self, response["volume"]) + + def delete(self, volume): + # type: (Union[Volume, BoundVolume]) -> BoundAction + """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :return: boolean + """ + self._client.request(url=f"/volumes/{volume.id}", method="DELETE") + return True + + def resize(self, volume, size): + # type: (Union[Volume, BoundVolume], int) -> BoundAction + """Changes the size of a volume. Note that downsizing a volume is not possible. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param size: int + New volume size in GB (must be greater than current size) + :return: :class:`BoundAction ` + """ + data = self._client.request( + url=f"/volumes/{volume.id}/actions/resize", + json={"size": size}, + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def attach(self, volume, server, automount=None): + # type: (Union[Volume, BoundVolume], Union[Server, BoundServer], Optional[bool]) -> BoundAction + """Attaches a volume to a server. Works only if the server is in the same location as the volume. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param server: :class:`BoundServer ` or :class:`Server ` + :param automount: boolean + :return: :class:`BoundAction ` + """ + data = {"server": server.id} + if automount is not None: + data["automount"] = automount + + data = self._client.request( + url=f"/volumes/{volume.id}/actions/attach", + json=data, + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def detach(self, volume): + # type: (Union[Volume, BoundVolume]) -> BoundAction + """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :return: :class:`BoundAction ` + """ + data = self._client.request( + url=f"/volumes/{volume.id}/actions/detach", + method="POST", + ) + return BoundAction(self._client.actions, data["action"]) + + def change_protection(self, volume, delete=None): + # type: (Union[Volume, BoundVolume], Optional[bool], Optional[bool]) -> BoundAction + """Changes the protection configuration of a volume. + + :param volume: :class:`BoundVolume ` or :class:`Volume ` + :param delete: boolean + If True, prevents the volume from being deleted + :return: :class:`BoundAction ` + """ + data = {} + if delete is not None: + data.update({"delete": delete}) + + response = self._client.request( + url="/volumes/{volume_id}/actions/change_protection".format( + volume_id=volume.id + ), + method="POST", + json=data, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/plugins/module_utils/vendor/hcloud/volumes/domain.py b/plugins/module_utils/vendor/hcloud/volumes/domain.py new file mode 100644 index 00000000..1185cf4c --- /dev/null +++ b/plugins/module_utils/vendor/hcloud/volumes/domain.py @@ -0,0 +1,103 @@ +try: + from dateutil.parser import isoparse +except ImportError: + isoparse = None + +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class Volume(BaseDomain, DomainIdentityMixin): + """Volume Domain + + :param id: int + ID of the Volume + :param name: str + Name of the Volume + :param server: :class:`BoundServer `, None + Server the Volume is attached to, None if it is not attached at all. + :param created: datetime + Point in time when the Volume was created + :param location: :class:`BoundLocation ` + Location of the Volume. Volume can only be attached to Servers in the same location. + :param size: int + Size in GB of the Volume + :param linux_device: str + Device path on the file system for the Volume + :param protection: dict + Protection configuration for the Volume + :param labels: dict + User-defined labels (key-value pairs) + :param status: str + Current status of the volume Choices: `creating`, `available` + :param format: str, None + Filesystem of the volume if formatted on creation, None if not formatted on creation. + """ + + STATUS_CREATING = "creating" + """Volume Status creating""" + STATUS_AVAILABLE = "available" + """Volume Status available""" + + __slots__ = ( + "id", + "name", + "server", + "location", + "size", + "linux_device", + "format", + "protection", + "labels", + "status", + "created", + ) + + def __init__( + self, + id, + name=None, + server=None, + created=None, + location=None, + size=None, + linux_device=None, + format=None, + protection=None, + labels=None, + status=None, + ): + self.id = id + self.name = name + self.server = server + self.created = isoparse(created) if created else None + self.location = location + self.size = size + self.linux_device = linux_device + self.format = format + self.protection = protection + self.labels = labels + self.status = status + + +class CreateVolumeResponse(BaseDomain): + """Create Volume Response Domain + + :param volume: :class:`BoundVolume ` + The created volume + :param action: :class:`BoundAction ` + The action that shows the progress of the Volume Creation + :param next_actions: List[:class:`BoundAction `] + List of actions that are performed after the creation, like attaching to a server + """ + + __slots__ = ("volume", "action", "next_actions") + + def __init__( + self, + volume, # type: BoundVolume + action, # type: BoundAction + next_actions, # type: List[BoundAction] + ): + self.volume = volume + self.action = action + self.next_actions = next_actions diff --git a/plugins/modules/hcloud_firewall.py b/plugins/modules/hcloud_firewall.py index 4ecf4805..ba33a848 100644 --- a/plugins/modules/hcloud_firewall.py +++ b/plugins/modules/hcloud_firewall.py @@ -171,13 +171,12 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud import APIException - from hcloud.firewalls.domain import FirewallRule -except ImportError: - APIException = None - FirewallRule = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import ( + APIException, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.firewalls.domain import ( + FirewallRule, +) class AnsibleHcloudFirewall(Hcloud): diff --git a/plugins/modules/hcloud_load_balancer_service.py b/plugins/modules/hcloud_load_balancer_service.py index 93b97edc..95926807 100644 --- a/plugins/modules/hcloud_load_balancer_service.py +++ b/plugins/modules/hcloud_load_balancer_service.py @@ -282,17 +282,15 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud import APIException - from hcloud.load_balancers.domain import ( - LoadBalancerHealtCheckHttp, - LoadBalancerHealthCheck, - LoadBalancerService, - LoadBalancerServiceHttp, - ) -except ImportError: - APIException = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import ( + APIException, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.load_balancers.domain import ( + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, + LoadBalancerService, + LoadBalancerServiceHttp, +) class AnsibleHcloudLoadBalancerService(Hcloud): diff --git a/plugins/modules/hcloud_load_balancer_target.py b/plugins/modules/hcloud_load_balancer_target.py index 922e3fb7..266903bc 100644 --- a/plugins/modules/hcloud_load_balancer_target.py +++ b/plugins/modules/hcloud_load_balancer_target.py @@ -138,17 +138,11 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.load_balancers.domain import ( - LoadBalancerTarget, - LoadBalancerTargetIP, - LoadBalancerTargetLabelSelector, - ) -except ImportError: - LoadBalancerTarget = None - LoadBalancerTargetLabelSelector = None - LoadBalancerTargetIP = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.load_balancers.domain import ( + LoadBalancerTarget, + LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, +) class AnsibleHcloudLoadBalancerTarget(Hcloud): diff --git a/plugins/modules/hcloud_route.py b/plugins/modules/hcloud_route.py index c922a020..c54931b8 100644 --- a/plugins/modules/hcloud_route.py +++ b/plugins/modules/hcloud_route.py @@ -90,11 +90,9 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.networks.domain import NetworkRoute -except ImportError: - NetworkRoute = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.networks.domain import ( + NetworkRoute, +) class AnsibleHcloudRoute(Hcloud): diff --git a/plugins/modules/hcloud_server.py b/plugins/modules/hcloud_server.py index 555323c2..9e32090b 100644 --- a/plugins/modules/hcloud_server.py +++ b/plugins/modules/hcloud_server.py @@ -333,18 +333,19 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.firewalls.domain import FirewallResource - from hcloud.servers.domain import Server, ServerCreatePublicNetwork - from hcloud.ssh_keys.domain import SSHKey - from hcloud.volumes.domain import Volume -except ImportError: - Volume = None - SSHKey = None - Server = None - ServerCreatePublicNetwork = None - FirewallResource = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.firewalls.domain import ( + FirewallResource, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.servers.domain import ( + Server, + ServerCreatePublicNetwork, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.ssh_keys.domain import ( + SSHKey, +) +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.volumes.domain import ( + Volume, +) class AnsibleHcloudServer(Hcloud): diff --git a/plugins/modules/hcloud_server_network.py b/plugins/modules/hcloud_server_network.py index 0a0fe7d0..d10000df 100644 --- a/plugins/modules/hcloud_server_network.py +++ b/plugins/modules/hcloud_server_network.py @@ -115,11 +115,9 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud import APIException -except ImportError: - APIException = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud import ( + APIException, +) class AnsibleHcloudServerNetwork(Hcloud): diff --git a/plugins/modules/hcloud_subnetwork.py b/plugins/modules/hcloud_subnetwork.py index 53f6d6d7..523d7b1b 100644 --- a/plugins/modules/hcloud_subnetwork.py +++ b/plugins/modules/hcloud_subnetwork.py @@ -127,11 +127,9 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud - -try: - from hcloud.networks.domain import NetworkSubnet -except ImportError: - NetworkSubnet = None +from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.networks.domain import ( + NetworkSubnet, +) class AnsibleHcloudSubnetwork(Hcloud): diff --git a/scripts/vendor.py b/scripts/vendor.py new file mode 100755 index 00000000..281f7cf3 --- /dev/null +++ b/scripts/vendor.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +""" +Fetch and bundles the hcloud package inside the collection. + +Fetch the desired version `HCLOUD_VERSION` from https://github.com/hetznercloud/hcloud-python +`HCLOUD_SOURCE_URL` using git, apply some code modifications to comply with ansible, +move the modified files at the vendor location `HCLOUD_VENDOR_PATH`. +""" + +import logging +import re +from pathlib import Path +from shutil import move, rmtree +from subprocess import check_call +from tempfile import TemporaryDirectory +from textwrap import dedent + +logger = logging.getLogger("vendor") + +HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python" +HCLOUD_VERSION = "v1.24.0" +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", + dedent( + r""" + try: + import requests + except ImportError: + requests = None + """ + ).strip(), + content, + ) + + # Wrap dateutil imports + content = re.sub( + r"from dateutil.parser import isoparse", + dedent( + r""" + try: + from dateutil.parser import isoparse + except ImportError: + isoparse = None + """ + ).strip(), + content, + ) + + # Remove requests.Response typings + content = re.sub( + r": requests\.Response", + r"", + content, + ) + + if content != content_orig: + logger.info("Applied code modifications on %s", file) + + file.write_text(content) + + +def main() -> int: + with TemporaryDirectory() as tmp_dir: + tmp_dir_path = Path(tmp_dir) + logger.info("Created temporary directory %s", tmp_dir_path) + + check_call(["git", "clone", "--depth=1", "--branch", HCLOUD_VERSION, HCLOUD_SOURCE_URL, tmp_dir_path]) + logger.info("Cloned the source files in %s", tmp_dir_path) + + apply_code_modifications(tmp_dir_path / "hcloud") + logger.info("Applied code modifications on the source files") + + rmtree(HCLOUD_VENDOR_PATH) + move(tmp_dir_path / "hcloud", HCLOUD_VENDOR_PATH) + logger.info("Bundled the modified sources files in the collection") + + return 0 + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)-8s: %(message)s") + raise SystemExit(main()) diff --git a/tests/integration/constraints.txt b/tests/integration/constraints.txt index 19d5ecf2..7e72ee47 100644 --- a/tests/integration/constraints.txt +++ b/tests/integration/constraints.txt @@ -1 +1,2 @@ -hcloud >= 1.10.0 # minimum version +python-dateutil>=2.7.5 +requests>=2.20 diff --git a/tests/integration/requirements.txt b/tests/integration/requirements.txt index d3249def..b7475e1a 100644 --- a/tests/integration/requirements.txt +++ b/tests/integration/requirements.txt @@ -1,2 +1,3 @@ netaddr -hcloud +python-dateutil +requests diff --git a/tests/utils/gitlab/gitlab.sh b/tests/utils/gitlab/gitlab.sh index b09bd2f3..e10cb67b 100755 --- a/tests/utils/gitlab/gitlab.sh +++ b/tests/utils/gitlab/gitlab.sh @@ -59,7 +59,8 @@ retry ansible-galaxy -vvv collection install community.general retry ansible-galaxy -vvv collection install ansible.netcommon retry ansible-galaxy -vvv collection install community.internal_test_tools retry pip install netaddr --disable-pip-version-check -retry pip install hcloud +retry pip install python-dateutil +retry pip install requests retry pip install rstcheck # END: HACK diff --git a/tests/utils/gitlab/sanity.sh b/tests/utils/gitlab/sanity.sh index 4ee96aef..359fb20c 100755 --- a/tests/utils/gitlab/sanity.sh +++ b/tests/utils/gitlab/sanity.sh @@ -43,5 +43,7 @@ pip install pylint==2.5.3 # shellcheck disable=SC2086 ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ --base-branch "${base_branch}" \ + --exclude plugins/module_utils/vendor/ \ + --exclude scripts/ \ --exclude tests/utils/ \ "${options[@]}" --allow-disabled diff --git a/tests/utils/shippable/sanity.sh b/tests/utils/shippable/sanity.sh index 9339aeda..30504f22 100755 --- a/tests/utils/shippable/sanity.sh +++ b/tests/utils/shippable/sanity.sh @@ -24,4 +24,6 @@ fi # shellcheck disable=SC2086 ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ --docker --base-branch "${base_branch}" \ + --exclude plugins/module_utils/vendor/ \ + --exclude scripts/ \ --allow-disabled diff --git a/tests/utils/shippable/shippable.sh b/tests/utils/shippable/shippable.sh index 8c0bd6de..c8996c50 100755 --- a/tests/utils/shippable/shippable.sh +++ b/tests/utils/shippable/shippable.sh @@ -77,7 +77,8 @@ fi retry ansible-galaxy -vvv collection install community.general retry ansible-galaxy -vvv collection install ansible.netcommon -retry pip install hcloud +retry pip install python-dateutil +retry pip install requests retry pip install netaddr --disable-pip-version-check retry ansible-galaxy -vvv collection install community.internal_test_tools # END: HACK