Merge branch 'github/pr/55_reprise'
[freeside.git] / bin / aradial-sftp_and_import
1 #!/usr/bin/perl -w
2
3 #i'm kinda like freeside-cdr-sftp_and_import... some parts should be libraried
4
5 use strict;
6 use Getopt::Std;
7 use Date::Parse;
8 use Date::Format;
9 use Text::CSV_XS;
10 use DBI qw( :sql_types );
11 use Net::SFTP::Foreign;
12 #use FS::UID qw( adminsuidsetup datasrc );
13
14 #adjusted these for what we're actually seeing in the real log files
15 our %aradial2db = (
16   #'Date' => '',
17   #'NASIP' => 'NASIPAddress',
18   'NASID' => 'NASIPAddress',
19   'AcctSessionId' => 'AcctSessionId',
20   'Port' => 'NasPortId',
21   #'Status-Type' => 'Acct-Status-Type',
22   #'UserID' => 'UserName',
23   'User ID' => 'UserName',
24   'Authentic' => 'AcctAuthentic',
25   'Service-Type' => 'ServiceType',
26   'FramedProtocol' => 'FramedProtocol',
27   #'FramedCompression' => '', #not handled, needed?  unlikely
28   'FramedAddress' => 'FramedIPAddress',
29   'Acct-Delay-Time' => 'AcctStartDelay', #?
30   'Session-Time' => 'AcctSessionTime',
31   #'Input-Gigawords' => '', #XXX handle lots of data
32   'Input-Octets' => 'AcctInputOctets',
33   #'Output-Gigawords' => '', #XXX handle lots of data
34   'Output-Octets' => 'AcctOutputOctets',
35   'NAS-Port-Type' => 'NASPortType',
36   'Acct-Terminate-Cause' => 'AcctTerminateCause',
37 );
38
39 our %bind_type = (
40   'AcctInputOctets'  => SQL_INTEGER,
41   'AcctOutputOctets' => SQL_INTEGER,
42   'AcctSessionTime'  => SQL_INTEGER,
43   'AcctStartDelay'   => SQL_INTEGER,
44   'AcctStopDelay'    => SQL_INTEGER,
45 );
46
47 #http://www.iana.org/assignments/radius-types/radius-types.xhtml#radius-types-10
48 our %status_type = (
49    1 => 'Start',
50    2 => 'Stop',
51    3 => 'Interim-Update',
52   #4-6,'Unassigned',
53    7 => 'Accounting-On',
54    8 => 'Accounting-Off',
55    9 => 'Tunnel-Start',
56   10 => 'Tunnel-Stop',
57   11 => 'Tunnel-Reject',
58   12 => 'Tunnel-Link-Start',
59   13 => 'Tunnel-Link-Stop',
60   14 => 'Tunnel-Link-Reject',
61   15 => 'Failed',
62 );
63
64 ###
65 # parse command line
66 ###
67
68 use vars qw( $opt_m $opt_a $opt_b $opt_r $opt_d $opt_v $opt_P );
69 getopts('m:abr:d:P:v:');
70
71 my %options = ();
72
73 my $user = shift or die &usage;
74 #adminsuidsetup $user;
75
76 # %%%FREESIDE_CACHE%%% & hardcoded datasrc
77 #my $cachedir = '%%%FREESIDE_CACHE%%%/cache.'. datasrc. '/cdrs';
78 my $cachedir = '/usr/local/etc/freeside/cache.DBI:Pg:dbname=freeside/cdrs';
79 mkdir $cachedir unless -d $cachedir;
80
81 my $servername = shift or die &usage;
82
83 my( $datasrc, $db_user, $db_pass ) = ( shift, shift, shift );
84 my $dbh = DBI->connect( $datasrc, $db_user, $db_pass)
85   or die "can't connect: $DBI::errstr\n";
86
87 my $csv = Text::CSV_XS->new;
88
89 ###
90 # get the file list
91 ###
92
93 warn "Retrieving directory listing\n" if $opt_v;
94
95 $opt_m = 'sftp' if !defined($opt_m);
96 $opt_m = lc($opt_m);
97
98 my $ls;
99
100 if($opt_m eq 'ftp') {
101   $options{'Port'}    = $opt_P if $opt_P;
102   $options{'Debug'}   = $opt_v if $opt_v;
103   $options{'Passive'} = $opt_a if $opt_a;
104
105   my $ls_ftp = ftp();
106
107   $ls = [ grep { /^.*$/i } $ls_ftp->ls ];
108 }
109 elsif($opt_m eq 'sftp') {
110   $options{'port'}    = $opt_P if $opt_P;
111   $options{'debug'}   = $opt_v if $opt_v;
112
113   my $ls_sftp = sftp();
114
115   $ls_sftp->setcwd($opt_r) or die "can't chdir to $opt_r\n"
116     if $opt_r;
117
118   $ls = $ls_sftp->ls('.', no_wanted  => qr/^\.+$/,
119                           names_only => 1 );
120 }
121 else {
122   die "Method '$opt_m' not supported; must be ftp or sftp\n";
123 }
124
125 ###
126 # import each file
127 ###
128
129 foreach my $filename ( @$ls ) {
130
131   next if $opt_d && $filename eq $opt_d;
132
133   warn "Downloading $filename\n" if $opt_v;
134
135   #get the file
136   if($opt_m eq 'ftp') {
137     my $ftp = ftp();
138     $ftp->get($filename, "$cachedir/$filename")
139       or die "Can't get $filename: ". $ftp->message . "\n";
140   }
141   else {
142     my $sftp = sftp();
143     $sftp->get($filename, "$cachedir/$filename")
144       or die "Can't get $filename: ". $sftp->error . "\n";
145   }
146
147   warn "Processing $filename\n" if $opt_v;
148  
149   open my $fh, "$cachedir/$filename" or die "$cachedir/$filename: $!";
150   my $header = $csv->getline($fh);
151
152   while ( my $row = $csv->getline($fh) ) {
153
154     my $i = 0;
155     my %hash = map { $_ => $row->[$i++] } @$header;
156
157     my %dbhash = map { $aradial2db{$_} => $hash{$_} }
158                    grep $aradial2db{$_},
159                      keys %hash;
160
161     my @keys = keys %dbhash;
162
163     #skip blank records
164     next unless grep defined($_), values %dbhash;
165
166     my $date = time2str( '%Y-%m-%d %X', str2time( $hash{'Date'} ) );
167
168     $hash{'Status-Type'} = $status_type{ $hash{'Status-Type'} }
169       if exists $status_type{ $hash{'Status-Type'} };
170
171     my $sql;
172     my @extra_values = ();
173     if ( $hash{'Status-Type'} eq 'Start' ) {
174
175       push @keys, 'AcctStartTime';
176       $dbhash{'AcctStartTime'} = $date;
177
178       $sql = 'INSERT INTO radacct ( '. join(',', @keys).
179              ' ) VALUES ( '. join(',', map ' ? ', @keys ). ' )';
180
181     } elsif ( $hash{'Status-Type'} eq 'Stop' ) {
182
183       my $AcctSessionId = delete($dbhash{AcctSessionId});
184
185       push @keys, 'AcctStopTime';
186       $dbhash{'AcctStopTime'} = $date;
187
188       push @extra_values, $AcctSessionId;
189
190       $sql = 'UPDATE radacct SET '. join(',', map "$_ = ?", @keys ).
191              ' WHERE AcctSessionId = ? ';
192
193     } elsif ( $hash{'Status-Type'} eq 'Interim' ) {
194       #not handled, but stop should capture the usage.  unless session are
195       # normally super-long, extending across month boundaries, or we need
196       # real-time-ish data usage detail, it isn't a big deal
197     } else {
198       warn 'Unknown Status-Type '. $hash{'Status-Type'}. "; skipping\n";
199       next;
200     }
201
202     my $sth = $dbh->prepare($sql) or die $dbh->errstr;
203
204     my $p_num = 1;
205     foreach my $value ( map $dbhash{$_}, @keys ) {
206       my $key = shift @keys;
207       my $type = exists($bind_type{$key}) ? $bind_type{$key} : SQL_VARCHAR;
208       $value ||= 0 if $type == SQL_INTEGER;
209       $sth->bind_param($p_num++, $value, $type);
210     }
211     foreach my $value ( @extra_values ) {
212       $sth->bind_param($p_num++, $value);
213     }
214
215     $sth->execute or die $sth->errstr;
216
217   }
218   
219   if ( $opt_d ) {
220     my $file_timestamp = $filename.'-'.time2str('%Y-%m-%d', time);
221     if ( $opt_m eq 'ftp') {
222       my $ftp = ftp();
223       $ftp->rename($filename, "$opt_d/$file_timestamp")
224         or do {
225           unlink "$cachedir/$filename";
226           die "Can't move $filename to $opt_d: ".$ftp->message . "\n";
227         };
228     } else {
229       my $sftp = sftp();
230       $sftp->rename($filename, "$opt_d/$file_timestamp")
231         or do {
232           unlink "$cachedir/$filename";
233           die "can't move $filename to $opt_d: ". $sftp->error . "\n";
234         };
235     }
236   }
237
238   unlink "$cachedir/$filename";
239
240 }
241
242 ###
243 # subs
244 ###
245
246 sub usage {
247   "Usage:
248   aradial-sftp_and_import [ -m method ] [ -a ] [ -b ]
249     [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ]
250     user [sftpuser@]servername dbi_datasrc dbi_username dbi_pass
251   ";
252 }
253
254 use vars qw( $sftp $ftp );
255
256 sub ftp {
257   return $ftp if $ftp && $ftp->pwd;
258   
259   my ($hostname, $userpass) = reverse split('@', $servername);
260   my ($ftp_user, $ftp_pass) = split(':', $userpass);
261
262   my $ftp = Net::FTP->new($hostname, %options) 
263     or die "FTP connection to '$hostname' failed.";
264   $ftp->login($ftp_user, $ftp_pass) or die "FTP login failed: ".$ftp->message;
265   $ftp->cwd($opt_r) or die "can't chdir to $opt_r\n" if $opt_r;
266   $ftp->binary or die "can't set BINARY mode: ". $ftp->message if $opt_b;
267   return $ftp;
268 }
269
270 sub sftp {
271
272   #reuse connections
273   return $sftp if $sftp && $sftp->cwd;
274
275   my %sftp = ( host => $servername );
276
277   $sftp = Net::SFTP::Foreign->new(%sftp);
278   $sftp->error and die "SFTP connection failed: ". $sftp->error;
279
280   $sftp;
281 }
282
283 =head1 NAME
284
285 freeside-aradial-sftp_and_import - Download Aradial "CDR" (really RADIUS detail) files from a remote server via SFTP
286
287 =head1 SYNOPSIS
288
289   aradial-sftp_and_import [ -m method ] [ -a ] [ -b ]
290     [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ]
291     user [sftpuser@]servername dbi_datasrc dbi_username dbi_pass
292
293 =head1 DESCRIPTION
294
295 Command line tool to download CDR files from a remote server via SFTP 
296 or FTP and then import them into the database.
297
298 -m: transfer method (sftp or ftp), defaults to sftp
299
300 -a: use ftp passive mode
301
302 -b: use ftp binary mode
303
304 -r: if specified, changes into this remote folder before starting
305
306 -d: if specified, moves files to the specified folder when done
307
308 -P: if specified, sets the port to use
309
310 -v: set verbosity level; this script only has one level, but it will 
311     be passed as the 'debug' argument to the transport method
312
313 user: freeside username
314
315 [sftpuser@]servername: remote server
316 (or ftpuser:ftppass@servername)
317
318 =head1 BUGS
319
320 =head1 SEE ALSO
321
322 L<FS::cdr>
323
324 =cut
325
326 1;
327