1 package FS::svc_broadband;
2 use base qw(FS::svc_Radius_Mixin FS::svc_Tower_Mixin FS::svc_Common);
7 { no warnings 'redefine'; use NetAddr::IP; }
8 use FS::Record qw( qsearchs qsearch dbh );
11 use FS::part_svc_router;
14 $FS::UID::callback{'FS::svc_broadband'} = sub {
20 FS::svc_broadband - Object methods for svc_broadband records
24 use FS::svc_broadband;
26 $record = new FS::svc_broadband \%hash;
27 $record = new FS::svc_broadband { 'column' => 'value' };
29 $error = $record->insert;
31 $error = $new_record->replace($old_record);
33 $error = $record->delete;
35 $error = $record->check;
37 $error = $record->suspend;
39 $error = $record->unsuspend;
41 $error = $record->cancel;
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:
49 FS::svc_broadband inherits from FS::svc_Common. The following fields are
54 =item svcnum - primary key
56 =item blocknum - see FS::addr_block
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
65 speed_down - maximum download speed, as above
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
83 Creates a new svc_broadband. To add the record to the database, see
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.
93 'name' => 'Wireless broadband',
94 'name_plural' => 'Wireless broadband services',
95 'longname_plural' => 'Fixed wireless broadband services',
96 'display_weight' => 50,
97 'cancel_weight' => 70,
98 'ip_field' => 'ip_addr',
100 'svcnum' => 'Service',
101 'description' => 'Descriptive label for this particular device',
102 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
103 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
104 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
105 'sectornum' => 'Tower sector',
106 'blocknum' => { 'label' => 'Address block',
108 'select_table' => 'addr_block',
109 'select_key' => 'blocknum',
110 'select_label' => 'cidr',
111 'disable_inventory' => 1,
113 'plan_id' => 'Service Plan Id',
114 'performance_profile' => 'Peformance Profile',
115 'authkey' => 'Authentication key',
116 'mac_addr' => 'MAC address',
117 'latitude' => 'Latitude',
118 'longitude' => 'Longitude',
119 'altitude' => 'Altitude',
120 'vlan_profile' => 'VLAN profile',
122 label => 'RADIUS groups',
123 type => 'select-radius_group.html',
124 #select_table => 'radius_group',
125 #select_key => 'groupnum',
126 #select_label => 'groupname',
127 disable_inventory => 1,
134 sub table { 'svc_broadband'; }
136 sub table_dupcheck_fields { ( 'mac_addr' ); }
140 Class method which returns a qsearch hash expression to search for parameters
141 specified in HASHREF.
147 =item unlinked - set to search for all unlinked services. Overrides all other options.
157 =item pkgpart - arrayref
159 =item routernum - arrayref
161 =item sectornum - arrayref
163 =item towernum - arrayref
172 my ($class, $params) = @_;
175 'LEFT JOIN cust_svc USING ( svcnum )',
176 'LEFT JOIN part_svc USING ( svcpart )',
177 'LEFT JOIN cust_pkg USING ( pkgnum )',
178 'LEFT JOIN cust_main USING ( custnum )',
181 # based on FS::svc_acct::search, probably the most mature of the bunch
183 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
186 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
187 push @where, "cust_main.agentnum = $1";
189 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
190 'null_right' => 'View/link unlinked services',
191 'table' => 'cust_main'
195 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
196 push @where, "custnum = $1";
199 #pkgpart, now properly untainted, can be arrayref
200 for my $pkgpart ( $params->{'pkgpart'} ) {
201 if ( ref $pkgpart ) {
202 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
203 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
205 elsif ( $pkgpart =~ /^(\d+)$/ ) {
206 push @where, "cust_pkg.pkgpart = $1";
210 #routernum, can be arrayref
211 for my $routernum ( $params->{'routernum'} ) {
212 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
213 if ( ref $routernum and grep { $_ } @$routernum ) {
214 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
215 push @where, "addr_block.routernum IN ($where)" if $where;
217 elsif ( $routernum =~ /^(\d+)$/ ) {
218 push @where, "addr_block.routernum = $1";
222 #sector and tower, as above
223 my @where_sector = $class->tower_sector_sql($params);
224 if ( @where_sector ) {
225 push @where, @where_sector;
226 push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
230 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
231 push @where, "svcnum = $1";
235 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
236 push @where, "svcpart = $1";
240 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
241 push @where, "ip_addr = '$1'";
245 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
246 push @where, "custnum = $1";
249 my $addl_from = join(' ', @from);
251 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
252 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
254 'table' => 'svc_broadband',
256 'select' => join(', ',
260 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
262 'extra_sql' => $extra_sql,
263 'addl_from' => $addl_from,
264 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
265 'count_query' => $count_query,
269 =item search_sql STRING
271 Class method which returns an SQL fragment to search for the given string.
276 my( $class, $string ) = @_;
277 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
278 $class->search_sql_field('ip_addr', $string );
279 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
280 $class->search_sql_field('mac_addr', uc($string));
281 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
282 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
290 Returns the IP address.
299 =item insert [ , OPTION => VALUE ... ]
301 Adds this record to the database. If there is an error, returns the error,
302 otherwise returns false.
304 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
305 defined. An FS::cust_svc record will be created and inserted.
307 Currently available options are: I<depend_jobnum>
309 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
310 jobnums), all provisioning jobs will have a dependancy on the supplied
311 jobnum(s) (they will not run until the specific job(s) complete(s)).
315 # Standard FS::svc_Common::insert
319 Delete this record from the database.
323 # Standard FS::svc_Common::delete
325 =item replace OLD_RECORD
327 Replaces the OLD_RECORD with this one in the database. If there is an error,
328 returns the error, otherwise returns false.
332 # Standard FS::svc_Common::replace
336 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
340 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
344 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
348 Checks all fields to make sure this is a valid broadband service. If there is
349 an error, returns the error, otherwise returns false. Called by the insert
356 my $x = $self->setfixed;
358 return $x unless ref($x);
361 my $mac_addr = uc($self->get('mac_addr'));
362 $mac_addr =~ s/[-: ]//g;
363 $self->set('mac_addr', $mac_addr);
366 $self->ut_numbern('svcnum')
367 || $self->ut_numbern('blocknum')
368 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
369 || $self->ut_textn('description')
370 || $self->ut_numbern('speed_up')
371 || $self->ut_numbern('speed_down')
372 || $self->ut_ipn('ip_addr')
373 || $self->ut_hexn('mac_addr')
374 || $self->ut_hexn('auth_key')
375 || $self->ut_coordn('latitude')
376 || $self->ut_coordn('longitude')
377 || $self->ut_sfloatn('altitude')
378 || $self->ut_textn('vlan_profile')
379 || $self->ut_textn('plan_id')
381 return $error if $error;
383 if($self->speed_up < 0) { return 'speed_up must be positive'; }
384 if($self->speed_down < 0) { return 'speed_down must be positive'; }
386 my $cust_svc = $self->svcnum
387 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
391 $cust_pkg = $cust_svc->cust_pkg;
393 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
394 return "Invalid pkgnum" unless $cust_pkg;
397 if ($self->blocknum) {
398 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
399 return $error if $error;
402 if ($cust_pkg && $self->blocknum) {
403 my $addr_agentnum = $self->addr_block->agentnum;
404 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
405 return "Address block does not service this customer";
409 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
410 my $l = $cust_pkg->cust_location_or_main;
411 if ( $l->ship_latitude && $l->ship_longitude ) {
412 $self->latitude( $l->ship_latitude );
413 $self->longitude( $l->ship_longitude );
414 } elsif ( $l->latitude && $l->longitude ) {
415 $self->latitude( $l->latitude );
416 $self->longitude( $l->longitude );
420 $error = $self->_check_ip_addr;
421 return $error if $error;
429 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
431 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
433 return "Must supply either address or block"
434 unless $self->blocknum;
435 my $next_addr = $self->addr_block->next_free_addr;
437 $self->ip_addr($next_addr->addr);
439 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
444 if (not($self->blocknum)) {
445 return "Must supply either address or block"
446 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
447 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
448 map { $_->addr_block }
449 $self->allowed_routers;
450 if (scalar(@block)) {
451 $self->blocknum($block[0]->blocknum);
453 return "Address not with available block.";
457 # This should catch errors in the ip_addr. If it doesn't,
458 # they'll almost certainly not map into the block anyway.
459 my $self_addr = $self->NetAddr; #netmask is /32
460 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
462 my $block_addr = $self->addr_block->NetAddr;
463 unless ($block_addr->contains($self_addr)) {
464 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
467 my $router = $self->addr_block->router
468 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
469 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
472 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
478 sub _check_duplicate {
481 return "MAC already in use"
482 if ( $self->mac_addr &&
483 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
492 Returns a NetAddr::IP object containing the IP address of this service. The netmask
499 new NetAddr::IP ($self->ip_addr);
504 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
510 qsearchs('addr_block', { blocknum => $self->blocknum });
515 =item allowed_routers
517 Returns a list of allowed FS::router objects.
521 sub allowed_routers {
523 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
528 The business with sb_field has been 'fixed', in a manner of speaking.
530 allowed_routers isn't agent virtualized because part_svc isn't agent
535 FS::svc_Common, FS::Record, FS::addr_block,
536 FS::part_svc, schema.html from the base documentation.