#!/usr/bin/perl -w
# $Revision: 1.3 $
# $Date: 2006-05-10 18:22:36 $
# Luis Mondesi < Luis.Mondesi@americanhm.com >
#
# DESCRIPTION: installs cfengine on remote host
# USAGE: push-cfagent HOST
# LICENSE: GPL
=pod
=head1 NAME
push-cfagent - script to push cfengine to a remote server
=head1 SYNOPSIS
B<push-cfagent> [-v,--version]
[-D,--debug]
[-h,--help]
[-k,--key KEY]
[-q,--quiet]
[-s,--send-key]
[-u,--username USERNAME]
[-U,--update-agent]
<HOST|FILE> [HOST ... [FILE ...]]
=head1 DESCRIPTION
This script pushes cfengine to a remote server. The script environment is setup like:
mkdir cfengine-masterfiles
mkdir cfengine-masterfiles/inputs # all your pristine .cf and .conf files are here: update.conf, cfservd.conf, cfagent.conf, ...
...
mkdir cfengine-masterfiles/scripts
mkdir -p cfengine-masterfiles/RPMS/{FC1,FC3,FC4,RH9}
cp push-cfagent cfengine-masterfiles/scripts/
...
running ./cfengine/scripts/push-cfagent HOST will look in cfengine/RPMS for the right RPM to install on the remote host (if the remote host is found to be running RedHat, Fedora, or any other RPM-based system -- Linux Standard Base files are used /etc/lsb-release). In the event that the RPM is missing, the script will attempt to use yum/apt-get to install the package.
For Debian and .deb based systems the script will simply call apt-get to install cfengine2.
After the installation, the files from "inputs" will be copied over (updates.conf initially). And a first run of `/usr/sbin/cfagent -q` will be made. That will get cfengine running on the remote system and copy over all the files according to the rules defined in your update.conf and cfagent.conf.
=head1 OPTIONS
=over 8
=item -v,--version
Prints version and exits
=item -D,--debug
Enables debug mode
=item -h,--help
Prints this help and exits
=item -k,--key
Use identity key KEY instead of default ~/.ssh/id_rsa
=item -q,--quiet
Do not print informational strings
=item -s,--send-key
Pushes local ~/.ssh/id_rsa.pub key to remote host ~/.ssh/authorized_keys file and exits
=item -u,--username USER
Connect to remote server using USER instead of the effective username
=item -U,--update-agent
Force the installation of cfengine even if the remote host has it installed already
=item HOST or FILE
Hostname or list of host names or ips from FILE to which cfengine will be pushed (via ssh)
=back
=cut
use strict;
$|++;
use sigtrap qw(handler _exit_safe normal-signals error-signals);
my $revision = "1.0"; # version
$ENV{PATH} .= ":/usr/sbin:/usr/local/sbin";
# standard Perl modules
use Getopt::Long;
Getopt::Long::Configure('bundling');
use POSIX; # cwd() ... man POSIX
use File::Spec::Functions; # abs2rel() and other dir/filename specific
use File::Copy;
use File::Find; # find();
use File::Basename; # basename() && dirname()
use FileHandle; # for progressbar
# globals:
my %hosts = ();
# Args:
my $PVERSION=0;
my $HELP=0;
my $DEBUG=0;
my $VERBOSE=0;
my $PKEY=0;
my $PG_KEY=""; # when empty adds all for us... $ENV{'HOME'}."/.ssh/id_rsa";
my $_ssh_agent = 0; # should we kill ssh-agent when done?
my $_ssh_id = 0; # should we remove id when done?
my $ssh_port = 22;
my $HOST=undef;
my $USERNAME=undef;
my $FORCE_INSTALL_AGENT=0;
my $QUIET=0;
my $MASTERFILES=catdir(dirname($0),"..");
# get options
GetOptions(
# flags
'v|version' => \$PVERSION,
'h|help' => \$HELP,
'D|debug' => sub { $DEBUG++; $VERBOSE++},
'V|verbose' => \$VERBOSE,
's|send-key' => \$PKEY,
'U|update-agent' => \$FORCE_INSTALL_AGENT,
'q|quiet' => \$QUIET,
# strings
'k|key=s' => \$PG_KEY,
'u|username=s' => \$USERNAME,
'm|masterfiles=s' => \$MASTERFILES,
) and $HOST = shift;
if ( $HELP or not defined($HOST) ) {
use Pod::Usage;
pod2usage(1);
exit 0;
}
if ( $PVERSION ) { print STDOUT ($revision); exit 0; }
my $RED="\033[1;31m";
my $GREEN="\033[0;32m";
my $NORM="\033[0;39m";
chdir($MASTERFILES); # make sure we are in the masterfiles directory
if ( defined($USERNAME) )
{
$USERNAME .= "\@" if ( $USERNAME !~ /^\s*$/);
} else {
warn("You are not running this as root, make sure you use --username='root' to use the root account on the remote system.
")
if ( $> != 0);
$USERNAME=""; # avoids warning
}
if ( -r $HOST )
{
warn("Reading hosts from file $HOST
");
open(FILE,"<",$HOST)
or warn("Failed to read file $HOST. $!
");
while (<FILE>)
{
my @hosts = split(/\s+|
/,$_);
foreach my $host (@hosts)
{
push(@ARGV,$host);# if (is_alive($host)); # we check alive later
}
}
close(FILE);
}
foreach my $HOST ($HOST,@ARGV)
{
print STDOUT ("Pushing cfagent to $HOST
") if (!$QUIET and !-r $HOST);
if ( is_alive($HOST) )
{
# TODO find out a way on how to connect via ssh once and keep the session open
# 1. what OS is this host running?
my @release = ("/etc/redhat-release","/etc/lsb-release","/etc/debian_version");
my $OS = undef;
# attempts to use ssh-agent to load our keys
# setup SSH
if ( !exists($ENV{'SSH_AUTH_SOCK'}) )
{
_setup_ssh_agent();
}
if ( !-S $ENV{'SSH_AUTH_SOCK'} )
{
_setup_ssh_agent();
}
# generate RSA keys if none found. I don't care about people who use DSA :-P
system("ssh-keygen -t rsa -b 1024")
if ( !-f "$ENV{'HOME'}/.ssh/id_rsa.pub" and !-f $PG_KEY );
$_ssh_id = system("ssh-add -l > /dev/null");
if ( $_ssh_id != 0 )
{
system("ssh-add $PG_KEY"); # if $PG_KEY is blank ssh-add adds all private keys
if ( $? != 0 )
{
warn ("Failed to authenticate. ssh-gent is not running? There is no valid private key? Hint: create a key with \`ssh-keygen -t rsa -b 1024\`. And then pass the key to us with: $0 --key $ENV{HOME}/.ssh/id_rsa
");
if ( prompt("Do you want to continue? You will be prompted for each password needed [y/N] ") !~ /^y/i )
{
_exit_safe(0);
}
}
$_ssh_id = 1; # we need to know that this agent should be killed later
}
# end setup SSH
# send our public key over to the remove host so that we can login with no password
if ( $PKEY )
{
system("cat $ENV{'HOME'}/.ssh/id_rsa.pub | ssh ".$USERNAME.$HOST." 'mkdir .ssh 2> /dev/null && chmod 0700 .ssh; cat - >> .ssh/authorized_keys; chmod 0644 .ssh/authorized_keys'");
print_error ("failed to send key to $HOST") and _exit_safe($?) if ( $? != 0 );
}
foreach my $file (@release)
{
# TODO use lsb_release to know the system distro
my $output = send_cmd($HOST,"cat $file");
next if ( ! defined($output) or $output =~ /^\s*$|.*No such file.*/mig );
if ( $output =~ /Fedora Core release 1/mig )
{
$OS="FC1";
last;
} elsif ( $output =~ /Fedora Core release 2/mig ) {
$OS="FC2";
last;
} elsif ( $output =~ /Fedora Core release 3/mig ) {
$OS="FC3";
last;
} elsif ( $output =~ /Fedora Core release 4/mig ) {
$OS="FC4";
last;
} elsif ( $output =~ /Fedora Core release 5/mig ) {
$OS="FC5";
last;
} elsif ( $output =~ /Red Hat Linux release 9/mig ) {
$OS="RH9";
last;
} elsif ( $file eq "/etc/debian_version" and $output =~ /3\./mig ) {
$OS="debian";
last;
} else {
print_error ("no such file $HOST:$file ...") if ($VERBOSE or $DEBUG);
}
last if (defined($OS));
}
$OS = "unknown" if (not defined($OS));
print_info ("$HOST is $OS");
# 2. install cfengine
my $INSTALLED = 0;
my $remote_tmp_dir="/tmp/cfengine-install-".time();
if ( defined($OS) and $OS ne "unknown" )
{
if ( $OS ne "debian" )
{
my $RPM_DIR = "RPMS/$OS/";
DIR:
$RPM_DIR = prompt("Please enter local path to directory holding RPMs to install on $HOST (type none to use apt-get|yum): [RPMS/FC1/] ") if ( !-d $RPM_DIR );
goto DIR if ( $RPM_DIR !~ /none/g and ! -d $RPM_DIR );
print_error ($RPM_DIR) if ($DEBUG);
if ( ! $FORCE_INSTALL_AGENT )
{
send_cmd($HOST,"test -x /usr/sbin/cfagent");
$INSTALLED = 1 if ( $? == 0 );
send_cmd($HOST,"rpm -q cfengine > /dev/null");
$INSTALLED = 1 if ( $? == 0 );
}
if ( ! $INSTALLED and -d $RPM_DIR )
{
# push RPMs to host
send_cmd($HOST,"mkdir $remote_tmp_dir");
if ( $? != 0 )
{
print_error ("Couldn't create remote directory $remote_tmp_dir on $HOST");
_exit_safe(1);
}
system("scp $RPM_DIR/*.rpm ${USERNAME}${HOST}:$remote_tmp_dir/");
die ("Could not copy RPMs to $HOST
") if ($? != 0);
send_cmd($HOST,"rpm -U $remote_tmp_dir/*.rpm");
if ( $? == 0 )
{
$INSTALLED = 1 ;
} else {
# try apt-get or yum
send_cmd($HOST,"apt-get -y install cfengine || yum install cfengine");
$INSTALLED = 1 if ( $? == 0 );
}
}
} elsif ($OS eq "debian") {
send_cmd($HOST,"test -x /usr/sbin/cfagent");
$INSTALLED = 1 if ( $? == 0 );
if ( !$INSTALLED )
{
# say yes to all prompts
send_cmd($HOST,"apt-get -y install cfengine2");
$INSTALLED = 1 if ( $? == 0 );
}
}
}
# 3. configure cfengine and run it for the first time
if ( $INSTALLED )
{
my $update_conf = "inputs/update.conf";
if ( ! -f $update_conf )
{
UPDATE:
$update_conf = prompt("Please enter local path to update.conf file: [inputs/update.conf] ");
goto UPDATE if ( ! -f $update_conf );
# TODO use cfagent -pf to test the syntax of file before uploading?
}
# copy update.conf
my $inputs_dir = ($OS ne "debian") ? "/var/cfengine/inputs":"/etc/cfengine";
send_cmd($HOST,"mkdir -p $inputs_dir");
# all this hackish stuff is to avoid having to use root to run this script:
system("scp $update_conf ${USERNAME}${HOST}:$inputs_dir");
# run cfagent for the first time after making sure the symlinks are removed from
# /var/cfengine/bin/cfagent
send_cmd($HOST,"/bin/rm -f /var/cfengine/bin/cf*");
send_cmd($HOST,"/usr/sbin/cfagent -q");
# cleanup
send_cmd($HOST,"/bin/rm -fr $remote_tmp_dir")
if (!$DEBUG);
} else {
print_error ("cfengine could not be installed on $HOST");
}
} else {
print_error ("Could not connect to host $HOST")
if ( !-r $HOST );
}
}
# helper functions #
# return filehandle for stdout/stderr of command send to $host over ssh
sub send_cmd
{
my $host = shift;
my $cmd = shift;
return undef if (not defined $host or not defined $cmd);
my $_cmd = "ssh ${USERNAME}${host} $cmd 2>&1";
print STDOUT ("$_cmd
") if ($VERBOSE);
my $str = qx/$_cmd/;
if ( $? != 0 )
{
print_error ("Failed to execute $cmd on $host. $str");
return undef;
}
return $str;
}
# @desc checks whether a given host is alive by pinging it.
# pinging to a given host will be cached/saved for us so that we don't
# have to test for a given host more than once.
# @arg 1 $host string or ip representing a given host
# @return 1 if true 0 if false
sub is_alive
{
my $host = shift;
return undef if ( not defined ( $host ) );
$hosts{$host}{'alive'} = 0 if ( not exists ($hosts{$host}{'alive'}) );
my $ping_args = ( qx/ping -V/ =~ /iputils/ ) ? " -w 4 " : "" ;
if ( $hosts{$host}{'alive'} == 0 )
{
my $tmp_str = undef;
$tmp_str = qx/ping $ping_args -c 1 $host/ if ( $hosts{$host}{'alive'} < 1 );
# 0 when good
# 256 when not good
debug ("*** pinging $host returned $?");
# return the opposite of ping's return output
$hosts{$host}{'alive'} = ( $? ) ? 0:1;
if ( $hosts{$host}{'alive'} > 0 )
{
# test to see if host is listening on SSH port
use IO::Socket;
my $socket = IO::Socket::INET->new(
PeerAddr=>$host,
PeerPort=>$ssh_port,
Proto=>"tcp",
Type=>SOCK_STREAM);
if ( ! $socket )
{
debug("*** couldn't connect to remove host ssh port $ssh_port. $@
");
$hosts{$host}{'alive'}=0;
} else {
debug ("*** ssh to $host on port $ssh_port is possible");
close($socket);
}
}
} else {
debug ("*** uh? We should never reach this... This means that we previously check for this host already. All checks were skipped.");
}
debug("is_alive returning ".$hosts{$host}{'alive'}." for $host");
return $hosts{$host}{'alive'};
}
# @desc checks whether a given host is alive by pinging it.
# pinging to a given host will be cached/saved for us so that we don't
# have to test for a given host more than once.
# @arg 1 $host string or ip representing a given host
# @return 1 if true 0 if false
# sub is_alive
# {
# my $host = shift;
# return undef if ( not defined ( $host ) );
# $hosts{$host}{'alive'} = 0 if ( not exists ($hosts{$host}{'alive'}) );
# if ( $hosts{$host}{'alive'} == 0 ) {
# my $tmp_str = undef;
# $tmp_str = qx/ping -c 1 $host/ if ( $hosts{$host}{'alive'} < 1 );
# # 0 when good
# # 256 when not good
# #print STDERR ("*** pinging $host returned $?
");
# # return the opposite of ping's return output
# my $ret = ( $? ) ? 0:1;
# $hosts{$host}{'alive'} = $ret; # save for future reference
# } else {
# print_error ("*** uh? We should never reach this...
");
# }
# #print STDERR ("is_alive returning ".$hosts{$host}{'alive'}." for $host
");
# return $hosts{$host}{'alive'};
# }
sub prompt
{
#@param 0 string := question to prompt
#returns answer
print STDOUT "@_";
my $rep= <STDIN>;
chomp($rep);
return $rep;
}
sub _setup_ssh_agent
{
$ENV{'SSH_AUTH_SOCK'}="";
my $_ssh_agent_env = qx/ssh-agent -s/;
print_error ($_ssh_agent_env,"
") if ($DEBUG);
$_ssh_agent_env =~ m/SSH_AUTH_SOCK=(.*); /gmi;
$ENV{'SSH_AUTH_SOCK'} = $1;
$_ssh_agent_env =~ m/SSH_AGENT_PID=(.*); /gmi;
$ENV{'SSH_AGENT_PID'} = $1;
if ( -S $ENV{'SSH_AUTH_SOCK'} )
{
$_ssh_agent = 1; # we should kill the agent when done
} else {
warn("Could not launch our ssh-agent
");
}
}
sub _exit_safe
{
my $status = shift;
$status = 0 if (not defined($status));
if ( $_ssh_id == 1 )
{
print ("Removing ssh identities from ssh-agent
");
system("ssh-add -D"); # delete identities
}
if ( $_ssh_agent == 1 )
{
print ("Killing our ssh-agent process
");
kill(15,$ENV{'SSH_AGENT_PID'});
}
exit $status;
}
sub print_error
{
print STDERR ($RED."@_".$NORM."
");
}
sub print_info
{
print STDOUT ($GREEN."@_".$NORM."
");
}
# @desc prints colored messages
sub debug
{
my $msg = "@_";
print STDERR ("$RED $msg $NORM
") if ( $DEBUG );
}
__END__
=head1 AUTHORS
Luis Mondesi <luis.mondesi@americanhm.com>
=cut
Advertisement