From ad3d3a151fcbc327af443abfd3e3e39f998ba2e7 Mon Sep 17 00:00:00 2001 From: Alessio Garzi Date: Wed, 29 Oct 2025 14:58:17 +0100 Subject: [PATCH] MT#63353 Rewrite ngcp-remainder in python3 Change-Id: I126be826f12780fc0c89e155fd19a82ac22971d5 (cherry picked from commit ad68788254aa1f96d13d45d6a78dc641fe7e9710) --- debian/control | 6 +- ngcp-reminder | 317 +++++++++++++++++++++++++++++++++---------------- reminder.conf | 2 +- 3 files changed, 219 insertions(+), 106 deletions(-) mode change 100755 => 100644 ngcp-reminder diff --git a/debian/control b/debian/control index d91b074..21fddbf 100644 --- a/debian/control +++ b/debian/control @@ -12,10 +12,8 @@ Package: ngcp-reminder Architecture: all Depends: asterisk, - libconfig-tiny-perl, - libdbd-mysql-perl, - libdbi-perl, - libreadonly-perl, + python3, + python3-mysqldb, ${misc:Depends}, ${perl:Depends}, Description: Reminder calls for NGCP platform diff --git a/ngcp-reminder b/ngcp-reminder old mode 100755 new mode 100644 index 776817f..9435dff --- a/ngcp-reminder +++ b/ngcp-reminder @@ -1,101 +1,216 @@ -#!/usr/bin/perl -w -use strict; -use warnings; - -use Config::Tiny; -use File::Temp qw(tempfile); -use File::Basename; -use File::Copy; -use DBI; -use Readonly; - -Readonly my $config_file => "/etc/ngcp-reminder/reminder.conf"; -Readonly my $owner => 'asterisk'; - -my $config = Config::Tiny->read($config_file) - or die "Program stopping, couldn't open the configuration file '$config_file'.\n"; -my %cfg = ( %{$config->{_}} ); - -if (!defined $cfg{weekdays} || $cfg{weekdays} =~ /^\s*$/) { - $cfg{weekdays} = '2,3,4,5,6,7'; -} -my @wdays = split /\s*,\s*/, $cfg{weekdays}; - -my $dsn = "DBI:mysql:database=$cfg{database};host=$cfg{dbhost};port=0"; - -my $dbh = DBI->connect($dsn, @{cfg}{qw(dbuser dbpassword)}) - or die "Cannot connect to db: ".$DBI::errstr; -my $sth = $dbh->prepare(<prepare("UPDATE voip_reminder SET active=0 WHERE id=?"); - -$sth->execute() or die "Cannot execute: ".$DBI::errstr; - -while (my $ref = $sth->fetchrow_hashref()) { - print "$ref->{'username'}\@$ref->{'domain'}, recur=$ref->{'recur'}, language=$ref->{'lang'}\n"; - - if ($ref->{'recur'} eq "weekdays") { - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time); - $wday++; # make sun=1, sat=7 - next unless grep { /^$wday$/ } @wdays; - } - - my ($tmp, $tmp_filename) = tempfile("$ref->{'username'}.XXXXXX", DIR => $cfg{tmpdir}, UNLINK => 0); - unless (defined $tmp) { - die "Failed to create temporary call file: $!\n"; - } - - print "Using tmpfile '$tmp_filename'\n"; - - print $tmp "Channel: SIP/$cfg{sip_peer}/$ref->{'username'}__AT__$ref->{'domain'}\n"; - print $tmp "MaxRetries: $cfg{retries}\n"; - print $tmp "RetryTime: $cfg{retry_time}\n"; - print $tmp "WaitTime: $cfg{wait_time}\n"; - print $tmp "Extension: s\n"; - print $tmp "Context: $cfg{context}\n"; - print $tmp "Priority: 1\n"; - print $tmp "Setvar: LANG=$ref->{'lang'}\n"; - close $tmp; - - my ($login,$pass,$uid,$gid) = getpwnam($owner) - or die "user '$owner' not in passwd file"; - chown $uid, $gid, $tmp_filename; - chmod 0600, $tmp_filename; - - my $out_filename = "$cfg{spool}/".basename($tmp_filename); - move "$tmp_filename", $out_filename - or die "Failed to move call '$tmp_filename' file to spool: $!\n"; - - if($ref->{'recur'} eq "never") { - $sth_d->execute($ref->{'id'}) - or die "Cannot execute: ".$DBI::errstr; - } -} - -$sth->finish; -$sth_d->finish; - -$dbh->disconnect; - -exit 0; +#!/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 + +CONFIG_FILE = "/etc/ngcp-reminder/reminder.conf" +LOG_FILE = "/var/log/ngcp/ngcp-reminder.log" +OWNER = "asterisk" + +# --- Logging setup ----------------------------------------------------------- +SYSLOG_ADDRESS = '/dev/log' # Standard Unix socket for local syslog + +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('%(name)s: [%(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(OWNER).pw_uid + gid = pwd.getpwnam(OWNER).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": + 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 diff --git a/reminder.conf b/reminder.conf index 66ff728..b8be059 100644 --- a/reminder.conf +++ b/reminder.conf @@ -8,7 +8,7 @@ retries = 2 retry_time = 60 wait_time = 30 context = reminder -sip_peer = sip_proxy +sip_peer = sip_proxy_ep spool = /var/spool/asterisk/outgoing tmpdir = /tmp