RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / part_export / textradius.pm
1 package FS::part_export::textradius;
2
3 use vars qw(@ISA %info $prefix);
4 use Fcntl qw(:flock);
5 use Tie::IxHash;
6 use FS::UID qw(datasrc);
7 use FS::part_export;
8
9 @ISA = qw(FS::part_export);
10
11 tie my %options, 'Tie::IxHash',
12   'user' => { label=>'Remote username', default=>'root' },
13   'users' => { label=>'users file location', default=>'/etc/raddb/users' },
14 ;
15
16 %info = (
17   'svc'     => 'svc_acct',
18   'desc'    =>
19     'Real-time export to a text /etc/raddb/users file (Livingston, Cistron)',
20   'options' => \%options,
21   'default_svc_class' => 'Internet',
22   'notes'   => <<'END'
23 This will edit a text RADIUS users file in place on a remote server.
24 Requires installation of
25 <a href="http://search.cpan.org/dist/RADIUS-UserFile">RADIUS::UserFile</a>
26 from CPAN.  If using RADIUS::UserFile 1.01, make sure to apply
27 <a href="http://rt.cpan.org/NoAuth/Bug.html?id=1210">this patch</a>.  Also
28 make sure <a href="http://rsync.samba.org/">rsync</a> is installed on the
29 remote machine, and <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">SSH is setup for unattended
30 operation</a>.
31 END
32 );
33
34 $prefix = "%%%FREESIDE_CONF%%%/export.";
35
36 sub rebless { shift; }
37
38 sub _export_insert {
39   my($self, $svc_acct) = (shift, shift);
40   $err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'insert',
41     $svc_acct->username, $svc_acct->radius_check, '-', $svc_acct->radius_reply);
42   ref($err_or_queue) ? '' : $err_or_queue;
43 }
44
45 sub _export_replace {
46   my( $self, $new, $old ) = (shift, shift, shift);
47   return "can't (yet?) change username with textradius"
48     if $old->username ne $new->username;
49   #return '' unless $old->_password ne $new->_password;
50   $err_or_queue = $self->textradius_queue( $new->svcnum, 'insert',
51     $new->username, $new->radius_check, '-', $new->radius_reply);
52   ref($err_or_queue) ? '' : $err_or_queue;
53 }
54
55 sub _export_delete {
56   my( $self, $svc_acct ) = (shift, shift);
57   $err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'delete',
58     $svc_acct->username );
59   ref($err_or_queue) ? '' : $err_or_queue;
60 }
61
62 #a good idea to queue anything that could fail or take any time
63 sub textradius_queue {
64   my( $self, $svcnum, $method ) = (shift, shift, shift);
65   my $queue = new FS::queue {
66     'svcnum' => $svcnum,
67     'job'    => "FS::part_export::textradius::textradius_$method",
68   };
69   $queue->insert(
70     $self->option('user')||'root',
71     $self->machine,
72     $self->option('users'),
73     @_,
74   ) or $queue;
75 }
76
77 sub textradius_insert { #subroutine, not method
78   my( $user, $host, $users, $username, @attributes ) = @_;
79
80   #silly arg processing
81   my($att, @check);
82   push @check, $att while @attributes && ($att=shift @attributes) ne '-';
83   my %check = @check;
84   my %reply = @attributes;
85
86   my $file = textradius_download($user, $host, $users);
87
88   eval "use RADIUS::UserFile;";
89   die $@ if $@;
90
91   my $userfile = new RADIUS::UserFile(
92     File        => $file,
93     Who         => [ $username ],
94     Check_Items => [ keys %check ],
95   ) or die "error parsing $file";
96
97   $userfile->remove($username);
98   $userfile->add(
99     Who        => $username,
100     Attributes => { %check, %reply },
101     Comment    => 'user added by Freeside',
102   ) or die "error adding to $file";
103
104   $userfile->update( Who => [ $username ] )
105     or die "error updating $file";
106
107   textradius_upload($user, $host, $users);
108
109 }
110
111 sub textradius_delete { #subroutine, not method
112   my( $user, $host, $users, $username ) = @_;
113
114   my $file = textradius_download($user, $host, $users);
115
116   eval "use RADIUS::UserFile;";
117   die $@ if $@;
118
119   my $userfile = new RADIUS::UserFile(
120     File        => $file,
121     Who         => [ $username ],
122   ) or die "error parsing $file";
123
124   $userfile->remove($username);
125
126   $userfile->update( Who => [ $username ] )
127     or die "error updating $file";
128
129   textradius_upload($user, $host, $users);
130 }
131
132 sub textradius_download {
133   my( $user, $host, $users ) = @_;
134
135   my $dir = $prefix. datasrc;
136   mkdir $dir, 0700 or die $! unless -d $dir;
137   $dir .= "/$host";
138   mkdir $dir, 0700 or die $! unless -d $dir;
139
140   my $dest = "$dir/users";
141
142   eval "use File::Rsync;";
143   die $@ if $@;
144   my $rsync = File::Rsync->new({ rsh => 'ssh' });
145
146   open(LOCK, "+>>$dest.lock")
147     and flock(LOCK,LOCK_EX)
148       or die "can't open $dest.lock: $!";
149
150   $rsync->exec( {
151     src  => "$user\@$host:$users",
152     dest => $dest,
153   } ); # true/false return value from exec is not working, alas
154   if ( $rsync->err ) {
155     die "error downloading $user\@$host:$users : ".
156         'exit status: '. $rsync->status. ', '.
157         'STDERR: '. join(" / ", $rsync->err). ', '.
158         'STDOUT: '. join(" / ", $rsync->out);
159   }
160
161   $dest;
162 }
163
164 sub textradius_upload {
165   my( $user, $host, $users ) = @_;
166
167   my $dir = $prefix. datasrc. "/$host";
168
169   eval "use File::Rsync;";
170   die $@ if $@;
171   my $rsync = File::Rsync->new({
172     rsh => 'ssh',
173     #dry_run => 1,
174   });
175   $rsync->exec( {
176     src  => "$dir/users",
177     dest => "$user\@$host:$users",
178   } ); # true/false return value from exec is not working, alas
179   if ( $rsync->err ) {
180     die "error uploading to $user\@$host:$users : ".
181         'exit status: '. $rsync->status. ', '.
182         'STDERR: '. join(" / ", $rsync->err). ', '.
183         'STDOUT: '. join(" / ", $rsync->out);
184   }
185
186   flock(LOCK,LOCK_UN);
187   close LOCK;
188
189 }
190
191 1;
192