#!/usr/bin/perl

use strict;

# hosts - print names of all hosts
#
#       erco@3dsite.com (Greg Ercolano)
#
#       This software is public domain. Please maintain version history.
#       Please notify me of bugs. Do not remove this header.
#       Use at your own risk.
#
#       VERS    DATE            AUTHOR          COMMENTS
#       1.00    09/20/98        Greg Ercolano   Initial version
#       1.10    08/23/00        Greg Ercolano   Added --rsh, --sh
#       1.20    12/02/02        Greg Ercolano   Rsh now uses fork/exec, sig handling
#       x.xx    xx/xx/xx        -               -
#       

# Include 'offline' in keys, if machine is offline

#    Keyname     Key's Meaning
#    ----------  -----------------------------------------------
#    sgi         All sgis
#    linux       All linux
#    prod        *Should* have production software installed
#    noprod      Should *not* have production software installed
#    timeserver  Should *not* be configured as a time _client_
#    mailserver  Should *not* be configured as a mail _client_
#    license     License server (currently unused?)
#    adm         Administrative machine (currently unused?)

$G::hosts = <<"EOF";
# Host          Keys                                            Comments
backup          linux,noprod,adm                                # Backups server
inet1           linux,noprod,adm,server,timeserver              # Network server: time/dns/etc
ed              linux,noprod,adm,server,mailserver              # Evil Dead
sd              linux,noprod,adm,server                         # Scooby Doo
rf1             linux,prod,rf                                   # render farm
rf2             linux,prod,rf                                   # render farm
rf3             linux,prod,rf                                   # render farm
rf4             linux,prod,rf                                   # render farm
rf5             linux,prod,rf                                   # render farm
rf6             linux,prod,rf                                   # render farm
rf7             linux,prod,rf                                   # render farm
rf8             linux,prod,rf                                   # render farm
rf9             linux,prod,rf                                   # render farm
rf10            linux,prod,rf                                   # render farm
rf11            linux,prod,rf,offline                           # render farm
boyne           sgi,prod,license                                # octane, License & Alfred server
maxa            sgi,prod
mega            sgi,prod
exa             sgi,prod,offline
xena            sgi,prod,offline
dakini          sgi,prod
newsgi1         sgi,prod
newsgi2         sgi,prod
othello         sgi,prod,offline
ykaneko         sgi,prod
bwhit           sgi,prod
EOF

# GLOBALS FOR INTERRUPT TRAP
my $G_pid = 0;		# pid of fork()ed process
my $G_time = 0;		# time since last ^C

# ^C INTERRUPT TRAP
sub TrapInt
{
    # KILL BACKGROUNDED PROCESS
    if ( $G_pid > 0 )
        { print STDERR "\n(Killed)"; kill($G_pid, -15); }

    # TWO INTERRUPTS WITHIN A SHORT TIME? EXIT
    if ( $G_time && (time() - $G_time) < 2 )
	{ print STDERR "\nhosts: Exiting (two ^C's within 2 secs)\n"; exit(1); }

    # SAVE CURRENT TIME FOR NEXT INTERRUPT
    $G_time = time();
}

# FORK AND EXEC
#    Better than system() at preserving arguments with spaces (eg. rsh)
#    and doesn't incur an unnecessary shell.
#
sub ForkExec
{
    $SIG{INT} = *TrapInt;		# trap interrupts while process running
    $G_pid = fork();			# save pid for trap
    if ( $G_pid == 0 )			# child process?
	{ exec(@_); print STDERR "exec $_[0]: $!\n"; exit(1); }
    wait(); 				# parent? wait for child
    $G_pid = 0;				# zero out pid
    $SIG{INT} = "IGNORE";		# IGNORE prevents single ^C from killing script
    return($? >> 8);
}

