1 package FS::svc_broadband;
4 use vars qw(@ISA $conf);
5 use FS::Record qw( qsearchs qsearch dbh );
9 use FS::part_svc_router;
12 @ISA = qw( FS::svc_Radius_Mixin FS::svc_Common );
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' => 'Broadband',
94 'name_plural' => 'Broadband services',
95 'longname_plural' => 'Fixed (username-less) broadband services',
96 'display_weight' => 50,
97 'cancel_weight' => 70,
99 'description' => 'Descriptive label for this particular device.',
100 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
101 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
102 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
103 'blocknum' => { 'label' => 'Address block',
105 'select_table' => 'addr_block',
106 'select_key' => 'blocknum',
107 'select_label' => 'cidr',
108 'disable_inventory' => 1,
110 'plan_id' => 'Service Plan Id',
111 'performance_profile' => 'Peformance Profile',
112 'authkey' => 'Authentication key',
113 'mac_addr' => 'MAC address',
114 'latitude' => 'Latitude',
115 'longitude' => 'Longitude',
116 'altitude' => 'Altitude',
117 'vlan_profile' => 'VLAN profile',
119 label => 'RADIUS groups',
120 type => 'select-radius_group.html',
121 #select_table => 'radius_group',
122 #select_key => 'groupnum',
123 #select_label => 'groupname',
124 disable_inventory => 1,
131 sub table { 'svc_broadband'; }
133 sub table_dupcheck_fields { ( 'mac_addr' ); }
137 Class method which returns a qsearch hash expression to search for parameters
138 specified in HASHREF.
144 =item unlinked - set to search for all unlinked services. Overrides all other options.
154 =item pkgpart - arrayref
156 =item routernum - arrayref
165 my ($class, $params) = @_;
168 'LEFT JOIN cust_svc USING ( svcnum )',
169 'LEFT JOIN part_svc USING ( svcpart )',
170 'LEFT JOIN cust_pkg USING ( pkgnum )',
171 'LEFT JOIN cust_main USING ( custnum )',
174 # based on FS::svc_acct::search, probably the most mature of the bunch
176 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
179 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
180 push @where, "cust_main.agentnum = $1";
182 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
183 'null_right' => 'View/link unlinked services',
184 'table' => 'cust_main'
188 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
189 push @where, "custnum = $1";
192 #pkgpart, now properly untainted, can be arrayref
193 for my $pkgpart ( $params->{'pkgpart'} ) {
194 if ( ref $pkgpart ) {
195 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
196 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
198 elsif ( $pkgpart =~ /^(\d+)$/ ) {
199 push @where, "cust_pkg.pkgpart = $1";
203 #routernum, can be arrayref
204 for my $routernum ( $params->{'routernum'} ) {
205 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
206 if ( ref $routernum and grep { $_ } @$routernum ) {
207 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
208 push @where, "addr_block.routernum IN ($where)" if $where;
210 elsif ( $routernum =~ /^(\d+)$/ ) {
211 push @where, "addr_block.routernum = $1";
216 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
217 push @where, "svcnum = $1";
221 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
222 push @where, "svcpart = $1";
226 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
227 push @where, "ip_addr = '$1'";
231 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
232 push @where, "custnum = $1";
235 my $addl_from = join(' ', @from);
237 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
238 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
240 'table' => 'svc_broadband',
242 'select' => join(', ',
246 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
248 'extra_sql' => $extra_sql,
249 'addl_from' => $addl_from,
250 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
251 'count_query' => $count_query,
255 =item search_sql STRING
257 Class method which returns an SQL fragment to search for the given string.
262 my( $class, $string ) = @_;
263 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
264 $class->search_sql_field('ip_addr', $string );
265 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
266 $class->search_sql_field('mac_addr', uc($string));
267 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
268 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
276 Returns the IP address.
285 =item insert [ , OPTION => VALUE ... ]
287 Adds this record to the database. If there is an error, returns the error,
288 otherwise returns false.
290 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
291 defined. An FS::cust_svc record will be created and inserted.
293 Currently available options are: I<depend_jobnum>
295 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
296 jobnums), all provisioning jobs will have a dependancy on the supplied
297 jobnum(s) (they will not run until the specific job(s) complete(s)).
301 # Standard FS::svc_Common::insert
305 Delete this record from the database.
309 # Standard FS::svc_Common::delete
311 =item replace OLD_RECORD
313 Replaces the OLD_RECORD with this one in the database. If there is an error,
314 returns the error, otherwise returns false.
318 # Standard FS::svc_Common::replace
322 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
326 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
330 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
334 Checks all fields to make sure this is a valid broadband service. If there is
335 an error, returns the error, otherwise returns false. Called by the insert
342 my $x = $self->setfixed;
344 return $x unless ref($x);
346 my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
347 my $lat_lower = $nw_coords ? 1 : -90;
348 my $lon_upper = $nw_coords ? -1 : 180;
351 $self->ut_numbern('svcnum')
352 || $self->ut_numbern('blocknum')
353 || $self->ut_textn('description')
354 || $self->ut_number('speed_up')
355 || $self->ut_number('speed_down')
356 || $self->ut_ipn('ip_addr')
357 || $self->ut_hexn('mac_addr')
358 || $self->ut_hexn('auth_key')
359 || $self->ut_coordn('latitude', $lat_lower, 90)
360 || $self->ut_coordn('longitude', -180, $lon_upper)
361 || $self->ut_sfloatn('altitude')
362 || $self->ut_textn('vlan_profile')
363 || $self->ut_textn('plan_id')
365 return $error if $error;
367 if($self->speed_up < 0) { return 'speed_up must be positive'; }
368 if($self->speed_down < 0) { return 'speed_down must be positive'; }
370 my $cust_svc = $self->svcnum
371 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
375 $cust_pkg = $cust_svc->cust_pkg;
377 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
378 return "Invalid pkgnum" unless $cust_pkg;
381 if ($self->blocknum) {
382 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
383 return $error if $error;
386 if ($cust_pkg && $self->blocknum) {
387 my $addr_agentnum = $self->addr_block->agentnum;
388 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
389 return "Address block does not service this customer";
393 $error = $self->_check_ip_addr;
394 return $error if $error;
402 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
404 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
406 return "Must supply either address or block"
407 unless $self->blocknum;
408 my $next_addr = $self->addr_block->next_free_addr;
410 $self->ip_addr($next_addr->addr);
412 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
417 if (not($self->blocknum)) {
418 return "Must supply either address or block"
419 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
420 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
421 map { $_->addr_block }
422 $self->allowed_routers;
423 if (scalar(@block)) {
424 $self->blocknum($block[0]->blocknum);
426 return "Address not with available block.";
430 # This should catch errors in the ip_addr. If it doesn't,
431 # they'll almost certainly not map into the block anyway.
432 my $self_addr = $self->NetAddr; #netmask is /32
433 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
435 my $block_addr = $self->addr_block->NetAddr;
436 unless ($block_addr->contains($self_addr)) {
437 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
440 my $router = $self->addr_block->router
441 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
442 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
445 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
451 sub _check_duplicate {
454 return "MAC already in use"
455 if ( $self->mac_addr &&
456 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
465 Returns a NetAddr::IP object containing the IP address of this service. The netmask
472 new NetAddr::IP ($self->ip_addr);
477 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
483 qsearchs('addr_block', { blocknum => $self->blocknum });
488 =item allowed_routers
490 Returns a list of allowed FS::router objects.
494 sub allowed_routers {
496 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
501 The business with sb_field has been 'fixed', in a manner of speaking.
503 allowed_routers isn't agent virtualized because part_svc isn't agent
508 FS::svc_Common, FS::Record, FS::addr_block,
509 FS::part_svc, schema.html from the base documentation.