Skip to content

Instantly share code, notes, and snippets.

@leverich
Created September 19, 2015 20:16

Revisions

  1. leverich created this gist Sep 19, 2015.
    157 changes: 157 additions & 0 deletions dsh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,157 @@
    #!/usr/bin/perl

    %hosts = (
    "example" => "host-0..9",
    );

    sub usage {
    print"
    dsh -- run commands with ssh on several hosts simultaneous
    Usage: $0 [-vdnsph] <hostlist> <commands>
    -p|--parallel -s|--sequential -h|--help
    -v|--verbose -d|--dryrun -n|--showname
    hostlist is a comma-separated list of hosts on which to run commands.
    The string '..' can be used to denote numerical ranges.
    The following host aliases are defined:
    ";

    for (sort keys %hosts) { printf "% 15s : %s\n", $_, $hosts{$_}; }

    print "
    Example usage: dsh -np example hostname
    ";

    exit shift;
    }

    use Getopt::Long;
    Getopt::Long::Configure("bundling", "require_order");
    GetOptions ("v|verbose" => \$verbose,
    "d|dryrun" => \$dryrun,
    "n|showname" => \$showname,
    "s|sequential" => \$sequential,
    "p|parallel" => \$parallel,
    "h|help" => \$help) or usage 1; # die $!;

    $sequential = 1 if !$parallel;
    $verbose = 1 if $dryrun;

    usage 0 if $help;

    sub load_hosts {
    my $hostsref = shift;
    my $path = "$ENV{HOME}/.dsh";

    return unless -d $path;
    opendir DIR, $path or die $!;
    for my $file (readdir DIR) {
    next unless -f "$path/$file";
    next if $file =~ /(,|\.\.)/; # so we don't screw up resolve_hosts()

    open FILE, "<$path/$file" or die $!;
    my @entries = ();
    while (<FILE>) {
    s/\#.*//; # strip comments
    s/^\s*//; # strip leading whitespace
    s/\s*$//; # strip trailing whitespace
    s/,/ /g; # convert commas to a space
    s/\s+/,/g; # convert spaces to a single comma
    next unless /./; # skip blank lines...
    print "consider $_\n";
    push @entries, $_;
    }
    close FILE;

    $hostsref->{$file} = join(",", @entries); # save
    }
    closedir DIR;
    }

    sub resolve_hosts {
    my $root = shift;

    my @parts = split(/\s*,\s*/, $root);
    my @hosts = ();

    for my $part (@parts) {
    ## case 1: needs to expand %hosts
    ## case 2: needs to expand ..
    ## case 3: no expansion needed
    if (exists $hosts{$part}) {
    push @hosts, resolve_hosts($hosts{$part});
    } elsif ($part =~ /^(.*?)(\d+)\.\.(\d+)(.*?)$/) {
    next if $2 < 0 or $3 > 1000 or !$1;

    for ($2..$3) {
    push @hosts, "$1$_$4";
    }
    } else {
    push @hosts, $part;
    }
    }

    return @hosts;
    }

    load_hosts(\%hosts);

    $hosts = shift @ARGV;
    @hosts = resolve_hosts($hosts);

    ## remove duplicates

    @uniq_hosts = ();
    %seen = ();

    for my $host (@hosts) {
    push @uniq_hosts, $host unless defined $seen{$host};
    $seen{$host} = 1;
    }

    @hosts = @uniq_hosts;

    ##

    usage 1 unless scalar @hosts;
    die "No command specified" unless scalar @ARGV;

    sub ssh {
    my $host = shift;
    my @args = ("-o", "StrictHostKeyChecking=no",
    "-o", "ForwardX11=no",
    "-o", "ForwardX11Trusted=no",
    "-o", "ForwardAgent=no",
    "-o", "BatchMode=yes",
    "-o", "ConnectTimeout=5", $host, @_);

    push (@args, "2>&1|sed 's/^/$host: /'") if $showname;

    print STDERR join(" ", "ssh", @args), "\n" if $verbose;
    my $ret = system { "ssh" } "ssh", @args;
    }

    sub run {
    my $host = shift;

    if ($sequential) {
    ssh($host, @_);
    } else {
    my $pid = fork;
    if ($pid == 0) {
    ssh($host, @_);
    exit 0;
    }
    }
    }

    print STDERR "Hostlist: " . join(" ", @hosts) . "\n" if $verbose;

    for my $host (@hosts) {
    run($host, @ARGV);
    }

    while (wait() != -1) {}