X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_export%2Fsqlradius.pm;h=7d0edd65ca3cc66ec0e25a4dc0758e83805ddf17;hb=573139dbd6c37808697bfa72a3a468bb0980d4dd;hp=4f67ac6c3a44c3dd1bfdc2e623d2b29b791b4208;hpb=624b2d44625f69d71175c3348cae635d580c890b;p=freeside.git
diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm
index 4f67ac6c3..7d0edd65c 100644
--- a/FS/FS/part_export/sqlradius.pm
+++ b/FS/FS/part_export/sqlradius.pm
@@ -1,5 +1,6 @@
package FS::part_export::sqlradius;
+use strict;
use vars qw(@ISA @EXPORT_OK $DEBUG %info %options $notes1 $notes2);
use Exporter;
use Tie::IxHash;
@@ -14,14 +15,28 @@ use Carp qw( cluck );
$DEBUG = 0;
+my %groups;
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',
@@ -34,16 +49,33 @@ 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)', } ,
+ '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 => 'select',
+ multi => 1,
+ option_label => sub {
+ $groups{$_[0]};
+ },
+ option_values => sub {
+ %groups = (
+ map { $_->groupnum, $_->long_description }
+ qsearch('radius_group', {}),
+ );
+ sort keys (%groups);
+ },
+ } ,
'groups_susp_reason' => { label =>
'Radius group mapping to reason (via template user) (svcnum|username|username@domain reasonnum|reason)',
type => 'textarea',
},
-
+ 'export_attrs' => {
+ type => 'checkbox',
+ label => 'Export RADIUS group attributes to this database',
+ },
;
$notes1 = <<'END';
-Real-time export of radcheck, radreply and usergroup
+Real-time export of radcheck, radreply and usergroup/radusergroup
tables to any SQL database for
FreeRADIUS
or ICRADIUS.
@@ -78,6 +110,7 @@ END
'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS)',
'options' => \%options,
'nodomain' => 'Y',
+ 'nas' => 'Y', # show export_nas selection in UI
'notes' => $notes1.
'This export does not export RADIUS realms (see also '.
'sqlradius_withdomain). '.
@@ -90,31 +123,42 @@ sub _groups_susp_reason_map { map { reverse( /^\s*(\S+)\s*(.*)$/ ) }
sub rebless { shift; }
-sub export_username {
+sub export_username { # override for other svcdb
my($self, $svc_acct) = (shift, shift);
warn "export_username called on $self with arg $svc_acct" if $DEBUG > 1;
$svc_acct->username;
}
+sub radius_reply { #override for other svcdb
+ my($self, $svc_acct) = (shift, shift);
+ $svc_acct->radius_reply;
+}
+
+sub radius_check { #override for other svcdb
+ my($self, $svc_acct) = (shift, shift);
+ $svc_acct->radius_check;
+}
+
sub _export_insert {
my($self, $svc_x) = (shift, shift);
foreach my $table (qw(reply check)) {
my $method = "radius_$table";
- my %attrib = $svc_x->$method();
+ my %attrib = $self->$method($svc_x);
next unless keys %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_x->radius_groups;
+ my @groups = $svc_x->radius_groups('hashref');
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_x->svcnum, 'usergroup_insert',
- $self->export_username($svc_x), @groups );
+ $self->export_username($svc_x), $usergroup, @groups );
return $err_or_queue unless ref($err_or_queue);
}
'';
@@ -136,8 +180,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;
@@ -187,8 +232,8 @@ sub _export_replace {
}
my $error;
- my (@oldgroups) = $old->radius_groups;
- my (@newgroups) = $new->radius_groups;
+ my (@oldgroups) = $old->radius_groups('hashref');
+ my (@newgroups) = $new->radius_groups('hashref');
$error = $self->sqlreplace_usergroups( $new->svcnum,
$self->export_username($new),
$jobnum ? $jobnum : '',
@@ -221,22 +266,27 @@ sub _export_suspend {
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;
+ my @newgroups = $self->suspended_usergroups($svc_acct);
+
+ unless (@newgroups) { #don't change password if assigning to a suspended group
+
+ 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;
+ }
+
}
- my $error;
- my (@newgroups) = $self->suspended_usergroups($svc_acct);
- $error =
- $self->sqlreplace_usergroups( $new->svcnum,
- $self->export_username($new),
- '',
- $svc_acct->usergroup,
- \@newgroups,
- );
+ my $error =
+ $self->sqlreplace_usergroups(
+ $new->svcnum,
+ $self->export_username($new),
+ '',
+ [ $svc_acct->radius_groups('hashref') ],
+ \@newgroups,
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -269,12 +319,13 @@ sub _export_unsuspend {
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,
- );
+ $error = $self->sqlreplace_usergroups(
+ $svc_acct->svcnum,
+ $self->export_username($svc_acct),
+ '',
+ \@oldgroups,
+ [ $svc_acct->radius_groups('hashref') ],
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -286,8 +337,9 @@ sub _export_unsuspend {
sub _export_delete {
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) );
+ $self->export_username($svc_x), $usergroup );
ref($err_or_queue) ? '' : $err_or_queue;
}
@@ -323,7 +375,7 @@ sub suspended_usergroups {
if (!$userspec && exists($reasonmap{$r->reason}));
}
my $suspend_user;
- if ($userspec =~ /^d+$/ ){
+ if ($userspec =~ /^\d+$/ ){
$suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
}elsif ($userspec =~ /^\S+\@\S+$/){
my ($username,$domain) = split(/\@/, $userspec);
@@ -334,7 +386,7 @@ sub suspended_usergroups {
$suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
}
#esalf
- return $suspend_user->radius_groups if $suspend_user;
+ return $suspend_user->radius_groups('hashref') if $suspend_user;
();
}
@@ -378,17 +430,21 @@ 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 = ?"
+ "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, Priority ) VALUES ( ?, ?, ? )"
) or die $dbh->errstr;
- foreach my $group ( @groups ) {
+ foreach ( @groups ) {
+ my $group = $_->{'groupname'};
+ my $priority = $_->{'priority'};
$s_sth->execute( $username, $group ) or die $s_sth->errstr;
if ($s_sth->fetchrow_arrayref->[0]) {
warn localtime() . ": sqlradius_usergroup_insert attempted to reinsert " .
@@ -396,20 +452,31 @@ sub sqlradius_usergroup_insert { #subroutine, not method
if $DEBUG;
next;
}
- $sth->execute( $username, $group )
+ $sth->execute( $username, $group, $priority )
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 ) {
+ foreach ( @groups ) {
+ my $group = $_->{'groupname'};
$sth->execute( $username, $group )
or die "can't delete from groupname table: ". $sth->errstr;
}
@@ -418,8 +485,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)
@@ -445,8 +513,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;
@@ -475,9 +544,11 @@ sub sqlreplace_usergroups {
push @delgroups, $oldgroup;
}
+ my $usergroup = $self->option('usergroup') || 'usergroup';
+
if ( @delgroups ) {
my $err_or_queue = $self->sqlradius_queue( $svcnum, 'usergroup_delete',
- $username, @delgroups );
+ $username, $usergroup, @delgroups );
return $err_or_queue
unless ref($err_or_queue);
if ( $jobnum ) {
@@ -491,7 +562,7 @@ sub sqlreplace_usergroups {
"with ". join(", ", @newgroups)
if $DEBUG;
my $err_or_queue = $self->sqlradius_queue( $svcnum, 'usergroup_insert',
- $username, @newgroups );
+ $username, $usergroup, @newgroups );
return $err_or_queue
unless ref($err_or_queue);
if ( $jobnum ) {
@@ -578,6 +649,7 @@ sub usage_sessions {
my $opt = {};
my($start, $end, $svc_acct, $ip, $prefix) = ( '', '', '', '', '');
+ my $summarize = 0;
if ( ref($_[0]) ) {
$opt = shift;
$start = $opt->{stoptime_start};
@@ -585,6 +657,7 @@ sub usage_sessions {
$svc_acct = $opt->{svc_acct};
$ip = $opt->{ip};
$prefix = $opt->{prefix};
+ $summarize = $opt->{summarize};
} else {
( $start, $end ) = splice(@_, 0, 2);
$svc_acct = @_ ? shift : '';
@@ -612,12 +685,16 @@ sub usage_sessions {
"$str2time acctstoptime ) as acctstoptime",
);
+ @fields = ( 'username', 'sum(acctsessiontime) as acctsessiontime', 'sum(acctinputoctets) as acctinputoctets',
+ 'sum(acctoutputoctets) as acctoutputoctets',
+ ) if $summarize;
+
my @param = ();
my @where = ();
if ( $svc_acct ) {
my $username = $self->export_username($svc_acct);
- if ( $svc_acct =~ /^([^@]+)\@([^@]+)$/ ) {
+ if ( $username =~ /^([^@]+)\@([^@]+)$/ ) {
push @where, '( UserName = ? OR ( UserName = ? AND Realm = ? ) )';
push @param, $username, $1, $2;
} else {
@@ -626,6 +703,11 @@ sub usage_sessions {
}
}
+ if ($self->option('process_single_realm')) {
+ push @where, 'Realm = ?';
+ push @param, $self->option('realm');
+ }
+
if ( length($ip) ) {
push @where, ' FramedIPAddress = ?';
push @param, $ip;
@@ -659,11 +741,15 @@ sub usage_sessions {
my $where = join(' AND ', @where);
$where = "WHERE $where" if $where;
+ my $groupby = '';
+ $groupby = 'GROUP BY username' if $summarize;
+
+ my $orderby = 'ORDER BY AcctStartTime DESC';
+ $orderby = '' if $summarize;
+
my $sth = $dbh->prepare('SELECT '. join(', ', @fields).
- " FROM radacct
- $where
- ORDER BY AcctStartTime DESC
- ") or die $dbh->errstr;
+ " FROM radacct $where $groupby $orderby
+ ") or die $dbh->errstr;
$sth->execute(@param) or die $sth->errstr;
[ map { { %$_ } } @{ $sth->fetchall_arrayref({}) } ];
@@ -695,7 +781,7 @@ sub update_svc {
AcctInputOctets, AcctOutputOctets
FROM radacct
WHERE FreesideStatus IS NULL
- AND AcctStopTime != 0
+ AND AcctStopTime IS NOT NULL
") or die $dbh->errstr;
$sth->execute() or die $sth->errstr;
@@ -719,43 +805,53 @@ sub update_svc {
my $oldAutoCommit = $FS::UID::AutoCommit; # can't undo side effects, but at
local $FS::UID::AutoCommit = 0; # least we can avoid over counting
- my @svc_acct =
- grep { qsearch( 'export_svc', { 'exportnum' => $self->exportnum,
- 'svcpart' => $_->cust_svc->svcpart, } )
- }
- qsearch( 'svc_acct',
- { 'username' => $UserName },
- '',
- $extra_sql
- );
-
+ 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";
- } else {
-
- 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 $cust_pkg = $svc_acct->cust_svc->cust_pkg;
- if ( $cust_pkg && $AcctStopTime < ( $cust_pkg->last_bill
- || $cust_pkg->setup ) ) {
- $status = 'skipped (too old)';
+ 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";
} 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
+
+ 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);
+ $status=join(' ', @st);
+ }
}
}
@@ -788,6 +884,257 @@ sub _try_decrement {
return 'skipped';
}
+=item export_nas_insert NAS
+
+=item export_nas_delete NAS
+
+=item export_nas_replace NEW_NAS OLD_NAS
+
+Update the NAS table (allowed RADIUS clients) on the attached RADIUS
+server. Currently requires the table to be named 'nas' and to follow
+the stock schema (/etc/freeradius/nas.sql).
+
+=cut
+
+sub export_nas_insert { shift->export_nas_action('insert', @_); }
+sub export_nas_delete { shift->export_nas_action('delete', @_); }
+sub export_nas_replace { shift->export_nas_action('replace', @_); }
+
+sub export_nas_action {
+ my $self = shift;
+ my ($action, $new, $old) = @_;
+ # find the NAS in the target table by its name
+ my $nasname = ($action eq 'replace') ? $old->nasname : $new->nasname;
+ my $nasnum = $new->nasnum;
+
+ my $err_or_queue = $self->sqlradius_queue('', "nas_$action",
+ nasname => $nasname,
+ nasnum => $nasnum
+ );
+ return $err_or_queue unless ref $err_or_queue;
+ '';
+}
+
+sub sqlradius_nas_insert {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ my $nas = qsearchs('nas', { nasnum => $opt{'nasnum'} })
+ or die "nasnum ".$opt{'nasnum'}.' not found';
+ # insert actual NULLs where FS::Record has translated to empty strings
+ my @values = map { length($nas->$_) ? $nas->$_ : undef }
+ qw( nasname shortname type secret server community description );
+ my $sth = $dbh->prepare('INSERT INTO nas
+(nasname, shortname, type, secret, server, community, description)
+VALUES (?, ?, ?, ?, ?, ?, ?)');
+ $sth->execute(@values) or die $dbh->errstr;
+}
+
+sub sqlradius_nas_delete {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ my $sth = $dbh->prepare('DELETE FROM nas WHERE nasname = ?');
+ $sth->execute($opt{'nasname'}) or die $dbh->errstr;
+}
+
+sub sqlradius_nas_replace {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ my $nas = qsearchs('nas', { nasnum => $opt{'nasnum'} })
+ or die "nasnum ".$opt{'nasnum'}.' not found';
+ my @values = map {$nas->$_}
+ qw( nasname shortname type secret server community description );
+ my $sth = $dbh->prepare('UPDATE nas SET
+ nasname = ?, shortname = ?, type = ?, secret = ?,
+ server = ?, community = ?, description = ?
+ WHERE nasname = ?');
+ $sth->execute(@values, $opt{'nasname'}) or die $dbh->errstr;
+}
+
+=item export_attr_insert RADIUS_ATTR
+
+=item export_attr_delete RADIUS_ATTR
+
+=item export_attr_replace NEW_RADIUS_ATTR OLD_RADIUS_ATTR
+
+Update the group attribute tables (radgroupcheck and radgroupreply) on
+the RADIUS server. In delete and replace actions, the existing records
+are identified by the combination of group name and attribute name.
+
+In the special case where attributes are being replaced because a group
+name (L->groupname) is changing, the pseudo-field
+'groupname' must be set in OLD_RADIUS_ATTR. It's probably best to do this
+
+
+=cut
+
+# some false laziness with NAS export stuff...
+
+sub export_attr_insert { shift->export_attr_action('insert', @_); }
+
+sub export_attr_delete { shift->export_attr_action('delete', @_); }
+
+sub export_attr_replace { shift->export_attr_action('replace', @_); }
+
+sub export_attr_action {
+ my $self = shift;
+ my ($action, $new, $old) = @_;
+ my ($attrname, $attrtype, $groupname) =
+ ($new->attrname, $new->attrtype, $new->radius_group->groupname);
+ if ( $action eq 'replace' ) {
+
+ if ( $new->attrtype ne $old->attrtype ) {
+ # they're in separate tables in the target
+ return $self->export_attr_action('delete', $old)
+ || $self->export_attr_action('insert', $new)
+ ;
+ }
+
+ # otherwise, just make sure we know the old attribute/group names
+ # so we can find the existing record
+ $attrname = $old->attrname;
+ $groupname = $old->groupname || $old->radius_group->groupname;
+ # maybe this should be enforced more strictly
+ warn "WARNING: attribute replace without 'groupname' set; assuming '$groupname'\n"
+ if !defined($old->groupname);
+ }
+
+ my $err_or_queue = $self->sqlradius_queue('', "attr_$action",
+ attrnum => $new->attrnum,
+ attrname => $attrname,
+ attrtype => $attrtype,
+ groupname => $groupname,
+ );
+ return $err_or_queue unless ref $err_or_queue;
+ '';
+}
+
+sub sqlradius_attr_insert {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} })
+ or die 'attrnum '.$opt{'attrnum'}.' not found';
+
+ my $table;
+ # make sure $table is completely safe
+ if ( $opt{'attrtype'} eq 'C' ) {
+ $table = 'radgroupcheck';
+ }
+ elsif ( $opt{'attrtype'} eq 'R' ) {
+ $table = 'radgroupreply';
+ }
+ else {
+ die "unknown attribute type '".$radius_attr->attrtype."'";
+ }
+
+ my @values = (
+ $opt{'groupname'}, map { $radius_attr->$_ } qw(attrname op value)
+ );
+ my $sth = $dbh->prepare(
+ 'INSERT INTO '.$table.' (groupname, attribute, op, value) VALUES (?,?,?,?)'
+ );
+ $sth->execute(@values) or die $dbh->errstr;
+}
+
+sub sqlradius_attr_delete {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+
+ my $table;
+ if ( $opt{'attrtype'} eq 'C' ) {
+ $table = 'radgroupcheck';
+ }
+ elsif ( $opt{'attrtype'} eq 'R' ) {
+ $table = 'radgroupreply';
+ }
+ else {
+ die "unknown attribute type '".$opt{'attrtype'}."'";
+ }
+
+ my $sth = $dbh->prepare(
+ 'DELETE FROM '.$table.' WHERE groupname = ? AND attribute = ?'
+ );
+ $sth->execute( @opt{'groupname', 'attrname'} ) or die $dbh->errstr;
+}
+
+sub sqlradius_attr_replace {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my %opt = @_;
+ my $radius_attr = qsearchs('radius_attr', { attrnum => $opt{'attrnum'} })
+ or die 'attrnum '.$opt{'attrnum'}.' not found';
+
+ my $table;
+ if ( $opt{'attrtype'} eq 'C' ) {
+ $table = 'radgroupcheck';
+ }
+ elsif ( $opt{'attrtype'} eq 'R' ) {
+ $table = 'radgroupreply';
+ }
+ else {
+ die "unknown attribute type '".$opt{'attrtype'}."'";
+ }
+
+ my $sth = $dbh->prepare(
+ 'UPDATE '.$table.' SET groupname = ?, attribute = ?, op = ?, value = ?
+ WHERE groupname = ? AND attribute = ?'
+ );
+
+ my $new_groupname = $radius_attr->radius_group->groupname;
+ my @new_values = (
+ $new_groupname, map { $radius_attr->$_ } qw(attrname op value)
+ );
+ $sth->execute( @new_values, @opt{'groupname', 'attrname'} )
+ or die $dbh->errstr;
+}
+
+=item export_group_replace NEW OLD
+
+Replace the L object OLD with NEW. This will change
+the group name and priority in all radusergroup records, and the group
+name in radgroupcheck and radgroupreply.
+
+=cut
+
+sub export_group_replace {
+ my $self = shift;
+ my ($new, $old) = @_;
+ return '' if $new->groupname eq $old->groupname
+ and $new->priority == $old->priority;
+
+ my $err_or_queue = $self->sqlradius_queue(
+ '',
+ 'group_replace',
+ ($self->option('usergroup') || 'usergroup'),
+ $new->hashref,
+ $old->hashref,
+ );
+ return $err_or_queue unless ref $err_or_queue;
+ '';
+}
+
+sub sqlradius_group_replace {
+ my $dbh = sqlradius_connect(shift, shift, shift);
+ my $usergroup = shift;
+ $usergroup =~ /^(rad)?usergroup$/
+ or die "bad usergroup table name: $usergroup";
+ my ($new, $old) = (shift, shift);
+ # apply renames to check/reply attribute tables
+ if ( $new->{'groupname'} ne $old->{'groupname'} ) {
+ foreach my $table (qw(radgroupcheck radgroupreply)) {
+ my $sth = $dbh->prepare(
+ 'UPDATE '.$table.' SET groupname = ? WHERE groupname = ?'
+ );
+ $sth->execute($new->{'groupname'}, $old->{'groupname'})
+ or die $dbh->errstr;
+ }
+ }
+ # apply renames and priority changes to usergroup table
+ my $sth = $dbh->prepare(
+ 'UPDATE '.$usergroup.' SET groupname = ?, priority = ? WHERE groupname = ?'
+ );
+ $sth->execute($new->{'groupname'}, $new->{'priority'}, $old->{'groupname'})
+ or die $dbh->errstr;
+}
+
###
#class methods
###
@@ -801,7 +1148,8 @@ sub all_sqlradius {
my @part_export = ();
push @part_export, qsearch('part_export', { 'exporttype' => $_ } )
- foreach qw( sqlradius sqlradius_withdomain radiator phone_sqlradius );
+ foreach qw( sqlradius sqlradius_withdomain radiator phone_sqlradius
+ broadband_sqlradius );
@part_export;
}