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);
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
169 my ($class, $params) = @_;
172 'LEFT JOIN cust_svc USING ( svcnum )',
173 'LEFT JOIN part_svc USING ( svcpart )',
174 'LEFT JOIN cust_pkg USING ( pkgnum )',
175 'LEFT JOIN cust_main USING ( custnum )',
178 # based on FS::svc_acct::search, probably the most mature of the bunch
180 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
183 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
184 push @where, "cust_main.agentnum = $1";
186 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
187 'null_right' => 'View/link unlinked services',
188 'table' => 'cust_main'
192 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
193 push @where, "custnum = $1";
196 #pkgpart, now properly untainted, can be arrayref
197 for my $pkgpart ( $params->{'pkgpart'} ) {
198 if ( ref $pkgpart ) {
199 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
200 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
202 elsif ( $pkgpart =~ /^(\d+)$/ ) {
203 push @where, "cust_pkg.pkgpart = $1";
207 #routernum, can be arrayref
208 for my $routernum ( $params->{'routernum'} ) {
209 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
210 if ( ref $routernum and grep { $_ } @$routernum ) {
211 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
212 push @where, "addr_block.routernum IN ($where)" if $where;
214 elsif ( $routernum =~ /^(\d+)$/ ) {
215 push @where, "addr_block.routernum = $1";
220 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
221 push @where, "svcnum = $1";
225 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
226 push @where, "svcpart = $1";
230 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
231 push @where, "ip_addr = '$1'";
235 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
236 push @where, "custnum = $1";
239 my $addl_from = join(' ', @from);
241 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
242 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
244 'table' => 'svc_broadband',
246 'select' => join(', ',
250 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
252 'extra_sql' => $extra_sql,
253 'addl_from' => $addl_from,
254 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
255 'count_query' => $count_query,
259 =item search_sql STRING
261 Class method which returns an SQL fragment to search for the given string.
266 my( $class, $string ) = @_;
267 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
268 $class->search_sql_field('ip_addr', $string );
269 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
270 $class->search_sql_field('mac_addr', uc($string));
271 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
272 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
280 Returns the IP address.
289 =item insert [ , OPTION => VALUE ... ]
291 Adds this record to the database. If there is an error, returns the error,
292 otherwise returns false.
294 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
295 defined. An FS::cust_svc record will be created and inserted.
297 Currently available options are: I<depend_jobnum>
299 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
300 jobnums), all provisioning jobs will have a dependancy on the supplied
301 jobnum(s) (they will not run until the specific job(s) complete(s)).
305 # Standard FS::svc_Common::insert
309 Delete this record from the database.
313 # Standard FS::svc_Common::delete
315 =item replace OLD_RECORD
317 Replaces the OLD_RECORD with this one in the database. If there is an error,
318 returns the error, otherwise returns false.
322 # Standard FS::svc_Common::replace
326 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
330 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
334 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
338 Checks all fields to make sure this is a valid broadband service. If there is
339 an error, returns the error, otherwise returns false. Called by the insert
346 my $x = $self->setfixed;
348 return $x unless ref($x);
351 my $mac_addr = uc($self->get('mac_addr'));
352 $mac_addr =~ s/[-: ]//g;
353 $self->set('mac_addr', $mac_addr);
356 $self->ut_numbern('svcnum')
357 || $self->ut_numbern('blocknum')
358 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
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')
366 || $self->ut_coordn('longitude')
367 || $self->ut_sfloatn('altitude')
368 || $self->ut_textn('vlan_profile')
369 || $self->ut_textn('plan_id')
371 return $error if $error;
373 if($self->speed_up < 0) { return 'speed_up must be positive'; }
374 if($self->speed_down < 0) { return 'speed_down must be positive'; }
376 my $cust_svc = $self->svcnum
377 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
381 $cust_pkg = $cust_svc->cust_pkg;
383 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
384 return "Invalid pkgnum" unless $cust_pkg;
387 if ($self->blocknum) {
388 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
389 return $error if $error;
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";
399 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
400 my $l = $cust_pkg->cust_location_or_main;
401 if ( $l->ship_latitude && $l->ship_longitude ) {
402 $self->latitude( $l->ship_latitude );
403 $self->longitude( $l->ship_longitude );
404 } elsif ( $l->latitude && $l->longitude ) {
405 $self->latitude( $l->latitude );
406 $self->longitude( $l->longitude );
410 $error = $self->_check_ip_addr;
411 return $error if $error;
419 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
421 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
423 return "Must supply either address or block"
424 unless $self->blocknum;
425 my $next_addr = $self->addr_block->next_free_addr;
427 $self->ip_addr($next_addr->addr);
429 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
434 if (not($self->blocknum)) {
435 return "Must supply either address or block"
436 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
437 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
438 map { $_->addr_block }
439 $self->allowed_routers;
440 if (scalar(@block)) {
441 $self->blocknum($block[0]->blocknum);
443 return "Address not with available block.";
447 # This should catch errors in the ip_addr. If it doesn't,
448 # they'll almost certainly not map into the block anyway.
449 my $self_addr = $self->NetAddr; #netmask is /32
450 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
452 my $block_addr = $self->addr_block->NetAddr;
453 unless ($block_addr->contains($self_addr)) {
454 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
457 my $router = $self->addr_block->router
458 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
459 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
462 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
468 sub _check_duplicate {
471 return "MAC already in use"
472 if ( $self->mac_addr &&
473 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
482 Returns a NetAddr::IP object containing the IP address of this service. The netmask
489 new NetAddr::IP ($self->ip_addr);
494 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
500 qsearchs('addr_block', { blocknum => $self->blocknum });
505 =item allowed_routers
507 Returns a list of allowed FS::router objects.
511 sub allowed_routers {
513 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
518 The business with sb_field has been 'fixed', in a manner of speaking.
520 allowed_routers isn't agent virtualized because part_svc isn't agent
525 FS::svc_Common, FS::Record, FS::addr_block,
526 FS::part_svc, schema.html from the base documentation.