diff options
Diffstat (limited to 'FS/bin')
25 files changed, 2153 insertions, 0 deletions
diff --git a/FS/bin/freeside-addgroup b/FS/bin/freeside-addgroup new file mode 100755 index 000000000..7b30f7d95 --- /dev/null +++ b/FS/bin/freeside-addgroup @@ -0,0 +1,50 @@ +#!/usr/bin/perl + +use strict; +use vars qw($opt_s); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::CurrentUser; +use FS::AccessRight; +use FS::access_group; +use FS::access_right; +use FS::access_groupagent; + +getopts("s"); +my $user = shift or die &usage; #just for adminsuidsetup +my $group = shift or die &usage; + +$FS::CurrentUser::upgrade_hack = 1; +#adminsuidsetup $rootuser; +adminsuidsetup $user; + +my $access_group = new FS::access_group { 'groupname' => $group }; +my $error = $access_group->insert; +die $error if $error; + +if ( $opt_s ) { + foreach my $rightname ( FS::AccessRight->rights ) { + my $access_right = new FS::access_right { + 'righttype' => 'FS::access_group', + 'rightobjnum' => $access_group->groupnum, + 'rightname' => $rightname, + }; + my $ar_error = $access_right->insert; + die $ar_error if $ar_error; + } + + foreach my $agent ( qsearch('agent', {} ) ) { + my $access_groupagent = new FS::access_groupagent { + 'groupnum' => $access_group->groupnum, + 'agentnum' => $agent->agentnum, + }; + my $aga_error = $access_groupagent->insert; + die $aga_error if $aga_error; + } +} + +sub usage { + die "Usage:\n\n freeside-addgroup [ -s ] username groupname" +} + diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource new file mode 100644 index 000000000..9cb12195a --- /dev/null +++ b/FS/bin/freeside-addoutsource @@ -0,0 +1,32 @@ +#!/bin/sh + +domain=$1 + +FREESIDE_CONF=%%%FREESIDE_CONF%%% +FREESIDE_CACHE=%%%FREESIDE_CACHE%%% +FREESIDE_EXPORT=%%%FREESIDE_EXPORT%%% + +#without this, [a-z]* matches CVS/, the copy doesn't return a sucessful error +# status, and the rest of the commands aren't run +export LANG=C + +createdb $domain && \ +\ +mkdir $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \ +\ +chown freeside $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \ +\ +cp /home/ivan/freeside/conf/[a-z]* $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \ +\ +touch $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \ +\ +chown freeside $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \ +\ +chmod 600 $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \ +\ +echo -e "DBI:Pg:dbname=$domain\nfreeside\n" >$FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \ +\ +mkdir $FREESIDE_CACHE/counters.DBI:Pg:dbname=$domain && \ +mkdir $FREESIDE_CACHE/cache.DBI:Pg:dbname=$domain && \ +mkdir $FREESIDE_EXPORT/export.DBI:Pg:dbname=$domain + diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser new file mode 100644 index 000000000..cbe792acc --- /dev/null +++ b/FS/bin/freeside-addoutsourceuser @@ -0,0 +1,18 @@ +#!/bin/sh + +username=$1 +domain=$2 +password=$3 +realdomain=$4 +FREESIDE_CONF=%%%FREESIDE_CONF%%% + +freeside-adduser -s conf.DBI:Pg:dbname=$domain/secrets \ + -n \ + $username #2>/dev/null + +[ -e $FREESIDE_CONF/dbdef.DBI:Pg:dbname=$domain ] \ + || ( freeside-setup -d $realdomain -u $username ) + +freeside-adduser -g 1 $username + +htpasswd -b $FREESIDE_CONF/htpasswd $username $password diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser new file mode 100644 index 000000000..237e29ef8 --- /dev/null +++ b/FS/bin/freeside-adduser @@ -0,0 +1,119 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_s $opt_g $opt_n); +use Fcntl qw(:flock); +use Getopt::Std; + +my $FREESIDE_CONF = "%%%FREESIDE_CONF%%%"; + +getopts("s:g:n"); +my $user = shift or die &usage; + +if ( $opt_s ) { + + #if ( -e "$FREESIDE_CONF/mapsecrets" ) { + # open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets") + # or die "can't open $FREESIDE_CONF/mapsecrets: $!"; + # while (<MAPSECRETS>) { + # /^(\S+) / or die "unparsable line in mapsecrets: $_"; + # die "user $user already exists\n" if $user eq $1; + # } + # close MAPSECRETS; + #} + + #insert new entry before a wildcard... + open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets") + and flock(MAPSECRETS,LOCK_EX) + or die "can't open $FREESIDE_CONF/mapsecrets: $!"; + open(NEW,">$FREESIDE_CONF/mapsecrets.new") + or die "can't open $FREESIDE_CONF/mapsecrets.new: $!"; + while(<MAPSECRETS>) { + if ( /^\*\s/ ) { + print NEW "$user $opt_s\n"; + } + print NEW $_; + } + close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!"; + close NEW or die "can't close $FREESIDE_CONF/mapsecrets.new: $!"; + rename("$FREESIDE_CONF/mapsecrets.new", "$FREESIDE_CONF/mapsecrets") + or die "can't move mapsecrets.new into place: $!"; + +} + +### + +exit if $opt_n; + +### + +use FS::UID qw(adminsuidsetup); +use FS::CurrentUser; +use FS::access_user; +use FS::access_usergroup; + +$FS::CurrentUser::upgrade_hack = 1; +#adminsuidsetup $rootuser; +adminsuidsetup $user; + +my $access_user = new FS::access_user { + 'username' => $user, + '_password' => 'notyet', + 'first' => 'Firstname', # $opt_f || + 'last' => 'Lastname', # $opt_l || +}; +my $au_error = $access_user->insert; +die $au_error if $au_error; + +if ( $opt_g ) { + + my $access_usergroup = new FS::access_usergroup { + 'usernum' => $access_user->usernum, + 'groupnum' => $opt_g, + }; + my $aug_error = $access_usergroup->insert; + die $aug_error if $aug_error; + +} + +### + +sub usage { + die "Usage:\n\n freeside-adduser [ -n ] [ -s ] [ -g groupnum ] username [ password ]" +} + +=head1 NAME + +freeside-adduser - Command line interface to add (freeside) users. + +=head1 SYNOPSIS + + freeside-adduser [ -n ] [ -s ] [ -g groupnum ] username [ password ] + +=head1 DESCRIPTION + +Adds a user to the Freeside billing system. This is for adding users (internal +sales/tech folks) to the web interface, not for adding customer accounts. + +This functionality is now available in the web interface as well, under +B<Configuration | Employees | View/Edit employees>. + + -g: initial groupnum + + Development/multi-DB options: + + -s: alternate secrets file + + -n: no ACL added, for bootstrapping + +=head1 NOTE + +No explicit htpasswd options are available in 1.7 - passwordsa are now +maintained automatically. + +=head1 SEE ALSO + +Base Freeside documentation + +=cut + diff --git a/FS/bin/freeside-apply-credits b/FS/bin/freeside-apply-credits new file mode 100755 index 000000000..ea6a7bdd0 --- /dev/null +++ b/FS/bin/freeside-apply-credits @@ -0,0 +1,21 @@ +#!/usr/bin/perl -Tw + +use strict; +use vars qw( $user $cust_main @customers ); +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::cust_main; + +$user = shift or die &usage; +&adminsuidsetup( $user ); + +my @customers = qsearch('cust_main', {} ); +die "No customers" unless (scalar(@customers) > 0); + +foreach $cust_main (@customers) { + print "Applying credits for customer #". $cust_main->custnum; + $cust_main->apply_credits; +} + + + diff --git a/FS/bin/freeside-count-active-customers b/FS/bin/freeside-count-active-customers new file mode 100755 index 000000000..759085a73 --- /dev/null +++ b/FS/bin/freeside-count-active-customers @@ -0,0 +1,17 @@ +#!/bin/sh + +domain=$1 + +echo "\t +select count(*) from cust_main where + 0 < ( SELECT COUNT(*) FROM cust_pkg + WHERE cust_pkg.custnum = cust_main.custnum + AND ( cust_pkg.cancel IS NULL + OR cust_pkg.cancel = 0 + ) + ) + OR 0 = ( SELECT COUNT(*) FROM cust_pkg + WHERE cust_pkg.custnum = cust_main.custnum + ); +" | psql -U freeside -q $domain | head -1 + diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily new file mode 100755 index 000000000..a06a2b185 --- /dev/null +++ b/FS/bin/freeside-daily @@ -0,0 +1,92 @@ +#!/usr/bin/perl -w + +use strict; +use Getopt::Std; +use FS::UID qw(adminsuidsetup); + +&untaint_argv; #what it sounds like (eww) +#use vars qw($opt_d $opt_v $opt_p $opt_a $opt_s $opt_y); +use vars qw(%opt); +getopts("p:a:d:vsy:", \%opt); + +my $user = shift or die &usage; +adminsuidsetup $user; + +use FS::Cron::bill qw(bill); +bill(%opt); + +use FS::Cron::notify qw(notify_flat_delay); +notify_flat_delay(%opt); + +use FS::Cron::vacuum qw(vacuum); +vacuum(); + +use FS::Cron::backup qw(backup_scp); +backup_scp(); + +### +# subroutines +### + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + # Date::Parse + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-daily [ -d 'date' ] user [ custnum custnum ... ]\n"; +} + +### +# documentation +### + +=head1 NAME + +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 ... ] + +=head1 DESCRIPTION + +Bills customers and runs invoice collection events. Should be run from +crontab daily. + +Bills customers. Searches for customers who are due for billing and calls +the bill and collect methods of a cust_main object. See L<FS::cust_main>. + + -d: Pretend it's 'date'. Date is in any format Date::Parse is happy with, + but be careful. + + -y: In addition to -d, which specifies an absolute date, the -y switch + specifies an offset, in days. For example, "-y 15" would increment the + "pretend date" 15 days from whatever was specified by the -d switch + (or now, if no -d switch was given). + + -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 + +user: From the mapsecrets file - see config.html from the base documentation + +custnum: if one or more customer numbers are specified, only bills those +customers. Otherwise, bills all customers. + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::cust_main>, config.html from the base documentation + +=cut + diff --git a/FS/bin/freeside-deloutsource b/FS/bin/freeside-deloutsource new file mode 100644 index 000000000..afc3a0118 --- /dev/null +++ b/FS/bin/freeside-deloutsource @@ -0,0 +1,14 @@ +#!/bin/sh + +domain=$1 +FREESIDE_CONF=%%%FREESIDE_CONF%%% +FREESIDE_CACHE=%%%FREESIDE_CACHE%%% +FREESIDE_EXPORT=%%%FREESIDE_EXPORT%%% + +dropdb $domain && \ +rm -rf $FREESIDE_CONF/conf.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf $FREESIDE_CACHE/counters.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf $FREESIDE_CACHE/cache.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf $FREESIDE_EXPORT/export.DBI:Pg:host=localhost\;dbname=$domain && \ +rm $FREESIDE_CONF/dbdef.DBI:Pg:host=localhost\;dbname=$domain + diff --git a/FS/bin/freeside-deloutsourceuser b/FS/bin/freeside-deloutsourceuser new file mode 100644 index 000000000..dc4ff9cdc --- /dev/null +++ b/FS/bin/freeside-deloutsourceuser @@ -0,0 +1,6 @@ +#!/bin/sh + +username=$1 + +freeside-deluser -h %%%FREESIDE_CONF%%%/htpasswd $username 2>/dev/null + diff --git a/FS/bin/freeside-deluser b/FS/bin/freeside-deluser new file mode 100644 index 000000000..a2a361a83 --- /dev/null +++ b/FS/bin/freeside-deluser @@ -0,0 +1,64 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_h); +use Fcntl qw(:flock); +use Getopt::Std; + +my $FREESIDE_CONF = "%%%FREESIDE_CONF%%%"; + +getopts("h:"); +my $user = shift or die &usage; + +if ( $opt_h ) { + open(HTPASSWD,"<$opt_h") + and flock(HTPASSWD,LOCK_EX) + or die "can't open $opt_h: $!"; + open(HTPASSWD_TMP,">$opt_h.tmp") or die "can't open $opt_h.tmp: $!"; + while (<HTPASSWD>) { + print HTPASSWD_TMP $_ unless /^$user:/; + } + close HTPASSWD_TMP; + rename "$opt_h.tmp", "$opt_h" or die $!; + flock(HTPASSWD,LOCK_UN); + close HTPASSWD; +} + +open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets") + and flock(MAPSECRETS,LOCK_EX) + or die "can't open $FREESIDE_CONF/mapsecrets: $!"; +open(MAPSECRETS_TMP,">>$FREESIDE_CONF/mapsecrets.tmp") + or die "can't open $FREESIDE_CONF/mapsecrets.tmp: $!"; +while (<MAPSECRETS>) { + print MAPSECRETS_TMP $_ unless /^$user\s/; +} +close MAPSECRETS_TMP; +rename "$FREESIDE_CONF/mapsecrets.tmp", "$FREESIDE_CONF/mapsecrets" or die $!; +flock(MAPSECRETS,LOCK_UN); +close MAPSECRETS; + +sub usage { + die "Usage:\n\n freeside-deluser [ -h htpasswd_file ] username" +} + +=head1 NAME + +freeside-deluser - Command line interface to add (freeside) users. + +=head1 SYNOPSIS + + freeside-deluser [ -h htpasswd_file ] username + +=head1 DESCRIPTION + +Adds a user to the Freeside billing system. This is for adding users (internal +sales/tech folks) to the web interface, not for adding customer accounts. + + -h: Also delete from the given htpasswd filename + +=head1 SEE ALSO + +L<freeside-adduser>, L<htpasswd>(1), base Freeside documentation + +=cut + diff --git a/FS/bin/freeside-email b/FS/bin/freeside-email new file mode 100755 index 000000000..7a93f78ee --- /dev/null +++ b/FS/bin/freeside-email @@ -0,0 +1,55 @@ +#!/usr/bin/perl -Tw + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch); +use FS::svc_acct; + +&untaint_argv; #what it sounds like (eww) +my $user = shift or die &usage; + +adminsuidsetup $user; + +my $conf = new FS::Conf; + +my @svc_acct = qsearch('svc_acct', {}); +my @emails = map $_->email, @svc_acct; + +print join("\n", @emails), "\n"; + +# subroutines + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + # Date::Parse + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-email user\n"; +} + +=head1 NAME + +freeside-email - Prints email addresses of all users on STDOUT + +=head1 SYNOPSIS + + freeside-email user + +=head1 DESCRIPTION + +Prints the email addresses of all customers on STDOUT, separated by newlines. + +user: From the mapsecrets file - see config.html from the base documentation + +=head1 BUGS + +=head1 SEE ALSO + +=cut + diff --git a/FS/bin/freeside-expiration-alerter b/FS/bin/freeside-expiration-alerter new file mode 100755 index 000000000..e49bd62aa --- /dev/null +++ b/FS/bin/freeside-expiration-alerter @@ -0,0 +1,222 @@ +#!/usr/bin/perl -Tw + +use strict; +use Date::Format; +use Time::Local; +use Text::Template; +use Getopt::Std; +use Net::SMTP; +use Mail::Header; +use Mail::Internet; +use FS::Conf; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::cust_main; + +use vars qw($smtpmachine @body); + +#hush, perl! +$FS::alerter::_template::first = ""; +$FS::alerter::_template::last = ""; +$FS::alerter::_template::company = ""; +$FS::alerter::_template::payby = ""; +$FS::alerter::_template::expdate = ""; + +# Set the mail program and other variables +my $mail_sender = "billing\@mydomain.tld"; # or invoice_from if available +my $failure_recipient = "postmaster"; # or invoice_from if available +my $warning_time = 30 * 24 * 60 * 60; +my $urgent_time = 15 * 24 * 60 * 60; +my $panic_time = 5 * 24 * 60 * 60; +my $window_time = 24 * 60 * 60; + +&untaint_argv; #what it sounds like (eww) + +#we're at now now (and later). +my($_date)= $^T; + +# Get the current month +my ($sec,$min,$hour,$mday,$mon,$year) = + (localtime($_date) )[0,1,2,3,4,5]; +$mon++; + +# Login to the database +my $user = shift or die &usage; +adminsuidsetup $user; + +# Get the needed configuration files +my $conf = new FS::Conf; +$smtpmachine = $conf->config('smtpmachine'); +$mail_sender = $conf->config('invoice_from') + if $conf->exists('invoice_from'); +$failure_recipient = $conf->config('invoice_from') + if $conf->exists('invoice_from'); + + +my(@customers)=qsearch('cust_main',{}); +if (scalar(@customers) == 0) +{ + exit 1; +} + +# Prepare for sending email + +$ENV{MAILADDRESS} = $mail_sender; +my $header = new Mail::Header ( [ + "From: Account Processor", + "To: $failure_recipient", + "Sender: $mail_sender", + "Reply-To: $mail_sender", + "Subject: Unnotified Billing Arrangement Expirations", +] ); + +my @alerter_template = $conf->config('alerter_template') + or die "cannot load config file alerter_template"; + +my $alerter = new Text::Template (TYPE => 'ARRAY', SOURCE => [ map "$_\n", @alerter_template ]) + or die "can't create new Text::Template object: Text::Template::ERROR"; +$alerter->compile() or die "can't compile template: Text::Template::ERROR"; + +# Now I can start looping +foreach my $customer (@customers) +{ + my $paydate = $customer->getfield('paydate'); + next if $paydate =~ /^\s*$/; #skip empty expiration dates + + my $custnum = $customer->getfield('custnum'); + my $first = $customer->getfield('first'); + my $last = $customer->getfield('last'); + my $company = $customer->getfield('company'); + my $payby = $customer->getfield('payby'); + my $payinfo = $customer->getfield('payinfo'); + my $daytime = $customer->getfield('daytime'); + my $night = $customer->getfield('night'); + + my ($payyear,$paymonth,$payday) = split (/-/,$paydate); + + my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); + + #credit cards expire at the end of the month/year of their exp date + if ($payby eq 'CARD' || $payby eq 'DCRD') { + ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++); + $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); + $expire_time--; + } + + if (($expire_time < $_date + $warning_time && + $expire_time > $_date + $warning_time - $window_time) || + ($expire_time < $_date + $urgent_time && + $expire_time > $_date + $urgent_time - $window_time) || + ($expire_time < $_date + $panic_time && + $expire_time > $_date + $panic_time - $window_time)) { + + + + my @packages = $customer->ncancelled_pkgs; + if (scalar(@packages) != 0) { + my @invoicing_list = $customer->invoicing_list; + if ( grep { $_ ne 'POST' } @invoicing_list ) { + my $header = new Mail::Header ( [ + "From: $mail_sender", + "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ), + "Sender: $mail_sender", + "Reply-To: $mail_sender", + "Date: ". time2str("%a, %d %b %Y %X %z", time), + "Subject: Billing Arrangement Expiration", + ] ); + $FS::alerter::_template::first = $first; + $FS::alerter::_template::last = $last; + $FS::alerter::_template::company = $company; + if ($payby eq 'CARD' || $payby eq 'DCRD') { + $FS::alerter::_template::payby = "credit card (" . + substr($payinfo, 0, 2) . "xxxxxxxxxx" . + substr($payinfo, -4) . ")"; + }elsif ($payby eq 'COMP') { + $FS::alerter::_template::payby = "complimentary account"; + }else{ + $FS::alerter::_template::payby = "current method"; + } + $FS::alerter::_template::expdate = $expire_time; + + my $message = new Mail::Internet ( + 'Header' => $header, + 'Body' => [ $alerter->fill_in( PACKAGE => 'FS::alerter::_template' ) ], + ); + $!=0; + $message->smtpsend( Host => $smtpmachine ) + or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) + or die "Can't send expiration email: $!"; + + } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { + push @body, sprintf(qq{%5d %-32.32s %4s %10s %12s %12s}, + $custnum, + $first . " " . $last . " " . $company, + $payby, + $paydate, + $daytime, + $night); + } + } + } +} + +# Now I need to send EMAIL +if (scalar(@body)) { + my $message = new Mail::Internet ( + 'Header' => $header, + 'Body' => [ (@body) ], + ); + $!=0; + $message->smtpsend( Host => $smtpmachine ) + or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) + or die "can't send alerter failure email to $failure_recipient". + " via server $smtpmachine with SMTP: $!"; +} + +# subroutines +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + $ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal argument \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-expiration-alerter user\n"; +} + +=head1 NAME + +freeside-expiration-alerter - Emails notifications of credit card expirations. + +=head1 SYNOPSIS + + freeside-expiration-alerter user + +=head1 DESCRIPTION + +Emails customers notice that their credit card or other billing arrangement +is about to expire. Usually run as a cron job. + +user: From the mapsecrets file - see config.html from the base documentation + +=head1 BUGS + +Yes..... Use at your own risk. No guarantees or warrantees of any +kind apply to this program. Parts of this program are hacked from +other GNU licensed software created mainly by Ivan Kohler. + +This is released under the GNU Public License. See www.gnu.org +for more information regarding this license. + +=head1 SEE ALSO + +L<FS::cust_main>, config.html from the base documentation + +=head1 AUTHOR + +Jeff Finucane <jeff@cmh.net> + +=cut + + diff --git a/FS/bin/freeside-monthly b/FS/bin/freeside-monthly new file mode 100755 index 000000000..a6c75e715 --- /dev/null +++ b/FS/bin/freeside-monthly @@ -0,0 +1,91 @@ +#!/usr/bin/perl -w + +use strict; +use Getopt::Std; +use FS::UID qw(adminsuidsetup); + +&untaint_argv; #what it sounds like (eww) +#use vars qw($opt_d $opt_v $opt_p $opt_a $opt_s $opt_y); +use vars qw(%opt); +getopts("p:a:d:vsy:", \%opt); + +my $user = shift or die &usage; +adminsuidsetup $user; + +use FS::Cron::bill qw(bill); +bill(%opt, 'freq'=>'1m' ); + +### +# subroutines +### + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + # Date::Parse + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-monthly [ -d 'date' ] user [ custnum custnum ... ]\n"; +} + +### +# documentation +### + +=head1 NAME + +freeside-monthly - Run monthly billing and invoice collection events. + +=head1 SYNOPSIS + + freeside-monthly [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] user [ custnum custnum ... ] + +=head1 DESCRIPTION + +Bills customers and runs invoice collection events, for the alternate monthly +event chain. If you have defined monthly event checks, should be run from +crontab monthly. + +Bills customers. Searches for customers who are due for billing and calls +the bill and collect methods of a cust_main object. See L<FS::cust_main>. + + -d: Pretend it's 'date'. Date is in any format Date::Parse is happy with, + but be careful. + + -y: In addition to -d, which specifies an absolute date, the -y switch + specifies an offset, in days. For example, "-y 15" would increment the + "pretend date" 15 days from whatever was specified by the -d switch + (or now, if no -d switch was given). + + -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 + +user: From the mapsecrets file - see config.html from the base documentation + +custnum: if one or more customer numbers are specified, only bills those +customers. Otherwise, bills all customers. + +=head1 NOTE + +In most cases, you would use freeside-daily only and not freeside-monthly. +freeside-monthly would only be used in cases where you have events that can +only be run once each month, for example, batching invoices to a third-party +print/mail provider. + +=head1 BUGS + +=head1 SEE ALSO + +L<freeside-daily>, L<FS::cust_main>, config.html from the base documentation + +=cut + diff --git a/FS/bin/freeside-prepaidd b/FS/bin/freeside-prepaidd new file mode 100644 index 000000000..73f7523c4 --- /dev/null +++ b/FS/bin/freeside-prepaidd @@ -0,0 +1,104 @@ +#!/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 $work_cust_pkg = $cust_pkg; + + my $cust_main = $cust_pkg->cust_main; + if ( $cust_main->total_unapplied_payments > 0 + or $cust_main->total_credited > 0 + ) + { + #this needs a flag to say only do the prepaid packages... + # and only try em if the renewal price matches.. but this will do for now + my $b_error = $cust_main->bill; + if ( $b_error ) { + warn "Error billing customer #". $cust_main->custnum; + next; + } + #$b_error = $cust_main->apply_payments_and_credits; + $b_error = $cust_main->apply_payments; + $b_error = $cust_main->apply_credits; + + $work_cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $work_cust_pkg->pkgnum } ); + + next + if $cust_main->balance <= 0 + and $work_cust_pkg->bill >= time; + } + + my $action = $work_cust_pkg->part_pkg->option('recur_action') || 'suspend'; + + my $error = $work_cust_pkg->$action(); + + warn "Error ${action}ing package ". $work_cust_pkg->pkgnum. + " for custnum ". $work_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 suspends or cancels 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 new file mode 100644 index 000000000..93d735d1a --- /dev/null +++ b/FS/bin/freeside-queued @@ -0,0 +1,250 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw( $DEBUG $kids $max_kids %kids ); +use POSIX qw(:sys_wait_h); +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; + +# no autoloading for non-FS classes... +use Net::SSH 0.07; + +$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'); + +warn "dropping privledges\n" if $DEBUG; +drop_root(); + + +$ENV{HOME} = (getpwuid($>))[7]; #for ssh + +warn "connecting to database\n" if $DEBUG; +$@ = 'not connected'; +while ( $@ ) { + eval { adminsuidsetup $user; }; + if ( $@ ) { + warn $@; + warn "sleeping for reconnect...\n"; + sleep 5; + } +} + +logfile( "%%%FREESIDE_LOG%%%/queuelog.". $FS::UID::datasrc ); + +warn "completing daemonization (detaching))\n" if $DEBUG; +daemonize2(); + +#-- + +my $warnkids=0; +while (1) { + + &reap_kids; + #prevent runaway forking + if ( $kids >= $max_kids ) { + warn "WARNING: maximum $kids children reached\n" unless $warnkids++; + &reap_kids; + sleep 1; #waiting for signals is cheap + next; + } + $warnkids=0; + + unless ( dbh && dbh->ping ) { + warn "WARNING: connection to database lost, reconnecting...\n"; + + eval { $FS::UID::dbh = myconnect; }; + + unless ( !$@ && dbh && dbh->ping ) { + warn "WARNING: still no connection to database, sleeping for retry...\n"; + sleep 10; + next; + } else { + warn "WARNING: reconnected to database\n"; + } + } + + #my($job, $ljob); + #{ + # my $oldAutoCommit = $FS::UID::AutoCommit; + # local $FS::UID::AutoCommit = 0; + $FS::UID::AutoCommit = 0; + + #assuming mysql 4.1 w/subqueries now + #my $nodepend = driver_name eq 'mysql' + # ? '' + # : 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'. + # ' WHERE queue_depend.jobnum = queue.jobnum ) '; + my $nodepend = 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'. + ' WHERE queue_depend.jobnum = queue.jobnum ) '; + + my $job = qsearchs( + 'queue', + { 'status' => 'new' }, + '', + driver_name eq 'mysql' + ? "$nodepend ORDER BY jobnum LIMIT 1 FOR UPDATE" + : "$nodepend ORDER BY jobnum FOR UPDATE LIMIT 1" + ) or do { + # if $oldAutoCommit { + dbh->commit or do { + warn "WARNING: database error, closing connection: ". dbh->errstr; + undef $FS::UID::dbh; + next; + }; + # } + sleep 5; #connecting to db is expensive + next; + }; + + #assuming mysql 4.1 w/subqueries now + #if ( driver_name eq 'mysql' + # && qsearch('queue_depend', { 'jobnum' => $job->jobnum } ) ) { + # dbh->commit or die dbh->errstr; #if $oldAutoCommit; + # sleep 5; #would be better if mysql could do everything in query above + # next; + #} + + my %hash = $job->hash; + $hash{'status'} = 'locked'; + my $ljob = new FS::queue ( \%hash ); + my $error = $ljob->replace($job); + if ( $error ) { + warn "WARNING: database error locking job, closing connection: ". + dbh->errstr; + undef $FS::UID::dbh; + next; + } + + # if $oldAutoCommit { + dbh->commit or do { + warn "WARNING: database error, closing connection: ". dbh->errstr; + undef $FS::UID::dbh; + next; + }; + # } + + $FS::UID::AutoCommit = 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"; + my %hash = $job->hash; + $hash{'status'} = 'failed'; + $hash{'statustext'} = "[freeside-queued] can't fork: $!"; + my $ljob = new FS::queue ( \%hash ); + my $error = $ljob->replace($job); + die $error if $error; + next; #don't increment the kid counter + }; + + if ( $pid ) { + $kids++; + $kids{$pid} = 1; + } else { #kid time + + #get new db handle + $FS::UID::dbh->{InactiveDestroy} = 1; + + forksuidsetup($user); + + #auto-use classes... + #if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) { + if ( $ljob->job =~ /(FS::part_export::\w+)::/ + || $ljob->job =~ /(FS::\w+)::/ + ) + { + my $class = $1; + eval "use $class;"; + if ( $@ ) { + warn "job use $class failed"; + my %hash = $ljob->hash; + $hash{'status'} = 'failed'; + $hash{'statustext'} = $@; + my $fjob = new FS::queue( \%hash ); + my $error = $fjob->replace($ljob); + die $error if $error; + exit; #end-of-kid + }; + } + + my $eval = "&". $ljob->job. '(@args);'; + warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG; + eval $eval; #throw away return value? suppose so + if ( $@ ) { + warn "job $eval failed"; + my %hash = $ljob->hash; + $hash{'status'} = 'failed'; + $hash{'statustext'} = $@; + my $fjob = new FS::queue( \%hash ); + my $error = $fjob->replace($ljob); + die $error if $error; + } else { + $ljob->delete; + } + + exit; + #end-of-kid + } + +} continue { + if ( sigterm() ) { + warn "received TERM signal; exiting\n"; + exit; + } + if ( sigint() ) { + warn "received INT signal; exiting\n"; + exit; + } +} + +sub usage { + die "Usage:\n\n freeside-queued user\n"; +} + +sub reap_kids { + foreach my $pid ( keys %kids ) { + my $kid = waitpid($pid, WNOHANG); + if ( $kid > 0 ) { + $kids--; + delete $kids{$kid}; + } + } +} + +=head1 NAME + +freeside-queued - Job queue daemon + +=head1 SYNOPSIS + + freeside-queued user + +=head1 DESCRIPTION + +Job queue daemon. Should be running at all times. + +user: from the mapsecrets file - see config.html from the base documentation + +=head1 VERSION + +=head1 BUGS + +=head1 SEE ALSO + +=cut + diff --git a/FS/bin/freeside-radgroup b/FS/bin/freeside-radgroup new file mode 100644 index 000000000..ed85626d2 --- /dev/null +++ b/FS/bin/freeside-radgroup @@ -0,0 +1,76 @@ +#!/usr/bin/perl -w + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::cust_svc; +use FS::svc_acct; + +&untaint_argv; #what it sounds like (eww) + +my($user, $action, $groupname, $svcpart) = @ARGV; + +adminsuidsetup $user; + +my @svc_acct = map { $_->svc_x } qsearch('cust_svc', { svcpart => $svcpart } ); + +if ( lc($action) eq 'add' ) { + foreach my $svc_acct ( @svc_acct ) { + my @groups = $svc_acct->radius_groups; + next if grep { $_ eq $groupname } @groups; + push @groups, $groupname; + my %hash = $svc_acct->hash; + $hash{usergroup} = \@groups; + my $new = new FS::svc_acct \%hash; + my $error = $new->replace($svc_acct); + die $error if $error; + } +} else { + die &usage; +} + +# subroutines + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-radgroup user action groupname svcpart\n"; +} + +=head1 NAME + +freeside-radgroup - Command line utility to manipulate radius groups + +=head1 SYNOPSIS + + freeside-addgroup user action groupname svcpart + +=head1 DESCRIPTION + + B<user> is a freeside user as added with freeside-adduser. + + B<command> is the action to take. Available actions are: I<add> + + B<groupname> is the group to add (or remove, etc.) + + B<svcpart> specifies which accounts will be updated. + +=head1 EXAMPLES + +freeside-radgroup freesideuser add groupname 3 + +Adds I<groupname> to all accounts with service definition 3. + +=head1 BUGS + +=head1 SEE ALSO + +L<freeside-adduser>, L<FS::svc_acct>, L<FS::part_svc> + +=cut + diff --git a/FS/bin/freeside-reexport b/FS/bin/freeside-reexport new file mode 100644 index 000000000..54af9dd80 --- /dev/null +++ b/FS/bin/freeside-reexport @@ -0,0 +1,71 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_s $opt_u $opt_p); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::part_export; +use FS::svc_acct; +use FS::cust_svc; + +my $user = shift or die &usage; +adminsuidsetup $user; + +my $export_x = shift or die &usage; +my @part_export; +if ( $export_x =~ /^(\d+)$/ ) { + @part_export = qsearchs('part_export', { exportnum=>$1 } ) + or die "exportnum $export_x not found\n"; +} else { + @part_export = qsearch('part_export', { exporttype=>$export_x } ) + or die "no exports of type $export_x found\n"; +} + +getopts('s:u:p:'); + +my @svc_x = (); +if ( $opt_s ) { + my $cust_svc = qsearchs('cust_svc', { svcnum=>$opt_s } ) + or die "svcnum $opt_s not found\n"; + push @svc_x, $cust_svc->svc_x; +} elsif ( $opt_u ) { + my $svc_x = qsearchs('svc_acct', { username=>$opt_u } ) + or die "username $opt_u not found\n"; + push @svc_x, $svc_x; +} elsif ( $opt_p ) { + push @svc_x, map { $_->svc_x } qsearch('cust_svc', { svcpart=>$opt_p } ); + die "no services with svcpart $opt_p found\n" unless @svc_x; +} + +foreach my $part_export ( @part_export ) { + foreach my $svc_x ( @svc_x ) { + my $error = $part_export->export_insert($svc_x); + die $error if $error; + } +} + + +sub usage { + die "Usage:\n\n freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]\n"; +} + +=head1 NAME + +freeside-reexport - Command line tool to re-trigger export jobs for existing services + +=head1 SYNOPSIS + + freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] + +=head1 DESCRIPTION + + Re-queues the export job for the specified exportnum or exporttype(s) and + specified service (selected by svcnum or username). + +=head1 SEE ALSO + +L<freeside-sqlradius-reset>, L<FS::part_export> + +=cut + diff --git a/FS/bin/freeside-reset-fixed b/FS/bin/freeside-reset-fixed new file mode 100755 index 000000000..5829d441b --- /dev/null +++ b/FS/bin/freeside-reset-fixed @@ -0,0 +1,69 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_p $opt_s $opt_r); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::cust_svc; +use FS::svc_Common; + +getopts('p:s:r'); + +my $user = shift or die &usage; +adminsuidsetup $user; + +die &usage + if ($opt_p && $opt_s); + +$FS::Record::nowarn_identical = 1; +$FS::svc_Common::noexport_hack = 1 + unless $opt_r; + +my @svc_x = (); +if ( $opt_s ) { + $opt_s =~ /^(\d+)$/ or die "invalid svcnum"; + my $cust_svc = qsearchs('cust_svc', { svcnum => $1 } ) + or die "svcnum $opt_s not found\n"; + push @svc_x, $cust_svc->svc_x; +} elsif ( $opt_p ) { + $opt_p =~ /^(\d+)$/ or die "invalid svcpart"; + push @svc_x, map { $_->svc_x } qsearch('cust_svc', { svcpart => $1 } ); + die "no services with svcpart $opt_p found\n" unless @svc_x; +} else { + push @svc_x, map { $_->svc_x } qsearch('cust_svc', {} ); + die "no services found\n" unless @svc_x; +} + +foreach my $svc_x ( @svc_x ) { + my $result = $svc_x->setfixed; + die $result unless ref($result); + my $error = $svc_x->replace + if $svc_x->modified; + die $error if $error; +} + + +sub usage { + die "Usage:\n\n freeside-reset-fixed user [ -s svcnum | -p svcpart ] [ -r ]\n"; +} + +=head1 NAME + +freeside-reset-fixed - Command line tool to set the fixed columns for existing services + +=head1 SYNOPSIS + + freeside-reset-fixed user [ -s svcnum | -p svcpart ] [ -r ] + +=head1 DESCRIPTION + + Resets the fixed columns for the specified service part or service number. + Re-exports the service if -r is specified. + +=head1 SEE ALSO + +L<freeside-reexport>, L<FS::part_svc> + +=cut + diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server new file mode 100644 index 000000000..187bc1469 --- /dev/null +++ b/FS/bin/freeside-selfservice-server @@ -0,0 +1,229 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw( $FREESIDE_LOG $FREESIDE_LOCK ); +use vars qw( $Debug %kids $kids $max_kids $ssh_pid %old_ssh_pid $keepalives ); +use subs qw( lock_write unlock_write myshutdown usage ); +use Fcntl qw(:flock); +use POSIX qw(:sys_wait_h); +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; + +use FS::Conf; +use FS::cust_bill; +use FS::cust_pkg; + +$FREESIDE_LOG = "%%%FREESIDE_LOG%%%"; +$FREESIDE_LOCK = "%%%FREESIDE_LOCK%%%"; + +$Debug = 1; # 2 will turn on more logging + # 3 will log packet contents, including passwords + +$max_kids = '10'; #? +$keepalives = 0; #let clientd turn it on, so we don't barf on old ones +$kids = 0; + +my $user = shift or die &usage; +my $machine = shift or die &usage; +my $tag = scalar(@ARGV) ? shift : ''; + +my $lock_file = "$FREESIDE_LOCK/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 + +adminsuidsetup $user; + +#logfile("/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc); #MACHINE +logfile("$FREESIDE_LOG/selfservice.$machine.log"); + +daemonize2(); + +my $conf = new FS::Conf; + +my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name? + +my $warnkids=0; +while (1) { + my($writer,$reader,$error) = (new IO::Handle, new IO::Handle, new IO::Handle); + warn "connecting to $machine\n" if $Debug; + + $ssh_pid = sshopen2($machine,$reader,$writer,$clientd,$tag); + +# nstore_fd(\*writer, {'hi'=>'there'}); + + warn "entering main loop\n" if $Debug; + my $undisp = 0; + my $keepalive_count = 0; + my $s = IO::Select->new( $reader ); + while (1) { + + &reap_kids; + + warn "waiting for packet from client\n" if $Debug && !$undisp; + $undisp = 1; + my @handles = $s->can_read(5); + unless ( @handles ) { + myshutdown() if sigint() || sigterm(); + if ( $keepalives && $keepalive_count++ > 10 ) { + $keepalive_count = 0; + lock_write; + nstore_fd( { _token => '_keepalive' }, $writer ); + unlock_write; + } + next; + } + + $undisp = 0; + + warn "receiving packet from client\n" if $Debug; + + my $packet = eval { fd_retrieve($reader); }; + if ( $@ ) { + warn "Storable error receiving packet from client". + " (assuming lost connection): $@\n" + if $Debug; + if ( $ssh_pid ) { + warn "sending TERM signal to ssh process $ssh_pid\n" if $Debug; + kill 'TERM', $ssh_pid; + $old_ssh_pid{$ssh_pid} = 1; + $ssh_pid = 0; + } + last; + } + warn "packet received\n". + join('', map { " $_=>$packet->{$_}\n" } keys %$packet ) + if $Debug > 2; + + if ( $packet->{_packet} eq '_enable_keepalive' ) { + warn "enabling keep alives\n" if $Debug; + $keepalives=1; + next; + } + + #prevent runaway forking + my $warnkids = 0; + while ( $kids >= $max_kids ) { + warn "WARNING: maximum $kids children reached\n" unless $warnkids++; + &reap_kids; + sleep 1; + } + + warn "forking child\n" if $Debug; + defined( my $pid = fork ) or die "can't fork: $!"; + if ( $pid ) { + $kids++; + $kids{$pid} = 1; + warn "child $pid spawned\n" if $Debug; + } else { #kid time + + ##get new db handle + $FS::UID::dbh->{InactiveDestroy} = 1; + forksuidsetup($user); + + #get db handle + #adminsuidsetup($user); + + my $type = $packet->{_packet}; + warn "calling $type handler\n" if $Debug; + my $rv = eval { FS::ClientAPI->dispatch($type, $packet); }; + if ( $@ ) { + warn my $error = "WARNING: error dispatching $type: $@"; + $rv = { _error => $error }; + } + $rv->{_token} = $packet->{_token}; #identifier + + open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!"; + lock_write; + warn "sending response\n" if $Debug; + nstore_fd($rv, $writer) or die "FATAL: can't send response: $!"; + $writer->flush or die "FATAL: can't flush: $!"; + unlock_write; + + warn "child exiting\n" if $Debug; + exit; #end-of-kid + } + + } + + myshutdown if sigint() || sigterm(); + warn "connection lost, reconnecting\n" if $Debug; + sleep 3; + +} + +### +# utility subroutines +### + +sub reap_kids { + #warn "reaping kids\n"; + foreach my $pid ( keys %kids ) { + my $kid = waitpid($pid, WNOHANG); + if ( $kid > 0 ) { + $kids--; + delete $kids{$kid}; + } + } + + foreach my $pid ( keys %old_ssh_pid ) { + waitpid($pid, WNOHANG) and delete $old_ssh_pid{$pid}; + } + #warn "done reaping\n"; +} + +sub myshutdown { + &reap_kids; + my $wait = 12; #wait up to 1 minute + while ( $kids > 0 && $wait-- ) { + warn "waiting for $kids children to terminate"; + sleep 5; + &reap_kids; + } + warn "abandoning $kids children" if $kids; + kill 'TERM', $ssh_pid if $ssh_pid; + die "exiting"; +} + +sub lock_write { + warn "locking $lock_file mutex for write to write stream\n" if $Debug > 1; + + #broken on freebsd? + #flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!"; + + flock(LOCKFILE, LOCK_EX) or die "FATAL: can't lock $lock_file: $!"; + +} + +sub unlock_write { + warn "unlocking $lock_file mutex\n" if $Debug > 1; + + #broken on freebsd? + #flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!"; + + flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!"; + +} + +sub usage { + die "Usage:\n\n freeside-selfservice-server user machine\n"; +} + diff --git a/FS/bin/freeside-setinvoice b/FS/bin/freeside-setinvoice new file mode 100644 index 000000000..708e2fa30 --- /dev/null +++ b/FS/bin/freeside-setinvoice @@ -0,0 +1,42 @@ +#!/usr/bin/perl + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch qsearchs); +use FS::cust_main; +use FS::svc_acct; + +&untaint_argv; #what it sounds like (eww) +my $user = shift or die &usage; + +adminsuidsetup $user; + +foreach my $cust_main ( + grep { ! scalar($_->invoicing_list) } + qsearch( 'cust_main', {} ) +) { + my @dest; + my @cust_pkg = $cust_main->ncancelled_pkgs; + foreach my $cust_pkg ( @cust_pkg ) { + foreach my $cust_svc ( $cust_pkg->cust_svc ) { + my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $cust_svc->svcnum } ); + push @dest, $svc_acct->svcnum if $svc_acct; + } + } + push @dest, 'POST' unless @dest; + $cust_main->invoicing_list(\@dest); +} + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-setinvoice user\n"; +} + + diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup new file mode 100755 index 000000000..ddc210f50 --- /dev/null +++ b/FS/bin/freeside-setup @@ -0,0 +1,130 @@ +#!/usr/bin/perl -Tw + +#to delay loading dbdef until we're ready +BEGIN { $FS::Schema::setup_hack = 1; } + +use strict; +use vars qw($opt_u $opt_d $opt_v); +use Getopt::Std; +use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets); +use FS::CurrentUser; +use FS::Schema qw( dbdef_dist reload_dbdef ); +use FS::Record; +#use FS::raddb; +use FS::Setup qw(create_initial_data); + +die "Not running uid freeside!" unless checkeuid(); + +#my %attrib2db = +# map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib; + +getopts("u:vd:"); +#my $user = shift or die &usage; + +getsecrets($opt_u); #$user); + +#needs to match FS::Record +my($dbdef_file) = "%%%FREESIDE_CONF%%%/dbdef.". datasrc; + +### + +#print "\nEnter the maximum username length: "; +#my($username_len)=&getvalue; +my $username_len = 32; #usernamemax config file + +#print "\n\n", <<END, ":"; +#Freeside tracks the RADIUS User-Name, check attribute Password and +#reply attribute Framed-IP-Address for each user. You can specify additional +#check and reply attributes (or you can add them later with the +#fs-radius-add-check and fs-radius-add-reply programs). +# +#First enter any additional RADIUS check attributes you need to track for each +#user, separated by whitespace. +#END +#my @check_attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; } +# split(" ",&getvalue); +# +#print "\n\n", <<END, ":"; +#Now enter any additional reply attributes you need to track for each user, +#separated by whitespace. +#END +#my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; } +# split(" ",&getvalue); +# +#print "\n\n", <<END, ":"; +#Do you wish to enable the tracking of a second, separate shipping/service +#address? +#END +#my $ship = &_yesno; +# +#sub getvalue { +# my($x)=scalar(<STDIN>); +# chop $x; +# $x; +#} +# +#sub _yesno { +# print " [y/N]:"; +# my $x = scalar(<STDIN>); +# $x =~ /^y/i; +#} + +#my @check_attributes = (); #add later +#my @attributes = (); #add later +#my $ship = $opt_s; + +### +# create a dbdef object from the old data structure +### + +my $dbdef = dbdef_dist; + +#important +$dbdef->save($dbdef_file); +&FS::Schema::reload_dbdef($dbdef_file); + +### +# create 'em +### + +$FS::CurrentUser::upgrade_hack = 1; +my $dbh = adminsuidsetup $opt_u; #$user; + +#create tables +$|=1; + +foreach my $statement ( $dbdef->sql($dbh) ) { + warn $statement if $statement =~ /TABLE cdr/; + $dbh->do( $statement ) + 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); + +create_initial_data('domain' => $opt_d); + +warn "Freeside database initialized - commiting transaction\n" if $opt_v; + +$dbh->commit or die $dbh->errstr; +$dbh->disconnect or die $dbh->errstr; + +warn "Database initialization committed successfully\n" if $opt_v; + +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 -d domain.name [ -v ]\n" + # [ -u user ] for devel/multi-db installs +} + +1; + + diff --git a/FS/bin/freeside-sqlradius-radacctd b/FS/bin/freeside-sqlradius-radacctd new file mode 100644 index 000000000..83fd4bfd1 --- /dev/null +++ b/FS/bin/freeside-sqlradius-radacctd @@ -0,0 +1,150 @@ +#!/usr/bin/perl -w + +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 FS::svc_acct; +#use FS::cust_svc; + +my $user = shift or die &usage; + +#daemonize1('freeside-sqlradius-radacctd', $user); #keep unique pid files w/multi installs +daemonize1('freeside-sqlradius-radacctd'); + +drop_root(); + +#$ENV{HOME} = (getpwuid($>))[7]; #for ssh + +adminsuidsetup $user; + +logfile( "%%%FREESIDE_LOG%%%/sqlradius-radacctd-log.". $FS::UID::datasrc ); + +daemonize2(); + +#-- + +#don't just look for ->can('usage_sessions'), we're sqlradius-specific +# (radiator is supposed to be setup with a radacct table) + +@part_export = + qsearch('part_export', { 'exporttype' => 'sqlradius' } ); +push @part_export, + qsearch('part_export', { 'exporttype' => 'sqlradius_withdomain' } ); +push @part_export, + qsearch('part_export', { 'exporttype' => 'radiator' } ); + +@part_export = grep { ! $_->option('ignore_accounting') } @part_export; + +die "no sqlradius, sqlradius_withdomain or radiator exports without". + " ignore_accounting" + unless @part_export; + +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; + 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 + + until ( sigint || sigterm ) { + $part_export->update_svc_acct(); + sleep 1; + } + + warn "child for ". $part_export->machine. " done"; + exit; + + } #eo kid + + } + + #reap up any kids that died... + &reap_kids; + + myshutdown() if sigterm() || sigint(); + + sleep 5; +} + +#-- + +sub myshutdown { + &reap_kids; + + #kill all the kids + kill 'TERM', $_ foreach grep $_, map $_->{'_radacct_kid'}, @part_export; + + 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 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'} = ''; + } + } + #warn "done reaping\n"; +} + +sub usage { + die "Usage:\n\n freeside-sqlradius-radacctd user\n"; +} + +=head1 NAME + +freeside-sqlradius-radacctd - Real-time radacct import daemon + +=head1 SYNOPSIS + + freeside-sqlradius-radacctd username + +=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 + +ALTER TABLE radacct ADD COLUMN FreesideStatus varchar(32) NULL; + +If you want to ignore the existing accountg records, also do: + +UPDATE radacct SET FreesideStatus = 'done' WHERE FreesideStatus IS NULL; + +=head1 SEE ALSO + +=cut + +1; + diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset new file mode 100755 index 000000000..2ac5012d4 --- /dev/null +++ b/FS/bin/freeside-sqlradius-reset @@ -0,0 +1,88 @@ +#!/usr/bin/perl -w + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::part_export; +use FS::svc_acct; +use FS::cust_svc; + +my $user = shift or die &usage; +adminsuidsetup $user; + +#my $machine = shift or die &usage; + +my @exports = (); +if ( @ARGV ) { + foreach my $exportnum ( @ARGV ) { + foreach my $exporttype (qw( sqlradius sqlradius_withdomain )) { + push @exports, qsearch('part_export', { exportnum => $exportnum, + exporttype => $exporttype, } ); + } + } + } else { + @exports = qsearch('part_export', { exporttype=>'sqlradius' } ); + push @exports, qsearch('part_export', { exporttype=>'sqlradius_withdomain' } ); +} + +foreach my $export ( @exports ) { + my $icradius_dbh = DBI->connect( + map { $export->option($_) } qw( datasrc username password ) + ) or die $DBI::errstr; + for my $table (qw( radcheck radreply usergroup )) { + my $sth = $icradius_dbh->prepare("DELETE FROM $table"); + $sth->execute or die "Can't reset $table table: ". $sth->errstr; + } + $icradius_dbh->disconnect; +} + +foreach my $export ( @exports ) { + + #my @svcparts = map { $_->svcpart } $export->export_svc; + + my @svc_acct = + map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) } + map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) } + grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) } + $export->export_svc; + + 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; + + } +} + +sub usage { + die "Usage:\n\n freeside-sqlradius-reset user [ exportnum, ... ]\n"; +} + +=head1 NAME + +freeside-sqlradius-reset - Command line interface to reset and recreate RADIUS SQL tables + +=head1 SYNOPSIS + + freeside-sqlradius-reset username [ EXPORTNUM, ... ] + +=head1 DESCRIPTION + +Deletes the radcheck, radreply and usergroup tables and repopulates them from +the Freeside database, for the specified exports, or, if no exports are +specified, for all sqlradius and sqlradius_withdomain exports. + +B<username> is a username added by freeside-adduser. + +=head1 SEE ALSO + +L<freeside-reexport>, L<FS::part_export>, L<FS::part_export::sqlradius> + +=cut + + + diff --git a/FS/bin/freeside-sqlradius-seconds b/FS/bin/freeside-sqlradius-seconds new file mode 100644 index 000000000..1c978fa8a --- /dev/null +++ b/FS/bin/freeside-sqlradius-seconds @@ -0,0 +1,58 @@ +#!/usr/bin/perl -Tw + +use strict; +use Date::Parse; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearchs); +use FS::svc_acct; + +my $fs_user = shift or die &usage; +adminsuidsetup( $fs_user ); + +my $target_user = shift or die &usage; +my $start = shift or die &usage; +$start = str2time($start); +my $stop = scalar(@ARGV) ? str2time(shift) : time; + +my $svc_acct = qsearchs( 'svc_acct', { 'username' => $target_user } ); +die "username $target_user not found\n" unless $svc_acct; + +print $svc_acct->seconds_since_sqlradacct( $start, $stop ). "\n"; + +sub usage { + die "Usage:\n\n freeside-sqlradius-seconds freeside_username target_username start_date stop_date\n"; +} + + +=head1 NAME + +freeside-sqlradius-seconds - Real-time radacct import daemon + +=head1 SYNOPSIS + + freeside-sqlradius-seconds freeside_username target_username start_date [ stop_date ] + +=head1 DESCRIPTION + +Returns the number of seconds the specified username has been online between +start_date (inclusive) and stop_date (exclusive). +See L<FS::svc_acct/seconds_since_sqlradacct> + +B<freeside_username> is a username added by freeside-adduser. +B<target_username> is the username of the user account to query. +B<start_date> and B<stop_date> are in any format Date::Parse is happy with. +B<stop_date> defaults to now if not specified. + +=head1 BUGS + +Selection of the account in question is rather simplistic in that +B<target_username> doesn't necessarily identify a unique account (and wouldn't +even if a domain was specified), and no sqlradius export is checked for. + +=head1 SEE ALSO + +L<FS::svc_acct/seconds_since_sqlradacct> + +=cut + +1; diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade new file mode 100755 index 000000000..3a4e4f8e3 --- /dev/null +++ b/FS/bin/freeside-upgrade @@ -0,0 +1,85 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_d $opt_q $opt_v); +use vars qw($DEBUG $DRY_RUN); +use Getopt::Std; +use DBIx::DBSchema 0.31; +use FS::UID qw(adminsuidsetup checkeuid datasrc ); #getsecrets); +use FS::CurrentUser; +use FS::Schema qw( dbdef dbdef_dist reload_dbdef ); + +die "Not running uid freeside!" unless checkeuid(); + +getopts("dq"); + +$DEBUG = !$opt_q; +#$DEBUG = $opt_v; + +$DRY_RUN = $opt_d; + +my $user = shift or die &usage; +$FS::CurrentUser::upgrade_hack = 1; +my $dbh = adminsuidsetup($user); + +#needs to match FS::Schema... +my $dbdef_file = "%%%FREESIDE_CONF%%%/dbdef.". datasrc; + +dbdef_create($dbh, $dbdef_file); + +delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload +reload_dbdef($dbdef_file); + +$DBIx::DBSchema::DEBUG = $DEBUG; +$DBIx::DBSchema::Table::DEBUG = $DEBUG; + +if ( $DRY_RUN ) { + print join(";\n", dbdef->sql_update_schema( dbdef_dist, $dbh ) ). ";\n"; + exit; +} else { + dbdef->update_schema( dbdef_dist, $dbh ); +} + +$dbh->commit or die $dbh->errstr; + +dbdef_create($dbh, $dbdef_file); + +$dbh->disconnect or die $dbh->errstr; + +### + +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 [ -d ] [ -q | -v ] user\n"; +} + +=head1 NAME + +freeside-upgrade - Upgrades database schema for new freeside verisons. + +=head1 SYNOPSIS + + freeside-adduser [ -d ] [ -q | -v ] + +=head1 DESCRIPTION + +Reads your existing database schema and updates it to match the current schema, +adding any columns or tables necessary. + + [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute + them. + + [ -q ]: Run quietly. This may become the default at some point. + + [ -v ]: Run verbosely, sending debugging information to STDERR. This is the + current default. + +=head1 SEE ALSO + +=cut + |