Friday, April 5, 2013

Executing commands on multiple Linux machines


Here is a small and convenient tool to execute same command over a set of Linux machines.
To use the tool we need to configure the machines for password-less SSH, also need Perl module Net::OpenSSH. We have to create a file $HOME/.allhosts and put the IP addresses or host names in this file in separate lines. 

Configuring password-less access to all the machines you have

generate punblic and private keys
$ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa

copy the public and private keys to all the machines you want to passwordless access:
scp ~/.ssh/id_rsa.pub userid@machine-ip:~/.ssh/id_rsa.pub
scp ~/.ssh/id_rsa userid@machine-ip:~/.ssh/id_rsa

Add the public key to the authorized keys files on every machines:
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

Now you will be able to ssh from any of these machines to any of these machines without a password

Install perl Net::OpenSSH module as shown below:
$sudo yum install perl-CPAN
$sudo perl -MCPAN -e "install Net::OpenSSH" 

Examples
to kill all the java processes running in the machines listed in yout $HOME/.allhosts, issue the below command:
$ perl multihostcmd.pl  "ps -ef  | grep java  | grep -v grep | awk ' { print \$2 }'  | xargs kill -9"
Of course you will need permission to kill those processs

To check all the users logged to these machines, issue the below command
$ perl multihostcmd.pl  who

Below is the script (you may also access it in github ):

#!/usr/bin/perl                                
############################################################################
# Save this in a a file, say multihostcmd.pl                                            
############################################################################   

use threads (
    'yield', 
    'stack_size' => 64 * 4096,
    'exit'       => 'threads_only',
    'stringify'                    
);                                 
use Cwd qw(abs_path);              
use Net::OpenSSH;                  

#
# executeCmdRemote
# executes the command on a remote host 
# command execution happens over ssh    
#                                       

sub executeCmdRemote {
    my ( $host, $command, @others ) = @_;
    my $ssh =  Net::OpenSSH->new($host,  
         master_opts => [-o => "ConnectionAttempts=2", -o => "ConnectTimeout=5"] );
    if ($ssh->error) {                                                             
        return;                                                                    
    }                                                                              
    ($out, $err, @others) =  $ssh->capture2({timeout => 20}, $command);            
    $outdata = "FROM $host ******************************************\n";          
    if ($out ne "") {                                                              
        $outdata .= $out;                                                          
    }                                                                              
    if ($err ne "") {                                                              
        $outdata .= $err;                                                          
    }                                                                              
    $outdata .= "\nEND OF DATA FROM $host ************************************* \n\n";
    print $outdata;                                                                   
}                                                                                     

#
# readHostFile
# reads from the file $HOME/.allhosts where this file contain a list of hosts
# each line on this file denotes a host name or host ip                      
# The given command is executed on this host                                 
#                                                                            
sub readHostFile {                                                           
    my ($array, @others) = @_;                                               
    $hostfile = $ENV{'HOME'} . '/.allhosts';                                 
    if (! -e  $hostfile) {                                                   
        print $hostfile . ' doesn\'t exist';                                 
        return;                                                              
    }                                                                        
    if (! -f  $hostfile) {                                                   
        print $hostfile . ' is not a regular file';                          
        return;                                                              
    }                                                                        
    open FH, "< $hostfile"  || return;                                       
    @$array = <FH>;                                                          
    close FH;
}

sub main {
    if ( $#ARGV < 0 ) {
        $executing_script = abs_path($0);
        print "Usage: $executing_script command-to-execute\n";
        exit(1);
    }
    $cmdline = join( ' ', @ARGV );
    my @hostarry = ();
    readHostFile(\@hostarry);
    if ($#hostarry < 0) {
        print "Empty list for hosts";
        exit(1);
    }

    my $thr = undef;
    my @allthreads = ();
    foreach my $host (@hostarry) {
       $host =~ s/^\s+//;
       $host =~ s/\s+$//;
       if  ($host eq "") {
           next;
       }
       # Create a thread to execute the command on the remote host
       $thr = threads->create('executeCmdRemote', $host, $cmdline);
       push @allthreads, $thr;
    }
    foreach $thr (@allthreads){
        $thr->join();
    }
}

# Call main
main();