1 package FS::part_export::huawei_hlr;
3 use vars qw(@ISA %info $DEBUG $CACHE);
5 use FS::Record qw(qsearch qsearchs dbh);
8 use FS::inventory_class;
9 use FS::inventory_item;
16 @ISA = qw(FS::part_export);
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)',
26 options=> ['SIM', 'USIM']
28 'alg' => { label => 'Authentication algorithm (required)',
30 options=> ['COMP128_1',
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' },
43 'desc' => 'Provision mobile phone service to Huawei HLR9820',
44 'options' => \%options,
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.
53 'Import SIMs' => 'misc/part_export/huawei_hlr-import_sim.html'
57 my( $self, $svc_phone ) = (shift, shift);
58 # svc_phone::check should ensure phonenum and sim_imsi are numeric
60 IMSI => '"'.$svc_phone->sim_imsi.'"',
61 ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
62 TPLID => $self->option('tplid'),
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;
72 my( $self, $new, $old ) = @_;
74 if ( $new->sim_imsi ne $old->sim_imsi ) {
77 ISDN => '"'.$old->countrycode.$old->phonenum.'"',
78 IMSI => '"'.$old->sim_imsi.'"',
79 NEWIMSI => '"'.$new->sim_imsi.'"',
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;
85 if ( $new->countrycode ne $old->countrycode or
86 $new->phonenum ne $old->phonenum ) {
89 ISDN => '"'.$old->countrycode.$old->phonenum.'"',
90 NEWISDN => '"'.$new->countrycode.$new->phonenum.'"',
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;
99 # no other svc_phone changes need to be exported
103 sub _export_suspend {
104 my( $self, $svc_phone ) = (shift, shift);
105 $self->_export_lock($svc_phone, 'TRUE');
108 sub _export_unsuspend {
109 my( $self, $svc_phone ) = (shift, shift);
110 $self->_export_lock($svc_phone, 'FALSE');
114 my ($self, $svc_phone, $lockstate) = @_;
115 # XXX I'm not sure this actually suspends. Need to test it.
118 IMSI => '"'.$svc_phone->sim_imsi.'"',
119 ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
122 GPRSLOCK=> $lockstate,
124 my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
125 ref($err_or_queue) ? '' : $err_or_queue;
129 my( $self, $svc_phone ) = (shift, shift);
132 #IMSI => '"'.$svc_phone->sim_imsi.'"',
133 ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
135 my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
136 ref($err_or_queue) ? '' : $err_or_queue;
140 my ($self, $svcnum, @command) = @_;
141 my $queue = FS::queue->new({
143 job => 'FS::part_export::huawei_hlr::run_command',
145 $queue->insert($self->exportnum, @command) || $queue;
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);
155 die $result->{error} if $result->{error};
161 local $DEBUG = $self->option('debug') || 0;
162 # Send a command to the SMU.
163 # The caller is responsible for quoting string parameters.
165 PeerAddr => $self->machine,
168 Timeout => ($self->option('timeout') || 30),
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";
175 warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG;
177 OPNAME => '"'.$self->option('opname').'"',
178 PWD => '"'.$self->option('pwd').'"',
180 if ($self->option('HLRSN')) {
181 unshift @login_param, 'HLRSN', $self->option('HLRSN');
183 my $login_result = $self->command($socket, 'LGI', @login_param);
184 die $login_result->{error} if $login_result->{error};
189 warn "Logging out.\n" if $DEBUG;
192 $self->command($socket, 'LGO');
198 my ($socket, $command, @param) = @_;
199 my $string = $command . ':';
201 $string .= shift(@param) . '=' . shift(@param);
202 $string .= ',' if @param;
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);
215 $line = $socket->getline();
216 warn $line if $DEBUG;
218 push @result, $line if length($line);
219 } until ( $line =~ /^---\s*END$/ or $socket->eof );
223 if ( $@ eq "timeout\n" ) {
224 return { error => 'request timed out' };
226 return { error => $@ };
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;
242 $return{error} = $message;
244 $return{trailer} = pop(@result);
245 $return{details} = join("\n",@result,'');
250 sub process_import_sim {
253 $param->{'job'} = $job;
254 my $exportnum = delete $param->{'exportnum'};
255 my $export = __PACKAGE__->by_key($exportnum);
256 my $file = delete $param->{'uploaded_files'};
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);
266 local $FS::UID::AutoCommit = 1; # yes, 1
269 my $job = $param->{'job'};
270 my $fh = $param->{'filehandle'};
271 my @lines = $fh->getlines;
273 my @command = 'ADD KI';
274 push @command, ('HLRSN', $self->option('hlrsn')) if $self->option('hlrsn');
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';
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 });
291 my $socket = $self->login;
293 my $total = scalar(@lines);
294 foreach my $line (@lines) {
296 $job->update_statustext(int(100*$num/$total).',Provisioning IMSIs...')
300 my ($imsi, $iccid, $pin1, $puk1, $pin2, $puk2, $acc, $ki) =
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";
307 if ($existing{$imsi}) {
308 warn "IMSI $imsi already in inventory, skipped\n";
312 # push IMSI/KI to the HLR
313 my $return = $self->command($socket,
316 'KIVALUE', qq{"$ki"},
319 if ( $return->{success} ) {
321 my $item = FS::inventory_item->new({
322 'classnum' => $classnum,
323 'agentnum' => $agentnum,
326 my $error = $item->insert;
328 die "IMSI $imsi added to HLR, but not to inventory:\n$error\n";
331 die "IMSI $imsi could not be added to HLR:\n".$return->{error}."\n";
334 $self->logout($socket);