package FS::svc_broadband;
+use base qw(
+ FS::svc_Radius_Mixin
+ FS::svc_Tower_Mixin
+ FS::svc_Torrus_Mixin
+ FS::svc_IP_Mixin
+ FS::svc_MAC_Mixin
+ FS::svc_Common
+);
use strict;
-use vars qw(@ISA $conf);
+use vars qw($conf);
+
+{ no warnings 'redefine'; use NetAddr::IP; }
use FS::Record qw( qsearchs qsearch dbh );
-use FS::svc_Common;
use FS::cust_svc;
use FS::addr_block;
-use NetAddr::IP;
-
-@ISA = qw( FS::svc_Common );
+use FS::part_svc_router;
+use FS::tower_sector;
$FS::UID::callback{'FS::svc_broadband'} = sub {
$conf = new FS::Conf;
interfaces thus saving address space. This has been found to work on most NAT
routers available.
+=item plan_id
+
=back
=head1 METHODS
sub table_info {
{
- 'name' => 'Broadband',
- 'name_plural' => 'Broadband services',
- 'longname_plural' => 'Fixed (username-less) broadband services',
+ 'name' => 'Wireless broadband',
+ 'name_plural' => 'Wireless broadband services',
+ 'longname_plural' => 'Fixed wireless broadband services',
'display_weight' => 50,
'cancel_weight' => 70,
+ 'ip_field' => 'ip_addr',
+ 'manual_require' => 1,
'fields' => {
- 'description' => 'Descriptive label for this particular device.',
- 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
- 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
- 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
- 'blocknum' => { 'label' => 'Address block',
+ 'svcnum' => 'Service',
+ 'description' => 'Descriptive label',
+ 'speed_down' => 'Download speed (Kbps)',
+ 'speed_up' => 'Upload speed (Kbps)',
+ 'ip_addr' => 'IP address',
+ 'blocknum' =>
+ { 'label' => 'Address block',
'type' => 'select',
'select_table' => 'addr_block',
- 'select_key' => 'blocknum',
+ 'select_key' => 'blocknum',
'select_label' => 'cidr',
'disable_inventory' => 1,
},
+ 'plan_id' => 'Service Plan Id',
+ 'performance_profile' => 'Peformance Profile',
+ 'authkey' => 'Authentication key',
+ 'mac_addr' => 'MAC address',
+ 'latitude' => 'Latitude',
+ 'longitude' => 'Longitude',
+ 'altitude' => 'Altitude',
+ 'vlan_profile' => 'VLAN profile',
+ 'sectornum' => 'Tower/sector',
+ 'routernum' => 'Router/block',
+ 'usergroup' => {
+ label => 'RADIUS groups',
+ type => 'select-radius_group.html',
+ #select_table => 'radius_group',
+ #select_key => 'groupnum',
+ #select_label => 'groupname',
+ disable_inventory => 1,
+ multiple => 1,
+ },
+ 'radio_serialnum' => 'Radio Serial Number',
+ 'radio_location' => 'Radio Location',
+ 'poe_location' => 'POE Location',
+ 'rssi' => 'RSSI',
+ 'suid' => 'SUID',
+ 'shared_svcnum' => { label => 'Shared Service',
+ type => 'search-svc_broadband',
+ disable_inventory => 1,
+ },
+ 'serviceid' => 'Torrus serviceid', #but is should be hidden
},
};
}
sub table { 'svc_broadband'; }
+sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF.
+
+Parameters:
+
+=over 4
+
+=item unlinked - set to search for all unlinked services. Overrides all other options.
+
+=item agentnum
+
+=item custnum
+
+=item svcpart
+
+=item ip_addr
+
+=item pkgpart - arrayref
+
+=item routernum - arrayref
+
+=item sectornum - arrayref
+
+=item towernum - arrayref
+
+=item order_by
+
+=back
+
+=cut
+
+sub _search_svc {
+ my( $class, $params, $from, $where ) = @_;
+
+ #routernum, can be arrayref
+ for my $routernum ( $params->{'routernum'} ) {
+ # this no longer uses addr_block
+ if ( ref $routernum and grep { $_ } @$routernum ) {
+ my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
+ my @orwhere = ();
+ push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
+ push @orwhere, "svc_broadband.routernum IS NULL"
+ if grep /^none$/, @$routernum;
+ push @$where, '( '.join(' OR ', @orwhere).' )';
+ }
+ elsif ( $routernum =~ /^(\d+)$/ ) {
+ push @$where, "svc_broadband.routernum = $1";
+ }
+ elsif ( $routernum eq 'none' ) {
+ push @$where, "svc_broadband.routernum IS NULL";
+ }
+ }
+
+ #this should probably move to svc_Tower_Mixin, or maybe we never should have
+ # done svc_acct # towers (or, as mark thought, never should have done
+ # svc_broadband)
+
+ #sector and tower, as above
+ my @where_sector = $class->tower_sector_sql($params);
+ if ( @where_sector ) {
+ push @$where, @where_sector;
+ push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
+ }
+
+ #ip_addr
+ if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
+ push @$where, "ip_addr = '$1'";
+ }
+
+}
+
=item search_sql STRING
Class method which returns an SQL fragment to search for the given string.
my( $class, $string ) = @_;
if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
$class->search_sql_field('ip_addr', $string );
- }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
+ } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
$class->search_sql_field('mac_addr', uc($string));
- }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
- $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
+ } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
+ $string =~ s/://g;
+ $class->search_sql_field('mac_addr', uc($string) );
+ } elsif ( $string =~ /^(\d+)$/ ) {
+ my $table = $class->table;
+ "$table.svcnum = $1";
} else {
'1 = 0'; #false
}
}
+=item smart_search STRING
+
+=cut
+
+sub smart_search {
+ my( $class, $string ) = @_;
+ qsearch({
+ 'table' => $class->table, #'svc_broadband',
+ 'hashref' => {},
+ 'extra_sql' => 'WHERE '. $class->search_sql($string),
+ });
+}
+
=item label
-Returns the IP address.
+Returns the IP address, MAC address and description.
=cut
sub label {
my $self = shift;
- $self->ip_addr;
+ my $label = 'IP:'. ($self->ip_addr || 'Unknown');
+ $label .= ', MAC:'. $self->mac_addr
+ if $self->mac_addr;
+ $label .= ' ('. $self->description. ')'
+ if $self->description;
+ return $label;
}
=item insert [ , OPTION => VALUE ... ]
Replaces the OLD_RECORD with this one in the database. If there is an error,
returns the error, otherwise returns false.
-=cut
-
# Standard FS::svc_Common::replace
=item suspend
return $x unless ref($x);
+ # remove delimiters
+ my $mac_addr = uc($self->get('mac_addr'));
+ $mac_addr =~ s/[\W_]//g;
+ $self->set('mac_addr', $mac_addr);
+
my $error =
$self->ut_numbern('svcnum')
|| $self->ut_numbern('blocknum')
+ || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
+ || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
|| $self->ut_textn('description')
- || $self->ut_number('speed_up')
- || $self->ut_number('speed_down')
+ || $self->ut_numbern('speed_up')
+ || $self->ut_numbern('speed_down')
|| $self->ut_ipn('ip_addr')
|| $self->ut_hexn('mac_addr')
|| $self->ut_hexn('auth_key')
- || $self->ut_coordn('latitude', -90, 90)
- || $self->ut_coordn('longitude', -180, 180)
+ || $self->ut_coordn('latitude')
+ || $self->ut_coordn('longitude')
|| $self->ut_sfloatn('altitude')
|| $self->ut_textn('vlan_profile')
+ || $self->ut_textn('plan_id')
+ || $self->ut_alphan('radio_serialnum')
+ || $self->ut_textn('radio_location')
+ || $self->ut_textn('poe_location')
+ || $self->ut_snumbern('rssi')
+ || $self->ut_numbern('suid')
+ || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
+ || $self->ut_textn('serviceid') #too lenient?
;
return $error if $error;
- #redundant, but better error message
- return "MAC already in use"
- if scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) );
-
- if($self->speed_up < 0) { return 'speed_up must be positive'; }
- if($self->speed_down < 0) { return 'speed_down must be positive'; }
+ if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
+ if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
my $cust_svc = $self->svcnum
? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
: '';
my $cust_pkg;
+ my $svcpart;
if ($cust_svc) {
$cust_pkg = $cust_svc->cust_pkg;
+ $svcpart = $cust_svc->svcpart;
}else{
$cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
return "Invalid pkgnum" unless $cust_pkg;
+ $svcpart = $self->svcpart;
}
-
- if ($self->blocknum) {
- $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
- return $error if $error;
- }
+ my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
- if ($cust_pkg && $self->blocknum) {
- my $addr_agentnum = $self->addr_block->agentnum;
- if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
- return "Address block does not service this customer";
- }
- }
-
- if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
- return "Must supply either address or block"
- unless $self->blocknum;
- my $next_addr = $self->addr_block->next_free_addr;
- if ($next_addr) {
- $self->ip_addr($next_addr->addr);
- } else {
- return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
- }
+ # assign IP address / router / block
+ $error = $self->svc_ip_check;
+ return $error if $error;
+ if ( !$self->ip_addr
+ and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
+ return 'IP address is required';
}
- if (not($self->blocknum)) {
- return "Must supply either address or block"
- unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
- my @block = grep { $_->NetAddr->contains($self->NetAddr) }
- map { $_->addr_block }
- $self->allowed_routers;
- if (scalar(@block)) {
- $self->blocknum($block[0]->blocknum);
- }else{
- return "Address not with available block.";
+ if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
+ my $l = $cust_pkg->cust_location_or_main;
+ if ( $l->ship_latitude && $l->ship_longitude ) {
+ $self->latitude( $l->ship_latitude );
+ $self->longitude( $l->ship_longitude );
+ } elsif ( $l->latitude && $l->longitude ) {
+ $self->latitude( $l->latitude );
+ $self->longitude( $l->longitude );
}
}
- # This should catch errors in the ip_addr. If it doesn't,
- # they'll almost certainly not map into the block anyway.
- my $self_addr = $self->NetAddr; #netmask is /32
- return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
-
- my $block_addr = $self->addr_block->NetAddr;
- unless ($block_addr->contains($self_addr)) {
- return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
- }
-
- my $router = $self->addr_block->router
- or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
- if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
- } # do nothing
- else {
- return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
- }
-
$self->SUPER::check;
}
-=item NetAddr
-
-Returns a NetAddr::IP object containing the IP address of this service. The netmask
-is /32.
-
-=cut
-
-sub NetAddr {
+sub _check_duplicate {
my $self = shift;
- new NetAddr::IP ($self->ip_addr);
-}
+ # Not a reliable check because the table isn't locked, but
+ # that's why we have a unique index. This is just to give a
+ # friendlier error message.
+ my @dup;
+ @dup = $self->find_duplicates('global', 'mac_addr');
+ if ( @dup ) {
+ return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
+ }
-=item addr_block
+ '';
+}
-Returns the FS::addr_block record (i.e. the address block) for this broadband service.
+#class method
+sub _upgrade_data {
+ my $class = shift;
+
+ local($FS::svc_Common::noexport_hack) = 1;
+
+ # fix wrong-case MAC addresses
+ my $dbh = dbh;
+ $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
+ or die $dbh->errstr;
+
+ # set routernum to addr_block.routernum
+ foreach my $self (qsearch('svc_broadband', {
+ blocknum => {op => '!=', value => ''},
+ routernum => ''
+ })) {
+ my $addr_block = $self->addr_block;
+ if ( !$addr_block ) {
+ # super paranoid mode
+ warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
+ next;
+ }
+ my $ip_addr = $self->ip_addr;
+ my $routernum = $addr_block->routernum;
+ if ( $routernum ) {
+ $self->set(routernum => $routernum);
+ my $error = $self->check;
+ # sanity check: don't allow this to change IP address or block
+ # (other than setting blocknum to null for a non-auto-assigned router)
+ if ( $self->ip_addr ne $ip_addr
+ or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
+ warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
+ next;
+ }
+
+ $error ||= $self->replace;
+ warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
+ ":\n$error; skipped\n"
+ if $error;
+ }
+ else {
+ warn "svcnum ".$self->svcnum.
+ ": no routernum in address block ".$addr_block->cidr.", skipped\n";
+ }
+ }
-=cut
+ # assign blocknums to services that should have them
+ my @all_blocks = qsearch('addr_block', { });
+ SVC: foreach my $self (
+ qsearch({
+ 'select' => 'svc_broadband.*',
+ 'table' => 'svc_broadband',
+ 'addl_from' => 'JOIN router USING (routernum)',
+ 'hashref' => {},
+ 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
+ 'AND router.manual_addr IS NULL',
+ })
+ ) {
+
+ next SVC if $self->ip_addr eq '';
+ my $NetAddr = $self->NetAddr;
+ # inefficient, but should only need to run once
+ foreach my $block (@all_blocks) {
+ if ($block->NetAddr->contains($NetAddr)) {
+ $self->set(blocknum => $block->blocknum);
+ my $error = $self->replace;
+ warn "WARNING: error assigning blocknum ".$block->blocknum.
+ " to service ".$self->svcnum."\n$error; skipped\n"
+ if $error;
+ next SVC;
+ }
+ }
+ warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
+ $self->svcnum;
+ #next SVC;
+ }
-sub addr_block {
- my $self = shift;
- qsearchs('addr_block', { blocknum => $self->blocknum });
+ '';
}
=back
-=item allowed_routers
-
-Returns a list of allowed FS::router objects.
-
-=cut
-
-sub allowed_routers {
- my $self = shift;
- map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
-}
-
=head1 BUGS
The business with sb_field has been 'fixed', in a manner of speaking.
allowed_routers isn't agent virtualized because part_svc isn't agent
virtualized
+Having both routernum and blocknum as foreign keys is somewhat dubious.
+
=head1 SEE ALSO
FS::svc_Common, FS::Record, FS::addr_block,