package Anvil::Tools::Get; # # This module contains methods used to handle access to frequently used data. # use strict; use warnings; use Scalar::Util qw(weaken isweak); use Data::Dumper; use Encode; use UUID::Tiny qw(:std); use Net::Netmask; our $VERSION = "3.0.0"; my $THIS_FILE = "Get.pm"; ### Methods; # anvil_version # bridges # cgi # date_and_time # free_memory # host_type # host_uuid # md5sum # os_type # switches # uptime # users_home # uuid # _salt # _wrap_to =pod =encoding utf8 =head1 NAME Anvil::Tools::Get Provides all methods related to getting access to frequently used data. =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->Get->X'. # # Example using 'date_and_time()'; my $date = $anvil->Get->date_and_time({...}); =head1 METHODS Methods in this module; =cut sub new { my $class = shift; my $self = { HOST => { UUID => "", }, }; 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 anvil_version This reads to C<< VERSION >> file of a local or remote machine. If the version file isn't found, C<< 0 >> is returned. Parameters; =head3 password (optional) This is the password to use when connecting to a remote machine. If not set, but C<< target >> is, an attempt to connect without a password will be made. =head3 port (optional) This is the TCP port to use when connecting to a remote machine. If not set, but C<< target >> is, C<< 22 >> will be used. =head3 remote_user (optional, default root) If C<< target >> is set, this will be the user we connect to the remote machine as. =head3 target (optional) This is the IP or host name of the machine to read the version of. If this is not set, the local system's version is checked. =cut # NOTE: the version is set in anvil.spec by sed'ing the release and arch onto anvil.version in anvil-core's %post sub anvil_version { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $password = defined $parameter->{password} ? $parameter->{password} : ""; my $port = defined $parameter->{port} ? $parameter->{port} : ""; my $remote_user = defined $parameter->{remote_user} ? $parameter->{remote_user} : "root"; my $target = defined $parameter->{target} ? $parameter->{target} : ""; my $version = 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { password => $anvil->Log->is_secure($password), port => $port, remote_user => $remote_user, target => $target, }}); # Is this a local call or a remote call? if ($anvil->Network->is_local({host => $target})) { # Local. $version = $anvil->Storage->read_file({file => $anvil->data->{path}{configs}{'anvil.version'}}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { version => $version }}); # Did we actually read a version? if ($version eq "!!error!!") { $version = 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 1, list => { version => $version }}); } } else { ### TODO: Remote calls are fragile. Move the version of dashboards into a variable to read from the database. # Remote call. If we're running as the apache user, we need to read the cached version for # the peer. otherwise, after we read the version, will write the cached version. my $user = getpwuid($<); my $cache_file = $anvil->data->{path}{directories}{anvil}."/anvil.".$target.".version"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { cache_file => $cache_file, user => $user, }}); if ($user eq "apache") { # Try to read the local cached version. if (-e $cache_file) { # Read it in. $version = $anvil->Storage->read_file({file => $cache_file}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { version => $version }}); } } else { my $shell_call = " if [ -e ".$anvil->data->{path}{configs}{'anvil.version'}." ]; then cat ".$anvil->data->{path}{configs}{'anvil.version'}."; else echo 0; fi; "; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0166", variables => { shell_call => $shell_call, target => $target, remote_user => $remote_user }}); my ($output, $error, $return_code) = $anvil->Remote->call({ debug => $debug, shell_call => $shell_call, target => $target, port => $port, password => $password, remote_user => $remote_user, }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { error => $error, output => $output, }}); $version = defined $output ? $output : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { version => $version }}); # Create/Update the cache file. if ($version) { my $update_cache = 1; my $old_version = ""; if (-e $cache_file) { $old_version = $anvil->Storage->read_file({file => $cache_file}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { old_version => $old_version }}); if ($old_version eq $version) { # No need to update $update_cache = 0; } else { } } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { update_cache => $update_cache }}); if ($update_cache) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0437", variables => { target => $target, file => $cache_file, }}); $anvil->Storage->write_file({ debug => $debug, file => $cache_file, body => $version, mode => "0666", overwrite => 1, }); } } } } # Clear off any newline. $version =~ s/\n//gs; return($version); } =head2 bridges This finds a list of bridges on the host. Bridges that are found are stored is ' =cut sub bridges { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Get->bridges()" }}); my ($output, $return_code) = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{bridge}." -json -details link show"}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, return_code => $return_code, }}); # Delete any previously known data if (exists $anvil->data->{'local'}{network}{bridges}) { delete $anvil->data->{'local'}{network}{bridges}; }; my $json = JSON->new->allow_nonref; my $bridge_data = $json->decode($output); #print Dumper $bridge_data; foreach my $hash_ref (@{$bridge_data}) { # If the ifname and master are the same, it's a bridge. my $type = "interface"; my $interface = $hash_ref->{ifname}; my $master_bridge = $hash_ref->{master}; if ($interface eq $master_bridge) { $type = "bridge"; $anvil->data->{'local'}{network}{bridges}{bridge}{$interface}{found} = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "local::network::bridges::bridge::${interface}::found" => $anvil->data->{'local'}{network}{bridges}{bridge}{$interface}{found}, }}); } else { # Store this interface under the bridge. $anvil->data->{'local'}{network}{bridges}{bridge}{$master_bridge}{connected_interface}{$interface} = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "local::network::bridges::bridge::${master_bridge}::connected_interface::${interface}" => $anvil->data->{'local'}{network}{bridges}{bridge}{$master_bridge}{connected_interface}{$interface}, }}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { interface => $interface, master_bridge => $master_bridge, type => $type, }}); foreach my $key (sort {$a cmp $b} keys %{$hash_ref}) { if (ref($hash_ref->{$key}) eq "ARRAY") { $anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key} = []; foreach my $value (sort {$a cmp $b} @{$hash_ref->{$key}}) { push @{$anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key}}, $value; } for (my $i = 0; $i < @{$anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key}}; $i++) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "local::network::bridges::${type}::${interface}::${key}->[$i]" => $anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key}->[$i], }}); } } else { $anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key} = $hash_ref->{$key}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "local::network::bridges::${type}::${interface}::${key}" => $anvil->data->{'local'}{network}{bridges}{$type}{$interface}{$key}, }}); } } } # Summary of found bridges. foreach my $interface (sort {$a cmp $b} keys %{$anvil->data->{'local'}{network}{bridges}{bridge}}) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "local::network::bridges::bridge::${interface}::found" => $anvil->data->{'local'}{network}{bridges}{bridge}{$interface}{found}, }}); } return(0); } =head2 cgi This reads in the CGI variables passed in by a form or URL. This method takes no parameters. =cut sub cgi { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; # This will store all of the CGI variables. $anvil->data->{sys}{cgi_string} = "?"; # Needed to read in passed CGI variables my $cgi = CGI->new(); my $cgis = []; my $cgi_count = 0; # Get the list of parameters coming in, if possible, if (exists $cgi->{param}) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'cgi->{param}' => $cgi->{param} }}); foreach my $variable (sort {$a cmp $b} keys %{$cgi->{param}}) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { variable => $variable }}); push @{$cgis}, $variable; } } $cgi_count = @{$cgis}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { cgi_count => $cgi_count }}); # If we don't have at least one variable, we're done. if ($cgi_count < 1) { return(0); } # NOTE: Later, we will have another array for handling file uploads. # Now read in the variables. foreach my $variable (sort {$a cmp $b} @{$cgis}) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { variable => $variable }}); $anvil->data->{cgi}{$variable}{value} = ""; $anvil->data->{cgi}{$variable}{mime_type} = "string"; $anvil->data->{cgi}{$variable}{file_handle} = ""; $anvil->data->{cgi}{$variable}{file_name} = ""; $anvil->data->{cgi}{$variable}{alert} = 0; # This is set if a sanity check fails # This is a special CGI key for download files (upload from the user's perspective) if ($variable eq "upload_file") { if (not $cgi->upload('upload_file')) { # Empty file passed, looks like the user forgot to select a file to upload. $anvil->Log->entry({log_level => 2, message_key => "log_0242", file => $THIS_FILE, line => __LINE__}); } else { $anvil->data->{cgi}{upload_file}{file_handle} = $cgi->upload('upload_file'); my $file = $anvil->data->{cgi}{upload_file}{file_handle}; $anvil->data->{cgi}{upload_file}{file_name} = $file; $anvil->data->{cgi}{upload_file}{mime_type} = $cgi->uploadInfo($file)->{'Content-Type'}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { variable => 'upload_file', "cgi::${variable}::file_handle" => $anvil->data->{cgi}{upload_file}{file_handle}, "cgi::${variable}::file_handle->handle" => $anvil->data->{cgi}{upload_file}{file_handle}->handle, "cgi::${variable}::file_name" => $anvil->data->{cgi}{upload_file}{file_name}, "cgi::${variable}::mime_type" => $anvil->data->{cgi}{upload_file}{mime_type}, "cgi->upload('upload_file')" => $cgi->upload('upload_file'), "cgi->upload('upload_file')->handle" => $cgi->upload('upload_file')->handle, }}); } } if (defined $cgi->param($variable)) { # Make this UTF8 if it isn't already. if (Encode::is_utf8($cgi->param($variable))) { $anvil->data->{cgi}{$variable}{value} = $cgi->param($variable); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cgi::${variable}::value" => $anvil->data->{cgi}{$variable}{value} }}); } else { $anvil->data->{cgi}{$variable}{value} = Encode::decode_utf8($cgi->param($variable)); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cgi::${variable}::value" => $anvil->data->{cgi}{$variable}{value} }}); } # Append to 'sys::cgi_string', so long as the variable doesn't have 'passwd' or 'password' in it. if (($variable !~ /password/) && ($variable !~ /passwd/)) { $anvil->data->{sys}{cgi_string} .= "$variable=".$anvil->data->{cgi}{$variable}{value}."&"; } } } # This is a pretty way of displaying the passed-in CGI variables. It loops through all we've got and # sorts out the longest variable name. Then it loops again, appending '.' to shorter ones so that # everything is lined up in the logs. This almost always prints, save for log level 0. if ($anvil->Log->level >= 1) { my $longest_variable = 0; foreach my $variable (sort {$a cmp $b} keys %{$anvil->data->{cgi}}) { next if $anvil->data->{cgi}{$variable} eq ""; if (length($variable) > $longest_variable) { $longest_variable = length($variable); } } # Now loop again. foreach my $variable (@{$cgis}) { next if $anvil->data->{cgi}{$variable} eq ""; my $difference = $longest_variable - length($variable); my $say_value = "value"; if ($difference == 0) { # Do nothing } elsif ($difference == 1) { $say_value .= " "; } elsif ($difference == 2) { $say_value .= " "; } else { my $dots = $difference - 2; $say_value .= " "; for (1 .. $dots) { $say_value .= "."; } $say_value .= " "; } # This is always '1' as the passed-in variables are what we want to see. my $censored_value = $anvil->data->{cgi}{$variable}{value}; if ((($variable =~ /passwd/) or ($variable =~ /password/)) && (not $anvil->Log->secure)) { # This is a password and we're not logging sensitive data, obfuscate it. $censored_value = $anvil->Words->string({key => "log_0186"}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 1, list => { "cgi::${variable}::$say_value" => $censored_value, }}); } } # Clear the last & $anvil->data->{sys}{cgi_string} =~ s/&$//; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::cgi_string" => $anvil->data->{sys}{cgi_string} }}); return(0); } =head2 date_and_time This method returns the date and/or time using either the current time, or a specified unix time. NOTE: This only returns times in 24-hour notation. =head2 Parameters; =head3 date_only (optional) If set, only the date will be returned (in C<< yyyy/mm/dd >> format). =head3 file_name (optional) When set, the date and/or time returned in a string more useful in file names. Specifically, it will replace spaces with 'C<< _ >>' and 'C<< : >>' and 'C<< / >>' for 'C<< - >>'. This will result in a string in the format like 'C<< yyyy-mm-dd_hh-mm-ss >>'. =head3 offset (optional) If set to a signed number, it will add or subtract the number of seconds from the 'C<< use_time >>' before processing. =head3 use_time (optional) This can be set to a unix timestamp. If it is not set, the current time is used. =head3 time_only (optional) If set, only the time will be returned (in C<< hh:mm:ss >> format). =head3 use_utc (optional) If set, C<< gmtime >> is used instead of C<< localtime >>. The effect of this is that GMTime (greenwhich mean time, UTC-0) is used instead of the local system's time zone. =cut sub date_and_time { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $offset = defined $parameter->{offset} ? $parameter->{offset} : 0; my $use_time = defined $parameter->{use_time} ? $parameter->{use_time} : time; my $use_utc = defined $parameter->{use_utc} ? $parameter->{use_utc} : 0; my $file_name = defined $parameter->{file_name} ? $parameter->{file_name} : 0; my $time_only = defined $parameter->{time_only} ? $parameter->{time_only} : 0; my $date_only = defined $parameter->{date_only} ? $parameter->{date_only} : 0; ### NOTE: This is used too early for normal error handling. # Are things sane? if ($use_time =~ /D/) { die "Get->date_and_time() was called with 'use_time' set to: [$use_time]. Only a unix timestamp is allowed.\n"; } if ($offset =~ /D/) { die "Get->date_and_time() was called with 'offset' set to: [$offset]. Only real number is allowed.\n"; } # Do my initial calculation. my $return_string = ""; my $time = {}; my $adjusted_time = $use_time + $offset; #print $THIS_FILE." ".__LINE__."; [ Debug ] - adjusted_time: [$adjusted_time]\n"; # Get the date and time pieces if ($use_utc) { ($time->{sec}, $time->{min}, $time->{hour}, $time->{mday}, $time->{mon}, $time->{year}, $time->{wday}, $time->{yday}, $time->{isdst}) = gmtime($adjusted_time); #print $THIS_FILE." ".__LINE__."; [ Debug ] - time->{sec}: [".$time->{sec}."], time->{min}: [".$time->{min}."], time->{hour}: [".$time->{hour}."], time->{mday}: [".$time->{mday}."], time->{mon}: [".$time->{mon}."], time->{year}: [".$time->{year}."], time->{wday}: [".$time->{wday}."], time->{yday}: [".$time->{yday}."], time->{isdst}: [".$time->{isdst}."]\n"; } else { ($time->{sec}, $time->{min}, $time->{hour}, $time->{mday}, $time->{mon}, $time->{year}, $time->{wday}, $time->{yday}, $time->{isdst}) = localtime($adjusted_time); #print $THIS_FILE." ".__LINE__."; [ Debug ] - time->{sec}: [".$time->{sec}."], time->{min}: [".$time->{min}."], time->{hour}: [".$time->{hour}."], time->{mday}: [".$time->{mday}."], time->{mon}: [".$time->{mon}."], time->{year}: [".$time->{year}."], time->{wday}: [".$time->{wday}."], time->{yday}: [".$time->{yday}."], time->{isdst}: [".$time->{isdst}."]\n"; } # Process the raw data $time->{pad_hour} = sprintf("%02d", $time->{hour}); $time->{mon}++; $time->{pad_min} = sprintf("%02d", $time->{min}); $time->{pad_sec} = sprintf("%02d", $time->{sec}); $time->{year} = ($time->{year} + 1900); $time->{pad_mon} = sprintf("%02d", $time->{mon}); $time->{pad_mday} = sprintf("%02d", $time->{mday}); #print $THIS_FILE." ".__LINE__."; [ Debug ] - time->{pad_hour}: [".$time->{pad_hour}."], time->{pad_min}: [".$time->{pad_min}."], time->{pad_sec}: [".$time->{pad_sec}."], time->{year}: [".$time->{year}."], time->{pad_mon}: [".$time->{pad_mon}."], time->{pad_mday}: [".$time->{pad_mday}."], time->{mon}: [".$time->{mon}."]\n"; # Now, the date and time separator depends on if 'file_name' is set. my $date_separator = $file_name ? "-" : "/"; my $time_separator = $file_name ? "-" : ":"; my $space_separator = $file_name ? "_" : " "; if ($time_only) { $return_string = $time->{pad_hour}.$time_separator.$time->{pad_min}.$time_separator.$time->{pad_sec}; #print $THIS_FILE." ".__LINE__."; [ Debug ] - return_string: [$return_string]\n"; } elsif ($date_only) { $return_string = $time->{year}.$date_separator.$time->{pad_mon}.$date_separator.$time->{pad_mday}; #print $THIS_FILE." ".__LINE__."; [ Debug ] - return_string: [$return_string]\n"; } else { $return_string = $time->{year}.$date_separator.$time->{pad_mon}.$date_separator.$time->{pad_mday}.$space_separator.$time->{pad_hour}.$time_separator.$time->{pad_min}.$time_separator.$time->{pad_sec}; #print $THIS_FILE." ".__LINE__."; [ Debug ] - return_string: [$return_string]\n"; } return($return_string); } =head2 host_name This takes a host UUID and returns the host name (as recorded in the C<< hosts >> table). If the entry is not found, an empty string is returned. Parameters; =head3 host_uuid (required) This is the C<< host_uuid >> to translate into a host name. =cut sub host_name { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $host_name = ""; my $host_uuid = defined $parameter->{host_uuid} ? $parameter->{host_uuid} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_uuid => $host_uuid }}); my $query = " SELECT host_name FROM hosts WHERE host_uuid = ".$anvil->Database->quote($host_uuid)."; "; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); my $count = @{$results}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { results => $results, count => $count, }}); if ($count == 1) { # Found it $host_name = defined $results->[0]->[0] ? $results->[0]->[0] : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_name => $host_name }}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_name => $host_name }}); return($host_name); } =head2 free_memory This returns, in bytes, host much free memory is available on the local system. =cut ### TODO: Make this work on remote systems. sub free_memory { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Get->free_memory()" }}); my $available = 0; my ($free_output, $free_rc) = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{free}." --bytes"}); foreach my $line (split/\n/, $free_output) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { line => $line }}); if ($line =~ /Mem:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) { my $total = $1; my $used = $2; my $free = $3; my $shared = $4; my $cache = $5; $available = $6; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { total => $total." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $total})."})", used => $used." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $used})."})", free => $free." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $free})."})", shared => $shared." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $shared})."})", cache => $cache." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $cache})."})", available => $available." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $available})."})", }}); } } return($available); } =head2 host_type This method tries to determine the host type and returns a value suitable for use is the C<< hosts >> table. my $type = $anvil->System->host_type(); First, it looks to see if C<< sys::host_type >> is set and, if so, uses that string as it is. If that isn't set, it then looks to see if the file C<< /etc/anvil/type.X >> exists, where C<< X >> is C<< node >>, C<< striker >> or C<< dr >>. If found, the appropriate type is returned. If that file doesn't exist, then it then checks to see which C<< anvil- >> rpm is installed. In order, it looks for C<< anvil-striker >>, then C<< anvil-node >> and finally C<< anvil-dr >>. If one of them is found, the appropriate C<< /etc/anvil/type.X >> is created. =cut sub host_type { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Get->host_type()" }}); my $host_type = ""; my $host_name = $anvil->_short_host_name; $host_type = "unknown"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type, host_name => $host_name, "sys::host_type" => $anvil->data->{sys}{host_type}, }}); if ($anvil->data->{sys}{host_type}) { $host_type = $anvil->data->{sys}{host_type}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type }}); } else { # Can I determine it by seeing a file? if (-e $anvil->data->{path}{configs}{'type.node'}) { $host_type = "node"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type }}); } elsif (-e $anvil->data->{path}{configs}{'type.striker'}) { $host_type = "striker"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type }}); } elsif (-e $anvil->data->{path}{configs}{'type.dr'}) { $host_type = "dr"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type }}); } else { # Last gasp here is to use 'rpm' to see which RPMs are installed. If we find one, # we'll touch 'type.X' file foreach my $rpm ("anvil-striker", "anvil-node", "anvil-dr") { my ($output, $return_code) = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{rpm}." -q ".$rpm}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, return_code => $return_code }}); if ($return_code eq "0") { # Found out what we are. if ($rpm eq "anvil-striker") { $host_type = "striker"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_type => $host_type }}); } my $key = "type.".$host_type; my $file = $anvil->data->{path}{configs}{$key}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { key => $key, file => $file, }}); # If we have a file and we're root, touch to the file. if (($file) && (($< == 0) or ($> == 0))) { my $error = $anvil->Storage->write_file({ debug => $debug, body => "", file => $file, }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 0, list => { error => $error }}); } last; } } } } return($host_type); } =head2 host_uuid This returns the local host's system UUID (as reported by 'dmidecode'). If the host UUID isn't available, and the program is not running with root priviledges, C<< #!error!# >> is returned. print "This host's UUID: [".$anvil->Get->host_uuid."]\n"; It is possible to override the local UUID, though it is not recommended. $anvil->Get->host_uuid({set => "720a0509-533d-406b-8fc1-03aca3e75fa7"}) =cut sub host_uuid { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $set = defined $parameter->{set} ? $parameter->{set} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { set => $set, 'HOST::UUID' => $anvil->{HOST}{UUID}, }}); if ($set) { $anvil->{HOST}{UUID} = $set; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "HOST::UUID" => $anvil->{HOST}{UUID} }}); } elsif (not $anvil->{HOST}{UUID}) { # Read /etc/anvil/host.uuid if it exists. If not, and if we're root, we'll create that file # using the UUID from dmidecode. my $uuid = ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { '$<' => $<, '$>' => $>, 'path::data::host_uuid' => $anvil->data->{path}{data}{host_uuid}, }}); if (-e $anvil->data->{path}{data}{host_uuid}) { # Read the UUID in $uuid = $anvil->Storage->read_file({debug => $debug, file => $anvil->data->{path}{data}{host_uuid}}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uuid => $uuid }}); } elsif (($< == 0) or ($> == 0)) { # Create the UUID file. ($uuid, my $return_code) = $anvil->System->call({debug => $debug, shell_call => $anvil->data->{path}{exe}{dmidecode}." --string system-uuid"}); $uuid = lc($uuid); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uuid => $uuid, return_code => $return_code, }}); } else { # Host UUID file doesn't exist and I'm Not running as root, I'm done. # We're done. $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0187"}); return("#!error!#"); } if ($anvil->Validate->is_uuid({uuid => $uuid})) { $anvil->{HOST}{UUID} = $uuid; if (not -e $anvil->data->{path}{data}{host_uuid}) { ### TODO: This will need to set the proper SELinux context. # Apache run scripts can't call the system UUID, so we'll write it to a text # file. $anvil->Storage->write_file({ debug => $debug, file => $anvil->data->{path}{data}{host_uuid}, body => $uuid, user => "apache", group => "apache", mode => "0666", overwrite => 0, }); $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "message_0011", variables => { file => $anvil->data->{path}{configs}{'postgresql.conf'} }}); } } else { # Bad UUID. $anvil->{HOST}{UUID} = ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "HOST::UUID" => $anvil->{HOST}{UUID} }}); $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0134", variables => { uuid => $uuid }}); return("#!error!#"); } } # We'll also store the host UUID in a variable. if ((not $anvil->data->{sys}{host_uuid}) && ($anvil->{HOST}{UUID})) { $anvil->data->{sys}{host_uuid} = $anvil->{HOST}{UUID}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::host_uuid" => $anvil->data->{sys}{host_uuid} }}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "HOST::UUID" => $anvil->{HOST}{UUID} }}); return($anvil->{HOST}{UUID}); } =head2 md5sum This returns the C<< md5sum >> of a given file. Parameters; =head3 file This is the full or relative path to the file. If the file doesn't exist, an empty string is returned. =cut sub md5sum { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $sum = ""; my $file = defined $parameter->{file} ? $parameter->{file} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { file => $file }}); if (-e $file) { my $shell_call = $anvil->data->{path}{exe}{md5sum}." ".$file; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); my ($return, $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'return' => $return, return_code => $return_code }}); # split the sum off. $sum = ($return =~ /^(.*?)\s+$file$/)[0]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { sum => $sum }}); } return($sum); } =head2 os_type This returns the operating system type and the system architecture as two separate string variables. # Run on RHEL 8, on a 64-bit system my ($os_type, $os_arch) = $anvil->Get->os_type(); # '$os_type' holds 'rhel8' ('rhel' or 'centos' + release version) # '$os_arch' holds 'x86_64' (specifically, 'uname --hardware-platform') If either can not be determined, C<< unknown >> will be returned. This method takes no parameters. =cut sub os_type { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Get->os_type()" }}); my $os_type = "unknown"; my $os_arch = "unknown"; ### NOTE: Examples; # Red Hat Enterprise Linux release 8.0 Beta (Ootpa) # Red Hat Enterprise Linux Server release 7.5 (Maipo) # CentOS Linux release 7.5.1804 (Core) # Read in the /etc/redhat-release file my $release = $anvil->Storage->read_file({file => $anvil->data->{path}{data}{'redhat-release'}}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { release => $release }}); if ($release =~ /Red Hat Enterprise Linux .* (\d+)\./) { # RHEL, with the major version number appended $os_type = "rhel".$1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { os_type => $os_type }}); } elsif ($release =~ /CentOS .*? (\d+)\./) { # CentOS, with the major version number appended $os_type = "centos".$1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { os_type => $os_type }}); } my ($output, $return_code) = $anvil->System->call({shell_call => $anvil->data->{path}{exe}{uname}." --hardware-platform"}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, return_code => $return_code }}); if ($output) { $os_arch = $output; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { os_arch => $os_arch }}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { os_type => $os_type, os_arch => $os_arch, }}); return($os_type, $os_arch); } =head2 switches This reads in the command line switches used to invoke the parent program. It takes no arguments, and data is stored in 'C<< $anvil->data->{switches}{x} >>', where 'x' is the switch used. Switches in the form 'C<< -x >>' and 'C<< --x >>' are treated the same and the corresponding 'C<< $anvil->data->{switches}{x} >>' will contain '#!set!#'. Switches in the form 'C<< -x foo >>', 'C<< --x foo >>', 'C<< -x=foo >>' and 'C<< --x=foo >>' are treated the same and the corresponding 'C<< $anvil->data->{switches}{x} >>' will contain 'foo'. The switches 'C<< -v >>', 'C<< -vv >>', 'C<< -vvv >>' and 'C<< -vvvv >>' will cause the active log level to automatically change to 1, 2, 3 or 4 respectively. Passing 'C<< -V >>' will set the log level to '0'. Anything after 'C<< -- >>' is treated as a raw string and is not processed. =cut sub switches { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $last_argument = ""; foreach my $argument (@ARGV) { if ($last_argument eq "raw") { # Don't process anything. $anvil->data->{switches}{raw} .= " $argument"; } elsif ($argument =~ /^-/) { # If the argument is just '--', appeand everything after it to 'raw'. if ($argument eq "--") { $last_argument = "raw"; $anvil->data->{switches}{raw} = ""; } else { ($last_argument) = ($argument =~ /^-{1,2}(.*)/)[0]; if ($last_argument =~ /=/) { # Break up the variable/value. ($last_argument, my $value) = (split /=/, $last_argument, 2); $anvil->data->{switches}{$last_argument} = $value; } else { $anvil->data->{switches}{$last_argument} = "#!SET!#"; } } } else { if ($last_argument) { $anvil->data->{switches}{$last_argument} = $argument; $last_argument = ""; } else { # Got a value without an argument, so just record it as '#!SET!#'. $anvil->data->{switches}{$argument} = "#!SET!#"; } } } # Clean up the initial space added to 'raw'. if ($anvil->data->{switches}{raw}) { $anvil->data->{switches}{raw} =~ s/^ //; } # Adjust the log level if requested. $anvil->Log->_adjust_log_level(); return(0); } =head2 uptime This returns, in seconds, how long the host has been up and running for. This method takes no parameters. =cut ### TODO: Make this work on remote hosts sub uptime { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Get->uptime()" }}); my $uptime = $anvil->Storage->read_file({ force_read => 1, cache => 0, file => $anvil->data->{path}{proc}{uptime}, }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uptime => $uptime }}); # Clean it up. We'll have gotten two numbers, the uptime in seconds (to two decimal places) and the # total idle time. We only care about the int number. $uptime =~ s/^(\d+)\..*$/$1/; $uptime =~ s/\n//gs; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uptime => $uptime }}); return($uptime); } =head2 users_home This method takes a user's name and returns the user's home directory. If the home directory isn't found, C<< 0 >> is returned. Parameters; =head3 user (optional, default is the user name of the real UID (as stored in '$<')) This is the user whose home directory you are looking for. =cut sub users_home { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $home_directory = 0; my $user = defined $parameter->{user} ? $parameter->{user} : getpwuid($<); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user => $user }}); # Make sure the user is only one digit. Sometimes $< (and others) will return multiple IDs. if ($user =~ /^\d+ \d$/) { $user =~ s/^(\d+)\s.*$/$1/; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user => $user }}); } # If the user is numerical, convert it to a name. if ($user =~ /^\d+$/) { $user = getpwuid($user); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user => $user }}); } # Still don't have a name? fail... if ($user eq "") { # No user? No bueno... $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Get->users_home()", parameter => "user" }}); return($home_directory); } my $body = $anvil->Storage->read_file({file => $anvil->data->{path}{data}{passwd}}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { body => $body }}); foreach my $line (split /\n/, $body) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }}); if ($line =~ /^$user:/) { $home_directory = (split/:/, $line)[5]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { home_directory => $home_directory }}); last; } } # Do I have the a user's $HOME now? if (not $home_directory) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0061", variables => { user => $user }}); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { home_directory => $home_directory }}); return($home_directory); } =head2 uuid This method returns a new v4 UUID (using 'UUID::Tiny'). Parameters; =head3 short (optional, default '0') This returns just the first 8 bytes of the uuid. For example, if the generated UUID is C<< 9e4b3f7c-5a98-40b6-9c34-84fdb24ddd30 >>, only C<< 9e4b3f7c >> is returned. =cut sub uuid { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $short = defined $parameter->{short} ? $parameter->{short} : 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { short => $short, }}); my $uuid = create_uuid_as_string(UUID_RANDOM); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uuid => $uuid }}); if ($short) { $uuid =~ s/^(\w+?)-.*$/$1/; } return($uuid); } # =head3 # # Private Functions; # # =cut ############################################################################################################# # Private functions # ############################################################################################################# =head2 _salt This generates a random salt string for use with internal Striker passwords. =cut sub _salt { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $salt = ""; my $salt_length = $anvil->data->{sys}{password}{salt_length} =~ /^\d+$/ ? $anvil->data->{sys}{password}{salt_length} : 16; my @seed = (" ", "~", "`", "!", "#", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "[", "}", "]", "|", ":", ";", "'", ",", "<", ".", ">", "/"); my @alpha = ("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"); my $seed_count = @seed; my $alpha_count = @alpha; my $skip_count = 0; for (1..$salt_length) { # We want to have a little randomness in the salt length, but not skip tooooo many times. if ((int(rand(20)) == 2) && ($skip_count <= 3)) { $skip_count++; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { skip_count => $skip_count }}); next; } # What character will this string be? my $this_integer = int(rand(3)); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { this_integer => $this_integer }}); if ($this_integer == 0) { # Inject a random digit $salt .= int(rand(10)); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { salt => $salt }}); } elsif ($this_integer == 1) { # Inject a random letter $salt .= $alpha[int(rand($alpha_count))]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { salt => $salt }}); } else { # Inject a random character $salt .= $seed[int(rand($seed_count))]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { salt => $salt }}); } } return($salt); } =head2 _wrap_to This determines how wide the user's terminal currently is and returns that width, as well as store it in C<< sys::terminal::columns >>. This takes no parameters. If there is a problem reading the column width, C<< 0 >> will be returned. =cut sub _wrap_to { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; # Get the column width my ($columns, $return_code) = $anvil->System->call({debug => $debug, redirect_stderr => 0, shell_call => $anvil->data->{path}{exe}{tput}." cols" }); if ((not defined $columns) or ($columns !~ /^\d+$/)) { # Set 0. $columns = 0; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { columns => $columns }}); } else { # Got a good value $anvil->data->{sys}{terminal}{columns} = $columns; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'sys::terminal::columns' => $anvil->data->{sys}{terminal}{columns} }}); } return($columns); } 1;