X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fsvc_broadband.pm;h=078df559ab087bb420af8bb8c9eb9de0cebdfc8f;hp=45f6c360168b88e6c9fbdbeb67d5c186daa14334;hb=4facec5414f411b97c9b19772c13f125a0b748cb;hpb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index 45f6c3601..078df559a 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -1,14 +1,22 @@ package FS::svc_broadband; +use base qw( + FS::svc_Radius_Mixin + FS::svc_Tower_Mixin + FS::svc_Torrus_Mixin + FS::svc_IP_Mixin + FS::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; @@ -69,6 +77,8 @@ customer's router will have the same address for both its internal and external interfaces thus saving address space. This has been found to work on most NAT routers available. +=item plan_id + =back =head1 METHODS @@ -85,80 +95,223 @@ points to. You can ask the object for a copy with the I method. =cut +sub table_info { + { + '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' => { + '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_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'; } -=item insert +sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); } -Adds this record to the database. If there is an error, returns the error, -otherwise returns false. +=item search HASHREF -The additional fields pkgnum and svcpart (see FS::cust_svc) should be -defined. An FS::cust_svc record will be created and inserted. +Class method which returns a qsearch hash expression to search for parameters +specified in HASHREF. -=cut +Parameters: -# Standard FS::svc_Common::insert +=over 4 -=item delete +=item unlinked - set to search for all unlinked services. Overrides all other options. -Delete this record from the database. +=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 -# Standard FS::svc_Common::delete +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"; + } + } -=item replace OLD_RECORD + #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) -Replaces the OLD_RECORD with this one in the database. If there is an error, -returns the error, otherwise returns false. + #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. =cut -# Standard FS::svc_Common::replace +sub search_sql { + my( $class, $string ) = @_; + if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) { + $class->search_sql_field('ip_addr', $string ); + } elsif ( $string =~ /^([A-F0-9]{12})$/i ) { + $class->search_sql_field('mac_addr', uc($string)); + } 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 sb_field +=item label -Returns a list of FS::sb_field objects assigned to this object. +Returns the IP address, MAC address and description. =cut -sub sb_field { +sub label { my $self = shift; - - return qsearch( 'sb_field', { svcnum => $self->svcnum } ); + 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 sb_field_hashref +=item insert [ , OPTION => VALUE ... ] + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +The additional fields pkgnum and svcpart (see FS::cust_svc) should be +defined. An FS::cust_svc record will be created and inserted. -Returns a hashref of the FS::sb_field key/value pairs for this object. +Currently available options are: I -Deprecated. Please don't use it. +If I is set (to a scalar jobnum or an array reference of +jobnums), all provisioning jobs will have a dependancy on the supplied +jobnum(s) (they will not run until the specific job(s) complete(s)). =cut -# Kristian wrote this, but don't hold it against him. He was under a powerful -# distracting influence whom he evidently found much more interesting than -# svc_broadband.pm. I can't say I blame him. +# Standard FS::svc_Common::insert -sub sb_field_hashref { - my $self = shift; - my $svcpart = shift; +=item delete - if ((not $svcpart) && ($self->cust_svc)) { - $svcpart = $self->cust_svc->svcpart; - } +Delete this record from the database. - my $hashref = {}; +=cut - map { - my $sb_field = qsearchs('sb_field', { sbfieldpart => $_->sbfieldpart, - svcnum => $self->svcnum }); - $hashref->{$_->getfield('name')} = $sb_field ? $sb_field->getfield('value') : ''; - } qsearch('part_sb_field', { svcpart => $svcpart }); +# Standard FS::svc_Common::delete - return $hashref; +=item replace OLD_RECORD -} +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +# Standard FS::svc_Common::replace =item suspend @@ -186,100 +339,185 @@ sub check { 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_foreign_key('blocknum', 'addr_block', 'blocknum') - || $self->ut_number('speed_up') - || $self->ut_number('speed_down') + || $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_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') + || $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; - if($self->speed_up < 0) { return 'speed_up must be positive'; } - if($self->speed_down < 0) { return 'speed_down must be positive'; } - - if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') { - $self->ip_addr($self->addr_block->next_free_addr->addr); - if (not $self->ip_addr) { - return "No free addresses in addr_block (blocknum: ".$self->blocknum.")"; - } + 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; } + my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg; - # 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; + # 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'; } - 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; + 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 ); + } } - - ''; #no error + $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; - return new NetAddr::IP ($self->ip_addr); -} - -=item addr_block + # 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.")"; + } -Returns the FS::addr_block record (i.e. the address block) for this broadband service. + ''; +} -=cut +#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"; + } + } -sub addr_block { - my $self = shift; + # 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; + } - return 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; - - return map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart }); -} - =head1 BUGS -I think there's one place in the code where we actually use sb_field_hashref. -That's a bug in itself. +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 -The real problem with it is that we're still grappling with the question of how -tightly xfields should be integrated with real fields. There are a few -different directions we could go with it--we I override several -functions in Record so that xfields behave almost exactly like real fields (can -be set with setfield(), appear in fields() and hash(), used as criteria in -qsearch(), etc.). +Having both routernum and blocknum as foreign keys is somewhat dubious. =head1 SEE ALSO -FS::svc_Common, FS::Record, FS::addr_block, FS::sb_field, +FS::svc_Common, FS::Record, FS::addr_block, FS::part_svc, schema.html from the base documentation. =cut