#!/usr/bin/perl

# Created on: 2012-01-18 08:29:06
# Create by:  Ivan Wills
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use strict;
use warnings;
use version;
use Getopt::Long;
use Pod::Usage;
use Data::Dumper qw/Dumper/;
use English qw/ -no_match_vars /;
use FindBin qw/$Bin/;
use Path::Class;
use POSIX ":sys_wait_h";

our $VERSION = version->new('0.0.1');
my ($name)   = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;

my %option = (
    host    => [],
    verbose => 0,
    man     => 0,
    help    => 0,
    VERSION => 0,
);

if ( !@ARGV ) {
    pod2usage( -verbose => 1 );
}

main();
exit 0;

sub main {

    $option{cssh} = `which cssh`;
    chomp $option{cssh};

    Getopt::Long::Configure('bundling');
    GetOptions(
        \%option,
        'host|h=s@',
        'parallel|p=i',
        'fork|f',
        'cssh|c=s',
        'interleave|i',
        'test|t!',
        'verbose|v+',
        'man',
        'help',
        'VERSION!',
    ) or pod2usage(2);

    if ( $option{'VERSION'} ) {
        print "$name Version = $VERSION\n";
        exit 1;
    }
    elsif ( $option{'man'} ) {
        pod2usage( -verbose => 2 );
    }
    elsif ( $option{'help'} ) {
        pod2usage( -verbose => 1 );
    }
    elsif ( !@ARGV && !( -x $option{cssh} ) ) {
        warn "You must specify a command or install or specify the location of cssh!\n";
        pod2usage( -verbose => 1 );
    }

    if ( !@ARGV ) {
        exec $option{cssh}, get_hosts($option{host});
    }

    # do stuff here
    my $remote_cmd = pop @ARGV;
    #my $remote_cmd = join ' ', map {s{'}{'\\''}gxms; $_} @ARGV;

    if (@ARGV) {
        push @{$option{host}}, @ARGV;
    }

    my @hosts = get_hosts($option{host});

    if ( !@hosts ) {
        warn "You must specify at least one host!\n";
        pod2usage( -verbose => 1 );
    }

    print join "\n", @hosts, '' if $option{verbose} > 1 or $remote_cmd eq '-';
    return if $remote_cmd eq '-';

    # store child processes if forking
    my @children;

    # loop over each host and run the remote command
    for my $host (@hosts) {
        my $cmd = "ssh $host " . shell_quote($remote_cmd);
        print "$cmd\n" if $option{verbose} || $option{test};
        next if $option{test};

        if ( $option{parallel} ) {
            my $child = fork;

            if ( $child ) {
                # parent stuff
                push @children, $child;

                if ( @children == $option{parallel} ) {
                    warn "Waiting for children to finish\n" if $option{verbose} > 1;
                    # reap children if reached max fork count
                    while ( my $pid = shift @children ) {
                        waitpid $pid, 0;
                    }
                }
            }
            elsif ( defined $child ) {
                # child code
                if ( $option{interleave} ) {
                    exec "$cmd 2>&1";
                }
                else {
                    my $out = `$cmd 2>&1`;

                    print "\n$cmd\n";
                    print $out;
                }
                exit;
            }
            else {
                die "Error: $!\n";
            }
        }
        else {
            system $cmd;
        }
    }

    # reap any outstanding children
    wait;

    return;
}

# converts host ranges to actual host names
sub get_hosts {
    my ($hosts) = @_;
    my @hosts;

    my $int_re       = qr/ [0-9a-zA-Z] /xms;
    my $range_re     = qr/ ($int_re) (?:[.][.]|-) ($int_re)/xms;
    my $group_re     = qr/ (?: $int_re | $range_re )       /xms;
    my $seperated_re = qr/ $group_re (?: , $group_re )  *  /xms;
    my $num_range_re = qr/ [[{] ( $seperated_re ) [\]}]    /xms;

    while ( my $host_range = shift @{$hosts} ) {
        my ($num_range) = $host_range =~ /$num_range_re/;

        if (!$num_range) {
            push @hosts, $host_range;
            next;
            #if ( is_host($host_range) ) {
            #    push @hosts, $host_range;
            #    next;
            #}
            #else {
            #    unshift @{$hosts}, $host_range;
            #    last;
            #}
        }

        my @numbs    = map { /$range_re/ ? ($1 .. $2) : ($_) } split /,/, $num_range;
        my @hostmaps = map { $a=$host_range; $a =~ s/$num_range_re/$_/e; $a } @numbs;

        if ( $hostmaps[0] =~ /$num_range_re/ ) {
            push @{$option{host}}, @hostmaps;
        }
        else {
            push @hosts, @hostmaps;
        }
    }

    return @hosts;
}

sub is_host {
    my $full_name = `host $_[0]`;
    return $full_name;
}

sub shell_quote {
    my ($text) = @_;

    if ($text =~ /[\s$|><;#]/xms) {
        $text =~ s/'/'\\''/gxms;
        $text = "'$text'";
    }

    return $text;
}

__DATA__

=head1 NAME

mssh - Multi host ssh executer

=head1 VERSION

This documentation refers to mssh version 0.1.

=head1 SYNOPSIS

   mssh [ --VERSION | --help | --man ]
   mssh [ -t | --test ] [ -v | --verbose ] -h host [-h host2 ..] 'remote command'
   mssh [ -t | --test ] [ -v | --verbose ] host [host2 ...] 'remote command'
   mssh [ -t | --test ] [ -v | --verbose ] -h host [-h host2 ..]

 OPTIONS:
  'remote command'
                  The command to execute on the remote hosts.
  host            See --host, only use if specifying a remote command

  -h --host[=]str Host or range of hosts to execute 'remote command' on
                  The range must be sourounded by [] and can be either comma
                  seperated integer/letters or .. for a range of all values
                  from the first to last. Note if clusterssh (cssh) is
                  installed and no 'remote command' specified C<cssh> will
                  be run
                  EG [0..5] for 1,2,3,4 and 5
                     [0,5]      1 and 5
                     [0..3,5]   1,2,3 and 5
                     [0-5]      1,2,3,4 and 5
                     [0-3,5]    1,2,3 and 5
                     {0..5} for 1,2,3,4 and 5
                     {0,5}      1 and 5
                     {0..3,5}   1,2,3 and 5
                     {0-5}      1,2,3,4 and 5
                     {0-3,5}    1,2,3 and 5
  -p --parallel[=]int
                  Fork calls to each server to run in parallel the value limits
                  the number of processes called at once.
                  Note without --interleave you wont see results until the each
                  server has completed so that results are groupped.
  -i --interleave When running parallel commands interleave the output of each host
  -t --test       Just show all the commands that would be run don't actually
                  run anything

  -v --verbose    Show more detailed option
     --VERSION    Prints the version information
     --help       Prints this help information
     --man        Prints the full documentation for mssh

=head1 DESCRIPTION

=head1 SUBROUTINES/METHODS

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gmail.com).

Patches are welcome.

=head1 AUTHOR

Ivan Wills - (ivan.wills@gmail.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2012 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW 2077).
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

=cut
