1 package FS::svc_broadband;
4 use vars qw(@ISA $conf);
6 use FS::Record qw( qsearchs qsearch dbh );
10 use FS::part_svc_router;
13 @ISA = qw( FS::svc_Radius_Mixin FS::svc_Common );
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' => 'Broadband',
95 'name_plural' => 'Broadband services',
96 'longname_plural' => 'Fixed (username-less) 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 '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
168 my ($class, $params) = @_;
171 'LEFT JOIN cust_svc USING ( svcnum )',
172 'LEFT JOIN part_svc USING ( svcpart )',
173 'LEFT JOIN cust_pkg USING ( pkgnum )',
174 'LEFT JOIN cust_main USING ( custnum )',
177 # based on FS::svc_acct::search, probably the most mature of the bunch
179 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
182 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
183 push @where, "cust_main.agentnum = $1";
185 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
186 'null_right' => 'View/link unlinked services',
187 'table' => 'cust_main'
191 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
192 push @where, "custnum = $1";
195 #pkgpart, now properly untainted, can be arrayref
196 for my $pkgpart ( $params->{'pkgpart'} ) {
197 if ( ref $pkgpart ) {
198 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
199 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
201 elsif ( $pkgpart =~ /^(\d+)$/ ) {
202 push @where, "cust_pkg.pkgpart = $1";
206 #routernum, can be arrayref
207 for my $routernum ( $params->{'routernum'} ) {
208 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
209 if ( ref $routernum and grep { $_ } @$routernum ) {
210 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
211 push @where, "addr_block.routernum IN ($where)" if $where;
213 elsif ( $routernum =~ /^(\d+)$/ ) {
214 push @where, "addr_block.routernum = $1";
219 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
220 push @where, "svcnum = $1";
224 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
225 push @where, "svcpart = $1";
229 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
230 push @where, "ip_addr = '$1'";
234 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
235 push @where, "custnum = $1";
238 my $addl_from = join(' ', @from);
240 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
241 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
243 'table' => 'svc_broadband',
245 'select' => join(', ',
249 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
251 'extra_sql' => $extra_sql,
252 'addl_from' => $addl_from,
253 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
254 'count_query' => $count_query,
258 =item search_sql STRING
260 Class method which returns an SQL fragment to search for the given string.
265 my( $class, $string ) = @_;
266 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
267 $class->search_sql_field('ip_addr', $string );
268 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
269 $class->search_sql_field('mac_addr', uc($string));
270 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
271 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
279 Returns the IP address.
288 =item insert [ , OPTION => VALUE ... ]
290 Adds this record to the database. If there is an error, returns the error,
291 otherwise returns false.
293 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
294 defined. An FS::cust_svc record will be created and inserted.
296 Currently available options are: I<depend_jobnum>
298 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
299 jobnums), all provisioning jobs will have a dependancy on the supplied
300 jobnum(s) (they will not run until the specific job(s) complete(s)).
304 # Standard FS::svc_Common::insert
308 Delete this record from the database.
312 # Standard FS::svc_Common::delete
314 =item replace OLD_RECORD
316 Replaces the OLD_RECORD with this one in the database. If there is an error,
317 returns the error, otherwise returns false.
321 # Standard FS::svc_Common::replace
325 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
329 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
333 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
337 Checks all fields to make sure this is a valid broadband service. If there is
338 an error, returns the error, otherwise returns false. Called by the insert
345 my $x = $self->setfixed;
347 return $x unless ref($x);
350 my $mac_addr = uc($self->get('mac_addr'));
351 $mac_addr =~ s/[-: ]//g;
352 $self->set('mac_addr', $mac_addr);
355 $self->ut_numbern('svcnum')
356 || $self->ut_numbern('blocknum')
357 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
358 || $self->ut_textn('description')
359 || $self->ut_numbern('speed_up')
360 || $self->ut_numbern('speed_down')
361 || $self->ut_ipn('ip_addr')
362 || $self->ut_hexn('mac_addr')
363 || $self->ut_hexn('auth_key')
364 || $self->ut_coordn('latitude')
365 || $self->ut_coordn('longitude')
366 || $self->ut_sfloatn('altitude')
367 || $self->ut_textn('vlan_profile')
368 || $self->ut_textn('plan_id')
370 return $error if $error;
372 if($self->speed_up < 0) { return 'speed_up must be positive'; }
373 if($self->speed_down < 0) { return 'speed_down must be positive'; }
375 my $cust_svc = $self->svcnum
376 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
380 $cust_pkg = $cust_svc->cust_pkg;
382 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
383 return "Invalid pkgnum" unless $cust_pkg;
386 if ($self->blocknum) {
387 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
388 return $error if $error;
391 if ($cust_pkg && $self->blocknum) {
392 my $addr_agentnum = $self->addr_block->agentnum;
393 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
394 return "Address block does not service this customer";
398 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
399 my $l = $cust_pkg->cust_location_or_main;
400 if ( $l->ship_latitude && $l->ship_longitude ) {
401 $self->latitude = $l->ship_latitude;
402 $self->longitude = $l->ship_longitude;
403 } elsif ( $l->latitude && $l->longitude ) {
404 $self->latitude = $l->latitude;
405 $self->longitude = $l->longitude;
409 $error = $self->_check_ip_addr;
410 return $error if $error;
418 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
420 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
422 return "Must supply either address or block"
423 unless $self->blocknum;
424 my $next_addr = $self->addr_block->next_free_addr;
426 $self->ip_addr($next_addr->addr);
428 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
433 if (not($self->blocknum)) {
434 return "Must supply either address or block"
435 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
436 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
437 map { $_->addr_block }
438 $self->allowed_routers;
439 if (scalar(@block)) {
440 $self->blocknum($block[0]->blocknum);
442 return "Address not with available block.";
446 # This should catch errors in the ip_addr. If it doesn't,
447 # they'll almost certainly not map into the block anyway.
448 my $self_addr = $self->NetAddr; #netmask is /32
449 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
451 my $block_addr = $self->addr_block->NetAddr;
452 unless ($block_addr->contains($self_addr)) {
453 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
456 my $router = $self->addr_block->router
457 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
458 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
461 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
467 sub _check_duplicate {
470 return "MAC already in use"
471 if ( $self->mac_addr &&
472 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
481 Returns a NetAddr::IP object containing the IP address of this service. The netmask
488 new NetAddr::IP ($self->ip_addr);
497 return '' unless $self->sectornum;
498 qsearchs('tower_sector', { sectornum => $self->sectornum });
503 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
509 qsearchs('addr_block', { blocknum => $self->blocknum });
514 =item allowed_routers
516 Returns a list of allowed FS::router objects.
520 sub allowed_routers {
522 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
527 The business with sb_field has been 'fixed', in a manner of speaking.
529 allowed_routers isn't agent virtualized because part_svc isn't agent
534 FS::svc_Common, FS::Record, FS::addr_block,
535 FS::part_svc, schema.html from the base documentation.