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