* Created the new Remote module, and in it, moved System->remote_call to Remote->call() and created the new add_target_to_known_hosts() method (and two private helper methods). These are adapted from the m2 code.

* Updated Storage->read_file and Storage->write_file to support reading and writing on remote systems (untested though)
* Created System->change_shell_user_password() that changes a shell user's password by manually generating an sha512 salted hash of the given password and uses the resulting hash to modify the target user's password, so the password should never be visible in the process list. Works on both local and remote systems, though it still needs testing.
* Created Storage->rsync() to handle moving files between the local and a remote system.

Signed-off-by: Digimer <digimer@alteeve.ca>
main
Digimer 7 years ago
parent a294c6c4fa
commit ea43896fca
  1. 26
      Anvil/Tools.pm
  2. 2
      Anvil/Tools/Get.pm
  3. 823
      Anvil/Tools/Remote.pm
  4. 737
      Anvil/Tools/Storage.pm
  5. 578
      Anvil/Tools/System.pm
  6. 1
      rpm/SPECS/anvil.spec
  7. 18
      share/words.xml
  8. 82
      tools/anvil-change-password
  9. 3
      tools/anvil-configure-network

@ -43,6 +43,7 @@ use Anvil::Tools::Database;
use Anvil::Tools::Convert;
use Anvil::Tools::Get;
use Anvil::Tools::Log;
use Anvil::Tools::Remote;
use Anvil::Tools::Storage;
use Anvil::Tools::System;
use Anvil::Tools::Template;
@ -117,6 +118,7 @@ sub new
CONVERT => Anvil::Tools::Convert->new(),
GET => Anvil::Tools::Get->new(),
LOG => Anvil::Tools::Log->new(),
REMOTE => Anvil::Tools::Remote->new(),
STORAGE => Anvil::Tools::Storage->new(),
SYSTEM => Anvil::Tools::System->new(),
TEMPLATE => Anvil::Tools::Template->new(),
@ -152,6 +154,7 @@ sub new
$anvil->Convert->parent($anvil);
$anvil->Get->parent($anvil);
$anvil->Log->parent($anvil);
$anvil->Remote->parent($anvil);
$anvil->Storage->parent($anvil);
$anvil->System->parent($anvil);
$anvil->Template->parent($anvil);
@ -422,6 +425,18 @@ sub Log
return ($self->{HANDLE}{LOG});
}
=head2 Remote
Access the C<Remote.pm> methods via 'C<< $anvil->Remote->method >>'.
=cut
sub Remote
{
my $self = shift;
return ($self->{HANDLE}{REMOTE});
}
=head2 Storage
Access the C<Storage.pm> methods via 'C<< $anvil->Storage->method >>'.
@ -767,8 +782,10 @@ sub _set_paths
dmidecode => "/usr/sbin/dmidecode",
echo => "/usr/bin/echo",
ethtool => "/usr/sbin/ethtool",
expect => "/usr/bin/expect",
'firewall-cmd' => "/usr/bin/firewall-cmd",
gethostip => "/usr/bin/gethostip",
head => "/usr/bin/head",
hostname => "/usr/bin/hostname",
hostnamectl => "/usr/bin/hostnamectl",
ifdown => "/sbin/ifdown",
@ -780,6 +797,7 @@ sub _set_paths
md5sum => "/usr/bin/md5sum",
'mkdir' => "/usr/bin/mkdir",
nmcli => "/bin/nmcli",
passwd => "/usr/bin/passwd",
ping => "/usr/bin/ping",
pgrep => "/usr/bin/pgrep",
ps => "/usr/bin/ps",
@ -787,11 +805,17 @@ sub _set_paths
'postgresql-setup' => "/usr/bin/postgresql-setup",
pwd => "/usr/bin/pwd",
rsync => "/usr/bin/rsync",
sed => "/usr/bin/sed",
'shutdown' => "/usr/sbin/shutdown",
'ssh-keyscan' => "/usr/bin/ssh-keygen",
strings => "/usr/bin/strings",
stty => "/usr/bin/stty",
su => "/usr/bin/su",
systemctl => "/usr/bin/systemctl",
touch => "/usr/bin/touch",
timeout => "/usr/bin/timeout",
touch => "/usr/bin/touch",
'tr' => "/usr/bin/tr",
usermod => "/usr/sbin/usermod",
uuidgen => "/usr/bin/uuidgen",
},
'lock' => {

@ -136,7 +136,7 @@ else
fi;
";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
my ($error, $output) = $anvil->System->remote_call({
my ($error, $output) = $anvil->Remote->call({
shell_call => $shell_call,
target => $target,
port => $port,

@ -0,0 +1,823 @@
package Anvil::Tools::Remote;
#
# This module contains methods used to handle storage related tasks
#
use strict;
use warnings;
use Data::Dumper;
use Scalar::Util qw(weaken isweak);
our $VERSION = "3.0.0";
my $THIS_FILE = "Remote.pm";
### Methods;
# add_target_to_known_hosts
# call
# _call_ssh_keyscan
# _check_known_hosts_for_target
=pod
=encoding utf8
=head1 NAME
Anvil::Tools::Remote
Provides all methods related to accessing a remote system. Currently, all methods use SSH for remote access
=head1 SYNOPSIS
use Anvil::Tools;
# Get a common object handle on all Anvil::Tools modules.
my $anvil = Anvil::Tools->new();
# Access to methods using '$anvil->Storage->X'.
#
# Example using 'find()';
my $foo_path = $anvil->Storage->find({file => "foo"});
=head1 METHODS
Methods in this module;
=cut
sub new
{
my $class = shift;
my $self = {
};
bless $self, $class;
return ($self);
}
# Get a handle on the Anvil::Tools object. I know that technically that is a sibling module, but it makes more
# sense in this case to think of it as a parent.
sub parent
{
my $self = shift;
my $parent = shift;
$self->{HANDLE}{TOOLS} = $parent if $parent;
# Defend against memory leads. See Scalar::Util'.
if (not isweak($self->{HANDLE}{TOOLS}))
{
weaken($self->{HANDLE}{TOOLS});;
}
return ($self->{HANDLE}{TOOLS});
}
#############################################################################################################
# Public methods #
#############################################################################################################
=head2 add_target_to_known_hosts
This checks the C<< user >>'s C<< ~/.ssh/known_hosts >> file for the presence of the C<< target >>'s SSH RSA fingerprint. If it isn't found, it uses C<< ssh-keyscan >> to add the host. Optionally, it can delete any existing fingerprints (useful for handling a rebuilt machine).
Returns C<< 0 >> on success, C<< 1 >> on failure.
Parameters;
=head3 delete_if_found (optional, default 0)
If set, and if a previous fingerprint was found for the C<< target >>, the old fingerprint will be removed.
B<< NOTE >>: Obviously, this introduces a possible security issue. Care needs to be taken that the key being removed is, in fact, no longer needed.
=head3 port (optional, default 22)
This is the TCP port to use when connecting to the C<< target >> over SSH.
=head3 target (required)
This is the IP address or (resolvable) host name of the machine who's key we're recording.
=head3 user (optional, defaults to user running this method)
This is the user who we're recording the key for.
=cut
sub add_target_to_known_hosts
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $delete_if_found = defined $parameter->{delete_if_found} ? $parameter->{delete_if_found} : 0;
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : $<;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
delete_if_found => $delete_if_found,
port => $port,
target => $target,
user => $user,
}});
# Get the local user's home
my $users_home = $anvil->Get->users_home({user => $user});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { users_home => $users_home }});
if (not $users_home)
{
# No sense proceeding... An error will already have been recorded.
return(1);
}
# I'll need to make sure I've seen the fingerprint before.
my $known_hosts = $users_home."/.ssh/known_hosts";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { known_hosts => $known_hosts }});
# OK, now do we have a 'known_hosts' at all?
my $known_machine = 0;
if (-e $known_hosts)
{
# Yup, see if the target is there already,
$known_machine = $anvil->Remote->_check_known_hosts_for_target({
target => $target,
port => $port,
known_hosts => $known_hosts,
user => $user,
delete_if_found => $delete_if_found,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { known_machine => $known_machine }});
}
# If either known_hosts didn't contain this target or simply didn't exist, add it.
if (not $known_machine)
{
# We don't know about this machine yet, so scan it.
my $added = $anvil->Remote->_call_ssh_keyscan({
target => $target,
port => $port,
user => $user,
known_hosts => $known_hosts});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { added => $added }});
if (not $added)
{
# Failed to add. :(
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "error_0009", variables => {
target => $target,
port => $port,
user => $user,
}});
return(1);
}
}
return(0);
}
=head2 call
This does a remote call over SSH. The connection is held open and the file handle for the target is cached and re-used unless a specific ssh_fh is passed or a request to close the connection is received.
Example;
# Call 'hostname' on a node.
my ($error, $output) = $anvil->Remote->call({
target => "an-a01n01.alteeve.com",
user => "admin",
password => "super secret password",
shell_call => "/usr/bin/hostname",
});
# Make a call with sensitive data that you want logged only if $anvil->Log->secure is set and close the
# connection when done.
my ($error, $output) = $anvil->Remote->call({
target => "an-a01n01.alteeve.com",
user => "root",
password => "super secret password",
shell_call => "/usr/sbin/fence_ipmilan -a an-a01n02.ipmi -l admin -p \"super secret password\" -o status",
secure => 1,
'close' => 1,
});
If there is any problem connecting to the target, C<< $error >> will contain a translated string explaining what went wrong. Checking if this is B<< false >> is a good way to verify that the call succeeded.
Any output from the call will be stored in C<< $output >>, which is an array reference with each output line as an array entry. STDERR and STDOUT are merged into the C<< $output >> array reference, with anything from STDERR coming first in the array.
B<NOTE>: By default, a connection to a target will be held open and cached to increase performance for future connections.
Parameters;
=head3 close (optional, default '0')
If set, the connection to the target will be closed at the end of the call.
=head3 log_level (optional, default C<< 3 >>)
If set, the method will use the given log level. Valid values are integers between C<< 0 >> and C<< 4 >>.
=head3 no_cache (optional, default C<< 0 >>)
If set, and if an existing cached connection is open, it will be closed and a new connection to the target will be established.
=head3 password (optional)
This is the password used to connect to the remote target as the given user.
B<NOTE>: Passwordless SSH is supported. If you can ssh to the target as the given user without a password, then no password needs to be given here.
=head3 port (optional, default C<< 22 >>)
This is the TCP port to use when connecting to the C<< target >>. The default is port 22.
B<NOTE>: See C<< target >> for optional port definition.
=head3 secure (optional, default C<< 0 >>)
If set, the C<< shell_call >> is treated as containing sensitive data and will not be logged unless C<< $anvil->Log->secure >> is enabled.
=head3 shell_call (required)
This is the command to run on the target machine as the target user.
=head3 target (required)
This is the host name or IP address of the target machine that the C<< shell_call >> will be run on.
B<NOTE>: If the target matches an entry in '/etc/ssh/ssh_config', the port defined there is used. If the port is set as part of the target name, the port in 'ssh_config' is ignored.
B<NOTE>: If the C<< target >> is presented in the format C<< target:port >>, the port will be separated from the target and used as the TCP port. If the C<< port >> parameter is set, however, the port split off the C<< target >> will be ignored.
=head3 user (optional, default 'root')
This is the user account on the C<< target >> to connect as and to run the C<< shell_call >> as. The C<< password >> if so this user's account on the C<< target >>.
=cut
sub call
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
# Get the target and port so that we can create the ssh_fh key
my $log_level = defined $parameter->{log_level} ? $parameter->{log_level} : 3;
if (($log_level !~ /^\d$/) or ($log_level < 0) or ($log_level > 4))
{
# Invalid log level, set 2.
$log_level = 3;
}
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $ssh_fh_key = $target.":".$port;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
port => $port,
target => $target,
}});
# This will store the SSH file handle for the given target after the initial connection.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = defined $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} ? $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Now pick up the rest of the variables.
my $close = defined $parameter->{'close'} ? $parameter->{'close'} : 0;
my $no_cache = defined $parameter->{no_cache} ? $parameter->{no_cache} : 0;
my $password = defined $parameter->{password} ? $parameter->{password} : $anvil->data->{sys}{root_password};
my $secure = defined $parameter->{secure} ? $parameter->{secure} : 0;
my $shell_call = defined $parameter->{shell_call} ? $parameter->{shell_call} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : "root";
my $start_time = time;
my $ssh_fh = $anvil->data->{cache}{ssh_fh}{$ssh_fh_key};
# NOTE: The shell call might contain sensitive data, so we show '--' if 'secure' is set and $anvil->Log->secure is not.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
'close' => $close,
password => $anvil->Log->secure ? $password : "--",
secure => $secure,
shell_call => ((not $anvil->Log->secure) && ($secure)) ? "--" : $shell_call,
ssh_fh => $ssh_fh,
start_time => $start_time,
user => $user,
}});
if (not $shell_call)
{
# No shell call
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Remote->call()", parameter => "shell_call" }});
return("!!error!!");
}
if (not $target)
{
# No target
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Remote->call()", parameter => "target" }});
return("!!error!!");
}
if (not $user)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Remote->call()", parameter => "user" }});
return("!!error!!");
}
# If the user didn't pass a port, but there is an entry in 'hosts::<host>::port', use it.
if ((not $parameter->{port}) && ($anvil->data->{hosts}{$target}{port}))
{
$port = $anvil->data->{hosts}{$target}{port};
}
# Break out the port, if needed.
my $state;
my $error;
if ($target =~ /^(.*):(\d+)$/)
{
$target = $1;
$port = $2;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
port => $port,
target => $target,
}});
# If the user passed a port, override this.
if ($parameter->{port} =~ /^\d+$/)
{
$port = $parameter->{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
}
else
{
# In case the user is using ports in /etc/ssh/ssh_config, we'll want to check for an entry.
$anvil->System->read_ssh_config();
$anvil->data->{hosts}{$target}{port} = "" if not defined $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "hosts::${target}::port" => $anvil->data->{hosts}{$target}{port} }});
if ($anvil->data->{hosts}{$target}{port} =~ /^\d+$/)
{
$port = $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
}
# Make sure the port is valid.
if ($port eq "")
{
$port = 22;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
elsif ($port !~ /^\d+$/)
{
$port = getservbyname($port, 'tcp');
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
if ((not defined $port) or (($port !~ /^\d+$/) or ($port < 0) or ($port > 65536)))
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0058", variables => { port => $port }});
return("!!error!!");
}
# If the target is a host name, convert it to an IP.
if (not $anvil->Validate->is_ipv4({ip => $target}))
{
my $new_target = $anvil->Convert->hostname_to_ip({host_name => $target});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { new_target => $new_target }});
if ($new_target)
{
$target = $new_target;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { target => $target }});
}
}
# If the user set 'no_cache', don't use any existing 'ssh_fh'.
if (($no_cache) && ($ssh_fh))
{
# Close the connection.
$ssh_fh->disconnect();
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0010", variables => { target => $target }});
# For good measure, blank both variables.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = "";
$ssh_fh = "";
}
# These will be merged into a single 'output' array before returning.
my $stdout_output = [];
my $stderr_output = [];
# If I don't already have an active SSH file handle, connect now.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { ssh_fh => $ssh_fh }});
if ($ssh_fh !~ /^Net::SSH2/)
{
use Time::HiRes qw (usleep ualarm gettimeofday tv_interval nanosleep
clock_gettime clock_getres clock_nanosleep clock
stat);
### NOTE: Nevermind, timeout isn't supported... >_< Find a newer version if IO::Socket::IP?
### TODO: Make the timeout user-configurable to handle slow connections. Make it
### 'sys::timeout::{all|host} = x'
my $start_time = [gettimeofday];
$ssh_fh = Net::SSH2->new(timeout => 1000);
if (not $ssh_fh->connect($target, $port))
{
my $connect_time = tv_interval ($start_time, [gettimeofday]);
#print "[".$connect_time."] - Connection failed time to: [$target:$port]\n";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", list => {
user => $user,
target => $target,
port => $port,
shell_call => $shell_call,
error => $@,
}});
# We'll now try to get a more useful message for the user and logs.
my $message_key = "message_0005";
my $variables = { target => $target };
if ($@ =~ /Bad hostname/i)
{
$message_key = "message_0001";
}
elsif ($@ =~ /Connection refused/i)
{
$message_key = "message_0002";
$variables = {
target => $target,
port => $port,
user => $user,
};
}
elsif ($@ =~ /No route to host/)
{
$message_key = "message_0003";
}
elsif ($@ =~ /timeout/)
{
$message_key = "message_0004";
}
$error = $anvil->Words->string({key => $message_key, variables => $variables});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => $message_key, variables => $variables});
}
my $connect_time = tv_interval ($start_time, [gettimeofday]);
#print "[".$connect_time."] - Connect time to: [$target:$port]\n";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { error => $error, ssh_fh => $ssh_fh }});
if (not $error)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
user => $user,
password => $anvil->Log->secure ? $password : "--",
}});
if (not $ssh_fh->auth_password($user, $password))
{
# Can we log in without a password?
my $user = getpwuid($<);
my $home_directory = $anvil->Get->users_home({user => $user});
my $public_key = $home_directory."/.ssh/id_rsa.pub";
my $private_key = $home_directory."/.ssh/id_rsa";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
user => $user,
home_directory => $home_directory,
public_key => $public_key,
private_key => $private_key,
}});
if ($ssh_fh->auth_publickey($user, $public_key, $private_key))
{
# We're in! Record the file handle for this target.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = $ssh_fh;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Log that we got in without a password.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "log_0062", variables => { target => $target }});
}
else
{
# This is for the user
$error = $anvil->Words->string({key => "message_0006", variables => { target => $target }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "message_0006", variables => { target => $target }});
}
}
else
{
# We're in! Record the file handle for this target.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = $ssh_fh;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Record our success
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0007", variables => { target => $target }});
}
}
}
### Special thanks to Rafael Kitover (rkitover@gmail.com), maintainer of Net::SSH2, for helping me
### sort out the polling and data collection in this section.
#
# Open a channel and make the call.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
error => $error,
ssh_fh => $ssh_fh,
}});
if (($ssh_fh =~ /^Net::SSH2/) && (not $error))
{
# We need to open a channel every time for 'exec' calls. We want to keep blocking off, but we
# need to enable it for the channel() call.
$ssh_fh->blocking(1);
my $channel = $ssh_fh->channel();
$ssh_fh->blocking(0);
# Make the shell call
if (not $channel)
{
# ... or not.
$ssh_fh = "";
$error = $anvil->Words->string({key => "message_0008", variables => { target => $target }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "message_0008", variables => { target => $target }});
}
else
{
### TODO: Timeout if the call doesn't respond in X seconds, closing the filehandle if hit.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => {
channel => $channel,
shell_call => $shell_call,
}});
$channel->exec("$shell_call");
# This keeps the connection open when the remote side is slow to return data, like in
# '/etc/init.d/rgmanager stop'.
my @poll = {
handle => $channel,
events => [qw/in err/],
};
# We'll store the STDOUT and STDERR data here.
my $stdout = "";
my $stderr = "";
# Not collect the data.
while(1)
{
$ssh_fh->poll(250, \@poll);
# Read in anything from STDOUT
while($channel->read(my $chunk, 80))
{
$stdout .= $chunk;
}
while ($stdout =~ s/^(.*)\n//)
{
my $line = $1;
$line =~ s/\r//g; # Remove \r from things like output of daemon start/stops.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { "STDOUT:line" => $line }});
push @{$stdout_output}, $line;
}
# Read in anything from STDERR
while($channel->read(my $chunk, 80, 1))
{
$stderr .= $chunk;
}
while ($stderr =~ s/^(.*)\n//)
{
my $line = $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { "STDERR:line" => $line }});
push @{$stderr_output}, $line;
}
# Exit when we get the end-of-file.
last if $channel->eof;
}
if ($stdout)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { stdout => $stdout }});
push @{$stdout_output}, $stdout;
}
if ($stderr)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { stderr => $stderr }});
push @{$stderr_output}, $stderr;
}
}
}
# Merge the STDOUT and STDERR
my $output = [];
foreach my $line (@{$stderr_output}, @{$stdout_output})
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { line => $line }});
push @{$output}, $line;
}
# Close the connection if requested.
if ($close)
{
if ($ssh_fh)
{
# Close it.
$ssh_fh->disconnect();
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0009", variables => { target => $target }});
}
# For good measure, blank both variables.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = "";
$ssh_fh = "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
}
$error = "" if not defined $error;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => {
error => $error,
ssh_fh => $ssh_fh,
output => $output,
}});
return($error, $output);
};
# =head3
#
# Private Functions;
#
# =cut
#############################################################################################################
# Private functions #
#############################################################################################################
=head2 _call_ssh_keyscan
This calls C<< ssh-keyscan >> to add a remote machine's fingerprint to the C<< user >>'s C<< known_hosts >> file.
Returns C<< 0 >> if the addition failed, returns C<< 1 >> if it was successful.
Parameters;
=head3 known_hosts (required)
This is the specific C<< known_hosts >> file we're checking.
=head3 port (optional, default 22)
This is the SSH TCP port used to connect to C<< target >>.
=head3 target (required)
This is the IP or (resolvable) host name of the machine who's RSA fingerprint we're checking.
=head3 user (optional, default to user running this method)
This is the user who's C<< known_hosts >> we're checking.
=cut
sub _call_ssh_keyscan
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $known_hosts = defined $parameter->{known_hosts} ? $parameter->{known_hosts} : "";
my $port = defined $parameter->{port} ? $parameter->{port} : "";
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : $<;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
known_hosts => $known_hosts,
port => $port,
target => $target,
user => $user,
}});
# Log what we're doing
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0159", variables => {
target => $target,
port => $port,
user => $user,
}});
my $shell_call = $anvil->data->{path}{exe}{'ssh-keyscan'}." ".$target." >> ".$known_hosts;
if (($port) && ($port ne "22"))
{
$shell_call = $anvil->data->{path}{exe}{'ssh-keyscan'}." -p ".$port." ".$target." >> ".$known_hosts;
}
my $output = $anvil->System->call({shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
}
# Set the ownership
$output = "";
$shell_call = $anvil->data->{path}{exe}{'chown'}." ".$user.":".$user." ".$known_hosts;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
}
# Verify that it's now there.
my $known_machine = $anvil->Remote->_check_known_hosts_for_target({
target => $target,
port => $port,
known_hosts => $known_hosts,
user => $user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { known_machine => $known_machine }});
return($known_machine);
}
=head3 _check_known_hosts_for_target
This checks to see if a given C<< target >> machine is in the C<< user >>'s C<< known_hosts >> file.
Returns C<< 0 >> if the target is not in the C<< known_hosts >> file, C<< 1 >> if it was found.
Parameters;
=head3 delete_if_found (optional, default 0)
Deletes the existing RSA fingerprint if one is found for the C<< target >>.
=head3 known_hosts (required)
This is the specific C<< known_hosts >> file we're checking.
=head3 port (optional, default 22)
This is the SSH TCP port used to connect to C<< target >>.
=head3 target (required)
This is the IP or (resolvable) host name of the machine who's RSA fingerprint we're checking.
=head3 user (optional, default to user running this method)
This is the user who's C<< known_hosts >> we're checking.
=cut
sub _check_known_hosts_for_target
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $delete_if_found = defined $parameter->{delete_if_found} ? $parameter->{delete_if_found} : 0;
my $known_hosts = defined $parameter->{known_hosts} ? $parameter->{known_hosts} : "";
my $port = defined $parameter->{port} ? $parameter->{port} : "";
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : $<;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
delete_if_found => $delete_if_found,
known_hosts => $known_hosts,
port => $port,
target => $target,
user => $user,
}});
# read it in and search.
my $known_machine = 0;
my $body = $anvil->Storage->read_file({file => $known_hosts});
foreach my $line (split/\n/, $body)
{
my $line = $_;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { line => $line }});
if (($line =~ /$target ssh-rsa /) or ($line =~ /\[$target\]:$port ssh-rsa /))
{
# We already know this machine (or rather, we already have a fingerprint for
# this machine).
$known_machine = 1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { known_machine => $known_machine }});
}
}
# If we know of this machine and we've been asked to remove it, do so.
if (($delete_if_found) && ($known_machine))
{
### NOTE: It appears the port is not needed.
# If we have a non-digit user, run this through 'su.
my $shell_call = $anvil->data->{path}{exe}{'ssh-keygen'}." -R ".$target;
if (($user) && ($user =~ /\D/))
{
$shell_call = $anvil->data->{path}{exe}{su}." - ".$user." -c '".$anvil->data->{path}{exe}{'ssh-keygen'}." -R ".$target."'";
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { shell_call => $shell_call }});
my $output = $anvil->System->call({shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
}
# Mark the machine as no longer known.
$known_machine = 0;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { known_machine => $known_machine }});
}
return($known_machine);
}
1;

