From 00de593a7e5b5b50aeec62c0ddb90db7bcd62f55 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 4 Sep 2013 12:53:30 -0700 Subject: [PATCH] assign entire address blocks to services for RADIUS Framed-Route option, #20742 --- FS/FS/Schema.pm | 1 + FS/FS/part_export/broadband_sqlradius.pm | 8 +- FS/FS/part_export/sqlradius.pm | 11 ++- FS/FS/part_svc.pm | 11 ++- FS/FS/router.pm | 114 +++++++++++++++++++---- FS/FS/svc_Common.pm | 19 ++++ FS/FS/svc_IP_Mixin.pm | 90 ++++++++++++++++++ httemplate/edit/elements/edit.html | 2 +- httemplate/edit/elements/part_svc_column.html | 13 +++ httemplate/edit/elements/svc_Common.html | 58 ++++++++++-- httemplate/edit/process/elements/svc_Common.html | 19 ++++ httemplate/edit/process/svc_acct.cgi | 19 +++- httemplate/edit/svc_acct.cgi | 46 +++++++++ httemplate/view/elements/svc_Common.html | 14 +++ httemplate/view/svc_acct/basics.html | 2 + 15 files changed, 391 insertions(+), 36 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 340b678e9..87a14aac4 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2282,6 +2282,7 @@ sub tables_hashref { 'selfservice_access', 'varchar', 'NULL', $char_d, '', '', 'classnum', 'int', 'NULL', '', '', '', 'restrict_edit_password','char', 'NULL', 1, '', '', + 'has_router', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'svcpart', 'unique' => [], diff --git a/FS/FS/part_export/broadband_sqlradius.pm b/FS/FS/part_export/broadband_sqlradius.pm index b5d1a80cb..522c6377c 100644 --- a/FS/FS/part_export/broadband_sqlradius.pm +++ b/FS/FS/part_export/broadband_sqlradius.pm @@ -6,6 +6,7 @@ use Tie::IxHash; use FS::Conf; use FS::Record qw( dbh str2time_sql ); #qsearch qsearchs ); use FS::part_export::sqlradius qw(sqlradius_connect); +use NEXT; FS::UID->install_callback(sub { $conf = new FS::Conf }); @@ -88,7 +89,9 @@ sub export_username { sub radius_reply { my($self, $svc_broadband) = (shift, shift); - my %reply; + # start with attributes the service wants + my %reply = $self->NEXT::radius_reply($svc_broadband); + # add export-specific stuff if ( length($self->option('ip_addr_as',1)) and length($svc_broadband->ip_addr) ) { $reply{$self->option('ip_addr_as')} = $svc_broadband->ip_addr; @@ -98,8 +101,9 @@ sub radius_reply { sub radius_check { my($self, $svc_broadband) = (shift, shift); + + my %check = $self->SUPER::radius_check($svc_broadband); my $password_attrib = $conf->config('radius-password') || 'Password'; - my %check; if ( $self->option('mac_as_password') ) { $check{$password_attrib} = $self->export_username($svc_broadband); } diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 833dd9a1d..c8a963dbc 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -9,6 +9,7 @@ use FS::part_export; use FS::svc_acct; use FS::export_svc; use Carp qw( cluck ); +use NEXT; @ISA = qw(FS::part_export); @EXPORT_OK = qw( sqlradius_connect ); @@ -133,12 +134,14 @@ sub export_username { # override for other svcdb sub radius_reply { #override for other svcdb my($self, $svc_acct) = (shift, shift); - $svc_acct->radius_reply; + my %every = $svc_acct->EVERY::radius_reply; + map { @$_ } values %every; } sub radius_check { #override for other svcdb my($self, $svc_acct) = (shift, shift); - $svc_acct->radius_check; + my %every = $svc_acct->EVERY::radius_check; + map { @$_ } values %every; } sub _export_insert { @@ -194,8 +197,8 @@ sub _export_replace { foreach my $table (qw(reply check)) { my $method = "radius_$table"; - my %new = $new->$method(); - my %old = $old->$method(); + my %new = $self->$method($new); + my %old = $self->$method($old); if ( grep { !exists $old{$_} #new attributes || $new{$_} ne $old{$_} #changed } keys %new diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index da794dd4c..a1168199c 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -65,6 +65,10 @@ empty for full access, "readonly" for read-only, "hidden" to hide it entirely right to change the password field, rather than just "Edit password". Only relevant to svc_acct for now. +=item has_router - Allow the service to have an L connected +through it. Probably only relevant to svc_broadband, svc_acct, and svc_dsl +for now. + =back =head1 METHODS @@ -394,11 +398,12 @@ sub check { $self->ut_numbern('svcpart') || $self->ut_text('svc') || $self->ut_alpha('svcdb') - || $self->ut_enum('disabled', [ '', 'Y' ] ) - || $self->ut_enum('preserve', [ '', 'Y' ] ) + || $self->ut_flag('disabled') + || $self->ut_flag('preserve') || $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] ) || $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' ) - || $self->ut_enum('restrict_edit_password', [ '', 'Y' ] ) + || $self->ut_flag('restrict_edit_password') + || $self->ut_flag('has_router') ; return $error if $error; diff --git a/FS/FS/router.pm b/FS/FS/router.pm index 6fa44b408..937dc1f45 100755 --- a/FS/FS/router.pm +++ b/FS/FS/router.pm @@ -63,16 +63,87 @@ sub table { 'router'; } Adds this record to the database. If there is an error, returns the error, otherwise returns false. -=item delete +If the pseudo-field 'blocknum' is set to an L number, then +that address block will be assigned to this router. Currently only one +block can be assigned this way. + +=cut + +sub insert { + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; -Deletes this record from the database. If there is an error, returns the -error, otherwise returns false. + my $self = shift; + my $error = $self->SUPER::insert(@_); + return $error if $error; + if ( $self->blocknum ) { + my $block = FS::addr_block->by_key($self->blocknum); + if ($block) { + if ($block->routernum) { + $error = "block ".$block->cidr." is already assigned to a router"; + } else { + $block->set('routernum', $self->routernum); + $block->set('manual_flag', 'Y'); + $error = $block->replace; + } + } else { + $error = "blocknum ".$self->blocknum." not found"; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + $dbh->commit if $oldAutoCommit; + return $error; +} =item replace OLD_RECORD Replaces OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. +=cut + +sub replace { + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $self = shift; + my $old = shift || $self->replace_old; + my $error = $self->SUPER::replace($old, @_); + return $error if $error; + + if ( defined($self->blocknum) ) { + #warn "FS::router::replace: blocknum = ".$self->blocknum."\n"; + # then release any blocks we're already holding + foreach my $block ($self->addr_block) { + $block->set('routernum', 0); + $block->set('manual_flag', ''); + $error ||= $block->replace; + } + if ( !$error and $self->blocknum > 0 ) { + # and, if the new blocknum is a real blocknum, assign it + my $block = FS::addr_block->by_key($self->blocknum); + if ( $block ) { + $block->set('routernum', $self->routernum); + $block->set('manual_flag', ''); + $error ||= $block->replace; + } else { + $error = "blocknum ".$self->blocknum." not found"; + } + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + $dbh->commit if $oldAutoCommit; + return $error; +} + =item check Checks all fields to make sure this is a valid record. If there is an error, @@ -89,6 +160,7 @@ sub check { || $self->ut_text('routername') || $self->ut_enum('manual_addr', [ '', 'Y' ]) || $self->ut_agentnum_acl('agentnum', 'Broadband global configuration') + || $self->ut_foreign_keyn('svcnum', 'cust_svc', 'svcnum') ; return $error if $error; @@ -97,28 +169,25 @@ sub check { =item delete -Deletes this router if and only if no address blocks (see L) -are currently allocated to it. +Deallocate all address blocks from this router and delete it. =cut sub delete { my $self = shift; - return 'Router has address blocks allocated to it' if $self->addr_block; - - local $SIG{HUP} = 'IGNORE'; - local $SIG{INT} = 'IGNORE'; - local $SIG{QUIT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{TSTP} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - - my $error = $self->SUPER::delete; + + my $error; + foreach my $block ($self->addr_block) { + $block->set('manual_flag', ''); + $block->set('routernum', 0); + $error ||= $block->replace; + } + + $error ||= $self->SUPER::delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -187,6 +256,19 @@ sub agent { qsearchs('agent', { 'agentnum' => shift->agentnum }); } +=item cust_svc + +Returns the cust_svc associated with this router, if any. This should be +the service that I, not any service +connected I the router. + +=cut + +sub cust_svc { + my $svcnum = shift->svcnum or return undef; + FS::cust_svc->by_key($svcnum); +} + =back =head1 SEE ALSO diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 0aea4559b..3993d3d64 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -367,6 +367,7 @@ sub delete { || $self->SUPER::delete || $self->export('delete', @$export_args) || $self->return_inventory + || $self->release_router || $self->predelete_hook || $self->cust_svc->delete ; @@ -989,6 +990,24 @@ sub inventory_item { }); } +=item release_router + +Delete any routers associated with this service. This will release their +address blocks, also. + +=cut + +sub release_router { + my $self = shift; + my @routers = qsearch('router', { svcnum => $self->svcnum }); + foreach (@routers) { + my $error = $_->delete; + return "$error (removing router '".$_->routername."')" if $error; + } + ''; +} + + =item cust_svc Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm index 7eda7e02c..ff7c2f5d4 100644 --- a/FS/FS/svc_IP_Mixin.pm +++ b/FS/FS/svc_IP_Mixin.pm @@ -3,6 +3,7 @@ package FS::svc_IP_Mixin; use strict; use base 'FS::IP_Mixin'; use FS::Record qw(qsearchs qsearch); +use NEXT; =item addr_block @@ -120,4 +121,93 @@ sub _is_used { } } +=item attached_router + +Returns the L attached via this service (as opposed to the one +this service is connected through), that is, a router whose "svcnum" field +equals this service's primary key. + +If the 'router_routernum' pseudo-field is set, returns that router instead. + +=cut + +sub attached_router { + my $self = shift; + if ( length($self->get('router_routernum') )) { + return FS::router->by_key($self->router_routernum); + } else { + qsearchs('router', { 'svcnum' => $self->svcnum }); + } +} + +=item attached_block + +Returns the address block (L) assigned to the attached_router, +if there is one. + +If the 'router_blocknum' pseudo-field is set, returns that block instead. + +=cut + +sub attached_block { + my $self = shift; + if ( length($self->get('router_blocknum')) ) { + return FS::addr_block->by_key($self->router_blocknum); + } else { + my $router = $self->attached_router or return ''; + my ($block) = $router->addr_block; + return $block || ''; + } +} + +=item radius_check + +Returns nothing. + +=cut + +sub radius_check { } + +=item radius_reply + +Returns RADIUS reply items that are relevant across all exports and +necessary for the IP address configuration of the service. Currently, that +means "Framed-Route" if there's an attached router. + +=cut + +sub radius_reply { + my $self = shift; + my %reply; + my ($block) = $self->attached_block; + if ( $block ) { + # block routed over dynamic IP: "192.168.100.0/29 0.0.0.0 1" + # or + # block routed over fixed IP: "192.168.100.0/29 192.168.100.1 1" + # (the "1" at the end is the route metric) + $reply{'Framed-Route'} = + $block->cidr . ' ' . + ($self->ip_addr || '0.0.0.0') . ' 1'; + } + %reply; +} + +sub replace_check { + my ($new, $old) = @_; + # this modifies $old, not $new, which is a slight abuse of replace_check, + # but there's no way to ensure that replace_old gets called... + # + # ensure that router_routernum and router_blocknum are set to their + # current values, so that exports remember the service's attached router + # and block even after they've been replaced + my $router = $old->attached_router; + my $block = $old->attached_block; + $old->set('router_routernum', $router ? $router->routernum : 0); + $old->set('router_blocknum', $block ? $block->blocknum : 0); + my $err_or_ref = $new->NEXT::replace_check($old) || ''; + # because NEXT::replace_check($old) ends up trying to AUTOLOAD replace_check + # which is dumb, but easily worked around + ref($err_or_ref) ? '' : $err_or_ref; +} + 1; diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index ed677d7ab..3270f0443 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -322,7 +322,7 @@ Example: % $include_common{$_} = $f->{$_} foreach grep exists($f->{$_}), % qw( js_only html_only select_only layers_only cell_style ),#selectlayers,? % qw( empty_label ), # select-* -% qw( value_col compare_sub ), # select-table +% qw( value_col compare_sub order_by ), # select-table % qw( table name_col ), #(select,checkboxes)-table % qw( target_table link_table ), #checkboxes-table % qw( hashref agent_virt agent_null agent_null_right ),#*-table diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html index 1c5b45314..aa3c647b8 100644 --- a/httemplate/edit/elements/part_svc_column.html +++ b/httemplate/edit/elements/part_svc_column.html @@ -248,6 +248,19 @@ that field. % } +% # special case: services with attached routers (false laziness...) +% if ( $svcdb eq 'svc_acct' or $svcdb eq 'svc_broadband' or $svcdb eq 'svc_dsl' ) { +% push @fields, 'has_router'; + + + <% emt('This service has an attached router') %> + + + has_router ? 'CHECKED' : '' %>> + + +% } <& /elements/progress-init.html, $svcdb, #form name diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html index d46d1cb42..321c68545 100644 --- a/httemplate/edit/elements/svc_Common.html +++ b/httemplate/edit/elements/svc_Common.html @@ -21,6 +21,7 @@ : ''; #? &{ $cb }( $cgi,$svc_x, $part_svc,$cust_pkg, $fields,$opt); } + }, 'edit_callback' => sub { @@ -43,6 +44,27 @@ : ''; #? &{ $cb }( $cgi,$svc_x, $part_svc,$cust_pkg, $fields,$opt); } + + if ( $part_svc->has_router ) { + my $router = qsearchs('router', {svcnum => $svc_x->svcnum}); + if ( $router ) { + $svc_x->set("router_$_", $router->get($_)) + foreach ('routername', 'routernum'); + my ($block) = $router->addr_block; # one-to-one for now + if ( $block ) { + $svc_x->set('router_blocknum', $block->blocknum); + # silly, but necessary...make the currently + # assigned block appear on the list + my ($field) = grep {ref($_) and + $_->{field} eq 'router_blocknum'} + @$fields; + $field->{extra_sql} = + ' OR routernum = '.$router->routernum; + $field->{curr_value} = $block->blocknum; + } + } + } + }, 'new_hashref_callback' => sub { @@ -72,7 +94,6 @@ } $svc_x->set_default_and_fixed; - }, 'field_callback' => sub { @@ -125,20 +146,21 @@ }, 'html_init' => sub { + my $html; my $cust_main; if ( $pkgnum ) { my $cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum}); $cust_main = $cust_pkg->cust_main if $cust_pkg; - } - $cust_main - ? include( '/elements/small_custview.html', + if ( $cust_main ) { + $html = include( '/elements/small_custview.html', $cust_main, '', 1, popurl(2). "view/cust_main.cgi" - ). '
' - : ''; - + ). '
'; + } + } + $html; }, 'html_table_bottom' => sub { @@ -200,6 +222,28 @@ sub label_fixup { $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\s*$/; } + if ( $part_svc->has_router ) { + # these will be set up as pseudo-fields in the new_ and edit_ callbacks + push @{ $opt->{'fields'} }, ( + { field => 'router_routernum', type => 'hidden' }, + { field => 'router_routername', type => 'text', size => 32 }, + # router address block selection + # (one-to-one for now) + { field => 'router_blocknum', + type => 'select-table', + table => 'addr_block', + hashref => { 'routernum' => '0' }, + agent_virt => 1, + agent_null => 1, + name_col => 'cidr', + order_by => 'ORDER BY ip_gateway, ip_netmask', + empty_label => '(none)', + disable_empty => 0, + }, + ); + $labels->{router_routername} = 'Attached router name'; + $labels->{router_blocknum} = 'Attached address block'; + } } diff --git a/httemplate/edit/process/elements/svc_Common.html b/httemplate/edit/process/elements/svc_Common.html index 06f4c00b1..e1b7cbe1a 100644 --- a/httemplate/edit/process/elements/svc_Common.html +++ b/httemplate/edit/process/elements/svc_Common.html @@ -1,6 +1,7 @@ <% include( 'process.html', 'edit_ext' => 'cgi', 'redirect' => popurl(3)."view/$table.cgi?", + 'args_callback' => $args_callback, %opt, ) %> @@ -16,4 +17,22 @@ foreach (fields($table)) { } } +my $args_callback = sub { + my ($cgi, $svc) = @_; + my $part_svc = FS::part_svc->by_key($cgi->param('svcpart')) + or die "svcpart required"; + if ( $part_svc->has_router ) { + my $router = FS::router->new({ + map { $_ => $cgi->param("router_$_") } + qw( routernum routername blocknum ) + }); + if (length($router->routername) == 0) { + #sensible default + $router->set('routername', $svc->label); + } + return (child_objects => [ $router ]); + } + (); +}; + diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi index d4bcd35ed..ca614cbd8 100755 --- a/httemplate/edit/process/svc_acct.cgi +++ b/httemplate/edit/process/svc_acct.cgi @@ -88,7 +88,7 @@ if ( ! $error ) { my $export_info = FS::part_export::export_info(); - my @svc_export_machine = + my @child_objects = map FS::svc_export_machine->new({ 'svcnum' => $svcnum, 'exportnum' => $_->exportnum, @@ -97,6 +97,19 @@ if ( ! $error ) { grep { $_->machine eq '_SVC_MACHINE' } $part_svc->part_export; + if ( $part_svc->has_router ) { + my $router = FS::router->new({ + map { $_ => $cgi->param("router_$_") } + qw( routernum routername blocknum ) + }); + if (length($router->routername == 0)) { + #sensible default + $router->set('routername', $new->label); + } + push @child_objects, $router; + } + + if ( $svcnum ) { foreach ( grep { $old->$_ != $new->$_ } qw( seconds upbytes downbytes totalbytes ) @@ -110,9 +123,9 @@ if ( ! $error ) { $error ||= $new->set_usage(\%hash); #unoverlimit and trigger radius changes last; #once is enough } - $error ||= $new->replace($old, 'child_objects'=>\@svc_export_machine); + $error ||= $new->replace($old, 'child_objects'=>\@child_objects); } else { - $error ||= $new->insert('child_objects'=>\@svc_export_machine); + $error ||= $new->insert('child_objects'=>\@child_objects); $svcnum = $new->svcnum; } } diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi index 574fb51eb..2c694a8fe 100755 --- a/httemplate/edit/svc_acct.cgi +++ b/httemplate/edit/svc_acct.cgi @@ -345,6 +345,35 @@ % } + +% if ( $part_svc->has_router ) { +<& /elements/hidden.html, + field => 'router_routernum', + curr_value => $svc_acct->router_routernum +&> +<& /elements/tr-input-text.html, + label => 'Attached router name', + field => 'router_routername', + size => 32, + curr_value => $svc_acct->router_routername +&> +<& /elements/tr-select-table.html, + label => 'Attached address block', + field => 'router_blocknum', + table => 'addr_block', + hashref => { 'routernum' => '0' }, + extra_sql => ($svc_acct->router_routernum ? + ' OR routernum = '.$svc_acct->router_routernum : ''), + agent_virt => 1, + agent_null => 1, + name_col => 'cidr', + order_by => 'ORDER BY ip_gateway, ip_netmask', + empty_label => '(none)', + disable_empty => 0, + curr_value => $svc_acct->router_blocknum +&> +% } + % foreach my $field ($svc_acct->virtual_fields) { % # If the flag is X, it won't even show up in $svc_acct->virtual_fields. % if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) { @@ -525,4 +554,21 @@ if ( $export_google ) { } #if $error } +if ( $part_svc->has_router ) { # duplicates the one in elements/svc_Common + if ( $svcnum ) { + my $router = qsearchs('router', {svcnum => $svc_acct->svcnum}); + if ( $router ) { + $svc_acct->set("router_$_", $router->get($_)) + foreach ('routername', 'routernum'); + my ($block) = $router->addr_block; + $svc_acct->set('router_blocknum', $block->blocknum) if ( $block ); + } + } + foreach (qw(router_routername router_routernum router_blocknum)) { + if ( $cgi->param($_) =~ /^(\w+)$/ ) { + $svc_acct->set($_, $1); + } + } +} + diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html index 02484341b..bc1cd45fc 100644 --- a/httemplate/view/elements/svc_Common.html +++ b/httemplate/view/elements/svc_Common.html @@ -214,6 +214,20 @@ if ($pkgnum) { $custnum = ''; } +# attached routers +if ( my $router = qsearchs('router', { svcnum => $svc_x->svcnum }) ) { + push @$fields, qw(router_routername router_block); + $labels->{'router_routername'} = 'Attached router'; + $labels->{'router_block'} = 'Attached address block'; + $svc_x->set('router_routername', $router->routername); + my $block = qsearchs('addr_block', { routernum => $router->routernum }); + if ( $block ) { + $svc_x->set('router_block', $block->cidr); + } else { + $svc_x->set('router_block', '(none)'); + } +} + &{ $opt{'svc_callback'} }( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, \%opt ) if $opt{'svc_callback'}; diff --git a/httemplate/view/svc_acct/basics.html b/httemplate/view/svc_acct/basics.html index 04e7bcff8..441c20add 100644 --- a/httemplate/view/svc_acct/basics.html +++ b/httemplate/view/svc_acct/basics.html @@ -152,6 +152,8 @@ sub slipip { <& /view/elements/tr.html, label=>mt('RADIUS groups'), value=>join('
', $svc_acct->radius_groups('long_description')) &> +<& router.html, 'svc_acct' => $svc_acct &> + %# Can this be abstracted further? Maybe a library function like %# widget('HTML', 'view', $svc_acct) ? It would definitely make UI %# style management easier. -- 2.11.0