#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2020, Hetzner Cloud GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: hcloud_load_balancer_service short_description: Create and manage the services of cloud Load Balancers on the Hetzner Cloud. description: - Create, update and manage the services of cloud Load Balancers on the Hetzner Cloud. author: - Lukas Kaemmerling (@LKaemmerling) version_added: 0.1.0 options: load_balancer: description: - The Name of the Hetzner Cloud Load Balancer the service belongs to type: str required: true listen_port: description: - The port the service listens on, i.e. the port users can connect to. type: int required: true destination_port: description: - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. - Required if services does not exist and protocol is tcp. type: int protocol: description: - Protocol of the service. - Required if Load Balancer does not exist. type: str choices: [ http, https, tcp ] proxyprotocol: description: - Enable the PROXY protocol. type: bool default: False http: description: - Configuration for HTTP and HTTPS services type: dict suboptions: cookie_name: description: - Name of the cookie which will be set when you enable sticky sessions type: str cookie_lifetime: description: - Lifetime of the cookie which will be set when you enable sticky sessions, in seconds type: int certificates: description: - List of Names or IDs of certificates type: list elements: str sticky_sessions: description: - Enable or disable sticky_sessions type: bool default: False redirect_http: description: - Redirect Traffic from Port 80 to Port 443, only available if protocol is https type: bool default: False health_check: description: - Configuration for health checks type: dict suboptions: protocol: description: - Protocol the health checks will be performed over type: str choices: [ http, https, tcp ] port: description: - Port the health check will be performed on type: int interval: description: - Interval of health checks, in seconds type: int timeout: description: - Timeout of health checks, in seconds type: int retries: description: - Number of retries until a target is marked as unhealthy type: int http: description: - Additional Configuration of health checks with protocol http/https type: dict suboptions: domain: description: - Domain we will set within the HTTP HOST header type: str path: description: - Path we will try to access type: str response: description: - Response we expect, if response is not within the health check response the target is unhealthy type: str status_codes: description: - List of HTTP status codes we expect to get when we perform the health check. type: list elements: str tls: description: - Verify the TLS certificate, only available if health check protocol is https type: bool default: False state: description: - State of the Load Balancer. default: present choices: [ absent, present ] type: str extends_documentation_fragment: - hetzner.hcloud.hcloud requirements: - hcloud-python >= 1.8.1 ''' EXAMPLES = """ - name: Create a basic Load Balancer service with Port 80 hcloud_load_balancer_service: load_balancer: my-load-balancer protocol: http listen_port: 80 state: present - name: Ensure the Load Balancer is absent (remove if needed) hcloud_load_balancer_service: load_balancer: my-Load Balancer protocol: http listen_port: 80 state: absent """ RETURN = """ hcloud_load_balancer_service: description: The Load Balancer service instance returned: Always type: complex contains: load_balancer: description: The name of the Load Balancer where the service belongs to returned: always type: str sample: my-load-balancer listen_port: description: The port the service listens on, i.e. the port users can connect to. returned: always type: int sample: 443 protocol: description: Protocol of the service returned: always type: str sample: http destination_port: description: - The port traffic is forwarded to, i.e. the port the targets are listening and accepting connections on. returned: always type: int sample: 80 proxyprotocol: description: - Enable the PROXY protocol. returned: always type: bool sample: false http: description: Configuration for HTTP and HTTPS services returned: always type: complex contains: cookie_name: description: Name of the cookie which will be set when you enable sticky sessions returned: always type: str sample: HCLBSTICKY cookie_lifetime: description: Lifetime of the cookie which will be set when you enable sticky sessions, in seconds returned: always type: int sample: 3600 certificates: description: List of Names or IDs of certificates returned: always type: list elements: str sticky_sessions: description: Enable or disable sticky_sessions returned: always type: bool sample: true redirect_http: description: Redirect Traffic from Port 80 to Port 443, only available if protocol is https returned: always type: bool sample: false health_check: description: Configuration for health checks returned: always type: complex contains: protocol: description: Protocol the health checks will be performed over returned: always type: str sample: http port: description: Port the health check will be performed on returned: always type: int sample: 80 interval: description: Interval of health checks, in seconds returned: always type: int sample: 15 timeout: description: Timeout of health checks, in seconds returned: always type: int sample: 10 retries: description: Number of retries until a target is marked as unhealthy returned: always type: int sample: 3 http: description: Additional Configuration of health checks with protocol http/https returned: always type: complex contains: domain: description: Domain we will set within the HTTP HOST header returned: always type: str sample: example.com path: description: Path we will try to access returned: always type: str sample: / response: description: Response we expect, if response is not within the health check response the target is unhealthy returned: always type: str status_codes: description: List of HTTP status codes we expect to get when we perform the health check. returned: always type: list elements: str sample: ["2??","3??"] tls: description: Verify the TLS certificate, only available if health check protocol is https returned: always type: bool sample: false """ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud try: from hcloud.load_balancers.domain import LoadBalancer, LoadBalancerService, LoadBalancerServiceHttp, \ LoadBalancerHealthCheck, LoadBalancerHealtCheckHttp from hcloud import APIException except ImportError: APIException = None class AnsibleHcloudLoadBalancerService(Hcloud): def __init__(self, module): Hcloud.__init__(self, module, "hcloud_load_balancer_service") self.hcloud_load_balancer = None self.hcloud_load_balancer_service = None def _prepare_result(self): http = None if self.hcloud_load_balancer_service.protocol != "tcp": http = { "cookie_name": to_native(self.hcloud_load_balancer_service.http.cookie_name), "cookie_lifetime": self.hcloud_load_balancer_service.http.cookie_name, "redirect_http": self.hcloud_load_balancer_service.http.redirect_http, "sticky_sessions": self.hcloud_load_balancer_service.http.sticky_sessions, "certificates": [to_native(certificate.name) for certificate in self.hcloud_load_balancer_service.http.certificates], } health_check = { "protocol": to_native(self.hcloud_load_balancer_service.health_check.protocol), "port": self.hcloud_load_balancer_service.health_check.port, "interval": self.hcloud_load_balancer_service.health_check.interval, "timeout": self.hcloud_load_balancer_service.health_check.timeout, "retries": self.hcloud_load_balancer_service.health_check.retries, } if self.hcloud_load_balancer_service.health_check.protocol != "tcp": health_check["http"] = { "domain": to_native(self.hcloud_load_balancer_service.health_check.http.domain), "path": to_native(self.hcloud_load_balancer_service.health_check.http.path), "response": to_native(self.hcloud_load_balancer_service.health_check.http.response), "certificates": [to_native(status_code) for status_code in self.hcloud_load_balancer_service.health_check.http.status_codes], "tls": self.hcloud_load_balancer_service.health_check.http.tls, } return { "load_balancer": to_native(self.hcloud_load_balancer.name), "protocol": to_native(self.hcloud_load_balancer_service.protocol), "listen_port": self.hcloud_load_balancer_service.listen_port, "destination_port": self.hcloud_load_balancer_service.destination_port, "proxyprotocol": self.hcloud_load_balancer_service.proxyprotocol, "http": http, "health_check": health_check, } def _get_load_balancer(self): try: self.hcloud_load_balancer = self.client.load_balancers.get_by_name( self.module.params.get("load_balancer") ) self._get_load_balancer_service() except Exception as e: self.module.fail_json(msg=e.message) def _create_load_balancer_service(self): self.module.fail_on_missing_params( required_params=["protocol"] ) if self.module.params.get("protocol") == "tcp": self.module.fail_on_missing_params( required_params=["destination_port"] ) params = { "protocol": self.module.params.get("protocol"), "listen_port": self.module.params.get("listen_port"), "proxyprotocol": self.module.params.get("proxyprotocol") } if self.module.params.get("destination_port"): params["destination_port"] = self.module.params.get("destination_port") if self.module.params.get("http"): params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) if self.module.params.get("health_check"): params["health_check"] = self.__get_service_health_checks( health_check=self.module.params.get("health_check")) if not self.module.check_mode: try: self.hcloud_load_balancer.add_service(LoadBalancerService(**params)).wait_until_finished( max_retries=1000) except Exception as e: self.module.fail_json(msg=e.message) self._mark_as_changed() self._get_load_balancer() self._get_load_balancer_service() def __get_service_http(self, http_arg): service_http = LoadBalancerServiceHttp(certificates=[]) if http_arg.get("cookie_name") is not None: service_http.cookie_name = http_arg.get("cookie_name") if http_arg.get("cookie_lifetime") is not None: service_http.cookie_lifetime = http_arg.get("cookie_lifetime") if http_arg.get("sticky_sessions") is not None: service_http.sticky_sessions = http_arg.get("sticky_sessions") if http_arg.get("redirect_http") is not None: service_http.redirect_http = http_arg.get("redirect_http") if http_arg.get("certificates") is not None: certificates = http_arg.get("certificates") if certificates is not None: for certificate in certificates: hcloud_cert = None try: try: hcloud_cert = self.client.certificates.get_by_name( certificate ) except Exception: hcloud_cert = self.client.certificates.get_by_id( certificate ) except Exception as e: self.module.fail_json(msg=e.message) service_http.certificates.append(hcloud_cert) return service_http def __get_service_health_checks(self, health_check): service_health_check = LoadBalancerHealthCheck() if health_check.get("protocol") is not None: service_health_check.protocol = health_check.get("protocol") if health_check.get("port") is not None: service_health_check.port = health_check.get("port") if health_check.get("interval") is not None: service_health_check.interval = health_check.get("interval") if health_check.get("timeout") is not None: service_health_check.timeout = health_check.get("timeout") if health_check.get("retries") is not None: service_health_check.retries = health_check.get("retries") if health_check.get("http") is not None: health_check_http = health_check.get("http") service_health_check.http = LoadBalancerHealtCheckHttp() if health_check_http.get("domain") is not None: service_health_check.http.domain = health_check_http.get("domain") if health_check_http.get("path") is not None: service_health_check.http.path = health_check_http.get("path") if health_check_http.get("response") is not None: service_health_check.http.response = health_check_http.get("response") if health_check_http.get("status_codes") is not None: service_health_check.http.status_codes = health_check_http.get("status_codes") if health_check_http.get("tls") is not None: service_health_check.http.tls = health_check_http.get("tls") return service_health_check def _update_load_balancer_service(self): changed = False try: params = { "listen_port": self.module.params.get("listen_port"), } if self.module.params.get("destination_port") is not None: if self.hcloud_load_balancer_service.destination_port != self.module.params.get("destination_port"): params["destination_port"] = self.module.params.get("destination_port") changed = True if self.module.params.get("protocol") is not None: if self.hcloud_load_balancer_service.protocol != self.module.params.get("protocol"): params["protocol"] = self.module.params.get("protocol") changed = True if self.module.params.get("proxyprotocol") is not None: if self.hcloud_load_balancer_service.proxyprotocol != self.module.params.get("proxyprotocol"): params["proxyprotocol"] = self.module.params.get("proxyprotocol") changed = True if self.module.params.get("http") is not None: params["http"] = self.__get_service_http(http_arg=self.module.params.get("http")) changed = True if self.module.params.get("health_check") is not None: params["health_check"] = self.__get_service_health_checks( health_check=self.module.params.get("health_check")) changed = True if not self.module.check_mode: self.hcloud_load_balancer.update_service(LoadBalancerService(**params)).wait_until_finished( max_retries=1000) except Exception as e: self.module.fail_json(msg=e.message) self._get_load_balancer() if changed: self._mark_as_changed() def _get_load_balancer_service(self): for service in self.hcloud_load_balancer.services: if self.module.params.get("listen_port") == service.listen_port: self.hcloud_load_balancer_service = service def present_load_balancer_service(self): self._get_load_balancer() if self.hcloud_load_balancer_service is None: self._create_load_balancer_service() else: self._update_load_balancer_service() def delete_load_balancer_service(self): try: self._get_load_balancer() if self.hcloud_load_balancer_service is not None: if not self.module.check_mode: try: self.hcloud_load_balancer.delete_service(self.hcloud_load_balancer_service).wait_until_finished( max_retries=1000) except Exception as e: self.module.fail_json(msg=e.message) self._mark_as_changed() self.hcloud_load_balancer_service = None except APIException as e: self.module.fail_json(msg=e.message) @staticmethod def define_module(): return AnsibleModule( argument_spec=dict( load_balancer={"type": "str", "required": True}, listen_port={"type": "int", "required": True}, destination_port={"type": "int"}, protocol={ "type": "str", "choices": ["http", "https", "tcp"], }, proxyprotocol={"type": "bool", "default": False}, http={ "type": "dict", "options": dict( cookie_name={ "type": "str" }, cookie_lifetime={ "type": "int" }, sticky_sessions={ "type": "bool", "default": False }, redirect_http={ "type": "bool", "default": False }, certificates={ "type": "list", "elements": "str" }, ) }, health_check={ "type": "dict", "options": dict( protocol={ "type": "str", "choices": ["http", "https", "tcp"], }, port={ "type": "int" }, interval={ "type": "int" }, timeout={ "type": "int" }, retries={ "type": "int" }, http={ "type": "dict", "options": dict( domain={ "type": "str" }, path={ "type": "str" }, response={ "type": "str" }, status_codes={ "type": "list", "elements": "str" }, tls={ "type": "bool", "default": False }, ) } ) }, state={ "choices": ["absent", "present"], "default": "present", }, **Hcloud.base_module_arguments() ), supports_check_mode=True, ) def main(): module = AnsibleHcloudLoadBalancerService.define_module() hcloud = AnsibleHcloudLoadBalancerService(module) state = module.params.get("state") if state == "absent": hcloud.delete_load_balancer_service() elif state == "present": hcloud.present_load_balancer_service() module.exit_json(**hcloud.get_result()) if __name__ == "__main__": main()