1 package FS::svc_broadband;
4 use vars qw(@ISA $conf);
6 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 );
12 use FS::part_svc_router;
15 $FS::UID::callback{'FS::svc_broadband'} = sub {
21 FS::svc_broadband - Object methods for svc_broadband records
25 use FS::svc_broadband;
27 $record = new FS::svc_broadband \%hash;
28 $record = new FS::svc_broadband { 'column' => 'value' };
30 $error = $record->insert;
32 $error = $new_record->replace($old_record);
34 $error = $record->delete;
36 $error = $record->check;
38 $error = $record->suspend;
40 $error = $record->unsuspend;
42 $error = $record->cancel;
46 An FS::svc_broadband object represents a 'broadband' Internet connection, such
47 as a DSL, cable modem, or fixed wireless link. These services are assumed to
48 have the following properties:
50 FS::svc_broadband inherits from FS::svc_Common. The following fields are
55 =item svcnum - primary key
57 =item blocknum - see FS::addr_block
60 speed_up - maximum upload speed, in bits per second. If set to zero, upload
61 speed will be unlimited. Exports that do traffic shaping should handle this
62 correctly, and not blindly set the upload speed to zero and kill the customer's
66 speed_down - maximum download speed, as above
68 =item ip_addr - the customer's IP address. If the customer needs more than one
69 IP address, set this to the address of the customer's router. As a result, the
70 customer's router will have the same address for both its internal and external
71 interfaces thus saving address space. This has been found to work on most NAT
84 Creates a new svc_broadband. To add the record to the database, see
87 Note that this stores the hash reference, not a distinct copy of the hash it
88 points to. You can ask the object for a copy with the I<hash> method.
94 'name' => 'Wireless broadband',
95 'name_plural' => 'Wireless broadband services',
96 'longname_plural' => 'Fixed wireless broadband services',
97 'display_weight' => 50,
98 'cancel_weight' => 70,
99 'ip_field' => 'ip_addr',
101 'svcnum' => 'Service',
102 'description' => 'Descriptive label for this particular device',
103 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
104 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
105 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
106 'sectornum' => 'Tower sector',
107 'blocknum' => { 'label' => 'Address block',
109 'select_table' => 'addr_block',
110 'select_key' => 'blocknum',
111 'select_label' => 'cidr',
112 'disable_inventory' => 1,
114 'plan_id' => 'Service Plan Id',
115 'performance_profile' => 'Peformance Profile',
116 'authkey' => 'Authentication key',
117 'mac_addr' => 'MAC address',
118 'latitude' => 'Latitude',
119 'longitude' => 'Longitude',
120 'altitude' => 'Altitude',
121 'vlan_profile' => 'VLAN profile',
123 label => 'RADIUS groups',
124 type => 'select-radius_group.html',
125 #select_table => 'radius_group',
126 #select_key => 'groupnum',
127 #select_label => 'groupname',
128 disable_inventory => 1,
135 sub table { 'svc_broadband'; }
137 sub table_dupcheck_fields { ( 'mac_addr' ); }
141 Class method which returns a qsearch hash expression to search for parameters
142 specified in HASHREF.
148 =item unlinked - set to search for all unlinked services. Overrides all other options.
158 =item pkgpart - arrayref
160 =item routernum - arrayref
162 =item sectornum - arrayref
164 =item towernum - arrayref
173 my ($class, $params) = @_;
176 'LEFT JOIN cust_svc USING ( svcnum )',
177 'LEFT JOIN part_svc USING ( svcpart )',
178 'LEFT JOIN cust_pkg USING ( pkgnum )',
179 'LEFT JOIN cust_main USING ( custnum )',
182 # based on FS::svc_acct::search, probably the most mature of the bunch
184 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
187 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
188 push @where, "cust_main.agentnum = $1";
190 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
191 'null_right' => 'View/link unlinked services',
192 'table' => 'cust_main'
196 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
197 push @where, "custnum = $1";
200 #pkgpart, now properly untainted, can be arrayref
201 for my $pkgpart ( $params->{'pkgpart'} ) {
202 if ( ref $pkgpart ) {
203 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
204 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
206 elsif ( $pkgpart =~ /^(\d+)$/ ) {
207 push @where, "cust_pkg.pkgpart = $1";
211 #routernum, can be arrayref
212 for my $routernum ( $params->{'routernum'} ) {
213 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
214 if ( ref $routernum and grep { $_ } @$routernum ) {
215 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
216 push @where, "addr_block.routernum IN ($where)" if $where;
218 elsif ( $routernum =~ /^(\d+)$/ ) {
219 push @where, "addr_block.routernum = $1";
223 #sector and tower, as above
224 my @where_sector = $class->tower_sector_sql($params);
225 if ( @where_sector ) {
226 push @where, @where_sector;
227 push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
231 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
232 push @where, "svcnum = $1";
236 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
237 push @where, "svcpart = $1";
241 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
242 push @where, "ip_addr = '$1'";
246 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
247 push @where, "custnum = $1";
250 my $addl_from = join(' ', @from);
252 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
253 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
255 'table' => 'svc_broadband',
257 'select' => join(', ',
261 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
263 'extra_sql' => $extra_sql,
264 'addl_from' => $addl_from,
265 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
266 'count_query' => $count_query,
270 =item search_sql STRING
272 Class method which returns an SQL fragment to search for the given string.
277 my( $class, $string ) = @_;
278 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
279 $class->search_sql_field('ip_addr', $string );
280 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
281 $class->search_sql_field('mac_addr', uc($string));
282 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
283 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
291 Returns the IP address.
300 =item insert [ , OPTION => VALUE ... ]
302 Adds this record to the database. If there is an error, returns the error,
303 otherwise returns false.
305 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
306 defined. An FS::cust_svc record will be created and inserted.
308 Currently available options are: I<depend_jobnum>
310 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
311 jobnums), all provisioning jobs will have a dependancy on the supplied
312 jobnum(s) (they will not run until the specific job(s) complete(s)).
316 # Standard FS::svc_Common::insert
320 Delete this record from the database.
324 # Standard FS::svc_Common::delete
326 =item replace OLD_RECORD
328 Replaces the OLD_RECORD with this one in the database. If there is an error,
329 returns the error, otherwise returns false.
333 # Standard FS::svc_Common::replace
337 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
341 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
345 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
349 Checks all fields to make sure this is a valid broadband service. If there is
350 an error, returns the error, otherwise returns false. Called by the insert
357 my $x = $self->setfixed;
359 return $x unless ref($x);
362 my $mac_addr = uc($self->get('mac_addr'));
363 $mac_addr =~ s/[-: ]//g;
364 $self->set('mac_addr', $mac_addr);
367 $self->ut_numbern('svcnum')
368 || $self->ut_numbern('blocknum')
369 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
370 || $self->ut_textn('description')
371 || $self->ut_numbern('speed_up')
372 || $self->ut_numbern('speed_down')
373 || $self->ut_ipn('ip_addr')
374 || $self->ut_hexn('mac_addr')
375 || $self->ut_hexn('auth_key')
376 || $self->ut_coordn('latitude')
377 || $self->ut_coordn('longitude')
378 || $self->ut_sfloatn('altitude')
379 || $self->ut_textn('vlan_profile')
380 || $self->ut_textn('plan_id')
382 return $error if $error;
384 if($self->speed_up < 0) { return 'speed_up must be positive'; }
385 if($self->speed_down < 0) { return 'speed_down must be positive'; }
387 my $cust_svc = $self->svcnum
388 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
392 $cust_pkg = $cust_svc->cust_pkg;
394 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
395 return "Invalid pkgnum" unless $cust_pkg;
398 if ($self->blocknum) {
399 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
400 return $error if $error;
403 if ($cust_pkg && $self->blocknum) {
404 my $addr_agentnum = $self->addr_block->agentnum;
405 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
406 return "Address block does not service this customer";
410 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
411 my $l = $cust_pkg->cust_location_or_main;
412 if ( $l->ship_latitude && $l->ship_longitude ) {
413 $self->latitude( $l->ship_latitude );
414 $self->longitude( $l->ship_longitude );
415 } elsif ( $l->latitude && $l->longitude ) {
416 $self->latitude( $l->latitude );
417 $self->longitude( $l->longitude );
421 $error = $self->_check_ip_addr;
422 return $error if $error;
430 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
432 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
434 return "Must supply either address or block"
435 unless $self->blocknum;
436 my $next_addr = $self->addr_block->next_free_addr;
438 $self->ip_addr($next_addr->addr);
440 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
445 if (not($self->blocknum)) {
446 return "Must supply either address or block"
447 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
448 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
449 map { $_->addr_block }
450 $self->allowed_routers;
451 if (scalar(@block)) {
452 $self->blocknum($block[0]->blocknum);
454 return "Address not with available block.";
458 # This should catch errors in the ip_addr. If it doesn't,
459 # they'll almost certainly not map into the block anyway.
460 my $self_addr = $self->NetAddr; #netmask is /32
461 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
463 my $block_addr = $self->addr_block->NetAddr;
464 unless ($block_addr->contains($self_addr)) {
465 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
468 my $router = $self->addr_block->router
469 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
470 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
473 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
479 sub _check_duplicate {
482 return "MAC already in use"
483 if ( $self->mac_addr &&
484 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
493 Returns a NetAddr::IP object containing the IP address of this service. The netmask
500 new NetAddr::IP ($self->ip_addr);
505 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
511 qsearchs('addr_block', { blocknum => $self->blocknum });
516 =item allowed_routers
518 Returns a list of allowed FS::router objects.
522 sub allowed_routers {
524 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
529 The business with sb_field has been 'fixed', in a manner of speaking.
531 allowed_routers isn't agent virtualized because part_svc isn't agent
536 FS::svc_Common, FS::Record, FS::addr_block,
537 FS::part_svc, schema.html from the base documentation.