auto-add service ips to network monitoring, RT#15536
[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_Radius_Mixin 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     'ip_field' => 'ip_addr',
99     'fields' => {
100       'description' => 'Descriptive label for this particular device.',
101       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
102       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
103       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
104       'blocknum'    => { 'label' => 'Address block',
105                          'type'  => 'select',
106                          'select_table' => 'addr_block',
107                          'select_key'   => 'blocknum',
108                          'select_label' => 'cidr',
109                          'disable_inventory' => 1,
110                        },
111      'plan_id' => 'Service Plan Id',
112      'performance_profile' => 'Peformance Profile',
113      'authkey'      => 'Authentication key',
114      'mac_addr'     => 'MAC address',
115      'latitude'     => 'Latitude',
116      'longitude'    => 'Longitude',
117      'altitude'     => 'Altitude',
118      'vlan_profile' => 'VLAN profile',
119      'usergroup'    => { 
120                          label => 'RADIUS groups',
121                          type  => 'select-radius_group.html',
122                          #select_table => 'radius_group',
123                          #select_key   => 'groupnum',
124                          #select_label => 'groupname',
125                          disable_inventory => 1,
126                          multiple => 1,
127                        },
128     },
129   };
130 }
131
132 sub table { 'svc_broadband'; }
133
134 sub table_dupcheck_fields { ( 'mac_addr' ); }
135
136 =item search HASHREF
137
138 Class method which returns a qsearch hash expression to search for parameters
139 specified in HASHREF.
140
141 Parameters:
142
143 =over 4
144
145 =item unlinked - set to search for all unlinked services.  Overrides all other options.
146
147 =item agentnum
148
149 =item custnum
150
151 =item svcpart
152
153 =item ip_addr
154
155 =item pkgpart - arrayref
156
157 =item routernum - arrayref
158
159 =item order_by
160
161 =back
162
163 =cut
164
165 sub search {
166   my ($class, $params) = @_;
167   my @where = ();
168   my @from = (
169     'LEFT JOIN cust_svc  USING ( svcnum  )',
170     'LEFT JOIN part_svc  USING ( svcpart )',
171     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
172     'LEFT JOIN cust_main USING ( custnum )',
173   );
174
175   # based on FS::svc_acct::search, probably the most mature of the bunch
176   #unlinked
177   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
178   
179   #agentnum
180   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
181     push @where, "cust_main.agentnum = $1";
182   }
183   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
184     'null_right' => 'View/link unlinked services',
185     'table' => 'cust_main'
186   );
187
188   #custnum
189   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
190     push @where, "custnum = $1";
191   }
192
193   #pkgpart, now properly untainted, can be arrayref
194   for my $pkgpart ( $params->{'pkgpart'} ) {
195     if ( ref $pkgpart ) {
196       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
197       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
198     }
199     elsif ( $pkgpart =~ /^(\d+)$/ ) {
200       push @where, "cust_pkg.pkgpart = $1";
201     }
202   }
203
204   #routernum, can be arrayref
205   for my $routernum ( $params->{'routernum'} ) {
206     push @from, 'LEFT JOIN addr_block USING ( blocknum )';
207     if ( ref $routernum and grep { $_ } @$routernum ) {
208       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
209       push @where, "addr_block.routernum IN ($where)" if $where;
210     }
211     elsif ( $routernum =~ /^(\d+)$/ ) {
212       push @where, "addr_block.routernum = $1";
213     }
214   }
215  
216   #svcnum
217   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
218     push @where, "svcnum = $1";
219   }
220
221   #svcpart
222   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
223     push @where, "svcpart = $1";
224   }
225
226   #ip_addr
227   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
228     push @where, "ip_addr = '$1'";
229   }
230
231   #custnum
232   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
233     push @where, "custnum = $1";
234   }
235   
236   my $addl_from = join(' ', @from);
237   my $extra_sql = '';
238   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
239   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
240   return( {
241       'table'   => 'svc_broadband',
242       'hashref' => {},
243       'select'  => join(', ',
244         'svc_broadband.*',
245         'part_svc.svc',
246         'cust_main.custnum',
247         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
248       ),
249       'extra_sql' => $extra_sql,
250       'addl_from' => $addl_from,
251       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
252       'count_query' => $count_query,
253     } );
254 }
255
256 =item search_sql STRING
257
258 Class method which returns an SQL fragment to search for the given string.
259
260 =cut
261
262 sub search_sql {
263   my( $class, $string ) = @_;
264   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
265     $class->search_sql_field('ip_addr', $string );
266   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
267     $class->search_sql_field('mac_addr', uc($string));
268   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
269     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
270   } else {
271     '1 = 0'; #false
272   }
273 }
274
275 =item label
276
277 Returns the IP address.
278
279 =cut
280
281 sub label {
282   my $self = shift;
283   $self->ip_addr;
284 }
285
286 =item insert [ , OPTION => VALUE ... ]
287
288 Adds this record to the database.  If there is an error, returns the error,
289 otherwise returns false.
290
291 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
292 defined.  An FS::cust_svc record will be created and inserted.
293
294 Currently available options are: I<depend_jobnum>
295
296 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
297 jobnums), all provisioning jobs will have a dependancy on the supplied
298 jobnum(s) (they will not run until the specific job(s) complete(s)).
299
300 =cut
301
302 # Standard FS::svc_Common::insert
303
304 =item delete
305
306 Delete this record from the database.
307
308 =cut
309
310 # Standard FS::svc_Common::delete
311
312 =item replace OLD_RECORD
313
314 Replaces the OLD_RECORD with this one in the database.  If there is an error,
315 returns the error, otherwise returns false.
316
317 =cut
318
319 # Standard FS::svc_Common::replace
320
321 =item suspend
322
323 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
324
325 =item unsuspend
326
327 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
328
329 =item cancel
330
331 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
332
333 =item check
334
335 Checks all fields to make sure this is a valid broadband service.  If there is
336 an error, returns the error, otherwise returns false.  Called by the insert
337 and replace methods.
338
339 =cut
340
341 sub check {
342   my $self = shift;
343   my $x = $self->setfixed;
344
345   return $x unless ref($x);
346
347   my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
348   my $lat_lower = $nw_coords ? 1 : -90;
349   my $lon_upper = $nw_coords ? -1 : 180;
350
351   # remove delimiters
352   my $mac_addr = uc($self->get('mac_addr'));
353   $mac_addr =~ s/[-: ]//g;
354   $self->set('mac_addr', $mac_addr);
355
356   my $error =
357     $self->ut_numbern('svcnum')
358     || $self->ut_numbern('blocknum')
359     || $self->ut_textn('description')
360     || $self->ut_numbern('speed_up')
361     || $self->ut_numbern('speed_down')
362     || $self->ut_ipn('ip_addr')
363     || $self->ut_hexn('mac_addr')
364     || $self->ut_hexn('auth_key')
365     || $self->ut_coordn('latitude', $lat_lower, 90)
366     || $self->ut_coordn('longitude', -180, $lon_upper)
367     || $self->ut_sfloatn('altitude')
368     || $self->ut_textn('vlan_profile')
369     || $self->ut_textn('plan_id')
370   ;
371   return $error if $error;
372
373   if($self->speed_up < 0) { return 'speed_up must be positive'; }
374   if($self->speed_down < 0) { return 'speed_down must be positive'; }
375
376   my $cust_svc = $self->svcnum
377                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
378                  : '';
379   my $cust_pkg;
380   if ($cust_svc) {
381     $cust_pkg = $cust_svc->cust_pkg;
382   }else{
383     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
384     return "Invalid pkgnum" unless $cust_pkg;
385   }
386     
387   if ($self->blocknum) {
388     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
389     return $error if $error;
390   }
391
392   if ($cust_pkg && $self->blocknum) {
393     my $addr_agentnum = $self->addr_block->agentnum;
394     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
395       return "Address block does not service this customer";
396     }
397   }
398
399   $error = $self->_check_ip_addr;
400   return $error if $error;
401
402   $self->SUPER::check;
403 }
404
405 sub _check_ip_addr {
406   my $self = shift;
407
408   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
409
410     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
411
412     return "Must supply either address or block"
413       unless $self->blocknum;
414     my $next_addr = $self->addr_block->next_free_addr;
415     if ($next_addr) {
416       $self->ip_addr($next_addr->addr);
417     } else {
418       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
419     }
420
421   }
422
423   if (not($self->blocknum)) {
424     return "Must supply either address or block"
425       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
426     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
427                  map { $_->addr_block }
428                  $self->allowed_routers;
429     if (scalar(@block)) {
430       $self->blocknum($block[0]->blocknum);
431     }else{
432       return "Address not with available block.";
433     }
434   }
435
436   # This should catch errors in the ip_addr.  If it doesn't,
437   # they'll almost certainly not map into the block anyway.
438   my $self_addr = $self->NetAddr; #netmask is /32
439   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
440
441   my $block_addr = $self->addr_block->NetAddr;
442   unless ($block_addr->contains($self_addr)) {
443     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
444   }
445
446   my $router = $self->addr_block->router 
447     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
448   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
449   } # do nothing
450   else {
451     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
452   }
453
454   '';
455 }
456
457 sub _check_duplicate {
458   my $self = shift;
459
460   return "MAC already in use"
461     if ( $self->mac_addr &&
462          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
463        );
464
465   '';
466 }
467
468
469 =item NetAddr
470
471 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
472 is /32.
473
474 =cut
475
476 sub NetAddr {
477   my $self = shift;
478   new NetAddr::IP ($self->ip_addr);
479 }
480
481 =item addr_block
482
483 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
484
485 =cut
486
487 sub addr_block {
488   my $self = shift;
489   qsearchs('addr_block', { blocknum => $self->blocknum });
490 }
491
492 =back
493
494 =item allowed_routers
495
496 Returns a list of allowed FS::router objects.
497
498 =cut
499
500 sub allowed_routers {
501   my $self = shift;
502   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
503 }
504
505 =head1 BUGS
506
507 The business with sb_field has been 'fixed', in a manner of speaking.
508
509 allowed_routers isn't agent virtualized because part_svc isn't agent
510 virtualized
511
512 =head1 SEE ALSO
513
514 FS::svc_Common, FS::Record, FS::addr_block,
515 FS::part_svc, schema.html from the base documentation.
516
517 =cut
518
519 1;
520