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

Download


#!/usr/bin/perl -w
# $Revision: 1.80 $
# Luis Mondesi < lemsx1@gmail.com >
#
# DESCRIPTION: A simple script to update my settings in $HOME @see --help. You can get the latest copy from: git clone http://git.kiskeyix.org/git/Applications.git
# USAGE: update-host [OPTIONS] [host1 host2 host3 ...]
#
# TODO:
#  - tests host file and print their new IP to STDOUT. hint slurp_hosts() and do host lookups on the names. Allow a --reconcile-hosts option so the new IP for the hostnames can be save to the file passed from the command line.
# LICENSE: GPL

use strict;
$|++;
use sigtrap qw(handler _exit_safe normal-signals error-signals);

use Getopt::Long;
Getopt::Long::Configure('bundling');
use File::Spec::Functions qw/ splitdir catdir catfile /
  ;    # abs2rel() and other dir/filename specific
use File::Basename qw/ basename /;

############## NO NEED TO MODIFY THESE #################

=pod

=head1 update-host

update-host - by Luis Mondesi L<<lemsx1@gmail.com>>

=head1 SYNOPSIS

B<update-host>  
[-D,--debug]
[-d,--delete-obsolete]
[--files <file1,file2,...>]
[--host <hostname>] 
[-h,--help]
[--hosts-file <path/to/file>]
[-k,--key]
[-l,--local]
[--local-path]
[-L,--links,--symlinks]
[-m,--master] 
[--print-files]
[--password PASSWORD]
[-r,--remove]
[-s,--send-key]
[-u,--username USER]
[-U,--usage]
[-V,--verbose]
[-v,--version]
[HOSTNAME1 [HOSTNAME2 ...]]

=head1 DESCRIPTION 

This script updates my user configurations (bash,vim,mutt,cvs) on this computer from a remote host, or distributes them from this host to others as defined in ~/.remote-hosts.

The files from the master host will be downloaded using a URL (wget). And all other downloads will be done via ssh (for hosts in ~/.remote-hosts by default).

The script tries to find if a given host is alive first by using ping.

The format of the remote-hosts file is the same as /etc/hosts.

=head1 EXAMPLES

B<update-host>  --master --links --local

