#!/usr/bin/perl -CSD # Purpose: template toolkit helper script ################################################################################ use v5.40; use English; use YAML::XS qw(LoadFile); use Hash::Merge qw(merge); use DBI; use Capture::Tiny qw(capture); use Config::Tiny; use DateTime::TimeZone; use DateTime::TimeZone::Catalog; use List::Util qw(any none); # vars my $config = {}; foreach my $file (@ARGV) { next unless -f $file; $config = merge($config, LoadFile($file)); } my $dbcredentials = "/etc/mysql/sipwise_extra.cnf"; my $ngcp_roles_file = "/etc/default/ngcp-roles"; my $ngcp_current_site = "/etc/ngcp-config/sites/current"; my $ngcp_roles = read_ngcp_roles(); my $ngcp_type = $ngcp_roles->{NGCP_TYPE}; my $ngcp_is_proxy = $ngcp_roles->{NGCP_IS_PROXY}; my @db_conn_types = qw(pair local); my $db_conn = {}; my $db_info = {}; my $dbname = $config->{ossbss}->{provisioning}->{database}->{name}; # funcs sub read_ngcp_roles { my $root = Config::Tiny->read($ngcp_roles_file, 'utf8'); if (my $err = $Config::Tiny::errstr) { print "Error when reading $ngcp_roles_file: $err\n"; exit 1; } unless ($root->{_}) { print "Unexpected result from reading $ngcp_roles_file\n"; exit 1; } map { $root->{_}{$_} =~ s/(^['"]|['"]$)//g } keys %{$root->{_}}; return $root->{_}; } sub get_current_site { my $link = readlink $ngcp_current_site; if (!$link && $!) { print "Cannot read link $link: $!\n"; return; } my $sites = $config->{sites} // return; my $current_site = $sites->{$link} // return; if (ref $current_site ne 'HASH') { print "Invalid sites configuration\n"; return; } return { name => $link, %{$current_site}, }; } sub get_multi_site_data { my $multi_site = $config->{sites_enable} // 'no' eq 'yes'; return unless $multi_site; my $sites = $config->{sites} // {}; my $current_site = get_current_site(); my $skip_site_prefixes = []; foreach my $site_name (keys %{$sites}) { next if $site_name eq $current_site->{name}; push @{$skip_site_prefixes}, "$site_name:"; } return { sites_enable => $multi_site, sites => $sites, current_site => $current_site, skip_site_prefixes => $skip_site_prefixes, } } sub db_connect { my ($dbhost, $dbport) = @_; my $dsn = "DBI:mysql:database=${dbname};host=${dbhost};port=${dbport};" . "mysql_read_default_file=${dbcredentials}"; my $dbh = DBI->connect($dsn, "", "", { PrintError => 0 }); if ($DBI::err) { die "Error: Could not connect to database '$dbname'" . " at '$dbhost:$dbport' using '${dbcredentials}': $DBI::errstr\n"; } return $dbh; } sub sync_lb_and_extra_sockets { my $extra_sockets_config = shift; my $usr_pref = shift; my $dom_pref = shift; my $peer_pref = shift; my $pref_name = shift; my $hosts = $config->{hosts} // die "Error: cannot fetch hosts\n"; my $lb_config = $config->{kamailio}{lb} // die "Error: cannot fetch kamailio lb config\n"; my %lb_pairs = (); my %ports = ( udp => $lb_config->{port}, tcp => $lb_config->{port}, tls => $lb_config->{tls}{port}, ); my $multi_site_data = get_multi_site_data(); my ($multi_site, $current_site, $skip_site_prefixes) = @{$multi_site_data}{qw/sites_enable current_site skip_site_prefixes/}; for my $host (keys %{$hosts}) { my $host_ref = $hosts->{$host}; my $ifaces = $host_ref->{interfaces}; next unless $host_ref->{status} eq 'online'; next unless any { $_ eq 'lb' } @{$host_ref->{role}}; foreach my $iface (@{$ifaces}) { my $iface_ref = $host_ref->{$iface}; next if none { $_ eq 'sip_ext' } @{$iface_ref->{type}}; my $shared_ips = $iface_ref->{shared_ip} // next; my $pair = substr $host, 0, -1; foreach my $ip (@{$shared_ips}) { for my $proto (qw(udp tcp tls)) { my $port = $ports{$proto}; my $label = "$pair:$iface:$ip:$proto"; if ($multi_site) { my $site_name = $current_site->{name} // 'unknown'; $label = "$site_name:$pair:$iface:$ip:$proto"; } $lb_pairs{$label} = "$proto:$ip:$port"; } } } } foreach my $extra_socket (keys %{$extra_sockets_config}) { my $label = $extra_socket; if ($multi_site) { my $site_name = $current_site->{name} // 'unknown'; $label = "$site_name:$label"; } $lb_pairs{$label} = $extra_sockets_config->{$extra_socket}; } generic_enum_sync( \%lb_pairs, $usr_pref, $dom_pref, $peer_pref, $pref_name, $skip_site_prefixes, ); return 1; } sub sync_rtp_interfaces { my $rtp_interfaces_config = shift; my $usr_pref = shift; my $dom_pref = shift; my $peer_pref = shift; my $pref_name = shift; my %rtp_interfaces = (); my $multi_site_data = get_multi_site_data(); my ($multi_site, $current_site, $skip_site_prefixes) = @{$multi_site_data}{qw/sites_enable current_site skip_site_prefixes/}; for my $rtp_interface (keys %{$rtp_interfaces_config}) { my $label = "$rtp_interface"; if ($multi_site) { my $site_name = $current_site->{name} // 'unknown'; $label = "$site_name:$rtp_interface"; } $rtp_interfaces{$label} = $rtp_interface; } generic_enum_sync( \%rtp_interfaces, $usr_pref, $dom_pref, $peer_pref, $pref_name, $skip_site_prefixes, ); return 1; } sub sync_smsc_peers { return generic_enum_sync(@_); } sub sync_general_timezone { if ($ngcp_type eq 'carrier' && $ngcp_is_proxy eq 'yes') { return do_sync_general_timezone(@_, 'pair') && do_sync_general_timezone(@_, 'local'); } return do_sync_general_timezone(@_, 'pair'); } sub sync_db_timezones { if ($ngcp_type eq 'carrier' && $ngcp_is_proxy eq 'yes') { return do_sync_db_timezones(@_, 'pair') && do_sync_db_timezones(@_, 'local'); } return do_sync_db_timezones(@_, 'pair'); } sub sync_timezone_version { if ($ngcp_type eq 'carrier' && $ngcp_is_proxy eq 'yes') { return do_sync_timezone_version(@_, 'pair') && do_sync_timezone_version(@_, 'local'); } return do_sync_timezone_version(@_, 'pair'); } sub generic_enum_sync { my $config_hash = shift; my $usr_pref = shift; my $dom_pref = shift; my $peer_pref = shift; my $pref_name = shift; my $skip_site_prefixes = shift // []; my $dbh = $db_conn->{pair}; my ($pref_id) = $dbh->selectrow_array(<prepare(<prepare(<prepare(<prepare(<execute($pref_id); if ($DBI::err) { die "Cannot not fetch preferences data: $DBI::errstr\n"; } my $db_hash = $sth->fetchall_hashref('label'); $sth->finish; foreach my $row (sort keys %{$db_hash}) { $row = $db_hash->{$row}; next if $row->{label} eq 'default'; next if any { $row->{label} =~ /^$_/ } @{$skip_site_prefixes}; if (!exists $config_hash->{$row->{label}}) { print "$pref_name $row->{label} does not exist anymore in config, delete from db\n"; $enum_delete_sth->execute($row->{id}); } elsif ($config_hash->{$row->{label}} ne $row->{value}) { print "update $pref_name $row->{label}=$row->{value} to $row->{label}=$config_hash->{$row->{label}} in db\n"; $enum_update_sth->execute($config_hash->{$row->{label}}, $row->{id}); delete $config_hash->{$row->{label}}; } else { print "$pref_name $row->{label}=$row->{value} is sync between config and db\n"; delete $config_hash->{$row->{label}}; } } foreach my $label (sort keys %{$config_hash}) { print "insert new $pref_name $label=$config_hash->{$label} from config into db\n"; $enum_insert_sth->execute($pref_id, $label, $config_hash->{$label}, $usr_pref ? 1 : 0, $dom_pref ? 1 : 0, $peer_pref ? 1: 0); } $enum_insert_sth->finish; $enum_update_sth->finish; $enum_delete_sth->finish; return 1; } sub do_sync_general_timezone { my $tz = shift; my $db_conn_type = shift; my $dbh = $db_conn->{$db_conn_type}; my $ok = 1; my ($sql_log_bin) = $dbh->selectrow_array('SELECT @@sql_log_bin'); try { die "Error: general.timezone value is not set\n" unless $tz; $dbh->do("SET sql_log_bin=0"); my ($current_tz) = $dbh->selectrow_array(<do(<do("SET sql_log_bin=$sql_log_bin"); return $ok; } sub do_sync_db_timezones { my $db_conn_type = shift; my $dbh = $db_conn->{$db_conn_type}; my ($dbhost, $dbport) = @{$db_info->{$db_conn_type}}{qw(host port)}; my ($tzinfo_version, undef, undef) = capture { system('dpkg-query -f "\${Version}" -W tzdata'); }; unless ($tzinfo_version) { print "Error: Could not retrieve tzdata package version\n"; return; } chomp $tzinfo_version; my $sql = ''; try { $dbh->begin_work() or die "Cannot start tx: $DBI::errstr\n"; my ($cur_tzinfo_version) = $dbh->selectrow_array(<do('USE mysql') or die "Cannot use mysql database: $DBI::errstr\n"; my $batch; my $delim = ';'; while ($sql = <$sql_stream>) { if ($sql =~ m#^\s*\\d\s+(\S+)#) { $delim = $1; } else { if ($sql =~ /^(.*)\Q$delim\E\s*$/) { if ($1) { $batch .= $1; } $dbh->do($batch) or die "Cannot insert timezone data: $DBI::errstr\n"; $batch = undef; } else { $batch .= $sql; } } } close $sql_stream; $dbh->do(<rollback(); return; } $dbh->commit(); return 1; } sub do_sync_timezone_version { my $db_conn_type = shift; my $dbh = $db_conn->{$db_conn_type}; my $olson_tz_version = DateTime::TimeZone::Catalog::OlsonVersion(); unless ($olson_tz_version) { print "Error: Could not fetch timezone catalog version\n"; return; } my $sql = ''; try { $dbh->begin_work() or die "Cannot start tx: $DBI::errstr\n"; my ($cur_tz_version) = $dbh->selectrow_array(<links; my $ch_upd_timezone = $dbh->prepare(<execute($new, $old) or die "Cannot execute: $DBI::errstr\n"; } $ch_upd_timezone->finish; $dbh->do(<rollback(); return; } $dbh->commit(); return 1; } sub main { unless ($dbname) { print "Error: Could not determine provisioning db name\n"; return 1; } # connect to the dbs foreach my $db_conn_type (@db_conn_types) { next if $db_conn_type eq 'local' && ($ngcp_type ne 'carrier' || $ngcp_is_proxy ne "yes"); my $dbhost = $config->{database}->{$db_conn_type}->{dbhost}; my $dbport = $config->{database}->{$db_conn_type}->{dbport}; unless ($dbhost) { print "Error: Could not determine '$db_conn_type' db hostname\n"; return 1; } unless ($dbport) { print "Error: Could not determine '$db_conn_type' db port\n"; return 1; } $db_conn->{$db_conn_type} = db_connect($dbhost, $dbport); $db_info->{$db_conn_type} = { host => $dbhost, port => $dbport }; } # lb nodes and kamailio.lb.extra_sockets sync_lb_and_extra_sockets( $config->{kamailio}->{lb}->{extra_sockets}, 1, 1, 1, 'outbound_socket', ) or return 1; # rtp_* interfaces sync_rtp_interfaces( { map { my $tmp = $_; $tmp =~ s/:.*//; ($_ => $_, $tmp => $tmp); } @{$config->{rtp_interfaces}} }, 1, 1, 1, 'rtp_interface', ) or return 1; # smsc_* interfaces sync_smsc_peers( { map { ($_->{id} => $_->{id}) } @{$config->{sms}->{smsc}} }, 0, 1, 0, 'smsc_peer', ) or return 1; # general.timezone sync_general_timezone($config->{general}->{timezone}) or return 1; # /usr/share/zoneinfo into MariaDB sync_db_timezones() or return 1; # check ngcp.timezone version and upgrade the database # if the version is changed sync_timezone_version() or return 1; # disconnect from dbs map { $_->disconnect; } values %${db_conn}; return 0; } # code my $rc = main(); exit $rc; # eof