-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b117065
commit dffc0d3
Showing
3 changed files
with
1,060 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
#!/usr/bin/perl | ||
|
||
# (c) 2017 Jim Salter. Licensed GPLv3. net-hydra --usage for details. | ||
|
||
# this software is licensed for use under the Free Software Foundation's GPL v3.0 license, as retrieved | ||
# from http://www.gnu.org/licenses/gpl-3.0.html on 2014-11-17. A copy should also be available in this | ||
# project's Git repository at https://github.com/jimsalterjrs/network-testing/blob/master/LICENSE. | ||
|
||
use strict; | ||
use Data::Dumper; # debugging - print contents of hash | ||
use Time::HiRes qw ( usleep ); | ||
|
||
my %args = getargs(@ARGV); | ||
|
||
# If this eval{} succeeds, Config::IniFiles becomes available. If it fails, $inifiles will be false. | ||
my $inifiles = eval { | ||
require Config::IniFiles; | ||
Config::IniFiles->import(); | ||
1; | ||
}; | ||
|
||
if ($args{'usage'}) { | ||
printusage(); | ||
exit 0; | ||
} | ||
|
||
# friendlier variable names for arguments | ||
my $conf_file = $args{'c'}; | ||
my $testname = $args{'testname'}; | ||
|
||
my $sshcmd = '/usr/bin/ssh'; | ||
|
||
if (! $inifiles) { | ||
# no Config::IniFiles available. Sadface. Die with honor now. | ||
print "\n"; | ||
print "net-hydra requires the Perl library Config::IniFiles. You may install it\n"; | ||
print "using CPAN, or (preferably) from your distribution's repositories.\n"; | ||
print "\n"; | ||
print "On Debian/Ubuntu/derivatives, try:\n"; | ||
print " sudo apt install libconfig-inifiles-perl\n"; | ||
print "\n"; | ||
print "On RedHat/derivatives, try:\n"; | ||
print " sudo yum install perl-Config-IniFiles\n"; | ||
print "\n"; | ||
exit 256; | ||
} | ||
|
||
# load test configs from multiclient.conf | ||
tie my %ini, 'Config::IniFiles', ( -file => $conf_file ) or die "FATAL: cannot load $conf_file."; | ||
|
||
# populate %clients from $ini{'clients'} for easier access later | ||
my %clients; | ||
my %sockets; | ||
my %directives; | ||
foreach my $client (keys %{ $ini{'clients'} }) { | ||
$clients{$client} = $ini{'clients'}{$client}; | ||
} | ||
|
||
|
||
# establish SSH control sessions for each device | ||
print "Establishing SSH control sessions, and checking clock synchronization on each client...\n"; | ||
|
||
foreach my $client (sort keys %clients) { | ||
my $socket = "/tmp/multiclient-$client-" . time(); | ||
$sockets{$client} = $socket; | ||
print "Establishing control socket for $client...\n"; | ||
open FH, "$sshcmd -M -S $socket -o ControlPersist=15m $clients{$client} exit |"; | ||
close FH; | ||
} | ||
|
||
# establish clock is synced at each device | ||
foreach my $client (sort keys %clients) { | ||
my $cmd = "/bin/date +%s"; | ||
print "Checking time synchronization for $client...\n"; | ||
my $sync = `$sshcmd -S $sockets{$client} $clients{$client} $cmd`; | ||
my $now = time(); | ||
if ( abs($now - $sync) >1 ) { | ||
my $delta = $now-$sync; | ||
print "Device $client at $clients{$client} is $delta seconds off from my clock.\n"; | ||
print "Please correct the time at $clients{$client} before attempting to run\n"; | ||
print "this job again.\n\n"; | ||
exit 255; | ||
} | ||
} | ||
|
||
# delay execution for two seconds per directive to execute | ||
my $delay = (scalar keys %{ $ini{'directives'} }) * 2 + 10; | ||
my $when = time()+$delay; | ||
|
||
# populate %clients from $ini{'clients'} for easier access later | ||
foreach my $client (keys %{ $ini{'directives'} }) { | ||
$directives{$client} = $ini{'directives'}{$client}; | ||
if ($directives{$client} =~ /^\$/) { | ||
# it's an alias, so populate it from the aliases table | ||
$directives{$client} = $ini{'aliases'}{$directives{$client}}; | ||
} | ||
# substitute exection time in for special value $when in the directives | ||
$directives{$client} =~ s/\$when/$when/g; | ||
# substitute testname in for special value $testname in the directives | ||
$directives{$client} =~ s/\$testname/$testname/g; | ||
} | ||
|
||
# loop through the directives given | ||
foreach my $client (sort keys %directives) { | ||
#print "$client is at $clients{$client} and needs to whenits $when $directives{$client} .\n"; | ||
print "Scheduling job on $client...\n"; | ||
my $cmd = "$sshcmd -S $sockets{$client} $clients{$client} whenits -d $when $directives{$client}"; | ||
#print "$cmd\n"; | ||
system $cmd; | ||
} | ||
|
||
my $now=time(); | ||
print "Executing on client systems in " . ($when-$now) . " seconds...\n"; | ||
while ($now < $when) { | ||
$now = time(); | ||
# move 1 line up, clear to beginning of that line | ||
print "\e[1F"; | ||
print "\e[K"; | ||
print "Executing on client systems in " . ($when-$now) . " seconds...\n"; | ||
usleep 100000; # sleep 100 milliseconds | ||
} | ||
|
||
print "\e[1F"; | ||
print "\e[K"; | ||
print "Client systems executing now.\n"; | ||
|
||
exit 0; | ||
|
||
|
||
########################################################################################################## | ||
|
||
|
||
sub printusage { | ||
print "Usage: net-hydra -c {configfile} [--testname {testname}]\n"; | ||
print "\n"; | ||
print "net-hydra requires a config file specifying [clients], [directives], and optionally\n"; | ||
print "[aliases], which it uses to schedule jobs to run simultaneously in the very near future\n"; | ||
print "on multiple client machines.\n"; | ||
print qq^ | ||
# sample nethydra.conf | ||
[clients] | ||
# list each client device here, by name and as SSH will connect to them. | ||
# | ||
local0 = root\@127.0.0.1 | ||
local1 = root\@127.0.0.2 | ||
[directives] | ||
# These are the command(s) to be run on each client device. Directives | ||
# beginning with \$ reference lines from the [aliases] section. Any use | ||
# of \$when, either here or in [aliases], will be replaced with the | ||
# scheduled execution time, in Unix epoch seconds; \$testname is replaced | ||
# with the --testname argument passed to net-hydra on the command-line. | ||
# | ||
local0 = \$4kstream | ||
local1 = /usr/bin/touch /tmp/test-\$testname-\$when.txt | ||
[aliases] | ||
# Defining aliases here keeps [directives] cleaner, so you can | ||
# see what you're doing a little more easily. | ||
# | ||
\$4kstream = netburn -u http://127.0.0.1/1M.bin -r 25 -o /tmp/\$testname-\$when.csv | ||
^; | ||
print "\n"; | ||
print "net-hydra requires the whenits command to be in the standard path on each client\n"; | ||
print "machine, for use precisely scheduling the directives to execute simultaneously.\n"; | ||
print "\n"; | ||
print "See https://github.com/jimsalterjrs/network-testing for sample configs and more\n"; | ||
print "information.\n\n"; | ||
exit 256; | ||
} | ||
|
||
sub getargs { | ||
my @args = @_; | ||
my %args; | ||
|
||
my %novaluearg; | ||
my %validarg; | ||
push my @validargs, ('usage','c','testname'); | ||
foreach my $item (@validargs) { $validarg{$item} = 1; } | ||
|
||
push my @novalueargs, ('usage'); | ||
foreach my $item (@novalueargs) { $novaluearg{$item} = 1; } | ||
|
||
push my @mandatoryargs, ('c'); | ||
|
||
# if user specified no args at all, force --usage on | ||
if (scalar @args == 0) { $args{'usage'}=1; } | ||
|
||
while (my $rawarg = shift(@args)) { | ||
my $arg = $rawarg; | ||
my $argvalue = ''; | ||
if ($rawarg =~ /=/) { | ||
# user specified the value for a CLI argument with = | ||
# instead of with blank space. separate appropriately. | ||
$argvalue = $arg; | ||
$arg =~ s/=.*$//; | ||
$argvalue =~ s/^.*=//; | ||
} | ||
if ($rawarg =~ /^--/) { | ||
# doubledash arg | ||
$arg =~ s/^--//; | ||
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } | ||
if ($novaluearg{$arg}) { | ||
$args{$arg} = 1; | ||
} else { | ||
# if this CLI arg takes a user-specified value and | ||
# we don't already have it, then the user must have | ||
# specified with a space, so pull in the next value | ||
# from the array as this value rather than as the | ||
# next argument. | ||
if ($argvalue eq '') { $argvalue = shift(@args); } | ||
$args{$arg} = $argvalue; | ||
} | ||
} elsif ($arg =~ /^-/) { | ||
# singledash arg | ||
$arg =~ s/^-//; | ||
if (! $validarg{$arg}) { die "ERROR: don't understand argument $rawarg.\n"; } | ||
if ($novaluearg{$arg}) { | ||
$args{$arg} = 1; | ||
} else { | ||
# if this CLI arg takes a user-specified value and | ||
# we don't already have it, then the user must have | ||
# specified with a space, so pull in the next value | ||
# from the array as this value rather than as the | ||
# next argument. | ||
if ($argvalue eq '') { $argvalue = shift(@args); } | ||
$args{$arg} = $argvalue; | ||
} | ||
} else { | ||
# bare arg | ||
die "ERROR: don't know what to do with bare argument $rawarg.\n"; | ||
} | ||
} | ||
|
||
# if we aren't checking for usage, | ||
my @missingargs; | ||
if (!defined $args{'usage'}) { | ||
foreach my $mandarg (@mandatoryargs) { | ||
if (! defined $args{$mandarg}) { push @missingargs, $mandarg; } | ||
} | ||
} | ||
if (scalar @missingargs) { | ||
printusage(); | ||
|
||
my $errortext = "ERROR: missing mandatory arguments "; | ||
foreach my $missingarg (@missingargs) { | ||
$errortext .= "-$missingarg, "; | ||
} | ||
# trim trailing ", " from errortext | ||
$errortext = substr($errortext,0,((length $errortext)-2)); | ||
die "$errortext\n"; | ||
} | ||
|
||
# insert default values for arguments here if necessary | ||
if (! defined $args{'testname'}) { $args{'testname'} = 'null'; } | ||
|
||
return %args; | ||
} | ||
|
Oops, something went wrong.