I was looking to do a bulk of work on a systematic one by one server way, that kind that you performed when a new user is required, root password change, massive reporting. I found a couple of common keys between Unix and Linux, like boot uses "ssh" to establish connections, "uname", "date" and "sudo" commands and the word "lastlogin" after you are in.
Like the versions and vendors changes between servers, I needed to define a scope of servers to run a set of commands.
It is the glue of the whole system.
CPAN Libraries
It needs CPAN Perl libraries (IO::Pty, Expect, Net::SSH::Expect) located in "lib" directory in order to work.
Arguments
It also requires two arguments, the first one will be a relative path to a simple text file containing a line by line list of servers. The second arguments also will be a relative path to a simple text file but containing a line by line list of commands to execute on the remote server. Both files have its own format that must be respected.
Options
Nowadays there are two options that you can adjust.
Global timeout used to wait for responses. Increase on slow connections.
my $TimeOut = 15;
Debug flag specially util to show login output when there is connection problem.
my $Debug = 0;
This file is the list of the servers, they will be enumerated one by line without blank lines. Each server will contain a valid user and password with the ability to get "sudo su -". Server IP, user and password will be separated by semicolon ":" on a single line. It is not mandatory to use IP address but it is a good practice to obtain the Hostname by "uname -a" and keep a trust relation IP-Hostname.
For each server it will try to establish a ssh connection using the user by key or interactive with the password given. Then it will try to get id 0 by "sudo su -" using the password. Once there as root, it will run and interact with each one of the command list saving the output.
A good server list would look like (NO BLANK LINES):
10.10.1.2:user:pass
10.10.1.3:foo:bar
10.10.1.4:root:password1
This file is the list of commands that will be executed on each one of the server list given.
There is one character reserved for internal use (MUST NOT BE USED):
' Single "'" is reserved to enclose the whole line as key for the XML report.
And two combinations for advance use (COULD BE USED WITH CAUTION):
COMMAND___QUESTION__ANSWER___QUESTION__ANSWER
___ Three "_" is reserved as level 1 separator for expect commands.
__ Two "_" is reserved as level 2 separator for expect commands.
A good command list would look like (NO BLANK LINES OR '):
uname -a
date
useradd -d /export/home/user1 -m -g admin -c "User Admin" -s /bin/bash user1
passwd user1___ssword:__temp123___ssword:__temp123
The script will produce a XML report by the default output. A good practice is to leave it reporting to a file and in another screen to leave a "tail -f" showing the process. The file is ready to be parsed by XML::Simple. Please note that sometimes there is garbage on the command output that is not UTF-8, this brings issues when the XML is trying to be loaded to memory by the object. The XML structure works as container for each server and its commands output. The best practice is to load this report to a SQL database, once there all of them could be parsed once and again on demand, keeping a server history.
Header
XML Standard tag.
<?xml version="1.0" encoding="UTF-8" ?>
Starts whole ShaGGyBot report.
<ShaGGyBot server_list="SERVERS" cmd_list="COMMANDS" date="DATE">
Server
Starts specified server report.
<server>
Login status could be "KEY" if logged using rsa/dsa key, "PASS" if logged using interactive password or "ERROR" what else.
<login id="KEY">
Show the IP and the user used.
<![CDATA[$IP $USER]]>
</login>
Sudo status could be "OK" if id 0 was checked fine with id command or "ERROR" what else.
<sudo id="OK">
Show the IP and the user used.
<![CDATA[$IP $USER]]>
</sudo>
Each command will have its own container where the id is the command line that we used on the list (that is why we can't use ' single quotes on that file).
<cmd id='$COMMAND'>
The output given by the command when was executed is saved here. Sometime it has garbage not utf8 that affects XML::Simple.
<![CDATA[$OUTPUT]]>
</cmd>
End specified server.
</server>
Footer
Ends whole ShaGGyBot report.
</ShaGGyBot>
To find how much RAM you have Solaris 10 machine you need the command "prtconf | head".
On a Linux machine you use "cat /proc/mem" instead.
Here you have two different scopes each one with it's set of commands, so you will define this situation by this way:
SCOPE: SERVER.Sun10
10.10.8.2:foo:bar
10.10.8.3:foo:bar
10.10.8.4:foo:bar
SET: CMD.ram.Sun10
uname -a
date
prtconf | head
./ShaGGyBot.pl SERVER.Sun10 CMD.ram.Sun10 > REPORT.ram.Sun10.XML
SCOPE: SERVER.Linux
10.10.9.2:foo:bar
10.10.9.3:foo:bar
10.10.9.4:foo:bar
SET: CMD.ram.Linux
uname -a
date
cat /proc/mem
./ShaGGyBot.pl SERVER.Linux CMD.ram.Linux > REPORT.ram.Linux.XML
#!/usr/bin/perl -w
#######
# ShaGGy Bot (0_0)
# The system administrator best friend.
# It will do what you want from a server to a target of servers and let you know the results on XML portable format file.
#
# GNU/GPLv3 - Andres Basile <basile@gmail.com>
#
# Contribs:
# Javier Herlein
# Sergio Puente
# Leonardo Giaccone
#
# Usage:
# ./ShaGGyBot.pl SERVER.LIST CMD.LIST > REPORT.XML
#
# Through this file you will find explanation for each step.
# I strongly recommend you to read it before use it.
# This script could damage one or more servers if it is not used properly.
# I will not take any responsability for how you use this tool.
#
#######
#######
# Settings
# On slow networks increase timeout
my $TimeOut = 5;
my $Debug = 0;
#
#######
#######
# External CPAN libreries
# IO::Pty, Expect, Net::SSH::Expect
# Download, extract, compile and copy under "lib" directory where the script is located.
# This work have to be repeated if there is a change on the architecture.
#
# lib/Expect.pod
# lib/Expect.pm
# lib/IO/Tty.pm
# lib/IO/Pty.pm
# lib/IO/Tty
# lib/IO/Tty/Constant.pm
# lib/auto/IO
# lib/auto/IO/Tty
# lib/auto/IO/Tty/Tty.bs
# lib/auto/IO/Tty/Tty.so
# lib/auto/Expect
# lib/auto/Net
# lib/auto/Net/SSH
# lib/auto/Net/SSH/Expect
# lib/Net/SSH
# lib/Net/SSH/Expect.pod
# lib/Net/SSH/Expect.pm
#
use lib "lib";
use Net::SSH::Expect;
#
# HARDCODED!!!
# Change "croak" for "warn" at lib/Net/SSH/Expect.pm line 585
# warn (SSH_CONNECTION_ABORTED);
# This is going to avoid that script abort due to "Conection rejected"
#
#######
#######
# Server list
# This is a file containing a list of servers including user and password.
# Each line must contain a "server:user:password" at least
# or aslo could include a root password in order to use just "su -"
# "server:user:password:root_pass"
# It is a mandatory argument and must be the first one.
my $server_list;
( ($server_list = $ARGV[0]) and (-r $server_list) ) or die("SERVER LIST not found");
#
# Open the server list
open(SERVER_LIST, $server_list);
# Loading the list to an array.
my @server_list_array;
@server_list_array = (<SERVER_LIST>);
# Close server list
close(SERVER_LIST);
#
#######
#######
# Command list
# This is a file containing a list of commands to execute as root.
# It is a mandatory argument and must be the second one.
###
# WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!! WARNING!!!
#
# Each line must contain a command that will be executed as root so be careful!!
###
# You must begin the file with two mandatory commands in order to get a healthy report.
# uname -a
# date
#
# Commands lines could contain up to 2 questions and answers for command.
# Format:
# COMMAND___QUESTION__ANSWER___QUESTION__ANSWER
#
# COMMAND - A command to execute on the remote host
# ___ - Is used as primary separator
# QUESTION - Is a string to look for. IE: password
# __ - Is used as separator between question and answer.
# ANSWER - Is a string to send as response. IE: set42day (password to set for the user)
#
# ie: You want to get all users and groups, so the command list file will look:
#---
# uname -a
# date
# cat /etc/passwd
# cat /etc/group
#---
#
# ie: You want to set "bot" password for user "ShaGGy", so the command list file will look:
#---
# uname -a
# date
# passwd ShaGGy___assword:__bot___assword:__bot
#---
#
my $cmd_list;
( ($cmd_list = $ARGV[1]) and (-r $cmd_list) ) or die("CMD LIST not found");
#
#
open(CMD_LIST, $cmd_list);
my @cmd_list_array;
@cmd_list_array = (<CMD_LIST>);
close(CMD_LIST);
#
#######
#######
# Header
print "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
my $date = localtime;
print "<ShaGGyBot server_list=\"$server_list\" cmd_list=\"$cmd_list\" date=\"$date\">\n";
#
#######
#######
# Functions
##
# Connection with private/public key (rsa/dsa)
sub connect_key {
my ($host, $username, $password) = split(':',$_[0]);
# Creating the object.
my $ssh = Net::SSH::Expect->new (
host => $host,
user => $username,
raw_pty => 1,
timeout => $TimeOut,
);
# Login
$ssh->run_ssh();
my $login = $ssh->read_all();
#
# It will search for "(Last) login" that is a common message on *nix family.
if ( $login and $login =~ /last/i ) {
# If it is found, return the connection.
return $ssh;
}
# If not, return null.
else {
if ( $Debug ) { print "<debug id=\"KEY\">\n$login\n</debug>\n"; }
return undef;
}
}
#
##
# Connection with password
sub connect_pass {
my ($host, $username, $password) = split(':',$_[0]);
my $ssh = Net::SSH::Expect->new (
host => $host,
user => $username,
password=> $password,
raw_pty => 1,
timeout => $TimeOut,
);
#
my $login;
# Eval is used to trap exeptions and avoid script abort.
eval { $login = $ssh->login(); };
#
if ( $login and $login =~ /last/i ) {
return $ssh;
}
#
else {
if ( $Debug ) { print "<debug id=\"PASS\">\n$login\n</debug>\n"; }
return undef;
}
}
#
##
# Getting SUDO
sub get_sudo {
my $ssh = $_[0];
my $password = $_[1];
# Try to execute "sudo" from known locations
$ssh->send("/usr/local/bin/sudo su -") or $ssh->send("sudo su -");
# If it ask for password.
if ( $ssh->waitfor('assword') ) {
# Give it.
$ssh->send($password);
}
# Check if it was succesfull
sleep 5;
$ssh->send("id");
my $su = $ssh->read_all(10);
#
if ( $su =~ /root/i ) {
# And return the object
return $ssh;
}
# If not, return null.
else {
if ( $Debug ) { print "<debug id=\"SUDO\">\n$su\n</debug>\n"; }
return undef;
}
}
#
##
# Getting SU
sub get_su {
my $ssh = $_[0];
my $password = $_[1];
# Try to execute "sudo" from known locations
$ssh->send("su -");
# If it ask for password.
if ( $ssh->waitfor('assword') ) {
# Give it.
$ssh->send($password);
}
# Check if it was succesfull
sleep 5;
$ssh->send("id");
my $su = $ssh->read_all(5);
#
if ( $su =~ /root/i ) {
# And return the object
return $ssh;
}
# If not, return null.
else {
if ( $Debug ) { print "<debug id=\"SU\">\n$su\n</debug>\n"; }
return undef;
}
}
#
#######
#######
# Main function
#
# For each server on the server list.
foreach $server (@server_list_array) {
chomp($server);
my ($host, $username, $password, $root) = split(':',$server);
my $ssh;
#
# Separator
print "<server>\n";
# Try login with key.
if ( $ssh = &connect_key($server) ) {
print "<login id=\"KEY\">\n<![CDATA[$host $username]]>\n</login>\n";
}
# Try login with pass.
elsif ( $ssh = &connect_pass($server) ) {
print "<login id=\"PASS\">\n<![CDATA[$host $username]]>\n</login>\n";
}
#
# If no login on this server, end here and continue with next line.
else {
print "<login id=\"ERROR\">\n<![CDATA[$host $username]]>\n</login>\n";
print "</server>\n";
next;
}
#
#
# Once loged, set terminal.
$ssh->exec("stty raw -echo");
#
my $admin;
# Check if the user has su or sudo.
# If root pass is provided try su first
if (($root) && (&get_su($ssh, $root))) {
print "<su id=\"OK\">\n<![CDATA[$host $username]]>\n</su>\n";
$admin = 1;
}
elsif ( &get_sudo($ssh, $password) ) {
print "<sudo id=\"OK\">\n<![CDATA[$host $username]]>\n</sudo>\n";
$admin = 1;
}
else {
$admin = 0;
}
#
if ($admin) {
# Run commands
foreach my $cmd_line (@cmd_list_array) {
chomp($cmd_line);
my @cmds = split('___', $cmd_line);
# If it is a simple command
if ( @cmds == 1 ) {
my $cmd_out = $ssh->exec($cmds[0]);
print "<cmd id=\'$cmds[0]\'>\n<![CDATA[$cmd_out]]>\n</cmd>\n";
}
# Else if it is a command with 1 response
elsif ( @cmds == 2 ) {
$ssh->send($cmds[0]);
my ($ques, $answ) = split('__', $cmds[1]);
if ( $ssh->waitfor($ques) ) {
$ssh->send($answ);
}
my $cmd_out = $ssh->read_all();
print "<cmd id=\"$cmds[0]\">\n<![CDATA[$cmd_out]]>\n</cmd>\n";
}
# Else if it is a command with 2 responses
elsif ( @cmds == 3 ) {
$ssh->send($cmds[0]);
my ($ques, $answ) = split('__', $cmds[1]);
if ( $ssh->waitfor($ques) ) {
$ssh->send($answ);
}
my ($ques2, $answ2) = split('__', $cmds[2]);
if ( $ssh->waitfor($ques2) ) {
$ssh->send($answ2);
}
my $cmd_out = $ssh->read_all();
print "<cmd id=\"$cmds[0]\">\n<![CDATA[$cmd_out]]>\n</cmd>\n";
}
# If not, it is unknown
else {
print "<cmd id=\"ERROR\">\n<![CDATA[$cmd_line]]>\n</cmd>\n";
}
}
#
print "</server>\n";
$ssh->close();
}
#
# If no sudo premissions, continue with the server on next line.
else {
print "<sudo id=\"ERROR\">\n<![CDATA[$host $username]]>\n</sudo>\n";
$ssh->close();
print "</server>\n";
next;
}
}
#
#######
#######
# Footer
print "</ShaGGyBot>\n";
#