# MAIN
{
    my $err = 1;
    my ($rshcmd, $shcmd, @hostkeys);

    # FLUSH ALL OUTPUT
    select(STDERR); $|;
    select(STDOUT); $|;

    # PARSE ARGS
    my $t;
    for ( $t=0; $t<=$#ARGV; $t++ )
    {
        # HELP
        if ( $ARGV[$t] eq "--help" )
            { HelpAndExit(); }

        # LIST KEYS
        if ( $ARGV[$t] eq "--list" )
            { print $G::hosts; exit(0); }

        # RSH
        if ( $ARGV[$t] eq "--rsh" )
        {
            $rshcmd = $ARGV[++$t];
            if ( $rshcmd eq "" ) { HelpAndExit(); }
            next;
        }

        # SH
        if ( $ARGV[$t] eq "--sh" )
        {
            $shcmd = $ARGV[++$t];
            if ( $shcmd eq "" ) { HelpAndExit(); }
            next;
        }

        # ALL ELSE: HOST KEYS
        push(@hostkeys, $ARGV[$t]);
    }

    # COMPILE HOST DATA INTO A HASH
    my %host;
    foreach ( split("\n", $G::hosts ) )
    {
        if ( /^#/ || /^$/ ) { next; }       # skip comment or blank lines
        s/\s+/ /;                           # fold multiple white into single
        my @words = split(/\s+/);
        $host{$words[0]}{'keys'}    = join(",",@words);
        $host{$words[0]}{'comment'} = $_;
        $host{$words[0]}{'comment'} =~ s/^[^#]*#\s+//;
    }

    # LOOP THRU ALL HOSTS
    my $name;
    foreach $name ( sort ( keys ( %host ) ) )
    {
        my $yes = -1;   # no postives specified yet
        my $no  = 0;    # negatives off unless specified

        my $arg;
        foreach $arg ( @hostkeys )
        {
            if ( $arg eq "" )   # skip previously parsed args
                { next; }

            # HANDLE POSITIVE MATCHES
            if ( $arg !~ /^-/ )
            {
                if ( $yes == -1 )       # assume off unless positive match 
                    { $yes = 0; }

                my $key;
                foreach $key ( split(/,/,$host{$name}{'keys'} ) )
                    { if ( $key eq $arg ) { $yes = 1; } }
            }
            # HANDLE NEGATIVE MATCHES
            elsif ( $arg =~ /^-(.*)/ )
            {
                my $nokey = $1;
                my $key;
                foreach $key ( split(/,/,$host{$name}{'keys'} ) )
                    { $no = (($nokey eq $key)?1:$no); }
            }
        }

        # 'NO' HAS PRECEDENCE
        if ( $no )
             { next; }

        # 'YES' IF NO POSITIVES SPECIFIED, OR IF MATCH
        if ( $yes == -1 ||              # no positives specified? Assume all
             $yes == 1) 	        # positive specified and passed?
        {
            if ( $rshcmd ne "" )
            { 
                printf(STDERR "%10s: ", $name);
                if ( ForkExec("rsh", $name, sprintf($rshcmd,$name,$name,$name,$name)) ) { $err = 1; }
                print STDERR "\n";
            }
            elsif ( $shcmd ne "" )
            {
                printf(STDERR "%10s: ", $name);
                if ( system(sprintf($shcmd,$name,$name,$name,$name)) ) { $err = 1; } 
                print STDERR "\n";
            }
            else
                { print "$name\n"; }
        }
    }

    exit($err);
}

# HELP
sub HelpAndExit()
{
    print <<"EOF";
USAGE
    hosts               # lists all hosts
    hosts --list        # list all hosts with keys
    hosts [[-]key ..]   # list hosts with limits
    hosts --sh 'cmd'    # run 'cmd' in local shell (%s=hostname)
    hosts --rsh 'cmd'   # run 'cmd' in remote shell (%s=hostname)

EXAMPLES
    hosts               # print all hosts
    hosts --list        # print all hosts with keys
    hosts prod          # print all production hosts (ie. non-servers)
    hosts sgi           # print all sgis
    hosts sgi -mega     # print all sgis except mega
    hosts sgi -offline  # print all except offline hosts
    hosts sgi prod      # print all sgi production machines
    hosts linux prod    # print all linux production machines

REAL WORLD
    hosts prod --sh 'rdist -c /usr/nreal/license.dat %s:/usr/nreal/license.dat'
                        # rdist the license file to all prod machines
    hosts --rsh 'date; uptime'
    			# run several commands on remotes
    hosts --rsh 'perl -e '\\''system("date; hostname");'\\'
    			# run one liner perl command on remotes..
			# quoting style is same for a plain rsh(1)

CAVEATS
    With --rsh, the following ^C behavior will be in effect:

        o Hitting ^C with at least 2 secs between each only kills 
	  the current rsh; hosts(1) will move on to the next host.

    	o Two ^C's in a row kills hosts(1).
EOF
    exit(1);
}
