diff options
Diffstat (limited to 'FS/bin')
-rw-r--r-- | FS/bin/freeside-addoutsource | 20 | ||||
-rw-r--r-- | FS/bin/freeside-addoutsourceuser | 4 | ||||
-rwxr-xr-x | FS/bin/freeside-daily | 90 | ||||
-rw-r--r-- | FS/bin/freeside-prepaidd | 75 | ||||
-rw-r--r-- | FS/bin/freeside-queued | 109 | ||||
-rw-r--r-- | FS/bin/freeside-selfservice-server | 131 | ||||
-rwxr-xr-x | FS/bin/freeside-setup | 1086 | ||||
-rw-r--r-- | FS/bin/freeside-sqlradius-radacctd | 212 | ||||
-rwxr-xr-x | FS/bin/freeside-sqlradius-reset | 3 | ||||
-rwxr-xr-x | FS/bin/freeside-upgrade | 131 |
10 files changed, 1392 insertions, 469 deletions
diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource index db4e7a307..5cec17f46 100644 --- a/FS/bin/freeside-addoutsource +++ b/FS/bin/freeside-addoutsource @@ -4,21 +4,21 @@ domain=$1 createdb $domain && \ \ -mkdir /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain && \ +mkdir /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ \ -chown freeside /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain && \ +chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ \ -cp /home/ivan/freeside/conf/[a-z]* /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain && \ +cp /home/ivan/freeside/conf/[a-z]* /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ \ -touch /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain/secrets && \ +touch /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ \ -chown freeside /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain/secrets && \ +chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ \ -chmod 600 /usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain/secrets && \ +chmod 600 /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ \ -echo -e "DBI:Pg:dbname=$domain\nfreeside\n" >/usr/local/etc/freeside/conf.DBI:Pg:dbname=$domain/secrets && \ +echo -e "DBI:Pg:host=localhost;dbname=$domain\nfreeside\n" >/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ \ -mkdir /usr/local/etc/freeside/counters.DBI:Pg:dbname=$domain && \ -mkdir /usr/local/etc/freeside/cache.DBI:Pg:dbname=$domain && \ -mkdir /usr/local/etc/freeside/export.DBI:Pg:dbname=$domain +mkdir /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \ +mkdir /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \ +mkdir /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser index cad07f1fd..abb515b6f 100644 --- a/FS/bin/freeside-addoutsourceuser +++ b/FS/bin/freeside-addoutsourceuser @@ -5,11 +5,11 @@ domain=$2 password=$3 freeside-adduser -h /usr/local/etc/freeside/htpasswd \ - -s conf.DBI:Pg:dbname=$domain/secrets \ + -s conf.DBI:Pg:host=localhost\;dbname=$domain/secrets \ -b \ $username $password 2>/dev/null -[ -e /usr/local/etc/freeside/dbdef.DBI:Pg:dbname=$domain ] \ +[ -e /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain ] \ || ( freeside-setup -s $username 2>/dev/null; \ /home/ivan/freeside/bin/populate-msgcat $username 2>/dev/null ) diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index 603da12b8..ae71112dd 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -5,78 +5,31 @@ use Fcntl qw(:flock); use Date::Parse; use Getopt::Std; use FS::UID qw(adminsuidsetup driver_name dbh datasrc); -use FS::Record qw(qsearch qsearchs dbdef); +use FS::Record qw(qsearch qsearchs); use FS::Conf; use FS::cust_main; &untaint_argv; #what it sounds like (eww) -use vars qw($opt_d $opt_v $opt_p $opt_a $opt_s $opt_y); -getopts("p:a:d:vsy:"); +use vars qw($opt_d $opt_v $opt_p $opt_s $opt_y); +getopts("p:d:vsy:"); my $user = shift or die &usage; adminsuidsetup $user; $FS::cust_main::DEBUG = 1 if $opt_v; -my %search = (); -$search{'payby'} = $opt_p if $opt_p; -$search{'agentnum'} = $opt_a if $opt_a; +my %search; +$search{'payby'} = $opt_p if $opt_p; + +my @cust_main = @ARGV + ? map { qsearchs('cust_main', { custnum => $_, %search } ) } @ARGV + : qsearch('cust_main', \%search ) +; #we're at now now (and later). my($time)= $opt_d ? str2time($opt_d) : $^T; $time += $opt_y * 86400 if $opt_y; -# select * from cust_main where -my $where_pkg = <<"END"; - 0 < ( select count(*) from cust_pkg - where cust_main.custnum = cust_pkg.custnum - and ( cancel is null or cancel = 0 ) - and ( setup is null or setup = 0 - or bill is null or bill <= $time - or ( expire is not null and expire <= $^T ) - ) - ) -END - -# or -my $where_bill_event = <<"END"; - 0 < ( select count(*) from cust_bill - where cust_main.custnum = cust_bill.custnum - and 0 < charged - - coalesce( - ( select sum(amount) from cust_bill_pay - where cust_bill.invnum = cust_bill_pay.invnum ) - ,0 - ) - - coalesce( - ( select sum(amount) from cust_credit_bill - where cust_bill.invnum = cust_credit_bill.invnum ) - ,0 - ) - and 0 < ( select count(*) from part_bill_event - where payby = cust_main.payby - and ( disabled is null or disabled = '' ) - and seconds <= $time - cust_bill._date - and 0 = ( select count(*) from cust_bill_event - where cust_bill.invnum = cust_bill_event.invnum - and part_bill_event.eventpart = cust_bill_event.eventpart - and status = 'done' - ) - - ) - ) -END - -my $extra_sql = ( scalar(%search) ? ' AND ' : ' WHERE ' ). "( $where_pkg OR $where_bill_event )"; - -my @cust_main; -if ( @ARGV ) { - @cust_main = map { qsearchs('cust_main', { custnum => $_, %search } ) } @ARGV -} else { - @cust_main = qsearch('cust_main', \%search, '', $extra_sql ); -} -; - my($cust_main,%saw); foreach $cust_main ( @cust_main ) { @@ -89,19 +42,6 @@ foreach $cust_main ( @cust_main ) { $cust_main->custnum. ": $error" if $error; } - # $^T not $time because -d is for pre-printing invoices - foreach my $cust_pkg ( - grep { $_->part_pkg->is_prepaid - && $_->bill && $_->bill < $^T && ! $_->susp - } - $cust_main->ncancelled_pkgs - ) { - my $error = $cust_pkg->suspend; - warn "Error suspending package ". $cust_pkg->pkgnum. - " for custnum ". $cust_main->custnum. - ": $error" - if $error; - } my $error = $cust_main->bill( 'time' => $time, 'resetup' => $opt_s, ); @@ -117,10 +57,8 @@ foreach $cust_main ( @cust_main ) { if ( driver_name eq 'Pg' ) { dbh->{AutoCommit} = 1; #so we can vacuum - foreach my $table ( dbdef->tables ) { - my $sth = dbh->prepare("VACUUM ANALYZE $table") or die dbh->errstr; - $sth->execute or die $sth->errstr; - } + my $sth = dbh->prepare('vacuum analyze') or die dbh->errstr; + $sth->execute or die $sth->errstr; } my $conf = new FS::Conf; @@ -172,7 +110,7 @@ freeside-daily - Run daily billing and invoice collection events. =head1 SYNOPSIS - freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] user [ custnum custnum ... ] + freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -s ] [ -v ] user [ custnum custnum ... ] =head1 DESCRIPTION @@ -194,8 +132,6 @@ the bill and collect methods of a cust_main object. See L<FS::cust_main>. -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>) - -a: Only process customers with the specified agentnum - -s: re-charge setup fees -v: enable debugging diff --git a/FS/bin/freeside-prepaidd b/FS/bin/freeside-prepaidd deleted file mode 100644 index e51a56350..000000000 --- a/FS/bin/freeside-prepaidd +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/perl -w - -use strict; -use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm); -use FS::UID qw(adminsuidsetup); -use FS::Record qw(qsearch); # qsearchs); -use FS::cust_pkg; - -my $user = shift or die &usage; - -#daemonize1('freeside-sprepaidd', $user); #keep unique pid files w/multi installs -daemonize1('freeside-prepaidd'); - -drop_root(); - -adminsuidsetup($user); - -logfile( "/usr/local/etc/freeside/prepaidd-log.". $FS::UID::datasrc ); - -daemonize2(); - -#-- - -while (1) { - - foreach my $cust_pkg ( - qsearch( { - 'select' => 'cust_pkg.*, part_pkg.plan', - 'table' => 'cust_pkg', - 'addl_from' => 'LEFT JOIN part_pkg USING ( pkgpart )', - #'hashref' => { 'plan' => 'prepaid' },#should check part_pkg::is_prepaid - #'extra_sql' => "AND bill < ". time. - 'hashref' => {}, - 'extra_sql' => "WHERE plan = 'prepaid' AND bill < ". time. - " AND bill IS NOT NULL". - " AND ( susp IS NULL OR susp = 0)". - " AND ( cancel IS NULL OR cancel = 0)" - } ) - ) { - my $error = $cust_pkg->suspend; - warn "Error suspended package ". $cust_pkg->pkgnum. - " for custnum ". $cust_pkg->custnum. - ": $error\n" - if $error; - } - - die "exiting" if sigterm() || sigint(); - sleep 5; - -} - -#-- - -sub usage { - die "Usage:\n\n freeside-prepaidd user\n"; -} - -=head1 NAME - -freeside-prepaidd - Real-time daemon for prepaid packages - -=head1 SYNOPSIS - - freeside-prepaidd - -=head1 DESCRIPTION - -Runs continuously and suspendes any prepaid customer packages which have -passed their renewal date (next bill date). - -=head1 SEE ALSO - -=cut - -1; diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued index 3a0a9b4e5..e14ddad8e 100644 --- a/FS/bin/freeside-queued +++ b/FS/bin/freeside-queued @@ -1,11 +1,13 @@ #!/usr/bin/perl -w use strict; -use vars qw( $DEBUG $kids $max_kids %kids ); -use POSIX qw(:sys_wait_h); +use vars qw( $log_file $sigterm $sigint $kids $max_kids %kids ); +use subs qw( _die _logmsg ); +use Fcntl qw(:flock); +use POSIX qw(:sys_wait_h setsid); +use Date::Format; use IO::File; use FS::UID qw(adminsuidsetup forksuidsetup driver_name dbh myconnect); -use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm); use FS::Record qw(qsearch qsearchs); use FS::queue; use FS::queue_depend; @@ -16,24 +18,40 @@ use FS::svc_acct; use Net::SSH 0.07; use FS::part_export; -$DEBUG = 0; - $max_kids = '10'; #guess it should be a config file... $kids = 0; my $user = shift or die &usage; -warn "starting daemonization (forking)\n" if $DEBUG; -#daemonize1('freeside-queued',$user); #to keep pid files unique w/multi installs -daemonize1('freeside-queued'); +#my $pid_file = "/var/run/freeside-queued.$user.pid"; +my $pid_file = "/var/run/freeside-queued.pid"; + +&daemonize1; + +#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +#$SIG{CHLD} = \&REAPER; -warn "dropping privledges\n" if $DEBUG; -drop_root(); +$sigterm = 0; +$sigint = 0; +$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; }; +$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; }; +my $freeside_gid = scalar(getgrnam('freeside')) + or die "can't setgid to freeside group\n"; +$) = $freeside_gid; +$( = $freeside_gid; +#if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd +($(,$)) = ($),$(); +$) = $freeside_gid; + +$> = $FS::UID::freeside_uid; +$< = $FS::UID::freeside_uid; +#freebsd is sofa king broken, won't setuid() +($<,$>) = ($>,$<); +$> = $FS::UID::freeside_uid; $ENV{HOME} = (getpwuid($>))[7]; #for ssh -warn "connecting to database\n" if $DEBUG; $@ = 'not connected'; while ( $@ ) { eval { adminsuidsetup $user; }; @@ -44,12 +62,14 @@ while ( $@ ) { } } -logfile( "/usr/local/etc/freeside/queuelog.". $FS::UID::datasrc ); +$log_file = "/usr/local/etc/freeside/queuelog.". $FS::UID::datasrc; + +&daemonize2; -warn "completing daemonization (detaching))\n" if $DEBUG; -daemonize2(); +$SIG{__DIE__} = \&_die; +$SIG{__WARN__} = \&_logmsg; -#-- +warn "freeside-queued starting\n"; my $warnkids=0; while (1) { @@ -67,7 +87,7 @@ while (1) { unless ( dbh && dbh->ping ) { warn "WARNING: connection to database lost, reconnecting...\n"; - eval { $FS::UID::dbh = myconnect; }; + eval { myconnect; }; unless ( !$@ && dbh && dbh->ping ) { warn "WARNING: still no connection to database, sleeping for retry...\n"; @@ -142,7 +162,6 @@ while (1) { #} my @args = $ljob->args; - splice @args, 0, 1, $ljob if $args[0] eq '_JOB'; defined( my $pid = fork ) or do { warn "WARNING: can't fork: $!\n"; @@ -165,12 +184,8 @@ while (1) { forksuidsetup($user); - #auto-use classes... - #if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) { - if ( $ljob->job =~ /(FS::part_export::\w+)::/ - || $ljob->job =~ /(FS::\w+)::/ - ) - { + #auto-use export classes... + if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) { my $class = $1; eval "use $class;"; if ( $@ ) { @@ -186,7 +201,7 @@ while (1) { } my $eval = "&". $ljob->job. '(@args);'; - warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG; + warn "running $eval"; eval $eval; #throw away return value? suppose so if ( $@ ) { warn "job $eval failed"; @@ -205,11 +220,11 @@ while (1) { } } continue { - if ( sigterm() ) { + if ( $sigterm ) { warn "received TERM signal; exiting\n"; exit; } - if ( sigint() ) { + if ( $sigint ) { warn "received INT signal; exiting\n"; exit; } @@ -219,6 +234,48 @@ sub usage { die "Usage:\n\n freeside-queued user\n"; } +sub _die { + my $msg = shift; + unlink $pid_file if -e $pid_file; + _logmsg($msg); +} + +sub _logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n"; + flock($log, LOCK_UN); + close $log; +} + +sub daemonize1 { + + chdir "/" or die "Can't chdir to /: $!"; + open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; + defined(my $pid = fork) or die "Can't fork: $!"; + if ( $pid ) { + print "freeside-queued started with pid $pid\n"; #logging to $log_file\n"; + exit unless $pid_file; + my $pidfh = new IO::File ">$pid_file" or exit; + print $pidfh "$pid\n"; + exit; + } + #open STDOUT, '>/dev/null' + # or die "Can't write to /dev/null: $!"; + #setsid or die "Can't start a new session: $!"; + #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + +} + +sub daemonize2 { + open STDOUT, '>/dev/null' + or die "Can't write to /dev/null: $!"; + setsid or die "Can't start a new session: $!"; + open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; +} + sub reap_kids { foreach my $pid ( keys %kids ) { my $kid = waitpid($pid, WNOHANG); diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server index c73349a60..c045893d1 100644 --- a/FS/bin/freeside-selfservice-server +++ b/FS/bin/freeside-selfservice-server @@ -1,16 +1,23 @@ #!/usr/bin/perl -w +# +# freeside-selfservice-server + +# alas, much false laziness with freeside-queued and fs_signup_server. at +# least it is slated to replace fs_{signup,passwd,mailadmin}_server +# should probably generalize the version in here, or better yet use +# Proc::Daemon or somesuch use strict; -use vars qw( $Debug %kids $kids $max_kids $ssh_pid $keepalives ); -use subs qw( lock_write unlock_write myshutdown usage ); +use vars qw( $Debug %kids $kids $max_kids $shutdown $log_file $ssh_pid + $keepalives ); +use subs qw( lock_write unlock_write ); use Fcntl qw(:flock); -use POSIX qw(:sys_wait_h); +use POSIX qw(:sys_wait_h setsid); use IO::Handle; use IO::Select; use IO::File; use Storable 2.09 qw(nstore_fd fd_retrieve); use Net::SSH qw(sshopen2); -use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm); use FS::UID qw(adminsuidsetup forksuidsetup); use FS::ClientAPI; @@ -21,6 +28,7 @@ use FS::cust_pkg; $Debug = 1; # 2 will turn on more logging # 3 will log packet contents, including passwords +$shutdown = 0; $max_kids = '10'; #? $keepalives = 0; #let clientd turn it on, so we don't barf on old ones $kids = 0; @@ -29,31 +37,12 @@ my $user = shift or die &usage; my $machine = shift or die &usage; my $tag = scalar(@ARGV) ? shift : ''; -my $lock_file = "/usr/local/etc/freeside/selfservice.$machine.writelock"; - - -# to keep pid files unique w/multi machines (and installs!) # $FS::UID::datasrc not posible -daemonize1("freeside-selfservice-server","$user.$machine"); - -#false laziness w/Daemon::drop_root -my $freeside_gid = scalar(getgrnam('freeside')) - or die "can't find freeside group\n"; - -open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!"; -chown $FS::UID::freeside_uid, $freeside_gid, $lock_file; - -drop_root(); - -$ENV{HOME} = (getpwuid($>))[7]; #for ssh +my $pid_file = "/var/run/freeside-selfservice-server.$user.$machine.pid"; -adminsuidsetup $user; - -#logfile("/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc); #MACHINE -logfile("/usr/local/etc/freeside/selfservice.$machine.log"); - -daemonize2(); +my $lock_file = "/usr/local/etc/freeside/selfservice.$machine.writelock"; +&init($user); my $conf = new FS::Conf; @@ -80,7 +69,7 @@ while (1) { $undisp = 1; my @handles = $s->can_read(5); unless ( @handles ) { - myshutdown() if sigint() || sigterm(); + &shutdown if $shutdown; if ( $keepalives && $keepalive_count++ > 10 ) { $keepalive_count = 0; lock_write; @@ -161,7 +150,7 @@ while (1) { } - myshutdown if sigint() || sigterm(); + &shutdown if $shutdown; warn "connection lost, reconnecting\n" if $Debug; sleep 3; @@ -183,7 +172,70 @@ sub reap_kids { #warn "done reaping\n"; } -sub myshutdown { +sub init { + my $user = shift; + + chdir "/" or die "Can't chdir to /: $!"; + open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; + defined(my $pid = fork) or die "Can't fork: $!"; + if ( $pid ) { + print "freeside-selfservice-server to $machine started with pid $pid\n"; #logging to $log_file + exit unless $pid_file; + my $pidfh = new IO::File ">$pid_file" or exit; + print $pidfh "$pid\n"; + exit; + } + +# sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +# #sub REAPER { my $pid = wait; $kids--; $SIG{CHLD} = \&REAPER; } +# $SIG{CHLD} = \&REAPER; + + $shutdown = 0; + $SIG{HUP} = sub { warn "SIGHUP received; shutting down\n"; $shutdown++; }; + $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $shutdown++; }; + $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $shutdown++; }; + $SIG{QUIT} = sub { warn "SIGQUIT received; shutting down\n"; $shutdown++; }; + $SIG{PIPE} = sub { warn "SIGPIPE received; shutting down\n"; $shutdown++; }; + + #false laziness w/freeside-queued + my $freeside_gid = scalar(getgrnam('freeside')) + or die "can't setgid to freeside group\n"; + + open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!"; + chown $FS::UID::freeside_uid, $freeside_gid, $lock_file; + + $) = $freeside_gid; + $( = $freeside_gid; + #if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd + ($(,$)) = ($),$(); + $) = $freeside_gid; + + $> = $FS::UID::freeside_uid; + $< = $FS::UID::freeside_uid; + #freebsd is sofa king broken, won't setuid() + ($<,$>) = ($>,$<); + $> = $FS::UID::freeside_uid; + #eslaf + + $ENV{HOME} = (getpwuid($>))[7]; #for ssh + adminsuidsetup $user; + + #$log_file = "/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc; #MACHINE NAME + $log_file = "/usr/local/etc/freeside/selfservice.$machine.log"; + + open STDOUT, '>/dev/null' + or die "Can't write to /dev/null: $!"; + setsid or die "Can't start a new session: $!"; + open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + + $SIG{__DIE__} = \&_die; + $SIG{__WARN__} = \&_logmsg; + + warn "freeside-selfservice-server starting\n"; + +} + +sub shutdown { &reap_kids; my $wait = 12; #wait up to 1 minute while ( $kids > 0 && $wait-- ) { @@ -196,6 +248,27 @@ sub myshutdown { die "exiting"; } +sub _die { + my $msg = shift; + unlink $pid_file if -e $pid_file; + _logmsg($msg); +} + +sub _logmsg { + chomp( my $msg = shift ); + _do_logmsg( "[server] [". scalar(localtime). "] [$$] $msg\n" ); +} + +sub _do_logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "$msg\n"; + flock($log, LOCK_UN); + close $log; +} + sub lock_write { warn "locking $lock_file mutex for write to write stream\n" if $Debug > 1; diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup index a16e51749..c867a721f 100755 --- a/FS/bin/freeside-setup +++ b/FS/bin/freeside-setup @@ -1,24 +1,29 @@ #!/usr/bin/perl -Tw #to delay loading dbdef until we're ready -BEGIN { $FS::Schema::setup_hack = 1; } +BEGIN { $FS::Record::setup_hack = 1; } use strict; use vars qw($opt_s); use Getopt::Std; use Locale::Country; use Locale::SubCountry; +use DBI; +use DBIx::DBSchema 0.21; +use DBIx::DBSchema::Table; +use DBIx::DBSchema::Column; +use DBIx::DBSchema::ColGroup::Unique; +use DBIx::DBSchema::ColGroup::Index; use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets); -use FS::Schema qw( dbdef_dist reload_dbdef ); use FS::Record; use FS::cust_main_county; -#use FS::raddb; +use FS::raddb; use FS::part_bill_event; die "Not running uid freeside!" unless checkeuid(); -#my %attrib2db = -# map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib; +my %attrib2db = + map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib; getopts("s"); my $user = shift or die &usage; @@ -70,25 +75,154 @@ my $username_len = 32; #usernamemax config file # $x =~ /^y/i; #} -#my @check_attributes = (); #add later -#my @attributes = (); #add later -#my $ship = $opt_s; +my @check_attributes = (); #add later +my @attributes = (); #add later +my $ship = $opt_s; + +### + +my($char_d) = 80; #default maxlength for text fields + +#my(@date_type) = ( 'timestamp', '', '' ); +my(@date_type) = ( 'int', 'NULL', '' ); +my(@perl_type) = ( 'text', 'NULL', '' ); +my @money_type = ( 'decimal', '', '10,2' ); ### # create a dbdef object from the old data structure ### -my $dbdef = dbdef_dist; +my(%tables)=&tables_hash_hack; + +#turn it into objects +my($dbdef) = new DBIx::DBSchema ( map { + my(@columns); + while (@{$tables{$_}{'columns'}}) { + my($name,$type,$null,$length)=splice @{$tables{$_}{'columns'}}, 0, 4; + push @columns, new DBIx::DBSchema::Column ( $name,$type,$null,$length ); + } + DBIx::DBSchema::Table->new( + $_, + $tables{$_}{'primary_key'}, + DBIx::DBSchema::ColGroup::Unique->new($tables{$_}{'unique'}), + DBIx::DBSchema::ColGroup::Index->new($tables{$_}{'index'}), + @columns, + ); +} (keys %tables) ); + +my $cust_main = $dbdef->table('cust_main'); +unless ($ship) { #remove ship_ from cust_main + $cust_main->delcolumn($_) foreach ( grep /^ship_/, $cust_main->columns ); +} else { #add indices + push @{$cust_main->index->lol_ref}, + map { [ "ship_$_" ] } qw( last company daytime night fax ); +} + +#add radius attributes to svc_acct + +my($svc_acct)=$dbdef->table('svc_acct'); + +my($attribute); +foreach $attribute (@attributes) { + $svc_acct->addcolumn ( new DBIx::DBSchema::Column ( + 'radius_'. $attribute, + 'varchar', + 'NULL', + $char_d, + )); +} + +foreach $attribute (@check_attributes) { + $svc_acct->addcolumn( new DBIx::DBSchema::Column ( + 'rc_'. $attribute, + 'varchar', + 'NULL', + $char_d, + )); +} + +#create history tables (false laziness w/create-history-tables) +foreach my $table ( grep { ! /^h_/ } $dbdef->tables ) { + my $tableobj = $dbdef->table($table) + or die "unknown table $table"; + + die "unique->lol_ref undefined for $table" + unless defined $tableobj->unique->lol_ref; + die "index->lol_ref undefined for $table" + unless defined $tableobj->index->lol_ref; + + my $h_tableobj = DBIx::DBSchema::Table->new( { + name => "h_$table", + primary_key => 'historynum', + unique => DBIx::DBSchema::ColGroup::Unique->new( [] ), + 'index' => DBIx::DBSchema::ColGroup::Index->new( [ + @{$tableobj->unique->lol_ref}, + @{$tableobj->index->lol_ref} + ] ), + columns => [ + DBIx::DBSchema::Column->new( { + 'name' => 'historynum', + 'type' => 'serial', + 'null' => 'NOT NULL', + 'length' => '', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_date', + 'type' => 'int', + 'null' => 'NULL', + 'length' => '', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_user', + 'type' => 'varchar', + 'null' => 'NOT NULL', + 'length' => '80', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_action', + 'type' => 'varchar', + 'null' => 'NOT NULL', + 'length' => '80', + 'default' => '', + 'local' => '', + } ), + map { + my $column = $tableobj->column($_); + + #clone so as to not disturb the original + $column = DBIx::DBSchema::Column->new( { + map { $_ => $column->$_() } + qw( name type null length default local ) + } ); + + $column->type('int') + if $column->type eq 'serial'; + #$column->default('') + # if $column->default =~ /^nextval\(/i; + #( my $local = $column->local ) =~ s/AUTO_INCREMENT//i; + #$column->local($local); + $column; + } $tableobj->columns + ], + } ); + $dbdef->addtable($h_tableobj); +} #important $dbdef->save($dbdef_file); -&FS::Schema::reload_dbdef($dbdef_file); +&FS::Record::reload_dbdef($dbdef_file); ### # create 'em ### -my $dbh = adminsuidsetup $user; +my($dbh)=adminsuidsetup $user; #create tables $|=1; @@ -98,12 +232,6 @@ foreach my $statement ( $dbdef->sql($dbh) ) { or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement"; } -#now go back and reverse engineer the db -#so we pick up the correct column DEFAULTs for #oidless inserts -dbdef_create($dbh, $dbdef_file); -delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload -reload_dbdef($dbdef_file); - #cust_main_county foreach my $country ( sort map uc($_), all_country_codes ) { @@ -168,15 +296,923 @@ $dbh->disconnect or die $dbh->errstr; #print "Freeside database initialized sucessfully\n"; -sub dbdef_create { # reverse engineer the schema from the DB and save to file - my( $dbh, $file ) = @_; - my $dbdef = new_native DBIx::DBSchema $dbh; - $dbdef->save($file); -} - sub usage { - die "Usage:\n freeside-setup user\n"; + die "Usage:\n freeside-setup [ -s ] user\n"; } -1; +### +# Now it becomes an object. much better. +### +sub tables_hash_hack { + + #note that s/(date|change)/_$1/; to avoid keyword conflict. + #put a kludge in FS::Record to catch this or? (pry need some date-handling + #stuff anyway also) + + my(%tables)=( #yech.} + + 'agent' => { + 'columns' => [ + 'agentnum', 'serial', '', '', + 'agent', 'varchar', '', $char_d, + 'typenum', 'int', '', '', + 'freq', 'int', 'NULL', '', + 'prog', @perl_type, + 'disabled', 'char', 'NULL', 1, + 'username', 'varchar', 'NULL', $char_d, + '_password','varchar', 'NULL', $char_d, + ], + 'primary_key' => 'agentnum', + 'unique' => [], + 'index' => [ ['typenum'], ['disabled'] ], + }, + + 'agent_type' => { + 'columns' => [ + 'typenum', 'serial', '', '', + 'atype', 'varchar', '', $char_d, + ], + 'primary_key' => 'typenum', + 'unique' => [], + 'index' => [], + }, + + 'type_pkgs' => { + 'columns' => [ + 'typenum', 'int', '', '', + 'pkgpart', 'int', '', '', + ], + 'primary_key' => '', + 'unique' => [ ['typenum', 'pkgpart'] ], + 'index' => [ ['typenum'] ], + }, + + 'cust_bill' => { + 'columns' => [ + 'invnum', 'serial', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'charged', @money_type, + 'printed', 'int', '', '', + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'invnum', + 'unique' => [], + 'index' => [ ['custnum'], ['_date'] ], + }, + + 'cust_bill_event' => { + 'columns' => [ + 'eventnum', 'serial', '', '', + 'invnum', 'int', '', '', + 'eventpart', 'int', '', '', + '_date', @date_type, + 'status', 'varchar', '', $char_d, + 'statustext', 'text', 'NULL', '', + ], + 'primary_key' => 'eventnum', + #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ], + 'unique' => [], + 'index' => [ ['invnum'], ['status'] ], + }, + + 'part_bill_event' => { + 'columns' => [ + 'eventpart', 'serial', '', '', + 'payby', 'char', '', 4, + 'event', 'varchar', '', $char_d, + 'eventcode', @perl_type, + 'seconds', 'int', 'NULL', '', + 'weight', 'int', '', '', + 'plan', 'varchar', 'NULL', $char_d, + 'plandata', 'text', 'NULL', '', + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'eventpart', + 'unique' => [], + 'index' => [ ['payby'], ['disabled'], ], + }, + + 'cust_bill_pkg' => { + 'columns' => [ + 'pkgnum', 'int', '', '', + 'invnum', 'int', '', '', + 'setup', @money_type, + 'recur', @money_type, + 'sdate', @date_type, + 'edate', @date_type, + 'itemdesc', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => '', + 'unique' => [], + 'index' => [ ['invnum'] ], + }, + + 'cust_bill_pkg_detail' => { + 'columns' => [ + 'detailnum', 'serial', '', '', + 'pkgnum', 'int', '', '', + 'invnum', 'int', '', '', + 'detail', 'varchar', '', $char_d, + ], + 'primary_key' => 'detailnum', + 'unique' => [], + 'index' => [ [ 'pkgnum', 'invnum' ] ], + }, + + 'cust_credit' => { + 'columns' => [ + 'crednum', 'serial', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'amount', @money_type, + 'otaker', 'varchar', '', 32, + 'reason', 'text', 'NULL', '', + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'crednum', + 'unique' => [], + 'index' => [ ['custnum'] ], + }, + + 'cust_credit_bill' => { + 'columns' => [ + 'creditbillnum', 'serial', '', '', + 'crednum', 'int', '', '', + 'invnum', 'int', '', '', + '_date', @date_type, + 'amount', @money_type, + ], + 'primary_key' => 'creditbillnum', + 'unique' => [], + 'index' => [ ['crednum'], ['invnum'] ], + }, + + 'cust_main' => { + 'columns' => [ + 'custnum', 'serial', '', '', + 'agentnum', 'int', '', '', +# 'titlenum', 'int', 'NULL', '', + 'last', 'varchar', '', $char_d, +# 'middle', 'varchar', 'NULL', $char_d, + 'first', 'varchar', '', $char_d, + 'ss', 'varchar', 'NULL', 11, + 'company', 'varchar', 'NULL', $char_d, + 'address1', 'varchar', '', $char_d, + 'address2', 'varchar', 'NULL', $char_d, + 'city', 'varchar', '', $char_d, + 'county', 'varchar', 'NULL', $char_d, + 'state', 'varchar', 'NULL', $char_d, + 'zip', 'varchar', 'NULL', 10, + 'country', 'char', '', 2, + 'daytime', 'varchar', 'NULL', 20, + 'night', 'varchar', 'NULL', 20, + 'fax', 'varchar', 'NULL', 12, + 'ship_last', 'varchar', 'NULL', $char_d, +# 'ship_middle', 'varchar', 'NULL', $char_d, + 'ship_first', 'varchar', 'NULL', $char_d, + 'ship_company', 'varchar', 'NULL', $char_d, + 'ship_address1', 'varchar', 'NULL', $char_d, + 'ship_address2', 'varchar', 'NULL', $char_d, + 'ship_city', 'varchar', 'NULL', $char_d, + 'ship_county', 'varchar', 'NULL', $char_d, + 'ship_state', 'varchar', 'NULL', $char_d, + 'ship_zip', 'varchar', 'NULL', 10, + 'ship_country', 'char', 'NULL', 2, + 'ship_daytime', 'varchar', 'NULL', 20, + 'ship_night', 'varchar', 'NULL', 20, + 'ship_fax', 'varchar', 'NULL', 12, + 'payby', 'char', '', 4, + 'payinfo', 'varchar', 'NULL', $char_d, + 'paycvv', 'varchar', 'NULL', 4, + #'paydate', @date_type, + 'paydate', 'varchar', 'NULL', 10, + 'payname', 'varchar', 'NULL', $char_d, + 'tax', 'char', 'NULL', 1, + 'otaker', 'varchar', '', 32, + 'refnum', 'int', '', '', + 'referral_custnum', 'int', 'NULL', '', + 'comments', 'text', 'NULL', '', + ], + 'primary_key' => 'custnum', + 'unique' => [], + #'index' => [ ['last'], ['company'] ], + 'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ], + [ 'daytime' ], [ 'night' ], [ 'fax' ], [ 'refnum' ], + ], + }, + + 'cust_main_invoice' => { + 'columns' => [ + 'destnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'dest', 'varchar', '', $char_d, + ], + 'primary_key' => 'destnum', + 'unique' => [], + 'index' => [ ['custnum'], ], + }, + + 'cust_main_county' => { #county+state+country are checked off the + #cust_main_county for validation and to provide + # a tax rate. + 'columns' => [ + 'taxnum', 'serial', '', '', + 'state', 'varchar', 'NULL', $char_d, + 'county', 'varchar', 'NULL', $char_d, + 'country', 'char', '', 2, + 'taxclass', 'varchar', 'NULL', $char_d, + 'exempt_amount', @money_type, + 'tax', 'real', '', '', #tax % + 'taxname', 'varchar', 'NULL', $char_d, + 'setuptax', 'char', 'NULL', 1, # Y = setup tax exempt + 'recurtax', 'char', 'NULL', 1, # Y = recur tax exempt + ], + 'primary_key' => 'taxnum', + 'unique' => [], + # 'unique' => [ ['taxnum'], ['state', 'county'] ], + 'index' => [], + }, + + 'cust_pay' => { + 'columns' => [ + 'paynum', 'serial', '', '', + #now cust_bill_pay #'invnum', 'int', '', '', + 'custnum', 'int', '', '', + 'paid', @money_type, + '_date', @date_type, + 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index into + # payment type table. + 'payinfo', 'varchar', 'NULL', $char_d, #see cust_main above + 'paybatch', 'varchar', 'NULL', $char_d, #for auditing purposes. + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'paynum', + 'unique' => [], + 'index' => [ [ 'custnum' ], [ 'paybatch' ], [ 'payby' ], [ '_date' ] ], + }, + + 'cust_pay_void' => { + 'columns' => [ + 'paynum', 'int', '', '', + 'custnum', 'int', '', '', + 'paid', @money_type, + '_date', @date_type, + 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index into + # payment type table. + 'payinfo', 'varchar', 'NULL', $char_d, #see cust_main above + 'paybatch', 'varchar', 'NULL', $char_d, #for auditing purposes. + 'closed', 'char', 'NULL', 1, + 'void_date', @date_type, + 'reason', 'varchar', 'NULL', $char_d, + 'otaker', 'varchar', '', 32, + ], + 'primary_key' => 'paynum', + 'unique' => [], + 'index' => [ [ 'custnum' ] ], + }, + + 'cust_bill_pay' => { + 'columns' => [ + 'billpaynum', 'serial', '', '', + 'invnum', 'int', '', '', + 'paynum', 'int', '', '', + 'amount', @money_type, + '_date', @date_type + ], + 'primary_key' => 'billpaynum', + 'unique' => [], + 'index' => [ [ 'paynum' ], [ 'invnum' ] ], + }, + + 'cust_pay_batch' => { #what's this used for again? list of customers + #in current CARD batch? (necessarily CARD?) + 'columns' => [ + 'paybatchnum', 'serial', '', '', + 'invnum', 'int', '', '', + 'custnum', 'int', '', '', + 'last', 'varchar', '', $char_d, + 'first', 'varchar', '', $char_d, + 'address1', 'varchar', '', $char_d, + 'address2', 'varchar', 'NULL', $char_d, + 'city', 'varchar', '', $char_d, + 'state', 'varchar', 'NULL', $char_d, + 'zip', 'varchar', 'NULL', 10, + 'country', 'char', '', 2, +# 'trancode', 'int', '', '', + 'cardnum', 'varchar', '', 16, + #'exp', @date_type, + 'exp', 'varchar', '', 11, + 'payname', 'varchar', 'NULL', $char_d, + 'amount', @money_type, + ], + 'primary_key' => 'paybatchnum', + 'unique' => [], + 'index' => [ ['invnum'], ['custnum'] ], + }, + + 'cust_pkg' => { + 'columns' => [ + 'pkgnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'pkgpart', 'int', '', '', + 'otaker', 'varchar', '', 32, + 'setup', @date_type, + 'bill', @date_type, + 'last_bill', @date_type, + 'susp', @date_type, + 'cancel', @date_type, + 'expire', @date_type, + 'manual_flag', 'char', 'NULL', 1, + ], + 'primary_key' => 'pkgnum', + 'unique' => [], + 'index' => [ ['custnum'] ], + }, + + 'cust_refund' => { + 'columns' => [ + 'refundnum', 'serial', '', '', + #now cust_credit_refund #'crednum', 'int', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'refund', @money_type, + 'otaker', 'varchar', '', 32, + 'reason', 'varchar', '', $char_d, + 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index + # into payment type table. + 'payinfo', 'varchar', 'NULL', $char_d, #see cust_main above + 'paybatch', 'varchar', 'NULL', $char_d, + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'refundnum', + 'unique' => [], + 'index' => [], + }, + + 'cust_credit_refund' => { + 'columns' => [ + 'creditrefundnum', 'serial', '', '', + 'crednum', 'int', '', '', + 'refundnum', 'int', '', '', + 'amount', @money_type, + '_date', @date_type + ], + 'primary_key' => 'creditrefundnum', + 'unique' => [], + 'index' => [ [ 'crednum', 'refundnum' ] ], + }, + + + 'cust_svc' => { + 'columns' => [ + 'svcnum', 'serial', '', '', + 'pkgnum', 'int', 'NULL', '', + 'svcpart', 'int', '', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ], + }, + + 'part_pkg' => { + 'columns' => [ + 'pkgpart', 'serial', '', '', + 'pkg', 'varchar', '', $char_d, + 'comment', 'varchar', '', $char_d, + 'promo_code', 'varchar', 'NULL', $char_d, + 'setup', @perl_type, + 'freq', 'varchar', '', $char_d, #billing frequency + 'recur', @perl_type, + 'setuptax', 'char', 'NULL', 1, + 'recurtax', 'char', 'NULL', 1, + 'plan', 'varchar', 'NULL', $char_d, + 'plandata', 'text', 'NULL', '', + 'disabled', 'char', 'NULL', 1, + 'taxclass', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => 'pkgpart', + 'unique' => [], + 'index' => [ [ 'promo_code' ], [ 'disabled' ] ], + }, + +# 'part_title' => { +# 'columns' => [ +# 'titlenum', 'int', '', '', +# 'title', 'varchar', '', $char_d, +# ], +# 'primary_key' => 'titlenum', +# 'unique' => [ [] ], +# 'index' => [ [] ], +# }, + + 'pkg_svc' => { + 'columns' => [ + 'pkgpart', 'int', '', '', + 'svcpart', 'int', '', '', + 'quantity', 'int', '', '', + 'primary_svc','char', 'NULL', 1, + ], + 'primary_key' => '', + 'unique' => [ ['pkgpart', 'svcpart'] ], + 'index' => [ ['pkgpart'] ], + }, + + 'part_referral' => { + 'columns' => [ + 'refnum', 'serial', '', '', + 'referral', 'varchar', '', $char_d, + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'refnum', + 'unique' => [], + 'index' => [ ['disabled'] ], + }, + + 'part_svc' => { + 'columns' => [ + 'svcpart', 'serial', '', '', + 'svc', 'varchar', '', $char_d, + 'svcdb', 'varchar', '', $char_d, + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'svcpart', + 'unique' => [], + 'index' => [ [ 'disabled' ] ], + }, + + 'part_svc_column' => { + 'columns' => [ + 'columnnum', 'serial', '', '', + 'svcpart', 'int', '', '', + 'columnname', 'varchar', '', 64, + 'columnvalue', 'varchar', 'NULL', $char_d, + 'columnflag', 'char', 'NULL', 1, + ], + 'primary_key' => 'columnnum', + 'unique' => [ [ 'svcpart', 'columnname' ] ], + 'index' => [ [ 'svcpart' ] ], + }, + + #(this should be renamed to part_pop) + 'svc_acct_pop' => { + 'columns' => [ + 'popnum', 'serial', '', '', + 'city', 'varchar', '', $char_d, + 'state', 'varchar', '', $char_d, + 'ac', 'char', '', 3, + 'exch', 'char', '', 3, + 'loc', 'char', 'NULL', 4, #NULL for legacy purposes + ], + 'primary_key' => 'popnum', + 'unique' => [], + 'index' => [ [ 'state' ] ], + }, + + 'part_pop_local' => { + 'columns' => [ + 'localnum', 'serial', '', '', + 'popnum', 'int', '', '', + 'city', 'varchar', 'NULL', $char_d, + 'state', 'char', 'NULL', 2, + 'npa', 'char', '', 3, + 'nxx', 'char', '', 3, + ], + 'primary_key' => 'localnum', + 'unique' => [], + 'index' => [ [ 'npa', 'nxx' ], [ 'popnum' ] ], + }, + + 'svc_acct' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'username', 'varchar', '', $username_len, #unique (& remove dup code) + '_password', 'varchar', '', 72, #13 for encryped pw's plus ' *SUSPENDED* (md5 passwords can be 34, blowfish 60) + 'sec_phrase', 'varchar', 'NULL', $char_d, + 'popnum', 'int', 'NULL', '', + 'uid', 'int', 'NULL', '', + 'gid', 'int', 'NULL', '', + 'finger', 'varchar', 'NULL', $char_d, + 'dir', 'varchar', 'NULL', $char_d, + 'shell', 'varchar', 'NULL', $char_d, + 'quota', 'varchar', 'NULL', $char_d, + 'slipip', 'varchar', 'NULL', 15, #four TINYINTs, bah. + 'seconds', 'int', 'NULL', '', #uhhhh + 'domsvc', 'int', '', '', + ], + 'primary_key' => 'svcnum', + #'unique' => [ [ 'username', 'domsvc' ] ], + 'unique' => [], + 'index' => [ ['username'], ['domsvc'] ], + }, + + #'svc_charge' => { + # 'columns' => [ + # 'svcnum', 'int', '', '', + # 'amount', @money_type, + # ], + # 'primary_key' => 'svcnum', + # 'unique' => [ [] ], + # 'index' => [ [] ], + #}, + + 'svc_domain' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'domain', 'varchar', '', $char_d, + 'catchall', 'int', 'NULL', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [ ['domain'] ], + 'index' => [], + }, + + 'domain_record' => { + 'columns' => [ + 'recnum', 'serial', '', '', + 'svcnum', 'int', '', '', + #'reczone', 'varchar', '', $char_d, + 'reczone', 'varchar', '', 255, + 'recaf', 'char', '', 2, + 'rectype', 'varchar', '', 5, + #'recdata', 'varchar', '', $char_d, + 'recdata', 'varchar', '', 255, + ], + 'primary_key' => 'recnum', + 'unique' => [], + 'index' => [ ['svcnum'] ], + }, + + 'svc_forward' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'srcsvc', 'int', 'NULL', '', + 'src', 'varchar', 'NULL', 255, + 'dstsvc', 'int', 'NULL', '', + 'dst', 'varchar', 'NULL', 255, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [ ['srcsvc'], ['dstsvc'] ], + }, + + 'svc_www' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'recnum', 'int', '', '', + 'usersvc', 'int', '', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + #'svc_wo' => { + # 'columns' => [ + # 'svcnum', 'int', '', '', + # 'svcnum', 'int', '', '', + # 'svcnum', 'int', '', '', + # 'worker', 'varchar', '', $char_d, + # '_date', @date_type, + # ], + # 'primary_key' => 'svcnum', + # 'unique' => [ [] ], + # 'index' => [ [] ], + #}, + + 'prepay_credit' => { + 'columns' => [ + 'prepaynum', 'serial', '', '', + 'identifier', 'varchar', '', $char_d, + 'amount', @money_type, + 'seconds', 'int', 'NULL', '', + ], + 'primary_key' => 'prepaynum', + 'unique' => [ ['identifier'] ], + 'index' => [], + }, + + 'port' => { + 'columns' => [ + 'portnum', 'serial', '', '', + 'ip', 'varchar', 'NULL', 15, + 'nasport', 'int', 'NULL', '', + 'nasnum', 'int', '', '', + ], + 'primary_key' => 'portnum', + 'unique' => [], + 'index' => [], + }, + + 'nas' => { + 'columns' => [ + 'nasnum', 'serial', '', '', + 'nas', 'varchar', '', $char_d, + 'nasip', 'varchar', '', 15, + 'nasfqdn', 'varchar', '', $char_d, + 'last', 'int', '', '', + ], + 'primary_key' => 'nasnum', + 'unique' => [ [ 'nas' ], [ 'nasip' ] ], + 'index' => [ [ 'last' ] ], + }, + + 'session' => { + 'columns' => [ + 'sessionnum', 'serial', '', '', + 'portnum', 'int', '', '', + 'svcnum', 'int', '', '', + 'login', @date_type, + 'logout', @date_type, + ], + 'primary_key' => 'sessionnum', + 'unique' => [], + 'index' => [ [ 'portnum' ] ], + }, + + 'queue' => { + 'columns' => [ + 'jobnum', 'serial', '', '', + 'job', 'text', '', '', + '_date', 'int', '', '', + 'status', 'varchar', '', $char_d, + 'statustext', 'text', 'NULL', '', + 'svcnum', 'int', 'NULL', '', + ], + 'primary_key' => 'jobnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ], [ 'status' ] ], + }, + + 'queue_arg' => { + 'columns' => [ + 'argnum', 'serial', '', '', + 'jobnum', 'int', '', '', + 'arg', 'text', 'NULL', '', + ], + 'primary_key' => 'argnum', + 'unique' => [], + 'index' => [ [ 'jobnum' ] ], + }, + + 'queue_depend' => { + 'columns' => [ + 'dependnum', 'serial', '', '', + 'jobnum', 'int', '', '', + 'depend_jobnum', 'int', '', '', + ], + 'primary_key' => 'dependnum', + 'unique' => [], + 'index' => [ [ 'jobnum' ], [ 'depend_jobnum' ] ], + }, + + 'export_svc' => { + 'columns' => [ + 'exportsvcnum' => 'serial', '', '', + 'exportnum' => 'int', '', '', + 'svcpart' => 'int', '', '', + ], + 'primary_key' => 'exportsvcnum', + 'unique' => [ [ 'exportnum', 'svcpart' ] ], + 'index' => [ [ 'exportnum' ], [ 'svcpart' ] ], + }, + + 'part_export' => { + 'columns' => [ + 'exportnum', 'serial', '', '', + #'svcpart', 'int', '', '', + 'machine', 'varchar', '', $char_d, + 'exporttype', 'varchar', '', $char_d, + 'nodomain', 'char', 'NULL', 1, + ], + 'primary_key' => 'exportnum', + 'unique' => [], + 'index' => [ [ 'machine' ], [ 'exporttype' ] ], + }, + + 'part_export_option' => { + 'columns' => [ + 'optionnum', 'serial', '', '', + 'exportnum', 'int', '', '', + 'optionname', 'varchar', '', $char_d, + 'optionvalue', 'text', 'NULL', '', + ], + 'primary_key' => 'optionnum', + 'unique' => [], + 'index' => [ [ 'exportnum' ], [ 'optionname' ] ], + }, + + 'radius_usergroup' => { + 'columns' => [ + 'usergroupnum', 'serial', '', '', + 'svcnum', 'int', '', '', + 'groupname', 'varchar', '', $char_d, + ], + 'primary_key' => 'usergroupnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ], [ 'groupname' ] ], + }, + + 'msgcat' => { + 'columns' => [ + 'msgnum', 'serial', '', '', + 'msgcode', 'varchar', '', $char_d, + 'locale', 'varchar', '', 16, + 'msg', 'text', '', '', + ], + 'primary_key' => 'msgnum', + 'unique' => [ [ 'msgcode', 'locale' ] ], + 'index' => [], + }, + + 'cust_tax_exempt' => { + 'columns' => [ + 'exemptnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'taxnum', 'int', '', '', + 'year', 'int', '', '', + 'month', 'int', '', '', + 'amount', @money_type, + ], + 'primary_key' => 'exemptnum', + 'unique' => [ [ 'custnum', 'taxnum', 'year', 'month' ] ], + 'index' => [], + }, + + 'router' => { + 'columns' => [ + 'routernum', 'serial', '', '', + 'routername', 'varchar', '', $char_d, + 'svcnum', 'int', 'NULL', '', + ], + 'primary_key' => 'routernum', + 'unique' => [], + 'index' => [], + }, + + 'part_svc_router' => { + 'columns' => [ + 'svcpart', 'int', '', '', + 'routernum', 'int', '', '', + ], + 'primary_key' => '', + 'unique' => [], + 'index' => [], + }, + + 'addr_block' => { + 'columns' => [ + 'blocknum', 'serial', '', '', + 'routernum', 'int', '', '', + 'ip_gateway', 'varchar', '', 15, + 'ip_netmask', 'int', '', '', + ], + 'primary_key' => 'blocknum', + 'unique' => [ [ 'blocknum', 'routernum' ] ], + 'index' => [], + }, + + 'svc_broadband' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'blocknum', 'int', '', '', + 'speed_up', 'int', '', '', + 'speed_down', 'int', '', '', + 'ip_addr', 'varchar', '', 15, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + 'part_virtual_field' => { + 'columns' => [ + 'vfieldpart', 'int', '', '', + 'dbtable', 'varchar', '', 32, + 'name', 'varchar', '', 32, + 'check_block', 'text', 'NULL', '', + 'length', 'int', 'NULL', '', + 'list_source', 'text', 'NULL', '', + 'label', 'varchar', 'NULL', 80, + ], + 'primary_key' => 'vfieldpart', + 'unique' => [], + 'index' => [], + }, + + 'virtual_field' => { + 'columns' => [ + 'recnum', 'int', '', '', + 'vfieldpart', 'int', '', '', + 'value', 'varchar', '', 128, + ], + 'primary_key' => '', + 'unique' => [ [ 'vfieldpart', 'recnum' ] ], + 'index' => [], + }, + + 'acct_snarf' => { + 'columns' => [ + 'snarfnum', 'int', '', '', + 'svcnum', 'int', '', '', + 'machine', 'varchar', '', 255, + 'protocol', 'varchar', '', $char_d, + 'username', 'varchar', '', $char_d, + '_password', 'varchar', '', $char_d, + ], + 'primary_key' => 'snarfnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ] ], + }, + + 'svc_external' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'id', 'int', 'NULL', '', + 'title', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + 'cust_pay_refund' => { + 'columns' => [ + 'payrefundnum', 'serial', '', '', + 'paynum', 'int', '', '', + 'refundnum', 'int', '', '', + '_date', @date_type, + 'amount', @money_type, + ], + 'primary_key' => 'payrefundnum', + 'unique' => [], + 'index' => [ ['paynum'], ['refundnum'] ], + }, + + 'part_pkg_option' => { + 'columns' => [ + 'optionnum', 'serial', '', '', + 'pkgpart', 'int', '', '', + 'optionname', 'varchar', '', $char_d, + 'optionvalue', 'text', 'NULL', '', + ], + 'primary_key' => 'optionnum', + 'unique' => [], + 'index' => [ [ 'pkgpart' ], [ 'optionname' ] ], + }, + + 'rate' => { + 'columns' => [ + 'ratenum', 'serial', '', '', + 'ratename', 'varchar', '', $char_d, + ], + 'primary_key' => 'ratenum', + 'unique' => [], + 'index' => [], + }, + + 'rate_detail' => { + 'columns' => [ + 'ratenum', 'int', '', '', + 'orig_regionnum', 'int', 'NULL', '', + 'dest_regionnum', 'int', '', '', + 'min_included', 'int', '', '', + 'min_charge', @money_type, + 'sec_granularity', 'int', '', '', + #time period (link to table of periods)? + ], + 'primary_key' => '', + 'unique' => [ [ 'ratenum', 'orig_regionnum', 'dest_regionnum' ] ], + 'index' => [], + }, + + 'rate_region' => { + 'columns' => [ + 'regionnum', 'serial', '', '', + 'regionname', 'varchar', '', $char_d, + ], + 'primary_key' => 'regionnum', + 'unique' => [], + 'index' => [], + }, + + 'rate_prefix' => { + 'columns' => [ + 'prefixnum', 'serial', '', '', + 'regionnum', 'int', '', '',, + 'countrycode', 'varchar', '', 3, + 'npa', 'varchar', 'NULL', 6, + 'nxx', 'varchar', 'NULL', 3, + ], + 'primary_key' => 'prefixnum', + 'unique' => [], + 'index' => [ [ 'countrycode' ], [ 'regionnum' ] ], + }, + + + ); + + %tables; + +} diff --git a/FS/bin/freeside-sqlradius-radacctd b/FS/bin/freeside-sqlradius-radacctd index e98eaa015..4e8d57c51 100644 --- a/FS/bin/freeside-sqlradius-radacctd +++ b/FS/bin/freeside-sqlradius-radacctd @@ -1,122 +1,159 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl -Tw use strict; -use vars qw( @part_export ); -use subs qw(myshutdown); -use POSIX qw(:sys_wait_h); -#use IO::File; -use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm); -use FS::UID qw(adminsuidsetup); #forksuidsetup driver_name dbh myconnect); -use FS::Record qw(qsearch); # qsearchs); -use FS::part_export; +use vars qw( $log_file $sigterm $sigint ); +use subs qw( _die _logmsg ); +use Fcntl qw(:flock); +use POSIX qw(setsid); +use Date::Format; +use IO::File; +use FS::UID qw(adminsuidsetup); +#use FS::Record qw(qsearch qsearchs); +#use FS::part_export; #use FS::svc_acct; #use FS::cust_svc; +#lots of false laziness w/freeside-queued + my $user = shift or die &usage; -#daemonize1('freeside-sqlradius-radacctd', $user); #keep unique pid files w/multi installs -daemonize1('freeside-sqlradius-radacctd'); +#my $pid_file = "/var/run/freeside-sqlradius-radacctd.$user.pid"; +my $pid_file = "/var/run/freeside-sqlradius-radacctd.pid"; -drop_root(); +&daemonize1; -#$ENV{HOME} = (getpwuid($>))[7]; #for ssh +#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +#$SIG{CHLD} = \&REAPER; -adminsuidsetup $user; +$sigterm = 0; +$sigint = 0; +$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; }; +$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; }; -logfile( "/usr/local/etc/freeside/sqlradius-radacctd-log.". $FS::UID::datasrc ); +my $freeside_gid = scalar(getgrnam('freeside')) + or die "can't setgid to freeside group\n"; +$) = $freeside_gid; +$( = $freeside_gid; +#if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd +($(,$)) = ($),$(); +$) = $freeside_gid; -daemonize2(); +$> = $FS::UID::freeside_uid; +$< = $FS::UID::freeside_uid; +#freebsd is sofa king broken, won't setuid() +($<,$>) = ($>,$<); +$> = $FS::UID::freeside_uid; -#-- +#$ENV{HOME} = (getpwuid($>))[7]; #for ssh +adminsuidsetup $user; -#don't just look for ->can('usage_sessions'), we're sqlradius-specific -# (radiator is supposed to be setup with a radacct table) +$log_file= "/usr/local/etc/freeside/sqlradius-radacctd-log.". $FS::UID::datasrc; -@part_export = - qsearch('part_export', { 'exporttype' => 'sqlradius' } ); -push @part_export, - qsearch('part_export', { 'exporttype' => 'sqlradius_withdomain' } ); -push @part_export, - qsearch('part_export', { 'exporttype' => 'radiator' } ); +&daemonize2; -@part_export = grep { ! $_->option('ignore_accounting') } @part_export; +$SIG{__DIE__} = \&_die; +$SIG{__WARN__} = \&_logmsg; -die "no sqlradius, sqlradius_withdomain or radiator exports without". - " ignore_accounting" - unless @part_export; +warn "freeside-sqlradius-radacctd starting\n"; + +#eslaf + +#my $machine = shift or die &usage; #would need to be up higher for real +my @exports = qsearch('part_export', { 'exporttype' => 'sqlradius' } ); while (1) { - #fork off one kid per export (machine) - # _>{'_radacct_kid'} is an evil kludge - foreach my $part_export ( grep ! $_->{'_radacct_kid'}, @part_export ) { - - defined( my $pid = fork ) or do { - warn "WARNING: can't fork to spawn child for ". $part_export->machine; + my %seen = (); + foreach my $export ( @exports ) { + next if $seen{$export->option('datasrc')}++; + my $dbh = DBI->connect( + map { $export->option($_) } qw( datasrc username password ) + ) or do { + warn "can't connect to ". $export->option('datasrc'). ": ". $DBI::errstr; next; - }; - - if ( $pid ) { - $part_export->{'_radacct_kid'} = $pid; - warn "child $pid spawned for ". $part_export->machine; - } else { #kid time + } - adminsuidsetup($user); #get our own db handle + # find old radacct position + #$lastid = 0; - until ( sigint || sigterm ) { - $part_export->update_svc_acct(); - sleep 1; - } + # get new radacct records + my $sth = $dbh->prepare('SELECT * FROM radacct WHERE radacctid > ?') or do { + warn "can't select in radacct table from ". $export->option('datasrc'). + ": ". $dbh->errstr; + next; + }; - warn "child for ". $part_export->machine. " done"; - exit; + while ( my $radacct = $sth->fetchrow_arrayref({}) ) { - } #eo kid + my $session = new FS::session { + portnum => + svcnum => + login => + #logout => + }; - } + } - #reap up any kids that died... - &reap_kids; + # look for updated radacct records & replace them - myshutdown() if sigterm() || sigint(); + } sleep 5; + } -#-- +#more false laziness w/freeside-queued -sub myshutdown { - &reap_kids; +sub usage { + die "Usage:\n\n freeside-sqlradius-radacctd user\n"; +} - #kill all the kids - kill 'TERM', $_ foreach grep $_, map $_->{'_radacct_kid'}, @part_export; +sub _die { + my $msg = shift; + unlink $pid_file if -e $pid_file; + _logmsg($msg); +} - my $wait = 12; #wait up to 1 minute - while ( ( grep $_->{'_radacct_kid'}, @part_export ) && $wait-- ) { - warn "waiting for children to terminate"; - sleep 5; - &reap_kids; - } - warn "abandoning children" if grep $_->{'_radacct_kid'}, @part_export; - die "exiting"; +sub _logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n"; + flock($log, LOCK_UN); + close $log; } -sub reap_kids { - #warn "reaping kids\n"; - foreach my $part_export ( grep $_->{'_radacct_kid'}, @part_export ) { - my $pid = $part_export->{'_radacct_kid'}; - my $kid = waitpid($pid, WNOHANG); - if ( $kid > 0 ) { - $part_export->{'_radacct_kid'} = ''; - } +sub daemonize1 { + + chdir "/" or die "Can't chdir to /: $!"; + open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; + defined(my $pid = fork) or die "Can't fork: $!"; + if ( $pid ) { + print "freeside-sqlradius-radacctd started with pid $pid\n"; + #logging to $log_file\n"; + exit unless $pid_file; + my $pidfh = new IO::File ">$pid_file" or exit; + print $pidfh "$pid\n"; + exit; } - #warn "done reaping\n"; + #open STDOUT, '>/dev/null' + # or die "Can't write to /dev/null: $!"; + #setsid or die "Can't start a new session: $!"; + #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + } -sub usage { - die "Usage:\n\n freeside-sqlradius-radacctd user\n"; +sub daemonize2 { + open STDOUT, '>/dev/null' + or die "Can't write to /dev/null: $!"; + setsid or die "Can't start a new session: $!"; + open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; } + +#eslaf + =head1 NAME freeside-sqlradius-radacctd - Real-time radacct import daemon @@ -127,24 +164,17 @@ freeside-sqlradius-radacctd - Real-time radacct import daemon =head1 DESCRIPTION -Imports records from an the SQL radacct tables of all sqlradius, -sqlradius_withdomain and radiator exports (except those with the -ignore_accounting flag) and updates the svc_acct.seconds for each account. -Runs as a daemon and updates the database in real-time. - -B<username> is a username added by freeside-adduser. - -=head1 RADIUS DATABASE CHANGES +Imports records from an SQL radacct table in real-time into the session +monitor. -ALTER TABLE radacct ADD COLUMN FreesideStatus varchar(32) NULL; +This enables per-minute or per-hour charges as well as the +"View active NAS ports" function. -If you want to ignore the existing accountg records, also do: - -UPDATE radacct SET FreesideStatus = 'done' WHERE FreesideStatus IS NULL; +B<username> is a username added by freeside-adduser. =head1 SEE ALSO -=cut +session.html from the base documentation. -1; +=cut diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset index 2ac5012d4..11cbe9e36 100755 --- a/FS/bin/freeside-sqlradius-reset +++ b/FS/bin/freeside-sqlradius-reset @@ -48,9 +48,6 @@ foreach my $export ( @exports ) { foreach my $svc_acct ( @svc_acct ) { - $svc_acct->check; #set any fixed usergroup so it'll export even if all - #svc_acct records don't have the group yet - #false laziness with FS::svc_acct::insert (like it matters) my $error = $export->export_insert($svc_acct); die $error if $error; diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade deleted file mode 100755 index 419384c2a..000000000 --- a/FS/bin/freeside-upgrade +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/perl -w - -use strict; -use vars qw($DEBUG $DRY_RUN); -use Term::ReadKey; -use DBIx::DBSchema 0.27; -use FS::UID qw(adminsuidsetup checkeuid datasrc ); #getsecrets); -use FS::Schema qw( dbdef dbdef_dist reload_dbdef ); - - -$DEBUG = 1; -$DRY_RUN = 0; - - -die "Not running uid freeside!" unless checkeuid(); - -my $user = shift or die &usage; -my $dbh = adminsuidsetup($user); - -#needs to match FS::Schema... -my $dbdef_file = "/usr/local/etc/freeside/dbdef.". datasrc; - -dbdef_create($dbh, $dbdef_file); - -delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload -reload_dbdef($dbdef_file); - - -foreach my $table ( dbdef_dist->tables ) { - - if ( dbdef->table($table) ) { - - warn "$table exists\n" if $DEBUG > 1; - - foreach my $column ( dbdef_dist->table($table)->columns ) { - if ( dbdef->table($table)->column($column) ) { - warn " $table.$column exists\n" if $DEBUG > 2; - } else { - - if ( $DEBUG ) { - print STDERR "column $table.$column does not exist. create?"; - next unless yesno(); - } - - foreach my $statement ( - dbdef_dist->table($table)->column($column)->sql_add_column( $dbh ) - ) { - warn "$statement\n" if $DEBUG || $DRY_RUN; - unless ( $DRY_RUN ) { - $dbh->do( $statement) - or die "CREATE error: ". $dbh->errstr. "\nexecuting: $statement"; - } - } - - } - - } - - #should eventually check & create missing indices - - #should eventually drop columns not in dbdef_dist... - - } else { - - if ( $DEBUG ) { - print STDERR "table $table does not exist. create?"; - next unless yesno(); - } - - foreach my $statement ( - dbdef_dist->table($table)->sql_create_table( $dbh ) - ) { - warn "$statement\n" if $DEBUG || $DRY_RUN; - unless ( $DRY_RUN ) { - $dbh->do( $statement) - or die "CREATE error: ". $dbh->errstr. "\nexecuting: $statement"; - } - } - - } - -} - -# should eventually drop tables not in dbdef_dist too i guess... - -$dbh->commit or die $dbh->errstr; - -dbdef_create($dbh, $dbdef_file); - -$dbh->disconnect or die $dbh->errstr; - -### - -my $all = 0; -sub yesno { - print STDERR ' [yes/no/all] '; - if ( $all ) { - warn "yes\n"; - return 1; - } else { - while ( 1 ) { - ReadMode 4; - my $x = lc(ReadKey); - ReadMode 0; - if ( $x eq 'n' ) { - warn "no\n"; - return 0; - } elsif ( $x eq 'y' ) { - warn "yes\n"; - return 1; - } elsif ( $x eq 'a' ) { - warn "yes\n"; - $all = 1; - return 1; - } - } - } -} - -sub dbdef_create { # reverse engineer the schema from the DB and save to file - my( $dbh, $file ) = @_; - my $dbdef = new_native DBIx::DBSchema $dbh; - $dbdef->save($file); -} - -sub usage { - die "Usage:\n freeside-upgrade user\n"; -} - -1; - |