#!/usr/bin/perl use strict; use warnings; my($app_svnid) = '$HeadURL$ $LastChangedRevision$'; ## no critic (RequireInterpolationOfMetachars) use lib substr(`ade-config ade_share_prefix`,0,-1) . '/include'; ## no critic (ProhibitBacktickOperators) use ADE; use Getopt::Long qw(:config no_ignore_case); use experimental 'smartmatch'; # for perl 5.18 use Data::Dumper; use List::Util; use Fatal qw( close closedir umask ); # obviate checking close's return code my(%rocon_defined_errors) = ( rocon_err_misc => { fmt => '%s' }, ); # Options my ($opt_mode, $opt_config_file, $opt_timeout, $opt_threads, $opt_ssh_cmd, $opt_quiet, $opt_command); # Other globals my($rocon_etc_prefix); my($rocon_progname); eval `rocon-config --format=perl rocon_etc_prefix`; ## no critic (ProhibitStringyEval, ProhibitBacktickOperators, RequireCheckingReturnValueOfEval) sub rocon { my($errstack_ref) = @_; my($rc, $rocon_conf_handle); # Set ADE options ADE::register_error_types(\%rocon_defined_errors); if (($rc=ADE::get_progname($errstack_ref, \$rocon_progname)) != $ADE::OK) { return($rc); } # Defaults for options $opt_config_file = $ENV{'ROCON_CFG_FILE'} ? $ENV{'ROCON_CFG_FILE'} : sprintf '%s/%s.conf', $rocon_etc_prefix, $rocon_progname; $opt_mode = undef; #$opt_ssh_cmd = ${ROCON_SSH_CMD:-ssh} $opt_ssh_cmd = 'ssh'; $opt_quiet = 0; $opt_timeout= 0; $opt_threads = 200; # Register options if (($rc=ADE::register_options($errstack_ref, 'rwemgc:sF:sqt:iT:i', 'ssh:s,quiet,timeout:i,threads:i', 'main::rocon_opt_handler_%s')) != $ADE::OK) { return($rc); } # Register handler functions if (($rc=ADE::set_callbacks($errstack_ref, \&rocon_usage_help, \&rocon_version, \&rocon_paths)) != $ADE::OK) { return($rc); } # Process options ADE::debug($errstack_ref, 10, 'rocon: processing options ...'); if (($rc=ADE::process_options($errstack_ref)) != $ADE::OK) { return($rc); } # Sanity checks and derivations if (not defined $opt_mode) { ADE::bad_usage() } ADE::debug($errstack_ref, 10, "rocon: opt_config_file=$opt_config_file"); if ($opt_mode ~~ [ qw( read write edit ) ] and $opt_config_file eq '-') { ADE::error($errstack_ref, 'rocon_err_misc', 'edit/read/write modes are meaningless if the config file is stdin'); return($ADE::FAIL); } if (($opt_config_file ne '-') and ($opt_mode ne 'read') and (not -r $opt_config_file)) { ADE::error($errstack_ref, 'rocon_err_misc', "$opt_config_file: can't read"); return($ADE::FAIL); } # We don't want any gnome-ask-pass or such popping up. delete $ENV{'DISPLAY'}; # Guts - delegate if ($opt_mode eq 'read') { $rc = mode_read($errstack_ref); } elsif ($opt_mode eq 'write') { $rc = mode_write($errstack_ref); } elsif ($opt_mode eq 'edit') { $rc = mode_edit($errstack_ref); } elsif ($opt_mode eq 'match') { $rc = mode_match($errstack_ref); } elsif ($opt_mode eq 'groups') { $rc = mode_groups($errstack_ref); } elsif ($opt_mode eq 'command') { $rc = mode_command($errstack_ref, $opt_command); } else { } return ($rc) if ($rc != $ADE::OK); return($ADE::OK); } sub mode_match { my($errstack_ref) = @_; my(@host_records, $perl_exp_string, $rocon_exp_string, $rc); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 < 1); $rocon_exp_string = join ' ', @ARGV; # Guts if (($rc=load_config_file($errstack_ref, $opt_config_file, \@host_records)) != $ADE::OK) { return($rc); } if (($rc=rewrite_rocon_exp_string_as_perl_exp_string($errstack_ref, $rocon_exp_string, \@host_records, \$perl_exp_string)) != $ADE::OK) { return($rc); } printf join '', map { "$_\n" } sort map { ${$_}{'host'} } grep { eval $perl_exp_string } @host_records; ## no critic (ProhibitStringyEval) return($ADE::OK); } sub mode_command { my($errstack_ref, $command) = @_; my(@host_records, $perl_exp_string, $rocon_exp_string, $rc, @commands, $length_of_longest_hostname, @hosts, $failed_count, @rcs); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 < 1); $rocon_exp_string = join ' ', @ARGV; ADE::debug($errstack_ref, 10, "mode_command: rocon_exp_string=$rocon_exp_string"); # Guts if (($rc=load_config_file($errstack_ref, $opt_config_file, \@host_records)) != $ADE::OK) { return($rc); } if (($rc=rewrite_rocon_exp_string_as_perl_exp_string($errstack_ref, $rocon_exp_string, \@host_records, \$perl_exp_string)) != $ADE::OK) { return($rc); } @hosts = sort map { ${$_}{'host'} } grep { eval $perl_exp_string } @host_records; ## no critic (ProhibitStringyEval) $length_of_longest_hostname = List::Util::max(map { length } @hosts); @commands = map { sprintf "{ { { setsid ssh -n $_ '$opt_command'; } 2>&1 1>&3 3>&- | sed 's/^/%s/'; } 3>&1 1>&2 | sed 's/^/%s/'; }", ($opt_quiet ? '' : sprintf '%-*s ', $length_of_longest_hostname+9, "$_\[stderr]:"), ($opt_quiet ? '' : sprintf '%-*s ', $length_of_longest_hostname+9, "$_\[stdout]:") } @hosts; if (($rc=ADE::fork_multi($errstack_ref, $opt_timeout, $opt_threads, \@commands, \@rcs)) != $ADE::OK) { return($rc); } ## Process return codes if ($failed_count = grep { $_ } @rcs) { ADE::set_messaging_parameters($errstack_ref, stack=>$errstack_ref); ADE::error($errstack_ref, 'rocon_err_misc', sprintf 'the following hosts failed: %s', join ', ', map { ($rcs[$_] != 0) ? $hosts[$_] : () } (0..$#rcs)); return($ADE::FAIL); } return($ADE::OK); } sub mode_write { my($errstack_ref) = @_; my($rocon_conf_handle); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 != 0); # Guts if ($opt_config_file eq '-') { $rocon_conf_handle = \*STDIN; } else { if (not open $rocon_conf_handle, '<', $opt_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', "$opt_config_file: not accessible"); return($ADE::FAIL); } } # Import config data. while (<$rocon_conf_handle>) { print; } if ($opt_config_file ne '-') { close $rocon_conf_handle; } return($ADE::OK); } sub mode_groups { my($errstack_ref) = @_; my(@host_records, $rc); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 != 0); # Sanity checks and derivations # Guts if (($rc=load_config_file($errstack_ref, $opt_config_file, \@host_records)) != $ADE::OK) { return($rc); } # Note that the use of: # printf(join('', map { "$_\n" } @list)) # instead of: # printf("%s\n",join('\n', @list)) # means we don't need to handle empty lists specially. Also note Note use of 'keys %{ .... }' # to convert hash reference to hash. Finally, also note Anonymous ordering function # '{ $a cmp $b }' needed as sort expects integers otherwise. printf join '', map { "$_\n" } sort { $a cmp $b } List::Util::uniq(map { keys %{ ${$_}{'memberships'} } } @host_records ); return($ADE::OK); } sub mode_read { my($errstack_ref) = @_; my($tmp_config_file, $tmp_config_handle, $rc); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 != 0); # Sanity checks and derivations if (not -w $opt_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', "$opt_config_file: not writable"); return($ADE::FAIL); } $tmp_config_file = sprintf '/tmp/%s.%d.conf', $rocon_progname, $$; # Guts ADE::register_temp_file($errstack_ref, $tmp_config_file); if (not open $tmp_config_handle, '>', $tmp_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', "$tmp_config_file: couldn't write"); return($ADE::FAIL); } while (<>) { # See 'perldoc -f print' for why its not okay to omit '$_' here. print $tmp_config_handle $_; } close $tmp_config_handle; if (($rc=replace_config_file($errstack_ref, $tmp_config_file)) != $ADE::OK) { return($rc); } if (not unlink $tmp_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', 'unlink failed'); return($ADE::FAIL); } ADE::deregister_temp_file($errstack_ref, $tmp_config_file); return($ADE::OK); } sub mode_edit { my($errstack_ref) = @_; my($tmp_config_file, $rc); # Process arguments ADE::show_bad_usage($errstack_ref) if ($#ARGV+1 != 0); # Sanity checks and derivations if (not -w $opt_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', "$opt_config_file: not writable"); return($ADE::FAIL); } $tmp_config_file = sprintf '/tmp/%s.%d.conf', $rocon_progname, $$; # Guts ADE::debug($errstack_ref, 10, "mode_edit: copying $opt_config_file to $tmp_config_file ..."); ADE::register_temp_file($errstack_ref, $tmp_config_file); if (system "cp $opt_config_file $tmp_config_file") { ADE::error($errstack_ref, 'rocon_err_misc', 'system failed'); return($ADE::FAIL); } if (system "\${EDITOR:-vi} $tmp_config_file") { ADE::error($errstack_ref, 'rocon_err_misc', 'system failed'); return($ADE::FAIL); } if (($rc=replace_config_file($errstack_ref, $tmp_config_file)) != $ADE::OK) { return($rc); } if (not unlink $tmp_config_file) { ADE::error($errstack_ref, 'rocon_err_misc', 'unlink failed'); return($ADE::FAIL); } ADE::deregister_temp_file($errstack_ref, $tmp_config_file); return($ADE::OK); } sub replace_config_file { my($errstack_ref, $new_config_file) = @_; my($rc, @host_records); if (($rc=load_config_file($errstack_ref, $new_config_file, \@host_records)) != $ADE::OK) { return($rc); } if (system "cp $new_config_file $opt_config_file") { ADE::error($errstack_ref, 'rocon_err_misc', 'system failed'); return($ADE::FAIL); } return($ADE::OK); } sub rewrite_rocon_exp_string_as_perl_exp_string { my($errstack_ref, $rocon_exp_string, $host_records_ref, $perl_evaluatable_exp_str_ref) = @_; my($perl_evaluatable_exp_word, @perl_evaluatable_exp_words, $rocon_exp_string2); my($perl_validatable_exp_word, @perl_validatable_exp_words, $perl_validatable_exp_str); my(%host_record); $rocon_exp_string = join ' ', @ARGV; $rocon_exp_string2 = $rocon_exp_string; # Convert rocon expression to perl expression # Pre-pad '((' and '))' to avoid split taking them as one word. $rocon_exp_string =~ s/([\(\)])/ $1 /g; # Squeeze multiple blanks to avoid split extracting the empty string between two blanks. $rocon_exp_string =~ s/(^ *| *$| +(?= ))//g; foreach my $rocon_exp_word (split / /, $rocon_exp_string) { ADE::debug($errstack_ref, 10, "rewrite_rocon_exp_string_as_perl_exp_string: rocon_exp_word=$rocon_exp_word"); # Keywords are replaced with lower case keywords (because 'NOT' etc are not understood by perl) if (lc $rocon_exp_word ~~ [ qw(\( \) and or not) ]) { $perl_evaluatable_exp_word = lc $rocon_exp_word; $perl_validatable_exp_word = lc $rocon_exp_word; # Integers and floats go through } elsif ($rocon_exp_word =~ /^\d+(?:|\.\d+)$/ or $rocon_exp_word =~ /^\.\d+$/) { $perl_evaluatable_exp_word = $rocon_exp_word; $perl_validatable_exp_word = lc $rocon_exp_word; # Anything else is a hostgroup or hostname. } elsif ($rocon_exp_word =~ /^[A-Za-z][-A-Za-z0-9_]*$/) { # Some private variables used just in this clause. my($matched_flag) = 0; # Scan all host records for the current word ... foreach my $host_record_ref (@{$host_records_ref}) { # ... in the 'host' attribute and ... if ($rocon_exp_word eq ${$host_record_ref}{'host'}) { $matched_flag = 1; last; } # ... and in keys of the 'memberships' attribute (which is a hash). foreach my $host_group (keys %{${$host_record_ref}{'memberships'}}) { if ($rocon_exp_word eq $host_group) { $matched_flag = 1; last; } } # That last 'last' broke out of scanning remaining host's memberships; # this one below breaks out of scanning remaining hosts. last if ($matched_flag); } if (not $matched_flag) { ADE::error($errstack_ref, 'rocon_err_misc', "$rocon_exp_word: not a known host or hostgroup"); return($ADE::FAIL); } $perl_evaluatable_exp_word = sprintf '(defined ${$_}{\'memberships\'}{\'%s\'} or (${$_}{\'host\'} eq \'%s\'))', $rocon_exp_word, $rocon_exp_word; ## no critic (RequireInterpolationOfMetachars) $perl_validatable_exp_word = 1; } else { ADE::error($errstack_ref, 'rocon_err_misc', "$rocon_exp_word: invalid token"); return($ADE::FAIL); } push @perl_evaluatable_exp_words, $perl_evaluatable_exp_word; push @perl_validatable_exp_words, $perl_validatable_exp_word; } ${$perl_evaluatable_exp_str_ref} = join ' ', @perl_evaluatable_exp_words; $perl_validatable_exp_str = join ' ', @perl_validatable_exp_words; ADE::debug($errstack_ref, 10, "rewrite_rocon_exp_string_as_perl_exp_string: perl_evaluatable_exp_str_ref=${$perl_evaluatable_exp_str_ref}"); ADE::debug($errstack_ref, 10, "rewrite_rocon_exp_string_as_perl_exp_string: perl_validatable_exp_str=$perl_validatable_exp_str"); # Check expression is valid. if (not defined eval $perl_validatable_exp_str) { ## no critic (ProhibitStringyEval) ADE::error($errstack_ref, 'rocon_err_misc', "$rocon_exp_string2: invalid expression"); return($ADE::FAIL); } return($ADE::OK); } sub load_config_file { my ($errstack_ref, $config_file, $host_records_ref) = @_; my ($rocon_conf_handle, %host_record); my ($host, $comma_separated_hostgroup_list, $hostgroup); # Guts start here #@host_records = (); # Open config file if ($config_file eq '-') { $rocon_conf_handle = \*STDIN; } else { if (not open $rocon_conf_handle, '<', $config_file) { ADE::error($errstack_ref, 'rocon_err_misc', "$config_file: not accessible"); return($ADE::FAIL); } } # Import config data. while (<$rocon_conf_handle>) { chomp; ADE::debug($errstack_ref, 10, "load_config_file: \$_=$_"); # Ignore blank lines and comments (for SOFT-66) next if (/^\s*(?:#|$)/); if (not (($host, $comma_separated_hostgroup_list) = (/^([A-Za-z][-A-Za-z0-9_]*):([A-Za-z][-A-Za-z0-9_]*(?:,[A-Za-z][-A-Za-z0-9_]*)*)/))) { ## no critic (ProhibitComplexRegexes) ADE::error($errstack_ref, 'rocon_err_misc', "$_: invalid line"); return($ADE::FAIL); } ADE::debug($errstack_ref, 10, "load_config_file: host=$host, comma_separated_hostgroup_list=$comma_separated_hostgroup_list"); $host_record{'host'} = $host; foreach my $hostgroup (split /,/, $comma_separated_hostgroup_list) { # Value is unimportant; it is whether it is defined or not that we test. $host_record{'memberships'}{$hostgroup} = 1; } push @{$host_records_ref}, { %host_record }; undef %host_record; } # Close config file. if ($config_file ne '-') { close $rocon_conf_handle; } ADE::debug($errstack_ref, 11, 'load_config_file: Dump: ' . Data::Dumper->Dump([$host_records_ref], [qw/$host_records_ref/])); return($ADE::OK); } sub rocon_opt_handler_r { my($errstack_ref) = @_; $opt_mode = 'read'; return $ADE::OK; } sub rocon_opt_handler_read ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_r(@_)); } sub rocon_opt_handler_w { my($errstack_ref) = @_; $opt_mode = 'write'; return $ADE::OK; } sub rocon_opt_handler_write ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_w(@_)); } sub rocon_opt_handler_e { my($errstack_ref) = @_; $opt_mode = 'edit'; return $ADE::OK; } sub rocon_opt_handler_edit ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_e(@_)); } sub rocon_opt_handler_m { my($errstack_ref) = @_; $opt_mode = 'match'; return $ADE::OK; } sub rocon_opt_handler_match ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_m(@_)); } sub rocon_opt_handler_g { my($errstack_ref) = @_; $opt_mode = 'groups'; return $ADE::OK; } sub rocon_opt_handler_groups ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_g(@_)); } sub rocon_opt_handler_c { my($errstack_ref, $local_command) = @_; $opt_mode = 'command'; $opt_command = $local_command; #ADE::debug($errstack_ref, 10, "rocon_opt_handler_c: opt_mode=$opt_mode, opt_command=$opt_command"); return $ADE::OK; } sub rocon_opt_handler_command ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_c(@_)); } sub rocon_opt_handler_F ## no critic (Capitalization) { my($errstack_ref, $local_config_file) = @_; $opt_config_file = $local_config_file; return $ADE::OK; } sub rocon_opt_handler_config ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_F(@_)); } sub rocon_opt_handler_ssh { my($errstack_ref, $local_ssh_cmd) = @_; $opt_ssh_cmd = $local_ssh_cmd; return $ADE::OK; } sub rocon_opt_handler_q { my($errstack_ref) = @_; $opt_quiet = 1; return $ADE::OK; } sub rocon_opt_handler_quiet ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_q(@_)); } sub rocon_opt_handler_t { my($errstack_ref, $local_timeout) = @_; $opt_timeout = $local_timeout; return $ADE::OK; } sub rocon_opt_handler_timeout ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_t(@_)); } sub rocon_opt_handler_T ## no critic (Capitalization) { my($errstack_ref, $local_threads) = @_; $opt_threads = $local_threads; return $ADE::OK; } sub rocon_opt_handler_threads ## no critic (RequireArgUnpacking) { return(rocon_opt_handler_T(@_)); } sub rocon_version { my($errstack_ref, $version_text_ref) = @_; return(ADE::extract_version($errstack_ref, $app_svnid, $version_text_ref)); } sub rocon_paths { my($errstack_ref, $pathlist_text_ref) = @_; my($rc); ${$pathlist_text_ref} = "Config-File: $opt_config_file\n" . "Ssh-Cmd: $opt_ssh_cmd"; return($ADE::OK); } sub rocon_usage_help { my($errstack_ref, $usage_text_short_ref, $usage_text_long_ref) = @_; ${$usage_text_short_ref} = "{ -e | { -c | -m } | -r | -w | -g }\n"; ${$usage_text_long_ref} = " -e edit the config file\n" . " -c run on matching hosts\n" . " -m list matching hosts\n" . " -w read config file, write to stdout\n" . " -F use alternative config file\n" . " -r read stdin, write to config file\n" . " -g list groups\n" . " -q | --quiet quieten command output reporting\n" . " --ssh= path to ssh command\n" . " -t | --timeout= set timeout (default: $opt_timeout)\n" . " -T | --threads= set threads (default: $opt_threads)\n"; return($ADE::OK); } ADE::main(\&rocon);