Last active
October 17, 2024 17:20
-
-
Save rikonor/acecb5f94fedb31f2f683a8400496c90 to your computer and use it in GitHub Desktop.
Ansible health-check module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
from __future__ import absolute_import, division, print_function | |
import http.client | |
import socket | |
import ssl | |
import time | |
from ansible.module_utils.basic import AnsibleModule | |
__metaclass__ = type | |
DOCUMENTATION = r""" | |
--- | |
module: health_check | |
short_description: Perform a health check on a web service with retry logic | |
version_added: "1.0.0" | |
description: This module performs a health check on a web service by attempting to connect to a specified address and port, retrying for the specified timeout duration. | |
options: | |
addr: | |
description: The IP address or hostname to connect to. | |
required: true | |
type: str | |
port: | |
description: The port number to connect to. | |
required: true | |
type: int | |
timeout: | |
description: The total timeout for the health check in seconds. | |
required: false | |
type: int | |
default: 30 | |
proto: | |
description: The protocol of the request (e.g., "https"). | |
required: false | |
type: str | |
default: "http" | |
path: | |
description: The path to request (e.g., "/health"). | |
required: false | |
type: str | |
default: "/" | |
hostname: | |
description: TLS hostname for SNI, required if `proto` is "https" | |
required: false | |
type: str | |
default: None | |
ca_data: | |
description: Root CA certificate, required if `proto` is "https" | |
required: false | |
type: str | |
default: None | |
expected_status: | |
description: The expected HTTP status code for a successful health check. | |
required: false | |
type: int | |
default: 200 | |
retry_interval: | |
description: The interval between retry attempts in seconds. | |
required: false | |
type: float | |
default: 1.0 | |
""" | |
EXAMPLES = r""" | |
# Perform a health check on localhost | |
- name: Health check (IPv4) | |
health_check: | |
addr: 127.0.0.1 | |
port: 80 | |
timeout: 30 | |
# Perform a health check on IPv6 localhost with custom settings | |
- name: Health check (IPv6) | |
health_check: | |
addr: "::1" | |
port: 80 | |
timeout: 60 | |
proto: "http" | |
path: "/healthz" | |
expected_status: 204 | |
retry_interval: 2.0 | |
""" | |
RETURN = r""" | |
healthy: | |
description: Indicates whether the health check was successful. | |
type: bool | |
returned: always | |
attempts: | |
description: The number of connection attempts made before succeeding or timing out. | |
type: int | |
returned: always | |
duration: | |
description: The time taken for the successful health check in seconds. | |
type: float | |
returned: always | |
status_code: | |
description: The HTTP status code received (if applicable). | |
type: int | |
returned: when an HTTP request was made | |
""" | |
def mk_result(healthy, changed=False, attempts=-1, duration=-1, status_code=-1, error=None): | |
return dict( | |
healthy=healthy, | |
changed=changed, | |
attempts=attempts, | |
duration=duration, | |
status_code=status_code, | |
error=error | |
) | |
def run_module(request_fn): | |
module_args = dict( | |
addr=dict(type="str", required=True), | |
port=dict(type="int", required=True), | |
timeout=dict(type="int", default=30), | |
proto=dict(type="str", default="http"), | |
path=dict(type="str", default="/"), | |
hostname=dict(type="str", default=None), | |
ca_data=dict(type="str", default=None), | |
expected_status=dict(type="int", default=200), | |
retry_interval=dict(type="float", default=1.0), | |
) | |
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) | |
if module.check_mode: | |
module.exit_json(changed=False) | |
params = module.params | |
start_time = time.time() | |
end_time = start_time + params['timeout'] | |
attempts = 0 | |
status_code = -1 | |
error = None | |
while time.time() < end_time: | |
attempts += 1 | |
try: | |
response = request_fn( | |
addr=params['addr'], | |
port=params['port'], | |
proto=params['proto'], | |
hostname=params['hostname'], | |
ca_data=params['ca_data'] | |
) | |
if response.status != params['expected_status']: | |
raise Exception( | |
f"unexpected status code: {status_code}, expected {params['expected_status']}") | |
module.exit_json(**mk_result( | |
healthy=True, | |
status_code=response.status, | |
attempts=attempts, | |
duration=(time.time() - start_time), | |
)) | |
except Exception as e: | |
time.sleep(params['retry_interval']) | |
error = f"{e}" | |
module.fail_json(msg="Failed", **mk_result( | |
healthy=False, | |
status_code=status_code, | |
attempts=attempts, | |
duration=(time.time() - start_time), | |
error=error | |
)) | |
def request( | |
addr, | |
port=80, | |
proto='http', | |
method='GET', | |
path='/', | |
hostname=None, | |
timeout=10, | |
ca_data=None | |
): | |
if proto not in ['http', 'https']: | |
raise ValueError("Protocol must be 'http' or 'https'") | |
# Open connect to addr:port | |
sock = socket.create_connection((addr, port), timeout=timeout) | |
if proto == 'http': | |
# If `http` - continue with plain connection | |
conn = http.client.HTTPConnection(addr) | |
if proto == 'https': | |
# If 'https' - verify the certificate | |
if hostname is None: | |
raise ValueError("Hostname must be provided for 'https'") | |
if ca_data is None: | |
raise ValueError("CA data must be provided for 'https'") | |
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | |
context.load_verify_locations(cadata=ca_data) | |
conn = http.client.HTTPSConnection(hostname, context=context) | |
conn.sock = context.wrap_socket(sock, server_hostname=hostname) | |
conn.timeout = timeout | |
conn.request(method, path) | |
return conn.getresponse() | |
def main(): | |
run_module(request_fn=request) | |
if __name__ == "__main__": | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
- name: Test | |
hosts: test_hosts | |
vars: | |
ca_data: | | |
# Issuer: CN=ISRG Root X1 O=Internet Security Research Group | |
# Subject: CN=ISRG Root X1 O=Internet Security Research Group | |
# Label: "ISRG Root X1" | |
# Serial: 172886928669790476064670243504169061120 | |
# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e | |
# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 | |
# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 | |
-----BEGIN CERTIFICATE----- | |
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw | |
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh | |
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 | |
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu | |
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY | |
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc | |
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ | |
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U | |
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW | |
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH | |
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC | |
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv | |
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn | |
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn | |
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw | |
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI | |
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV | |
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq | |
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL | |
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ | |
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK | |
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 | |
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur | |
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC | |
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc | |
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq | |
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA | |
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d | |
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= | |
-----END CERTIFICATE----- | |
tasks: | |
- name: Print message | |
ansible.builtin.debug: | |
msg: "Done" | |
- name: Perform health-check on http-gateway (IPv4 / https) | |
health_check: | |
proto: https | |
addr: 127.0.0.1 | |
port: 443 | |
path: / | |
hostname: example.com | |
ca_data: | | |
{{ ca_data }} | |
expected_status: 307 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ANSIBLE_LIBRARY=$PWD/library \ | |
ansible \ | |
-m health_check -a 'addr=127.0.0.1 port=8000 path=/index.html expected_status=404 timeout=3' \ | |
-i inventory.yaml \ | |
test_hosts |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment