check_expire/check_expire
Faustin Lammler 342f89844b
Add unixtime option
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.
2021-07-07 10:13:50 +02:00

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")