SIM/KI inventory for Huawei HLR export, #21514
[freeside.git] / FS / FS / part_export / huawei_hlr.pm
1 package FS::part_export::huawei_hlr;
2
3 use vars qw(@ISA %info $DEBUG $CACHE);
4 use Tie::IxHash;
5 use FS::Record qw(qsearch qsearchs dbh);
6 use FS::part_export;
7 use FS::svc_phone;
8 use FS::inventory_class;
9 use FS::inventory_item;
10 use IO::Socket::INET;
11 use Data::Dumper;
12 use MIME::Base64 qw(decode_base64);
13 use Storable qw(thaw);
14
15 use strict;
16
17 $DEBUG = 0;
18 @ISA = qw(FS::part_export);
19
20 tie my %options, 'Tie::IxHash',
21   'opname'    => { label=>'Operator login' },
22   'pwd'       => { label=>'Operator password' },
23   'tplid'     => { label=>'Template number' },
24   'hlrsn'     => { label=>'HLR serial number' },
25   'k4sno'     => { label=>'K4 serial number' },
26   'cardtype'  => { label  => 'Card type',
27                    type   => 'select', 
28                    options=> ['SIM', 'USIM']
29                  },
30   'alg'       => { label  => 'Authentication algorithm',
31                    type   => 'select',
32                    options=> ['COMP128_1',
33                               'COMP128_2',
34                               'COMP128_3',
35                               'MILENAGE' ],
36                  },
37   'opcvalue'  => { label=>'OPC value (for MILENAGE only)' },
38   'opsno'     => { label=>'OP serial number (for MILENAGE only)' },
39   'timeout'   => { label=>'Timeout (seconds)', default => 120 },
40   'debug'     => { label=>'Enable debugging', type=>'checkbox' },
41 ;
42
43 %info = (
44   'svc'     => 'svc_phone',
45   'desc'    => 'Provision mobile phone service to Huawei HLR9820',
46   'options' => \%options,
47   'notes'   => <<'END'
48 Connects to a Huawei Subscriber Management Unit via TCP and configures mobile
49 phone services according to a template.  The <i>sim_imsi</i> field must be 
50 set on the service, and the template must exist.
51 END
52 );
53
54 sub actions {
55   'Import SIMs' => 'misc/part_export/huawei_hlr-import_sim.html'
56 }
57
58 sub _export_insert {
59   my( $self, $svc_phone ) = (shift, shift);
60   # svc_phone::check should ensure phonenum and sim_imsi are numeric
61   my @command = (
62     IMSI   => '"'.$svc_phone->sim_imsi.'"',
63     ISDN   => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
64     TPLID  => $self->option('tplid'),
65   );
66   unshift @command, 'HLRSN', $self->option('hlrsn')
67     if $self->option('hlrsn');
68   unshift @command, 'ADD TPLSUB';
69   my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
70   ref($err_or_queue) ? '' : $err_or_queue;
71 }
72
73 sub _export_replace  {
74   my( $self, $new, $old ) = @_;
75   my $depend_jobnum;
76   if ( $new->sim_imsi ne $old->sim_imsi ) {
77     my @command = (
78       'MOD IMSI',
79       ISDN    => '"'.$old->countrycode.$old->phonenum.'"',
80       IMSI    => '"'.$old->sim_imsi.'"',
81       NEWIMSI => '"'.$new->sim_imsi.'"',
82     );
83     my $err_or_queue = $self->queue_command($new->svcnum, @command);
84     return $err_or_queue unless ref $err_or_queue;
85     $depend_jobnum = $err_or_queue->jobnum;
86   }
87   if ( $new->countrycode ne $old->countrycode or 
88        $new->phonenum ne $old->phonenum ) {
89     my @command = (
90       'MOD ISDN',
91       ISDN    => '"'.$old->countrycode.$old->phonenum.'"',
92       NEWISDN => '"'.$new->countrycode.$new->phonenum.'"',
93     );
94     my $err_or_queue = $self->queue_command($new->svcnum, @command);
95     return $err_or_queue unless ref $err_or_queue;
96     if ( $depend_jobnum ) {
97       my $error = $err_or_queue->depend_insert($depend_jobnum);
98       return $error if $error;
99     }
100   }
101   # no other svc_phone changes need to be exported
102   '';
103 }
104
105 sub _export_suspend {
106   my( $self, $svc_phone ) = (shift, shift);
107   $self->_export_lock($svc_phone, 'TRUE');
108 }
109
110 sub _export_unsuspend {
111   my( $self, $svc_phone ) = (shift, shift);
112   $self->_export_lock($svc_phone, 'FALSE');
113 }
114
115 sub _export_lock {
116   my ($self, $svc_phone, $lockstate) = @_;
117   # XXX I'm not sure this actually suspends.  Need to test it.
118   my @command = (
119     'MOD LCK',
120     IMSI    => '"'.$svc_phone->sim_imsi.'"',
121     ISDN    => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
122     IC      => $lockstate,
123     OC      => $lockstate,
124     GPRSLOCK=> $lockstate,
125   );
126   my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
127   ref($err_or_queue) ? '' : $err_or_queue;
128 }
129
130 sub _export_delete {
131   my( $self, $svc_phone ) = (shift, shift);
132   my @command = (
133     'RMV SUB',
134     #IMSI    => '"'.$svc_phone->sim_imsi.'"',
135     ISDN    => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
136   );
137   my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
138   ref($err_or_queue) ? '' : $err_or_queue;
139 }
140
141 sub queue_command {
142   my ($self, $svcnum, @command) = @_;
143   my $queue = FS::queue->new({
144       svcnum  => $svcnum,
145       job     => 'FS::part_export::huawei_hlr::run_command',
146   });
147   $queue->insert($self->exportnum, @command) || $queue;
148 }
149
150 sub run_command {
151   my ($exportnum, @command) = @_;
152   my $self = FS::part_export->by_key($exportnum);
153   my $socket = $self->login;
154   my $result = $self->command($socket, @command);
155   $self->logout($socket);
156   $socket->close;
157   die $result->{error} if $result->{error};
158   '';
159 }
160
161 sub login {
162   my $self = shift;
163   local $DEBUG = $self->option('debug') || 0;
164   # Send a command to the SMU.
165   # The caller is responsible for quoting string parameters.
166   my %socket_param = (
167     PeerAddr  => $self->machine,
168     PeerPort  => 7777,
169     Proto     => 'tcp',
170     Timeout   => ($self->option('timeout') || 30),
171   );
172   warn "Connecting to ".$self->machine."...\n" if $DEBUG;
173   warn Dumper(\%socket_param) if $DEBUG;
174   my $socket = IO::Socket::INET->new(%socket_param)
175     or die "Failed to connect: $!\n";
176
177   warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG;
178   my @login_param = (
179     OPNAME => '"'.$self->option('opname').'"',
180     PWD    => '"'.$self->option('pwd').'"',
181   );
182   if ($self->option('HLRSN')) {
183     unshift @login_param, 'HLRSN', $self->option('HLRSN');
184   }
185   my $login_result = $self->command($socket, 'LGI', @login_param);
186   die $login_result->{error} if $login_result->{error};
187   return $socket;
188 }
189
190 sub logout {
191   warn "Logging out.\n" if $DEBUG;
192   my $self = shift;
193   my ($socket) = @_;
194   $self->command($socket, 'LGO');
195   $socket->close;
196 }
197
198 sub command {
199   my $self = shift;
200   my ($socket, $command, @param) = @_;
201   my $string = $command . ':';
202   while (@param) {
203     $string .= shift(@param) . '=' . shift(@param);
204     $string .= ',' if @param;
205   }
206   $string .= "\n;";
207   my @result;
208   eval { # timeout
209     local $SIG{ALRM} = sub { die "timeout\n" };
210     alarm ($self->option('timeout') || 120);
211     warn "Sending to server:\n$string\n\n" if $DEBUG;
212     $socket->print($string);
213     warn "Received:\n";
214     my $line;
215     local $/ = "\r\n";
216     do {
217       $line = $socket->getline();
218       warn $line if $DEBUG;
219       chomp $line;
220       push @result, $line if length($line);
221     } until ( $line =~ /^---\s*END$/ or $socket->eof );
222     alarm 0;
223   };
224   my %return;
225   if ( $@ eq "timeout\n" ) {
226     return { error => 'request timed out' };
227   } elsif ( $@ ) {
228     return { error => $@ };
229   } else {
230     #+++    HLR9820        <date> <time>\n
231     my $header = shift(@result);
232     $header =~ /(\+\+\+.*)/
233       or return { error => 'malformed response: '.$header };
234     $return{header} = $1;
235     #SMU    #<serial number>\n
236     $return{smu} = shift(@result);
237     #%%<command string>%%\n 
238     $return{echo} = shift(@result); # should match the input
239     #<message code>: <message description>\n
240     my $message = shift(@result);
241     if ($message =~ /^SUCCESS/) {
242       $return{success} = $message;
243     } else { #/^ERR/
244       $return{error} = $message;
245     }
246     $return{trailer} = pop(@result);
247     $return{details} = join("\n",@result,'');
248   }
249   \%return;
250 }
251
252 sub process_import_sim {
253   my $job = shift;
254   my $param = thaw(decode_base64(shift));
255   $param->{'job'} = $job;
256   my $exportnum = delete $param->{'exportnum'};
257   my $export = __PACKAGE__->by_key($exportnum);
258   my $file = delete $param->{'uploaded_files'};
259   $file =~ s/^file://;
260   my $dir = $FS::UID::cache_dir .'/cache.'. $FS::UID::datasrc;
261   open( $param->{'filehandle'}, '<', "$dir/$file" )
262     or die "unable to open '$file'.\n";
263   my $error = $export->import_sim($param);
264 }
265
266 sub import_sim {
267   # import a SIM list
268   local $FS::UID::AutoCommit = 1; # yes, 1
269   my $self = shift;
270   my $param = shift;
271   my $job = $param->{'job'};
272   my $fh = $param->{'filehandle'};
273   my @lines = $fh->getlines;
274
275   my @command = 'ADD KI';
276   push @command, ('HLRSN', $self->option('hlrsn')) if $self->option('hlrsn');
277
278   my @args = ('OPERTYPE', 'ADD');
279   push @args, ('K4SNO', $self->option('k4sno')) if $self->option('k4sno');
280   push @args, ('CARDTYPE', $self->option('cardtype'),
281                'ALG',      $self->option('alg'));
282   push @args, ('OPCVALUE', $self->option('opcvalue'),
283                'OPSNO',    $self->option('opsno'))
284     if $self->option('alg') eq 'MILENAGE';
285
286   my $agentnum = $param->{'agentnum'};
287   my $classnum = $param->{'classnum'};
288   my $class = FS::inventory_class->by_key($classnum)
289     or die "bad inventory class $classnum\n";
290   my %existing = map { $_->item, 1 } 
291     qsearch('inventory_item', { 'classnum' => $classnum });
292
293   my $socket = $self->login;
294   my $num=0;
295   my $total = scalar(@lines);
296   foreach my $line (@lines) {
297     $num++;
298     $job->update_statustext(int(100*$num/$total).',Provisioning IMSIs...')
299       if $job;
300
301     chomp $line;
302     my ($imsi, $iccid, $pin1, $puk1, $pin2, $puk2, $acc, $ki) = 
303       split(' ', $line);
304     # the only fields we really care about are the IMSI and KI.
305     if ($imsi !~ /^\d{15}$/ or $ki !~ /^[0-9A-Z]{32}$/) {
306       warn "misspelled line in SIM file: $line\n";
307       next;
308     }
309     if ($existing{$imsi}) {
310       warn "IMSI $imsi already in inventory, skipped\n";
311       next;
312     }
313
314     # push IMSI/KI to the HLR
315     my $return = $self->command($socket,
316       @command,
317       'IMSI', $imsi,
318       'KIVALUE', $ki,
319       @args
320     );
321     if ( $return->{success} ) {
322       # add to inventory
323       my $item = FS::inventory_item->new({
324           'classnum'  => $classnum,
325           'agentnum'  => $agentnum,
326           'item'      => $imsi,
327       });
328       my $error = $item->insert;
329       if ( $error ) {
330         die "IMSI $imsi added to HLR, but not to inventory:\n$error\n";
331       }
332     } else {
333       die "IMSI $imsi could not be added to HLR:\n".$return->{error}."\n";
334     }
335   } #foreach $line
336   $self->logout($socket);
337   return;
338 }
339
340 1;