From: Ivan Kohler Date: Sat, 19 Aug 2017 22:49:20 +0000 (-0700) Subject: expect-style ssh interaction, for interation w/cisco and other networking eqipment... X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=60379ff3215e4bfe644c4777e8156195133d9c49 expect-style ssh interaction, for interation w/cisco and other networking eqipment, RT#77180 --- diff --git a/FS/FS/part_export/broadband_shellcommands.pm b/FS/FS/part_export/broadband_shellcommands.pm index 44280a200..d3e495c45 100644 --- a/FS/FS/part_export/broadband_shellcommands.pm +++ b/FS/FS/part_export/broadband_shellcommands.pm @@ -70,7 +70,18 @@ sub _export_command { my $command = $self->option($action); return '' if $command =~ /^\s*$/; - #set variables for the command + my $command_string = $self->_export_subvars( $svc_broadband, $command ); + + $self->shellcommands_queue( $svc_broadband->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => $command_string, + ); +} + +sub _export_subvars { + my( $self, $svc_broadband, $command ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -85,20 +96,25 @@ sub _export_command { $locationnum = $cust_pkg ? $cust_pkg->locationnum : ''; $custnum = $cust_pkg ? $cust_pkg->custnum : ''; - #done setting variables for the command + eval(qq("$command")); +} - $self->shellcommands_queue( $svc_broadband->svcnum, +sub _export_replace { + my($self, $new, $old ) = (shift, shift, shift); + my $command = $self->option('replace'); + + my $command_string = $self->_export_subvars_replace( $new, $old, $command ); + + $self->shellcommands_queue( $new->svcnum, user => $self->option('user')||'root', host => $self->machine, - command => eval(qq("$command")), + command => $command_string, ); } -sub _export_replace { - my($self, $new, $old ) = (shift, shift, shift); - my $command = $self->option('replace'); +sub _export_subvars_replace { + my( $self, $new, $old, $command ) = @_; - #set variable for the command no strict 'vars'; { no strict 'refs'; @@ -120,15 +136,10 @@ sub _export_replace { $new_locationnum = $new_cust_pkg ? $new_cust_pkg->locationnum : ''; $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : ''; - #done setting variables for the command - - $self->shellcommands_queue( $new->svcnum, - user => $self->option('user')||'root', - host => $self->machine, - command => eval(qq("$command")), - ); + eval(qq("$command")); } + #a good idea to queue anything that could fail or take any time sub shellcommands_queue { my( $self, $svcnum ) = (shift, shift); diff --git a/FS/FS/part_export/broadband_shellcommands_expect.pm b/FS/FS/part_export/broadband_shellcommands_expect.pm new file mode 100644 index 000000000..ec525d38a --- /dev/null +++ b/FS/FS/part_export/broadband_shellcommands_expect.pm @@ -0,0 +1,19 @@ +package FS::part_export::broadband_shellcommands_expect; +use base qw( FS::part_export::shellcommands_expect ); + +use strict; +use FS::part_export::broadband_shellcommands; + +our %info = %FS::part_export::shellcommands_expect::info; +$info{'svc'} = 'svc_broadband'; +$info{'desc'} = 'Real time export via remote SSH, with interactive ("Expect"-like) scripting, for svc_broadband services'; + +sub _export_subvars { + FS::part_export::broadband_shellcommands::_export_subvars(@_) +} + +sub _export_subvars_replace { + FS::part_export::broadband_shellcommands::_export_subvars_replace(@_) +} + +1; diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm index 647dc5f4d..775af17ae 100644 --- a/FS/FS/part_export/shellcommands.pm +++ b/FS/FS/part_export/shellcommands.pm @@ -4,6 +4,7 @@ use vars qw(@ISA %info); use Tie::IxHash; use Date::Format; use String::ShellQuote; +use Net::OpenSSH; use FS::part_export; use FS::Record qw( qsearch qsearchs ); @@ -296,7 +297,7 @@ sub _export_command_or_super { } else { $self->_export_command($action, @_); } -}; +} sub _export_command { my ( $self, $action, $svc_acct) = (shift, shift, shift); @@ -305,6 +306,41 @@ sub _export_command { return '' if $command =~ /^\s*$/; my $stdin = $self->option($action."_stdin"); + my( $command_string, $stdin_string ) = + $self->_export_subvars( $svc_acct, $command, $stdin ); + + $self->ssh_or_queue( $svc_acct, $command_string, $stdin_string ); +} + +sub ssh_or_queue { + my( $self, $svc_acct, $command_string, $stdin_string ) = @_; + + my @ssh_cmd_args = ( + user => $self->option('user') || 'root', + host => $self->svc_machine($svc_acct), + command => $command_string, + stdin_string => $stdin_string, + ignored_errors => $self->option('ignored_errors') || '', + ignore_all_errors => $self->option('ignore_all_errors'), + fail_on_output => $self->option('fail_on_output'), + ); + + if ( $self->option($action. '_no_queue') ) { + # discard return value just like freeside-queued. + eval { ssh_cmd(@ssh_cmd_args) }; + $error = $@; + $error = $error->full_message if ref $error; # Exception::Class::Base + return $error. + ' ('. $self->exporttype. ' to '. $self->svc_machine($svc_acct). ')' + if $error; + } else { + $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args ); + } +} + +sub _export_subvars { + my( $self, $svc_acct, $command, $stdin ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -412,27 +448,7 @@ sub _export_command { my $command_string = eval(qq("$command")); return "error filling in command: $@" if $@; - my @ssh_cmd_args = ( - user => $self->option('user') || 'root', - host => $self->svc_machine($svc_acct), - command => $command_string, - stdin_string => $stdin_string, - ignored_errors => $self->option('ignored_errors') || '', - ignore_all_errors => $self->option('ignore_all_errors'), - fail_on_output => $self->option('fail_on_output'), - ); - - if ( $self->option($action. '_no_queue') ) { - # discard return value just like freeside-queued. - eval { ssh_cmd(@ssh_cmd_args) }; - $error = $@; - $error = $error->full_message if ref $error; # Exception::Class::Base - return $error. - ' ('. $self->exporttype. ' to '. $self->svc_machine($svc_acct). ')' - if $error; - } else { - $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args ); - } + ( $command_string, $stdin_string ); } sub _export_replace { @@ -440,6 +456,16 @@ sub _export_replace { my $command = $self->option('usermod'); return '' if $command =~ /^\s*$/; my $stdin = $self->option('usermod_stdin'); + + my( $command_string, $stdin_string ) = + $self->_export_subvars_replace( $new, $old, $command, $stdin ); + + $self->ssh_or_queue( $new, $command_string, $stdin_string ); +} + +sub _export_subvars_replace { + my( $self, $new, $old, $command, $stdin ) = @_; + no strict 'vars'; { no strict 'refs'; @@ -511,27 +537,7 @@ sub _export_replace { my $command_string = eval(qq("$command")); - my @ssh_cmd_args = ( - user => $self->option('user') || 'root', - host => $self->svc_machine($new), - command => $command_string, - stdin_string => $stdin_string, - ignored_errors => $self->option('ignored_errors') || '', - ignore_all_errors => $self->option('ignore_all_errors'), - fail_on_output => $self->option('fail_on_output'), - ); - - if($self->option('usermod_no_queue')) { - # discard return value just like freeside-queued. - eval { ssh_cmd(@ssh_cmd_args) }; - $error = $@; - $error = $error->full_message if ref $error; # Exception::Class::Base - return $error. ' ('. $self->exporttype. ' to '. $self->svc_machine($new). ')' - if $error; - } - else { - $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args ); - } + ( $command_string, $stdin_string ); } #a good idea to queue anything that could fail or take any time @@ -545,7 +551,6 @@ sub shellcommands_queue { } sub ssh_cmd { #subroutine, not method - use Net::OpenSSH; my $opt = { @_ }; open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n"; my $ssh = Net::OpenSSH->new( diff --git a/FS/FS/part_export/shellcommands_expect.pm b/FS/FS/part_export/shellcommands_expect.pm new file mode 100644 index 000000000..c2a4118e2 --- /dev/null +++ b/FS/FS/part_export/shellcommands_expect.pm @@ -0,0 +1,128 @@ +package FS::part_export::shellcommands_expect; +use base qw( FS::part_export::shellcommands ); + +use strict; +use Tie::IxHash; +use Net::OpenSSH; +use Expect; +#use FS::Record qw( qsearch qsearchs ); + +tie my %options, 'Tie::IxHash', + 'user' => { label =>'Remote username', default=>'root' }, + 'useradd' => { label => 'Insert commands', type => 'textarea', }, + 'userdel' => { label => 'Delete commands', type => 'textarea', }, + 'usermod' => { label => 'Modify commands', type => 'textarea', }, + 'suspend' => { label => 'Suspend commands', type => 'textarea', }, + 'unsuspend' => { label => 'Unsuspend commands', type => 'textarea', }, + 'debug' => { label => 'Enable debugging', + type => 'checkbox', + value => 1, + }, +; + +our %info = ( + 'svc' => 'svc_acct', + 'desc' => 'Real time export via remote SSH, with interactive ("Expect"-like) scripting, for svc_acct services', + 'options' => \%options, + 'notes' => q[ +Interactively run commands via SSH in a remote terminal, like "Expect". In +most cases, you probably want a regular shellcommands (or broadband_shellcommands, etc.) export instead, unless +you have a specific need to interact with a terminal-based interface in an +"Expect"-like fashion. +

