#!/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 . """ 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 def usage(msg=None): print("Usage: %s -H domain-name [-c critical -w warning -u -t timeout]" % 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, retrieved on %s, RDAP server is %s" % \ (database.description, database.version, database.publication, database.retrieved, 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 unknown(msg=None): if msg is None: msg = "Unknown" print("%s UNKNOWN: %s" % (domain, msg), end="") details() sys.exit(STATE_UNKNOWN) 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="]) 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 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) server = database.find(domain) if server is None: unknown("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 try: response = requests.get("%s/domain/%s" % (server, domain), timeout=timeout) except requests.exceptions.Timeout: unknown("Timeout when trying to reach %s" % server) 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"]) 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")