#!/usr/bin/perl -w
require 5.003;
use strict;
use integer;

#
# executes a command on or for a set of hosts,
# setting `host' and `hid' shell variables for each host
#

die("usage: $0 <address_range|host_group_name> [[-] <command> [arguments]]\n") if @ARGV < 1;

my ($HostGroup, @Cmd) = @ARGV;

# these are primary host addresses, not aliases!
my %Hosts = (
	cl1 => ['10.0.13.61-62'],
	sv1 => ['10.0.13.129-130'],
);

# convert address ranges to address array
foreach my $v (values %Hosts) {
	my @addrs = ();
	foreach my $addr (@{$v}) {
		push @addrs, &range2arr($addr);
	}
	@{$v} = @addrs;
}

# form groups
foreach my $k (keys %Hosts) {
	my ($id) = ($k =~ /^cl(\d+)$/);
	next unless defined $id;
	my @gr = (@{$Hosts{"cl$id"}}, @{$Hosts{"sv$id"}});
	$Hosts{"gr$id"} = [ @gr ];
}

my @hosts = defined $Hosts{$HostGroup} ?
	@{$Hosts{$HostGroup}} : (&range2arr($HostGroup));

die("host group $HostGroup is unknown; stopped") unless @hosts;

if (!@Cmd) {
	print join(' ', @hosts), "\n";
	exit 0;
}

print("hosts: ", join(' ', @hosts), "\n");

my $local = $Cmd[0] eq '-'; shift @Cmd if $local;
my $cmd = join(' ', @Cmd);


my $hid = 0;
my %nameIds = ( clt => 0, srv => 0 );

foreach my $host (@hosts) {
	print("==> $host\n");
	$ENV{'host'} = $host;
	$ENV{'hid'} = ++$hid;

	my $name = &isClient($host) ? 'clt' : 'srv';
	$nameIds{$name}++;
	$ENV{'hname'} = $name . $nameIds{$name};

	if ($local) {
		warn("- $cmd\n");
		system("$cmd") == 0 and next;
	} else {
		system("ssh $host $cmd") == 0 and next;
	}
	print("failure on host $host\n");
}

exit(0);

sub isClient($) {
	my $host = shift;
	my ($n) = ($host =~ /(\d)$/);
	return undef unless defined $n;
	return ($n & 1) > 0;
}

sub range2cls {
	my %h = (&range2arr(shift));
	return sort keys %h;
}

sub range2svs {
	my %h = (&range2arr(shift));
	return sort values %h;
}

sub range2arr {
	my $range = shift;
	my @specs = split(/\./, $range);

	return () unless @specs > 1;

	my @bins = ();
	foreach my $spec (@specs) {
		my ($min, $max) = $spec =~ /-/ ?
			($spec =~ /^(\d+)-(\d+)$/) : ($spec =~ /^(\d+)$/);
		return undef unless defined $min;
		$max = $min if !defined($max);
		push @bins, { min=>$min, max=>$max, pos=>$min };
	}

	my @res = ();
	while (1) {
		push @res, &curAddr(\@bins);
	} continue {
		last unless nextIter(\@bins);
	}

	return @res;
}

sub nextIter {
	my ($bins, $level) = @_;
	$level = $#{$bins} if !defined $level;
	return undef if $level < 0;
	
	my $b = $bins->[$level];
	if ($b->{pos} >= $b->{max}) {
		$b->{pos} = $b->{min};
		return &nextIter($bins, $level-1);
	}
	$b->{pos}++;

	if ($b->{pos} % 255 == 0) { # skip 255s
		$b->{pos}++;
	}

	return 1;
}

sub curAddr {
	my $bins = shift;
	
	my $addr = '';
	for (my $i = 0; $i <= $#{$bins}; ++$i) {
		my $b = $bins->[$i];
		if ($b->{max} > 255) { # spill
			$addr .= sprintf("%d.", $b->{pos} / 255);
		}
		$addr .= sprintf("%d.", $b->{pos} % 255);
	}
	chop($addr);
	return $addr;
}

sub odd {
	my $h = shift;
	return ($h & 1) > 0
}


syntax highlighted by Code2HTML, v. 0.9.1