svc_broadband torrus, RT#14133
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2 use base qw(
3   FS::svc_Radius_Mixin
4   FS::svc_Tower_Mixin
5   FS::svc_Torrus_Mixin
6   FS::svc_IP_Mixin 
7   FS::svc_MAC_Mixin
8   FS::svc_Common
9 );
10
11 use strict;
12 use vars qw($conf);
13
14 { no warnings 'redefine'; use NetAddr::IP; }
15 use FS::Record qw( qsearchs qsearch dbh );
16 use FS::cust_svc;
17 use FS::addr_block;
18 use FS::part_svc_router;
19 use FS::tower_sector;
20
21 $FS::UID::callback{'FS::svc_broadband'} = sub { 
22   $conf = new FS::Conf;
23 };
24
25 =head1 NAME
26
27 FS::svc_broadband - Object methods for svc_broadband records
28
29 =head1 SYNOPSIS
30
31   use FS::svc_broadband;
32
33   $record = new FS::svc_broadband \%hash;
34   $record = new FS::svc_broadband { 'column' => 'value' };
35
36   $error = $record->insert;
37
38   $error = $new_record->replace($old_record);
39
40   $error = $record->delete;
41
42   $error = $record->check;
43
44   $error = $record->suspend;
45
46   $error = $record->unsuspend;
47
48   $error = $record->cancel;
49
50 =head1 DESCRIPTION
51
52 An FS::svc_broadband object represents a 'broadband' Internet connection, such
53 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
54 have the following properties:
55
56 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
57 currently supported:
58
59 =over 4
60
61 =item svcnum - primary key
62
63 =item blocknum - see FS::addr_block
64
65 =item
66 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
67 speed will be unlimited.  Exports that do traffic shaping should handle this
68 correctly, and not blindly set the upload speed to zero and kill the customer's
69 connection.
70
71 =item
72 speed_down - maximum download speed, as above
73
74 =item ip_addr - the customer's IP address.  If the customer needs more than one
75 IP address, set this to the address of the customer's router.  As a result, the
76 customer's router will have the same address for both its internal and external
77 interfaces thus saving address space.  This has been found to work on most NAT
78 routers available.
79
80 =item plan_id
81
82 =back
83
84 =head1 METHODS
85
86 =over 4
87
88 =item new HASHREF
89
90 Creates a new svc_broadband.  To add the record to the database, see
91 "insert".
92
93 Note that this stores the hash reference, not a distinct copy of the hash it
94 points to.  You can ask the object for a copy with the I<hash> method.
95
96 =cut
97
98 sub table_info {
99   {
100     'name' => 'Wireless broadband',
101     'name_plural' => 'Wireless broadband services',
102     'longname_plural' => 'Fixed wireless broadband services',
103     'display_weight' => 50,
104     'cancel_weight'  => 70,
105     'ip_field' => 'ip_addr',
106     'fields' => {
107       'svcnum'      => 'Service',
108       'description' => 'Descriptive label',
109       'speed_down'  => 'Download speed (Kbps)',
110       'speed_up'    => 'Upload speed (Kbps)',
111       'ip_addr'     => 'IP address',
112       'blocknum'    => 
113       { 'label' => 'Address block',
114                          'type'  => 'select',
115                          'select_table' => 'addr_block',
116                           'select_key'   => 'blocknum',
117                          'select_label' => 'cidr',
118                          'disable_inventory' => 1,
119                        },
120      'plan_id' => 'Service Plan Id',
121      'performance_profile' => 'Peformance Profile',
122      'authkey'      => 'Authentication key',
123      'mac_addr'     => 'MAC address',
124      'latitude'     => 'Latitude',
125      'longitude'    => 'Longitude',
126      'altitude'     => 'Altitude',
127      'vlan_profile' => 'VLAN profile',
128      'sectornum'    => 'Tower/sector',
129      'routernum'    => 'Router/block',
130      'usergroup'    => { 
131                          label => 'RADIUS groups',
132                          type  => 'select-radius_group.html',
133                          #select_table => 'radius_group',
134                          #select_key   => 'groupnum',
135                          #select_label => 'groupname',
136                          disable_inventory => 1,
137                          multiple => 1,
138                        },
139       'radio_serialnum' => 'Radio Serial Number',
140       'radio_location'  => 'Radio Location',
141       'poe_location'    => 'POE Location',
142       'rssi'            => 'RSSI',
143       'suid'            => 'SUID',
144       'shared_svcnum'   => { label             => 'Shared Service',
145                              type              => 'search-svc_broadband',
146                              disable_inventory => 1,
147                            },
148       'serviceid' => 'Torrus serviceid', #but is should be hidden
149     },
150   };
151 }
152
153 sub table { 'svc_broadband'; }
154
155 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
156
157 =item search HASHREF
158
159 Class method which returns a qsearch hash expression to search for parameters
160 specified in HASHREF.
161
162 Parameters:
163
164 =over 4
165
166 =item unlinked - set to search for all unlinked services.  Overrides all other options.
167
168 =item agentnum
169
170 =item custnum
171
172 =item svcpart
173
174 =item ip_addr
175
176 =item pkgpart - arrayref
177
178 =item routernum - arrayref
179
180 =item sectornum - arrayref
181
182 =item towernum - arrayref
183
184 =item order_by
185
186 =back
187
188 =cut
189
190 sub _search_svc {
191   my( $class, $params, $from, $where ) = @_;
192
193   #routernum, can be arrayref
194   for my $routernum ( $params->{'routernum'} ) {
195     # this no longer uses addr_block
196     if ( ref $routernum and grep { $_ } @$routernum ) {
197       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
198       my @orwhere = ();
199       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
200       push @orwhere, "svc_broadband.routernum IS NULL" 
201         if grep /^none$/, @$routernum;
202       push @$where, '( '.join(' OR ', @orwhere).' )';
203     }
204     elsif ( $routernum =~ /^(\d+)$/ ) {
205       push @$where, "svc_broadband.routernum = $1";
206     }
207     elsif ( $routernum eq 'none' ) {
208       push @$where, "svc_broadband.routernum IS NULL";
209     }
210   }
211
212   #this should probably move to svc_Tower_Mixin, or maybe we never should have
213   # done svc_acct # towers (or, as mark thought, never should have done
214   # svc_broadband)
215
216   #sector and tower, as above
217   my @where_sector = $class->tower_sector_sql($params);
218   if ( @where_sector ) {
219     push @$where, @where_sector;
220     push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
221   }
222  
223   #ip_addr
224   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
225     push @$where, "ip_addr = '$1'";
226   }
227
228 }
229
230 =item search_sql STRING
231
232 Class method which returns an SQL fragment to search for the given string.
233
234 =cut
235
236 sub search_sql {
237   my( $class, $string ) = @_;
238   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
239     $class->search_sql_field('ip_addr', $string );
240   } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
241     $class->search_sql_field('mac_addr', uc($string));
242   } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
243     $string =~ s/://g;
244     $class->search_sql_field('mac_addr', uc($string) );
245   } elsif ( $string =~ /^(\d+)$/ ) {
246     my $table = $class->table;
247     "$table.svcnum = $1";
248   } else {
249     '1 = 0'; #false
250   }
251 }
252
253 =item smart_search STRING
254
255 =cut
256
257 sub smart_search {
258   my( $class, $string ) = @_;
259   qsearch({
260     'table'     => $class->table, #'svc_broadband',
261     'hashref'   => {},
262     'extra_sql' => 'WHERE '. $class->search_sql($string),
263   });
264 }
265
266 =item label
267
268 Returns the IP address, MAC address and description.
269
270 =cut
271
272 sub label {
273   my $self = shift;
274   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
275   $label .= ', MAC:'. $self->mac_addr
276     if $self->mac_addr;
277   $label .= ' ('. $self->description. ')'
278     if $self->description;
279   return $label;
280 }
281
282 =item insert [ , OPTION => VALUE ... ]
283
284 Adds this record to the database.  If there is an error, returns the error,
285 otherwise returns false.
286
287 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
288 defined.  An FS::cust_svc record will be created and inserted.
289
290 Currently available options are: I<depend_jobnum>
291
292 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
293 jobnums), all provisioning jobs will have a dependancy on the supplied
294 jobnum(s) (they will not run until the specific job(s) complete(s)).
295
296 =cut
297
298 # Standard FS::svc_Common::insert
299
300 =item delete
301
302 Delete this record from the database.
303
304 =cut
305
306 # Standard FS::svc_Common::delete
307
308 =item replace OLD_RECORD
309
310 Replaces the OLD_RECORD with this one in the database.  If there is an error,
311 returns the error, otherwise returns false.
312
313 # Standard FS::svc_Common::replace
314
315 =item suspend
316
317 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
318
319 =item unsuspend
320
321 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
322
323 =item cancel
324
325 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
326
327 =item check
328
329 Checks all fields to make sure this is a valid broadband service.  If there is
330 an error, returns the error, otherwise returns false.  Called by the insert
331 and replace methods.
332
333 =cut
334
335 sub check {
336   my $self = shift;
337   my $x = $self->setfixed;
338
339   return $x unless ref($x);
340
341   # remove delimiters
342   my $mac_addr = uc($self->get('mac_addr'));
343   $mac_addr =~ s/[\W_]//g;
344   $self->set('mac_addr', $mac_addr);
345
346   my $error =
347     $self->ut_numbern('svcnum')
348     || $self->ut_numbern('blocknum')
349     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
350     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
351     || $self->ut_textn('description')
352     || $self->ut_numbern('speed_up')
353     || $self->ut_numbern('speed_down')
354     || $self->ut_ipn('ip_addr')
355     || $self->ut_hexn('mac_addr')
356     || $self->ut_hexn('auth_key')
357     || $self->ut_coordn('latitude')
358     || $self->ut_coordn('longitude')
359     || $self->ut_sfloatn('altitude')
360     || $self->ut_textn('vlan_profile')
361     || $self->ut_textn('plan_id')
362     || $self->ut_alphan('radio_serialnum')
363     || $self->ut_textn('radio_location')
364     || $self->ut_textn('poe_location')
365     || $self->ut_snumbern('rssi')
366     || $self->ut_numbern('suid')
367     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
368     || $self->ut_textn('serviceid') #too lenient?
369   ;
370   return $error if $error;
371
372   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
373   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
374
375   my $cust_svc = $self->svcnum
376                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
377                  : '';
378   my $cust_pkg;
379   my $svcpart;
380   if ($cust_svc) {
381     $cust_pkg = $cust_svc->cust_pkg;
382     $svcpart = $cust_svc->svcpart;
383   }else{
384     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
385     return "Invalid pkgnum" unless $cust_pkg;
386     $svcpart = $self->svcpart;
387   }
388   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
389
390   # assign IP address / router / block
391   $error = $self->svc_ip_check;
392   return $error if $error;
393   if ( !$self->ip_addr 
394        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
395     return 'IP address is required';
396   }
397
398   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
399     my $l = $cust_pkg->cust_location_or_main;
400     if ( $l->ship_latitude && $l->ship_longitude ) {
401       $self->latitude(  $l->ship_latitude  );
402       $self->longitude( $l->ship_longitude );
403     } elsif ( $l->latitude && $l->longitude ) {
404       $self->latitude(  $l->latitude  );
405       $self->longitude( $l->longitude );
406     }
407   }
408
409   $self->SUPER::check;
410 }
411
412 sub _check_duplicate {
413   my $self = shift;
414   # Not a reliable check because the table isn't locked, but 
415   # that's why we have a unique index.  This is just to give a
416   # friendlier error message.
417   my @dup;
418   @dup = $self->find_duplicates('global', 'mac_addr');
419   if ( @dup ) {
420     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
421   }
422
423   '';
424 }
425
426 #class method
427 sub _upgrade_data {
428   my $class = shift;
429
430   local($FS::svc_Common::noexport_hack) = 1;
431
432   # fix wrong-case MAC addresses
433   my $dbh = dbh;
434   $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
435     or die $dbh->errstr;
436
437   # set routernum to addr_block.routernum
438   foreach my $self (qsearch('svc_broadband', {
439       blocknum => {op => '!=', value => ''},
440       routernum => ''
441     })) {
442     my $addr_block = $self->addr_block;
443     if ( !$addr_block ) {
444       # super paranoid mode
445       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
446       next;
447     }
448     my $ip_addr = $self->ip_addr;
449     my $routernum = $addr_block->routernum;
450     if ( $routernum ) {
451       $self->set(routernum => $routernum);
452       my $error = $self->check;
453       # sanity check: don't allow this to change IP address or block
454       # (other than setting blocknum to null for a non-auto-assigned router)
455       if ( $self->ip_addr ne $ip_addr 
456         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
457         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
458         next;
459       }
460
461       $error ||= $self->replace;
462       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
463           ":\n$error; skipped\n"
464         if $error;
465     }
466     else {
467       warn "svcnum ".$self->svcnum.
468         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
469     }
470   }
471
472   # assign blocknums to services that should have them
473   my @all_blocks = qsearch('addr_block', { });
474   SVC: foreach my $self ( 
475     qsearch({
476         'select' => 'svc_broadband.*',
477         'table' => 'svc_broadband',
478         'addl_from' => 'JOIN router USING (routernum)',
479         'hashref' => {},
480         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
481                        'AND router.manual_addr IS NULL',
482     }) 
483   ) {
484    
485     next SVC if $self->ip_addr eq '';
486     my $NetAddr = $self->NetAddr;
487     # inefficient, but should only need to run once
488     foreach my $block (@all_blocks) {
489       if ($block->NetAddr->contains($NetAddr)) {
490         $self->set(blocknum => $block->blocknum);
491         my $error = $self->replace;
492         warn "WARNING: error assigning blocknum ".$block->blocknum.
493         " to service ".$self->svcnum."\n$error; skipped\n"
494           if $error;
495         next SVC;
496       }
497     }
498     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
499       $self->svcnum;
500     #next SVC;
501   }
502
503   '';
504 }
505
506 =back
507
508 =head1 BUGS
509
510 The business with sb_field has been 'fixed', in a manner of speaking.
511
512 allowed_routers isn't agent virtualized because part_svc isn't agent
513 virtualized
514
515 Having both routernum and blocknum as foreign keys is somewhat dubious.
516
517 =head1 SEE ALSO
518
519 FS::svc_Common, FS::Record, FS::addr_block,
520 FS::part_svc, schema.html from the base documentation.
521
522 =cut
523
524 1;
525