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
81 Creates a new svc_broadband. To add the record to the database, see
84 Note that this stores the hash reference, not a distinct copy of the hash it
85 points to. You can ask the object for a copy with the I<hash> method.
91 'name' => 'Broadband',
92 'name_plural' => 'Broadband services',
93 'longname_plural' => 'Fixed (username-less) broadband services',
94 'display_weight' => 50,
95 'cancel_weight' => 70,
97 'description' => 'Descriptive label for this particular device.',
98 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
99 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
100 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
101 'blocknum' => { 'label' => 'Address block',
103 'select_table' => 'addr_block',
104 'select_key' => 'blocknum',
105 'select_label' => 'cidr',
106 'disable_inventory' => 1,
112 sub table { 'svc_broadband'; }
114 sub table_dupcheck_fields { ( 'mac_addr' ); }
118 Class method which returns a qsearch hash expression to search for parameters
119 specified in HASHREF.
125 =item unlinked - set to search for all unlinked services. Overrides all other options.
135 =item pkgpart - arrayref
137 =item routernum - arrayref
146 my ($class, $params) = @_;
149 'LEFT JOIN cust_svc USING ( svcnum )',
150 'LEFT JOIN part_svc USING ( svcpart )',
151 'LEFT JOIN cust_pkg USING ( pkgnum )',
152 'LEFT JOIN cust_main USING ( custnum )',
155 # based on FS::svc_acct::search, probably the most mature of the bunch
157 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
160 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
161 push @where, "agentnum = $1";
163 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
164 'null_right' => 'View/link unlinked services',
165 'table' => 'cust_main'
169 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
170 push @where, "custnum = $1";
173 #pkgpart, now properly untainted, can be arrayref
174 for my $pkgpart ( $params->{'pkgpart'} ) {
175 if ( ref $pkgpart ) {
176 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
177 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
179 elsif ( $pkgpart =~ /^(\d+)$/ ) {
180 push @where, "cust_pkg.pkgpart = $1";
184 #routernum, can be arrayref
185 for my $routernum ( $params->{'routernum'} ) {
186 push @from, 'LEFT JOIN addr_block USING ( blocknum )';
187 if ( ref $routernum and grep { $_ } @$routernum ) {
188 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
189 push @where, "addr_block.routernum IN ($where)" if $where;
191 elsif ( $routernum =~ /^(\d+)$/ ) {
192 push @where, "addr_block.routernum = $1";
197 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
198 push @where, "svcnum = $1";
202 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
203 push @where, "svcpart = $1";
207 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
208 push @where, "ip_addr = '$1'";
212 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
213 push @where, "custnum = $1";
216 my $addl_from = join(' ', @from);
218 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
219 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
221 'table' => 'svc_broadband',
223 'select' => join(', ',
227 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
229 'extra_sql' => $extra_sql,
230 'addl_from' => $addl_from,
231 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
232 'count_query' => $count_query,
236 =item search_sql STRING
238 Class method which returns an SQL fragment to search for the given string.
243 my( $class, $string ) = @_;
244 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
245 $class->search_sql_field('ip_addr', $string );
246 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
247 $class->search_sql_field('mac_addr', uc($string));
248 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
249 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
257 Returns the IP address.
266 =item insert [ , OPTION => VALUE ... ]
268 Adds this record to the database. If there is an error, returns the error,
269 otherwise returns false.
271 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
272 defined. An FS::cust_svc record will be created and inserted.
274 Currently available options are: I<depend_jobnum>
276 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
277 jobnums), all provisioning jobs will have a dependancy on the supplied
278 jobnum(s) (they will not run until the specific job(s) complete(s)).
282 # Standard FS::svc_Common::insert
286 Delete this record from the database.
290 # Standard FS::svc_Common::delete
292 =item replace OLD_RECORD
294 Replaces the OLD_RECORD with this one in the database. If there is an error,
295 returns the error, otherwise returns false.
299 # Standard FS::svc_Common::replace
303 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
307 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
311 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
315 Checks all fields to make sure this is a valid broadband service. If there is
316 an error, returns the error, otherwise returns false. Called by the insert
323 my $x = $self->setfixed;
325 return $x unless ref($x);
328 $self->ut_numbern('svcnum')
329 || $self->ut_numbern('blocknum')
330 || $self->ut_textn('description')
331 || $self->ut_number('speed_up')
332 || $self->ut_number('speed_down')
333 || $self->ut_ipn('ip_addr')
334 || $self->ut_hexn('mac_addr')
335 || $self->ut_hexn('auth_key')
336 || $self->ut_coordn('latitude', -90, 90)
337 || $self->ut_coordn('longitude', -180, 180)
338 || $self->ut_sfloatn('altitude')
339 || $self->ut_textn('vlan_profile')
341 return $error if $error;
343 if($self->speed_up < 0) { return 'speed_up must be positive'; }
344 if($self->speed_down < 0) { return 'speed_down must be positive'; }
346 my $cust_svc = $self->svcnum
347 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
351 $cust_pkg = $cust_svc->cust_pkg;
353 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
354 return "Invalid pkgnum" unless $cust_pkg;
357 if ($self->blocknum) {
358 $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
359 return $error if $error;
362 if ($cust_pkg && $self->blocknum) {
363 my $addr_agentnum = $self->addr_block->agentnum;
364 if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
365 return "Address block does not service this customer";
369 $error = $self->_check_ip_addr;
370 return $error if $error;
378 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
380 return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
382 return "Must supply either address or block"
383 unless $self->blocknum;
384 my $next_addr = $self->addr_block->next_free_addr;
386 $self->ip_addr($next_addr->addr);
388 return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
393 if (not($self->blocknum)) {
394 return "Must supply either address or block"
395 unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
396 my @block = grep { $_->NetAddr->contains($self->NetAddr) }
397 map { $_->addr_block }
398 $self->allowed_routers;
399 if (scalar(@block)) {
400 $self->blocknum($block[0]->blocknum);
402 return "Address not with available block.";
406 # This should catch errors in the ip_addr. If it doesn't,
407 # they'll almost certainly not map into the block anyway.
408 my $self_addr = $self->NetAddr; #netmask is /32
409 return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
411 my $block_addr = $self->addr_block->NetAddr;
412 unless ($block_addr->contains($self_addr)) {
413 return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
416 my $router = $self->addr_block->router
417 or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
418 if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
421 return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
427 sub _check_duplicate {
430 return "MAC already in use"
431 if ( $self->mac_addr &&
432 scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
441 Returns a NetAddr::IP object containing the IP address of this service. The netmask
448 new NetAddr::IP ($self->ip_addr);
453 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
459 qsearchs('addr_block', { blocknum => $self->blocknum });
464 =item allowed_routers
466 Returns a list of allowed FS::router objects.
470 sub allowed_routers {
472 map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
477 The business with sb_field has been 'fixed', in a manner of speaking.
479 allowed_routers isn't agent virtualized because part_svc isn't agent
484 FS::svc_Common, FS::Record, FS::addr_block,
485 FS::part_svc, schema.html from the base documentation.