Skip to content

Instantly share code, notes, and snippets.

@rikonor
Last active October 17, 2024 17:20
Show Gist options
  • Save rikonor/acecb5f94fedb31f2f683a8400496c90 to your computer and use it in GitHub Desktop.
Save rikonor/acecb5f94fedb31f2f683a8400496c90 to your computer and use it in GitHub Desktop.
Ansible health-check module
#!/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()
---
- 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
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