A Nagios (and compatible) plugin to check the impending expiration of a domain name. It relies on RDAP exclusively (no whois) and works with every top-level domain with RDAP (which includes all the ICANN ones).
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
5.7 KiB

1 year ago
#!/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
timeout = 20 # Seconds
1 year ago
def usage(msg=None):
print("Usage: %s -H domain-name [-c critical -w warning -u -t timeout]" % sys.argv[0], end="")
1 year ago
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, retrieved on %s, RDAP server is %s" % \
(database.description, database.version, database.publication, database.retrieved, server))
1 year ago
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 unknown(msg=None):
if msg is None:
msg = "Unknown"
print("%s UNKNOWN: %s" % (domain, msg), end="")
details()
sys.exit(STATE_UNKNOWN)
1 year ago
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:t:uvVw:",
["critical=", "expiration", "help",
"timeout=", "unixtime", "verbose",
"version", "warning="])
1 year ago
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 == "--timeout" or option == "-t":
timeout = int(value)
elif option == "--unixtime" or option == "-u":
unixtime = True
1 year ago
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))
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)
try:
database = ianardap.IanaRDAPDatabase()
except Exception as e:
unknown("Exception when retrieving the IANA database: \"%s\"" % e)
1 year ago
server = database.find(domain)
if server is None:
unknown("No RDAP server found for %s" % domain)
1 year ago
if server.endswith("/"):
server = server[:-1] # Donuts RDAP server balks when there are two slashes and reply 404
try:
response = requests.get("%s/domain/%s" % (server, domain), timeout=timeout)
except requests.exceptions.Timeout:
unknown("Timeout when trying to reach %s" % server)
1 year ago
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:
unknown("No recognized format for datetime \"%s\"" % event["eventDate"])
1 year ago
now = datetime.datetime.now(tz=UTCINFO)
if unixtime == True:
print(int(expiration.strftime("%s")))
sys.exit(STATE_OK)
1 year ago
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")