Faustin Lammler
342f89844b
Add an option to return the expiration date in unixtime (time since epoch). ./check_expire -H domain -u It's particularly useful for alerting or monitoring systems that do their own calculation based on the metric they receive (and not based on exit codes). For instance you can use the following in zabbix 5.4: - item: check_expire["-H","{HOST.CONN}","-u"] - trigger: last(/check_expire["-H","{HOST.CONN}","-u"])-now()<2592000 Where 2592000 is 30 days in seconds.
155 lines
5.2 KiB
Python
Executable File
155 lines
5.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Monitoring plugin (Nagios-compatible) to check the impeding
|
|
expiration of a domain name, through RDAP.
|
|
|
|
The monitoring plugin API is documented at
|
|
<https://www.monitoring-plugins.org/doc/guidelines.html>.
|
|
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import datetime
|
|
import getopt
|
|
import re
|
|
|
|
# http://python-requests.org/ for easier HTTPS retrieval
|
|
import requests
|
|
|
|
# Local module
|
|
import ianardap
|
|
|
|
VERSION = "0.0"
|
|
|
|
# Funny diversity in RDAP servers' output...
|
|
RFC3339 = "%Y-%m-%dT%H:%M:%SZ%z"
|
|
RFC3339TZ = "%Y-%m-%dT%H:%M:%S%z" # We cannot use it directly, Python uses HHMM as offset, not HH:MM as RFC 3339 does :-(
|
|
RFC3339LONG = "%Y-%m-%dT%H:%M:%S.%fZ%z" # Microseconds support (%f) does not seem documented
|
|
|
|
UTCINFO = datetime.timezone(offset=datetime.timedelta(seconds=0))
|
|
|
|
# Do not touch
|
|
# https://www.monitoring-plugins.org/doc/guidelines.html#AEN78
|
|
STATE_OK = 0
|
|
STATE_WARNING = 1
|
|
STATE_CRITICAL = 2
|
|
STATE_UNKNOWN = 3
|
|
STATE_DEPENDENT = 4
|
|
|
|
# Can be changed on the command-line
|
|
verbose = False
|
|
domain = None
|
|
critical_t = datetime.timedelta(days=2)
|
|
warning_t = datetime.timedelta(days=7)
|
|
unixtime = False
|
|
# TODO implement timeout for HTTPS requests. -t option. Does Requests allow it?
|
|
|
|
def usage(msg=None):
|
|
print("Usage: %s -H domain-name [-c critical -w warning -u unixtime]" % sys.argv[0], end="")
|
|
if msg is not None and msg != "":
|
|
print(" (%s)" % msg)
|
|
else:
|
|
print("")
|
|
|
|
def details():
|
|
if verbose:
|
|
print(" RDAP database \"%s\", version %s published on %s, RDAP server is %s" % \
|
|
(database.description, database.version, database.publication, server))
|
|
else:
|
|
print("")
|
|
|
|
def error(msg=None):
|
|
if msg is None:
|
|
msg = "Unknown error"
|
|
print("%s CRITICAL: %s" % (domain, msg), end="")
|
|
details()
|
|
sys.exit(STATE_CRITICAL)
|
|
|
|
def warning(msg=None):
|
|
if msg is None:
|
|
msg = "Unknown warning"
|
|
print("%s WARNING: %s" % (domain, msg), end="")
|
|
details()
|
|
sys.exit(STATE_WARNING)
|
|
|
|
def ok(msg=None):
|
|
if msg is None:
|
|
msg = "Unknown message but everything is OK"
|
|
print("%s OK: %s" % (domain, msg), end="")
|
|
details()
|
|
sys.exit(STATE_OK)
|
|
|
|
try:
|
|
optlist, args = getopt.getopt (sys.argv[1:], "c:hH:vVw:u",
|
|
["critical=", "expiration", "help", "verbose", "version", "warning=", "unixtime"])
|
|
for option, value in optlist:
|
|
if option == "--critical" or option == "-c":
|
|
critical_t = datetime.timedelta(days=int(value))
|
|
elif option == "--help" or option == "-h":
|
|
usage()
|
|
sys.exit(STATE_OK)
|
|
elif option == "--hostname" or option == "-H":
|
|
domain = value
|
|
elif option == "--verbose" or option == "-v":
|
|
verbose = True
|
|
elif option == "--version" or option == "-V":
|
|
print("%s version %s" % (sys.argv[0], VERSION))
|
|
sys.exit(STATE_OK)
|
|
elif option == "--warning" or option == "-w":
|
|
warning_t = datetime.timedelta(days=int(value))
|
|
elif option == "--unixtime" or option == "-u":
|
|
unixtime = True
|
|
else:
|
|
# Should never occur, it is trapped by getopt
|
|
print("Unknown option %s" % option)
|
|
sys.exit(STATE_UNKNOWN)
|
|
except getopt.error as reason:
|
|
usage(reason)
|
|
sys.exit(STATE_UNKNOWN)
|
|
if len(args) != 0:
|
|
usage("Extraneous arguments")
|
|
sys.exit(STATE_UNKNOWN)
|
|
if domain is None:
|
|
usage("-H is mandatory")
|
|
sys.exit(STATE_UNKNOWN)
|
|
database = ianardap.IanaRDAPDatabase()
|
|
server = database.find(domain)
|
|
if server is None:
|
|
error("No RDAP server found for %s" % domain)
|
|
if server.endswith("/"):
|
|
server = server[:-1] # Donuts RDAP server balks when there are two slashes and reply 404
|
|
response = requests.get("%s/domain/%s" % (server, domain))
|
|
if response.status_code != 200:
|
|
error("Invalid RDAP return code: %s" % response.status_code)
|
|
rdap = json.loads(response.content)
|
|
for event in rdap["events"]:
|
|
if event["eventAction"] == "expiration":
|
|
try:
|
|
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339LONG)
|
|
except ValueError:
|
|
try:
|
|
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339)
|
|
except ValueError:
|
|
# Python standard library offers no way to parse all RFC3339-compliant datetimes, so we have to hack.
|
|
if re.search(r"\+[0-9]{2}:[0-9]{2}$", event["eventDate"]):
|
|
dt = re.sub(r"\+([0-9]{2}):([0-9]{2})$", r"+\1\2", event["eventDate"])
|
|
expiration = datetime.datetime.strptime(dt, RFC3339TZ)
|
|
else:
|
|
print("No recognized format for datetime \"%s\"" % event["eventDate"])
|
|
sys.exit(STATE_UNKNOWN)
|
|
now = datetime.datetime.now(tz=UTCINFO)
|
|
if unixtime == True:
|
|
print(int(expiration.strftime("%s")))
|
|
sys.exit(STATE_OK)
|
|
if expiration < now:
|
|
error("domain %s is already expired (%s ago)" % (domain, (now-expiration)))
|
|
rest = expiration-now
|
|
if rest < critical_t:
|
|
error("expires in %s, HURRY UP!!!" % (rest))
|
|
elif rest < warning_t:
|
|
warning("expires in %s, renew now" % (rest))
|
|
else:
|
|
ok("expires in %s." % (rest))
|
|
error("No expiration found")
|