| Chess | Tools | Data | Blog | Poetry | Why? | Wiki | Admin | Logout |

Download


#!/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