Add collectd support

This commit is contained in:
sqozz 2019-10-15 07:59:23 +02:00
parent 8f5ad667aa
commit 0366c1c5e4

View file

@ -8,9 +8,7 @@ from pytz import utc as UTC
from datetime import datetime from datetime import datetime
#collectd stuff #collectd stuff
#import collectd import collectd
import ipaddress
import pdb import pdb
@ -18,39 +16,125 @@ DOMAIN = "geekify.de"
address_family = "ipv4" address_family = "ipv4"
TIMEOUT_SECONDS = 1 TIMEOUT_SECONDS = 1
plugin_config = {}
def configure(config): def configure(config):
for c in config.children: machines = {}
print(c) for config_child in config.children:
key = config_child.key.lower()
values = config_child.values
# currently only "host" entries are supported
if config_child.key.lower() == "host":
# host can contain several hosts at once, even the ones containing additional config
for machine_name in values:
machine_details = {}
# host entry contains advanced/additional config options
for detail in config_child.children:
key_name = detail.key.lower()
machine_detail_values = []
# TODO: all the difference here is casting value into an int or not. This can maybe be improved or reduced somehow?
if key_name in ("addressfamily",):
for value in detail.values:
machine_detail_values.append(int(value))
elif key_name in ("sninames",):
machine_detail_values = []
for value in detail.values:
machine_detail_values.append(value)
else:
# additional keys are just ignored
pass
machine_details.update({key_name: machine_detail_values})
machines.update({machine_name: machine_details})
plugin_config.update({"machines": machines})
today = datetime.now(UTC) def check_hosts():
port = 443 for host, host_config in plugin_config.get("machines", {}).items():
if address_family == "ipv4": for address_family in host_config.get("addressfamily", [4, 6]):
af = socket.AF_INET for sni_name in host_config.get("sninames", [host]):
elif address_family == "ipv6": try:
af = socket.AF_INET6 cert_details = get_tls_metrics(host, sni_name, address_family)
else: for key in cert_details.keys():
proto = None val = collectd.Values(host="", plugin="pyopenssl", plugin_instance="host_{}-sni_{}".format(host, sni_name), type_instance="ipv{}-{}".format(address_family, key))
cert_detail = cert_details.get(key)
if key in ("valid", "expired"):
val.type = "gauge"
val.dispatch(values=[1 if cert_detail else 0])
elif key in ("notAfter", "notBefore"):
val.type = "gauge"
val.dispatch(values=[cert_detail.timestamp()])
elif key in ("remaining", "active_since"):
val.type = "gauge"
val.dispatch(values=[int(cert_detail.total_seconds())])
elif key in ("issuer", "digests"):
# currently there is no way to store static strings with collectd
pass
except Exception as e:
print("Failed to check host {} with SNI {} over IPv{}: ".format(host, sni_name, address_family), end="")
print(e)
pass
try: def get_tls_metrics(host, sni_name, address_family):
addrinfo = socket.getaddrinfo(DOMAIN, None, af, proto=socket.IPPROTO_TCP) cert_details = {"valid": True}
today = datetime.now(UTC)
port = 443
if address_family == 4:
af = socket.AF_INET
elif address_family == 6:
af = socket.AF_INET6
else:
proto = None
addrinfo = socket.getaddrinfo(host, None, af, proto=socket.IPPROTO_TCP)
# addrinfo first contains an array of connections made, we want the first ([0]) and only one we made # addrinfo first contains an array of connections made, we want the first ([0]) and only one we made
# this tuple then contains (family, type, proto, canonname, sockaddr) - we want sockaddr == [4] # this tuple then contains (family, type, proto, canonname, sockaddr) - we want sockaddr == [4]
# this then can contain either (host, port) or (host, port, flowinfo, scopeid) depending on set address family. Since host is anyways at [0] we take this as final ip address # this then can contain either (host, port) or (host, port, flowinfo, scopeid) depending on set address family. Since host is anyways at [0] we take this as final ip address
host = addrinfo[0][4][0] host = addrinfo[0][4][0]
except socket.gaierror:
host = DOMAIN
context = ssl.create_default_context() context = ssl.create_default_context()
with socket.create_connection((host, port), TIMEOUT_SECONDS) as sock: while True:
with context.wrap_socket(sock, server_hostname=DOMAIN) as sslsock: try:
der_cert = sslsock.getpeercert(True) with socket.create_connection((host, port), TIMEOUT_SECONDS) as sock:
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert) with context.wrap_socket(sock, server_hostname=sni_name) as sslsock:
der_cert = sslsock.getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
break
except ssl.CertificateError as error:
if context.verify_mode == ssl.CERT_REQUIRED and context.check_hostname:
cert_details["valid"] = False
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
else:
raise error
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, der_cert) x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, der_cert)
exp = x509.get_notAfter().decode("utf-8") notAfter = x509.get_notAfter().decode("utf-8")
expirary_date = date_parser.parse(exp) notAfter = date_parser.parse(notAfter)
delta = expirary_date - today notBefore = x509.get_notBefore().decode("utf-8")
print(delta.total_seconds()) notBefore = date_parser.parse(notBefore)
remaining = notAfter - today
active_since = today - notBefore
pdb.set_trace() issuer_components = x509.get_issuer().get_components()
issuer_components_decoded = list(map(lambda bin_component: list(map(lambda keyvalue: keyvalue.decode("utf-8"), bin_component)), issuer_components))
joined_components = list(map(lambda x: "=".join(x), issuer_components_decoded))
issuer = ",".join(joined_components)
digests = {}
for digest_type in ("md5", "sha1", "sha256"):
digests.update({digest_type: x509.digest(digest_type).decode("utf-8")})
cert_details.update({
"expired": x509.has_expired(),
"notAfter": notAfter,
"notBefore": notBefore,
"remaining": remaining,
"active_since": active_since,
"issuer": issuer,
"digests": digests,
})
return cert_details
collectd.register_config(configure)
collectd.register_read(check_hosts)