use FS::svc_acct;
use FS::export_svc;
use Carp qw( cluck );
+use NEXT;
@ISA = qw(FS::part_export);
@EXPORT_OK = qw( sqlradius_connect );
},
'show_called_station' => {
type => 'checkbox',
- label => 'Show the Called-Station-ID on session reports',
+ label => 'Show the Called-Station-ID on session reports', #as a phone number
},
'overlimit_groups' => {
label => 'Radius groups to assign to svc_acct which has exceeded its bandwidth or time limit (if not overridden by overlimit_groups global or per-agent config)',
type => 'checkbox',
label => 'Export RADIUS group attributes to this database',
},
+ 'disconnect_ssh' => {
+ label => 'To send a disconnection request to each RADIUS client when modifying, suspending or deleting an account, enter a ssh connection string (username@host) with access to the radclient program',
+ },
+ 'disconnect_port' => {
+ label => 'Port to send disconnection requests to, default 1700',
+ },
+ 'disconnect_log' => {
+ label => 'Print disconnect output and errors to the queue log (will otherwise fail silently)',
+ type => 'checkbox',
+ },
;
$notes1 = <<'END';
sub radius_reply { #override for other svcdb
my($self, $svc_acct) = (shift, shift);
- $svc_acct->radius_reply;
+ my %every = $svc_acct->EVERY::radius_reply;
+ map { @$_ } values %every;
}
sub radius_check { #override for other svcdb
my($self, $svc_acct) = (shift, shift);
- $svc_acct->radius_check;
+ my %every = $svc_acct->EVERY::radius_check;
+ map { @$_ } values %every;
}
sub _export_insert {
my $dbh = dbh;
my $jobnum = '';
+
+ # disconnect users before changing username
+ if ($self->option('disconnect_ssh')) {
+ my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect',
+ 'disconnect_ssh' => $self->option('disconnect_ssh'),
+ 'svc_acct_username' => $old->username,
+ 'disconnect_port' => $self->option('disconnect_port'),
+ 'disconnect_log' => $self->option('disconnect_log'),
+ );
+ unless ( ref($err_or_queue) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $err_or_queue;
+ }
+ $jobnum = $err_or_queue->jobnum; # chain all of these dependencies
+ }
+
if ( $self->export_username($old) ne $self->export_username($new) ) {
my $usergroup = $self->option('usergroup') || 'usergroup';
my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'rename',
$dbh->rollback if $oldAutoCommit;
return $err_or_queue;
}
+ if ( $jobnum ) {
+ my $error = $err_or_queue->depend_insert( $jobnum );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
$jobnum = $err_or_queue->jobnum;
}
foreach my $table (qw(reply check)) {
my $method = "radius_$table";
- my %new = $new->$method();
- my %old = $old->$method();
+ my %new = $self->$method($new);
+ my %old = $self->$method($old);
if ( grep { !exists $old{$_} #new attributes
|| $new{$_} ne $old{$_} #changed
} keys %new
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $jobnum = '';
+
+ # disconnect users before changing anything
+ if ($self->option('disconnect_ssh')) {
+ my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect',
+ 'disconnect_ssh' => $self->option('disconnect_ssh'),
+ 'svc_acct_username' => $svc_acct->username,
+ 'disconnect_port' => $self->option('disconnect_port'),
+ 'disconnect_log' => $self->option('disconnect_log'),
+ );
+ unless ( ref($err_or_queue) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $err_or_queue;
+ }
+ $jobnum = $err_or_queue->jobnum;
+ }
+
my @newgroups = $self->suspended_usergroups($svc_acct);
unless (@newgroups) { #don't change password if assigning to a suspended group
$dbh->rollback if $oldAutoCommit;
return $err_or_queue;
}
-
+ if ( $jobnum ) {
+ my $error = $err_or_queue->depend_insert( $jobnum );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
}
my $error =
sub _export_delete {
my( $self, $svc_x ) = (shift, shift);
+
+ my $jobnum = '';
+
+ # disconnect users before changing anything
+ if ($self->option('disconnect_ssh')) {
+ my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'user_disconnect',
+ 'disconnect_ssh' => $self->option('disconnect_ssh'),
+ 'svc_acct_username' => $svc_x->username,
+ 'disconnect_port' => $self->option('disconnect_port'),
+ 'disconnect_log' => $self->option('disconnect_log'),
+ );
+ return $err_or_queue unless ref($err_or_queue);
+ $jobnum = $err_or_queue->jobnum;
+ }
+
my $usergroup = $self->option('usergroup') || 'usergroup';
my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'delete',
$self->export_username($svc_x), $usergroup );
+ if ( $jobnum ) {
+ my $error = $err_or_queue->depend_insert( $jobnum );
+ return $error if $error;
+ }
+
ref($err_or_queue) ? '' : $err_or_queue;
}
=item acctoutputoctets
+=item callingstationid
+
=item calledstationid
=back
$opt = shift;
$start = $opt->{stoptime_start};
$end = $opt->{stoptime_end};
- $svc_acct = $opt->{svc_acct};
+ $svc_acct = $opt->{svc} || $opt->{svc_acct};
$ip = $opt->{ip};
$prefix = $opt->{prefix};
$summarize = $opt->{summarize};
my @fields = (
qw( username realm framedipaddress
acctsessiontime acctinputoctets acctoutputoctets
- calledstationid
+ callingstationid calledstationid
),
"$str2time acctstarttime ) as acctstarttime",
"$str2time acctstoptime ) as acctstoptime",
"$RadAcctId ($UserName\@$Realm for ${AcctSessionTime}s"
if $DEBUG;
- $UserName = lc($UserName) unless $conf->exists('username-uppercase');
-
- #my %search = ( 'username' => $UserName );
+ my $fs_username = $UserName;
- my $extra_sql = '';
- if ( ref($self) =~ /withdomain/ ) { #well...
- $extra_sql = " AND '$Realm' = ( SELECT domain FROM svc_domain
- WHERE svc_domain.svcnum = svc_acct.domsvc ) ";
- }
+ $fs_username = lc($fs_username) unless $conf->exists('username-uppercase');
- my $oldAutoCommit = $FS::UID::AutoCommit; # can't undo side effects, but at
- local $FS::UID::AutoCommit = 0; # least we can avoid over counting
+ #my %search = ( 'username' => $fs_username );
- my $status = 'skipped';
+ my $status = '';
my $errinfo = "for RADIUS detail RadAcctID $RadAcctId ".
"(UserName $UserName, Realm $Realm)";
- if ( $self->option('process_single_realm')
- && $self->option('realm') ne $Realm )
- {
- warn "WARNING: wrong realm $errinfo - skipping\n" if $DEBUG;
- } else {
- my @svc_acct =
- grep { qsearch( 'export_svc', { 'exportnum' => $self->exportnum,
- 'svcpart' => $_->cust_svc->svcpart, } )
- }
- qsearch( 'svc_acct',
- { 'username' => $UserName },
- '',
- $extra_sql
- );
-
- if ( !@svc_acct ) {
- warn "WARNING: no svc_acct record found $errinfo - skipping\n";
- } elsif ( scalar(@svc_acct) > 1 ) {
- warn "WARNING: multiple svc_acct records found $errinfo - skipping\n";
+ my $extra_sql = '';
+ if ( ref($self) =~ /withdomain/ ) { #well, should be a callback to that
+ #module or something
+ my $domain;
+ if ( $Realm ) {
+ $domain = $Realm;
+ } elsif ( $fs_username =~ /\@/ ) {
+ ($fs_username, $domain) = split('@', $fs_username);
} else {
+ warn 'WARNING: nothing Realm column and no @realm in UserName column '.
+ "$errinfo -- skipping\n" if $DEBUG;
+ $status = 'skipped (no realm)';
+ }
- my $svc_acct = $svc_acct[0];
- warn "found svc_acct ". $svc_acct->svcnum. " $errinfo\n" if $DEBUG;
+ $extra_sql = " AND '$domain' = ( SELECT domain FROM svc_domain
+ WHERE svc_domain.svcnum = svc_acct.domsvc ) ";
+ }
- $svc_acct->last_login($AcctStartTime);
- $svc_acct->last_logout($AcctStopTime);
+ my $oldAutoCommit = $FS::UID::AutoCommit; # can't undo side effects, but at
+ local $FS::UID::AutoCommit = 0; # least we can avoid over counting
- my $session_time = $AcctStopTime;
- $session_time = $AcctStartTime if $self->option('ignore_long_sessions');
+ unless ( $status ) {
- my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
- if ( $cust_pkg && $session_time < ( $cust_pkg->last_bill
- || $cust_pkg->setup ) ) {
- $status = 'skipped (too old)';
+ $status = 'skipped';
+
+ if ( $self->option('process_single_realm')
+ && $self->option('realm') ne $Realm )
+ {
+ warn "WARNING: wrong realm $errinfo - skipping\n" if $DEBUG;
+ } else {
+ my @svc_acct =
+ grep { qsearch( 'export_svc', { 'exportnum' => $self->exportnum,
+ 'svcpart' => $_->cust_svc->svcpart,
+ }
+ )
+ }
+ qsearch( 'svc_acct',
+ { 'username' => $fs_username },
+ '',
+ $extra_sql
+ );
+
+ if ( !@svc_acct ) {
+ warn "WARNING: no svc_acct record found $errinfo - skipping\n";
+ } elsif ( scalar(@svc_acct) > 1 ) {
+ warn "WARNING: multiple svc_acct records found $errinfo - skipping\n";
} else {
- my @st;
- push @st, _try_decrement($svc_acct, 'seconds', $AcctSessionTime);
- push @st, _try_decrement($svc_acct, 'upbytes', $AcctInputOctets);
- push @st, _try_decrement($svc_acct, 'downbytes', $AcctOutputOctets);
- push @st, _try_decrement($svc_acct, 'totalbytes', $AcctInputOctets
- + $AcctOutputOctets);
- $status=join(' ', @st);
+
+ my $svc_acct = $svc_acct[0];
+ warn "found svc_acct ". $svc_acct->svcnum. " $errinfo\n" if $DEBUG;
+
+ $svc_acct->last_login($AcctStartTime);
+ $svc_acct->last_logout($AcctStopTime);
+
+ my $session_time = $AcctStopTime;
+ $session_time = $AcctStartTime
+ if $self->option('ignore_long_sessions');
+
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ if ( $cust_pkg && $session_time < ( $cust_pkg->last_bill
+ || $cust_pkg->setup ) ) {
+ $status = 'skipped (too old)';
+ } else {
+ my @st;
+ push @st, _try_decrement($svc_acct,'seconds', $AcctSessionTime);
+ push @st, _try_decrement($svc_acct,'upbytes', $AcctInputOctets);
+ push @st, _try_decrement($svc_acct,'downbytes', $AcctOutputOctets);
+ push @st, _try_decrement($svc_acct,'totalbytes', $AcctInputOctets
+ + $AcctOutputOctets);
+ $status=join(' ', @st);
+ }
}
}
+
}
warn "setting FreesideStatus to $status $errinfo\n" if $DEBUG;
or die $dbh->errstr;
}
+=item sqlradius_user_disconnect
+
+For a specified user, sends a disconnect request to all nas in the server database.
+
+Accepts L</sqlradius_connect> connection input and the following named parameters:
+
+I<disconnect_ssh> - user@host with access to radclient program (required)
+
+I<svc_acct_username> - the user to be disconnected (required)
+
+I<disconnect_port> - the port (on the nas) to send disconnect requests to (defaults to 1700)
+
+I<disconnect_log> - if true, print disconnect command & output to the error log
+
+Note this is NOT the opposite of sqlradius_connect.
+
+=cut
+
+sub sqlradius_user_disconnect {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ # get list of nas
+ my $sth = $dbh->prepare('select nasname, secret from nas') or die $dbh->errstr;
+ $sth->execute() or die $dbh->errstr;
+ my $nas = $sth->fetchall_arrayref({});
+ $sth->finish();
+ $dbh->disconnect();
+ die "No nas found in radius db" unless @$nas;
+ # set up ssh connection
+ eval "use Net::SSH";
+ my $ssh = Net::OpenSSH->new($opt{'disconnect_ssh'});
+ die "Couldn't establish SSH connection: " . $ssh->error
+ if $ssh->error;
+ # send individual disconnect requests
+ my $user = $opt{'svc_acct_username'}; #svc_acct username
+ my $port = $opt{'disconnect_port'} || 1700; #or should we pull this from the db?
+ foreach my $nas (@$nas) {
+ my $nasname = $nas->{'nasname'};
+ my $secret = $nas->{'secret'};
+ my $command = qq(echo "User-Name=$user" | radclient -r 1 $nasname:$port disconnect '$secret');
+ my ($output, $errput) = $ssh->capture2($command);
+ warn $command . "\n" . $output . $errput . $ssh->error . "\n"
+ if $opt{'disconnect_log'};
+ }
+ return '';
+}
+
###
# class method to fetch groups/attributes from the sqlradius install on upgrade
###