diff options
author | Mark Wells <mark@freeside.biz> | 2012-11-29 22:03:29 -0800 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2012-11-29 22:03:29 -0800 |
commit | a2a69f909cad813d7164bae805e87f5874a9fdae (patch) | |
tree | 29e426af02eb03106e55507103fc90f92fa55f61 | |
parent | 226fffec6fd0154ea8798b58321d4d119341879f (diff) |
broadband_snmp export: better MIB selection
-rw-r--r-- | FS/FS/Mason.pm | 1 | ||||
-rw-r--r-- | FS/FS/part_export.pm | 13 | ||||
-rw-r--r-- | FS/FS/part_export/broadband_snmp.pm | 150 | ||||
-rwxr-xr-x | httemplate/browse/part_export.cgi | 48 | ||||
-rw-r--r-- | httemplate/edit/cdr_type.cgi | 22 | ||||
-rw-r--r-- | httemplate/edit/elements/part_export/broadband_snmp.html | 100 | ||||
-rw-r--r-- | httemplate/edit/elements/part_export/foot.html | 6 | ||||
-rw-r--r-- | httemplate/edit/elements/part_export/head.html | 19 | ||||
-rw-r--r-- | httemplate/edit/part_export.cgi | 9 | ||||
-rw-r--r-- | httemplate/edit/process/cdr_type.cgi | 1 | ||||
-rw-r--r-- | httemplate/edit/process/part_export.cgi | 29 | ||||
-rw-r--r-- | httemplate/edit/rate_time.cgi | 41 | ||||
-rw-r--r-- | httemplate/elements/auto-table.html | 311 | ||||
-rw-r--r-- | httemplate/elements/select-mib-popup.html | 170 | ||||
-rw-r--r-- | httemplate/elements/xmlhttp.html | 32 | ||||
-rw-r--r-- | httemplate/misc/xmlhttp-mib-browse.html | 161 |
16 files changed, 877 insertions, 236 deletions
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 944a4836c..004701646 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -56,6 +56,7 @@ if ( -e $addl_handler_use_file ) { #use CGI::Carp qw(fatalsToBrowser); use CGI::Cookie; use List::Util qw( max min sum ); + use Scalar::Util qw( blessed ); use Data::Dumper; use Date::Format; use Time::Local; diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index b0f708a66..ed66b41a1 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -615,6 +615,19 @@ sub weight { export_info()->{$self->exporttype}->{'weight'} || 0; } +=item info + +Returns a reference to (a copy of) the export's %info hash. + +=cut + +sub info { + my $self = shift; + $self->{_info} ||= { + %{ export_info()->{$self->exporttype} } + }; +} + =back =head1 SUBROUTINES diff --git a/FS/FS/part_export/broadband_snmp.pm b/FS/FS/part_export/broadband_snmp.pm index 44b4dbabb..9afca0872 100644 --- a/FS/FS/part_export/broadband_snmp.pm +++ b/FS/FS/part_export/broadband_snmp.pm @@ -3,7 +3,7 @@ package FS::part_export::broadband_snmp; use strict; use vars qw(%info $DEBUG); use base 'FS::part_export'; -use Net::SNMP qw(:asn1 :snmp); +use SNMP; use Tie::IxHash; $DEBUG = 0; @@ -11,21 +11,21 @@ $DEBUG = 0; my $me = '['.__PACKAGE__.']'; tie my %snmp_version, 'Tie::IxHash', - v1 => 'snmpv1', - v2c => 'snmpv2c', - # 3 => 'v3' not implemented + v1 => '1', + v2c => '2c', + # v3 unimplemented ; -tie my %snmp_type, 'Tie::IxHash', - i => INTEGER, - u => UNSIGNED32, - s => OCTET_STRING, - n => NULL, - o => OBJECT_IDENTIFIER, - t => TIMETICKS, - a => IPADDRESS, - # others not implemented yet -; +#tie my %snmp_type, 'Tie::IxHash', +# i => INTEGER, +# u => UNSIGNED32, +# s => OCTET_STRING, +# n => NULL, +# o => OBJECT_IDENTIFIER, +# t => TIMETICKS, +# a => IPADDRESS, +# # others not implemented yet +#; tie my %options, 'Tie::IxHash', 'version' => { label=>'SNMP version', @@ -33,14 +33,11 @@ tie my %options, 'Tie::IxHash', options => [ keys %snmp_version ], }, 'community' => { label=>'Community', default=>'public' }, - ( - map { $_.'_command', - { label => ucfirst($_) . ' commands', - type => 'textarea', - default => '', - } - } qw( insert delete replace suspend unsuspend ) - ), + + 'action' => { multiple=>1 }, + 'oid' => { multiple=>1 }, + 'value' => { multiple=>1 }, + 'ip_addr_change_to_new' => { label=>'Send IP address changes to new address', type=>'checkbox' @@ -51,28 +48,14 @@ tie my %options, 'Tie::IxHash', %info = ( 'svc' => 'svc_broadband', 'desc' => 'Send SNMP requests to the service IP address', + 'config_element' => '/edit/elements/part_export/broadband_snmp.html', 'options' => \%options, 'no_machine' => 1, 'weight' => 10, 'notes' => <<'END' Send one or more SNMP SET requests to the IP address registered to the service. -Enter one command per line. Each command is a target OID, data type flag, -and value, separated by spaces. -The data type flag is one of the following: -<font size="-1"><ul> -<li><i>i</i> = INTEGER</li> -<li><i>u</i> = UNSIGNED32</li> -<li><i>s</i> = OCTET-STRING (as ASCII)</li> -<li><i>a</i> = IPADDRESS</li> -<li><i>n</i> = NULL</li></ul> The value may interpolate fields from svc_broadband by prefixing the field name with <b>$</b>, or <b>$new_</b> and <b>$old_</b> for replace operations. -The value may contain whitespace; quotes are not necessary.<br> -<br> -For example, to set the SNMPv2-MIB "sysName.0" object to the string -"svc_broadband" followed by the service number, use the following -command:<br> -<pre>1.3.6.1.2.1.1.5.0 s svc_broadband$svcnum</pre><br> END ); @@ -105,19 +88,18 @@ sub export_command { my $self = shift; my ($action, $svc_new, $svc_old) = @_; - my $command_text = $self->option($action.'_command'); - return if !length($command_text); - - warn "$me parsing ${action}_command:\n" if $DEBUG; + my @a = split("\n", $self->option('action')); + my @o = split("\n", $self->option('oid')); + my @v = split("\n", $self->option('value')); my @commands; - foreach (split /\n/, $command_text) { - my ($oid, $type, $value) = split /\s/, $_, 3; - $oid =~ /^(\d+\.)*\d+$/ or die "invalid OID '$oid'\n"; - my $typenum = $snmp_type{$type} or die "unknown data type '$type'\n"; - $value = '' if !defined($value); # allow sending an empty string + warn "$me parsing $action commands:\n" if $DEBUG; + while (@a) { + my $oid = shift @o; + my $value = shift @v; + next unless shift(@a) eq $action; # ignore commands for other actions $value = $self->substitute($value, $svc_new, $svc_old); - warn "$me $oid $type $value\n" if $DEBUG; - push @commands, $oid, $typenum, $value; + warn "$me $oid :=$value\n" if $DEBUG; + push @commands, $oid, $value; } my $ip_addr = $svc_new->ip_addr; @@ -128,13 +110,13 @@ sub export_command { warn "$me opening session to $ip_addr\n" if $DEBUG; my %opt = ( - -hostname => $ip_addr, - -community => $self->option('community'), - -timeout => $self->option('timeout') || 20, + DestHost => $ip_addr, + Community => $self->option('community'), + Timeout => ($self->option('timeout') || 20) * 1000, ); my $version = $self->option('version'); - $opt{-version} = $snmp_version{$version} or die 'invalid version'; - $opt{-varbindlist} = \@commands; # just for now + $opt{Version} = $snmp_version{$version} or die 'invalid version'; + $opt{VarList} = \@commands; # for now $self->snmp_queue( $svc_new->svcnum, %opt ); } @@ -151,16 +133,22 @@ sub snmp_queue { sub snmp_request { my %opt = @_; - my $varbindlist = delete $opt{-varbindlist}; - my ($session, $error) = Net::SNMP->session(%opt); - die "Couldn't create SNMP session: $error" if !$session; + my $flatvarlist = delete $opt{VarList}; + my $session = SNMP::Session->new(%opt); warn "$me sending SET request\n" if $DEBUG; - my $result = $session->set_request( -varbindlist => $varbindlist ); - $error = $session->error(); - $session->close(); - if (!defined $result) { + my @varlist; + while (@$flatvarlist) { + my @this = splice(@$flatvarlist, 0, 2); + push @varlist, [ $this[0], 0, $this[1], undef ]; + # XXX new option to choose the IID (array index) of the object? + } + + $session->set(\@varlist); + my $error = $session->{ErrorStr}; + + if ( $session->{ErrorNum} ) { die "SNMP request failed: $error\n"; } } @@ -181,4 +169,46 @@ sub substitute { $value; } +sub _upgrade_exporttype { + eval 'use FS::Record qw(qsearch qsearchs)'; + # change from old style with numeric oid, data type flag, and value + # on consecutive lines + foreach my $export (qsearch('part_export', + { exporttype => 'broadband_snmp' } )) + { + # for the new options + my %new_options = ( + 'action' => [], + 'oid' => [], + 'value' => [], + ); + foreach my $action (qw(insert replace delete suspend unsuspend)) { + my $old_option = qsearchs('part_export_option', + { exportnum => $export->exportnum, + optionname => $action.'_command' } ); + next if !$old_option; + my $text = $old_option->optionvalue; + my @commands = split("\n", $text); + foreach (@commands) { + my ($oid, $type, $value) = split /\s/, $_, 3; + push @{$new_options{action}}, $action; + push @{$new_options{oid}}, $oid; + push @{$new_options{value}}, $value; + } + my $error = $old_option->delete; + warn "error migrating ${action}_command option: $error\n" if $error; + } + foreach (keys(%new_options)) { + my $new_option = FS::part_export_option->new({ + exportnum => $export->exportnum, + optionname => $_, + optionvalue => join("\n", @{ $new_options{$_} }) + }); + my $error = $new_option->insert; + warn "error inserting '$_' option: $error\n" if $error; + } + } #foreach $export + ''; +} + 1; diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi index b7ecc00a6..91238a0fd 100755 --- a/httemplate/browse/part_export.cgi +++ b/httemplate/browse/part_export.cgi @@ -43,14 +43,56 @@ function part_export_areyousure(href) { <TD CLASS="inv" BGCOLOR="<% $bgcolor %>"> <% itable() %> % my %opt = $part_export->options; -% foreach my $opt ( keys %opt ) { +% my $defs = $part_export->info->{options}; +% my %multiples; +% foreach my $opt (keys %$defs) { # is a Tie::IxHash +% my $group = $defs->{$opt}->{multiple}; +% if ( $group ) { +% my @values = split("\n", $opt{$opt}); +% $multiples{$group} ||= []; +% push @{ $multiples{$group} }, [ $opt, @values ] if @values; +% delete $opt{$opt}; +% } elsif (length($opt{$opt})) { # the normal case +%# foreach my $opt ( keys %opt ) { <TR> <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>: </TD> <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD> </TR> -% } - +% delete $opt{$opt}; +% } +% } +% # now any that are somehow not in the options list +% foreach my $opt (keys %opt) { +% if ( length($opt{$opt}) ) { + <TR> + <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>: </TD> + <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD> + </TR> +% } +% } +% # now show any multiple-option groups +% foreach (sort keys %multiples) { +% my $set = $multiples{$_}; + <TR><TD ALIGN="center" COLSPAN=2><TABLE CLASS="grid"> + <TR> +% foreach my $col (@$set) { + <TH><% shift @$col %></TH> +% } + </TR> +% while ( 1 ) { + <TR> +% my $end = 1; +% foreach my $col (@$set) { + <TD><% shift @$col %></TD> +% $end = 0 if @$col; +% } + </TR> +% last if $end; +% } + </TABLE></TD></TR> +% } #foreach keys %multiples + </TABLE> </TD> diff --git a/httemplate/edit/cdr_type.cgi b/httemplate/edit/cdr_type.cgi index 5d2c66216..c69610607 100644 --- a/httemplate/edit/cdr_type.cgi +++ b/httemplate/edit/cdr_type.cgi @@ -7,11 +7,24 @@ calls and SMS messages. Each CDR type must have a set of rates configured in the rate tables. <BR> <FORM METHOD="POST" ACTION="<% "${p}edit/process/cdr_type.cgi" %>"> -<% include('/elements/auto-table.html', - 'header' => [ 'Type#', 'Name' ], - 'fields' => [ qw( cdrtypenum cdrtypename ) ], +<TABLE ID="AutoTable" BORDER=0 CELLSPACING=0> + <TR> + <TH>Type#</TH> + <TH>Name</TH> + </TR> + <TR ID="cdr_template"> + <TD> + <INPUT NAME="cdrtypenum" SIZE=16 MAXLENGTH=16 ALIGN="right"> + </TD> + <TD> + <INPUT NAME="cdrtypename" SIZE=16 MAXLENGTH=16> + </TD> + </TR> +<& /elements/auto-table.html, + 'template_row' => 'cdr_template', 'data' => \@data, - ) %> +&> +</TABLE> <INPUT TYPE="submit" VALUE="Apply changes"> </FORM> <BR> <% include('/elements/footer.html') %> <%init> @@ -20,7 +33,6 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); my @data = ( - map { [ $_->cdrtypenum, $_->cdrtypename ] } qsearch({ 'table' => 'cdr_type', 'hashref' => {}, diff --git a/httemplate/edit/elements/part_export/broadband_snmp.html b/httemplate/edit/elements/part_export/broadband_snmp.html new file mode 100644 index 000000000..8df0b8e02 --- /dev/null +++ b/httemplate/edit/elements/part_export/broadband_snmp.html @@ -0,0 +1,100 @@ +<%doc> +</%doc> +<& head.html, %opt &> +<INPUT TYPE="hidden" NAME="options" VALUE="community,version,ip_addr_change_to_new,timeout"> +<& /elements/tr-select.html, + label => 'SNMP version', + field => 'version', + options => [ '', 'v1', 'v2c' ], + labels => { v1 => '1', v2c => '2c' }, + curr_value => $part_export->option('version') &> +<& /elements/tr-input-text.html, + label => 'Community', + field => 'community', + curr_value => $part_export->option('community'), +&> +<& /elements/tr-checkbox.html, + label => 'Send IP address changes to new address', + field => 'ip_addr_change_to_new', + value => 1, + curr_value => $part_export->option('ip_addr_change_to_new'), +&> +<& /elements/tr-input-text.html, + label => 'Timeout (seconds)', + field => 'timeout', + curr_value => $part_export->option('timeout'), +&> +</TABLE> +<script type="text/javascript"> +function open_select_mib(obj) { + nd(1); // if there's already one open, close it + var rownum = obj.rownum; + var curr_oid = obj.value || ''; + var url = '<%$fsurl%>/elements/select-mib-popup.html?' + + 'callback=receive_mib;' + + 'arg=' + rownum + + ';curr_value=' + curr_oid; + overlib( + OLiframeContent(url, 550, 450, '<% $popup_name %>', 0, 'auto'), + CAPTION, 'Select MIB object', STICKY, AUTOSTATUSCAP, + MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK, + BGCOLOR, '#333399', CGCOLOR, '#333399', + CLOSETEXT, 'Close' + ); +} +function receive_mib(obj, rownum) { + //console.log(JSON.stringify(obj)); + // we don't really need the numeric OID or any of the other properties + document.getElementById('oid'+rownum).value = obj.fullname; + document.getElementById('datatype'+rownum).innerHTML = obj.type; +} +</script> + +<table bgcolor="#cccccc" border=0 cellspacing=3> +<TR> + <TH>Action</TH> + <TH>Object</TH> + <TH>Type</TH> + <TH>Value</TH> +</TR> +<TR id="mytemplate"> + <TD> + <SELECT NAME="action"> +% foreach ('', qw(insert delete replace suspend unsuspend)) { + <OPTION VALUE="<%$_%>"><%$_%></OPTION> +% } + </SELECT> + </TD> + <TD> + <INPUT NAME="oid" ID="oid" SIZE="60" onclick="open_select_mib(this)"> + </TD> + <TD> + <SPAN ID="datatype"></SPAN> + </TD> + <TD> + <INPUT NAME="value" ID="value"> + </TD> +</TR> +<& /elements/auto-table.html, + template_row => 'mytemplate', + fieldorder => ['action', 'oid', 'value'], + data => \@data, +&> +<INPUT TYPE="hidden" NAME="multi_options" VALUE="action,oid,value"> +<& foot.html, %opt &> +<%init> +my %opt = @_; +my $part_export = $opt{part_export} || FS::part_export->new; + +my @actions = split("\n", $part_export->option('action')); +my @oids = split("\n", $part_export->option('oid')); +my @values = split("\n", $part_export->option('value')); + +my @data; +while (@actions or @oids or @values) { + my @thisrow = (shift(@actions), shift(@oids), shift(@values)); + push @data, \@thisrow if grep length($_), @thisrow; +} + +my $popup_name = 'popup-'.time."-$$-".rand() * 2**32; +</%init> diff --git a/httemplate/edit/elements/part_export/foot.html b/httemplate/edit/elements/part_export/foot.html new file mode 100644 index 000000000..9cb8073ce --- /dev/null +++ b/httemplate/edit/elements/part_export/foot.html @@ -0,0 +1,6 @@ +</TABLE> +<INPUT TYPE="hidden" NAME="nodomain" VALUE="<% $opt{export_info}{nodomain} %>"> +<INPUT TYPE="submit" VALUE="<% $opt{part_export}->exportnum ? 'Apply changes' : 'Add export' %>"> +<%init> +my %opt = @_; +</%init> diff --git a/httemplate/edit/elements/part_export/head.html b/httemplate/edit/elements/part_export/head.html new file mode 100644 index 000000000..cb0ab894a --- /dev/null +++ b/httemplate/edit/elements/part_export/head.html @@ -0,0 +1,19 @@ +% if ( $export_info->{no_machine} ) { +<INPUT TYPE="hidden" NAME="machine" VALUE=""> +<INPUT TYPE="hidden" NAME="svc_machine" VALUE="N"> +% } else { +% # clone this from edit/part_export.cgi if this case ever gets used +% } +<INPUT TYPE="hidden" NAME="exporttype" VALUE="<%$layer |h%>"> +<% ntable('cccccc', 2) %> +<TR> + <TD ALIGN="right" ><% emt('Description') %></TD> + <TD BGCOLOR="#ffffff" WIDTH="600"><% $notes %></TD> +</TR> +<%init> +my %opt = @_; +my $layer = $opt{layer}; +my $part_export = $opt{part_export}; +my $export_info = $opt{export_info}; +my $notes = $opt{notes} || $export_info->{notes}; +</%init> diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi index 0407ee77b..4dd253be8 100644 --- a/httemplate/edit/part_export.cgi +++ b/httemplate/edit/part_export.cgi @@ -62,6 +62,15 @@ my $widget = new HTML::Widgets::SelectLayers( 'html_between' => "</TD></TR></TABLE>\n", 'layer_callback' => sub { my $layer = shift; + # create 'config_element' to generate the whole layer with a Mason component + if ( my $include = $exports->{$layer}{config_element} ) { + # might need to adjust the scope of this at some point + return $m->scomp($include, + part_export => $part_export, + layer => $layer, + export_info => $exports->{$layer} + ); + } my $html = qq!<INPUT TYPE="hidden" NAME="exporttype" VALUE="$layer">!. ntable("#cccccc",2); diff --git a/httemplate/edit/process/cdr_type.cgi b/httemplate/edit/process/cdr_type.cgi index b661de75d..ba9881dc4 100644 --- a/httemplate/edit/process/cdr_type.cgi +++ b/httemplate/edit/process/cdr_type.cgi @@ -10,7 +10,6 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); my %vars = $cgi->Vars; -warn Dumper(\%vars)."\n"; my %old = map { $_->cdrtypenum => $_ } qsearch('cdr_type', {}); diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi index 6432d6b15..bcb9c0df1 100644 --- a/httemplate/edit/process/part_export.cgi +++ b/httemplate/edit/process/part_export.cgi @@ -13,15 +13,40 @@ my $exportnum = $cgi->param('exportnum'); my $old = qsearchs('part_export', { 'exportnum'=>$exportnum } ) if $exportnum; +my %vars = $cgi->Vars; #fixup options #warn join('-', split(',',$cgi->param('options'))); my %options = map { - my @values = $cgi->param($_); - my $value = scalar(@values) > 1 ? join (' ', @values) : $values[0]; + my $value = $vars{$_}; + $value =~ s/\0/ /g; # deal with multivalued options $value =~ s/\r\n/\n/g; #browsers? (textarea) $_ => $value; } split(',', $cgi->param('options')); +# deal with multiline options +# %vars should never contain incomplete rows, but just in case it does, +# we make a list of all the row indices that contain values, and +# then write a line in each option for each row, even if it's empty. +# This ensures that all values with the same row index line up. +my %optionrows; +foreach my $option (split(',', $cgi->param('multi_options'))) { + $optionrows{$option} = {}; + my %values; # bear with me + for (keys %vars) { + /^$option(\d+)/ or next; + $optionrows{$option}{$1} = $vars{$option.$1}; + $optionrows{_ALL_}{$1} = 1 if length($vars{$option.$1}); + } +} +foreach my $option (split(',', $cgi->param('multi_options'))) { + my $value = ''; + foreach my $row (sort keys %{$optionrows{_ALL_}}) { + $value .= ($optionrows{$option}{$row} || '') . "\n"; + } + chomp($value); + $options{$option} = $value; +} + my $new = new FS::part_export ( { map { $_, scalar($cgi->param($_)); diff --git a/httemplate/edit/rate_time.cgi b/httemplate/edit/rate_time.cgi index 7ee39efca..9e6b8736c 100644 --- a/httemplate/edit/rate_time.cgi +++ b/httemplate/edit/rate_time.cgi @@ -15,12 +15,34 @@ <TD><INPUT TYPE="text" NAME="ratetimename" VALUE="<% $rate_time ? $rate_time->ratetimename : '' %>"></TD> </TR> </TABLE> -<% include('/elements/auto-table.html', - 'header' => [ '', 'Start','','', '','End','','' ], - 'fields' => [ qw(sd sh sm sa ed eh em ea) ], - 'select' => [ ($day, $hour, $min, $ampm) x 2 ], - 'data' => \@data, - ) %> +<TABLE> + <TR> + <TH COLSPAN=4 ALIGN="center">Start</TH> + <TH COLSPAN=4 ALIGN="center">End</TH> + </TR> + <TR id="mytemplate"> +% for my $pre (qw(s e)) { +% for my $f (qw(d h m a)) { # day, hour, minute, am/pm + <TD> + <SELECT NAME="<%$pre.$f%>"> +% my $i = 0; +% while ($i < @{ $choices{$f} }) { + <OPTION VALUE="<%$choices{$f}[$i]%>"> +% $i++; + <%$choices{$f}[$i]%></OPTION> +% $i++; +% } + </SELECT> + </TD> +% } #$f +% } #$pre + </TR> +<& /elements/auto-table.html, + 'template_row' => 'mytemplate', + 'data' => \@data, + 'fieldorder' => [qw(sd sh sm sa ed eh em ea)], +&> +</TABLE> <INPUT TYPE="submit" VALUE="<% $rate_time ? 'Apply changes' : 'Add period'%>"> </FORM> <BR> @@ -42,7 +64,12 @@ my $day = [ 0 => 'Sun', my $hour = [ map( {$_, sprintf('%02d',$_) } 12, 1..11 )]; my $min = [ map( {$_, sprintf('%02d',$_) } 0,30 )]; my $ampm = [ 0 => 'AM', 1 => 'PM' ]; - +my %choices = ( + 'd' => $day, + 'h' => $hour, + 'm' => $min, + 'a' => $ampm, +); if($ratetimenum) { $action = 'Edit'; $rate_time = qsearchs('rate_time', {ratetimenum => $ratetimenum}) diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html index 49222745a..ed011097e 100644 --- a/httemplate/elements/auto-table.html +++ b/httemplate/elements/auto-table.html @@ -1,166 +1,181 @@ <%doc> - -Example: -<% include('/elements/auto-table.html', - - ### - # required - ### - - 'header' => [ '#', 'Item', 'Amount' ], - 'fields' => [ 'id', 'name', 'amount' ], - - ### - # highly recommended - ### - - 'size' => [ 4, 12, 8 ], - 'maxl' => [ 4, 12, 8 ], - 'align' => [ 'right', 'left', 'right' ], - - ### - # optional - ### - - 'data' => [ [ 1, 'Widget', 25 ], - [ 12, 'Super Widget, 7 ] ], - #or - 'records' => [ qsearch('item', { } ) ], - # or any other array of FS::Record objects - - 'select' => [ '', - [ 1 => 'option 1', - 2 => 'option 2', ... - ], # options for second field - '' ], - - 'prefix' => 'mytable_', -) %> - -Values will be passed through as "mytable_id1", etc. +(within a form) +<table> +<tr> + <th>Field 1</th> + <th>Field 2</th> +</tr> +<tr id="mytemplate"> + <td><input type="text" name="field1"></td> + <td><select name="field2">...</td> + ... +</tr> +</table> +<& /elements/auto-table.html, + table => 'mytable', + template_row = 'mytemplate', + rows => [ + { field1 => 'foo', field2 => 'CA', ... }, + { field1 => 'bar', field2 => 'TX', ... }, ... + ], +&> + + or if you prefer: +... + fieldorder => [ 'field1', 'field2', ... ], + rows => [ + [ 'foo', 'CA' ], + [ 'bar', 'TX' ], + ], + +In the process/ handler, something like: +my @rows; +my %vars = $cgi->Vars; +for my $k ( keys %vars ) { + $k =~ /^${pre}magic(\d+)$/ or next; + my $rownum = $1; + # find all submitted names ending in this rownum + my %thisrow = + map { $_ => $vars{$_} } + grep /^(.*[\d])$rownum$/, keys %vars; + $thisrow->{num} = delete $thisrow{"${pre}magic$rownum"}; + push @rows, $thisrow; +} </%doc> - -<TABLE ID="<% $prefix %>AutoTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0> - <TR> -% foreach (@header) { - <TH><% $_ %></TH> -% } - </TR> -% my $row = 0; -% for ( $row = 0; $row < scalar @data; $row++ ) { - <TR> -% my $col = 0; -% for ( $col = 0; $col < scalar @fields; $col++ ) { -% my $id = $prefix . $fields[$col]; -% # don't suffix rownum in the final, blank row -% $id .= $row if $row < (scalar @data) - 1; - <TD> -% my @o = @{ $select[$col] }; -% if( @o ) { - <SELECT NAME="<% $id %>" ID="<% $id %>"> -% while(@o) { -% my $val = shift @o; - <OPTION VALUE=<% $val %><% -$val eq $data[$row][$col] ? ' SELECTED' : ''%>><% shift @o %></OPTION> -% } - </SELECT> -% } -% else { - <INPUT TYPE = "text" - NAME = "<% $id %>" - ID = "<% $id %>" - SIZE = <% $size[$col] %> - MAXLENGTH = <% $maxl[$col] %> - STYLE = "text-align:<% $align[$col] %>" - VALUE = "<% $data[$row][$col] %>" -% if( $opt{'autoadd'} ) { - onchange = "possiblyAddRow(this);" -% } - > - </TD> -% } -% } - <TD> - <IMG SRC = "<% "${p}images/cross.png" %>" - ALT = "X" - onclick = "deleteRow(this);" - > - </TD> - </TR> -% } -</TABLE> -% if( !$opt{'autoadd'} ) { -<INPUT TYPE="button" VALUE="Add" onclick="<% $prefix %>addRow();"><BR> -% } - -<SCRIPT TYPE="text/javascript"> - var <% $prefix %>rownum = <% $row %>; - var <% $prefix %>table = document.getElementById('<% $prefix %>AutoTable'); - // last row is initially blank, clone it and remove it - var <% $prefix %>_blank = - <% $prefix %>table.rows[<% $prefix %>table.rows.length-1].cloneNode(true); -% if( !$opt{'autoadd'} ) { - <% $prefix %>table.deleteRow(<% $prefix %>table.rows.length-1); -% } - - - - function rownum_of(obj) { - return (obj.parentNode.parentNode.sectionRowIndex); +<tbody id="<%$pre%>autotable"></tbody> +<script type="text/javascript"> +var <%$pre%>template; +var <%$pre%>tbody; +var <%$pre%>next_rownum; +var <%$pre%>set_rownum; +var <%$pre%>addRow; +var <%$pre%>deleteRow; +var <%$pre%>fieldorder = <% to_json($fieldorder) %>; + +function <%$pre%>possiblyAddRow_factory(obj) { + var callback = obj.onchange; + return function() { + if ( obj.rownum == <%$pre%>tbody.lastChild.rownum ) { + // then this is the last row, and it's being changed, so spawn a new row + <%$pre%>addRow(); + } + if ( callback ) { + callback.apply(obj); + } } +} - function <% $prefix %>possiblyAddRow(obj) { - if ( <% $prefix %>rownum == rownum_of(obj) ) { - <% $prefix %>addRow(); +function <%$pre%>set_rownum(obj, rownum) { + obj.rownum = rownum; + if ( obj.id ) { + obj.id = obj.id + rownum; + } + if ( obj.name ) { + obj.name = obj.name + rownum; + // also, in this case it's a form field that will be part of the record + // so set up an onchange handler + obj.onchange = <%$pre%>possiblyAddRow_factory(obj); + } + for (var i = 0; i < obj.children.length; i++) { + if ( obj.children[i] instanceof Node ) { + <%$pre%>set_rownum(obj.children[i], rownum); } } +} - function <% $prefix %>addRow() { - var row = <% $prefix %>table.insertRow(-1); - var cells = <% $prefix %>_blank.cells; - for (i=0; i<cells.length; i++) { - var node = row.appendChild(cells[i].cloneNode(true)); - var input = node.children[0]; - input.id = input.id + row.sectionRowIndex; - input.name = input.name + row.sectionRowIndex; +function <%$pre%>addRow(data) { + // duplicate the node + // warning: cloneNode doesn't clone event handlers that were set through + // the DOM + // if 'data' is an object, prepopulate the row's fields with the object's + // elements + // returns the rownum of the new row + var row = <%$pre%>template.cloneNode(true); + <%$pre%>tbody.appendChild(row); + var this_rownum = <%$pre%>next_rownum; + <%$pre%>set_rownum(row, this_rownum); + if(data instanceof Array) { + for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) { + var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0]; + if (el) { + el.value = data[i]; + } + } + } else if (data instanceof Object) { + for (var field in data) { + var el = document.getElementsByName(field + this_rownum)[0]; + if (el) { + el.value = data[field]; +% # doesn't work for checkbox + } } - <% $prefix %>rownum++; + } // else nothing + <%$pre%>next_rownum++; + return this_rownum; +} + +function <%$pre%>deleteRow(rownum) { + if ( rownum == <%$pre%>tbody.lastChild.rownum ) { + // if this is the last row, spawn another one after it + <%$pre%>addRow(); } + var r = document.getElementById('<%$pre%>row' + rownum); + <%$pre%>tbody.removeChild(r); +} - function deleteRow(obj) { - if(<% $prefix %>rownum == rownum_of(obj)) { - <% $prefix %>addRow(); - } - <% $prefix %>table.deleteRow(rownum_of(obj)); - <% $prefix %>rownum--; - return(false); +function <%$pre%>init() { + <%$pre%>template = document.getElementById(<% $template_row |js_string%>); + <%$pre%>tbody = document.getElementById('<%$pre%>autotable'); + <%$pre%>next_rownum = <%$pre%>template.sectionRowIndex; + // detach the template row + var table = <%$pre%>template.parentNode; + table.removeChild(<%$pre%>template); + // give it an id + <%$pre%>template.id = <%$pre |js_string%> + 'row'; + // and a magic identifier so we know it's been submitted + var magic = document.createElement('INPUT'); + magic.setAttribute('type', 'hidden'); + magic.setAttribute('name', '<%$pre%>magic'); + magic.value = '1'; + // and a delete button +%# should this be enclosed in an actual <button> for aesthetics? + var delete_button = document.createElement('IMG'); + delete_button.id = 'delete_button'; + delete_button.src = '<%$fsurl%>images/cross.png'; + delete_button.alt = 'X'; + // use an inline string for this so that it will be cloned properly + delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);"); + var delete_cell = document.createElement('TD'); + delete_cell.appendChild(delete_button); + delete_cell.appendChild(magic); // it has to go somewhere + <%$pre%>template.appendChild(delete_cell); + + // preload rows + var rows = <% to_json(\@rows) %>; + for (var i = 0; i < rows.length; i++) { + <%$pre%>addRow(rows[i]); } -</SCRIPT> + <%$pre%>addRow(); +} +<%$pre%>init(); +</script> <%init> my %opt = @_; - -my @header = @{ $opt{'header'} }; -my @fields = @{ $opt{'fields'} }; -my @data = (); -if($opt{'data'}) { - @data = @{ $opt{'data'} }; -} -elsif($opt{'records'}) { - foreach my $rec (@{ $opt{'records'} }) { - push @data, [ map { $rec->getfield($_) } @fields ]; +my $pre = ''; +$pre = $opt{'table'} . '_' if $opt{'table'}; +my $template_row = $opt{'template_row'} + or die "auto-table requires template_row\n"; # a DOM id + +my %vars = $cgi->Vars; +# rows that we will preload, as hashrefs of name => value +my @rows = @{ $opt{'data'} || [] }; +foreach (@rows) { + # allow an array of FS::Record objects to be passed + if ( blessed($_) and $_->isa('FS::Record') ) { + $_ = $_->hashref; } } -# else @data = (); -push @data, [ map {''} @fields ]; # make a blank row - -my $prefix = $opt{'prefix'}; -my @size = $opt{'size'} ? @{ $opt{'size'} } : (map {16} @fields); -my @maxl = $opt{'maxl'} ? @{ $opt{'maxl'} } : @size; -my @align = $opt{'align'} ? @{ $opt{'align'} } : (map {'right'} @fields); -my @select = @{ $opt{'select'} || [] }; -foreach (0..scalar(@fields)-1) { - $select[$_] ||= []; -} +my $fieldorder = $opt{'fieldorder'} || []; </%init> diff --git a/httemplate/elements/select-mib-popup.html b/httemplate/elements/select-mib-popup.html new file mode 100644 index 000000000..f8e3ae3da --- /dev/null +++ b/httemplate/elements/select-mib-popup.html @@ -0,0 +1,170 @@ +<& /elements/header-popup.html &> +<TABLE WIDTH="100%"> +<TR> + <TD ALIGN="right">Module:</TD> + <TD><SELECT ID="select_module"></SELECT></TD> +</TR> +<TR> + <TD ALIGN="right">Object:</TD> + <TD><INPUT TYPE="text" NAME="path" ID="input_path"></TD> +</TR> +<TR> + <TD COLSPAN=2> + <SELECT STYLE="width:100%" SIZE=12 ID="select_path"></SELECT> + </TD> +</TR> +<TR> + <TH COLSPAN=2 ID="mib_objectID"></TH> +</TR> +<TR> + <TD ALIGN="right">Module: </TD><TD ID="mib_moduleID"></TD> +</TR> +<TR> + <TD ALIGN="right">Data type: </TD><TD ID="mib_type"></TD> +</TR> +<TR> + <TH COLSPAN=2> + <BUTTON ID="submit_button" onclick="submit()" DISABLED=1>Continue</BUTTON> + </TH> +</TR> +</TABLE> +<& /elements/xmlhttp.html, + url => $p.'misc/xmlhttp-mib-browse.html', + subs => [qw( search get_module_list )], +&> +<SCRIPT TYPE="text/javascript"> + +var selected_mib; + +function show_info(state) { + document.getElementById('mib_objectID').style.display = + document.getElementById('mib_moduleID').style.display = + document.getElementById('mib_type').style.display = + state ? 'block' : 'none'; +} + +function clear_list() { + var select_path = document.getElementById('select_path'); + select_path.options.length = 0; +} + +function add_item(value) { + var select_path = document.getElementById('select_path'); + var input_path = document.getElementById('input_path'); + var opt = document.createElement('option'); + var v = value; + if ( v.match(/-$/) ) { + opt.className = 'leaf'; + v = v.substring(0, v.length - 1); + } + opt.value = opt.text = v; + opt.selected = (input_path.value == v); + select_path.add(opt, null); +} + +var timerID = 0; + +function populate(json_result) { + var result = JSON.parse(json_result); + clear_list(); + for (var x in result['choices']) { + opt = document.createElement('option'); + add_item(result['choices'][x]); + } + if ( result['objectID'] ) { + selected_mib = result; + show_info(true); + // show details on the selected node + document.getElementById('mib_objectID').innerHTML = result.objectID; + document.getElementById('mib_moduleID').innerHTML = result.moduleID; + document.getElementById('mib_type').innerHTML = result.type; + document.getElementById('submit_button').disabled = !result.type; + } else { + selected_mib = undefined; + show_info(false); + } +} + +function populate_modules(json_result) { + var result = JSON.parse(json_result); + var select_module = document.getElementById('select_module'); + var opt = document.createElement('option'); + opt.value = 'ANY'; + opt.text = '(any)'; + select_module.add(opt, null); + for (var x in result['modules']) { + opt = document.createElement('option'); + opt.value = opt.text = result['modules'][x]; + select_module.add(opt, null); + } +} + +function dispatch_search() { + // called from the interval timer + var search_string = document.getElementById('select_module').value + ':' + + document.getElementById('input_path').value; + + search(search_string, populate); +} + +function delayed_search() { + // onkeyup handler for the text input + // 500ms after the user stops typing, send the search request + if (timerID != 0) { + clearTimeout(timerID); + } + timerID = setTimeout(dispatch_search, 500); +} + +function handle_choose_object() { + // onchange handler for the selector + // when the user picks an option, set the text input to that, and then + // search for it as though it was entered + var input_path = document.getElementById('input_path'); + input_path.value = this.value; + dispatch_search(); +} + +function handle_choose_module() { + input_path.value = ''; // just to avoid confusion + delayed_search(); +} + +function submit() { +% if ( $callback ) { + <% $callback %>; + parent.nd(1); // close popup +% } else { + alert(document.getElementById('input_path').value); +% } +} + +var input_path = document.getElementById('input_path'); +input_path.onkeyup = delayed_search; +var select_path = document.getElementById('select_path'); +select_path.onchange = handle_choose_object; +var select_module = document.getElementById('select_module'); +select_module.onchange = handle_choose_module; +% if ( $cgi->param('curr_value') ) { +input_path.value = <% $cgi->param('curr_value') |js_string %>; +% } +dispatch_search(); +get_module_list('', populate_modules); + +</SCRIPT> +<& /elements/footer.html &> +<%init> +my $callback = 'alert("(no callback defined)" + selected_mib.stringify)'; +$cgi->param('callback') =~ /^(\w+)$/; +if ( $1 ) { + # construct the JS function call expresssion + $callback = 'window.parent.' . $1 . '(selected_mib'; + foreach ($cgi->param('arg')) { + # pass-through arguments + /^(\w+)$/ or next; + $callback .= ",'$1'"; + } + $callback .= ')'; +} + +</%init> diff --git a/httemplate/elements/xmlhttp.html b/httemplate/elements/xmlhttp.html index ac6f9916e..a9e65c790 100644 --- a/httemplate/elements/xmlhttp.html +++ b/httemplate/elements/xmlhttp.html @@ -14,14 +14,15 @@ Example: ); </%doc> -<% include( '/elements/rs_init_object.html' ) %> +<& /elements/rs_init_object.html &> +<& /elements/init_overlib.html &> <SCRIPT TYPE="text/javascript"> % foreach my $func ( @{$opt{'subs'}} ) { % % my $furl = $url; % $furl =~ s/\"/\\\\\"/; #javascript escape -% +%#" % @@ -66,15 +67,26 @@ Example: } else { var data = xmlhttp.responseText; //alert('received response: ' + data); - a[a.length-1](data); if ( data.indexOf("<b>System error</b>") > -1 ) { - var w; - if ( w = window.open("about:blank") ) { - w.document.write(data); - } else { - // popup blocking? should use an overlib popup instead - alert("Error popup disabled; try disabling popup blocking to see"); - } + // trim this a little + var end = data.indexOf('<a href="#raw">') - 1; + data = data.substring(0, end); + + overlib(data, + WIDTH, 480, MIDX, 0, MIDY, 0, + CAPTION, 'Error', STICKY, AUTOSTATUSCAP, DRAGGABLE, + CLOSECLICK, BGCOLOR, '#f00', CGCOLOR, '#f00' + ); + //var w; + //if ( w = window.open("about:blank") ) { + // w.document.write(data); + //} else { + // // popup blocking? should use an overlib popup instead + // alert("Error popup disabled; try disabling popup blocking to see"); + //} + } else { + // invoke the callback + a[a.length-1](data); } } } diff --git a/httemplate/misc/xmlhttp-mib-browse.html b/httemplate/misc/xmlhttp-mib-browse.html new file mode 100644 index 000000000..f3084ff6f --- /dev/null +++ b/httemplate/misc/xmlhttp-mib-browse.html @@ -0,0 +1,161 @@ +%#<% Data::Format::HTML->new->format($index{by_path}) %> +% my $json = "JSON"->new->canonical; +<% $json->encode($result) %> +<%init> +#<%once> #enable me in production +use SNMP; +SNMP::initMib(); +my $mib = \%SNMP::MIB; + +# make an index of the leaf nodes +my %index = ( + by_objectID => {}, # {.1.3.6.1.2.1.1.1} + by_fullname => {}, # {iso.org.dod.internet.mgmt.mib-2.system.sysDescr} + by_path => {}, # {iso}{org}{dod}{internet}{mgmt}{mib-2}{system}{sysDescr} + module => {}, #{SNMPv2-MIB}{by_path}{iso}{org}... + #{SNMPv2-MIB}{by_fullname}{iso.org...} +); + +my %name_of_oid = (); # '.1.3.6.1' => 'iso.org.dod.internet' + +# build up path names +my $fullname; +$fullname = sub { + my $oid = shift; + return $name_of_oid{$oid} if exists $name_of_oid{$oid}; + + my $object = $mib->{$oid}; + my $myname = '.' . $object->{label}; + # cut off the last element and recurse + $oid =~ /^(\.[\d\.]+)?(\.\d+)$/; + if ( length($1) ) { + $myname = $fullname->($1) . $myname; + } + return $name_of_oid{$oid} = $myname +}; + +my @oids = keys(%$mib); # dotted numeric OIDs +foreach my $oid (@oids) { + my $object = {}; + %$object = %{ $mib->{$oid} }; # untie it + # and remove references + delete $object->{parent}; + delete $object->{children}; + delete $object->{nextNode}; + $index{by_objectID}{$oid} = $object; + my $myname = $fullname->($oid); + $object->{fullname} = $myname; + $index{by_fullname}{$myname} = $object; + my $moduleID = $object->{moduleID}; + $index{module}{$moduleID} ||= { by_fullname => {}, by_path => {} }; + $index{module}{$moduleID}{by_fullname}{$myname} = $object; +} +my @names = sort {$a cmp $b} keys %{ $index{by_fullname} }; +foreach my $myname (@names) { + my $obj = $index{by_fullname}{$myname}; + my $moduleID = $obj->{moduleID}; + my @parts = split('\.', $myname); + shift @parts; # always starts with an empty string + for ($index{by_path}, $index{module}{$moduleID}{by_path}) { + my $subindex = $_; + for my $this_part (@parts) { + $subindex = $subindex->{$this_part} ||= {}; + } + # $subindex now = $index{by_path}{foo}{bar}{baz}. + # set {''} = the object with that name. + # and set object $index{by_path}{foo}{bar}{baz}{''} = + # the object named .foo.bar.baz + $subindex->{''} = $obj; + } +} + +#</%once> +#<%init> +# no ACL for this +my $sub = $cgi->param('sub'); +my $result = {}; +if ( $sub eq 'search' ) { + warn "search: ".$cgi->param('arg')."\n"; + my ($module, $string) = split(':', $cgi->param('arg'), 2); + my $idx; # the branch of the index to use for this search + if ( $module eq 'ANY' ) { + $idx = \%index; + } elsif (exists($index{module}{$module}) ) { + $idx = $index{module}{$module}; + } else { + warn "unknown MIB moduleID: $module\n"; + $idx = {}; # will return nothing, because you've somehow sent a bad moduleID + } + if ( exists($index{by_fullname}{$string}) ) { + warn "exact match\n"; + # don't make this module-selective--if the path matches an existing + # object, return that object + %$result = %{ $index{by_fullname}{$string} }; # put the object info in $result + #warn Dumper $result; + } + my @choices; # menu options to return + if ( $string =~ /^[\.\d]+$/ ) { + # then this is a numeric path + # ignore the module filter, and return everything starting with $string + if ( $string =~ /^\./ ) { + @choices = grep /^\Q$string\E/, keys %{$index{by_objectID}}; + } else { + # or everything containing it + @choices = grep /\Q$string\E/, keys %{$index{by_objectID}}; + } + @choices = map { $index{by_objectID}{$_}->{fullname} } @choices; + } elsif ( $string eq '' or $string =~ /^\./ ) { + # then this is an absolute path + my @parts = split('\.', $string); + shift @parts; + my $subindex = $idx->{by_path}; + my $path = ''; + @choices = keys %$subindex; + # walk all the specified path parts + foreach my $this_part (@parts) { + # stop before walking off the map + last if !exists($subindex->{$this_part}); + $subindex = $subindex->{$this_part}; + $path .= '.' . $this_part; + @choices = grep {$_} keys %$subindex; + } + # skip uninteresting nodes: those that aren't accessible nodes (have no + # data type), and have only one path forward + while ( scalar(@choices) == 1 + and (!exists $subindex->{''} or $subindex->{''}->{type} eq '') ) { + + $subindex = $subindex->{ $choices[0] }; + $path .= '.' . $choices[0]; + @choices = grep {$_} keys %$subindex; + + } + + # if we are on an existing node, and the entered path didn't exactly + # match another node, return the current node as the result + if (!keys %$result and exists($subindex->{''})) { + %$result = %{ $subindex->{''} }; + } + # prepend the path up to this point + foreach (@choices) { + $_ = $path.'.'.$_; + # also label accessible nodes for the UI + if ( exists($subindex->{$_}{''}) and $subindex->{$_}{''}{'type'} ) { + $_ .= '-'; + } + } + # also include one level above the originally requested path, + # for tree-like navigation + if ( $string =~ /^(.+)\.[^\.]+/ ) { + unshift @choices, $1; + } + } else { + # then this is a full-text search + warn "/$string/\n"; + @choices = grep /\Q$string\E/i, keys(%{ $idx->{by_fullname} }); + } + @choices = sort @choices; + $result->{choices} = \@choices; +} elsif ( $sub eq 'get_module_list' ) { + $result = { modules => [ sort keys(%{ $index{module} }) ] }; +} +</%init> |