expect-style ssh interaction, for interation w/cisco and other networking eqipment...
authorIvan Kohler <ivan@freeside.biz>
Sat, 19 Aug 2017 22:49:20 +0000 (15:49 -0700)
committerIvan Kohler <ivan@freeside.biz>
Sat, 19 Aug 2017 22:49:20 +0000 (15:49 -0700)
FS/FS/part_export/broadband_shellcommands.pm
FS/FS/part_export/broadband_shellcommands_expect.pm [new file with mode: 0644]
FS/FS/part_export/shellcommands.pm
FS/FS/part_export/shellcommands_expect.pm [new file with mode: 0644]
debian/control

index 44280a2..d3e495c 100644 (file)
@@ -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 (file)
index 0000000..ec525d3
--- /dev/null
@@ -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;
index 647dc5f..775af17 100644 (file)
@@ -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 (file)
index 0000000..c2a4118
--- /dev/null
@@ -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.
+<BR><BR>
+
+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
+<a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
+<BR><BR>
+
+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;
index c4144e1..390d3c0 100644 (file)
@@ -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.