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