Reputation: 31
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
Reputation: 2320
I have code which does this very thing. Here's the gist of it.
my $ssh = Net::OpenSSH->new( $host, %args );
$ssh->error and die("error");
my ( $pty, undef ) = $ssh->open2pty( $remote_command );
my $exp = Expect->init( $pty );
Upvotes: 2
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.
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
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