X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fpart_export%2Fsqlradius.pm;h=15aa9862053beae95d30d48858ff67fcd80ac9f7;hp=03802b2a797b13d351e64997bc34adf8bd194d92;hb=74e058c8a010ef6feb539248a550d0bb169c1e94;hpb=beba6672fb9c9c5769c81f8029bb88cd2bc910e9 diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 03802b2a7..15aa98620 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -1,13 +1,16 @@ package FS::part_export::sqlradius; -use vars qw(@ISA $DEBUG %info %options $notes1 $notes2); +use vars qw(@ISA @EXPORT_OK $DEBUG %info %options $notes1 $notes2); +use Exporter; use Tie::IxHash; -use FS::Record qw( dbh qsearch ); +use FS::Record qw( dbh qsearch qsearchs str2time_sql ); use FS::part_export; use FS::svc_acct; use FS::export_svc; +use Carp qw( cluck ); @ISA = qw(FS::part_export); +@EXPORT_OK = qw( sqlradius_connect ); $DEBUG = 0; @@ -15,10 +18,23 @@ tie %options, 'Tie::IxHash', 'datasrc' => { label=>'DBI data source ' }, 'username' => { label=>'Database username' }, 'password' => { label=>'Database password' }, + 'usergroup' => { label => 'Group table', + type => 'select', + options => [qw( usergroup radusergroup ) ], + }, 'ignore_accounting' => { type => 'checkbox', label => 'Ignore accounting records from this database' }, + 'process_single_realm' => { + type => 'checkbox', + label => 'Only process one realm of accounting records', + }, + 'realm' => { label => 'The realm of of accounting records to be processed' }, + 'ignore_long_sessions' => { + type => 'checkbox', + label => 'Ignore sessions which span billing periods', + }, 'hide_ip' => { type => 'checkbox', label => 'Hide IP address information on session reports', @@ -31,18 +47,24 @@ tie %options, 'Tie::IxHash', type => 'checkbox', label => 'Show the Called-Station-ID on session reports', }, + '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)', } , + 'groups_susp_reason' => { label => + 'Radius group mapping to reason (via template user) (svcnum|username|username@domain reasonnum|reason)', + type => 'textarea', + }, + ; $notes1 = <<'END'; -Real-time export of radcheck, radreply and usergroup tables to any SQL database -for FreeRADIUS, -ICRADIUS -or Radiator. +Real-time export of radcheck, radreply and usergroup/radusergroup +tables to any SQL database for +FreeRADIUS +or ICRADIUS. END $notes2 = <<'END'; An existing RADIUS database will be updated in realtime, but you can use -freeside-sqlradius-reset +freeside-sqlradius-reset to delete the entire RADIUS database and repopulate the tables from the Freeside database. See the DBI documentation @@ -66,7 +88,7 @@ END %info = ( 'svc' => 'svc_acct', - 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS, Radiator)', + 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS)', 'options' => \%options, 'nodomain' => 'Y', 'notes' => $notes1. @@ -75,30 +97,38 @@ END $notes2 ); +sub _groups_susp_reason_map { map { reverse( /^\s*(\S+)\s*(.*)$/ ) } + split( "\n", shift->option('groups_susp_reason')); +} + sub rebless { shift; } sub export_username { my($self, $svc_acct) = (shift, shift); - warn "export_username called on $self with arg $svc_acct" if $DEBUG; + warn "export_username called on $self with arg $svc_acct" if $DEBUG > 1; $svc_acct->username; } sub _export_insert { - my($self, $svc_acct) = (shift, shift); + my($self, $svc_x) = (shift, shift); foreach my $table (qw(reply check)) { my $method = "radius_$table"; - my %attrib = $svc_acct->$method(); + my %attrib = $svc_x->$method(); next unless keys %attrib; - my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'insert', - $table, $self->export_username($svc_acct), %attrib ); + my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'insert', + $table, $self->export_username($svc_x), %attrib ); return $err_or_queue unless ref($err_or_queue); } - my @groups = $svc_acct->radius_groups; + my @groups = $svc_x->radius_groups; if ( @groups ) { + cluck localtime(). ": queuing usergroup_insert for ". $svc_x->svcnum. + " (". $self->export_username($svc_x). " with ". join(", ", @groups) + if $DEBUG; + my $usergroup = $self->option('usergroup') || 'usergroup'; my $err_or_queue = $self->sqlradius_queue( - $svc_acct->svcnum, 'usergroup_insert', - $self->export_username($svc_acct), @groups ); + $svc_x->svcnum, 'usergroup_insert', + $self->export_username($svc_x), $usergroup, @groups ); return $err_or_queue unless ref($err_or_queue); } ''; @@ -120,8 +150,9 @@ sub _export_replace { my $jobnum = ''; 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', - $self->export_username($new), $self->export_username($old) ); + $self->export_username($new), $self->export_username($old), $usergroup ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -170,59 +201,109 @@ sub _export_replace { } } - # (sorta) false laziness with FS::svc_acct::replace - my @oldgroups = @{$old->usergroup}; #uuuh - my @newgroups = $new->radius_groups; - my @delgroups = (); - foreach my $oldgroup ( @oldgroups ) { - if ( grep { $oldgroup eq $_ } @newgroups ) { - @newgroups = grep { $oldgroup ne $_ } @newgroups; - next; - } - push @delgroups, $oldgroup; + my $error; + my (@oldgroups) = $old->radius_groups; + my (@newgroups) = $new->radius_groups; + $error = $self->sqlreplace_usergroups( $new->svcnum, + $self->export_username($new), + $jobnum ? $jobnum : '', + \@oldgroups, + \@newgroups, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - if ( @delgroups ) { - my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_delete', - $self->export_username($new), @delgroups ); - unless ( ref($err_or_queue) ) { - $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; - } - } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; +} + +sub _export_suspend { + my( $self, $svc_acct ) = (shift, shift); + + my $new = $svc_acct->clone_suspended; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'insert', + 'check', $self->export_username($new), $new->radius_check ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; } - if ( @newgroups ) { - my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_insert', - $self->export_username($new), @newgroups ); - unless ( ref($err_or_queue) ) { - $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; + my (@newgroups) = $self->suspended_usergroups($svc_acct); + $error = + $self->sqlreplace_usergroups( $new->svcnum, + $self->export_username($new), + '', + $svc_acct->usergroup, + \@newgroups, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; +} +sub _export_unsuspend { + my( $self, $svc_acct ) = (shift, shift); + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'insert', + 'check', $self->export_username($svc_acct), $svc_acct->radius_check ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + + my $error; + my (@oldgroups) = $self->suspended_usergroups($svc_acct); + $error = $self->sqlreplace_usergroups( $svc_acct->svcnum, + $self->export_username($svc_acct), + '', + \@oldgroups, + $svc_acct->usergroup, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } sub _export_delete { - my( $self, $svc_acct ) = (shift, shift); - my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'delete', - $self->export_username($svc_acct) ); + my( $self, $svc_x ) = (shift, shift); + my $usergroup = $self->option('usergroup') || 'usergroup'; + my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'delete', + $self->export_username($svc_x), $usergroup ); ref($err_or_queue) ? '' : $err_or_queue; } @@ -240,6 +321,39 @@ sub sqlradius_queue { ) or $queue; } +sub suspended_usergroups { + my ($self, $svc_acct) = (shift, shift); + + return () unless $svc_acct; + + #false laziness with FS::part_export::shellcommands + #subclass part_export? + + my $r = $svc_acct->cust_svc->cust_pkg->last_reason('susp'); + my %reasonmap = $self->_groups_susp_reason_map; + my $userspec = ''; + if ($r) { + $userspec = $reasonmap{$r->reasonnum} + if exists($reasonmap{$r->reasonnum}); + $userspec = $reasonmap{$r->reason} + if (!$userspec && exists($reasonmap{$r->reason})); + } + my $suspend_user; + if ($userspec =~ /^d+$/ ){ + $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } ); + }elsif ($userspec =~ /^\S+\@\S+$/){ + my ($username,$domain) = split(/\@/, $userspec); + for my $user (qsearch( 'svc_acct', { 'username' => $username } )){ + $suspend_user = $user if $userspec eq $user->email; + } + }elsif ($userspec){ + $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } ); + } + #esalf + return $suspend_user->radius_groups if $suspend_user; + (); +} + sub sqlradius_insert { #subroutine, not method my $dbh = sqlradius_connect(shift, shift, shift); my( $table, $username, %attributes ) = @_; @@ -268,7 +382,7 @@ sub sqlradius_insert { #subroutine, not method $i_sth->execute( $username, $attribute, - ( $attribute =~ /Password/i ? '==' : ':=' ), + ( $attribute eq 'Password' ? '==' : ':=' ), $attributes{$attribute}, ) or die $i_sth->errstr; @@ -280,24 +394,48 @@ sub sqlradius_insert { #subroutine, not method sub sqlradius_usergroup_insert { #subroutine, not method my $dbh = sqlradius_connect(shift, shift, shift); - my( $username, @groups ) = @_; + my $username = shift; + my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup'; + my @groups = @_; + + my $s_sth = $dbh->prepare( + "SELECT COUNT(*) FROM $usergroup WHERE UserName = ? AND GroupName = ?" + ) or die $dbh->errstr; my $sth = $dbh->prepare( - "INSERT INTO usergroup ( UserName, GroupName ) VALUES ( ?, ? )" + "INSERT INTO $usergroup ( UserName, GroupName ) VALUES ( ?, ? )" ) or die $dbh->errstr; + foreach my $group ( @groups ) { + $s_sth->execute( $username, $group ) or die $s_sth->errstr; + if ($s_sth->fetchrow_arrayref->[0]) { + warn localtime() . ": sqlradius_usergroup_insert attempted to reinsert " . + "$group for $username\n" + if $DEBUG; + next; + } $sth->execute( $username, $group ) or die "can't insert into groupname table: ". $sth->errstr; } + if ( $s_sth->{Active} ) { + warn "sqlradius s_sth still active; calling ->finish()"; + $s_sth->finish; + } + if ( $sth->{Active} ) { + warn "sqlradius sth still active; calling ->finish()"; + $sth->finish; + } $dbh->disconnect; } sub sqlradius_usergroup_delete { #subroutine, not method my $dbh = sqlradius_connect(shift, shift, shift); - my( $username, @groups ) = @_; + my $username = shift; + my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup'; + my @groups = @_; my $sth = $dbh->prepare( - "DELETE FROM usergroup WHERE UserName = ? AND GroupName = ?" + "DELETE FROM $usergroup WHERE UserName = ? AND GroupName = ?" ) or die $dbh->errstr; foreach my $group ( @groups ) { $sth->execute( $username, $group ) @@ -308,8 +446,9 @@ sub sqlradius_usergroup_delete { #subroutine, not method sub sqlradius_rename { #subroutine, not method my $dbh = sqlradius_connect(shift, shift, shift); - my($new_username, $old_username) = @_; - foreach my $table (qw(radreply radcheck usergroup )) { + my($new_username, $old_username) = (shift, shift); + my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup'; + foreach my $table (qw(radreply radcheck), $usergroup ) { my $sth = $dbh->prepare("UPDATE $table SET Username = ? WHERE UserName = ?") or die $dbh->errstr; $sth->execute($new_username, $old_username) @@ -335,8 +474,9 @@ sub sqlradius_attrib_delete { #subroutine, not method sub sqlradius_delete { #subroutine, not method my $dbh = sqlradius_connect(shift, shift, shift); my $username = shift; + my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup'; - foreach my $table (qw( radcheck radreply usergroup )) { + foreach my $table (qw( radcheck radreply), $usergroup ) { my $sth = $dbh->prepare( "DELETE FROM $table WHERE UserName = ?" ); $sth->execute($username) or die "can't delete from $table table: ". $sth->errstr; @@ -350,10 +490,81 @@ sub sqlradius_connect { DBI->connect(@_) or die $DBI::errstr; } +sub sqlreplace_usergroups { + my ($self, $svcnum, $username, $jobnum, $old, $new) = @_; + + # (sorta) false laziness with FS::svc_acct::replace + my @oldgroups = @$old; + my @newgroups = @$new; + my @delgroups = (); + foreach my $oldgroup ( @oldgroups ) { + if ( grep { $oldgroup eq $_ } @newgroups ) { + @newgroups = grep { $oldgroup ne $_ } @newgroups; + next; + } + push @delgroups, $oldgroup; + } + + my $usergroup = $self->option('usergroup') || 'usergroup'; + + if ( @delgroups ) { + my $err_or_queue = $self->sqlradius_queue( $svcnum, 'usergroup_delete', + $username, $usergroup, @delgroups ); + return $err_or_queue + unless ref($err_or_queue); + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + return $error if $error; + } + } + + if ( @newgroups ) { + cluck localtime(). ": queuing usergroup_insert for $svcnum ($username) ". + "with ". join(", ", @newgroups) + if $DEBUG; + my $err_or_queue = $self->sqlradius_queue( $svcnum, 'usergroup_insert', + $username, $usergroup, @newgroups ); + return $err_or_queue + unless ref($err_or_queue); + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + return $error if $error; + } + } + ''; +} + + #-- +=item usage_sessions HASHREF + =item usage_sessions TIMESTAMP_START TIMESTAMP_END [ SVC_ACCT [ IP [ PREFIX [ SQL_SELECT ] ] ] ] +New-style: pass a hashref with the following keys: + +=over 4 + +=item stoptime_start - Lower bound for AcctStopTime, as a UNIX timestamp + +=item stoptime_end - Upper bound for AcctStopTime, as a UNIX timestamp + +=item open_sessions - Only show records with no AcctStopTime (typically used without stoptime_* options and with starttime_* options instead) + +=item starttime_start - Lower bound for AcctStartTime, as a UNIX timestamp + +=item starttime_end - Upper bound for AcctStartTime, as a UNIX timestamp + +=item svc_acct + +=item ip + +=item prefix + +=back + +Old-style: + TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see L. Also see L and L for conversion functions. @@ -395,11 +606,24 @@ Returns an arrayref of hashrefs with the following fields: #some false laziness w/cust_svc::seconds_since_sqlradacct sub usage_sessions { - my( $self, $start, $end ) = splice(@_, 0, 3); - my $svc_acct = @_ ? shift : ''; - my $ip = @_ ? shift : ''; - my $prefix = @_ ? shift : ''; - #my $select = @_ ? shift : '*'; + my( $self ) = shift; + + my $opt = {}; + my($start, $end, $svc_acct, $ip, $prefix) = ( '', '', '', '', ''); + if ( ref($_[0]) ) { + $opt = shift; + $start = $opt->{stoptime_start}; + $end = $opt->{stoptime_end}; + $svc_acct = $opt->{svc_acct}; + $ip = $opt->{ip}; + $prefix = $opt->{prefix}; + } else { + ( $start, $end ) = splice(@_, 0, 2); + $svc_acct = @_ ? shift : ''; + $ip = @_ ? shift : ''; + $prefix = @_ ? shift : ''; + #my $select = @_ ? shift : '*'; + } $end ||= 2147483647; @@ -409,16 +633,7 @@ sub usage_sessions { qw( datasrc username password ) ); #select a unix time conversion function based on database type - my $str2time; - if ( $dbh->{Driver}->{Name} =~ /^mysql(PP)?$/ ) { - $str2time = 'UNIX_TIMESTAMP('; - } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) { - $str2time = 'EXTRACT( EPOCH FROM '; - } else { - warn "warning: unknown database type ". $dbh->{Driver}->{Name}. - "; guessing how to convert to UNIX timestamps"; - $str2time = 'extract(epoch from '; - } + my $str2time = str2time_sql( $dbh->{Driver}->{Name} ); my @fields = ( qw( username realm framedipaddress @@ -430,37 +645,61 @@ sub usage_sessions { ); my @param = (); - my $where = ''; + my @where = (); if ( $svc_acct ) { my $username = $self->export_username($svc_acct); - if ( $svc_acct =~ /^([^@]+)\@([^@]+)$/ ) { - $where = '( UserName = ? OR ( UserName = ? AND Realm = ? ) ) AND'; + if ( $username =~ /^([^@]+)\@([^@]+)$/ ) { + push @where, '( UserName = ? OR ( UserName = ? AND Realm = ? ) )'; push @param, $username, $1, $2; } else { - $where = 'UserName = ? AND'; + push @where, 'UserName = ?'; push @param, $username; } } + if ($self->option('process_single_realm')) { + push @where, 'Realm = ?'; + push @param, $self->option('realm'); + } + if ( length($ip) ) { - $where .= ' FramedIPAddress = ? AND'; + push @where, ' FramedIPAddress = ?'; push @param, $ip; } if ( length($prefix) ) { #assume sip: for now, else things get ugly trying to match /^\w+:$prefix/ - $where .= " CalledStationID LIKE 'sip:$prefix\%' AND"; + push @where, " CalledStationID LIKE 'sip:$prefix\%'"; } - push @param, $start, $end; + if ( $start ) { + push @where, "$str2time AcctStopTime ) >= ?"; + push @param, $start; + } + if ( $end ) { + push @where, "$str2time AcctStopTime ) <= ?"; + push @param, $end; + } + if ( $opt->{open_sessions} ) { + push @where, 'AcctStopTime IS NULL'; + } + if ( $opt->{starttime_start} ) { + push @where, "$str2time AcctStartTime ) >= ?"; + push @param, $opt->{starttime_start}; + } + if ( $opt->{starttime_end} ) { + push @where, "$str2time AcctStartTime ) <= ?"; + push @param, $opt->{starttime_end}; + } + + my $where = join(' AND ', @where); + $where = "WHERE $where" if $where; my $sth = $dbh->prepare('SELECT '. join(', ', @fields). " FROM radacct - WHERE $where - $str2time AcctStopTime ) >= ? - AND $str2time AcctStopTime ) <= ? - ORDER BY AcctStartTime DESC + $where + ORDER BY AcctStartTime DESC ") or die $dbh->errstr; $sth->execute(@param) or die $sth->errstr; @@ -472,19 +711,25 @@ sub usage_sessions { =cut -sub update_svc_acct { +sub update_svc { my $self = shift; + my $conf = new FS::Conf; + + my $fdbh = dbh; my $dbh = sqlradius_connect( map $self->option($_), qw( datasrc username password ) ); + my $str2time = str2time_sql( $dbh->{Driver}->{Name} ); my @fields = qw( radacctid username realm acctsessiontime ); my @param = (); my $where = ''; my $sth = $dbh->prepare(" - SELECT RadAcctId, UserName, Realm, AcctSessionTime + SELECT RadAcctId, UserName, Realm, AcctSessionTime, + $str2time AcctStartTime), $str2time AcctStopTime), + AcctInputOctets, AcctOutputOctets FROM radacct WHERE FreesideStatus IS NULL AND AcctStopTime != 0 @@ -492,47 +737,72 @@ sub update_svc_acct { $sth->execute() or die $sth->errstr; while ( my $row = $sth->fetchrow_arrayref ) { - my($RadAcctId, $UserName, $Realm, $AcctSessionTime) = @$row; + my($RadAcctId, $UserName, $Realm, $AcctSessionTime, $AcctStartTime, + $AcctStopTime, $AcctInputOctets, $AcctOutputOctets) = @$row; warn "processing record: ". "$RadAcctId ($UserName\@$Realm for ${AcctSessionTime}s" if $DEBUG; - my %search = ( 'username' => $UserName ); + $UserName = lc($UserName) unless $conf->exists('username-uppercase'); + + #my %search = ( '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 ) "; - my $svc_domain = qsearch } - my @svc_acct = - grep { qsearch( 'export_svc', { 'exportnum' => $self->exportnum, - 'svcpart' => $_->cust_svc->svcpart, } ) - } - qsearch( 'svc_acct', - { 'username' => $UserName }, - '', - $extra_sql - ); + my $oldAutoCommit = $FS::UID::AutoCommit; # can't undo side effects, but at + local $FS::UID::AutoCommit = 0; # least we can avoid over counting + my $status = 'skipped'; my $errinfo = "for RADIUS detail RadAcctID $RadAcctId ". "(UserName $UserName, Realm $Realm)"; - my $status = 'skipped'; - 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"; + + if ( $self->option('process_single_realm') + && $self->option('realm') ne $Realm ) + { + warn "WARNING: wrong realm $errinfo - skipping\n" if $DEBUG; } else { - my $svc_acct = $svc_acct[0]; - warn "found svc_acct ". $svc_acct->svcnum. " $errinfo\n" if $DEBUG; - if ( $svc_acct->seconds !~ /^$/ ) { - warn " svc_acct.seconds found (". $svc_acct->seconds. - ") - decrementing\n" - if $DEBUG; - $svc_acct->decrement_seconds($AcctSessionTime); - $status = 'done'; + 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"; } else { - warn " no existing seconds value for svc_acct - skiping\n" if $DEBUG; + + 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); + } } } @@ -543,9 +813,49 @@ sub update_svc_acct { ) or die $dbh->errstr; $psth->execute($status, $RadAcctId) or die $psth->errstr; + $fdbh->commit or die $fdbh->errstr if $oldAutoCommit; + } } +sub _try_decrement { + my ($svc_acct, $column, $amount) = @_; + if ( $svc_acct->$column !~ /^$/ ) { + warn " svc_acct.$column found (". $svc_acct->$column. + ") - decrementing\n" + if $DEBUG; + my $method = 'decrement_' . $column; + my $error = $svc_acct->$method($amount); + die $error if $error; + return 'done'; + } else { + warn " no existing $column value for svc_acct - skipping\n" if $DEBUG; + } + return 'skipped'; +} + +### +#class methods +### + +sub all_sqlradius { + #my $class = shift; + + #don't just look for ->can('usage_sessions'), we're sqlradius-specific + # (radiator is supposed to be setup with a radacct table) + #i suppose it would be more slick to look for things that inherit from us.. + + my @part_export = (); + push @part_export, qsearch('part_export', { 'exporttype' => $_ } ) + foreach qw( sqlradius sqlradius_withdomain radiator phone_sqlradius ); + @part_export; +} + +sub all_sqlradius_withaccounting { + my $class = shift; + grep { ! $_->option('ignore_accounting') } $class->all_sqlradius; +} + 1;