From 8d5e3adf6a362c1043cd3d8c68595fed6b846b3f Mon Sep 17 00:00:00 2001 From: fbsanchez Date: Wed, 30 Sep 2020 13:09:48 +0200 Subject: [PATCH] WIP Omnishell common libraries --- pandora_agents/unix/pandora_agent | 349 +-------------- .../win32/omnishell/omnishell_client.pl | 21 + pandora_server/lib/PandoraFMS/Omnishell.pm | 400 ++++++++++++++++++ 3 files changed, 426 insertions(+), 344 deletions(-) create mode 100644 pandora_agents/win32/omnishell/omnishell_client.pl create mode 100644 pandora_server/lib/PandoraFMS/Omnishell.pm diff --git a/pandora_agents/unix/pandora_agent b/pandora_agents/unix/pandora_agent index 35caf972ea..50195aea2a 100755 --- a/pandora_agents/unix/pandora_agent +++ b/pandora_agents/unix/pandora_agent @@ -33,18 +33,6 @@ use IO::Socket; use Sys::Syslog; use Time::Local; -my $YAML = 0; -# Dynamic load. Avoid unwanted behaviour. -eval { - eval 'require YAML::Tiny;1' or die('YAML::Tiny lib not found, commands feature won\'t be available'); -}; -if ($@) { - $YAML = 0; - print STDERR $@; -} else { - $YAML = 1; -} - # Agent XML data my $Xml; @@ -1488,336 +1476,6 @@ sub check_collections () { } } -################################################################################ -# Check for remote commands defined. -################################################################################ -sub prepare_commands { - if ($YAML == 0) { - log_message( - 'error', - 'Cannot use commands without YAML dependency, please install it.' - ); - return; - } - - # Force configuration file read. - my @commands = read_config('cmd_file'); - - if (empty(\@commands)) { - $Conf{'commands'} = {}; - } else { - foreach my $rcmd (@commands) { - $Conf{'commands'}->{trim($rcmd)} = {}; - } - } - - # Cleanup old commands. Not registered. - cleanup_old_commands(); - - foreach my $ref (keys %{$Conf{'commands'}}) { - my $file_content; - my $download = 0; - my $rcmd_file = $ConfDir.'/commands/'.$ref.'.rcmd'; - - # Check for local .rcmd.done files - if (-e $rcmd_file.'.done') { - # Ignore. - delete $Conf{'commands'}->{$ref}; - next; - } - - # Search for local .rcmd file - if (-e $rcmd_file) { - my $remote_md5_file = $Conf{'temporal'}.'/'.$ref.'.md5'; - - $file_content = read_file($rcmd_file); - if (recv_file($ref.'.md5', $remote_md5_file) != 0) { - # Remote file could not be retrieved, skip. - delete $Conf{'commands'}->{$ref}; - next; - } - - my $local_md5 = md5($file_content); - my $remote_md5 = md5(read_file($remote_md5_file)); - - if ($local_md5 ne $remote_md5) { - # Must be downloaded again. - $download = 1; - } - } else { - $download = 1; - } - - # Search for remote .rcmd file - if ($download == 1) { - # Download .rcmd file - if (recv_file($ref.'.rcmd') != 0) { - # Remote file could not be retrieved, skip. - delete $Conf{'commands'}->{$ref}; - next; - } else { - # Success - move($Conf{'temporal'}.'/'.$ref.'.rcmd', $rcmd_file); - } - } - - # Parse and prepare in memory skel. - eval { - $Conf{'commands'}->{$ref} = YAML::Tiny->read($rcmd_file); - }; - if ($@) { - # Failed. - log_message('error', 'Failed to decode command. ' . "\n".$@); - delete $Conf{'commands'}->{$ref}; - next; - } - - } -} - -################################################################################ -# Command report. -################################################################################ -sub report_command { - my ($ref, $err_level) = @_; - - # Retrieve content from .stdout and .stderr - my $stdout_file = $Conf{'temporal'}.'/'.$ref.'.stdout'; - my $stderr_file = $Conf{'temporal'}.'/'.$ref.'.stderr'; - - my $return; - eval { - $return = { - 'error_level' => $err_level, - 'stdout' => read_file($stdout_file), - 'stderr' => read_file($stderr_file), - }; - - $return->{'name'} = $Conf{'commands'}->{$ref}->[0]->{'name'}; - }; - if ($@) { - log_message('error', 'Failed to report command output. ' . $@); - } - - # Cleanup - unlink($stdout_file) if (-e $stdout_file); - unlink($stderr_file) if (-e $stderr_file); - - # Mark command as done. - open (my $R_FILE, '> '.$ConfDir.'/commands/'.$ref.'.rcmd.done'); - print $R_FILE $err_level; - close($R_FILE); - - - $return->{'stdout'} = '' unless defined ($return->{'stdout'}); - $return->{'stderr'} = '' unless defined ($return->{'stderr'}); - - return $return; -} - -################################################################################ -# Cleanup unreferenced rcmd and rcmd.done files. -################################################################################ -sub cleanup_old_commands { - # Cleanup old .rcmd and .rcmd.done files. - my %registered = map { $_.'.rcmd' => 1 } keys %{$Conf{'commands'}}; - if(opendir(my $dir, $ConfDir.'/commands/')) { - while (my $item = readdir($dir)) { - - # Skip other files. - next if ($item !~ /\.rcmd$/); - - # Clean .rcmd.done file if its command is not referenced in conf. - if (!defined($registered{$item})) { - if (-e $ConfDir.'/commands/'.$item) { - unlink($ConfDir.'/commands/'.$item); - } - if (-e $ConfDir.'/commands/'.$item.'.done') { - unlink($ConfDir.'/commands/'.$item.'.done'); - } - } - } - - # Close dir. - closedir($dir); - } - -} - -################################################################################ -# Executes a command using defined timeout. -################################################################################ -sub execute_command_timeout { - my ($cmd, $timeout) = @_; - - if (!defined($timeout) - || !looks_like_number($timeout) - || $timeout <= 0 - ) { - `$cmd`; - return $?>>8; - } - - my $remaining_timeout = $timeout; - - my $RET; - my $output; - - my $pid = open ($RET, "-|"); - if (!defined($pid)) { - # Failed to fork. - log_message('error', '[command] Failed to fork.'); - return undef; - } - if ($pid == 0) { - # Child. - my $ret; - eval { - local $SIG{ALRM} = sub { die "timeout\n" }; - alarm $timeout; - `$cmd`; - alarm 0; - }; - - my $result = ($?>>8); - return $result; - - # Exit child. - # Child finishes. - exit; - - } else { - # Parent waiting. - while( --$remaining_timeout > 0 ){ - if (wait == -1) { - last; - } - # Wait child up to timeout seconds. - sleep 1; - } - } - - if ($remaining_timeout > 0) { - # Retrieve output from child. - $output = do { local $/; <$RET> }; - $output = $output>>8; - } - else { - # Timeout expired. - return 124; - } - - close($RET); - - return $output; -} - -################################################################################ -# Executes a block of commands, returns error level, leaves output in -# redirection set by $std_files. E.g: -# $std_files = ' >> /tmp/stdout 2>> /tmp/stderr -################################################################################ -sub execute_command_block { - my ($commands, $std_files, $timeout, $retry) = @_; - - return 0 unless defined($commands); - - my $retries = $retry; - - $retries = 1 unless looks_like_number($retries) && $retries > 0; - - my $err_level = 0; - $std_files = '' unless defined ($std_files); - - if (ref($commands) ne "ARRAY") { - return 0 if $commands eq ''; - - do { - $err_level = execute_command_timeout( - "($commands) $std_files", - $timeout - ); - - # Do not retry if success. - last if looks_like_number($err_level) && $err_level == 0; - } while ((--$retries) > 0); - - } else { - foreach my $comm (@{$commands}) { - next unless defined($comm); - $retries = $retry; - $retries = 1 unless looks_like_number($retries) && $retries > 0; - - do { - $err_level = execute_command_timeout( - "($comm) $std_files", - $timeout - ); - - # Do not retry if success. - $retries = 0 if looks_like_number($err_level) && $err_level == 0; - - } while ((--$retries) > 0); - - # Do not continue evaluating block if failed. - last unless ($err_level == 0); - } - } - - return $err_level; -} - -################################################################################ -# Evalate given command. -################################################################################ -sub evaluate_command { - my ($ref) = @_; - - # Not found. - return unless defined $Conf{'commands'}->{$ref}; - - # Already completed. - return if (-e $ConfDir.'/commands/'.$ref.'.rcmd.done'); - - # [0] because how library works. - my $cmd = $Conf{'commands'}->{$ref}->[0]; - - my $std_files = ' >> '.$Conf{'temporal'}.'/'.$ref.'.stdout '; - $std_files .= ' 2>> '.$Conf{'temporal'}.'/'.$ref.'.stderr '; - - # Check preconditions - my $err_level; - - $err_level = execute_command_block( - $cmd->{'preconditions'}, - $std_files, - $cmd->{'timeout'} - ); - - # Precondition not satisfied. - return report_command($ref, $err_level) unless ($err_level == 0); - - # Main run. - $err_level = execute_command_block( - $cmd->{'script'}, - $std_files, - $cmd->{'timeout'} - ); - - # Script not success. - return report_command($ref, $err_level) unless ($err_level == 0); - - # Check postconditions - $err_level = execute_command_block( - $cmd->{'postconditions'}, - $std_files, - $cmd->{'timeout'} - ); - - # Return results. - return report_command($ref, $err_level); -} - ################################################################################ # Sleep function ################################################################################ @@ -3453,8 +3111,11 @@ while (1) { # Check file collections check_collections () unless ($Conf{'debug'} eq '1'); - # Check scheduled commands - prepare_commands() unless ($Conf{'debug'} eq '1'); + if ($Conf{'debug'} ne '1') { + # Check scheduled commands + my $omni = new PandoraFMS::Omnishell(\%Conf); + $omni->prepare_commands(); + } # Launch broker agents @BrokerPid = (); diff --git a/pandora_agents/win32/omnishell/omnishell_client.pl b/pandora_agents/win32/omnishell/omnishell_client.pl new file mode 100644 index 0000000000..5abfec8797 --- /dev/null +++ b/pandora_agents/win32/omnishell/omnishell_client.pl @@ -0,0 +1,21 @@ +#!/usr/bin/perl +################################################################################ +# Pandora FMS Omnishell client. +# +# (c) Fco de Borja Sánchez +# +################################################################################ +use strict; +use warnings; + +use lib '/usr/lib/perl5'; +use PandoraFMS::Tools; +use PandoraFMS::Omnishell; + +my %Conf; + +if ($Conf{'debug'} ne '1') { + # Check scheduled commands + my $omni = new PandoraFMS::Omnishell(\%Conf); + $omni->prepare_commands(); +} \ No newline at end of file diff --git a/pandora_server/lib/PandoraFMS/Omnishell.pm b/pandora_server/lib/PandoraFMS/Omnishell.pm new file mode 100644 index 0000000000..b47955470c --- /dev/null +++ b/pandora_server/lib/PandoraFMS/Omnishell.pm @@ -0,0 +1,400 @@ +package PandoraFMS::Omnishell; +################################################################################ +# Pandora FMS Omnishell common functions. +# +# (c) Fco de Borja Sánchez +# +################################################################################ +use strict; +use warnings; + +my $YAML = 0; +# Dynamic load. Avoid unwanted behaviour. +eval { + eval 'require YAML::Tiny;1' or die('YAML::Tiny lib not found, commands feature won\'t be available'); +}; +if ($@) { + $YAML = 0; +} else { + $YAML = 1; +} + +use lib '/usr/lib/perl5'; + +our @ISA = ("Exporter"); +our %EXPORT_TAGS = ( 'all' => [ qw( ) ] ); +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +our @EXPORT = qw(); + + +################################################################################ +# return last error. +################################################################################ +sub get_last_error { + my ($self) = @_; + + if (!is_empty($self->{'last_error'})) { + return $self->{'last_error'}; + } + + return ''; +} + + +################################################################################ +# Create new omnishell handler. +################################################################################ +sub new { + my ($class,$args) = @_; + + if (ref($args) ne 'HASH') { + return undef; + } + + my $self = { + 'last_error' => undef, + %{$args} + }; + + + $self = bless($self, $class); + + return $self; +} + + +################################################################################ +# Check for remote commands defined. +################################################################################ +sub prepare_commands { + my ($self) = @_; + + if ($YAML == 0) { + log_message( + 'error', + 'Cannot use commands without YAML dependency, please install it.' + ); + return; + } + + # Force configuration file read. + my @commands = read_config('cmd_file'); + + if (empty(\@commands)) { + $self->{'commands'} = {}; + } else { + foreach my $rcmd (@commands) { + $self->{'commands'}->{trim($rcmd)} = {}; + } + } + + # Cleanup old commands. Not registered. + cleanup_old_commands(); + + foreach my $ref (keys %{$self->{'commands'}}) { + my $file_content; + my $download = 0; + my $rcmd_file = $self->{'ConfDir'}.'/commands/'.$ref.'.rcmd'; + + # Check for local .rcmd.done files + if (-e $rcmd_file.'.done') { + # Ignore. + delete $self->{'commands'}->{$ref}; + next; + } + + # Search for local .rcmd file + if (-e $rcmd_file) { + my $remote_md5_file = $self->{'temporal'}.'/'.$ref.'.md5'; + + $file_content = read_file($rcmd_file); + if (recv_file($ref.'.md5', $remote_md5_file) != 0) { + # Remote file could not be retrieved, skip. + delete $self->{'commands'}->{$ref}; + next; + } + + my $local_md5 = md5($file_content); + my $remote_md5 = md5(read_file($remote_md5_file)); + + if ($local_md5 ne $remote_md5) { + # Must be downloaded again. + $download = 1; + } + } else { + $download = 1; + } + + # Search for remote .rcmd file + if ($download == 1) { + # Download .rcmd file + if (recv_file($ref.'.rcmd') != 0) { + # Remote file could not be retrieved, skip. + delete $self->{'commands'}->{$ref}; + next; + } else { + # Success + move($self->{'temporal'}.'/'.$ref.'.rcmd', $rcmd_file); + } + } + + # Parse and prepare in memory skel. + eval { + $self->{'commands'}->{$ref} = YAML::Tiny->read($rcmd_file); + }; + if ($@) { + # Failed. + log_message('error', 'Failed to decode command. ' . "\n".$@); + delete $self->{'commands'}->{$ref}; + next; + } + + } +} + +################################################################################ +# Command report. +################################################################################ +sub report_command { + my ($self, $ref, $err_level) = @_; + + # Retrieve content from .stdout and .stderr + my $stdout_file = $self->{'temporal'}.'/'.$ref.'.stdout'; + my $stderr_file = $self->{'temporal'}.'/'.$ref.'.stderr'; + + my $return; + eval { + $return = { + 'error_level' => $err_level, + 'stdout' => read_file($stdout_file), + 'stderr' => read_file($stderr_file), + }; + + $return->{'name'} = $self->{'commands'}->{$ref}->[0]->{'name'}; + }; + if ($@) { + log_message('error', 'Failed to report command output. ' . $@); + } + + # Cleanup + unlink($stdout_file) if (-e $stdout_file); + unlink($stderr_file) if (-e $stderr_file); + + # Mark command as done. + open (my $R_FILE, '> '.$self->{'ConfDir'}.'/commands/'.$ref.'.rcmd.done'); + print $R_FILE $err_level; + close($R_FILE); + + + $return->{'stdout'} = '' unless defined ($return->{'stdout'}); + $return->{'stderr'} = '' unless defined ($return->{'stderr'}); + + return $return; +} + +################################################################################ +# Cleanup unreferenced rcmd and rcmd.done files. +################################################################################ +sub cleanup_old_commands { + my ($self) = @_; + + # Cleanup old .rcmd and .rcmd.done files. + my %registered = map { $_.'.rcmd' => 1 } keys %{$self->{'commands'}}; + if(opendir(my $dir, $self->{'ConfDir'}.'/commands/')) { + while (my $item = readdir($dir)) { + + # Skip other files. + next if ($item !~ /\.rcmd$/); + + # Clean .rcmd.done file if its command is not referenced in conf. + if (!defined($registered{$item})) { + if (-e $self->{'ConfDir'}.'/commands/'.$item) { + unlink($self->{'ConfDir'}.'/commands/'.$item); + } + if (-e $self->{'ConfDir'}.'/commands/'.$item.'.done') { + unlink($self->{'ConfDir'}.'/commands/'.$item.'.done'); + } + } + } + + # Close dir. + closedir($dir); + } + +} + +################################################################################ +# Executes a command using defined timeout. +################################################################################ +sub execute_command_timeout { + my ($cmd, $timeout) = @_; + + if (!defined($timeout) + || !looks_like_number($timeout) + || $timeout <= 0 + ) { + `$cmd`; + return $?>>8; + } + + my $remaining_timeout = $timeout; + + my $RET; + my $output; + + my $pid = open ($RET, "-|"); + if (!defined($pid)) { + # Failed to fork. + log_message('error', '[command] Failed to fork.'); + return undef; + } + if ($pid == 0) { + # Child. + my $ret; + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm $timeout; + `$cmd`; + alarm 0; + }; + + my $result = ($?>>8); + return $result; + + # Exit child. + # Child finishes. + exit; + + } else { + # Parent waiting. + while( --$remaining_timeout > 0 ){ + if (wait == -1) { + last; + } + # Wait child up to timeout seconds. + sleep 1; + } + } + + if ($remaining_timeout > 0) { + # Retrieve output from child. + $output = do { local $/; <$RET> }; + $output = $output>>8; + } + else { + # Timeout expired. + return 124; + } + + close($RET); + + return $output; +} + +################################################################################ +# Executes a block of commands, returns error level, leaves output in +# redirection set by $std_files. E.g: +# $std_files = ' >> /tmp/stdout 2>> /tmp/stderr +################################################################################ +sub execute_command_block { + my ($commands, $std_files, $timeout, $retry) = @_; + + return 0 unless defined($commands); + + my $retries = $retry; + + $retries = 1 unless looks_like_number($retries) && $retries > 0; + + my $err_level = 0; + $std_files = '' unless defined ($std_files); + + if (ref($commands) ne "ARRAY") { + return 0 if $commands eq ''; + + do { + $err_level = execute_command_timeout( + "($commands) $std_files", + $timeout + ); + + # Do not retry if success. + last if looks_like_number($err_level) && $err_level == 0; + } while ((--$retries) > 0); + + } else { + foreach my $comm (@{$commands}) { + next unless defined($comm); + $retries = $retry; + $retries = 1 unless looks_like_number($retries) && $retries > 0; + + do { + $err_level = execute_command_timeout( + "($comm) $std_files", + $timeout + ); + + # Do not retry if success. + $retries = 0 if looks_like_number($err_level) && $err_level == 0; + + } while ((--$retries) > 0); + + # Do not continue evaluating block if failed. + last unless ($err_level == 0); + } + } + + return $err_level; +} + +################################################################################ +# Evalate given command. +################################################################################ +sub evaluate_command { + my ($self, $ref) = @_; + + # Not found. + return unless defined $self->{'commands'}->{$ref}; + + # Already completed. + return if (-e $self->{'ConfDir'}.'/commands/'.$ref.'.rcmd.done'); + + # [0] because how library works. + my $cmd = $self->{'commands'}->{$ref}->[0]; + + my $std_files = ' >> '.$self->{'temporal'}.'/'.$ref.'.stdout '; + $std_files .= ' 2>> '.$self->{'temporal'}.'/'.$ref.'.stderr '; + + # Check preconditions + my $err_level; + + $err_level = execute_command_block( + $cmd->{'preconditions'}, + $std_files, + $cmd->{'timeout'} + ); + + # Precondition not satisfied. + return report_command($ref, $err_level) unless ($err_level == 0); + + # Main run. + $err_level = execute_command_block( + $cmd->{'script'}, + $std_files, + $cmd->{'timeout'} + ); + + # Script not success. + return report_command($ref, $err_level) unless ($err_level == 0); + + # Check postconditions + $err_level = execute_command_block( + $cmd->{'postconditions'}, + $std_files, + $cmd->{'timeout'} + ); + + # Return results. + return report_command($ref, $err_level); +} + +1; \ No newline at end of file