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,
98 'ip_field' => 'ip_addr',
100 'description' => 'Descriptive label for this particular device.',
101 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
102 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
103 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
104 'blocknum' => { 'label' => 'Address block',
106 'select_table' => 'addr_block',
107 'select_key' => 'blocknum',
108 'select_label' => 'cidr',
109 'disable_inventory' => 1,
111 'plan_id' => 'Service Plan Id',
112 'performance_profile' => 'Peformance Profile',
113 'authkey' => 'Authentication key',
114 'mac_addr' => 'MAC address',
115 'latitude' => 'Latitude',
116 'longitude' => 'Longitude',
117 'altitude' => 'Altitude',
118 'vlan_profile' => 'VLAN profile',
120 label => 'RADIUS groups',
121 type => 'select-radius_group.html',
122 #select_table => 'radius_group',
123 #select_key => 'groupnum',
124 #select_label => 'groupname',
125 disable_inventory => 1,
132 sub table { 'svc_broadband'; }
134 sub table_dupcheck_fields { ( 'mac_addr' ); }
138 Class method which returns a qsearch hash expression to search for parameters
139 specified in HASHREF.
145 =item unlinked - set to search for all unlinked services. Overrides all other options.
155 =item pkgpart - arrayref
157 =item routernum - arrayref
166 my ($class, $params) = @_;
169 'LEFT JOIN cust_svc USING ( svcnum )',
170 'LEFT JOIN part_svc USING ( svcpart )',
171 'LEFT JOIN cust_pkg USING ( pkgnum )',
172 'LEFT JOIN cust_main USING ( custnum )',
175 # based on FS::svc_acct::search, probably the most mature of the bunch
177 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
180 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
181 push @where, "cust_main.agentnum = $1";
183 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
184 'null_right' => 'View/link unlinked services',
185 'table' => 'cust_main'
189 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
190 push @where, "custnum = $1";
193 #pkgpart, now properly untainted, can be arrayref
194 for my $pkgpart ( $params->{'pkgpart'} ) {
195 if ( ref $pkgpart ) {
196 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
197 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
199 elsif ( $pkgpart =~ /^(\d+)$/ ) {
200 push @where, "cust_pkg.pkgpart = $1";
204 #routernum, can be arrayref
205 for my $routernum ( $params->{'routernum'} ) {
206 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
207 if ( ref $routernum and grep { $_ } @$routernum ) {
208 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
209 push @where, "addr_block.routernum IN ($where)" if $where;
211 elsif ( $routernum =~ /^(\d+)$/ ) {
212 push @where, "addr_block.routernum = $1";
217 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
218 push @where, "svcnum = $1";
222 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
223 push @where, "svcpart = $1";
227 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
228 push @where, "ip_addr = '$1'";
232 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
233 push @where, "custnum = $1";
236 my $addl_from = join(' ', @from);
238 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
239 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
241 'table' => 'svc_broadband',
243 'select' => join(', ',
247 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
249 'extra_sql' => $extra_sql,
250 'addl_from' => $addl_from,
251 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
252 'count_query' => $count_query,
256 =item search_sql STRING
258 Class method which returns an SQL fragment to search for the given string.
263 my( $class, $string ) = @_;
264 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
265 $class->search_sql_field('ip_addr', $string );
266 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
267 $class->search_sql_field('mac_addr', uc($string));
268 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
269 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
277 Returns the IP address.
286 =item insert [ , OPTION => VALUE ... ]
288 Adds this record to the database. If there is an error, returns the error,
289 otherwise returns false.
291 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
292 defined. An FS::cust_svc record will be created and inserted.
294 Currently available options are: I<depend_jobnum>
296 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
297 jobnums), all provisioning jobs will have a dependancy on the supplied
298 jobnum(s) (they will not run until the specific job(s) complete(s)).
302 # Standard FS::svc_Common::insert
306 Delete this record from the database.
310 # Standard FS::svc_Common::delete
312 =item replace OLD_RECORD
314 Replaces the OLD_RECORD with this one in the database. If there is an error,
315 returns the error, otherwise returns false.
319 # Standard FS::svc_Common::replace
323 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
327 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
331 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
335 Checks all fields to make sure this is a valid broadband service. If there is
336 an error, returns the error, otherwise returns false. Called by the insert
343 my $x = $self->setfixed;
345 return $x unless ref($x);
347 my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
348 my $lat_lower = $nw_coords ? 1 : -90;
349 my $lon_upper = $nw_coords ? -1 : 180;
352 my $mac_addr = uc($self->get('mac_addr'));
353 $mac_addr =~ s/[-: ]//g;
354 $self->set('mac_addr', $mac_addr);
357 $self->ut_numbern('svcnum')
358 || $self->ut_numbern('blocknum')
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', $lat_lower, 90)
366 || $self->ut_coordn('longitude', -180, $lon_upper)
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 $error = $self->_check_ip_addr;
400 return $error if $error;
408 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
410 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
412 return "Must supply either address or block"
413 unless $self->blocknum;
414 my $next_addr = $self->addr_block->next_free_addr;
416 $self->ip_addr($next_addr->addr);
418 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
423 if (not($self->blocknum)) {
424 return "Must supply either address or block"
425 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
426 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
427 map { $_->addr_block }
428 $self->allowed_routers;
429 if (scalar(@block)) {
430 $self->blocknum($block[0]->blocknum);
432 return "Address not with available block.";
436 # This should catch errors in the ip_addr. If it doesn't,
437 # they'll almost certainly not map into the block anyway.
438 my $self_addr = $self->NetAddr; #netmask is /32
439 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
441 my $block_addr = $self->addr_block->NetAddr;
442 unless ($block_addr->contains($self_addr)) {
443 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
446 my $router = $self->addr_block->router
447 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
448 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
451 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
457 sub _check_duplicate {
460 return "MAC already in use"
461 if ( $self->mac_addr &&
462 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
471 Returns a NetAddr::IP object containing the IP address of this service. The netmask
478 new NetAddr::IP ($self->ip_addr);
483 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
489 qsearchs('addr_block', { blocknum => $self->blocknum });
494 =item allowed_routers
496 Returns a list of allowed FS::router objects.
500 sub allowed_routers {
502 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
507 The business with sb_field has been 'fixed', in a manner of speaking.
509 allowed_routers isn't agent virtualized because part_svc isn't agent
514 FS::svc_Common, FS::Record, FS::addr_block,
515 FS::part_svc, schema.html from the base documentation.