add svc_broadband shellcommands export, RT11250
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2
3 use strict;
4 use vars qw(@ISA $conf);
5 use FS::Record qw( qsearchs qsearch dbh );
6 use FS::svc_Common;
7 use FS::cust_svc;
8 use FS::addr_block;
9 use FS::part_svc_router;
10 use NetAddr::IP;
11
12 @ISA = qw( FS::svc_Common );
13
14 $FS::UID::callback{'FS::svc_broadband'} = sub { 
15   $conf = new FS::Conf;
16 };
17
18 =head1 NAME
19
20 FS::svc_broadband - Object methods for svc_broadband records
21
22 =head1 SYNOPSIS
23
24   use FS::svc_broadband;
25
26   $record = new FS::svc_broadband \%hash;
27   $record = new FS::svc_broadband { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37   $error = $record->suspend;
38
39   $error = $record->unsuspend;
40
41   $error = $record->cancel;
42
43 =head1 DESCRIPTION
44
45 An FS::svc_broadband object represents a 'broadband' Internet connection, such
46 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
47 have the following properties:
48
49 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
50 currently supported:
51
52 =over 4
53
54 =item svcnum - primary key
55
56 =item blocknum - see FS::addr_block
57
58 =item
59 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
60 speed will be unlimited.  Exports that do traffic shaping should handle this
61 correctly, and not blindly set the upload speed to zero and kill the customer's
62 connection.
63
64 =item
65 speed_down - maximum download speed, as above
66
67 =item ip_addr - the customer's IP address.  If the customer needs more than one
68 IP address, set this to the address of the customer's router.  As a result, the
69 customer's router will have the same address for both its internal and external
70 interfaces thus saving address space.  This has been found to work on most NAT
71 routers available.
72
73 =item plan_id
74
75 =back
76
77 =head1 METHODS
78
79 =over 4
80
81 =item new HASHREF
82
83 Creates a new svc_broadband.  To add the record to the database, see
84 "insert".
85
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to.  You can ask the object for a copy with the I<hash> method.
88
89 =cut
90
91 sub table_info {
92   {
93     'name' => 'Broadband',
94     'name_plural' => 'Broadband services',
95     'longname_plural' => 'Fixed (username-less) broadband services',
96     'display_weight' => 50,
97     'cancel_weight'  => 70,
98     'fields' => {
99       'description' => 'Descriptive label for this particular device.',
100       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
101       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
102       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
103       'blocknum'    => { 'label' => 'Address block',
104                          'type'  => 'select',
105                          'select_table' => 'addr_block',
106                          'select_key'   => 'blocknum',
107                          'select_label' => 'cidr',
108                          'disable_inventory' => 1,
109                        },
110      'plan_id' => 'Service Plan Id',
111      'performance_profile' => 'Peformance Profile',
112      'authkey'      => 'Authentication key',
113      'mac_addr'     => 'MAC address',
114      'latitude'     => 'Latitude',
115      'longitude'    => 'Longitude',
116      'altitude'     => 'Altitude',
117      'vlan_profile' => 'VLAN profile',
118     },
119   };
120 }
121
122 sub table { 'svc_broadband'; }
123
124 sub table_dupcheck_fields { ( 'mac_addr' ); }
125
126 =item search HASHREF
127
128 Class method which returns a qsearch hash expression to search for parameters
129 specified in HASHREF.
130
131 Parameters:
132
133 =over 4
134
135 =item unlinked - set to search for all unlinked services.  Overrides all other options.
136
137 =item agentnum
138
139 =item custnum
140
141 =item svcpart
142
143 =item ip_addr
144
145 =item pkgpart - arrayref
146
147 =item routernum - arrayref
148
149 =item order_by
150
151 =back
152
153 =cut
154
155 sub search {
156   my ($class, $params) = @_;
157   my @where = ();
158   my @from = (
159     'LEFT JOIN cust_svc  USING ( svcnum  )',
160     'LEFT JOIN part_svc  USING ( svcpart )',
161     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
162     'LEFT JOIN cust_main USING ( custnum )',
163   );
164
165   # based on FS::svc_acct::search, probably the most mature of the bunch
166   #unlinked
167   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
168   
169   #agentnum
170   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
171     push @where, "agentnum = $1";
172   }
173   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
174     'null_right' => 'View/link unlinked services',
175     'table' => 'cust_main'
176   );
177
178   #custnum
179   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
180     push @where, "custnum = $1";
181   }
182
183   #pkgpart, now properly untainted, can be arrayref
184   for my $pkgpart ( $params->{'pkgpart'} ) {
185     if ( ref $pkgpart ) {
186       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
187       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
188     }
189     elsif ( $pkgpart =~ /^(\d+)$/ ) {
190       push @where, "cust_pkg.pkgpart = $1";
191     }
192   }
193
194   #routernum, can be arrayref
195   for my $routernum ( $params->{'routernum'} ) {
196     push @from, 'LEFT JOIN addr_block USING ( blocknum )';
197     if ( ref $routernum and grep { $_ } @$routernum ) {
198       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
199       push @where, "addr_block.routernum IN ($where)" if $where;
200     }
201     elsif ( $routernum =~ /^(\d+)$/ ) {
202       push @where, "addr_block.routernum = $1";
203     }
204   }
205  
206   #svcnum
207   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
208     push @where, "svcnum = $1";
209   }
210
211   #svcpart
212   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
213     push @where, "svcpart = $1";
214   }
215
216   #ip_addr
217   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
218     push @where, "ip_addr = '$1'";
219   }
220
221   #custnum
222   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
223     push @where, "custnum = $1";
224   }
225   
226   my $addl_from = join(' ', @from);
227   my $extra_sql = '';
228   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
229   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
230   return( {
231       'table'   => 'svc_broadband',
232       'hashref' => {},
233       'select'  => join(', ',
234         'svc_broadband.*',
235         'part_svc.svc',
236         'cust_main.custnum',
237         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
238       ),
239       'extra_sql' => $extra_sql,
240       'addl_from' => $addl_from,
241       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
242       'count_query' => $count_query,
243     } );
244 }
245
246 =item search_sql STRING
247
248 Class method which returns an SQL fragment to search for the given string.
249
250 =cut
251
252 sub search_sql {
253   my( $class, $string ) = @_;
254   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
255     $class->search_sql_field('ip_addr', $string );
256   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
257     $class->search_sql_field('mac_addr', uc($string));
258   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
259     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
260   } else {
261     '1 = 0'; #false
262   }
263 }
264
265 =item label
266
267 Returns the IP address.
268
269 =cut
270
271 sub label {
272   my $self = shift;
273   $self->ip_addr;
274 }
275
276 =item insert [ , OPTION => VALUE ... ]
277
278 Adds this record to the database.  If there is an error, returns the error,
279 otherwise returns false.
280
281 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
282 defined.  An FS::cust_svc record will be created and inserted.
283
284 Currently available options are: I<depend_jobnum>
285
286 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
287 jobnums), all provisioning jobs will have a dependancy on the supplied
288 jobnum(s) (they will not run until the specific job(s) complete(s)).
289
290 =cut
291
292 # Standard FS::svc_Common::insert
293
294 =item delete
295
296 Delete this record from the database.
297
298 =cut
299
300 # Standard FS::svc_Common::delete
301
302 =item replace OLD_RECORD
303
304 Replaces the OLD_RECORD with this one in the database.  If there is an error,
305 returns the error, otherwise returns false.
306
307 =cut
308
309 # Standard FS::svc_Common::replace
310
311 =item suspend
312
313 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
314
315 =item unsuspend
316
317 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
318
319 =item cancel
320
321 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
322
323 =item check
324
325 Checks all fields to make sure this is a valid broadband service.  If there is
326 an error, returns the error, otherwise returns false.  Called by the insert
327 and replace methods.
328
329 =cut
330
331 sub check {
332   my $self = shift;
333   my $x = $self->setfixed;
334
335   return $x unless ref($x);
336
337   my $error =
338     $self->ut_numbern('svcnum')
339     || $self->ut_numbern('blocknum')
340     || $self->ut_textn('description')
341     || $self->ut_number('speed_up')
342     || $self->ut_number('speed_down')
343     || $self->ut_ipn('ip_addr')
344     || $self->ut_hexn('mac_addr')
345     || $self->ut_hexn('auth_key')
346     || $self->ut_coordn('latitude', -90, 90)
347     || $self->ut_coordn('longitude', -180, 180)
348     || $self->ut_sfloatn('altitude')
349     || $self->ut_textn('vlan_profile')
350     || $self->ut_textn('plan_id')
351   ;
352   return $error if $error;
353
354   if($self->speed_up < 0) { return 'speed_up must be positive'; }
355   if($self->speed_down < 0) { return 'speed_down must be positive'; }
356
357   my $cust_svc = $self->svcnum
358                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
359                  : '';
360   my $cust_pkg;
361   if ($cust_svc) {
362     $cust_pkg = $cust_svc->cust_pkg;
363   }else{
364     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
365     return "Invalid pkgnum" unless $cust_pkg;
366   }
367     
368   if ($self->blocknum) {
369     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
370     return $error if $error;
371   }
372
373   if ($cust_pkg && $self->blocknum) {
374     my $addr_agentnum = $self->addr_block->agentnum;
375     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
376       return "Address block does not service this customer";
377     }
378   }
379
380   $error = $self->_check_ip_addr;
381   return $error if $error;
382
383   $self->SUPER::check;
384 }
385
386 sub _check_ip_addr {
387   my $self = shift;
388
389   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
390
391     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
392
393     return "Must supply either address or block"
394       unless $self->blocknum;
395     my $next_addr = $self->addr_block->next_free_addr;
396     if ($next_addr) {
397       $self->ip_addr($next_addr->addr);
398     } else {
399       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
400     }
401
402   }
403
404   if (not($self->blocknum)) {
405     return "Must supply either address or block"
406       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
407     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
408                  map { $_->addr_block }
409                  $self->allowed_routers;
410     if (scalar(@block)) {
411       $self->blocknum($block[0]->blocknum);
412     }else{
413       return "Address not with available block.";
414     }
415   }
416
417   # This should catch errors in the ip_addr.  If it doesn't,
418   # they'll almost certainly not map into the block anyway.
419   my $self_addr = $self->NetAddr; #netmask is /32
420   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
421
422   my $block_addr = $self->addr_block->NetAddr;
423   unless ($block_addr->contains($self_addr)) {
424     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
425   }
426
427   my $router = $self->addr_block->router 
428     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
429   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
430   } # do nothing
431   else {
432     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
433   }
434
435   '';
436 }
437
438 sub _check_duplicate {
439   my $self = shift;
440
441   return "MAC already in use"
442     if ( $self->mac_addr &&
443          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
444        );
445
446   '';
447 }
448
449
450 =item NetAddr
451
452 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
453 is /32.
454
455 =cut
456
457 sub NetAddr {
458   my $self = shift;
459   new NetAddr::IP ($self->ip_addr);
460 }
461
462 =item addr_block
463
464 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
465
466 =cut
467
468 sub addr_block {
469   my $self = shift;
470   qsearchs('addr_block', { blocknum => $self->blocknum });
471 }
472
473 =back
474
475 =item allowed_routers
476
477 Returns a list of allowed FS::router objects.
478
479 =cut
480
481 sub allowed_routers {
482   my $self = shift;
483   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
484 }
485
486 =head1 BUGS
487
488 The business with sb_field has been 'fixed', in a manner of speaking.
489
490 allowed_routers isn't agent virtualized because part_svc isn't agent
491 virtualized
492
493 =head1 SEE ALSO
494
495 FS::svc_Common, FS::Record, FS::addr_block,
496 FS::part_svc, schema.html from the base documentation.
497
498 =cut
499
500 1;
501