@ -23,8 +23,10 @@ my $THIS_FILE = "Storage.pm";
# read_file
# read_mode
# record_md5sums
# rsync
# search_directories
# write_file
# _create_rsync_wrapper
=pod
@ -831,12 +833,36 @@ This is an optional parameter that controls whether the file is cached in case s
=head3 file (required)
This is the name of the file to read.
This is the name of the file to read. When reading from a remote machine, it must be a full path and file name.
=head3 force_read (optional)
This is an otpional parameter that, if set, forces the file to be read, bypassing cache if it exists. Set this to C<< 1 >> to bypass the cache.
=head3 password (optional)
If C<< target >> is set, this is the password used to log into the remote system as the C<< remote_user >>. If it is not set, an attempt to connect without a password will be made (though this will usually fail).
=head3 port (optional, default 22)
If C<< target >> is set, this is the TCP port number used to connect to the remote machine.
=head3 remote_user (optional)
If C<< target >> is set, this is the user account that will be used when connecting to the remote system.
=head3 secure (optional, default 0)
If set to C<< 1 >>, the body of the read file will be treated as sensitive from a logging perspective.
=head3 target (optional)
If set, the file will be read from the target machine. This must be either an IP address or a resolvable host name.
The file will be copied to the local system using C<< $anvil->Storage->rsync() >> and stored in C<< /tmp/<file_path_and_name>.<target> >>. if C<< cache >> is set, the file will be preserved locally. Otherwise it will be deleted once it has been read into memory.
B<< Note >>: the temporary file will be prefixed with the path to the file name, with the C<< / >> converted to C<< _ >>.
=cut
sub read_file
{
@ -845,14 +871,24 @@ sub read_file
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $body = "";
my $cache = defined $parameter->{cache} ? $parameter->{cache} : 1;
my $file = defined $parameter->{file} ? $parameter->{file} : "";
my $force_read = defined $parameter->{force_read} ? $parameter->{force_read} : 0;
my $body = "";
my $cache = defined $parameter->{cache} ? $parameter->{cache} : 1;
my $file = defined $parameter->{file} ? $parameter->{file} : "";
my $force_read = defined $parameter->{force_read} ? $parameter->{force_read} : 0;
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $remote_user = defined $parameter->{remote_user} ? $parameter->{remote_user} : "";
my $secure = defined $parameter->{secure} ? $parameter->{secure} : 0;
my $target = defined $parameter->{target} ? $parameter->{target} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
cache => $cache,
file => $file,
force_read => $force_read,
port => $port,
password => $anvil->Log->secure ? $password : "--",
remote_user => $remote_user,
secure => $secure,
target => $target,
}});
if (not $file)
@ -860,44 +896,129 @@ sub read_file
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->read_file()", parameter => "file" }});
return("!!error!!");
}
elsif (not -e $file)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0021", variables => { file => $file }});
return("!!error!!");
}
elsif (not -r $file)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0022", variables => { file => $file }});
return("!!error!!");
}
# If I've read this before, don't read it again.
if ((exists $anvil->data->{cache}{file}{$file}) && (not $force_read))
# Reading locally or remote?
if ($target)
{
# Use the cache
$body = $anvil->data->{cache}{file}{$file};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { body => $body }});
# Remote. Make sure the passed file is a full path and file name.
if ($file !~ /^\/\w/)
{
# Not a fully defined path, abort.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0160", variables => { file => $file }});
return("!!error!!");
}
if ($file =~ /\/$/)
{
# The file name is missing.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0161", variables => { file => $file }});
return("!!error!!");
}
# Setup the temp file name.
my $temp_file = $file;
$temp_file =~ s/\//_/g;
$temp_file = "/tmp/".$temp_file.".".$target;
$temp_file =~ s/\s+/_/g;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { temp_file => $temp_file }});
# If the temp file exists and 'force_read' is set, remove it.
if (($force_read) && (-e $temp_file))
{
unlink $temp_file;
}
# Do we have this cached?
if ((exists $anvil->data->{cache}{file}{$temp_file}) && (not $force_read))
{
# Use the cache
$body = $anvil->data->{cache}{file}{$temp_file};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { body => $body }});
}
else
{
# Read from the target by rsync'ing the file here.
$anvil->Storage->rsync({
destination => $temp_file,
password => $password,
port => $port,
source => $remote_user."\@".$target.$file,
});
if (-e $temp_file)
{
# Got it! read it in.
my $shell_call = $temp_file;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0012", variables => { shell_call => $shell_call }});
open (my $file_handle, "<", $shell_call) or $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0015", variables => { shell_call => $shell_call, error => $! }});
while(<$file_handle>)
{
chomp;
my $line = $_;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0023", variables => { line => $line }});
$body .= $line."\n";
}
close $file_handle;
$body =~ s/\n$//s;
if ($cache)
{
$anvil->data->{cache}{file}{$temp_file} = $body;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cache::file::${temp_file}" => $anvil->data->{cache}{file}{$temp_file} }});
}
}
else
{
# Something went wrong...
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0162", variables => {
remote_file => $remote_user."\@".$target.$file,
local_file => $temp_file,
}});
return("!!error!!");
}
}
}
else
{
# Read from disk.
my $shell_call = $file;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0012", variables => { shell_call => $shell_call }});
open (my $file_handle, "<", $shell_call) or $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0015", variables => { shell_call => $shell_call, error => $! }});
while(<$file_handle>)
# Local
if (not -e $file)
{
chomp;
my $line = $_;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0023", variables => { line => $line }});
$body .= $line."\n";
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0021", variables => { file => $file }});
return("!!error!!");
}
elsif (not -r $file)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0022", variables => { file => $file }});
return("!!error!!");
}
close $file_handle;
$body =~ s/\n$//s;
if ($cache)
# If I've read this before, don't read it again.
if ((exists $anvil->data->{cache}{file}{$file}) && (not $force_read))
{
$anvil->data->{cache}{file}{$file} = $body;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cache::file::$file" => $anvil->data->{cache}{file}{$file} }});
# Use the cache
$body = $anvil->data->{cache}{file}{$file};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { body => $body }});
}
else
{
# Read from disk.
my $shell_call = $file;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0012", variables => { shell_call => $shell_call }});
open (my $file_handle, "<", $shell_call) or $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0015", variables => { shell_call => $shell_call, error => $! }});
while(<$file_handle>)
{
chomp;
my $line = $_;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0023", variables => { line => $line }});
$body .= $line."\n";
}
close $file_handle;
$body =~ s/\n$//s;
if ($cache)
{
$anvil->data->{cache}{file}{$file} = $body;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cache::file::${file}" => $anvil->data->{cache}{file}{$file} }});
}
}
}
@ -1008,6 +1129,212 @@ sub record_md5sums
return(0);
}
=head2 rsync
This method copies a file or directory (and its contents) to a remote machine using C<< rsync >> and an C<< expect >> wrapper.
This supports the source B<< or >> the destination being remote, so the C<< source >> or C<< destination >> paramter can be in the format C<< <remote_user>@<target>:/file/path >>. If neither parameter is remove, a local C<< rsync >> operation will be performed.
On success, C<< 0 >> is returned. If a problem arises, C<< 1 >> is returned.
B<< NOTE >>: This method does not take C<< remote_user >> or C<< target >>. These are parsed off the C<< source >> or C<< destination >> parameter.
Parameters;
=head3 destination (required)
This is the source being copied. Be careful with the closing C<< / >>! Generally you will always want to have the destination end in a closing slash, to ensure the files go B<< under >> the estination directory. The same as is the case when using C<< rsync >> directly.
=head3 password (optional)
This is the password used to connect to the target machine (if either the source or target is remote).
=head3 port (optional, default 22)
This is the TCP port used to connect to the target machine.
=head3 source (required)
The source can be a directory, or end in a wildcard (ie: C<< .../* >>) to copy multiple files/directories at the same time.
=head3 switches (optional, default -av)
These are the switches to pass to C<< rsync >>. If you specify this and you still want C<< -avS >>, be sure to include it. This parameter replaces the default.
B<< NOTE >>: If C<< port >> is specified, C<< -e 'ssh -p <port> >> will be appended automatically, so you do not need to specify this.
=head3 try_again (optional, default 1)
If this is set to C<< 1 >>, and if a conflict is found with the SSH RSA key (C<< Offending key in... >> error) when trying the C<< rsync >> call, the offending key will be removed and a second attempt will be made. On the second attempt, this is set to C<< 0 >> to prevent a recursive loop if the removal fails.
B<< NOTE >>: This is the default to better handle a rebuilt node, dashboard or DR machine. Of course, this is a possible security problem so please consider it's use on a case by case basis.
=cut
sub rsync
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
# Check my parameters.
my $destination = defined $parameter->{destination} ? $parameter->{destination} : "";
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $source = defined $parameter->{source} ? $parameter->{source} : "";
my $switches = defined $parameter->{switches} ? $parameter->{switches} : "-avS";
my $try_again = defined $parameter->{try_again} ? $parameter->{try_again} : 1;
my $remote_user = "";
my $target = "";
my $failed = 0;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
destination => $destination,
password => $anvil->Log->secure ? $password : "--",
port => $port,
source => $source,
switches => $switches,
try_again => $try_again,
}});
# Add an argument for the port if set
if ($port ne "22")
{
$switches .= " -e 'ssh -p $port'";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { switches => $switches }});
}
# Make sure I have everything I need.
if (not $source)
{
# No source
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->rsync()", parameter => "source" }});
return(1);
}
if (not $destination)
{
# No destination
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->rsync()", parameter => "destination" }});
return(1);
}
# If either the source or destination is remote, we need to make sure we have the remote machine in
# the current user's ~/.ssh/known_hosts file.
if ($source =~ /^(.*?)@(.*?):/)
{
$remote_user = $1;
$target = $2;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
remote_user => $remote_user,
target => $target,
}});
}
elsif ($destination =~ /^(.*?)@(.*?):/)
{
$remote_user = $1;
$target = $2;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
remote_user => $remote_user,
target => $target,
}});
}
# If local, call rsync directly. If remote, setup the rsync wrapper
my $wrapper_script = "";
my $shell_call = $anvil->data->{path}{exe}{rsync}." ".$switches." ".$source." ".$destination;
if ($target)
{
# If we didn't get a port, but the target is pre-configured for a port, use it.
if ((not $parameter->{port}) && ($anvil->data->{hosts}{$target}{port}))
{
$port = $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { port => $port }});
}
# Make sure we know the fingerprint of the remote machine
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, key => "log_0158", variables => { target => $target }});
$anvil->Remote->add_target_to_known_hosts({
target => $target,
user => $<,
});
# Remote target, wrapper needed.
$wrapper_script = $anvil->Storage->_create_rsync_wrapper({
target => $target,
password => $password,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { wrapper_script => $wrapper_script }});
# And make the shell call
$shell_call = $wrapper_script." ".$switches." ".$source." ".$destination;
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { shell_call => $shell_call }});
# Now make the call
my $conflict = "";
my $output = $anvil->System->call({shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /Offending key in (\/.*\/).ssh\/known_hosts:(\d+)$/)
{
### TODO: I'm still mixed on taking this behaviour... a trade off between useability
### and security... As of now, the logic for doing it is that the BCN should
### be isolated and secured so favour usability.
# Need to delete the old key or warn the user.
my $path = $1;
my $line_number = $2;
$failed = 1;
my $source = $path.".ssh\/known_hosts";
my $destination = $path."known_hosts.".$anvil->Get->date_and_time({file_name => 1});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
path => $path,
line_number => $line_number,
failed => $failed,
source => $source,
destination => $destination,
}});
if ($line_number)
{
$conflict = $anvil->data->{path}{exe}{cp}." ".$source." ".$destination." && ".$anvil->data->{path}{exe}{sed}." -ie '".$line_number."d' ".$source;
}
}
}
# If there was a conflict, clear it and try again.
if (($conflict) && ($try_again))
{
# Remove the conflicting fingerprint.
my $output = $anvil->System->call({shell_call => $conflict});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
}
# Try again.
$failed = $anvil->Storage->rsync({
destination => $destination,
password => $password,
port => $port,
source => $source,
switches => $switches,
try_again => 0,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { failed => $failed }});
}
# Clean up the rsync wrapper, if appropriate.
if (($wrapper_script) && (-e $wrapper_script))
{
unlink $wrapper_script;
}
return($failed);
}
=head2 search_directories
This method returns an array reference of directories to search within for files and directories.
@ -1116,9 +1443,15 @@ sub search_directories
=head2 write_file
This writes out a file on the local system. It can optionally set the mode as well.
This writes out a file, either locally or on a remote system. It can optionally set the ownership and mode as well.
$anvil->Storage->write_file({file => "/tmp/foo", body => "some data", mode => 0644});
$anvil->Storage->write_file({
file => "/tmp/foo",
body => "some data",
user => "admin",
group => "admin",
mode => "0644",
});
If it fails to write the file, an alert will be logged.
@ -1140,20 +1473,38 @@ This is the group name or group ID to set the ownership of the file to.
=head3 mode (optional)
This is the numeric mode to set on the file. It expects four digits to cover the sticky bit, but will work with three digits.
This is the B<< quoted >> numeric mode to set on the file. It expects four digits to cover the sticky bit, but will work with three digits.
=head3 overwrite (optional)
Normally, if the file already exists, it won't be overwritten. Setting this to 'C<< 1 >>' will cause the file to be overwritten.
=head3 port (optional, default 22)
If C<< target >> is set, this is the TCP port number used to connect to the remote machine.
=head3 password (optional)
If C<< target >> is set, this is the password used to log into the remote system as the C<< remote_user >>. If it is not set, an attempt to connect without a password will be made (though this will usually fail).
=head3 secure (optional)
If set to 'C<< 1 >>', the body is treated as containing secure data for logging purposes.
=head3 target (optional)
If set, the file will be written on the target machine. This must be either an IP address or a resolvable host name.
The file will be written locally in C<< /tmp/<file_name> >>, C<< $anvil->Storage->rsync() >> will be used to copy the file, and finally the local temprary copy will be removed.
=head3 user (optional)
This is the user name or user ID to set the ownership of the file to.
=head3 remote_user (optional)
If C<< target >> is set, this is the user account that will be used when connecting to the remote system.
=cut
sub write_file
{
@ -1162,21 +1513,29 @@ sub write_file
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $body = defined $parameter->{body} ? $parameter->{body} : "";
my $file = defined $parameter->{file} ? $parameter->{file} : "";
my $group = defined $parameter->{group} ? $parameter->{group} : "";
my $mode = defined $parameter->{mode} ? $parameter->{mode} : "";
my $overwrite = defined $parameter->{overwrite} ? $parameter->{overwrite} : 0;
my $secure = defined $parameter->{secure} ? $parameter->{secure} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : "";
my $body = defined $parameter->{body} ? $parameter->{body} : "";
my $file = defined $parameter->{file} ? $parameter->{file} : "";
my $group = defined $parameter->{group} ? $parameter->{group} : "";
my $mode = defined $parameter->{mode} ? $parameter->{mode} : "";
my $overwrite = defined $parameter->{overwrite} ? $parameter->{overwrite} : 0;
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $secure = defined $parameter->{secure} ? $parameter->{secure} : "";
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : "root";
my $remote_user = defined $parameter->{remote_user} ? $parameter->{remote_user} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => $secure, list => {
body => $body,
file => $file,
group => $group,
mode => $mode,
overwrite => $overwrite,
secure => $secure,
user => $user,
body => $body,
file => $file,
group => $group,
mode => $mode,
overwrite => $overwrite,
port => $port,
password => $anvil->Log->secure ? $password : "--",
secure => $secure,
target => $target,
user => $user,
remote_user => $remote_user,
}});
# Make sure the user and group and just one digit or word.
@ -1188,64 +1547,185 @@ sub write_file
}});
my $error = 0;
if ((-e $file) && (not $overwrite))
{
# Nope.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0040", variables => { file => $file }});
$error = 1;
}
# Make sure the passed file is a full path and file name.
if ($file !~ /^\/\w/)
{
# Not a fully defined path, abort.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0041", variables => { file => $file }});
$error = 1;
}
if (not $error)
if ($file =~ /\/$/)
{
# Break the directory off the file.
my ($directory, $file_name) = ($file =~ /^(\/.*)\/(.*)$/);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
directory => $directory,
file_name => $file_name,
}});
# The file name is missing.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0157", variables => { file => $file }});
$error = 1;
}
if (not -e $directory)
# Break the directory off the file.
my ($directory, $file_name) = ($file =~ /^(\/.*)\/(.*)$/);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
directory => $directory,
file_name => $file_name,
}});
# Now, are we writing locally or on a remote system?
if ($target)
{
# If we didn't get a port, but the target is pre-configured for a port, use it.
if ((not $parameter->{port}) && ($anvil->data->{hosts}{$target}{port}))
{
# Don't pass the mode as the file's mode is likely not executable.
$anvil->Storage->make_directory({
directory => $directory,
group => $group,
user => $user,
});
$port = $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { port => $port }});
}
# If 'secure' is set, the file will probably contain sensitive data so touch the file and set
# the mode before writing it.
if ($secure)
# Remote. See if the file exists on the remote system (and that we can connect to the remote
# system).
my $shell_call = "
if [ -e '".$file."' ];
then
".$anvil->data->{path}{exe}{echo}." 'exists';
else
".$anvil->data->{path}{exe}{echo}." 'not found';
fi";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
($error, my $output) = $anvil->Remote->call({
target => $target,
user => $remote_user,
password => $password,
shell_call => $shell_call,
});
if (not $error)
{
my $shell_call = $anvil->data->{path}{exe}{touch}." ".$file;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
$anvil->System->call({shell_call => $shell_call});
$anvil->Storage->change_mode({target => $file, mode => $mode});
}
# No error. Did the file exist?
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'output->[0]' => $output->[0] }});
if (($output->[0] eq "exists") && (not $overwrite))
{
# Abort, we're not allowed to overwrite.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0040", variables => { file => $file }});
$error = 1;
}
# Now write the file.
my $shell_call = $file;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, secure => $secure, key => "log_0013", variables => { shell_call => $shell_call }});
open (my $file_handle, ">", $shell_call) or $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, secure => $secure, priority => "err", key => "log_0016", variables => { shell_call => $shell_call, error => $! }});
print $file_handle $body;
close $file_handle;
# Make sure the directory exists on the remote machine. In this case, we'll use 'mkdir -p' if it isn't.
if (not $error)
{
my $shell_call = "
if [ -d '".$directory."' ];
then
".$anvil->data->{path}{exe}{echo}." 'exists';
else
".$anvil->data->{path}{exe}{echo}." 'not found';
fi";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
($error, my $output) = $anvil->Remote->call({
target => $target,
user => $remote_user,
password => $password,
shell_call => $shell_call,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'output->[0]' => $output->[0] }});
if ($output->[0] eq "not found")
{
# Create the directory
my $shell_call = $anvil->data->{path}{exe}{'mkdir'}." -p ".$directory;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
($error, my $output) = $anvil->Remote->call({
target => $target,
user => $remote_user,
password => $password,
shell_call => $shell_call,
});
}
if ($mode)
if (not $error)
{
# OK, now write the file locally, then we'll rsync it over.
my $temp_file = $file;
$temp_file =~ s/\//_/g;
$temp_file = "/tmp/".$temp_file;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { temp_file => $temp_file }});
$anvil->Storage->write_file({
body => $body,
debug => $debug,
file => $temp_file,
group => $group,
mode => $mode,
overwrite => 1,
secure => $secure,
user => $user,
});
# Now rsync it.
if (-e $temp_file)
{
$anvil->Storage->rsync({
destination => $remote_user."\@".$target.$file,
password => $password,
port => $port,
source => $temp_file,
});
# Unlink
unlink $temp_file;
}
else
{
# Something went wrong writing it.
$error = 1;
}
}
}
}
}
else
{
# Local
if ((-e $file) && (not $overwrite))
{
$anvil->Storage->change_mode({target => $file, mode => $mode});
# Nope.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0040", variables => { file => $file }});
$error = 1;
}
if (($user) or ($group))
if (not $error)
{
$anvil->Storage->change_owner({target => $file, user => $user, group => $group});
if (not -e $directory)
{
# Don't pass the mode as the file's mode is likely not executable.
$anvil->Storage->make_directory({
directory => $directory,
group => $group,
user => $user,
});
}
# If 'secure' is set, the file will probably contain sensitive data so touch the file and set
# the mode before writing it.
if ($secure)
{
my $shell_call = $anvil->data->{path}{exe}{touch}." ".$file;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }});
$anvil->System->call({shell_call => $shell_call});
$anvil->Storage->change_mode({target => $file, mode => $mode});
}
# Now write the file.
my $shell_call = $file;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, secure => $secure, key => "log_0013", variables => { shell_call => $shell_call }});
open (my $file_handle, ">", $shell_call) or $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, secure => $secure, priority => "err", key => "log_0016", variables => { shell_call => $shell_call, error => $! }});
print $file_handle $body;
close $file_handle;
if ($mode)
{
$anvil->Storage->change_mode({target => $file, mode => $mode});
}
if (($user) or ($group))
{
$anvil->Storage->change_owner({target => $file, user => $user, group => $group});
}
}
}
@ -1263,4 +1743,79 @@ sub write_file
# Private functions #
#############################################################################################################
=head2
This does the actual work of creating the C<< expect >> wrapper script and returns the path to that wrapper for C<< rsync >> calls.
If there is a problem, an empty string will be returned.
Parameters;
=head3 target (required)
This is the IP address or (resolvable) hostname of the remote machine.
=head3 password (required)
This is the password of the user you will be connecting to the remote machine as.
=cut
sub _create_rsync_wrapper
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
# Check my parameters.
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $password = defined $parameter->{password} ? $parameter->{password} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
password => $anvil->Log->secure ? $password : "--",
target => $target,
}});
if (not $target)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->_create_rsync_wrapper()", parameter => "target" }});
return("");
}
if (not $password)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->_create_rsync_wrapper()", parameter => "password" }});
return("");
}
my $timeout = 3600;
my $wrapper_script = "/tmp/rsync.$target";
my $wrapper_body = "
".$anvil->data->{path}{exe}{echo}." #!".$anvil->data->{path}{exe}{expect}."
".$anvil->data->{path}{exe}{echo}." set timeout ".$timeout."
".$anvil->data->{path}{exe}{echo}." eval spawn rsync \$argv
".$anvil->data->{path}{exe}{echo}." expect \"password:\" \{ send \"".$password."\\n\" \}
".$anvil->data->{path}{exe}{echo}." expect eof
";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
wrapper_script => $wrapper_script,
wrapper_body => $wrapper_body,
}});
$anvil->Storage->write_file({
body => $wrapper_body,
debug => $debug,
file => $wrapper_script,
mode => "0700",
overwrite => 1,
secure => 1,
});
if (not -e $wrapper_script)
{
# Failed!
$wrapper_script = "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { wrapper_script => $wrapper_script }});
}
return($wrapper_script);
}
1;

@ -15,6 +15,7 @@ my $THIS_FILE = "System.pm";
### Methods;
# call
# change_shell_user_password
# check_daemon
# check_memory
# determine_host_type
@ -25,7 +26,6 @@ my $THIS_FILE = "System.pm";
# manage_firewall
# ping
# read_ssh_config
# remote_call
# reload_daemon
# start_daemon
# stop_daemon
@ -186,6 +186,125 @@ sub call
return($output);
}
=head2 change_shell_user_password
This changes the password for a shell user account. It can change the password on either the local or a remote machine.
The return code will be C<< 255 >> on internal error. Otherwise, it will be the code returned from the C<< passwd >> call.
B<< Note >>; The password is salted and (sha-512, C<< $6$<salt>$<hash>$ >>
Parameters;
=head3 new_password (required)
This is the new password to set. The user should be encouraged to select a good (long) password.
=head3 password (optional)
If you are changing the password of a user on a remote machine, this is the password used to connect to that machine. If not passed, an attempt to connect with passwordless SSH will be made (but this won't be the case in most instances). Ignored if C<< target >> is not given.
=head3 port (optional, default 22)
This is the TCP port number to use if connecting to a remote machine over SSH. Ignored if C<< target >> is not given.
=head3 target (optional)
This is the IP address or (resolvable) host name of the target machine whose user account you want to change the password
=head3 user (required)
This is the user name whose password is being changed.
=cut
sub change_shell_user_password
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $new_password = $parameter->{new_password} ? $parameter->{new_password} : "";
my $password = $parameter->{password} ? $parameter->{password} : "";
my $port = $parameter->{port} ? $parameter->{port} : "";
my $target = $parameter->{target} ? $parameter->{target} : "";
my $user = $parameter->{user} ? $parameter->{user} : "";
my $return_code = 255;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
user => $user,
target => $target,
port => $port,
new_password => $anvil->Log->secure ? $new_password : "--",
password => $anvil->Log->secure ? $password : "--",
}});
# Do I have a user?
if (not $user)
{
# Woops!
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->change_shell_user_password()", parameter => "user" }});
return($return_code);
}
# OK, what about a password?
if (not $new_password)
{
# Um...
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->change_shell_user_password()", parameter => "new_password" }});
return($return_code);
}
# Only the root user can do this!
# $< == real UID, $> == effective UID
if (($< != 0) && ($> != 0))
{
# Not root
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0156", variables => { method => "Systeme->change_shell_user_password()" }});
return($return_code);
}
# Generate a salt and then use it to create a hash.
my $salt = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{openssl}." rand 1000 | ".$anvil->data->{path}{exe}{strings}." | ".$anvil->data->{path}{exe}{'grep'}." -io [0-9A-Za-z\.\/] | ".$anvil->data->{path}{exe}{head}." -n 16 | ".$anvil->data->{path}{exe}{'tr'}." -d '\n'" });
my $new_hash = $user.":".crypt($new_password,"\$6\$".$salt."\$");
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => {
salt => $salt,
new_hash => $new_hash,
}});
# Update the password using 'usermod'. NOTE: The single-quotes are crtical!
my $output = "";
my $shell_call = $anvil->data->{path}{exe}{usermod}." --password '".$new_hash."'; ".$anvil->data->{path}{exe}{'echo'}." return_code:\$?";
if ($target)
{
# Remote call.
$output = $anvil->Remote->call({
shell_call => $shell_call,
target => $target,
port => $port,
password => $password,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
}
else
{
# Local call
$output = $anvil->System->call({shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
}
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /^return_code:(\d+)$/)
{
$return_code = $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { return_code => $return_code }});
}
}
return($return_code);
}
=head2 check_daemon
This method checks to see if a daemon is running or not. If it is, it returns 'C<< 1 >>'. If the daemon isn't running, it returns 'C<< 0 >>'. If the daemon wasn't found, 'C<< 2 >>' is returned.
@ -213,6 +332,8 @@ sub check_daemon
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output }});
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /return_code:(\d+)/)
{
my $return_code = $1;
@ -1045,7 +1166,7 @@ B<NOTE>: The payload will have 28 bytes removed to account for ICMP overhead. So
This is the port used to access a remote machine. This is used when pinging from a remote machine to a given ping target.
B<NOTE>: See C<< System->remote_call >> for additional information on specifying the SSH port as part of the target.
B<NOTE>: See C<< Remote->call >> for additional information on specifying the SSH port as part of the target.
=head3 target (optional)
@ -1137,7 +1258,7 @@ sub ping
if (($target) && ($target ne "local") && ($target ne $anvil->_hostname) && ($target ne $anvil->_short_hostname))
{
### Remote calls
$output = $anvil->System->remote_call({
$output = $anvil->Remote->call({
shell_call => $shell_call,
target => $target,
port => $port,
@ -1236,457 +1357,6 @@ sub read_ssh_config
return(0);
}
=head2 remote_call
This does a remote call over SSH. The connection is held open and the file handle for the target is cached and re-used unless a specific ssh_fh is passed or a request to close the connection is received.
Example;
# Call 'hostname' on a node.
my ($error, $output) = $anvil->System->remote_call({
target => "an-a01n01.alteeve.com",
user => "admin",
password => "super secret password",
shell_call => "/usr/bin/hostname",
});
# Make a call with sensitive data that you want logged only if $anvil->Log->secure is set and close the
# connection when done.
my ($error, $output) = $anvil->System->remote_call({
target => "an-a01n01.alteeve.com",
user => "root",
password => "super secret password",
shell_call => "/usr/sbin/fence_ipmilan -a an-a01n02.ipmi -l admin -p \"super secret password\" -o status",
secure => 1,
close => 1,
});
B<NOTE>: By default, a connection to a target will be held open and cached to increase performance for future connections.
Parameters;
=head3 close (optional, default '0')
If set, the connection to the target will be closed at the end of the call.
=head3 log_level (optional, default C<< 3 >>)
If set, the method will use the given log level. Valid values are integers between C<< 0 >> and C<< 4 >>.
=head3 no_cache (optional, default C<< 0 >>)
If set, and if an existing cached connection is open, it will be closed and a new connection to the target will be established.
=head3 password (optional)
This is the password used to connect to the remote target as the given user.
B<NOTE>: Passwordless SSH is supported. If you can ssh to the target as the given user without a password, then no password needs to be given here.
=head3 port (optional, default C<< 22 >>)
This is the TCP port to use when connecting to the C<< target >>. The default is port 22.
B<NOTE>: See C<< target >> for optional port definition.
=head3 secure (optional, default C<< 0 >>)
If set, the C<< shell_call >> is treated as containing sensitive data and will not be logged unless C<< $anvil->Log->secure >> is enabled.
=head3 shell_call (required)
This is the command to run on the target machine as the target user.
=head3 target (required)
This is the host name or IP address of the target machine that the C<< shell_call >> will be run on.
B<NOTE>: If the target matches an entry in '/etc/ssh/ssh_config', the port defined there is used. If the port is set as part of the target name, the port in 'ssh_config' is ignored.
B<NOTE>: If the C<< target >> is presented in the format C<< target:port >>, the port will be separated from the target and used as the TCP port. If the C<< port >> parameter is set, however, the port split off the C<< target >> will be ignored.
=head3 user (optional, default 'root')
This is the user account on the C<< target >> to connect as and to run the C<< shell_call >> as. The C<< password >> if so this user's account on the C<< target >>.
=cut
sub remote_call
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
# Get the target and port so that we can create the ssh_fh key
my $log_level = defined $parameter->{log_level} ? $parameter->{log_level} : 3;
if (($log_level !~ /^\d$/) or ($log_level < 0) or ($log_level > 4))
{
# Invalid log level, set 2.
$log_level = 3;
}
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $target = defined $parameter->{target} ? $parameter->{target} : "";
my $ssh_fh_key = $target.":".$port;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
port => $port,
target => $target,
}});
# This will store the SSH file handle for the given target after the initial connection.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = defined $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} ? $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Now pick up the rest of the variables.
my $close = defined $parameter->{'close'} ? $parameter->{'close'} : 0;
my $no_cache = defined $parameter->{no_cache} ? $parameter->{no_cache} : 0;
my $password = defined $parameter->{password} ? $parameter->{password} : $anvil->data->{sys}{root_password};
my $secure = defined $parameter->{secure} ? $parameter->{secure} : 0;
my $shell_call = defined $parameter->{shell_call} ? $parameter->{shell_call} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : "root";
my $start_time = time;
my $ssh_fh = $anvil->data->{cache}{ssh_fh}{$ssh_fh_key};
# NOTE: The shell call might contain sensitive data, so we show '--' if 'secure' is set and $anvil->Log->secure is not.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
'close' => $close,
password => $anvil->Log->secure ? $password : "--",
secure => $secure,
shell_call => ((not $anvil->Log->secure) && ($secure)) ? "--" : $shell_call,
ssh_fh => $ssh_fh,
start_time => $start_time,
user => $user,
}});
if (not $shell_call)
{
# No shell call
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->remote_call()", parameter => "shell_call" }});
return("!!error!!");
}
if (not $target)
{
# No target
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->remote_call()", parameter => "target" }});
return("!!error!!");
}
if (not $user)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->remote_call()", parameter => "user" }});
return("!!error!!");
}
# If the user didn't pass a port, but there is an entry in 'hosts::<host>::port', use it.
if ((not $parameter->{port}) && ($anvil->data->{hosts}{$target}{port}))
{
$port = $anvil->data->{hosts}{$target}{port};
}
# Break out the port, if needed.
my $state;
my $error;
if ($target =~ /^(.*):(\d+)$/)
{
$target = $1;
$port = $2;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
port => $port,
target => $target,
}});
# If the user passed a port, override this.
if ($parameter->{port} =~ /^\d+$/)
{
$port = $parameter->{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
}
else
{
# In case the user is using ports in /etc/ssh/ssh_config, we'll want to check for an entry.
$anvil->System->read_ssh_config();
$anvil->data->{hosts}{$target}{port} = "" if not defined $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "hosts::${target}::port" => $anvil->data->{hosts}{$target}{port} }});
if ($anvil->data->{hosts}{$target}{port} =~ /^\d+$/)
{
$port = $anvil->data->{hosts}{$target}{port};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
}
# Make sure the port is valid.
if ($port eq "")
{
$port = 22;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
elsif ($port !~ /^\d+$/)
{
$port = getservbyname($port, 'tcp');
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { port => $port }});
}
if ((not defined $port) or (($port !~ /^\d+$/) or ($port < 0) or ($port > 65536)))
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0058", variables => { port => $port }});
return("!!error!!");
}
# If the target is a host name, convert it to an IP.
if (not $anvil->Validate->is_ipv4({ip => $target}))
{
my $new_target = $anvil->Convert->hostname_to_ip({host_name => $target});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { new_target => $new_target }});
if ($new_target)
{
$target = $new_target;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { target => $target }});
}
}
# If the user set 'no_cache', don't use any existing 'ssh_fh'.
if (($no_cache) && ($ssh_fh))
{
# Close the connection.
$ssh_fh->disconnect();
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0010", variables => { target => $target }});
# For good measure, blank both variables.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = "";
$ssh_fh = "";
}
# These will be merged into a single 'output' array before returning.
my $stdout_output = [];
my $stderr_output = [];
# If I don't already have an active SSH file handle, connect now.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { ssh_fh => $ssh_fh }});
if ($ssh_fh !~ /^Net::SSH2/)
{
### NOTE: Nevermind, timeout isn't supported... >_< Find a newer version if IO::Socket::IP?
### TODO: Make the timeout user-configurable to handle slow connections. Make it
### 'sys::timeout::{all|host} = x'
my $start_time = [gettimeofday];
$ssh_fh = Net::SSH2->new(timeout => 1000);
if (not $ssh_fh->connect($target, $port))
{
my $connect_time = tv_interval ($start_time, [gettimeofday]);
#print "[".$connect_time."] - Connection failed time to: [$target:$port]\n";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", list => {
user => $user,
target => $target,
port => $port,
shell_call => $shell_call,
error => $@,
}});
# We'll now try to get a more useful message for the user and logs.
my $message_key = "message_0005";
my $variables = { target => $target };
if ($@ =~ /Bad hostname/i)
{
$message_key = "message_0001";
}
elsif ($@ =~ /Connection refused/i)
{
$message_key = "message_0002";
$variables = {
target => $target,
port => $port,
user => $user,
};
}
elsif ($@ =~ /No route to host/)
{
$message_key = "message_0003";
}
elsif ($@ =~ /timeout/)
{
$message_key = "message_0004";
}
$error = $anvil->Words->string({key => $message_key, variables => $variables});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => $message_key, variables => $variables});
}
my $connect_time = tv_interval ($start_time, [gettimeofday]);
#print "[".$connect_time."] - Connect time to: [$target:$port]\n";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { error => $error, ssh_fh => $ssh_fh }});
if (not $error)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
user => $user,
password => $anvil->Log->secure ? $password : "--",
}});
if (not $ssh_fh->auth_password($user, $password))
{
# Can we log in without a password?
my $user = getpwuid($<);
my $home_directory = $anvil->Get->users_home({user => $user});
my $public_key = $home_directory."/.ssh/id_rsa.pub";
my $private_key = $home_directory."/.ssh/id_rsa";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
user => $user,
home_directory => $home_directory,
public_key => $public_key,
private_key => $private_key,
}});
if ($ssh_fh->auth_publickey($user, $public_key, $private_key))
{
# We're in! Record the file handle for this target.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = $ssh_fh;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Log that we got in without a password.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "log_0062", variables => { target => $target }});
}
else
{
# This is for the user
$error = $anvil->Words->string({key => "message_0006", variables => { target => $target }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "message_0006", variables => { target => $target }});
}
}
else
{
# We're in! Record the file handle for this target.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = $ssh_fh;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
# Record our success
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0007", variables => { target => $target }});
}
}
}
### Special thanks to Rafael Kitover (rkitover@gmail.com), maintainer of Net::SSH2, for helping me
### sort out the polling and data collection in this section.
#
# Open a channel and make the call.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => {
error => $error,
ssh_fh => $ssh_fh,
}});
if (($ssh_fh =~ /^Net::SSH2/) && (not $error))
{
# We need to open a channel every time for 'exec' calls. We want to keep blocking off, but we
# need to enable it for the channel() call.
$ssh_fh->blocking(1);
my $channel = $ssh_fh->channel();
$ssh_fh->blocking(0);
# Make the shell call
if (not $channel)
{
# ... or not.
$ssh_fh = "";
$error = $anvil->Words->string({key => "message_0008", variables => { target => $target }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "message_0008", variables => { target => $target }});
}
else
{
### TODO: Timeout if the call doesn't respond in X seconds, closing the filehandle if hit.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => {
channel => $channel,
shell_call => $shell_call,
}});
$channel->exec("$shell_call");
# This keeps the connection open when the remote side is slow to return data, like in
# '/etc/init.d/rgmanager stop'.
my @poll = {
handle => $channel,
events => [qw/in err/],
};
# We'll store the STDOUT and STDERR data here.
my $stdout = "";
my $stderr = "";
# Not collect the data.
while(1)
{
$ssh_fh->poll(250, \@poll);
# Read in anything from STDOUT
while($channel->read(my $chunk, 80))
{
$stdout .= $chunk;
}
while ($stdout =~ s/^(.*)\n//)
{
my $line = $1;
$line =~ s/\r//g; # Remove \r from things like output of daemon start/stops.
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { "STDOUT:line" => $line }});
push @{$stdout_output}, $line;
}
# Read in anything from STDERR
while($channel->read(my $chunk, 80, 1))
{
$stderr .= $chunk;
}
while ($stderr =~ s/^(.*)\n//)
{
my $line = $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { "STDERR:line" => $line }});
push @{$stderr_output}, $line;
}
# Exit when we get the end-of-file.
last if $channel->eof;
}
if ($stdout)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { stdout => $stdout }});
push @{$stdout_output}, $stdout;
}
if ($stderr)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { stderr => $stderr }});
push @{$stderr_output}, $stderr;
}
}
}
# Merge the STDOUT and STDERR
my $output = [];
foreach my $line (@{$stderr_output}, @{$stdout_output})
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => { line => $line }});
push @{$output}, $line;
}
# Close the connection if requested.
if ($close)
{
if ($ssh_fh)
{
# Close it.
$ssh_fh->disconnect();
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $log_level, key => "message_0009", variables => { target => $target }});
}
# For good measure, blank both variables.
$anvil->data->{cache}{ssh_fh}{$ssh_fh_key} = "";
$ssh_fh = "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, list => { "cache::ssh_fh::${ssh_fh_key}" => $anvil->data->{cache}{ssh_fh}{$ssh_fh_key} }});
}
$error = "" if not defined $error;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $log_level, secure => $secure, list => {
error => $error,
ssh_fh => $ssh_fh,
output => $output,
}});
return($error, $output);
};
=head2 reload_daemon
This method reloads a daemon (typically to pick up a change in configuration). The return code from the start request will be returned.

@ -35,6 +35,7 @@ Requires: perl-JSON
Requires: perl-Log-Journald
Requires: perl-Net-SSH2
Requires: perl-NetAddr-IP
Requires: perl-Time-HiRes
Requires: perl-XML-Simple
Requires: postgresql-contrib
Requires: postgresql-plperl

@ -39,6 +39,11 @@ Author: Madison Kelly <mkelly@alteeve.ca>
<key name="message_0015">The reconfigure of the network has begun.</key>
<key name="message_0016">The hostname: [#!variable!hostname!#] has been set.</key>
<key name="message_0017">Failed to set the hostname: [#!variable!hostname!#]! The hostname is currently [#!variable!bad_hostname!#]. This is probably a program error.</key>
<key name="message_0018">What would you like the new password to be?</key>
<key name="message_0019">Please enter the password again to confirm.</key>
<key name="message_0020">About to update the local passwords (shell users, database and web interface).</key>
<key name="message_0021">Proceed? [y/N]</key>
<key name="message_0022">Aborting.</key>
<!-- Log entries -->
<key name="log_0001">Starting: [#!variable!program!#].</key>
@ -112,7 +117,7 @@ Connecting to Database with configuration ID: [#!variable!id!#]
<key name="log_0055">Initialized PostgreSQL.</key>
<key name="log_0056">Updated: [#!variable!file!#] to listen on all interfaces.</key>
<key name="log_0057">Updated: [#!variable!file!#] to require passwords for access.</key>
<key name="log_0058"><![CDATA[[ Error ] - The method System->remote_call() was called but the port: [#!variable!port!#] is invalid. It must be a digit between '1' and '65535'.]]></key>
<key name="log_0058"><![CDATA[[ Error ] - The method Remote->call() was called but the port: [#!variable!port!#] is invalid. It must be a digit between '1' and '65535'.]]></key>
<key name="log_0059">Started the PostgreSQL database server.</key>
<key name="log_0060">Database user: [#!variable!user!#] already exists with ID: [#!variable!id!#].</key>
<key name="log_0061"><![CDATA[[ Error ] - The method Get->users_home() was asked to find the home directory for the user: [#!variable!user!#], but was unable to do so.]]></key>
@ -229,6 +234,13 @@ The database connection error was:
<key name="log_0153">The Storage->backup() method was called with the source file: [#!variable!source_file!#], which isn't actually a file.</key>
<key name="log_0154">The file: [#!variable!source_file!#] has been backed up as: [#!variable!target_file!#].</key>
<key name="log_0155">Removing the old network configuration file: [#!variable!file!#] as part of the network reconfiguration.</key>
<key name="log_0156"><![CDATA[[ Error ] - The method: [#!variable!method!#] must be called with root-level priviledges.]]></key>
<key name="log_0157"><![CDATA[[ Error ] - The method Storage->write_file() was asked to write the file: [#!variable!file!#] but it appears to be missing the file name. Aborting.]]></key>
<key name="log_0158">Ensuring we've recorded: [#!variable!target!#]'s RSA fingerprint.</key>
<key name="log_0159">Adding the target: [#!variable!target!#]:[#!variable!port!#]'s RSA fingerprint to: [#!variable!user!#]'s list of known hosts.</key>
<key name="log_0160"><![CDATA[[ Error ] - The method Storage->read_file() was asked to read the remote file: [#!variable!file!#] but it is not a full path. Aborting.]]></key>
<key name="log_0161"><![CDATA[[ Error ] - The method Storage->read_file() was asked to read the remote file: [#!variable!file!#] but it appears to be missing the file name. Aborting.]]></key>
<key name="log_0162"><![CDATA[[ Error ] - The method Storage->read_file() tried to rsync the remote file: [#!variable!remote_file!#] to the local temporary file: [#!variable!local_file!#], but it did not arrive. There might be more information above.]]></key>
<!-- Test words. Do NOT change unless you update 't/Words.t' or tests will needlessly fail. -->
<key name="t_0000">Test</key>
@ -324,6 +336,10 @@ Here we will inject 't_0006', which injects 't_0001' which has a variable: [#!st
<key name="error_0003">None of the databases are accessible, unable to proceed.</key>
<key name="error_0004">The gateway address doesn't match any of your networks.</key>
<key name="error_0005">This program must run with 'root' level privileges.</key>
<key name="error_0006">No password was given, exiting.</key>
<key name="error_0007">The passwords don't match, exiting.</key>
<key name="error_0008">Failed to read the file: [#!variable!file!#]. Please see the logs for details</key>
<key name="error_0009">Failed to add the target: [#!variable!target!#]:[#!variable!port!#]'s RSA fingerprint to: [#!variable!user!#]'s list of known hosts.</key>
<!-- These are works and strings used by javascript/jqery -->
<key name="js_0001">Up</key>

@ -6,6 +6,8 @@
# 0 = Normal exit.
# 1 = The program is not running as root.
# 2 = Failed to connect to database(s).
# 3 = User didn't enter a password or the passwords didn't match.
# 4 = The password file doesn't exist, wasn't readable or was empty.
#
use strict;
@ -56,6 +58,66 @@ if (not $connections)
$anvil->nice_exit({exit_code => 2});
}
# The order that we pick up the new password is;
# 1. If we've been told of a password file, read it
# 2. If the user passed the password with --new-password <secret>, use that.
# 3. Ask the user for the new password.
if ($anvil->data->{switches}{password_file})
{
# Read the password in from the file.
if (-e $anvil->data->{switches}{password_file})
{
$anvil->data->{switches}{'new-password'} = $anvil->Storage->read_file({file => $anvil->data->{switches}{password_file}});
}
else
{
# The file doesn't exist.
print $anvil->Words->string({key => "error_0008", variables => { file => $anvil->data->{switches}{password_file} }});
$anvil->nice_exit({exit_code => 4});
}
}
elsif (not $anvil->data->{switches}{'new-password'})
{
print $anvil->Words->string({key => "message_0018"})."\n";
# Turn off echo
my $old_stty = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." --save"});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, secure => 0, list => { old_stty => $old_stty }});
$anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." -echo"});
my $password1 = <STDIN>;
chomp($password1);
$password1 =~ s/^\s+//;
$password1 =~ s/\s+$//;
# Turn echo on
$anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." ".$old_stty});
if (not $password1)
{
print $anvil->Words->string({key => "error_0006"})."\n";
$anvil->nice_exit({code => 3});
}
print $anvil->Words->string({key => "message_0019"})."\n";
# Turn off echo
$anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." -echo"});
my $password2 = <STDIN>;
chomp($password2);
$password2 =~ s/^\s+//;
$password2 =~ s/\s+$//;
# Turn echo on
$anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." ".$old_stty});
if ($password1 eq $password2)
{
$anvil->data->{switches}{'new-password'} = $password1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, secure => 1, list => { "switches::new_password" => $anvil->data->{switches}{'new-password'} }});
}
else
{
print $anvil->Words->string({key => "error_0007"})."\n";
$anvil->nice_exit({code => 3});
}
}
### TODO: Check for access to all known Anvil! nodes and warn the user that they will have to manually update
### the password for us on any node we can't access
### NOTE: 'anvil' can be a name or UUID
@ -68,7 +130,19 @@ else
{
### TODO: Support '--peers' to also update the peer dashboards.
# Updating just ourself
update_local_passwords($anvil);
print $anvil->Words->string({key => "message_0020"})."\n";
print $anvil->Words->string({key => "message_0021"})." ";
my $answer = <STDIN>;
chomp($answer);
if ($answer =~ /^y/)
{
update_local_passwords($anvil);
}
else
{
# Abort.
print $anvil->Words->string({key => "message_0022"})."\n";
}
}
@ -84,7 +158,11 @@ sub update_local_passwords
my ($anvil) = @_;
# Update the local users.
foreach my $user ("admin", "root")
{
print "Updating: [$user] with password: [".$anvil->data->{switches}{'new-password'}."]\n";
$anvil->System->change_shell_user_password({user => $user, new_password => $anvil->data->{switches}{'new-password'}});
}
# Update the database password.
return(0);

@ -59,6 +59,9 @@ if (not $connections)
$anvil->nice_exit({exit_code => 2});
}
die "Testing...\n";
pickup_job_details($anvil);
reconfigure_network($anvil);

Loading…
Cancel
Save