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.

367 lines
11 KiB

#!/usr/bin/perl
#
# This script exposes access to the Perl modules written for the Anvil! system.
# It can be executed on Anvil! nodes, DR hosts, or strikers.
#
# Upon execution, creates an instance of Anvil::Tools which provides reference
# to all *.pm instances under the Anvil/Tools/ directory. With these
# references, proceed to execute 1 operation specified by one of --query,
# --sub, or --data flags.
#
#
# --- Notes ---
#
# * In this documentation, a "JSON object" does NOT include array.
#
#
# --- Usages ---
#
# To read from database:
# anvil-access-module --query <SQL query> [--uuid <database UUID>]
# > [ [row0_value0, row0_value1, row0_value2, ...],
# [row1_value0, row1_value1, row1_value2, ...],
# [row2_value0, row2_value1, row2_value2, ...], ... ]
#
# To write to database:
# anvil-access-module --mode write --query <SQL command> [--uuid <database UUID>]
# > { write_code: 0 | 1 | "!!error!!" }
#
# * The --query flag is required for specifying the SQL query (or command)
# for both read and write.
#
# * To perform a write, the --mode flag must be set to 'write' (quotes are
# optional).
#
# * It's possible to specify which database to access by providing the
# UUID; this can be ignored most of the time because it's rare to only
# target one database in a redundant system.
#
# ! A non-zero 'write_code' means the write failed. Try running the same
# subroutine with a lower debug value, for example:
#
# anvil-access-module --sub 'write' --sub-params '{ "query": <SQL command>, "debug": 2 }'
#
#
# To execute a Perl subroutine:
# anvil-access-module --sub <subroutine name> [--sub-module <module name>] [--sub-params <JSON object>]
# > { sub_results: only_value | [value0, value1, value2, ...] }
#
# * The --sub flag is required for specifying the name of the target
# subroutine.
#
# * The --sub-module flag sets the module name that the subroutine exists
# in. Module name is the file name (case sensitive and without extension)
# of any .pm file under '<root of this repository>/Anvil/Tools/'.
#
# This flag defaults to 'Database'.
#
# * The --sub-params flag accepts a JSON object which will be converted to a
# Perl hash and passed to the target subroutine as parameters.
#
# This flag defaults to '{}' (blank JSON object).
#
# ! In the case where the target subroutine returns a tuple or array, the
# 'sub_results' key in the output JSON object will be an array.
#
#
# To access the data hash:
# anvil-access-module --data <JSON object> [--predata <JSON array>]
# > { ... }
#
# * The --data flag is required for specifying the data structure to copy
# from the data hash. The script will recursively traverse each of the
# given JSON object's properties and pick values from the data hash for
# each property key that exists.
#
# JSON object:
# {
# [key: string]: boolean | number | null | <JSON object>;
# }
#
# * The --predata flag is a 2 dimentional JSON array for speficying 1 or more
# subroutines to execute in ascending-index-order before extracting data
# from the data hash. Each element of the top-level array contains a
# 2nd-level array.
#
# Each 2nd-level array contains:
# * in element 0, a string in Perl syntax that identifies the target
# subroutine, and
# * in element 1, a JSON object with parameters to supply to the target
# subroutine.
#
# JSON array:
# [subroutine: string, parameters: object][];
#
#
# --- Example usages ---
#
# Select hosts from database:
# $ anvil-access-module --query "SELECT host_uuid, host_name FROM hosts;"
# [["09a3ac2f-5904-42a6-bb5e-28cffc7fa4af","mockhost01"],["df3653e3-7378-43e2-be2a-ead1b8aee881","mockhost02"],...]
#
# Get local host name:
# $ anvil-access-module --sub 'host_name' --sub-module 'Get' --sub-params '{ "debug": 1 }'
# {"sub_results":"..."}
#
# Get database data and 1 path from data hash:
# $ anvil-access-module --data '{ "database": true, "path": { "exe": { "grep": true } } }'
# {"database":{...},"path":{"exe":{"grep":"/usr/bin/grep"}}}
#
# Get network data collected and recorded by the Network->get_ips() subroutine:
# $ anvil-access-module --predata '[ ["Network->get_ips", { "debug": 1 }] ]' --data '{ "network": true }'
# {"network":{...}}
#
use strict;
use warnings;
use Anvil::Tools;
use JSON;
use Data::Dumper;
$| = 1;
my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0];
my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0];
if (($running_directory =~ /^\./) && ($ENV{PWD}))
{
$running_directory =~ s/^\./$ENV{PWD}/;
}
my $anvil = Anvil::Tools->new();
sub is_array
{
return ref($_[0]) eq "ARRAY";
}
sub is_hash
{
return ref($_[0]) eq "HASH";
}
sub db_access
{
my $parameters = shift;
my $db_access_mode = $parameters->{db_access_mode};
my $db_uuid = $parameters->{db_uuid};
my $sql_query = $parameters->{sql_query};
my $access_parameters = { query => $sql_query, uuid => $db_uuid, source => $THIS_FILE, line => __LINE__ };
return ($db_access_mode eq "write")
? { write_code => $anvil->Database->write($access_parameters) }
: $anvil->Database->query($access_parameters);
}
sub call_pre_data_fns
{
my $parameters = shift;
my $fns = $parameters->{fns};
if (is_array($fns))
{
foreach my $fn_wrapper ( @{$fns} )
{
if (is_array($fn_wrapper))
{
# The double dash ( // ) operator is similar to the or ( || )
# operator; it tests for defined instead of true.
my $pre_chain = @{$fn_wrapper}[0] // "";
my @fn_params = @{$fn_wrapper}[1..$#{$fn_wrapper}] // ();
my @chain = split(/->|,/, $pre_chain);
my $intermediate = $anvil;
my $key_index = 0;
foreach my $key ( @chain )
{
last if not defined $intermediate->${key};
if ($key_index == $#chain && $intermediate->can($key))
{
eval { $intermediate->${key}(@fn_params); };
}
else
{
$intermediate = $intermediate->${key};
}
$key_index += 1;
}
}
}
}
}
sub get_anvil_data
{
my $parameters = shift;
my $chain = $parameters->{chain};
my $source_intermediate = $anvil->data;
my $target_intermediate = $parameters->{data};
my $key_index = 0;
foreach my $key ( @{$chain} )
{
last if not exists $source_intermediate->{$key};
$source_intermediate = $source_intermediate->{$key};
if (not exists $target_intermediate->{$key})
{
$target_intermediate->{$key} = {};
}
if ($key_index < $#{$chain})
{
$target_intermediate = $target_intermediate->{$key};
}
else
{
$target_intermediate->{$key} = $source_intermediate;
}
$key_index += 1;
}
}
sub call_fn
{
my $parameters = shift;
my $chain = $parameters->{chain};
my $fallback = $parameters->{fallback};
my $fn_wrapper = $parameters->{fn};
if (exists $fn_wrapper->{fn})
{
my $fn = $fn_wrapper->{fn};
my $fn_params = $fn_wrapper->{params};
$fn_params->{chain} = $chain;
return $fn->($fn_params);
}
else
{
return $fallback;
}
}
sub foreach_nested
{
my $parameters = shift;
# Required parameters:
my $hash = $parameters->{hash};
# Optional parameters:
my $chain = exists $parameters->{chain} ? $parameters->{chain} : ();
my $depth = exists $parameters->{depth} ? $parameters->{depth} : 0;
my $on_key = exists $parameters->{on_key} ? $parameters->{on_key} : {};
my $on_chain_end = exists $parameters->{on_chain_end} ? $parameters->{on_chain_end} : {};
foreach my $key (keys %{$hash})
{
my $is_continue_chain = 1;
my $value = $hash->{$key};
push(@{$chain}, $key);
$is_continue_chain = call_fn({ chain => $chain, fallback => $is_continue_chain, fn => $on_key });
if ( ($is_continue_chain) && is_hash($value) )
{
foreach_nested({
chain => $chain,
depth => $depth + 1,
hash => $value,
on_chain_end => $on_chain_end,
on_key => $on_key,
});
}
else
{
call_fn({ chain => $chain, fn => $on_chain_end });
}
pop(@{$chain});
}
}
$anvil->Get->switches;
$anvil->Database->connect;
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 2, secure => 0, key => "log_0132" });
if (not $anvil->data->{sys}{database}{connections})
{
# No databases, exit.
$anvil->Log->entry({ source => $THIS_FILE, line => __LINE__, level => 0, 'print' => 1, priority => "err", key => "error_0003" });
$anvil->nice_exit({ exit_code => 1 });
}
my $data_hash = $anvil->data->{switches}{'data'};
my $db_access_mode = defined $anvil->data->{switches}{'mode'} ? $anvil->data->{switches}{'mode'} : "";
my $db_uuid = $anvil->data->{switches}{'uuid'};
my $pre_data = $anvil->data->{switches}{'predata'};
my $sql_query = $anvil->data->{switches}{'query'};
my $sub_module_name = defined $anvil->data->{switches}{'sub-module'} ? $anvil->data->{switches}{'sub-module'} : "Database";
my $sub_name = defined $anvil->data->{switches}{'sub'} ? $anvil->data->{switches}{'sub'} : "";
my $sub_params = defined $anvil->data->{switches}{'sub-params'} ? $anvil->data->{switches}{'sub-params'} : "{}";
if ($sql_query)
{
my $results = db_access({ db_uuid => $db_uuid, sql_query => $sql_query, db_access_mode => $db_access_mode });
print JSON->new->utf8->encode($results)."\n";
}
elsif ($anvil->${sub_module_name}->can($sub_name))
{
my $decoded_sub_params;
my $is_decode_sub_params_success = eval { $decoded_sub_params = decode_json($sub_params); };
if (not $is_decode_sub_params_success)
{
print STDERR "error: failed to parse subroutine parameters\n";
$anvil->nice_exit({ exit_code => 1 });
}
my (@results) = $anvil->${sub_module_name}->${sub_name}($decoded_sub_params);
print JSON->new->utf8->encode({ sub_results => scalar(@results) > 1 ? \@results : $results[0] })."\n";
}
elsif ($data_hash)
{
if ($pre_data)
{
my $decoded_pre_data;
my $is_decode_pre_data_success = eval { $decoded_pre_data = decode_json($pre_data); };
if ($is_decode_pre_data_success && is_array($decoded_pre_data))
{
call_pre_data_fns({ fns => $decoded_pre_data });
}
}
my $decoded_data_hash;
my $is_decode_data_hash_success = eval { $decoded_data_hash = decode_json($data_hash); };
if (not $is_decode_data_hash_success)
{
print STDERR "error: failed to parse data structure\n";
$anvil->nice_exit({ exit_code => 1 });
}
my $get_anvil_data_params = { data => {} };
foreach_nested({
hash => $decoded_data_hash,
on_chain_end => { fn => \&get_anvil_data, params => $get_anvil_data_params },
});
print JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})."\n";
}
else
{
print STDERR "error: missing switches and perhaps their respective parameters; one of --data, --query, or --sub is required\n";
$anvil->nice_exit({ exit_code => 1 });
}
$anvil->nice_exit({ exit_code => 0 });