From eb9d1063e1203231ee0c6922ea5638370f7b5ece Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 27 Feb 2012 10:31:47 -0800 Subject: [PATCH] allow svc_broadband to link directly to a router, #14698 --- FS/FS/Schema.pm | 2 + FS/FS/Upgrade.pm | 5 +- FS/FS/addr_block.pm | 39 +++-- FS/FS/part_export/router.pm | 4 +- FS/FS/router.pm | 16 ++ FS/FS/svc_broadband.pm | 192 ++++++++++++++------- httemplate/browse/router.cgi | 4 + httemplate/edit/router.cgi | 3 + httemplate/edit/svc_broadband.cgi | 5 +- httemplate/elements/select-tiered.html | 12 +- httemplate/elements/tr-select-router_block_ip.html | 94 ++++++++++ httemplate/search/report_svc_broadband.html | 4 + httemplate/search/svc_broadband.cgi | 4 +- httemplate/view/svc_broadband.cgi | 9 +- 14 files changed, 297 insertions(+), 96 deletions(-) create mode 100644 httemplate/elements/tr-select-router_block_ip.html diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 483c5e00a..b2dddcafc 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2484,6 +2484,7 @@ sub tables_hashref { 'routername', 'varchar', '', $char_d, '', '', 'svcnum', 'int', 'NULL', '', '', '', 'agentnum', 'int', 'NULL', '', '', '', + 'auto_addr', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'routernum', 'unique' => [], @@ -2519,6 +2520,7 @@ sub tables_hashref { 'columns' => [ 'svcnum', 'int', '', '', '', '', 'description', 'varchar', 'NULL', $char_d, '', '', + 'routernum', 'int', 'NULL', '', '', '', 'blocknum', 'int', 'NULL', '', '', '', 'sectornum', 'int', 'NULL', '', '', '', 'speed_up', 'int', 'NULL', '', '', '', diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index d00bb5cd0..8f66c66b5 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -264,7 +264,10 @@ sub upgrade_data { 'part_export' => [], #insert default tower_sector if not present - 'tower', + 'tower' => [], + + #routernum/blocknum + 'svc_broadband' => [], ; \%hash; diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index be42cb50b..e00f587c6 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -8,6 +8,7 @@ use FS::svc_broadband; use FS::Conf; use NetAddr::IP; use Carp qw( carp ); +use List::Util qw( first ); @ISA = qw( FS::Record ); @@ -222,37 +223,43 @@ sub cidr { $self->NetAddr->cidr; } -=item next_free_addr +=item free_addrs Returns a NetAddr::IP object corresponding to the first unassigned address in the block (other than the network, broadcast, or gateway address). If -there are no free addresses, returns false. There are never free addresses +there are no free addresses, returns nothing. There are never free addresses when manual_flag is true. +=item next_free_addr + +Returns a NetAddr::IP object for the first unassigned address in the block, +or '' if there are none. + =cut -sub next_free_addr { +sub free_addrs { my $self = shift; - return '' if $self->manual_flag; + return if $self->manual_flag; my $conf = new FS::Conf; my @excludeaddr = $conf->config('exclude_ip_addr'); -my @used = -( (map { $_->NetAddr->addr } - ($self, - qsearch('svc_broadband', { blocknum => $self->blocknum })) - ), @excludeaddr -); - - my @free = $self->NetAddr->hostenum; - while (my $ip = shift @free) { - if (not grep {$_ eq $ip->addr;} @used) { return $ip; }; - } + my %used = map { $_ => 1 } + ( + (map { $_->NetAddr->addr } + ($self, + qsearch('svc_broadband', { blocknum => $self->blocknum })) + ), @excludeaddr + ); - ''; + grep { !$used{$_->addr} } $self->NetAddr->hostenum; + +} +sub next_free_addr { + my $self = shift; + ($self->free_addrs, '')[0] } =item allocate -- deprecated diff --git a/FS/FS/part_export/router.pm b/FS/FS/part_export/router.pm index 42aa51cf6..6a1d676f4 100644 --- a/FS/FS/part_export/router.pm +++ b/FS/FS/part_export/router.pm @@ -302,13 +302,13 @@ sub _queue { } sub _get_router { - my ($self, $svc_broadband, %args) = (shift, shift, shift, @_); + my ($self, $svc_broadband, %args) = (shift, shift, @_); my $router; if ($args{'routernum'}) { $router = qsearchs('router', { routernum => $args{'routernum'}}); } else { - $router = $svc_broadband->addr_block->router; + $router = $svc_broadband->router; } return($router); diff --git a/FS/FS/router.pm b/FS/FS/router.pm index f66f2cee8..99373e5d1 100755 --- a/FS/FS/router.pm +++ b/FS/FS/router.pm @@ -40,6 +40,9 @@ fields are currently supported: =item svcnum - svcnum of the owning FS::svc_broadband, if appropriate +=item auto_addr - flag to automatically assign IP addresses to services +linked to this router ('Y' or null). + =back =head1 METHODS @@ -83,6 +86,7 @@ sub check { my $error = $self->ut_numbern('routernum') || $self->ut_text('routername') + || $self->ut_enum('auto_addr', [ '', 'Y' ]) || $self->ut_agentnum_acl('agentnum', 'Broadband global configuration') ; return $error if $error; @@ -128,6 +132,11 @@ sub delete { Returns a list of FS::addr_block objects (address blocks) associated with this object. +=item auto_addr_block + +Returns a list of address blocks on which auto-assignment of IP addresses +is enabled. + =cut sub addr_block { @@ -135,6 +144,13 @@ sub addr_block { return qsearch('addr_block', { routernum => $self->routernum }); } +sub auto_addr_block { + my $self = shift; + return () if !$self->auto_addr; + return qsearch('addr_block', { routernum => $self->routernum, + manual_flag => '' }); +} + =item part_svc_router Returns a list of FS::part_svc_router objects associated with this diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index f5aa2fe9d..ac463bc5c 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -101,15 +101,15 @@ sub table_info { '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.', - 'sectornum' => 'Tower sector', - 'blocknum' => { 'label' => 'Address block', - 'type' => 'select', - 'select_table' => 'addr_block', - 'select_key' => 'blocknum', - 'select_label' => 'cidr', - 'disable_inventory' => 1, - }, + #'ip_addr' => 'IP address. Leave blank for automatic assignment.', + #'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', @@ -118,6 +118,8 @@ sub table_info { 'longitude' => 'Longitude', 'altitude' => 'Altitude', 'vlan_profile' => 'VLAN profile', + 'sectornum' => 'Tower/sector', + 'routernum' => 'Router/block', 'usergroup' => { label => 'RADIUS groups', type => 'select-radius_group.html', @@ -209,13 +211,20 @@ sub search { #routernum, can be arrayref for my $routernum ( $params->{'routernum'} ) { - push @from, 'LEFT JOIN addr_block USING ( blocknum )'; + # this no longer uses addr_block if ( ref $routernum and grep { $_ } @$routernum ) { - my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum ); - push @where, "addr_block.routernum IN ($where)" if $where; + 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, "addr_block.routernum = $1"; + push @where, "svc_broadband.routernum = $1"; + } + elsif ( $routernum eq 'none' ) { + push @where, "svc_broadband.routernum IS NULL"; } } @@ -327,8 +336,6 @@ Delete this record from the database. 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 @@ -365,6 +372,7 @@ sub check { 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_numbern('speed_up') @@ -380,8 +388,8 @@ sub check { ; 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(($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 } ) @@ -393,18 +401,27 @@ sub check { $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } ); return "Invalid pkgnum" unless $cust_pkg; } - - 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 ($self->routernum) { + return "Router ".$self->routernum." does not provide this service" + unless qsearchs('part_svc_router', { + svcpart => $self->cust_svc->svcpart, + routernum => $self->routernum + }); + + my $router = $self->router; + return "Router ".$self->routernum." does not serve this customer" + if $router->agentnum and $router->agentnum != $agentnum; - 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 ( $router->auto_addr ) { + my $error = $self->assign_ip_addr; + return $error if $error; } - } + else { + $self->blocknum(''); + } + } # if $self->routernum if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) { my $l = $cust_pkg->cust_location_or_main; @@ -423,55 +440,60 @@ sub check { $self->SUPER::check; } -sub _check_ip_addr { - my $self = shift; +=item assign_ip_addr - if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') { +Assign an address block matching the selected router, and the selected block +if there is one. - return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum +=cut - 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.")"; - } +sub assign_ip_addr { + my $self = shift; + my @blocks; + my $ip_addr; + if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) { + # simple case: user chose a block, find an address in that block + # (this overrides an existing IP address if it's not in the block) + @blocks = ($self->addr_block); + } + elsif ( $self->routernum ) { + @blocks = $self->router->auto_addr_block; + } + else { + return ''; } - 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."; + foreach my $block ( @blocks ) { + if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) { + # don't change anything + return ''; } + $ip_addr = $block->next_free_addr; + last if $ip_addr; } - - # 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; + if ( $ip_addr ) { + $self->set(ip_addr => $ip_addr->addr); + return ''; } - - 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; + return 'No IP address available on this router'; } +} +sub _check_ip_addr { + my $self = shift; + + if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') { + return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); + return 'IP address required'; + } +# if (my $dup = qsearchs('svc_broadband', { +# ip_addr => $self->ip_addr, +# svcnum => {op=>'!=', value => $self->svcnum} +# }) ) { +# return 'IP address conflicts with svcnum '.$dup->svcnum; +# } ''; } @@ -510,6 +532,17 @@ sub addr_block { qsearchs('addr_block', { blocknum => $self->blocknum }); } +=item router + +Returns the FS::router record for this service. + +=cut + +sub router { + my $self = shift; + qsearchs('router', { routernum => $self->routernum }); +} + =back =item allowed_routers @@ -520,7 +553,34 @@ Returns a list of allowed FS::router objects. sub allowed_routers { my $self = shift; - map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart }); + map { $_->router } qsearch('part_svc_router', + { svcpart => $self->cust_svc->svcpart }); +} + + +#class method +sub _upgrade_data { + my $class = shift; + + # set routernum to addr_block.routernum + foreach my $self (qsearch('svc_broadband', { + blocknum => {op => '!=', value => ''}, + routernum => '' + })) { + my $addr_block = $self->addr_block; + if ( my $routernum = $addr_block->routernum ) { + $self->set(routernum => $routernum); + my $error = $self->replace; + die "error assigning routernum $routernum to service ".$self->svcnum. + ":\n$error\n" + if $error; + } + else { + warn "svcnum ".$self->svcnum. + ": no routernum in address block ".$addr_block->cidr.", skipped\n"; + } + } + ''; } =head1 BUGS @@ -530,6 +590,8 @@ 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, diff --git a/httemplate/browse/router.cgi b/httemplate/browse/router.cgi index 069ca9b20..21047d7fc 100644 --- a/httemplate/browse/router.cgi +++ b/httemplate/browse/router.cgi @@ -9,6 +9,7 @@ 'count_query' => "SELECT count(*) from router $count_sql", 'header' => [ 'Router name', 'Address block(s)', + 'IP addressing', 'Action', ], 'fields' => [ 'routername', @@ -16,10 +17,12 @@ shift->addr_block ); }, + sub { shift->auto_addr ? 'Automatic' : 'Manual' }, sub { 'Delete' }, ], 'links' => [ [ "${p2}edit/router.cgi?", 'routernum' ], '', + '', [ "${p}misc/delete-router.html?", 'routernum' ], ], 'agent_virt' => 1, @@ -52,4 +55,5 @@ my $count_sql = $extra_sql. ( $extra_sql =~ /WHERE/ ? ' AND' : 'WHERE' ). 'null_right' => 'Broadband global configuration', ); + diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi index 70eaa4576..6672d5d75 100755 --- a/httemplate/edit/router.cgi +++ b/httemplate/edit/router.cgi @@ -6,11 +6,14 @@ 'labels' => { 'routernum' => 'Router', 'routername' => 'Name', 'svc_part' => 'Service', + 'agentnum' => 'Agent', + 'auto_addr' => 'Assign IP addresses automatically', }, 'fields' => [ { 'field'=>'routername', 'type'=>'text', 'size'=>32 }, { 'field'=>'agentnum', 'type'=>'select-agent' }, { 'field'=>'svcnum', 'type'=>'hidden' }, + { 'field'=>'auto_addr','type'=>'checkbox','value'=>'Y'}, ], 'error_callback' => $callback, 'edit_callback' => $callback, diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi index ff57ac0ce..8fccb1fa8 100644 --- a/httemplate/edit/svc_broadband.cgi +++ b/httemplate/edit/svc_broadband.cgi @@ -100,10 +100,9 @@ END ; my @fields = ( - qw( description ip_addr speed_down speed_up ), + qw( description speed_down speed_up ), { field=>'sectornum', type=>'select-tower_sector', }, - qw( blocknum ), - { field=>'block_label', type=>'fixed' }, + { field=>'routernum', type=>'select-router_block_ip', }, qw( mac_addr latitude longitude altitude vlan_profile performance_profile authkey plan_id ), ); diff --git a/httemplate/elements/select-tiered.html b/httemplate/elements/select-tiered.html index 35f9e5a60..99b285274 100644 --- a/httemplate/elements/select-tiered.html +++ b/httemplate/elements/select-tiered.html @@ -35,6 +35,7 @@ contain the following: isn't fully tested. - after: an HTML string to be inserted after the select element, before the next one. By default there's nothing between them. +- onchange: an additional javascript function to be called on change. For convenience, "curr_value" and "field" can be passed as part of the main argument list, and will be applied to the last tier. @@ -43,8 +44,13 @@ main argument list, and will be applied to the last tier. % $i = 0; % foreach my $tier (@$tiers) { % my $onchange; -% $onchange="onchange='${pre}select_change(this, $i)'" +% $onchange="${pre}select_change(this, $i)" % if $i < scalar(@$tiers) - 1; +% +% $onchange .= ';'.$tier->{onchange}."(this, $i);" +% if $tier->{onchange}; +% +% $onchange = "onchange='$onchange'" if $onchange; <% $opt{'ip_addr'} || '' %> +% } +% else { + +% } + <% $opt{'ip_addr'} || '' %> + (automatic) + + +<%init> +my %opt = @_; +my @routers; + +my $svc_x = $opt{'object'}; +if ( $svc_x ) { + $opt{$_} = $svc_x->$_ + foreach qw(routernum blocknum ip_addr svcpart); +} + +warn Dumper(\%opt); + +my $svcpart = $opt{'svcpart'} || ''; +my %fixed; # which fields are fixed +$svcpart =~ /^\d*$/ or die "invalid svcpart '$svcpart'"; +if ( $svcpart ) { + my $part_svc = FS::part_svc->by_key($svcpart); + # Traditionally, columnflag 'F' on IP address means that it MUST + # be auto-assigned (or, if null IP addresses are allowed, that + # it must be null). + foreach (qw(routernum blocknum ip_addr)) { + my $psc = $part_svc->part_svc_column($_); + if ( $psc and $psc->columnflag eq 'F' ) { + $fixed{$_} = $psc->columnvalue; + } + } + if ( $fixed{'routernum'} ) { + @routers = (FS::router->by_key($fixed{'routernum'})) + } + else { + @routers = map { $_->router } + qsearch('part_svc_router', { svcpart => $svcpart }); + } +} +else { + @routers = qsearch('router', {}); +} + +my %auto_addr_routernum = map { $_->routernum, $_->auto_addr } @routers; + diff --git a/httemplate/search/report_svc_broadband.html b/httemplate/search/report_svc_broadband.html index ee4dfce90..37f21b79f 100755 --- a/httemplate/search/report_svc_broadband.html +++ b/httemplate/search/report_svc_broadband.html @@ -26,6 +26,10 @@ 'multiple' => 'multiple', ) %> + + + Include services with no router + <% include( '/elements/tr-selectmultiple-part_pkg.html', %pkg_search, diff --git a/httemplate/search/svc_broadband.cgi b/httemplate/search/svc_broadband.cgi index 605b829b4..ee62e9084 100755 --- a/httemplate/search/svc_broadband.cgi +++ b/httemplate/search/svc_broadband.cgi @@ -15,8 +15,8 @@ 'fields' => [ 'svcnum', 'svc', sub { - my $blocknum = shift->blocknum or return ''; - $routerbyblock{$blocknum}->routername; + my $router = shift->router; + $router ? $router->routername : ''; }, @tower_fields, 'ip_addr', diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi index 2e93d42fd..05ae632bd 100644 --- a/httemplate/view/svc_broadband.cgi +++ b/httemplate/view/svc_broadband.cgi @@ -27,7 +27,7 @@ $labels{'coordinates'} = 'Latitude/Longitude'; my @fields = ( 'description', - { field => 'router', value => \&router }, + { field => 'routernum', value => \&router }, 'speed_down', 'speed_up', { field => 'ip_addr', value => \&ip_addr }, @@ -48,9 +48,10 @@ push @fields, sub router { my $svc = shift; - my $addr_block = $svc->addr_block or return ''; - my $router = $addr_block->router or return ''; - $router->routernum . ': ' . $router->routername; + my $router = $svc->router or return ''; + my $block = $svc->addr_block; + $block = '; '.$block->cidr if $block; + $router->routernum . ': ' . $router->routername . $block } sub ip_addr { -- 2.11.0