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_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',
122 sub table { 'svc_broadband'; }
124 sub table_dupcheck_fields { ( 'mac_addr' ); }
128 Class method which returns a qsearch hash expression to search for parameters
129 specified in HASHREF.
135 =item unlinked - set to search for all unlinked services. Overrides all other options.
145 =item pkgpart - arrayref
147 =item routernum - arrayref
156 my ($class, $params) = @_;
159 'LEFT JOIN cust_svc USING ( svcnum )',
160 'LEFT JOIN part_svc USING ( svcpart )',
161 'LEFT JOIN cust_pkg USING ( pkgnum )',
162 'LEFT JOIN cust_main USING ( custnum )',
165 # based on FS::svc_acct::search, probably the most mature of the bunch
167 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
170 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
171 push @where, "agentnum = $1";
173 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
174 'null_right' => 'View/link unlinked services',
175 'table' => 'cust_main'
179 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
180 push @where, "custnum = $1";
183 #pkgpart, now properly untainted, can be arrayref
184 for my $pkgpart ( $params->{'pkgpart'} ) {
185 if ( ref $pkgpart ) {
186 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
187 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
189 elsif ( $pkgpart =~ /^(\d+)$/ ) {
190 push @where, "cust_pkg.pkgpart = $1";
194 #routernum, can be arrayref
195 for my $routernum ( $params->{'routernum'} ) {
196 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
197 if ( ref $routernum and grep { $_ } @$routernum ) {
198 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
199 push @where, "addr_block.routernum IN ($where)" if $where;
201 elsif ( $routernum =~ /^(\d+)$/ ) {
202 push @where, "addr_block.routernum = $1";
207 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
208 push @where, "svcnum = $1";
212 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
213 push @where, "svcpart = $1";
217 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
218 push @where, "ip_addr = '$1'";
222 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
223 push @where, "custnum = $1";
226 my $addl_from = join(' ', @from);
228 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
229 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
231 'table' => 'svc_broadband',
233 'select' => join(', ',
237 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
239 'extra_sql' => $extra_sql,
240 'addl_from' => $addl_from,
241 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
242 'count_query' => $count_query,
246 =item search_sql STRING
248 Class method which returns an SQL fragment to search for the given string.
253 my( $class, $string ) = @_;
254 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
255 $class->search_sql_field('ip_addr', $string );
256 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
257 $class->search_sql_field('mac_addr', uc($string));
258 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
259 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
267 Returns the IP address.
276 =item insert [ , OPTION => VALUE ... ]
278 Adds this record to the database. If there is an error, returns the error,
279 otherwise returns false.
281 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
282 defined. An FS::cust_svc record will be created and inserted.
284 Currently available options are: I<depend_jobnum>
286 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
287 jobnums), all provisioning jobs will have a dependancy on the supplied
288 jobnum(s) (they will not run until the specific job(s) complete(s)).
292 # Standard FS::svc_Common::insert
296 Delete this record from the database.
300 # Standard FS::svc_Common::delete
302 =item replace OLD_RECORD
304 Replaces the OLD_RECORD with this one in the database. If there is an error,
305 returns the error, otherwise returns false.
309 # Standard FS::svc_Common::replace
313 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
317 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
321 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
325 Checks all fields to make sure this is a valid broadband service. If there is
326 an error, returns the error, otherwise returns false. Called by the insert
333 my $x = $self->setfixed;
335 return $x unless ref($x);
337 my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
338 my $lat_lower = $nw_coords ? 1 : -90;
339 my $lon_upper = $nw_coords ? -1 : 180;
342 $self->ut_numbern('svcnum')
343 || $self->ut_numbern('blocknum')
344 || $self->ut_textn('description')
345 || $self->ut_number('speed_up')
346 || $self->ut_number('speed_down')
347 || $self->ut_ipn('ip_addr')
348 || $self->ut_hexn('mac_addr')
349 || $self->ut_hexn('auth_key')
350 || $self->ut_coordn('latitude', $lat_lower, 90)
351 || $self->ut_coordn('longitude', -180, $lon_upper)
352 || $self->ut_sfloatn('altitude')
353 || $self->ut_textn('vlan_profile')
354 || $self->ut_textn('plan_id')
356 return $error if $error;
358 if($self->speed_up < 0) { return 'speed_up must be positive'; }
359 if($self->speed_down < 0) { return 'speed_down must be positive'; }
361 my $cust_svc = $self->svcnum
362 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
366 $cust_pkg = $cust_svc->cust_pkg;
368 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
369 return "Invalid pkgnum" unless $cust_pkg;
372 if ($self->blocknum) {
373 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
374 return $error if $error;
377 if ($cust_pkg && $self->blocknum) {
378 my $addr_agentnum = $self->addr_block->agentnum;
379 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
380 return "Address block does not service this customer";
384 $error = $self->_check_ip_addr;
385 return $error if $error;
393 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
395 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
397 return "Must supply either address or block"
398 unless $self->blocknum;
399 my $next_addr = $self->addr_block->next_free_addr;
401 $self->ip_addr($next_addr->addr);
403 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
408 if (not($self->blocknum)) {
409 return "Must supply either address or block"
410 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
411 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
412 map { $_->addr_block }
413 $self->allowed_routers;
414 if (scalar(@block)) {
415 $self->blocknum($block[0]->blocknum);
417 return "Address not with available block.";
421 # This should catch errors in the ip_addr. If it doesn't,
422 # they'll almost certainly not map into the block anyway.
423 my $self_addr = $self->NetAddr; #netmask is /32
424 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
426 my $block_addr = $self->addr_block->NetAddr;
427 unless ($block_addr->contains($self_addr)) {
428 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
431 my $router = $self->addr_block->router
432 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
433 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
436 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
442 sub _check_duplicate {
445 return "MAC already in use"
446 if ( $self->mac_addr &&
447 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
456 Returns a NetAddr::IP object containing the IP address of this service. The netmask
463 new NetAddr::IP ($self->ip_addr);
468 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
474 qsearchs('addr_block', { blocknum => $self->blocknum });
479 =item allowed_routers
481 Returns a list of allowed FS::router objects.
485 sub allowed_routers {
487 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
492 The business with sb_field has been 'fixed', in a manner of speaking.
494 allowed_routers isn't agent virtualized because part_svc isn't agent
499 FS::svc_Common, FS::Record, FS::addr_block,
500 FS::part_svc, schema.html from the base documentation.