Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1375 lines
35 KiB

package Anvil::Tools;
#
# This is the "root" package that manages the sub modules and controls access to their methods.
#
BEGIN
{
our $VERSION = "3.0.0";
# This suppresses the 'could not find ParserDetails.ini in /PerlApp/XML/SAX' warning message in
# XML::Simple calls.
#$ENV{HARNESS_ACTIVE} = 1;
}
use strict;
use warnings;
use Scalar::Util qw(weaken isweak);
use Time::HiRes;
use Data::Dumper;
use CGI;
my $THIS_FILE = "Tools.pm";
### Methods;
# data
# environment
# nice_exit
# refresh
# _add_hash_reference
# _anvil_version
# _host_name
# _make_hash_reference
# _set_defaults
# _set_paths
# _short_host_name
use utf8;
binmode(STDERR, ':encoding(utf-8)');
binmode(STDOUT, ':encoding(utf-8)');
# I intentionally don't use EXPORT, @ISA and the like because I want my "subclass"es to be accessed in a
# somewhat more OO style. I know some may wish to strike me down for this, but I like the idea of accessing
# methods via their containing module's name. (A La: C<< $anvil->Module->method >> rather than C<< $anvil->method >>).
use Anvil::Tools::Account;
use Anvil::Tools::Alert;
use Anvil::Tools::Cluster;
use Anvil::Tools::Convert;
use Anvil::Tools::Database;
use Anvil::Tools::DRBD;
use Anvil::Tools::Email;
use Anvil::Tools::Get;
use Anvil::Tools::Job;
use Anvil::Tools::Log;
use Anvil::Tools::Network;
use Anvil::Tools::Remote;
use Anvil::Tools::Server;
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
use Anvil::Tools::Striker;
use Anvil::Tools::Storage;
use Anvil::Tools::System;
use Anvil::Tools::Template;
use Anvil::Tools::Words;
use Anvil::Tools::Validate;
=pod
=encoding utf8
=head1 NAME
Anvil::Tools
Provides a common oject handle to all Anvil::Tools::* module methods and handles invocation configuration.
=head1 SYNOPSIS
use Anvil::Tools;
# Get a common object handle on all Anvil::Tools::* modules.
my $anvil = Anvil::Tools->new();
# Again, but this time sets some initial values in the '$anvil->data' hash.
my $anvil = Anvil::Tools->new(
{
data => {
foo => "",
bar => [],
baz => {},
},
});
# This example gets the handle and also sets the default user and log
# languages as Japanese, sets a custom log file and sets the log level to
# '2'.
my $anvil = Anvil::Tools->new(
{
'Log' => {
user_language => "jp",
log_language => "jp"
level => 2,
},
});
=head1 DESCRIPTION
The Anvil::Tools module and all sub-modules are designed for use by Alteeve-based applications. It can be used as a general framework by anyone interested.
Core features are;
* Supports per user, per logging language selection where translations from from XML-formatted "String" files that support UTF8 and variable substitutions.
* Support for command-line and HTML output. Skinning support for HTML-based user interfaces.
* Redundant database access, resynchronization and archiving.
* Highly-native with minimal use of external perl modules and compiled code.
=head1 METHODS
Methods in the core module;
=cut
# The constructor through which all other module's methods will be accessed.
sub new
{
my $class = shift;
my $parameter = shift;
my $self = {
HANDLE => {
ACCOUNT => Anvil::Tools::Account->new(),
ALERT => Anvil::Tools::Alert->new(),
CLUSTER => Anvil::Tools::Cluster->new(),
CONVERT => Anvil::Tools::Convert->new(),
DATABASE => Anvil::Tools::Database->new(),
DRBD => Anvil::Tools::DRBD->new(),
EMAIL => Anvil::Tools::Email->new(),
GET => Anvil::Tools::Get->new(),
LOG => Anvil::Tools::Log->new(),
JOB => Anvil::Tools::Job->new(),
NETWORK => Anvil::Tools::Network->new(),
REMOTE => Anvil::Tools::Remote->new(),
SERVER => Anvil::Tools::Server->new(),
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
STRIKER => Anvil::Tools::Striker->new(),
STORAGE => Anvil::Tools::Storage->new(),
SYSTEM => Anvil::Tools::System->new(),
TEMPLATE => Anvil::Tools::Template->new(),
WORDS => Anvil::Tools::Words->new(),
VALIDATE => Anvil::Tools::Validate->new(),
# This is to be removed before development ends.
'log' => {
main => "",
},
},
DATA => {},
ENV_VALUES => {
ENVIRONMENT => 'cli',
},
HOST => {
# This is the host's UUID. It should never be manually set.
UUID => "",
ANVIL_VERSION => "",
},
};
# Bless you!
bless $self, $class;
# This isn't needed, but it makes the code below more consistent with and portable to other modules.
my $anvil = $self;
weaken($anvil); # Helps avoid memory leaks. See Scalar::Utils
# Get a handle on the various submodules
$anvil->Account->parent($anvil);
$anvil->Alert->parent($anvil);
$anvil->Cluster->parent($anvil);
$anvil->Convert->parent($anvil);
$anvil->Database->parent($anvil);
$anvil->DRBD->parent($anvil);
$anvil->Email->parent($anvil);
$anvil->Get->parent($anvil);
$anvil->Log->parent($anvil);
$anvil->Job->parent($anvil);
$anvil->Network->parent($anvil);
$anvil->Remote->parent($anvil);
$anvil->Server->parent($anvil);
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
$anvil->Striker->parent($anvil);
$anvil->Storage->parent($anvil);
$anvil->System->parent($anvil);
$anvil->Template->parent($anvil);
$anvil->Words->parent($anvil);
$anvil->Validate->parent($anvil);
# Set some system paths and system default variables
$anvil->_set_defaults();
$anvil->_set_paths();
# Record the start time.
$anvil->data->{ENV_VALUES}{START_TIME} = Time::HiRes::time;
# Set passed parameters if needed.
my $debug = 3;
if (ref($parameter) eq "HASH")
{
# Local parameters...
if ($parameter->{log_level})
{
$anvil->Log->level({set => $parameter->{log_level}});
}
if ($parameter->{log_secure})
{
$anvil->Log->secure({set => $parameter->{log_secure}});
}
if ($parameter->{debug})
{
$debug = $parameter->{debug};
}
}
elsif ($parameter)
{
# Um...
print $THIS_FILE." ".__LINE__."; Anvil::Tools->new() invoked with an invalid parameter. Expected a hash reference, but got: [$parameter]\n";
exit(1);
}
# This will help clean up if we catch a signal.
$SIG{INT} = sub { $anvil->catch_sig({signal => "INT"}); };
$SIG{TERM} = sub { $anvil->catch_sig({signal => "TERM"}); };
# This sets the environment this program is running in.
if ($ENV{SERVER_NAME})
{
$anvil->environment("html");
# There is no PWD environment variable, so we'll use 'DOCUMENT_ROOT' as 'PWD'
$ENV{PWD} = $ENV{DOCUMENT_ROOT};
}
else
{
$anvil->environment("cli");
}
# Setup my '$anvil->data' hash right away so that I have a place to store the strings hash.
$anvil->data($parameter->{data}) if $parameter->{data};
# Initialize the list of directories to seach.
$anvil->Storage->search_directories({debug => $debug, initialize => 1});
# I need to read the initial words early.
$anvil->Words->read({debug => $debug});
# If the local './anvil.conf' file exists, read it in.
if (-r $anvil->data->{path}{configs}{'anvil.conf'})
{
$anvil->Storage->read_config({debug => 3, file => $anvil->data->{path}{configs}{'anvil.conf'}});
### TODO: Should anvil.conf override parameters?
# Let parameters override config file values.
if ($parameter->{log_level})
{
$anvil->Log->level({set => $parameter->{log_level}});
}
if ($parameter->{log_secure})
{
$anvil->Log->secure({set => $parameter->{log_secure}});
}
}
# Get the local host UUID.
$anvil->Get->host_uuid({debug => $debug});
# Read in any command line switches.
$anvil->Get->switches({debug => $debug});
# Read in the local Anvil! version.
#...
return ($self);
}
#############################################################################################################
# Public methods #
#############################################################################################################
=head2 data
This is the method used to access the main hash reference that all user-accessible values are stored in. This includes words, configuration file variables and so forth.
When called without an argument, it returns the existing '$anvil->data' hash reference.
my $anvil = $anvil->data();
When called with a hash reference as the argument, it sets '$anvil->data' to the new hash.
my $some_hash = {};
my $anvil = $anvil->data($some_hash);
Data can be entered into or access by treating '$anvil->data' as a normal hash reference.
my $anvil = Anvil::Tools->new(
{
data => {
foo => "",
bar => [6, 4, 12],
baz => {
animal => "Cat",
thing => "Boat",
},
},
});
# Copy the 'Cat' value into the $animal variable.
my $animal = $anvil->data->{baz}{animal};
# Set 'A thing' in 'foo'.
$anvil->data->{foo} = "A thing";
The C<< $anvil >> variable is set inside all modules and acts as shared storage for variables, values and references in all modules. It acts as the core storage for most applications using Anvil::Tools.
=cut
sub data
{
my ($anvil) = shift;
# Pick up the passed in hash, if any.
$anvil->{DATA} = shift if $_[0];
return ($anvil->{DATA});
}
=head2 environment
This is the method used to check or set whether the program is outputting to command line or a browser.
When called without an argument, it returns the current environment.
if ($anvil->environment() eq "cli")
{
# format for STDOUT
}
elsif ($anvil->environment() eq "html")
{
# Use the template system to output HTML
}
When called with a string as the argument, that string will be set as the environment string.
$anvil->environment("cli");
Technically, any string can be used, however only 'cli' or 'html' are used by convention.
=cut
sub environment
{
my ($anvil) = shift;
weaken($anvil);
# Pick up the passed in delimiter, if any.
if ($_[0])
{
$anvil->data->{ENV_VALUES}{ENVIRONMENT} = shift;
# Load the CGI stuff if we're in a browser
if ($anvil->data->{ENV_VALUES}{ENVIRONMENT} eq "html")
{
CGI::Carp->import(qw(fatalsToBrowser));
}
}
return ($anvil->data->{ENV_VALUES}{ENVIRONMENT});
}
=head2 nice_exit
This is a simple method to exit cleanly, closing database connections and exiting with the set exit code.
Parameters;
=head3 exit_code (optional)
If set, this will be the exit code. The default is to exit with code C<< 0 >>.
=cut
sub nice_exit
{
my $self = shift;
my $parameter = shift;
my $anvil = $self;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $exit_code = defined $parameter->{exit_code} ? $parameter->{exit_code} : 0;
# Close database connections (if any).
$anvil->Database->disconnect({debug => $debug});
# Report the runtime.
my $end_time = Time::HiRes::time;
my $run_time = $end_time - $anvil->data->{ENV_VALUES}{START_TIME};
my $caller = ($0 =~ /^.*\/(.*)$/)[0];
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:ENV_VALUES::START_TIME' => $anvil->data->{ENV_VALUES}{START_TIME},
's2:end_time' => $end_time,
's3:run_time' => $run_time,
's4:caller' => $caller,
}});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0135", variables => { 'caller' => $caller, runtime => $run_time }});
my ($package, $filename, $line) = caller;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:package' => $package,
's2:filename' => $filename,
's3:line' => $line,
}});
# Close the log file.
if ($anvil->data->{HANDLE}{'log'}{main})
{
close $anvil->data->{HANDLE}{'log'}{main};
$anvil->data->{HANDLE}{'log'}{main} = "";
}
exit($exit_code);
}
=head2 refresh
This method re-reads the configuration file and resets paths, defaults and re-reads the words file(s).
=cut
sub refresh
{
my $self = shift;
my $parameter = shift;
my $anvil = $self;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->_set_paths();
$anvil->_set_defaults(); # This reset the log level
$anvil->Storage->read_config(); # This reset the log level also
$anvil->Get->switches; # Re-read to let switches override again.
$anvil->Words->read();
return(0);
}
#############################################################################################################
# Public methods used to access sub modules. #
#############################################################################################################
=head1 Submodule Access Methods
The methods below are used to access methods of submodules using 'C<< $anvil->Module->method() >>'.
=cut
=head2 Account
Access the C<Acount.pm> methods via 'C<< $anvil->Alert->method >>'.
=cut
sub Account
{
my $self = shift;
return ($self->{HANDLE}{ACCOUNT});
}
=head2 Alert
Access the C<Alert.pm> methods via 'C<< $anvil->Alert->method >>'.
=cut
sub Alert
{
my $self = shift;
return ($self->{HANDLE}{ALERT});
}
=head2 Cluster
Access the C<Cluster.pm> methods via 'C<< $anvil->Cluster->method >>'.
=cut
sub Cluster
{
my $self = shift;
return ($self->{HANDLE}{CLUSTER});
}
=head2 Convert
Access the C<Convert.pm> methods via 'C<< $anvil->Convert->method >>'.
=cut
sub Convert
{
my $self = shift;
return ($self->{HANDLE}{CONVERT});
}
=head2 Database
Access the C<Database.pm> methods via 'C<< $anvil->Database->method >>'.
=cut
sub Database
{
my $self = shift;
return ($self->{HANDLE}{DATABASE});
}
=head2 DRBD
Access the C<DRBD.pm> methods via 'C<< $anvil->DRBD->method >>'.
=cut
sub DRBD
{
my $self = shift;
return ($self->{HANDLE}{DRBD});
}
=head2 Email
Access the C<Email.pm> methods via 'C<< $anvil->Email->method >>'.
=cut
sub Email
{
my $self = shift;
return ($self->{HANDLE}{EMAIL});
}
=head2 Get
Access the C<Get.pm> methods via 'C<< $anvil->Get->method >>'.
=cut
sub Get
{
my $self = shift;
return ($self->{HANDLE}{GET});
}
=head2 Job
Access the C<Job.pm> methods via 'C<< $anvil->Log->method >>'.
=cut
sub Job
{
my $self = shift;
return ($self->{HANDLE}{JOB});
}
=head2 Log
Access the C<Log.pm> methods via 'C<< $anvil->Log->method >>'.
=cut
sub Log
{
my $self = shift;
return ($self->{HANDLE}{LOG});
}
=head2 Network
Access the C<Network.pm> methods via 'C<< $anvil->Network->method >>'.
=cut
sub Network
{
my $self = shift;
return ($self->{HANDLE}{NETWORK});
}
=head2 Remote
Access the C<Remote.pm> methods via 'C<< $anvil->Remote->method >>'.
=cut
sub Remote
{
my $self = shift;
return ($self->{HANDLE}{REMOTE});
}
=head2 Server
Access the C<Server.pm> methods via 'C<< $anvil->Server->method >>'.
=cut
sub Server
{
my $self = shift;
return ($self->{HANDLE}{SERVER});
}
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
=head2 Striker
Access the C<Striker.pm> methods via 'C<< $anvil->Striker->method >>'.
=cut
sub Striker
{
my $self = shift;
return ($self->{HANDLE}{STRIKER});
}
=head2 Storage
Access the C<Storage.pm> methods via 'C<< $anvil->Storage->method >>'.
=cut
sub Storage
{
my $self = shift;
return ($self->{HANDLE}{STORAGE});
}
=head2 System
Access the C<System.pm> methods via 'C<< $anvil->System->method >>'.
=cut
sub System
{
my $self = shift;
return ($self->{HANDLE}{SYSTEM});
}
=head2 Template
Access the C<Template.pm> methods via 'C<< $anvil->Template->method >>'.
=cut
sub Template
{
my $self = shift;
return ($self->{HANDLE}{TEMPLATE});
}
=head2 Words
Access the C<Words.pm> methods via 'C<< $anvil->Words->method >>'.
=cut
sub Words
{
my $self = shift;
return ($self->{HANDLE}{WORDS});
}
=head2 Validate
Access the C<Validate.pm> methods via 'C<< $anvil->Validate->method >>'.
=cut
sub Validate
{
my $self = shift;
return ($self->{HANDLE}{VALIDATE});
}
=head1 Private Functions;
These methods generally should never be called from a program using Anvil::Tools. However, we are not your boss.
=cut
#############################################################################################################
# Private methods #
#############################################################################################################
=head2 _add_hash_reference
This is a helper to the '$anvil->_make_hash_reference' method. It is called each time a new string is to be created as a new hash key in the passed hash reference.
NOTE: Contributed by Shaun Fryer and Viktor Pavlenko by way of Toronto Perl Mongers.
=cut
sub _add_hash_reference
{
my $self = shift;
my $href1 = shift;
my $href2 = shift;
for my $key (keys %$href2)
{
if (ref $href1->{$key} eq 'HASH')
{
$self->_add_hash_reference( $href1->{$key}, $href2->{$key} );
}
else
{
$href1->{$key} = $href2->{$key};
}
}
}
=head2 _anvil_version
=cut
sub _anvil_version
{
my $self = shift;
my $anvil = $self;
$anvil->{HOST}{ANVIL_VERSION} = "" if not defined $anvil->{HOST}{ANVIL_VERSION};
if ($anvil->{HOST}{ANVIL_VERSION} eq "")
{
# Try to read the local Anvil! version.
$anvil->{HOST}{ANVIL_VERSION} = $anvil->Get->anvil_version();
}
return($anvil->{HOST}{ANVIL_VERSION});
}
=head3 _domain_name
This returns the domain name portion of the systen's host name. That is to say, the host name after the first '.'. If there is no domain portion, nothing is returned.
=cut
sub _domain_name
{
my $self = shift;
my $anvil = $self;
my $domain_name = $anvil->_host_name;
$domain_name =~ s/^.*?\.//;
$domain_name = "" if not defined $domain_name;
return($domain_name);
}
=head2 _host_name
This returns the (full) host name for the machine this is running on.
=cut
sub _host_name
{
my $self = shift;
my $anvil = $self;
my $host_name = "";
if ($ENV{HOSTNAME})
{
# We have an environment variable, so use it.
$host_name = $ENV{HOSTNAME};
}
else
{
# The environment variable isn't set. Call 'hostnamectl' on the command line.
($host_name, my $return_code) = $anvil->System->call({debug => 9999, shell_call => $anvil->data->{path}{exe}{hostnamectl}." --static"});
}
return($host_name);
}
=head2 _get_hash_reference
This is called when we need to parse a double-colon separated string into two or more elements which represent keys in the 'C<< $anvil->data >>' hash. Once suitably split up, the value is read and returned.
For example;
$anvil->data->{foo}{bar} = "baz";
my $value = $anvil->_get_hash_reference({ key => "foo::bar" });
The 'C<< $value >>' now contains "C<< baz >>".
NOTE: If the key is not found, 'C<< undef >>' is returned.
Parameters;
=head3 key (required)
This is the key to return the value for. If it is not passed, or if it does not have 'C<< :: >>' in it, 'C<< undef >>' will be returned.
=cut
sub _get_hash_reference
{
# 'href' is the hash reference I am working on.
my $self = shift;
my $parameter = shift;
my $anvil = $self;
die "$THIS_FILE ".__LINE__."; The hash key string: [".$parameter->{key}."] doesn't seem to be valid. It should be a string in the format 'foo::bar::baz'.\n" if $parameter->{key} !~ /::/;
# Split up the keys.
my $key = $parameter->{key} ? $parameter->{key} : "";
my $value = undef; # We return 'undef' so that the caller can tell the difference between an empty string versus nothing found.
if ($key =~ /::/)
{
my @keys = split /::/, $key;
my $last_key = pop @keys;
# Re-order the array.
my $current_hash_ref = $anvil->data;
foreach my $key (@keys)
{
$current_hash_ref = $current_hash_ref->{$key};
}
$value = $current_hash_ref->{$last_key};
}
return ($value);
}
=head2 _make_hash_reference
This takes a string with double-colon seperators and divides on those double-colons to create a hash reference where each element is a hash key.
NOTE: Contributed by Shaun Fryer and Viktor Pavlenko by way of Toronto Perl Mongers.
=cut
sub _make_hash_reference
{
my $self = shift;
my $href = shift;
my $key_string = shift;
my $value = shift;
my @keys = split /::/, $key_string;
my $last_key = pop @keys;
my $_href = {};
$_href->{$last_key} = $value;
while (my $key = pop @keys)
{
my $elem = {};
$elem->{$key} = $_href;
$_href = $elem;
}
$self->_add_hash_reference($href, $_href);
}
=head2 _set_defaults
This sets default variable values for the program.
=cut
sub _set_defaults
{
my ($anvil) = shift;
$anvil->data->{scancore} = {
timing => {
# Delay between DB connection attempts when no databases are available?
agent_runtime => 30,
db_retry_interval => 2,
# Delay between scans?
run_interval => 30,
},
};
$anvil->data->{sys} = {
apache => {
user => "admin",
},
daemon => {
dhcpd => "dhcpd.service",
firewalld => "firewalld.service",
httpd => "httpd.service",
postgresql => "postgresql.service",
tftp => "tftp.socket",
},
daemons => {
restart_firewalld => 1,
},
database => {
archive => {
compress => 1,
count => 50000,
directory => "/usr/local/anvil/archives/",
division => 6000,
trigger => 100000,
},
connections => 0,
### WARNING: The order the tables are resync'ed is important! Any table that has a
### foreign key needs to resync *AFTER* the tables with the primary keys.
# NOTE: Check that this list is complete with;
# grep 'CREATE TABLE' share/anvil.sql | grep -v history. | awk '{print $3}'
core_tables => [
"hosts", # Always has to be first.
"ssh_keys",
"users",
"host_variable",
"sessions", # Has to come after users and hosts
"anvils",
"alerts",
"recipients",
"notifications",
"mail_servers",
"variables",
"jobs",
"bridges",
"bonds",
"network_interfaces",
"ip_addresses",
"files",
"file_locations",
"servers",
"definitions",
"oui",
"mac_to_ip",
"updated",
"alert_sent",
"states",
"manifests",
"fences",
"upses",
],
failed_connection_log_level => 1,
local_lock_active => 0,
local_uuid => "",
locking_reap_age => 300,
log_transactions => 0,
maximum_batch_size => 25000,
name => "anvil",
read_uuid => "",
test_table => "hosts",
timestamp => "",
user => "admin",
use_handle => "",
},
host_type => "",
host_uuid => "",
language => "en_CA",
'log' => {
date => 1,
# Stores the '-v|...|-vvv' so that shell calls can be run at the same level as the
# avtive program when set by the user at the command line.
level => "",
},
manage => {
firewall => 1,
},
password => {
algorithm => "sha512",
hash_count => 500000,
salt_length => 16,
},
privacy => {
strong => 0,
},
# On actual RHEL systems, this will be used to ensure that given repos are enabled on given
# machines types. Obviously, this requires that the host has been subscribed.
rhel => {
repos => {
common => ["codeready-builder-for-rhel-8-x86_64-rpms"],
dashboard => [],
dr => [],
node => ["rhel-8-for-x86_64-highavailability-rpms"],
},
},
terminal => {
columns => 80,
stty => "",
},
use_base2 => 1,
user => {
name => "admin",
cookie_valid => 0,
language => "en_CA",
skin => "alteeve",
},
# This is data filled from the active user's database table.
users => {
user_name => "",
user_password_hash => "",
user_salt => "",
user_algorithm => "",
user_hash_count => "",
user_language => "",
user_is_admin => "",
user_is_experienced => "",
user_is_trusted => "",
},
};
$anvil->data->{defaults} = {
database => {
locking => {
reap_age => 300,
}
},
language => {
# Default language for all output shown to a user.
output => 'en_CA',
},
limits => {
# This is the maximum number of times we're allow to loop when injecting variables
# into a string being processed in Anvil::Tools::Words->string();
string_loops => 1000,
},
'log' => {
db_transactions => 0,
facility => "local0",
language => "en_CA",
level => 1,
secure => 0,
server => "",
tag => "anvil",
},
# NOTE: These are here to allow foreign users to override western defaults in anvil.conf.
kickstart => {
keyboard => "--vckeymap=us --xlayouts='us'",
password => "Initial1",
timezone => "Etc/GMT --isUtc",
},
# See 'striker' -> 'sub generate_ip()' function comments for details on how m3 IPs are handled.
network => {
# BCN starts at 10.200(+n)/16
bcn => {
network => "10.200.0.0",
subnet_mask => "255.255.0.0",
switch_octet3 => "1",
pdu_octet3 => "2",
ups_octet3 => "3",
striker_octet3 => "4",
striker_ipmi_octet3 => "5",
},
dns => "8.8.8.8, 8.8.4.4",
# The IFN will not be under our control. So for suggestion to the user purpose only,
# IFN starts at 10.255/16
ifn => {
network => "10.255.0.0",
subnet_mask => "255.255.0.0",
striker_octet3 => "4",
},
# SN starts at 10.100(+n)/16
sn => {
network => "10.100.0.0",
subnet_mask => "255.255.0.0",
},
test => {
domains => ["alteeve.com", "redhat.com", "google.com"],
},
},
template => {
html => "alteeve",
},
};
return(0);
}
=head2 _set_paths
This sets default paths to many system commands, checking to make sure the binary exists at the path and, if not, try to find it.
=cut
sub _set_paths
{
my ($anvil) = shift;
# Executables
$anvil->data->{path} = {
configs => {
'anvil.conf' => "/etc/anvil/anvil.conf",
'anvil.version' => "/etc/anvil/anvil.version",
'autoindex.conf' => "/etc/httpd/conf.d/autoindex.conf",
'corosync.conf' => "/etc/corosync/corosync.conf",
'dhcpd.conf' => "/etc/dhcp/dhcpd.conf",
'dnf.conf' => "/etc/dnf/dnf.conf",
'firewalld.conf' => "/etc/firewalld/firewalld.conf",
'global-common.conf' => "/etc/drbd.d/global_common.conf",
hosts => "/etc/hosts",
'httpd.conf' => "/etc/httpd/conf/httpd.conf",
'journald_anvil' => "/etc/systemd/journald.conf.d/anvil.conf",
'pg_hba.conf' => "/var/lib/pgsql/data/pg_hba.conf",
'postgresql.conf' => "/var/lib/pgsql/data/postgresql.conf",
pxe_default => "/var/lib/tftpboot/pxelinux.cfg/default",
pxe_uefi => "/var/lib/tftpboot/pxelinux.cfg/uefi",
postfix_main => "/etc/postfix/main.cf",
postfix_relay_password => "/etc/postfix/relay_password",
ssh_config => "/etc/ssh/ssh_config",
'type.striker' => "/etc/anvil/type.striker",
'type.dr' => "/etc/anvil/type.dr",
'type.node' => "/etc/anvil/type.node",
},
data => {
'.htpasswd' => "/etc/httpd/.htpasswd",
'chrony.conf' => "/etc/chrony.conf",
group => "/etc/group",
issue => "/etc/issue",
httpd_conf => "/etc/httpd/conf/httpd.conf",
host_ssh_key => "/etc/ssh/ssh_host_ecdsa_key.pub",
host_uuid => "/etc/anvil/host.uuid",
network_cache => "/tmp/network_cache.anvil",
passwd => "/etc/passwd",
'redhat-release' => "/etc/redhat-release",
fences_unified_metadata => "/var/www/html/fences_unified_metadata.xml",
},
directories => {
alert_emails => "/var/spool/anvil",
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
anvil => "/etc/anvil",
backups => "/root/anvil-backups",
'cgi-bin' => "/var/www/cgi-bin",
fence_agents => "/usr/sbin",
firewalld_services => "/usr/lib/firewalld/services",
firewalld_zones_etc => "/etc/firewalld/zones", # Changes when firewall-cmd ... --permanent is used.
firewalld_zones => "/usr/lib/firewalld/zones",
html => "/var/www/html",
ifcfg => "/etc/sysconfig/network-scripts",
scan_agents => "/usr/sbin/scancore-agents",
shared => {
archives => "/mnt/shared/archives",
base => "/mnt/shared",
definitions => "/mnt/shared/definitions",
files => "/mnt/shared/files",
incoming => "/mnt/shared/incoming",
provision => "/mnt/shared/provision",
temp => "/mnt/shared/temp",
},
skins => "/var/www/html/skins",
status => "/var/www/html/status",
syslinux => "/usr/share/syslinux",
tftpboot => "/var/lib/tftpboot",
tools => "/usr/sbin",
units => "/usr/lib/systemd/system",
},
exe => {
'anvil-change-password' => "/usr/sbin/anvil-change-password",
'anvil-check-memory' => "/usr/sbin/anvil-check-memory",
'anvil-configure-host' => "/usr/sbin/anvil-configure-host",
'anvil-daemon' => "/usr/sbin/anvil-daemon",
'anvil-download-file' => "/usr/sbin/anvil-download-file",
'anvil-file-details' => "/usr/sbin/anvil-file-details",
'anvil-join-anvil' => "/usr/sbin/anvil-join-anvil",
'anvil-maintenance-mode' => "/usr/sbin/anvil-maintenance-mode",
'anvil-manage-firewall' => "/usr/sbin/anvil-manage-firewall",
'anvil-manage-keys' => "/usr/sbin/anvil-manage-keys",
'anvil-manage-power' => "/usr/sbin/anvil-manage-power",
'anvil-parse-fence-agents' => "/usr/sbin/anvil-parse-fence-agents",
'anvil-report-memory' => "/usr/sbin/anvil-report-memory",
'anvil-update-files' => "/usr/sbin/anvil-update-files",
'anvil-update-states' => "/usr/sbin/anvil-update-states",
'anvil-update-system' => "/usr/sbin/anvil-update-system",
bridge => "/usr/sbin/bridge",
bzip2 => "/usr/bin/bzip2",
'call_striker-get-peer-data' => "/usr/sbin/call_striker-get-peer-data",
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
cat => "/usr/bin/cat",
'chmod' => "/usr/bin/chmod",
'chown' => "/usr/bin/chown",
chronyc => "/usr/bin/chronyc",
cibadmin => "/usr/sbin/cibadmin",
cp => "/usr/bin/cp",
createdb => "/usr/bin/createdb",
createrepo_c => "/usr/bin/createrepo_c",
createuser => "/usr/bin/createuser",
crm_error => "/usr/sbin/crm_error",
dmidecode => "/usr/sbin/dmidecode",
dnf => "/usr/bin/dnf",
drbdadm => "/usr/sbin/drbdadm",
drbdsetup => "/usr/sbin/drbdsetup",
echo => "/usr/bin/echo",
ethtool => "/usr/sbin/ethtool",
expect => "/usr/bin/expect",
fence_pacemaker => "/usr/sbin/fence_pacemaker",
'firewall-cmd' => "/usr/bin/firewall-cmd",
free => "/usr/bin/free",
gcc => "/usr/bin/gcc",
getent => "/usr/bin/getent",
gethostip => "/usr/bin/gethostip",
'grep' => "/usr/bin/grep",
head => "/usr/bin/head",
hostnamectl => "/usr/bin/hostnamectl",
htpasswd => "/usr/bin/htpasswd",
ifdown => "/sbin/ifdown",
ifup => "/sbin/ifup",
ip => "/usr/sbin/ip",
ipmitool => "/usr/bin/ipmitool",
'iptables-save' => "/usr/sbin/iptables-save",
journalctl => "/usr/bin/journalctl",
logger => "/usr/bin/logger",
ls => "/usr/bin/ls",
lvchange => "/usr/sbin/lvchange",
lvs => "/usr/sbin/lvs",
lvscan => "/usr/sbin/lvscan",
man => "/usr/bin/man",
md5sum => "/usr/bin/md5sum",
'mkdir' => "/usr/bin/mkdir",
modifyrepo_c => "/usr/bin/modifyrepo_c",
mv => "/usr/bin/mv",
nmap => "/usr/bin/nmap",
nmcli => "/bin/nmcli",
openssl => "/usr/bin/openssl",
passwd => "/usr/bin/passwd",
pcs => "/usr/sbin/pcs",
ping => "/usr/bin/ping",
pgrep => "/usr/bin/pgrep",
ps => "/usr/bin/ps",
psql => "/usr/bin/psql",
'postgresql-setup' => "/usr/bin/postgresql-setup",
postmap => "/usr/sbin/postmap",
postqueue => "/usr/sbin/postqueue",
pwd => "/usr/bin/pwd",
pvs => "/usr/sbin/pvs",
pvscan => "/usr/sbin/pvscan",
rpm => "/usr/bin/rpm",
rsync => "/usr/bin/rsync",
sed => "/usr/bin/sed",
'shutdown' => "/usr/sbin/shutdown",
'ssh-keygen' => "/usr/bin/ssh-keygen",
'ssh-keyscan' => "/usr/bin/ssh-keyscan",
'stat' => "/usr/bin/stat",
stonith_admin => "/usr/sbin/stonith_admin",
strings => "/usr/bin/strings",
'striker-get-peer-data' => "/usr/sbin/striker-get-peer-data",
'striker-initialize-host' => "/usr/sbin/striker-initialize-host",
'striker-manage-install-target' => "/usr/sbin/striker-manage-install-target",
'striker-manage-peers' => "/usr/sbin/striker-manage-peers",
'striker-parse-oui' => "/usr/sbin/striker-parse-oui",
'striker-prep-database' => "/usr/sbin/striker-prep-database",
'striker-scan-network' => "/usr/sbin/striker-scan-network",
stty => "/usr/bin/stty",
su => "/usr/bin/su",
* Got the node/dr host initialization form to the point where it can test access and decide if it should show the Red Hat account form. Decided that for M3, node/dr host setup will now be a four-stage process; initial install (over PXE), initialization (install the proper anvil-{node,dr} RPM and connect to the database), setup/map the network, and then add to an Anvil! pair. * Updated striker to no longer try to SSH to a remote machine. To enable this, we'd have to give apache a shell and an SSH key, which is dumb and dangerous when considered. * Created tools/striker-get-peer-data which is meant to be invoked as the 'admin' user (via a setuid c-wrapper). It collects basic data about a target machine and reports what it finds on STDOUT. It gets the password for the target via the database. * Updated anvil-daemon to check/create/update setuid c-wrapper(s), which for now is limited to call_striker-initialize-host. * Created Anvil/Tools/Striker.pm to store Striker web-specific methods, including get_peer_data() which calls tools/striker-initialize-host via the setuid admin call_striker-initialize-host c-wrapper. * In order to allow striker via apache to read a peer's anvil.version, which it can no longer do over SSH, any connection to a peer where the anvil.version is read is cached as /etc/anvil/anvil.<peer>.version. When Get->anvil_version is called as 'apache', this file is read instead. * Updated Database->resync_databases() and ->_find_behind_databases() to ignore the 'states' table. * Created tools/striker-initialize-host which will be called as a job to initialize a node/dr host. Signed-off-by: Digimer <digimer@alteeve.ca>
5 years ago
'subscription-manager' => "/usr/sbin/subscription-manager",
systemctl => "/usr/bin/systemctl",
timeout => "/usr/bin/timeout",
touch => "/usr/bin/touch",
tput => "/usr/bin/tput",
'tr' => "/usr/bin/tr",
uname => "/usr/bin/uname",
usermod => "/usr/sbin/usermod",
uuidgen => "/usr/bin/uuidgen",
virsh => "/usr/bin/virsh",
vgs => "/usr/sbin/vgs",
vgscan => "/usr/sbin/vgscan",
wget => "/usr/bin/wget",
},
json => {
all_status => "all_status.json",
files => "files.json",
},
'lock' => {
database => "/tmp/anvil-tools.database.lock",
},
'log' => {
main => "/var/log/anvil.log",
alert => "/var/log/anvil.alert.log",
},
proc => {
uptime => "/proc/uptime",
},
secure => {
postgres_pgpass => "/var/lib/pgsql/.pgpass",
},
sql => {
'anvil.sql' => "/usr/share/anvil/anvil.sql",
},
systemd => {
httpd_enabled_symlink => "/etc/systemd/system/multi-user.target.wants/httpd.service",
tftp_enabled_symlink => "/etc/systemd/system/sockets.target.wants/tftp.socket",
},
urls => {
skins => "/skins",
oui_file => "http://standards.ieee.org/develop/regauth/oui/oui.txt",
},
words => {
'words.xml' => "/usr/share/anvil/words.xml",
},
};
# Make sure we actually have the requested files.
foreach my $type (sort {$a cmp $b} keys %{$anvil->data->{path}})
{
# We don't look for urls because they're relative to the domain. We also don't look for
# configs as we might find backups.
next if $type eq "urls";
next if $type eq "configs";
foreach my $file (sort {$a cmp $b} keys %{$anvil->data->{path}{$type}})
{
if (not -e $anvil->data->{path}{$type}{$file})
{
my $full_path = $anvil->Storage->find({file => $file});
if (($full_path) && ($full_path ne "#!not_found!#"))
{
$anvil->data->{path}{$type}{$file} = $full_path;
}
}
}
};
return(0);
}
=head3 _short_host_name
This returns the short host name for the machine this is running on. That is to say, the host name up to the first '.'.
=cut
sub _short_host_name
{
my $self = shift;
my $anvil = $self;
my $short_host_name = $anvil->_host_name;
$short_host_name =~ s/\..*$//;
return($short_host_name);
}
=head1 Exit Codes
=head2 C<1>
Anvil::Tools->new() passed something other than a hash reference.
=head2 C<2>
Failed to find the requested file in C<< Anvil::Tools::Storage->find >> and 'fatal' was set.
=head1 Requirements
The following packages are required on EL7.
* C<expect>
* C<httpd>
* C<mailx>
* C<perl-Test-Simple>
* C<policycoreutils-python>
* C<postgresql>
* C<syslinux>
* C<perl-XML-Simple>
=head1 Recommended Packages
The following packages provide non-critical functionality.
* C<subscription-manager>
=cut
# This catches SIGINT and SIGTERM and fires out an email before shutting down.
sub catch_sig
{
my $self = shift;
my $parameter = shift;
my $anvil = $self;
my $signal = $parameter->{signal} ? $parameter->{signal} : "";
if ($signal)
{
print "\n\nProcess with PID: [$$] exiting on SIG".$signal.".\n";
if ($anvil->data->{sys}{terminal}{stty})
{
# Restore the terminal.
print "Restoring the terminal\n";
$anvil->System->call({shell_call => $anvil->data->{path}{exe}{stty}." ".$anvil->data->{sys}{terminal}{stty}});
}
}
$anvil->nice_exit({exit_code => 255});
}
1;