I have been using Perl::Net::SSH to automate running some scripts on my remote boxes. However, some of these scripts take a really long time to complete (hour or two) and sometimes, I stop getting data from them, without actually losing the connection.
Here's the code I'm using:
sub run_regression_tests {
for(my $i = 0; $i < #servers; $i++){
my $inner = $users[$i];
foreach(#$inner){
my $user = $_;
my $server = $servers[$i];
my $outFile;
open($outFile, ">" . $outputDir . $user . "#" . $server . ".log.txt");
print $outFile "Opening connection to $user at $server on " . localtime() . "\n\n";
close($outFile);
my $pid = $pm->start and next;
print "Connecting to $user#" . "$server...\n";
my $hasWentToDownloadYet = 0;
my $ssh = Net::SSH::Perl->new($server, %sshParams);
$ssh->login($user, $password);
$ssh->register_handler("stdout", sub {
my($channel, $buffer) = #_;
my $outFile;
open($outFile, ">>", $outputDir . $user . "#" . $server . ".log.txt");
print $outFile $buffer->bytes;
close($outFile);
my #lines = split("\n", $buffer->bytes);
foreach(#lines){
if($_ =~ m/REGRESSION TEST IS COMPLETE/){
$ssh->_disconnect();
if(!$hasWentToDownloadYet){
$hasWentToDownloadYet = 1;
print "Caught exit signal.\n";
print("Regression tests for ${user}\#${server} finised.\n");
download_regression_results($user, $server);
$pm->finish;
}
}
}
});
$ssh->register_handler("stderr", sub {
my($channel, $buffer) = #_;
my $outFile;
open($outFile, ">>", $outputDir . $user . "#" . $server . ".log.txt");
print $outFile $buffer->bytes;
close($outFile);
});
if($debug){
$ssh->cmd('tail -fn 40 /GDS/gds/gdstest/t-gds-master/bin/comp.reg');
}else{
my ($stdout, $stderr, $exit) = $ssh->cmd('. ./.profile && cleanall && my.comp.reg');
if(!$exit){
print "SSH connection failed for ${user}\#${server} finised.\n";
}
}
#$ssh->cmd('. ./.profile');
if(!$hasWentToDownloadYet){
$hasWentToDownloadYet = 1;
print("Regression tests for ${user}\#${server} finised.\n");
download_regression_results($user, $server);
}
$pm->finish;
}
}
sleep(1);
print "\n\n\nAll tests started. Tests typically take 1 hour to complete.\n";
print "If they take significantly less time, there could be an error.\n";
print "\n\nNo output will be printed until all commands have executed and finished.\n";
print "If you wish to watch the progress tail -f one of the logs this script produces.\n Example:\n\t" . 'tail -f ./gds1#tdgds10.log.txt' . "\n";
$pm->wait_all_children;
print "\n\nAll Tests are Finished. \n";
}
And here is my %sshParams:
my %sshParams = (
protocol => '2',
port => '22',
options => [
"TCPKeepAlive yes",
"ConenctTimeout 10",
"BatchMode yes"
]
);
Sometimes randomly one of the long running commands just halts printing/firing the stdout or stderr events and never exits. The ssh connection doesn't die (as far as I'm aware) because the $ssh->cmd is still blocking.
Any idea how to correct this behaviour?
In your %sshParams hash, you may need to add "TCPKeepAlive yes" to your options:
$sshParams{'options'} = ["BatchMode yes", "TCPKeepAlive yes"];
Those options might or might not be right for you, but the TCPKeepAlive is something I would recommend setting for any long running SSH connection. If you have any kind of stateful firewall in your path it could drop the state if it hasn't passed traffic over the connection for a long period of time.
It fails probably due to the way you look into the output for the REGRESSION TEST IS COMPLETE mark. It may be split over two different SSH packets and so your callback will never found it.
Better, use a remote command that ends when it is done as this one-liner:
perl -pe 'BEGIN {$p = open STDIN, "my.comp.reg |" or die $!}; kill TERM => -$p if /REGRESSION TEST IS COMPLETE/}'
Otherwise, you are closing the remote connection but not stopping the remote process that will stay alive.
Besides that, you should try using Net::OpenSSH or Net::OpenSSH::Parallel instead of Net::SSH::Perl:
use Net::OpenSSH::Parallel;
my $pssh = Net::OpenSSH::Parallel->new;
for my $i (0..$#server) {
my $server = $server[$i];
for my $user (#{$users[$ix]}) {
$pssh->add_host("$user\#$server", password => $password);
}
}
if ($debug) {
$pssh->all(cmd => { stdout_file => "$outputDir%USER%\#%HOST%.log.txt",
stderr_to_stdout => 1 },
'fail -fn 40 /GDS/gds/gdstest/t-gds-master/bin/comp.reg');
}
else {
$pssh->all(cmd => { stdout_file => "$outputDir%USER%\#%HOST%.log.txt",
stderr_to_stdout => 1 },
'. ./.profile && cleanall && my.comp.reg');
}
$pssh->all(scp_get => $remote_regression_results_path, "regression_results/%USER%\#%HOST%/");
$pssh->run;
Related
Yesterday, I wrote a perl plugin script for Pidgin 2.10.9, running on Windows 7, and using Strawberry Perl 5.10.1.5
Basically, on the receipt of an IM, it uses backticks to call a console application (written in .NET) and returns the console output to the sender as an IM.
I had to reboot this morning, but ever since I rebooted, it has stopped working.
So, I changed the backticks to use "capture". That didn't work either, but it at least gave me this error:
(15:00:33) Plugin: Error: Error in IPC::System::Simple plumbing: "Can't dup STDOUT" - "Bad file descriptor" at (eval 12) line 53
I have no idea what's changed from yesterday to today, and wondered if anybody knew what might be causing the error?
Thanks
Edit: Thought I'd add my code
use Purple;
#use IPC::System::Simple qw(system systemx capture capturex);
use IPC::System::Simple qw(capture capturex);
%PLUGIN_INFO = (
perl_api_version => 2,
name => "PlugIn",
version => "0.1",
summary => "AutoResp",
description => "PlugIn",
author => "Mark Watkin",
url => "http://",
load => "plugin_load",
unload => "plugin_unload"
);
sub plugin_init {
return %PLUGIN_INFO;
}
sub plugin_load {
my $plugin = shift;
Purple::Debug::info("PlugIn", "plugin_load()\n");
$data = "";
$conversation_handle = Purple::Conversations::get_handle();
Purple::Signal::connect($conversation_handle, "received-im-msg", $plugin, \&signal_chat_callback, $data);
}
sub plugin_unload {
my $plugin = shift;
Purple::Debug::info("PlugIn", "plugin_unload()\n");
}
sub signal_chat_callback {
# The signal data and the user data come in as arguments
my ($account, $sender, $message, $conv, $flags) = #_;
Purple::Debug::info("PlugIn", "Account Alias \"" . $account->get_alias() . "\"\n");
if( $account->get_alias() eq "PlugIn" )
{
Purple::Debug::info("PlugIn", "Request: \"" . $message . "\"\n");
if(!$conv)
{
Purple::Debug::info("PlugIn", "No conversation\n");
$conv = Purple::Conversation->new(1, $account, $sender);
}
$im = $conv->get_im_data();
$im->send( "One moment please..." );
my $query = "";
# eval {
# $query = capture("\"D:\\SourceCode\\PlugInNET\\bin\\Debug\\PlugInNET.exe\" \"" . $message . "\"");
# #$query = capture("\"D:\\SourceCode\\PlugInNET\\bin\\Debug\\PlugInNET.exe\"", "\"" . $message . "\"");
# #my $query = capture("D:\\SourceCode\\PlugInNET\\bin\\Debug\\PlugInNET.exe");
# #my $query = `\"D:\\SourceCode\\PlugInNET\\bin\\Debug\\PlugInNET.exe\" \"$message\"`;
# #my $query = `dir /b`;
# };
# if( $# )
# {
# Purple::Debug::info("PlugIn", "Error: " . $# . "\n");
# }
Purple::Debug::info("PlugIn", "Query: " . $query . "\n");
open ( my $fh, "-|", "D:\\SourceCode\\PlugInNET\\bin\\Debug\\PlugInNET.exe \"$message\"" ) or die "Cannot run free, $ERRNO";
while (<$fh>)
{
Purple::Debug::info("PlugIn", "Read: Line " . $_ . "\n");
$query = $query . $_ . "\n";
}
close $fh;
Purple::Debug::info("PlugIn", "Query: " . $query . "\n");
if( $query eq "" )
{
$im->send( "I'm sorry, my brain doesn't seem to be functioning at the moment" );
} else {
#msgs = split(/-----------\n/, $query);
foreach( #msgs )
{
Purple::Debug::info("PlugIn", "Result Msg: \"" . $_ . "\"\n");
$im->send( "<BODY>" . $_ . "</BODY>" );
}
}
}
}
The plan was to fix up the paths once I had it working properly
Please consider using file handles instead of backticks to capture stdout from another source. You'll be able collect errors.
#!/usr/bin/perl
use strict;
use warnings;
use English;
# No taint protection in this example
open ( my $fh, '-|', '/usr/bin/free' ) or die "Cannot run free, $ERRNO";
while (<$fh>)
{
print;
}
close $fh;
Perl telnet does not wait for the end of the previous command. In the file infile.log I see the continuation of the command my # config = $ telnet-> cmd ("sh run");, but the script is already beginning to run the command print ("Format configuration", $ _, "\ n");. As a result, I get an empty array #config.
foreach (#linksys_sps){
print ("Connecting to ",$_,"\n");
my $telnet = new Net::Telnet ( Timeout=>10,Errmode=>'return',Input_Log => "infile.log");
$telnet->open($_);
if ($telnet->errmsg){
print "Can't connect to " . $_ . " Error: " . $telnet->errmsg . "\n";
} else {
$telnet->max_buffer_length(5 * 1024 * 1024);
$telnet->waitfor('/User Name:$/i');
$telnet->print('admin');
$telnet->waitfor('/Password:$/i');
$telnet->print('password');
print ("Set Terminal Variable ",$_,"\n");
$telnet->cmd("terminal datadump");
print ("Create file ",$_,"\n");
my $file = sprintf($folder."/".$_);
print ("Create file ",$file,"\n");
system "touch $file";
system "chown nobody $file";
print ("Read configuration ",$_,"\n");
my #config = $telnet->cmd("sh run");
print ("Write configuration ",$_," to file ",$file,"\n");
open my $fh, "> $file" or die "Can't open $file : $!";
foreach (#config) {
print $fh "$_"; # Print each entry in our array to the file
}
close $fh;
print ("Set Terminal Variable ",$_,"\n");
$telnet->cmd("no terminal datadump");
$telnet->cmd("exit");
}
}
How to fix it?
From the docs:
The methods login() and cmd() use the prompt setting in the object to
determine when a login or remote command is complete. Those methods
will fail with a time-out if you don't set the prompt correctly.
In otherwords, you need to add the prompt parameter when you create the $telnet object:
my $switch_name = 's-east';
my $telnet = new Net::Telnet (
Timeout=>10,
Errmode=>'return',
Input_Log => "infile.log",
Prompt => '/\Q$switch_name\E#\s?$/'
);
(It looks for the shell prompt to know that a command is completed).
Due to the instability of the cmd(), I switched to using waitfor(String => $string).
New script below:
foreach (#linksys_sps){
my $file = sprintf($folder."/".$_);
print ("Create file ",$file,"\n");
system "touch $file";
my $string = sprintf($_."#");
print ("Prompt String is ",$string,"\n");
print ("Connecting to ",$_,"\n");
my $telnet = new Net::Telnet (
Timeout=>20,
Errmode=>'return',
Input_Log => "infile.log"
);
$telnet->open($_);
if ($telnet->errmsg){
print "Can't connect to " . $_ . " Error: " . $telnet->errmsg . "\n";
} else {
$telnet->waitfor('/User Name:$/i');
$telnet->print('admin');
$telnet->waitfor('/Password:$/i');
$telnet->print('password');
$telnet->waitfor(String => $string );
print ("Set Backup Terminal Variable ",$_,"\n");
$telnet->print("terminal datadump");
$telnet->waitfor(String => $string );
print ("Read configuration ",$_,"\n");
$telnet->print('sh run');
my #config = $telnet->waitfor(String => $string );
print ("Write configuration ",$_," to file ",$file,"\n");
open my $fh, '>', $file or die "Cannot open file: $!";
foreach my $config (#config) {
print $fh "$config\n";
}
close $fh;
print ("Set Default Terminal Variable ",$_,"\n");
$telnet->print('no terminal datadump');
$telnet->waitfor(String => $string );
$telnet->print('exit');
}
}
I am trying to create a remote-login script with perl. I am currently getting input data using
$var = <$client>;
chomp $var;
However, I am trying to have the client input a password and I want to hide the password in the linux fashion with the client by not echoing what is typed. Is there any way I can do this?
EDIT:
$serv = IO::Socket::INET->new (
Proto => 'tcp',
LocalPort => $port,
Listen => 10,
Reuse => 1)
|| die "Can't create server: $!";
while ($client = $serv->accept()) {
eval {
$client->autoflush(1); # Always remember to flush!
$who = $client->peerhost;
print STDERR "Connection from $who\n";
print $client hostname . " login: ";
$usr = <$client>;
chomp $usr;
$usr =~ s/\W//g;
print STDERR "User $usr\n";
die unless (length $usr < 20 && length $usr > 1);
print $client "Encrypted Password: ";
$pass = <$client>;
chomp $pass;
die unless (length $pass < 20 && length $pass > 1);
print STDERR "$who: Pass $pass\n";
};
close $client;
}
This is local console echo, nothing to do with your socket.
There are many ways to turn off console echo using Perl, but my favourite is IO::Termios (perhaps I'm biased because I wrote it ;) )
use IO::Termios;
my $stdin = IO::Termios->new(\*STDIN);
$stdin->setflag_echo(0);
I'm trying to write a script that will get event log information off of a remote windows machine using the win32::ole module and a WMI query. I can ping the machine but no matter what my WMI connection always fails using the ConnectServer() method. I'm pretty sure its not a firewall related problem. Here is my code:
use Win32::OLE qw(in);
use Net::Ping;
use constant wbemFlagReturnImmediately => 0x10;
use constant wbemFlagForwardOnly => 0x20;
my $computer = "10.10.10.15";
my $user = "Administrator";
my $pwd = "pass";
$p = Net::Ping->new();
print "$computer is alive.\n" if $p->ping($host);
$p->close();
my $locatorObj =Win32::OLE->new("WbemScripting.SWbemLocator") or die "ERROR CREATING OBJ";
$locatorObj->{Security_}->{impersonationlevel} = 3;
my $objWMIService = $locatorObj->ConnectServer($computer, "root\civm2", $user, $pwd) or die "WMI connection failed.\n";
my $colItems = $objWMIService->ExecQuery("SELECT * FROM Win32_NTLogEvent", "WQL",
wbemFlagReturnImmediately | wbemFlagForwardOnly);
foreach my $objItem (in $colItems) {
print "Category: $objItem->{Category}\n";
print "CategoryString: $objItem->{CategoryString}\n";
print "ComputerName: $objItem->{ComputerName}\n";
print "Data: " . join(",", (in $objItem->{Data})) . "\n";
print "EventCode: $objItem->{EventCode}\n";
print "EventIdentifier: $objItem->{EventIdentifier}\n";
print "EventType: $objItem->{EventType}\n";
print "InsertionStrings: " . join(",", (in $objItem->{InsertionStrings})) . "\n";
print "Logfile: $objItem->{Logfile}\n";
print "Message: $objItem->{Message}\n";
print "RecordNumber: $objItem->{RecordNumber}\n";
print "SourceName: $objItem->{SourceName}\n";
print "TimeGenerated: $objItem->{TimeGenerated}\n";
print "TimeWritten: $objItem->{TimeWritten}\n";
print "Type: $objItem->{Type}\n";
print "User: $objItem->{User}\n";
print "\n";
}
Any ideas why my attempt to connect always fails? Thanks :)
The ConnectServer call has a couple of potential issues:
I believe it needs two back slashes.
And It has a typo: civm2 -> cimv2
And it might reveal more information by adding a call to retrieve the error information:
my $objWMIService = $locatorObj->ConnectServer($computer, "root\\cimv2", $user, $pwd)
or die "WMI connection failed.\n", Win32::OLE->LastError;
I am having issues getting the -e and -d file test operators to work reliably.
Listings 1 and 2 are in the same directory, on an NTFS Windows XP SP 3 system. However, Listing 2 insists that a directory exists(it doesn't), and Listing 1 gets it right. Listing 2 is part of the main program.
Also, interesting, my logger routine is refusing to create/write to a file, even when I trap open and print with or croak statements. I suspect the two are connected. I've included that as Listing 3.
My opinion is Perl's global variables are getting unset/set somewhere and I managed to fry them(although I've tried to be very careful).
Thanks!
Listing 1:
use strict;
use warnings;
my $dir = "somedir2";
my $result= (-e $dir);
if( ! (-e $dir))
{
print "$dir doesn't exist\n";
}
else
{
print "$dir exists\n";
}
#print "$result\n";
if(! (-d $dir))
{
print "$dir isn't a dir!\n";
}
else
{
print "$dir is a dir\n";
}
Listing 2:
#Does the output directory not exist?
open_logger("logfile.txt");
logger("initializing logfile.");
logger("Checking $outputdir for existence...");
if( ! ( -e $outputdir))
{
logger("$outputdir doesn't exist...creating");
#if so, then create it
if( ! mkdir($outputdir))
{
$result = "Could not create $outputdir: $!";
logger("Could not create $outputdir: $!");
return ($success, $result);
}
logger("Created $outputdir");
}
else
{
logger("$outputdir exists...");
}
#is the directory not a directory? then die - something's off.
if( ! ( -d $outputdir))
{
$result = "Failure: output directory $outputdir not a directory!\n";
logger("$outputdir isn't a directory");
return ($success, $result);
}
Listing 3:
my $fh_logger_package;
sub open_logger
{
my $filename = shift;
open(FILE, ">$filename") or croak("Could not init logfile: $!");
$fh_logger_package = \*FILE;
print STDERR "Opened $filename\n";
if( ! (-e $filename))
{
croak("Crikey! Did not create file!");
}
}
sub logger
{
my $message = shift;
print STDERR $message . "\n";
print $fh_logger_package $message . "\n" or croak("Could not print to filehandle...");
}
-e and -d aren't affected by any variables, but if you are not specifying the full path, then they check relative to the current working directory. It is possible that the code is either being started from a different location or changes the directory it is in itself.