Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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::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     'manual_require' => 1,
107     'fields' => {
108       'svcnum'      => 'Service',
109       'description' => 'Descriptive label',
110       'speed_down'  => 'Download speed (Kbps)',
111       'speed_up'    => 'Upload speed (Kbps)',
112       'ip_addr'     => 'IP address',
113       'blocknum'    => {
114                          'label'             => 'Address block',
115                          'type'              => 'select',
116                          'select_table'      => 'addr_block',
117                           'select_key'       => 'blocknum',
118                          'select_label'      => 'cidr',
119                          'disable_inventory' => 1,
120                        },
121      'plan_id' => 'Service Plan Id',
122      'performance_profile' => 'Peformance Profile',
123      'authkey'      => 'Authentication key',
124      'mac_addr'     => 'MAC address',
125      'latitude'     => 'Latitude',
126      'longitude'    => 'Longitude',
127      'altitude'     => 'Altitude',
128      'vlan_profile' => 'VLAN profile',
129      'sectornum'    => 'Tower/sector',
130      'routernum'    => 'Router/block',
131      'usergroup'    => { 
132                          label => 'RADIUS groups',
133                          type  => 'select-radius_group.html',
134                          #select_table => 'radius_group',
135                          #select_key   => 'groupnum',
136                          #select_label => 'groupname',
137                          disable_select => 1,
138                          disable_inventory => 1,
139                          multiple => 1,
140                        },
141       'radio_serialnum' => 'Radio Serial Number',
142       'radio_location'  => 'Radio Location',
143       'poe_location'    => 'POE Location',
144       'rssi'            => 'RSSI',
145       'suid'            => 'SUID',
146       'shared_svcnum'   => { label             => 'Shared Service',
147                              type              => 'search-svc_broadband',
148                              disable_inventory => 1,
149                            },
150       'serviceid' => 'Torrus serviceid', #but is should be hidden
151     },
152   };
153 }
154
155 sub table { 'svc_broadband'; }
156
157 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
158
159 =item search HASHREF
160
161 Class method which returns a qsearch hash expression to search for parameters
162 specified in HASHREF.
163
164 Parameters:
165
166 =over 4
167
168 =item unlinked - set to search for all unlinked services.  Overrides all other options.
169
170 =item agentnum
171
172 =item custnum
173
174 =item svcpart
175
176 =item ip_addr
177
178 =item pkgpart - arrayref
179
180 =item routernum - arrayref
181
182 =item sectornum - arrayref
183
184 =item towernum - arrayref
185
186 =item order_by
187
188 =back
189
190 =cut
191
192 sub _search_svc {
193   my( $class, $params, $from, $where ) = @_;
194
195   #routernum, can be arrayref
196   for my $routernum ( $params->{'routernum'} ) {
197     # this no longer uses addr_block
198     if ( ref $routernum and grep { $_ } @$routernum ) {
199       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
200       my @orwhere = ();
201       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
202       push @orwhere, "svc_broadband.routernum IS NULL" 
203         if grep /^none$/, @$routernum;
204       push @$where, '( '.join(' OR ', @orwhere).' )';
205     }
206     elsif ( $routernum =~ /^(\d+)$/ ) {
207       push @$where, "svc_broadband.routernum = $1";
208     }
209     elsif ( $routernum eq 'none' ) {
210       push @$where, "svc_broadband.routernum IS NULL";
211     }
212   }
213
214   #this should probably move to svc_Tower_Mixin, or maybe we never should have
215   # done svc_acct # towers (or, as mark thought, never should have done
216   # svc_broadband)
217
218   #sector and tower, as above
219   my @where_sector = $class->tower_sector_sql($params);
220   if ( @where_sector ) {
221     push @$where, @where_sector;
222     push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
223   }
224  
225   #ip_addr
226   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
227     push @$where, "ip_addr = '$1'";
228   }
229
230 }
231
232 =item search_sql STRING
233
234 Class method which returns an SQL fragment to search for the given string.
235
236 =cut
237
238 sub search_sql {
239   my( $class, $string ) = @_;
240   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
241     $class->search_sql_field('ip_addr', $string );
242   } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
243     $class->search_sql_field('mac_addr', uc($string));
244   } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
245     $string =~ s/://g;
246     $class->search_sql_field('mac_addr', uc($string) );
247   } elsif ( $string =~ /^(\d+)$/ ) {
248     my $table = $class->table;
249     "$table.svcnum = $1";
250   } else {
251     '1 = 0'; #false
252   }
253 }
254
255 =item smart_search STRING
256
257 =cut
258
259 sub smart_search {
260   my( $class, $string ) = @_;
261   qsearch({
262     'table'     => $class->table, #'svc_broadband',
263     'hashref'   => {},
264     'extra_sql' => 'WHERE '. $class->search_sql($string),
265   });
266 }
267
268 =item label
269
270 Returns the IP address, MAC address and description.
271
272 =cut
273
274 sub label {
275   my $self = shift;
276   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
277   $label .= ', MAC:'. $self->mac_addr
278     if $self->mac_addr;
279   $label .= ' ('. $self->description. ')'
280     if $self->description;
281   return $label;
282 }
283
284 =item insert [ , OPTION => VALUE ... ]
285
286 Adds this record to the database.  If there is an error, returns the error,
287 otherwise returns false.
288
289 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
290 defined.  An FS::cust_svc record will be created and inserted.
291
292 Currently available options are: I<depend_jobnum>
293
294 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
295 jobnums), all provisioning jobs will have a dependancy on the supplied
296 jobnum(s) (they will not run until the specific job(s) complete(s)).
297
298 =cut
299
300 # Standard FS::svc_Common::insert
301
302 =item delete
303
304 Delete this record from the database.
305
306 =cut
307
308 # Standard FS::svc_Common::delete
309
310 =item replace OLD_RECORD
311
312 Replaces the OLD_RECORD with this one in the database.  If there is an error,
313 returns the error, otherwise returns false.
314
315 # Standard FS::svc_Common::replace
316
317 =item suspend
318
319 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
320
321 =item unsuspend
322
323 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
324
325 =item cancel
326
327 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
328
329 =item check
330
331 Checks all fields to make sure this is a valid broadband service.  If there is
332 an error, returns the error, otherwise returns false.  Called by the insert
333 and replace methods.
334
335 =cut
336
337 sub check {
338   my $self = shift;
339   my $x = $self->setfixed;
340
341   return $x unless ref($x);
342
343   # remove delimiters
344   my $mac_addr = uc($self->get('mac_addr'));
345   $mac_addr =~ s/[\W_]//g;
346   $self->set('mac_addr', $mac_addr);
347
348   my $error =
349     $self->ut_numbern('svcnum')
350     || $self->ut_numbern('blocknum')
351     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
352     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
353     || $self->ut_textn('description')
354     || $self->ut_numbern('speed_up')
355     || $self->ut_numbern('speed_down')
356     || $self->ut_ipn('ip_addr')
357     || $self->ut_hexn('mac_addr')
358     || $self->ut_hexn('auth_key')
359     || $self->ut_coordn('latitude')
360     || $self->ut_coordn('longitude')
361     || $self->ut_sfloatn('altitude')
362     || $self->ut_textn('vlan_profile')
363     || $self->ut_textn('plan_id')
364     || $self->ut_alphan('radio_serialnum')
365     || $self->ut_textn('radio_location')
366     || $self->ut_textn('poe_location')
367     || $self->ut_snumbern('rssi')
368     || $self->ut_numbern('suid')
369     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
370     || $self->ut_textn('serviceid') #too lenient?
371   ;
372   return $error if $error;
373
374   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
375   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
376
377   my $cust_svc = $self->svcnum
378                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
379                  : '';
380   my $cust_pkg;
381   my $svcpart;
382   if ($cust_svc) {
383     $cust_pkg = $cust_svc->cust_pkg;
384     $svcpart = $cust_svc->svcpart;
385   }else{
386     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
387     return "Invalid pkgnum" unless $cust_pkg;
388     $svcpart = $self->svcpart;
389   }
390   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
391
392   # assign IP address / router / block
393   $error = $self->svc_ip_check;
394   return $error if $error;
395   if ( !$self->ip_addr 
396        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
397     return 'IP address is required';
398   }
399
400   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
401     my $l = $cust_pkg->cust_location_or_main;
402     if ( $l->ship_latitude && $l->ship_longitude ) {
403       $self->latitude(  $l->ship_latitude  );
404       $self->longitude( $l->ship_longitude );
405     } elsif ( $l->latitude && $l->longitude ) {
406       $self->latitude(  $l->latitude  );
407       $self->longitude( $l->longitude );
408     }
409   }
410
411   $self->SUPER::check;
412 }
413
414 sub _check_duplicate {
415   my $self = shift;
416   # Not a reliable check because the table isn't locked, but 
417   # that's why we have a unique index.  This is just to give a
418   # friendlier error message.
419   my @dup;
420   @dup = $self->find_duplicates('global', 'mac_addr');
421   if ( @dup ) {
422     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
423   }
424
425   '';
426 }
427
428 #class method
429 sub _upgrade_data {
430   my $class = shift;
431
432   local($FS::svc_Common::noexport_hack) = 1;
433
434   # fix wrong-case MAC addresses
435   my $dbh = dbh;
436   $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
437     or die $dbh->errstr;
438
439   # set routernum to addr_block.routernum
440   foreach my $self (qsearch('svc_broadband', {
441       blocknum => {op => '!=', value => ''},
442       routernum => ''
443     })) {
444     my $addr_block = $self->addr_block;
445     if ( !$addr_block ) {
446       # super paranoid mode
447       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
448       next;
449     }
450     my $ip_addr = $self->ip_addr;
451     my $routernum = $addr_block->routernum;
452     if ( $routernum ) {
453       $self->set(routernum => $routernum);
454       my $error = $self->check;
455       # sanity check: don't allow this to change IP address or block
456       # (other than setting blocknum to null for a non-auto-assigned router)
457       if ( $self->ip_addr ne $ip_addr 
458         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
459         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
460         next;
461       }
462
463       $error ||= $self->replace;
464       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
465           ":\n$error; skipped\n"
466         if $error;
467     }
468     else {
469       warn "svcnum ".$self->svcnum.
470         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
471     }
472   }
473
474   # assign blocknums to services that should have them
475   my @all_blocks = qsearch('addr_block', { });
476   SVC: foreach my $self ( 
477     qsearch({
478         'select' => 'svc_broadband.*',
479         'table' => 'svc_broadband',
480         'addl_from' => 'JOIN router USING (routernum)',
481         'hashref' => {},
482         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
483                        'AND router.manual_addr IS NULL',
484     }) 
485   ) {
486    
487     next SVC if $self->ip_addr eq '';
488     my $NetAddr = $self->NetAddr;
489     # inefficient, but should only need to run once
490     foreach my $block (@all_blocks) {
491       if ($block->NetAddr->contains($NetAddr)) {
492         $self->set(blocknum => $block->blocknum);
493         my $error = $self->replace;
494         warn "WARNING: error assigning blocknum ".$block->blocknum.
495         " to service ".$self->svcnum."\n$error; skipped\n"
496           if $error;
497         next SVC;
498       }
499     }
500     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
501       $self->svcnum;
502     #next SVC;
503   }
504
505   require FS::Misc::FixIPFormat;
506   FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
507       'svc_broadband', 'svcnum', 'ip_addr',
508   );
509
510   '';
511 }
512
513 =back
514
515 =head1 BUGS
516
517 The business with sb_field has been 'fixed', in a manner of speaking.
518
519 allowed_routers isn't agent virtualized because part_svc isn't agent
520 virtualized
521
522 Having both routernum and blocknum as foreign keys is somewhat dubious.
523
524 =head1 SEE ALSO
525
526 FS::svc_Common, FS::Record, FS::addr_block,
527 FS::part_svc, schema.html from the base documentation.
528
529 =cut
530
531 1;