#!/usr/bin/env python3 import os import shutil import tempfile import pwd import logging import logging.handlers import MySQLdb import configparser from datetime import datetime, timezone import grp CONFIG_FILE = "/etc/ngcp-reminder/reminder.conf" LOG_FILE = "/var/log/ngcp/ngcp-reminder.log" OWNERLOGFILE = "root" OWNERTMPFILE = "asterisk" # --- Logging setup ----------------------------------------------------------- SYSLOG_ADDRESS = '/dev/log' # Standard Unix socket for local syslog # Ensure proper permissions and ownership for the log file if not os.path.exists(LOG_FILE): open(LOG_FILE, "w").close() uid = pwd.getpwnam(OWNERLOGFILE).pw_uid gid = grp.getgrnam('adm').gr_gid os.chown(LOG_FILE, uid, gid) os.chmod(LOG_FILE, 0o640) # rw-r----- logger = logging.getLogger("ngcp-reminder") logger.setLevel(logging.INFO) # Syslog handler (facility local0) syslog_handler = logging.handlers.SysLogHandler( address=SYSLOG_ADDRESS, facility=logging.handlers.SysLogHandler.LOG_LOCAL0 ) syslog_formatter = logging.Formatter( 'ngcp-reminder: [%(levelname)s] %(message)s') syslog_handler.setFormatter(syslog_formatter) logger.addHandler(syslog_handler) # File handler (optional fallback) file_handler = logging.FileHandler(LOG_FILE) file_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) # --- Configuration reader ---------------------------------------------------- def read_config(path): """Read key=value configuration file without section headers.""" parser = configparser.ConfigParser() with open(path) as f: data = f.read() parser.read_string("[DEFAULT]\n" + data) return parser["DEFAULT"] # --- Database connection ----------------------------------------------------- def connect_db(cfg): """Establish a connection to the MySQL database.""" try: db = MySQLdb.connect( host=cfg.get("dbhost", "localhost"), user=cfg.get("dbuser"), passwd=cfg.get("dbpassword"), db=cfg.get("database"), ) return db except Exception as e: logger.error(f"Database connection failed: {e}") raise # --- Call file creation ------------------------------------------------------ def create_call_file( cfg, username, domain, lang, recur, reminder_id, weekdays ): """Create an Asterisk .call file based on the reminder configuration.""" tmpdir = cfg.get("tmpdir", "/tmp") spool = cfg.get("spool", "/var/spool/asterisk/outgoing") # Normalize PJSIP peer name peer = cfg.get("sip_peer", "sip_proxy_ep") if not peer.upper().startswith(("SIP/", "PJSIP/")): peer = f"PJSIP/{peer}" tmp_prefix = f"{username}__AT__{domain}." fd, tmp_filename = tempfile.mkstemp( prefix=tmp_prefix, dir=tmpdir, text=True ) os.close(fd) try: # Write call file content with open(tmp_filename, "w") as f: f.write(f"Channel: {peer}/sip:{username}@{domain}\n") f.write(f"MaxRetries: {cfg.get('retries', '2')}\n") f.write(f"RetryTime: {cfg.get('retry_time', '60')}\n") f.write(f"WaitTime: {cfg.get('wait_time', '30')}\n") f.write("Extension: s\n") f.write(f"Context: {cfg.get('context', 'reminder')}\n") f.write("Priority: 1\n") f.write(f"Setvar: LANG={lang or 'en'}\n") # Set file ownership and permissions uid = pwd.getpwnam(OWNERTMPFILE).pw_uid gid = pwd.getpwnam(OWNERTMPFILE).pw_gid os.chown(tmp_filename, uid, gid) os.chmod(tmp_filename, 0o600) # Read back for logging with open(tmp_filename, "r") as f: content = f.read() final_path = os.path.join(spool, os.path.basename(tmp_filename)) shutil.move(tmp_filename, final_path) logger.info( f"Created call file: {final_path}\n" "--- File content ---\n" f"{content}\n" "--------------------" ) except Exception as e: logger.error(f"Error creating call file for {username}@{domain}: {e}") if os.path.exists(tmp_filename): os.remove(tmp_filename) # --- Main logic -------------------------------------------------------------- def main(): """Main execution logic for the NGCP reminder system.""" cfg = read_config(CONFIG_FILE) weekdays = [ w.strip() for w in cfg.get("weekdays", "2,3,4,5,6,7").split(",") ] db = connect_db(cfg) cur = db.cursor(MySQLdb.cursors.DictCursor) sql = """ SELECT s.username, d.domain, r.recur, r.id, vup.value as lang FROM provisioning.voip_reminder r LEFT JOIN provisioning.voip_subscribers s ON r.subscriber_id = s.id LEFT JOIN provisioning.voip_domains d ON s.domain_id = d.id LEFT JOIN billing.v_subscriber_timezone tz ON tz.uuid = s.uuid LEFT JOIN provisioning.voip_usr_preferences vup ON r.subscriber_id=vup.subscriber_id LEFT JOIN provisioning.voip_preferences vp ON vp.id=vup.attribute_id WHERE r.active = 1 AND vp.attribute = "language" AND r.time = time_format(CONVERT_TZ(now(), "localtime", tz.name), '%H:%i:00') UNION SELECT s.username, d.domain, r.recur, r.id, vdp.value as lang FROM provisioning.voip_reminder r LEFT JOIN provisioning.voip_subscribers s ON r.subscriber_id = s.id LEFT JOIN provisioning.voip_domains d ON s.domain_id = d.id LEFT JOIN billing.v_subscriber_timezone tz ON tz.uuid = s.uuid LEFT JOIN provisioning.voip_dom_preferences vdp ON vdp.domain_id=d.id LEFT JOIN provisioning.voip_preferences vp ON vp.id=vdp.attribute_id AND r.time = time_format(CONVERT_TZ(now(), "localtime", tz.name), '%H:%i:00') WHERE r.active = 1 AND vp.attribute = "language" LIMIT 100; """ cur.execute(sql) rows = cur.fetchall() for ref in rows: username = ref["username"] domain = ref["domain"] recur = ref["recur"] lang = ref.get("lang", "en") reminder_id = ref["id"] # Weekday check for recurring reminders if recur == "weekdays": wday = datetime.now(timezone.utc).isoweekday() if str(wday) not in weekdays: continue create_call_file( cfg, username, domain, lang, recur, reminder_id, weekdays ) # Disable one-time reminders if recur == "never": cur2 = db.cursor() cur2.execute( "UPDATE voip_reminder SET active=0 WHERE id=%s", (reminder_id,) ) cur2.close() db.commit() cur.close() db.close() logger.info("Reminder cycle completed successfully.") # --- Entry point ------------------------------------------------------------- if __name__ == "__main__": try: main() except Exception as e: logger.exception(f"Unhandled exception: {e}") raise