+ +Each line specifies a string to match and a command to +run after that string is found, separated by the first space. For example, to +run "exit" after a prompt ending in "#" is sent, "# exit". You will need to +setup SSH for unattended operation. +

+ +In commands, all variable substitutions of the regular shellcommands (or +broadband_shellcommands, etc.) export are available (use a backslash to escape +a literal $). +] +); + +sub _export_command { + my ( $self, $action, $svc_acct) = (shift, shift, shift); + my @lines = split("\n", $self->option($action) ); + + return '' unless @lines; + + my @commands = (); + foreach my $line (@lines) { + my($match, $command) = split(' ', $line, 2); + my( $command_string ) = $self->_export_subvars( $svc_acct, $command, '' ); + push @commands, [ $match, $command_string ]; + } + + $self->shellcommands_expect_queue( $svc_acct->svcnum, @commands ); +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, shift); + my @lines = split("\n", $self->option('replace') ); + + return '' unless @lines; + + my @commands = (); + foreach my $line (@lines) { + my($match, $command) = split(' ', $line, 2); + my( $command_string ) = $self->_export_subvars_replace( $new, $old, $command, '' ); + push @commands, [ $match, $command_string ]; + } + + $self->shellcommands_expect_queue( $new->svcnum, @commands ); +} + +sub shellcommands_expect_queue { + my( $self, $svcnum, @commands ) = @_; + + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::shellcommands_expect::ssh_expect", + }; + $queue->insert( + user => $self->option('user') || 'root', + host => $self->machine, + debug => $self->option('debug'), + commands => \@commands, + ); +} + +sub ssh_expect { #subroutine, not method + my $opt = { @_ }; + + my $dest = $opt->{'user'}.'@'.$opt->{'host'}; + + open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n"; + my $ssh = Net::OpenSSH->new( $dest, 'default_stdin_fh' => $def_in ); + # ignore_all_errors doesn't override SSH connection/auth errors-- + # probably correct + die "Couldn't establish SSH connection to $dest: ". $ssh->error + if $ssh->error; + + my ($pty, $pid) = $ssh->open2pty + or die "Couldn't start a remote terminal session"; + my $expect = Expect->init($pty); + #not useful #$expect->debug($opt->{debug} ? 3 : 0); + + foreach my $line ( @{ $opt->{commands} } ) { + my( $match, $command ) = @$line; + + warn "Waiting for '$match'\n" if $opt->{debug}; + + my $matched = $expect->expect(30, $match); + unless ( $matched ) { + my $err = "Never saw '$match'\n"; + warn $err; + die $err; + } + warn "Running '$command'\n" if $opt->{debug}; + $expect->send("$command\n"); + } + + ''; +} + +1; diff --git a/debian/control b/debian/control index c4144e1fb..390d3c0e2 100644 --- a/debian/control +++ b/debian/control @@ -90,7 +90,7 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor, libxml-writer-perl, libio-socket-ssl-perl, libmap-splat-perl, libdatetime-format-ical-perl, librest-client-perl, libbusiness-onlinepayment-perl, - libnet-vitelity-perl (>= 0.05), libnet-sslglue-perl + libnet-vitelity-perl (>= 0.05), libnet-sslglue-perl, libexpect-perl Suggests: libbusiness-onlinepayment-perl Description: Libraries for Freeside billing and trouble ticketing Freeside is a web-based billing and trouble ticketing application.