lafe
lafe

Reputation: 31

Perl and SSH communication

I need a ssh communication between a client that runs in a Perl script and a host in a linux pc. The Perl script will establish ssh communication, send a command to the host to run a remote .py script that is stored in the linux machine, and then send commands to the python script, that will execute specific actions, and wait for next command to execute.

So far, I couldn’t find a way to do it. Any suggestions? Plus, I need a solution with “use Net::OpenSSH” and not with “use Net::SSH::Perl”

The python script goes like that:

import sys

def process_command(command):
    if command == "cmd1":
        sys.stdout.write("cmd1 executed\n")
    elif command == "cmd2":
        sys.stdout.write("cmd2 executed \n")
    elif command == "exit":
        sys.stdout.write("exit!\n")
        sys.exit(0)
    else:
        sys.stdout.write("Invalid command\n")

for line in sys.stdin:
    # Remove any leading/trailing whitespaces and newline characters
    command=line.strip()
    process_command(command)
    # send an ack message to Perl client
    sys.stdout.write("OK")
    sys.stdout.flush()  # Ensure the output is immediately flushed

when I run it from the linux’s terminal, it seems to work.

The Perl subroutines go like that:

use Net::OpenSSH; 
sub _openSshConnection
{   
    my $self = shift;
    my $success = 0;
 
    if (!defined $self->{args}->{ctrlHost} ||
        !defined $self->{args}->{ctrlUser} ||
        !defined $self->{args}->{ctrlPass})
    {   
        $self->_DEBUG("Hostname & User credentials not defined.");  
        $success = 0;
    }
    else
    {
        $self->_DEBUG("Establising SSH connection to %s.", $self->{args}->{ctrlHost});
        my $sshDir = UBX::Utils::getOpenSSHCtlDir();
        my $ssh = Net::OpenSSH->new( $self->{args}->{ctrlHost},
                                user => $self->{args}->{ctrlUser},
                                password => $self->{args}->{ctrlPass},
                                master_opts => [-o => "StrictHostKeyChecking=no", '-X']
                                ctl_dir => $sshDir,
                                strict_mode => 0
                                );
        $success = $ssh;
        if ($ssh->error and die "Couldn't establish SSH connection: ". $ssh->error) 
        {   
            $success = 0;
        }       
    }
    $self->_DEBUG("SSH connection:".$success);
    return $success;
}

##########################################################################################
sub python_Start
{
my $self = shift;
    my $success = 0;
    my @commands = ('cmd1','cmd2');
my $cmd = 'python3 /path/script.py';
$self->{ssh}->system($cmd);

Like that if I go to my Perl terminal, write a command (cmd1), it is executed, and I receive in the terminal also the messages sent from python. But how do I write and read the commands and responses inside my script? I want something like:

foreach my $command (@commands) {
    # send command1
    # get ack
        # then send the following command 
        # etc
}

Upvotes: 2

Views: 323

Answers (3)

Diab Jerius
Diab Jerius

Reputation: 2320

I have code which does this very thing. Here's the gist of it.

  1. Create a connection
    my $ssh = Net::OpenSSH->new( $host, %args ); 
    $ssh->error and die("error");
  1. Create a PTY that Expect can use
    my ( $pty, undef ) = $ssh->open2pty( $remote_command );
  1. Connect Expect to it
    my $exp = Expect->init( $pty );
  1. Profit

Upvotes: 2

Håkon Hægland
Håkon Hægland

Reputation: 40778

But how do I write and read the commands and responses inside my script?

You could try using open_ex() to open pipes for reading and writing from the python script. For example,

my $ssh = Net::OpenSSH->new(
    $host,
    user     => $user,
    port     => $port,
    password => $password,
);
$ssh->error and
  die "Couldn't establish SSH connection: ". $ssh->error;

my @cmd = ('python3', 'script.py');
my %opts = (
    stdin_pipe   => 1,
    stdout_pipe  => 1,
    stderr_pipe  => 1,
);

my ($in, $out, $err, $pid) = $ssh->open_ex(\%opts, @cmd);
say "PID = $pid";
my @commands = ('cmd1', 'cmd2', 'exit');
for my $cmd (@commands) {
    say $in $cmd;
    my $result = <$out>;
    chomp $result;
    say "got result: '$result'";
    sleep 1;
}
my $kid = waitpid $pid, 0;  # reap the slave SSH process
#say "waitpid returned: $kid";

Note that in general you should use IO::Select to avoid deadlocks when trying to read from a process that might be expecting input before it writes anything.

Edit

To handle possible deadlocks you could replace the for loop with:

# .... <-- previous code as before
use IO::Select;
my $timeout = 5;
use constant {
    READ    => 0x1,
    WRITE   => 0x2,
    TIMEOUT => 0x4,
    ERROR   => 0x8,
    EOF     => 0x10,
};

my $cmd = shift @commands;
while (1) {
    my ($status, $child_output) = read_or_write_data($in, $out, $cmd, $timeout);
    if ($status & READ) {
        say "got result: '$child_output'";
    }
    if ($status & WRITE) {
        say "Sendt command: '$cmd'";
        $cmd = shift @commands;
    }
    if ($status & TIMEOUT) {
        say "Read/write from child timed out";
        last;
    }
    if ($status & ERROR) {
        say "Read/write from child failed";
        last;
    }
    if ($status & EOF) {
        say "Child process read handle EOF";
        last;
    }
}

my $kid = waitpid $pid, 0;  # reap the slave SSH process
#say "waitpid returned: $kid";

sub read_or_write_data {
    my ($writer, $reader, $cmd, $timeout) = @_;
    my $status = 0;
    my $sel_writers = IO::Select->new( $writer );
    my $sel_readers = IO::Select->new( $reader );
    my $child_output = '';
    local $! = 0; # Clear ERRNO such that we can differentiate between timeout and other errors
    my @sel_result = IO::Select::select( $sel_readers, $sel_writers, undef, $timeout );
    if (!@sel_result) {
        if ($!) {
            say "Select() failed with error: $!";
            $status |= ERROR;
        }
        else {
            $status |= TIMEOUT;
        }
    }
    my @read_ready = @{ $sel_result[0] };
    my @write_ready = @{ $sel_result[1] };
    if ( @write_ready ) {
        if ($cmd) {
            say $writer $cmd;
            $status |= WRITE;
        }
    }
    if ( @read_ready ) {
        $child_output = <$reader>;
        if (!defined $child_output) {
            $status |= EOF;
        }
        else {
            chomp $child_output;
            $status |= READ;
        }
    }
    return ($status, $child_output);
}

Upvotes: 3

zdim
zdim

Reputation: 66944

One way, using capture from Net::OpenSSH

use warnings;
use strict;
use feature 'say';

use Net::OpenSSH; 

my $host = shift or die "Usage: $0 hostname\n";

#my $ssh = Net::OpenSSH->new($host, %ctor_opts);  # provide password etc
my $ssh = Net::OpenSSH->new($host); 
die "ssh failed: ", $ssh->error if $ssh->error; 

my @ret = $ssh->capture('ls -l'); 
chomp @ret;
say for @ret[0..2], '...', @ret[-2,-1]; 

say '';

@ret = $ssh->capture('ls | head -5'); 

print for @ret;

See documentation for how to provide the password via options to the constructor.

Upvotes: 1

Related Questions