fcfb91470c294d3b991dbd0d24cba67de9ed65cb hiram Tue Apr 21 14:20:57 2026 -0700 create ottoRequest cron watch script refs #31811 diff --git src/hg/utils/otto/userRequests/ottoRequest.py src/hg/utils/otto/userRequests/ottoRequest.py new file mode 100755 index 00000000000..51d30188970 --- /dev/null +++ src/hg/utils/otto/userRequests/ottoRequest.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""ottoRequest.py - check ottoRequest table for pending requests +and send email notification for each one found. + +Intended to run from cron. Reads the notification email address and +table name from an hg.conf file. Uses hgsql for database access. + +Usage: + ottoRequest.py [-c /path/to/hg.conf] + +Options: + -c, --conf Path to hg.conf [default: /usr/local/apache/cgi-bin/hg.conf] +""" + +import argparse +import os +import subprocess +import sys + + +def parseHgConf(path): + """Return a dict of key=value pairs from an hg.conf file. + Handles 'include' directives with paths relative to the + directory of the file containing the include.""" + conf = {} + confDir = os.path.dirname(os.path.abspath(path)) + try: + with open(path) as fh: + for line in fh: + line = line.strip() + if not line or line.startswith('#'): + continue + if line.startswith('include '): + inclPath = line.split(None, 1)[1] + if not os.path.isabs(inclPath): + inclPath = os.path.join(confDir, inclPath) + inclConf = parseHgConf(inclPath) + conf.update(inclConf) + continue + if '=' in line: + key, value = line.split('=', 1) + conf[key.strip()] = value.strip() + except FileNotFoundError: + sys.exit(f"Error: config file not found: {path}") + except PermissionError: + sys.exit(f"Error: cannot read config file: {path}") + return conf + + +def hgsqlQuery(db, sql): + """Run a SQL query via hgsql and return rows as list of dicts.""" + cmd = ['hgsql', db, '-N', '-B', '-e', sql] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + sys.exit(f"hgsql error: {result.stderr.strip()}") + rows = [] + if not result.stdout.strip(): + return rows + for line in result.stdout.strip().split('\n'): + fields = line.split('\t') + rows.append({ + 'id': fields[0], + 'requestType': fields[1], + 'fromDb': fields[2], + 'toDb': fields[3], + 'email': fields[4], + 'comment': fields[5], + 'requestTime': fields[6], + }) + return rows + + +def hgsqlUpdate(db, sql): + """Run a SQL update/insert statement via hgsql.""" + cmd = ['hgsql', db, '-N', '-B', '-e', sql] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"hgsql update error: {result.stderr.strip()}", file=sys.stderr) + return False + return True + + +def sendMail(toAddr, subject, body): + """Send email via /usr/sbin/sendmail.""" + message = f"To: {toAddr}\nSubject: {subject}\n\n{body}\n" + result = subprocess.run(['/usr/sbin/sendmail', '-t'], + input=message, capture_output=True, text=True) + if result.returncode != 0: + print(f"Warning: sendmail failed: {result.stderr.strip()}", + file=sys.stderr) + return False + return True + + +def main(): + parser = argparse.ArgumentParser( + description='Process pending ottoRequest entries.') + parser.add_argument('-c', '--conf', + default='/usr/local/apache/cgi-bin/hg.conf', + help='Path to hg.conf [default: %(default)s]') + args = parser.parse_args() + + conf = parseHgConf(args.conf) + + dbName = conf.get('central.db') + if not dbName: + sys.exit("Error: central.db not defined in config") + + table = conf.get('ottoTable', 'ottoRequest') + + notifyEmail = conf.get('chainFileRequestEmail') + if not notifyEmail: + sys.exit("Error: chainFileRequestEmail not defined in config") + + # find pending requests + sql = (f"SELECT id, requestType, fromDb, toDb, email, comment, " + f"requestTime FROM {table} WHERE doneStatus = 0") + pending = hgsqlQuery(dbName, sql) + + if not pending: + return # nothing to do -- silent for cron + + for req in pending: + subject = f"ottoRequest #{req['id']}: {req['requestType']} pending" + body = ( + f"##################################################\n" + f"Pending {req['requestType']} request #{req['id']}\n" + f"\n" + f" From: {req['fromDb']}\n" + f" To: {req['toDb']}\n" + f" Email: {req['email']}\n" + f" Comment: {req['comment']}\n" + f" Time: {req['requestTime']}\n" + f"##################################################\n" + f"testing ottoRequest watch cron job\n" + f"##################################################\n" + ) + if sendMail(notifyEmail, subject, body): + print(f"Notified {notifyEmail} about request #{req['id']}") + hgsqlUpdate(dbName, + f"UPDATE {table} SET doneStatus = 1" + f" WHERE id = {req['id']}") + else: + print(f"Failed to notify about request #{req['id']}", + file=sys.stderr) + + +if __name__ == '__main__': + main()