updates localhost using files from the default local settings folder ($ENV{"HOME"}/Shared/Software/settings. See --local-path) and creates symlinks from Applications/share/* to the respective locations.

B<update-host>  --master --host URL

updates localhost using files from URL. If not --host is given, it assumes: lems.kiskeyix.org

B<update-host> --hosts-file ~/.hosts

updates all hosts found in ~/.hosts. If --hosts-file is omitted, it will upgrade all hosts found in ~/.remote-hosts

B<update-host> --hosts-file ~/.hosts --files "bashrc.tar.bz2,vimrc.tar.bz2"

updates only bashrc and vimrc files to all hosts found in ~/.hosts

B<update-host> --delete-obsolete HOST HOST2 ...

Removes obsolete files from hosts passed as arguments

B<update-host> --local --links HOST 

Creates tarball files for ~/Applications and sends these to the remote host HOST

=head1 BUGS

* --files cannot contain spaces and must be .tar.bz2 or .tar.gz.

* there is no way to pass $EXCEPTIONS from the command line.

=head1 OPTIONS

=cut

my $PVERSION          = 0;
my $HELP              = 0;
my $MASTER_HOST       = "http://lems.kiskeyix.org";
my $LOCAL_PATH        = "$ENV{'HOME'}/Shared/Software/settings";
my $REMOTE_HOSTS_FILE = "$ENV{'HOME'}/.remote-hosts";
my $PG_KEY = "";    # when empty adds all for us... $ENV{'HOME'}."/.ssh/id_rsa";

# path to temporary directory
my $TMP = "tmp";

# ary of systems to be excluded from updating
# separate by |. i.e.: 127.0.0.1|192.168.1.2|10.1.1.1
my $EXCLUDE = "172.16.1.250|as400";

# files we will be updating
my @FILES =
  ("Applications.tar.bz2", "bashrc.tar.bz2", "vimrc.tar.bz2", "muttrc.tar.bz2");

# links to $ENV{HOME} if DOLINKS is true
my @_CONFIG_FILES = (
                     "Applications/share/shell/bash_profile",
                     "Applications/share/shell/bashrc",
                     "Applications/share/shell/inputrc",
                     "Applications/share/shell/cvsrc",
                     "Applications/share/vim",
                     "Applications/share/mutt",
                    );

# files that should be tidied!
my @_OBSOLETE = (
                 "Application/scripts/cvsdelete.sh",
                 "Application/scripts/cvsadd.sh",
                 "Application/scripts/backup.sh",
                 "Applications/scripts/isoMount.sh",
                 "Applications/scripts/mount_image.sh",
                 "Applications/scripts/mvall.sh",
                 "Applications/scripts/myestablished.sh",
                 "Applications/scripts/mylisten.sh",
                 "Applications/scripts/myproc.sh",
                 "Applications/scripts/mywait.sh",
                 "Applications/scripts/rcmd.sh",
                 "Applications/scripts/rundig.sh",
                 "Applications/scripts/service.sh",
                 "Applications/scripts/ssh-tunnel.sh",
                 "Applications/scripts/tar-all.sh",            # now compress
                 "Applications/scripts/untar-all.sh",          # now uncompress
                 "Applications/scripts/utar-all.sh",
                 "Applications/scripts/vnctunnel.sh",
                 "Applications/scripts/regex-test.pl",
                 "Applications/scripts/profile-computer.pl",
                 "Applications/scripts/leet.pl",
                 "Applications/scripts/ntpdate",
                 "Applications/scripts/motion.sh",
                 "Applications/3ddesk.desktop",
                 "Applications/Gnome*ScreenSaver*Lock.desktop", # spaces might break this
                 "Applications/TkSETI.desktop",
                 "Applications/firefox.desktop",
                 "Applications/nvu.desktop",
                 "Applications/qtopiadesktop.desktop",
                 "Applications/ut2003.desktop",
                 "Applications/ut2004demo.desktop",
                 "Applications/virus_removal.desktop",
                );
my %hosts;    # global multi-dimensional hash to store hosts' information
my $VERBOSE      = 0;
my $DEBUG        = 0;
my $USAGE        = 0;
my $PRINT_FILES  = 0;
my $REMOVE_FILES = 0;
my $DOLINKS      = 0;
my $DOOBSOLETE   = 0;
my $MASTER       = 0;   # update current host from $MASTER_HOST
my $LOCAL        = 0;   # do not get any new copies, use the local files instead
my $PKEY         = 0;   # should we push our id_rsa.pub key to the remote host?
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;    # TODO add --ssh-port to allow users to change the ssh port at will
my $RED          = "\033[1;31m";
my $NORM         = "\033[0;39m";
my $GREEN        = "\033[0;32m";
my $VERBOSE_ARGS = "";
my $WGET_ARGS    = "--continue --timestamping";
my $HOST         = undef;
my $USERNAME     = undef;
my $PASSWORD     = undef;

my $CONF_FILES = undef;    # comma separated list

## GET OPTIONS ##

=pod

=over 8

=item -D,--debug

Enables debug mode

=item -d,--delete-obsolete

Deletes the obsolete scripts from the @_OBSOLETE array (hard coded)

=item --files FILES

Comma separated list of files to get/upgrade instead of the defaults at @FILES

=item -h,--help

Prints this help and exits

=item --host URL

Sets $MASTER_HOST to URL. This is the URL from where the original files will be downloaded.

=item --hosts-file FILE

Uses FILE instead of ~/.remote-hosts as our hosts file. This file is in the same format as /etc/hosts.

=item -k,--key KEY

Uses private key KEY instead of ~/.ssh/id_rsa for ssh-agent

=item -l,--local

Use copies in $LOCAL_PATH instead of remote $MASTER_HOST when --master is given. @see --local-path.
Otherwise, it will create local versions of the files using ~/Applications and these new files will be used instead on the remote hosts. 

Example: 
update-host --local --links REMOTE_HOST

This will create the Applications.tar.bz2, bashrc.tar.bz2, vimrc.tar.bz2 and muttrc.tar.bz2 files from ~/Applications and send those files over to the remote host.

=item --local-path PATH

Sets $LOCAL_PATH to PATH instead of the default $ENV{"HOME"}/Shared/Software/settings

=item -L,--symlinks,--links

Do symlinks instead of unpacking bashrc.tar.bz2, vimrc.tar.bz2, muttrc.tar.bz2. See --files.

.bashrc -> Applications/share/shell/bashrc

.bash_profile -> Applications/share/shell/bashrc

.inputrc -> Applications/share/shell/inputrc

.cvsrc -> Applications/share/shell/cvsrc

.vim -> Applications/share/vim

.mutt -> Applications/share/mutt

See @_CONFIG_FILES

=item -m,--master

Update localhost from master. Default is to upgrade all the computers found in ~/.remote-hosts. @see --host

=item --print-files

Prints a list of files and exits. @see --files

=item --password

Send this password if prompted by ssh. This is highly insecure! Use this only if you know what you are doing and trust the security of your computer. This only makes sense when sending keys to various hosts and you don't want to retype the password for the given account e/a time. Needs Expect.pm.

=item -r,--remove

Remove all temporary files after updating

=item -s,--send-key

Pushes local ~/.ssh/id_rsa.pub key to remote host ~/.ssh/authorized_keys file and exits

=item -u,--username USER

Use USER as the name to use when connecting via ssh to HOST

=item -U,--usage

Prints usage information

=item -V,--verbose

be verbose

=item -v,--version

Prints version and exits

=back

=cut

GetOptions(

    # flags
    'v|version'          => \$PVERSION,
    'h|help'             => \$HELP,
    'U|usage'            => \$USAGE,
    'D|debug'            => \$DEBUG,
    'd|delete-obsolete'  => \$DOOBSOLETE,
    'r|remove'           => \$REMOVE_FILES,
    'V|verbose'          => \$VERBOSE,
    'm|master|localhost' => \$MASTER,
    'l|local'            => \$LOCAL,
    'print-files'        => \$PRINT_FILES,
    'L|symlinks|links'   => \$DOLINKS,
    's|send-key'         => \$PKEY,

    # strings
    'k|key=s'      => \$PG_KEY,
    'host=s'       => \$MASTER_HOST,
    'files=s'      => \$CONF_FILES,
    'local-path=s' => \$LOCAL_PATH,
    'hosts-file=s' => \$REMOTE_HOSTS_FILE,
    'u|username=s' => \$USERNAME,
    'password=s'   => \$PASSWORD
) and $HOST = shift;

## START SCRIPT ##

if ($HELP)
{
    use Pod::Text;
    my $parser = Pod::Text->new(sentence => 0, width => 78);
    $parser->parse_from_file(File::Spec->catfile("$0"), \*STDOUT);
    _exit_safe(0);
}

if ($USAGE)
{
    use Pod::Usage;
    pod2usage(1);
    _exit_safe(0);
}

# username will be passed to e/a ssh call
if (defined($USERNAME) and $USERNAME !~ /^\s*$/)
{
    $USERNAME .= "\@";
}
else
{
    $USERNAME = "";
}

if ($VERBOSE)
{
    $VERBOSE_ARGS = "v";
}
else
{
    $WGET_ARGS = "-nv $WGET_ARGS";
}

## main ##

# when doing symlinks, we only need one file
@FILES = ("Applications.tar.bz2") if ($DOLINKS);

# yet, user can override @FILES from the command line...
if (defined($CONF_FILES))
{
    $CONF_FILES =~ s/[,|\s]+/ /g;    # separate by single spaces
    @FILES = split(/\s/, $CONF_FILES);
}

if ($PRINT_FILES)
{
    print STDOUT (join(",", @FILES), "
");
    _exit_safe(0);
}

my $j       = 0;
my $n_files = @FILES + 0;
my $silent  = (!$VERBOSE) ? " > /dev/null 2>&1" : "";

# update localhost from master and exit
if ($MASTER)
{
    my @failed      = ();    # array of files that failed
    my $FAIL_FLAG   = 0;
    my $SUCESS_FLAG = 0;

    _mkdir("$ENV{'HOME'}/.backup");
    _mkdir("$ENV{'HOME'}/tmp");
    system("touch $ENV{'HOME'}/.signaturerc")
      if (!-e "$ENV{'HOME'}/.signaturerc");

    _mkdir($LOCAL_PATH);
    chdir($LOCAL_PATH) or die("$LOCAL_PATH $!");
    if (!$LOCAL)
    {

        # download files from remote (master) host:
        foreach my $i (@FILES)
        {
            if ($MASTER_HOST =~ m/^http:/i && -r $i)
            {

                # wget is not too smart when using http
                debug("Deleting $i");
                unlink($i);    # silently delete old copies
            }
            printf STDOUT ("%20s", "Downloading $MASTER_HOST/$i ");
            system("wget $WGET_ARGS $MASTER_HOST/$i " . $silent);
            print STDOUT ($?) ? " failed
" : " done
";
        }
    }
    chdir($ENV{'HOME'});
    foreach my $i (@FILES)
    {
        if (!-r "$LOCAL_PATH/$i")
        {
            debug("Skipping $LOCAL_PATH/$i because we can't read it");
            next;
        }
        next if ($DOLINKS and $i =~ /bashrc|vim|mutt/i);    # skip shell config
        $j++;
        print STDOUT "
";
        my $cmd = "";
        my $_zcat =
          ($i =~ /\.bz2$/i) ? which("bzcat") : which("zcat");   # zcat or bzcat?
        my $_tar_arg = ($i =~ /\.bz2$/i) ? "j" : "z";           # gzip or bzip2?

        my $tar = which("tar");
        if (-x $_zcat)
        {
            $cmd =
              "command $_zcat '$LOCAL_PATH/$i' | command $tar x"
              . $VERBOSE_ARGS . "f - ";
        }
        else
        {

            # assume tar can decompress on the fly
            debug("Falling back to $tar...");
            $cmd =
                "command $tar x"
              . $VERBOSE_ARGS
              . $_tar_arg
              . "f '$LOCAL_PATH/$i'";
        }

        my $err = system($cmd. $silent);
        printf STDOUT ("%20s", "localhost");    # host padded
        if (!$err)
        {
            printf STDOUT ("|%-" . $n_files . "s|", "#" x $j);    # progress
            $SUCESS_FLAG++;
        }
        else
        {
            $FAIL_FLAG++;
            push(@failed, $i);
            my $n_failed = $#failed + 1;

            # last tar command failed?
            if (!$SUCESS_FLAG)
            {
                printf STDOUT ("|%-" . $n_files . "s|", "!" x $n_failed)
                  ;    # failed progress
            }
            else
            {

                # TODO keep the order of file failures
                printf STDOUT (
                               "|%-" . ($n_files - $n_failed) . "s",
                               "#" x ($n_files - $n_failed)
                              );    # successful progress
                printf STDOUT ("%-" . ($n_failed) . "s|", "!" x $n_failed)
                  ;                 # failed progress
            }
        }
    }
    print STDERR ("
localhost: These files failed: " . join(" ", @failed))
      if ($FAIL_FLAG);

    # do special operations:
    _do_links("localhost") if ($DOLINKS);
    unlink(@_OBSOLETE)     if ($DOOBSOLETE);

    print STDOUT ("
");
    _exit_safe(0);
}

# setup SSH
if (exists($ENV{'SSH_AUTH_SOCK'}) and !-S $ENV{'SSH_AUTH_SOCK'})
{
    $ENV{'SSH_AUTH_SOCK'} = "";
    my $_ssh_agent_env = qx/ssh-agent -s/;
    debug($_ssh_agent_env);
    $_ssh_agent_env =~ m/SSH_AUTH_SOCK=(.*); /gmi;
    debug("SSH_AUTH_SOCK before: ", $ENV{'SSH_AUTH_SOCK'});
    $ENV{'SSH_AUTH_SOCK'} = $1;
    debug("SSH_AUTH_SOCK after: ", $ENV{'SSH_AUTH_SOCK'});
    $_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
");
    }
}
$_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-agent 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

if ($PKEY)
{
    print STDERR (
        "host argument missing.
Usage: update-host --send-key HOST1 [HOST2 ...]
"
      )
      and _exit_safe(1)
      if (!defined($HOST) or $HOST =~ /^\s*$/);

    # 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");

    # test if our key already exists on the remote host
    open(KEY, "< $ENV{'HOME'}/.ssh/id_rsa.pub");
    my $key = <KEY>;
    close(KEY);
    chomp($key);
    $key =~ s/\S+\s+(\S+)\s+.*/$1/;
    $key = "NO_KEY" if ($key =~ /^\s*$/);
    $key =~ s/([\&\$\'\"\;\ ])/\\$1/g;    # escape shell chars
    print ("Using ssh key $key
") if ($VERBOSE);
    my $ssh_cmd =
        "cat $ENV{'HOME'}/.ssh/id_rsa.pub | ssh "
      . $USERNAME
      . $HOST
      . " 'mkdir ~/.ssh 2> /dev/null && chmod 0700 ~/.ssh; grep $key ~/.ssh/authorized_keys > /dev/null 2>&1 || cat - >> ~/.ssh/authorized_keys; chmod 0644 ~/.ssh/authorized_keys'";

    my $ret = 0;

    if ($PASSWORD)
    {

        # password was supplied from the command line, handle it with expect
        eval "use Expect";
        if ($@)
        {
            print STDERR "
$0 ERROR: Could not load the Expect.pm module.
"
              . "       To install this module use:
"
              . "       perl -e shell -MCPAN
"
              . "       On Debian just: 
"
              . "       apt-get install libexpect-perl libio-stty-perl 

";
            print STDERR "$@
";
            exit 1;
        }
        my $exp = Expect->new();
        $exp->stty(qw(raw -echo));
        my @args = ();    # no need
        $exp->spawn($ssh_cmd, @args)
          or die "Cannot spawn $ssh_cmd: $!
";

        # send some string there:
        $exp->expect(
            10,

            [
             qr/.*\(yes.no\)/ => sub {
                 my $self = shift;
                 $self->send("yes
");
                 exp_continue();
               }
            ],

            [
             qr/.*password: / => sub {
                 my $self = shift;
                 $self->send("$PASSWORD
");
                 exp_continue();
               }
            ]
        );
        $exp->soft_close();
    }
    else
    {
        $ret = system($ssh_cmd);
    }
    print STDERR ("failed to send key to $HOST") and _exit_safe($?)
      if ($? != 0);

    # if we have more hosts, keep going
    foreach my $host (@ARGV)
    {
        system("cat $ENV{'HOME'}/.ssh/id_rsa.pub | ssh "
            . $USERNAME
            . $host
            . " 'mkdir ~/.ssh 2> /dev/null && chmod 0700 ~/.ssh; grep -q $key ~/.ssh/authorized_keys 2> /dev/null || cat - >> ~/.ssh/authorized_keys; chmod 0644 ~/.ssh/authorized_keys'"
        );
        print STDERR ("failed to send key to $HOST") and _exit_safe($?)
          if ($? != 0);
    }

    _exit_safe(0);
}

# update a single host or set of hosts passed from the command line
# and exit
if ($HOST)
{
    my $host = $HOST;
    if (!$DEBUG)
    {

        if (grep(/$EXCLUDE/, $host))
        {
            print STDERR ("WARNING! This host is in the excluded list
");
        }
        else
        {
            print update_host($host);    # prints error if any
        }
    }
    else
    {
        debug("Doing host $host");
        if (is_alive($host))
        {
            debug("$host, it's alive!");
        }
        else
        {
            debug("$host, is kaboom!");
        }
        debug("*** SKIPPED $host");
    }

    # do all other hosts if any
    foreach my $host (@ARGV)
    {
        debug("Doing host $host");
        print STDERR ("WARNING! This host is in the excluded list
")
          if (grep(/$EXCLUDE/, $host));
        if (!$DEBUG)
        {
            print update_host($host);    # prints error if any
        }
        else
        {
            if (is_alive($host))
            {
                debug("$host, it's alive!");
            }
            else
            {
                debug("$host, is kaboom!");
            }
            debug("*** SKIPPED $host");
        }
    }
    _exit_safe(0);
}

# do all hosts from a remote file (if any)
slurp_hosts($REMOTE_HOSTS_FILE);

foreach my $h (keys %hosts)
{
    debug("*** DOING: $h");
    next if (grep(/$EXCLUDE/, $h));
    if (is_alive($h) != 1)
    {
        printf STDOUT ("%20s", $h);
        print STDOUT (" is dead or no ssh server on port $ssh_port
");
        next;
    }
    if (!$DEBUG)
    {
        print update_host($h);    # prints error if any
    }
    else
    {

        # debugging
        debug("*** SKIPPED $h");
    }
}

# @desc slurps fields of a hosts file to a global hash: %hosts
sub slurp_hosts
{
    my $file = shift;
    open(FILE, $file) || die("Could not open $file: $!
");
    while (<FILE>)
    {
        chomp($_);
        if ($_ gt "" && $_ !~ /^\s*#/)
        {
            if ($_ =~ /(\d+.\d+.\d+.\d+)\s+(\w+)/, $_)
            {
                $hosts{$1}{ip} = $2;
            }
        }
    }
}

# @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 finds binaries in your $PATH
sub which
{

    # @param 0 string := binary to find in $ENV{PATH}
    # returns binary path or -1 if not found
    my $binary = shift;
    my $path   = undef;    # holds string to return when found.
    foreach my $binary_path (split(/:/, $ENV{"PATH"}))
    {

        # first binary in $ENV{PATH}
        if (-x "$binary_path" . "/$binary")
        {
            $path = "$binary_path" . "/$binary";
            last;
        }
    }
    return $path;          # -1 means not found
}

# @desc prints colored messages
sub debug
{
    my $msg = "@_";
    print STDERR ("$RED $msg $NORM
") if ($DEBUG);
}

# @desc do all needed steps to update a given host
# @return error string if any errors were found
sub update_host
{
    my $h           = shift;
    my @failed      = ();      # array of failes that failed
    my $tar         = "tar";
    my $j           = 0;       # number of files done
    my $SUCESS_FLAG = 0;
    my $FAIL_FLAG   = 0;

    # sanity check. make sure host is alive:
    return "ERROR: host $h is dead or no ssh server on port $ssh_port
"
      if (!is_alive($h));

    # make sure the .backup and $TMP dir exists
    # and make sure this signature file exists:
    system(  "ssh "
           . $USERNAME
           . $h
           . " 'mkdir -p $TMP ~/.backup; chmod 0700 ~/.backup; touch ~/.signaturerc' "
           . $silent);

    # copy and unpack files:
    foreach my $i (@FILES)
    {
        next if ($DOLINKS and $i =~ /bashrc|vim|mutt/i);    # skip shell config

        # if the file does not exist, then we create our own on the fly
        if (!-r "$LOCAL_PATH/$i" or $LOCAL)
        {
            die(
                "We do not have a copy of Applications yet! Get one using: cd ~/ && git clone http://git.kiskeyix.org/git/Applications
"
               ) if (!-d "$ENV{'HOME'}/Applications");
            chdir($ENV{'HOME'}) or die("Cannot change to \$HOME. $!.
");
            system("mkdir -p $LOCAL_PATH");
            die("Could not create directory $LOCAL_PATH. $!
") if ($? != 0);
            if ($i =~ /Applications/)
            {
                $i = "Applications.tar.bz2";    # sanity check
                system(
                    "tar --exclude=\"bin\" --exclude=\"include\" --exclude=\"man\" --exclude=\"CVS\" --exclude=\"etc\" --exclude=\".cvsignore\" --exclude=\".git\" -cjf $LOCAL_PATH/$i Applications"
                );
            }
            elsif ($i =~ /vim/)
            {
                $i = " vimrc . tar . bz2 ";
                system(" tar-- exclude = \"CVS\" -h -cjf $LOCAL_PATH/$i .vim");
            }
            elsif ($i =~ /mutt/)
            {
                $i = "muttrc.tar.bz2";
                system("tar --exclude=\"CVS\" -h -cjf $LOCAL_PATH/$i .mutt");
            }
            elsif ($i =~ /bash/)
            {
                $i = "bashrc.tar.bz2";
                system(
                    "tar -h -cjf $LOCAL_PATH/$i .bashrc .bash_profile .dir_colors .inputrc .cvsrc"
                );
            }
            else
            {
                die("What is this?? $i?
");
            }
        }
        $j++;
        print STDOUT "
";    # return cursor to beginning of line
        printf STDOUT ("%20s", $h)
          ;                   # host name padded to print progressbar correctly

        system("scp $LOCAL_PATH/$i " . $USERNAME . "$h:$TMP/ " . $silent);

        my $_zcat    = ($i =~ /\.bz2$/i) ? "bzcat" : "zcat";    # zcat or bzcat?
        my $_tar_arg = ($i =~ /\.bz2$/i) ? "j"     : "z";       # gzip or bzip2?
        my $cmd = "ssh "
          . $USERNAME
          . $h
          . " 'command $_zcat '$TMP/$i' | command $tar x"
          . $VERBOSE_ARGS . "f -' ";

        my $err = system($cmd. $silent);

        if (!$err)
        {
            $SUCESS_FLAG++;
            printf STDOUT ("|%-" . $n_files . "s|", "#" x $j);    # progress
        }
        else
        {
            $FAIL_FLAG++;
            push(@failed, $i);
            my $n_failed = $#failed + 1;

            # last tar command failed?
            if (!$SUCESS_FLAG)
            {
                printf STDOUT ("|%-" . $n_files . "s|", "!" x $n_failed)
                  ;    # failed progress
            }
            else
            {

                # TODO keep the order of file failures
                printf STDOUT (
                               "|%-" . ($n_files - $n_failed) . "s",
                               "#" x ($n_files - $n_failed)
                              );    # successful progress
                printf STDOUT ("%-" . ($n_failed) . "s|", "!" x $n_failed)
                  ;                 # failed progress
            }
        }

        if ($REMOVE_FILES)
        {
            system(  "ssh "
                   . $USERNAME
                   . $h
                   . " command rm -f '$TMP/$i' "
                   . $silent);
        }
    }

    if ($DOOBSOLETE)
    {
        my $joined_files = "'" . join("' '", @_OBSOLETE) . "'";
        debug("deleting obsolete: ", $joined_files, "
");
        system(  "ssh "
               . $USERNAME
               . $h
               . " command rm -f $joined_files "
               . $silent);
    }

    print STDOUT ("
$h These files failed: " . join(" ", @failed) . "
")
      if ($FAIL_FLAG);

    if ($DOLINKS)
    {
        _do_links($h);    # remote links
    }

    # no errors:
    return "
";
}

# @desc implements `mkdir -p`
sub _mkdir
{
    my $path = shift;
    my $mode = shift;
    $mode = (defined $mode) ? $mode : 0700;
    my $root = ($path =~ m,^([/|\\|:]),) ? $1 : "";    # relative or full path?
    my @dirs = splitdir($path);
    my $last = "";
    my $flag = 1;
    foreach (@dirs)
    {
        next if ($_ =~ m/^\s*$/);
        $last = ($flag > 1) ? catdir($last, $_) : "$root" . "$_";
        mkdir($last) if (!-d $last);
        $flag++;
    }
    chmod($mode,$last) if ($flag > 1); # we created at least one dir, and we need to change its mode
    return $flag;    # number of directories created
}

sub _do_links
{
    my $host = shift;
    return undef if (!defined($host));
    chdir($ENV{'HOME'}) if ($host eq "localhost");
    foreach my $_config_file (@_CONFIG_FILES)
    {
        my $_conf = "." . basename($_config_file);    # dot file
        if ($host eq "localhost")
        {
            if (!-l $_conf)
            {
                if (-e $_conf)
                {

                    # backup
                    if (!rename($_conf, "$_conf.bak"))
                    {

                        # oops
                        print STDERR (
                              "Failed to backup " . $ENV{'HOME'} . "/$_conf
");
                        next;
                    }
                }
            }
            else
            {

                # we already have a symlink here,
                # we need to make sure that the link points to the
                # correct path:
                # TODO test that symlink is valid and move on
                # for now, we simply remove the link
                next if (!unlink($_conf));
            }
            symlink($_config_file, $_conf);
        }
        else
        {

            # dealing with a remote hosts
            system(  "ssh "
                   . $USERNAME
                   . $host
                   . " 'command rm -fr $_conf;"
                   . "command ln -sf $_config_file $_conf $silent'");
        }
    }
}

sub prompt
{

    #@param 0 string := question to prompt
    #returns answer
    print STDOUT (@_);
    my $rep = <STDIN>;
    chomp($rep);
    return $rep;
}

sub _exit_safe
{
    my $status = shift;
    $status = 0 if (not defined($status));

    # TODO handle more signals
    my %exit = (INT => '9');
    if ($_ssh_id == 1)
    {
        system("ssh-add -D");    # delete identities
    }

    if ($_ssh_agent == 1)
    {
        kill(15, $ENV{'SSH_AGENT_PID'});
    }
    if ($status =~ /^[0-9]+$/)
    {
        exit $status;
    }
    exit $exit{$status};
}


Advertisement