581 lines
14 KiB
Perl
581 lines
14 KiB
Perl
package Cluster::SSH::Helper;
|
|
|
|
use 5.006;
|
|
use strict;
|
|
use warnings;
|
|
use Config::Tiny;
|
|
use String::ShellQuote;
|
|
use SNMP;
|
|
use File::BaseDir qw/xdg_config_home/;
|
|
|
|
=head1 NAME
|
|
|
|
Cluster::SSH::Helper - The great new Cluster::SSH::Helper!
|
|
|
|
=head1 VERSION
|
|
|
|
Version 0.1.0
|
|
|
|
=cut
|
|
|
|
our $VERSION = '0.1.0';
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
Quick summary of what the module does.
|
|
|
|
Perhaps a little code snippet.
|
|
|
|
use Cluster::SSH::Helper;
|
|
|
|
my $csh = Cluster::SSH::Helper->new();
|
|
...
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 new
|
|
|
|
Any initially obvious errors in the passed config or hosts config will result in this method dieing.
|
|
|
|
Tho hash references are required . The first is the general config and the second is the hosts config.
|
|
|
|
my $csh;
|
|
eval({
|
|
$csh= Cluster::SSH::Helper->new( \%config, %hosts );
|
|
});
|
|
if ( $@ ){
|
|
die( 'Cluster::SSH::Helper->new failed... '.$@ );
|
|
}
|
|
|
|
=cut
|
|
|
|
sub new {
|
|
my $config = $_[1];
|
|
my $hosts = $_[2];
|
|
|
|
# make sure we have a config and that it is a hash
|
|
if ( defined($config) ) {
|
|
if ( ( ref($config) ne 'HASH' )
|
|
&& ( ref($config) ne 'Config::Tiny' ) )
|
|
{
|
|
die( 'The passed reference for the config is not a hash... ' . ref($config) );
|
|
}
|
|
|
|
if ( !defined( $config->{'_'} ) ) {
|
|
$config->{'_'} = {};
|
|
}
|
|
|
|
if ( defined( $config->{'_'}{'env'} ) ) {
|
|
if ( !defined( $config->{ $config->{'_'}{'env'} } ) ) {
|
|
die( '"' . $config->{'_'}{'env'} . '" is specified as for $config->{_}{env} but it is undefined' );
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# ALL DEFAULTS!
|
|
$config = { '_' => {} };
|
|
}
|
|
|
|
# make sure we have a hosts and that it is a hash
|
|
if ( defined($hosts) ) {
|
|
if ( ( ref($hosts) ne 'HASH' ) && ( ref($config) ne 'Config::Tiny' ) ) {
|
|
die('The passed reference for the hosts is not a hash');
|
|
}
|
|
}
|
|
else {
|
|
# this module is useless with out it
|
|
die('No hash reference passed for the hosts config');
|
|
}
|
|
|
|
my $self = {
|
|
config => $config,
|
|
hosts => $hosts,
|
|
};
|
|
bless $self;
|
|
|
|
# set defaults
|
|
if ( !defined( $self->{config}{_}{warn_on_poll} ) ) {
|
|
$self->{config}{_}{warn_on_poll} = 1;
|
|
}
|
|
if ( !defined( $self->{config}{_}{method} ) ) {
|
|
$self->{config}{_}{method} = 'load_1m';
|
|
}
|
|
if ( !defined( $self->{config}{_}{ssh} ) ) {
|
|
$self->{config}{_}{ssh} = 'ssh';
|
|
}
|
|
if ( !defined( $self->{config}{_}{snmp} ) ) {
|
|
$self->{config}{_}{snmp} = '-v 2c -c public';
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
=head1 new_from_ini
|
|
|
|
Initiates the this object from a file contiaining the general config and host confg
|
|
from two INI files.
|
|
|
|
There are two optional arguments. The first one is the path to the config INI and the
|
|
second is the path to the hosts INI.
|
|
|
|
If not specifiedied, xdg_config_home.'/cluster-ssh-helper/config.ini'
|
|
and xdg_config_home.'/cluster-ssh-helper/hosts.ini' are used.
|
|
|
|
my $csh;
|
|
eval({
|
|
$csh= Cluster::SSH::Helper->new_from_ini( $config_path, $hosts_path );
|
|
});
|
|
if ( $@ ){
|
|
die( 'Cluster::SSH::Helper->new_from_ini failed... '.$@ );
|
|
}
|
|
|
|
=cut
|
|
|
|
sub new_from_ini {
|
|
my $config_file = $_[1];
|
|
my $hosts_file = $_[2];
|
|
|
|
if ( !defined($config_file) ) {
|
|
$config_file = xdg_config_home . '/cluster-ssh-helper/config.ini';
|
|
}
|
|
if ( !defined($hosts_file) ) {
|
|
$hosts_file = xdg_config_home . '/cluster-ssh-helper/hosts.ini';
|
|
}
|
|
|
|
my $config = Config::Tiny->read($config_file);
|
|
if ( !defined($config) ) {
|
|
die( "Failed to read '" . $config_file . "'" );
|
|
}
|
|
|
|
my $hosts = Config::Tiny->read($hosts_file);
|
|
if ( !defined($hosts) ) {
|
|
die( 'Failed to read"' . $hosts_file . '"' );
|
|
}
|
|
|
|
my $self = Cluster::SSH::Helper->new( $config, $hosts );
|
|
|
|
return $self;
|
|
}
|
|
|
|
=head2 run
|
|
|
|
=head3 command
|
|
|
|
This is a array to use for building the command that will be run.
|
|
Basically thinking of it as @ARGV where $ARGV[0] will be the command
|
|
and the rest will the the various flags etc.
|
|
|
|
=head3 env
|
|
|
|
This is a alternative env value to use if not using the defaults.
|
|
|
|
=head3 method
|
|
|
|
This is the selector method to use if not using the default.
|
|
|
|
=head3 debug
|
|
|
|
If set to true, returns the command in question after figuring out what it is.
|
|
|
|
eval({
|
|
$csh->run({
|
|
command=>'uname -a',
|
|
});
|
|
});
|
|
|
|
=cut
|
|
|
|
sub run {
|
|
my $self = $_[ 0 ];
|
|
my $opts = $_[ 1 ];
|
|
|
|
# make sure we have the options
|
|
if ( !defined( $opts ) ) {
|
|
die( 'No options hash passed' );
|
|
}
|
|
else {
|
|
if ( ref( $opts ) ne 'HASH' ) {
|
|
die( 'The passed item was not a hash reference' );
|
|
}
|
|
|
|
if ( !defined( $opts->{ command } ) ) {
|
|
die( '$opts->{command} is not defined' );
|
|
}
|
|
}
|
|
|
|
# Gets the method to use
|
|
if (!defined( $opts->{method} )) {
|
|
$opts->{method}=$self->{config}{_}{method};
|
|
}
|
|
|
|
# gets the env to use
|
|
my $default_env;
|
|
if ( !defined( $opts->{env} ) ) {
|
|
if ( defined( $self->{config}{_}{env} ) ) {
|
|
$opts->{env} = $self->{config}{_}{env};
|
|
}
|
|
$default_env=1;
|
|
}
|
|
|
|
# _ is the root of the ini file... not a section, which are meant to be used as env
|
|
if ( ( defined( $opts->{env} ) && ( $opts->{env} eq '_' ) ) ) {
|
|
die('"_" is not a valid name for a enviroment section');
|
|
}
|
|
|
|
# figure out what host to use
|
|
my $host;
|
|
if ( $opts->{method} eq 'load_1m' ) {
|
|
$host = $self->lowest_load_1n;
|
|
} elsif ( $opts->{method} eq 'ram_used_percent' ) {
|
|
$host = $self->lowest_used_ram_percent;
|
|
} else {
|
|
die( '"' . $opts->{method} . '" is not a valid method' );
|
|
}
|
|
|
|
# if we don't have a host, no point in proceeding
|
|
if (!defined($host)) {
|
|
die('Unable to get a host. Host chooser method returned undef.');
|
|
}
|
|
|
|
# real in host specific stuff
|
|
my $ssh_host = $host;
|
|
if ( defined( $self->{hosts}{$host}{host} ) ) {
|
|
$ssh_host = $self->{hosts}{$host}{host};
|
|
}
|
|
if ( defined( $self->{hosts}{$host}{env} ) && $default_env ) {
|
|
$opts->{env} = $self->{hosts}{$host}{env};
|
|
}
|
|
|
|
# makes sure the section exists if one was specified
|
|
my $env_command;
|
|
if ( defined( $opts->{env} )
|
|
&& !defined( $self->{config}{_}{ $opts->{env} } ) ) {
|
|
die( '"'
|
|
. $opts->{env}
|
|
. '" is not a defined section in the config' );
|
|
|
|
# builds the env commant
|
|
$env_command = '/usr/bin/env';
|
|
foreach my $key ( keys( @{ $self->{config}{ $$opts->{env} } } ) ) {
|
|
$env_command =
|
|
$env_command . ' '
|
|
. shell_quote( shell_quote($key) ) . '='
|
|
. shell_quote(
|
|
shell_quote( $self->{config}{ $opts->{env}{$key} } ) );
|
|
}
|
|
|
|
}
|
|
|
|
# the initial command to use
|
|
my $command = $self->{config}{_}{ssh};
|
|
if ( defined( $self->{config}{_}{ssh_user} ) ) {
|
|
$command = $command . ' ' . shell_quote( $self->{config}{_}{ssh_user} ) . '@' . shell_quote($ssh_host);
|
|
}
|
|
else {
|
|
$command = $command . ' ' . $host;
|
|
}
|
|
|
|
# add the env command if needed
|
|
if (defined($env_command)) {
|
|
$command=$command.' '.$env_command;
|
|
}
|
|
|
|
# add on each argument
|
|
foreach my $part ( @{ $opts->{command} } ) {
|
|
if ( defined($env_command) ) {
|
|
$command = $command . ' '
|
|
. shell_quote( shell_quote( shell_quote($part) ) );
|
|
} else {
|
|
$command = $command . ' ' . shell_quote( shell_quote($part) );
|
|
}
|
|
}
|
|
|
|
if ( $opts->{debug} ) {
|
|
return $command;
|
|
}
|
|
|
|
print $command."\n";
|
|
system($command);
|
|
|
|
return $?;
|
|
}
|
|
|
|
=head2 lowest_load_1n
|
|
|
|
This returns the host with the lowest 1 minute load.
|
|
|
|
my $host = $csh->lowest_load_1m;
|
|
|
|
=cut
|
|
|
|
sub lowest_load_1n {
|
|
my $self = $_[ 0 ];
|
|
|
|
my @hosts = keys( %{ $self->{ hosts } } );
|
|
|
|
# holds a list of the 1 minute laod values for each host
|
|
my %host_loads;
|
|
use Data::Dumper;
|
|
foreach my $host (@hosts) {
|
|
my $snmp;
|
|
my $load;
|
|
# see if we can create it and fetch the OID
|
|
eval {
|
|
$snmp = $self->snmp_command($host).' 1.3.6.1.4.1.2021.10.1.6.1';
|
|
$load=`$snmp`;
|
|
chomp($load);
|
|
$load=~s/.*\:\ //;
|
|
};
|
|
|
|
my $poll_failed;
|
|
if ( $@ && $self->{config}{_}{warn_on_poll} ) {
|
|
warn( 'Polling for "' . $host . '" failed with... ' . $@ );
|
|
$poll_failed = 1;
|
|
}
|
|
|
|
if ( defined($load) ) {
|
|
# if we are here, then polling worked with out issue
|
|
$host_loads{$host} = $load;
|
|
} elsif ( defined($poll_failed)
|
|
&& !defined($load) ) {
|
|
# If here, it means the polling did not die, but it also did not work
|
|
warn( 'Polling for "' . $host . '" returned undef' );
|
|
}
|
|
}
|
|
|
|
my @sorted = sort { $host_loads{$a} <=> $host_loads{$b} } keys(%host_loads);
|
|
|
|
# we did not manage to poll anything... or thre are zero hosts
|
|
if (!defined($sorted[0])) {
|
|
die('There are either zero defined hosts or polling failed for all hosts');
|
|
}
|
|
|
|
return $sorted[0];
|
|
}
|
|
|
|
=head2 lowest_used_ram_percent
|
|
|
|
This returns the host with the lowest percent of used RAM.
|
|
|
|
my $host = $csh->lowest_used_ram_percent;
|
|
|
|
=cut
|
|
|
|
sub lowest_used_ram_percent {
|
|
my $self = $_[ 0 ];
|
|
|
|
my @hosts = keys( %{ $self->{ hosts } } );
|
|
|
|
# holds a list of the 1 minute laod values for each host
|
|
my %hosts_polled;
|
|
|
|
foreach my $host (@hosts) {
|
|
my $snmp;
|
|
my $used;
|
|
my $total;
|
|
my $used_value;
|
|
my $total_value;
|
|
my $percent;
|
|
# see if we can create it and fetch the OID
|
|
eval {
|
|
$snmp = $self->snmp_command($host).' 1.3.6.1.4.1.2021.10.1.6.1';
|
|
$used = $snmp.' 1.3.6.1.4.1.2021.4.6.0';
|
|
$total = $snmp.' 1.3.6.1.4.1.2021.4.5.0';
|
|
|
|
$used_value=`$used`;
|
|
$total_value=`$total_value`;
|
|
|
|
$used_value=~s/.*\:\ //;
|
|
$total_value=~s/.*\:\ //;
|
|
|
|
$percent = $used_value / $total_value;
|
|
};
|
|
|
|
my $poll_failed;
|
|
if ( $@ && $self->{config}{_}{warn_on_poll} ) {
|
|
warn( 'Polling for "' . $host . '" failed with... ' . $@ );
|
|
$poll_failed = 1;
|
|
}
|
|
|
|
if ( defined($total) ) {
|
|
# if we are here, then polling worked with out issue
|
|
$hosts_polled{$host} = $percent;
|
|
} elsif ( defined($poll_failed)
|
|
&& !defined($percent) ) {
|
|
# If here, it means the polling did not die, but it also did not work
|
|
warn( 'Polling for "' . $host . '" returned undef' );
|
|
}
|
|
}
|
|
|
|
my @sorted = sort { $hosts_polled{$a} <=> $hosts_polled{$b} } keys(%hosts_polled);
|
|
|
|
# we did not manage to poll anything... or thre are zero hosts
|
|
if (!defined($sorted[0])) {
|
|
die('There are either zero defined hosts or polling failed for all hosts');
|
|
}
|
|
|
|
return $sorted[0];
|
|
}
|
|
|
|
=head2 snmp_command
|
|
|
|
Generates the full gammand to be used with snmpget minus that OID to fetch.
|
|
|
|
One argument is taken and that is the host to generate it for.
|
|
|
|
As long as the host exists, this command will work.
|
|
|
|
my $cmd;
|
|
eval({
|
|
$cmd=$cshelper->snmp_command($host);
|
|
});
|
|
|
|
=cut
|
|
|
|
sub snmp_command{
|
|
my $self = $_[0];
|
|
my $host = $_[1];
|
|
|
|
if (!defined($host)) {
|
|
die('No host defined');
|
|
}
|
|
|
|
if (!defined($self->{hosts}{$host})) {
|
|
die('The host "'.$host.'" is not configured');
|
|
}
|
|
|
|
my $snmp='';
|
|
|
|
if (defined($self->{config}{_}{snmp})) {
|
|
$snmp=$self->{config}{_}{snmp};
|
|
}
|
|
|
|
if (defined($self->{hosts}{snmp})) {
|
|
$snmp=$self->{hosts}{snmp};
|
|
}
|
|
|
|
return 'snmpget -O vU '.$snmp.' '.$host;
|
|
}
|
|
|
|
=head1 CONFIGURATOIN
|
|
|
|
=head2 GENERAL
|
|
|
|
The general configuration.
|
|
|
|
This is written to be easily loadable via L<Config::Tiny>, hence why '_' is used.
|
|
|
|
=head3 _
|
|
|
|
This contains the default settings to use. Each of these may be over ridden on a per host basis.
|
|
|
|
If use SNMPv3, please see L<Net::SNMP> for more information on the various session options.
|
|
|
|
=head4 method
|
|
|
|
This is default method to use for selecting the what host to run the command on. If not specified,
|
|
'load' is used.
|
|
|
|
load_1m , checks 1 minute load and uses the lowest
|
|
ram_used_percent , uses the host with the lowest used percent of RAM
|
|
|
|
=head4 ssh
|
|
|
|
The default command to use for SSH.
|
|
|
|
If not specified, just 'ssh' will be used.
|
|
|
|
This should not include either user or host.
|
|
|
|
=head4 ssh_user
|
|
|
|
If specified, this will be the default user to use for SSH.
|
|
|
|
If not specified, SSH is invoked with out specifying a user.
|
|
|
|
=head4 snmp
|
|
|
|
This is the default options to use with netsnmp. This should be like
|
|
'-v 2c -c public' or the like.
|
|
|
|
If not specified, this defaults to '-v 2c -c public'.
|
|
|
|
=head4 env
|
|
|
|
If specified, '/usr/bin/env' will be inserted before the command to be ran.
|
|
|
|
The name=values pairs for this will be built using the hash key specified by this.
|
|
|
|
=head4 warn_on_poll
|
|
|
|
Issue a warn() on SNMP timeouts or other polling issues. This won't be a automatic failure. Simply timing out
|
|
on SNMP means that host will be skipped and not considered.
|
|
|
|
This defaults to 1, true.
|
|
|
|
=head2 HOSTS
|
|
|
|
This contains the hosts to connect to.
|
|
|
|
Each key of this hash reference is name of the host in question. The value is a hash
|
|
containing any desired over rides to the general config.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Zane C. Bowers-Hadley, C<< <vvelox at vvelox> >>
|
|
|
|
=head1 BUGS
|
|
|
|
Please report any bugs or feature requests to C<bug-cluster-ssh-helper at rt.cpan.org>, or through
|
|
the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Cluster-SSH-Helper>. I will be notified, and then you'll
|
|
automatically be notified of progress on your bug as I make changes.
|
|
|
|
|
|
|
|
|
|
=head1 SUPPORT
|
|
|
|
You can find documentation for this module with the perldoc command.
|
|
|
|
perldoc Cluster::SSH::Helper
|
|
|
|
|
|
You can also look for information at:
|
|
|
|
=over 4
|
|
|
|
=item * RT: CPAN's request tracker (report bugs here)
|
|
|
|
L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Cluster-SSH-Helper>
|
|
|
|
=item * AnnoCPAN: Annotated CPAN documentation
|
|
|
|
L<http://annocpan.org/dist/Cluster-SSH-Helper>
|
|
|
|
=item * CPAN Ratings
|
|
|
|
L<https://cpanratings.perl.org/d/Cluster-SSH-Helper>
|
|
|
|
=item * Search CPAN
|
|
|
|
L<https://metacpan.org/release/Cluster-SSH-Helper>
|
|
|
|
=back
|
|
|
|
|
|
=head1 ACKNOWLEDGEMENTS
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT
|
|
|
|
This software is Copyright (c) 2019 by Zane C. Bowers-Hadley.
|
|
|
|
This is free software, licensed under:
|
|
|
|
The Artistic License 2.0 (GPL Compatible)
|
|
|
|
|
|
=cut
|
|
|
|
1; # End of Cluster::SSH::Helper
|