diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2019-11-28 20:41:14 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2019-11-28 21:19:40 +0200 |
commit | 2ec31a163a6e6dc690aa7117a70374059451db76 (patch) | |
tree | ac21dbe4f9abfe9578d0b2ba133e4ea6dbc1b29f | |
parent | 1fdeaf5c01c4f2be12bc65d240baa2245ab03208 (diff) | |
download | rex-2ec31a163a6e6dc690aa7117a70374059451db76.tar.gz rex-2ec31a163a6e6dc690aa7117a70374059451db76.tar.bz2 |
rex group: initial implementation
Selected hosts and host groups are kept on a stack. The host group
on top of the stack is the current one. The 'rex group' command is
provided to maintain the host group stack. It takes four subcommands:
git push NAME push current group selection on stack and
select NAME as a new host group
git pop pop current hostgroup selection off stack
git show display the currently selected host group
(and, optionally, hosts in it)
git select COMMAND create a temporary unnamed hostgroup from
hosts on which COMMAND exits with 0 status
and push it on stack
-rw-r--r-- | NEWS | 9 | ||||
-rwxr-xr-x | rex | 335 |
2 files changed, 290 insertions, 54 deletions
@@ -1,10 +1,9 @@ -Rex NEWS -- history of user-visible changes. 2016-10-01 -Copyright (C) 2012-2016 Sergey Poznyakoff +Rex NEWS -- history of user-visible changes. 2019-11-28 See the end of file for copying conditions. Please send bug reports and suggestions to <gray+rex@gnu.org.ua> -Version 4.0 (Git) +Version 4.0, 2016-10-01 This release provide a set of commands for use in rc files. The use of set statements and direct modification of the config array are @@ -99,7 +98,7 @@ new rc: option no-host-header on -Version 3.0 (Git) +Version 3.0, 2016-08-29 Major rewrite. @@ -204,7 +203,7 @@ Initial version. ========================================================================= Copyright information: -Copyright (C) 2012-2016 Sergey Poznyakoff +Copyright (C) 2012-2019 Sergey Poznyakoff Permission is granted to anyone to make or distribute verbatim copies of this document as received, in any medium, provided that the @@ -20,6 +20,7 @@ exec expect "$0" -- "$@" set version "4.0" set sysconfdir "/etc/rex" set usrconfdir "$env(HOME)/.rex" +set defaultfile "$usrconfdir/default" set confpath [list $usrconfdir $sysconfdir] set libpath [list $usrconfdir/script $sysconfdir/script] @@ -157,7 +158,6 @@ proc catch_config_error {prog code} { } } default { - puts "B" return -code [dict get $options -code] -options $options } } @@ -167,7 +167,6 @@ proc option_set {args} { global optiondef global config -# puts "option_set [llength $args] $args" if {[llength $args] < 2 || [llength $args] > 3} { return -code error "bad # args" } @@ -699,7 +698,7 @@ namespace eval ::getopt { append line " $word" } } - if {[string length $line] > 0} { + if {[info exists line] && [string length $line] > 0} { puts $line } } @@ -2197,7 +2196,7 @@ proc runcmd {hosts command} { {set +e} } } - + while {[job count $batch] > 0} { expect { -i [job sidlist $batch] @@ -2289,6 +2288,11 @@ proc runcmd {hosts command} { ::hostproc::getline $host $text } + {STATUS} { + if {[regexp -- {([0-9]+)\r} $expect_out(0,string) x status]} { + job set batch $sid status $status + } + } {STTY} { if {[regexp -- {REXDIR=(.+)\r} $expect_out(0,string) x dir]} { job set batch $sid wd $dir @@ -2328,6 +2332,10 @@ proc runcmd {hosts command} { } } {COMMAND} { + send -i $sid "echo $?\r" + job transition batch $sid STATUS + } + {STATUS} { if {[job exists $batch $sid wd]} { set host [job get $batch $sid host] debug 3 "[job id $batch $sid]: removing $host:[job get $batch $sid wd]" @@ -2378,11 +2386,13 @@ proc runcmd {hosts command} { set sid $expect_out(spawn_id) switch -- [job get $batch $sid state] { {LOGOUT} { - debug 2 "EOF from [job get $batch $sid hostname], waiting for $sid" + set hostname [job get $batch $sid hostname] + debug 2 "EOF from $hostname, waiting for $sid" + set res [job get $batch $sid status] job close batch $sid wait -i $sid - ::hostproc::finish $host output($host) + ::hostproc::finish $hostname output($host) $res } {COPY} { debug 3 "[job get $batch $sid cmd] finished" @@ -2448,18 +2458,22 @@ namespace eval ::hostproc { puts -nonewline $line } + proc logout {host ref status} {} + # The finish function is called when EOF has been received from a host. # The REF argument is the name of the variable which, if exists, contains - # full text received from the host. - proc finish {host ref} { + # full text received from the host. + # STATUS is the return status as received from wait. + proc finish {host ref status} { upvar $ref text + logout $host $ref $status if {[info exists text]} { if {![config_option no-host-header]} { - puts "[hostname $host]:" + puts "$host:" } foreach line $text { if {[config_option prefix]} { - puts -nonewline "[hostname $host]> " + puts -nonewline "$host> " } puts -nonewline $line } @@ -2474,6 +2488,7 @@ namespace eval ::config { variable initialized 0 variable cfgvars {sudo hosts user password command} variable update_hint + variable hostgroup_stack array set update_hint { hosts {var { return "host $var" }} sudo {var { if {$var} { @@ -2687,7 +2702,7 @@ namespace eval ::config { variable cfgvars variable initialized variable update_hint - + foreach var $cfgvars { if {[exists $var]} { terror -nosave "warning: your rc file sets obsolete variable \"$var\"" @@ -2717,6 +2732,29 @@ namespace eval ::config { debug 3 "config(hosts) = $config(hosts)" } } + + proc push {} { + global config + array set cv {} + foreach key { option,hostgroup default,selected } { + if [info exists config($key)] { + set cv($key) $config($key) + } + } + + lappend config(default,hostgroup_stack) [array get cv] + } + + proc pop {} { + global config + unset -nocomplain config(option,hostgroup) + unset -nocomplain config(default,selected) + if [info exists config(default,hostgroup_stack)] { + array set config [lindex $config(default,hostgroup_stack) end] + set config(default,hostgroup_stack) \ + [lreplace $config(default,hostgroup_stack) end end] + } + } } proc scanlibpath {file} { @@ -2781,7 +2819,7 @@ proc hostlist_setup {} { } } } - + if {![info exists config(hosts)] || [llength $config(hosts)] == 0} { if {![info exists config(option,hostgroup)]} { terror "no host list" @@ -2884,10 +2922,48 @@ proc ispipeline {cmd} { regexp -- {(^(if|case|for|while|time|function|select)[[:space:]])|[|&><;]} $cmd } +proc read_hostgroup {groupname} { + global confpath + + unset -nocomplain cfg + foreach dir $confpath { + set groupdir "$dir/hostgroup" + if {![file exists $groupdir]} { + lappend missing $groupdir + continue + } + set cfg "$groupdir/$groupname/rc" + if {[file exists $cfg]} { + debug 2 "reading configuration file $cfg" + ::config::read $cfg + break + } + unset -nocomplain cfg + } + if {![info exists cfg]} { + terror "no such hostgroup: $groupname" + if {[info exists missing]} { + if {[llength $missing] == [llength $confpath]} { + terror "at least one of the following directories must exist:" + foreach dir $missing { + terror " $dir" + } + } elseif {[config_option verbose]} { + warning "the following directories don't exist:" + foreach dir $missing { + terror " $dir" + } + } + } + exit 1 + } +} + proc rex_parse_cmdline {args} { global argc argv argv0 set arglist {} + set nodefault false while {[llength $args] > 0} { switch -- [lindex $args 0] { -usage - @@ -2898,6 +2974,10 @@ proc rex_parse_cmdline {args} { set opt [lshift args] lappend arglist $opt [lshift args] } + -nodefault { + lshift args + set nodefault true + } default { break } } } @@ -2910,6 +2990,7 @@ proc rex_parse_cmdline {args} { global rexdb global confpath global config + global defaultfile foreach dir $confpath { set db "$dir/db" @@ -2933,45 +3014,27 @@ proc rex_parse_cmdline {args} { } } } + if !$nodefault { + readdefault + } } catch_config_error {::getopt::run $parser global } 1 # Read hostgroup config - if {[info exists config(option,hostgroup)]} { - unset -nocomplain cfg - foreach dir $confpath { - set groupdir "$dir/hostgroup" - if {![file exists $groupdir]} { - lappend missing $groupdir - continue - } - set cfg "$groupdir/[config_option hostgroup]/rc" - if {[file exists $cfg]} { - debug 2 "reading configuration file $cfg" - ::config::read $cfg - break - } - unset -nocomplain cfg - } - if {![info exists cfg]} { - terror "no such hostgroup: $config(option,hostgroup)" - if {[info exists missing]} { - if {[llength $missing] == [llength $confpath]} { - terror "at least one of the following directories must exist:" - foreach dir $missing { - terror " $dir" - } - } elseif {[config_option verbose]} { - warning "the following directories don't exist:" - foreach dir $missing { - terror " $dir" - } - } - } - exit 1 + if {[info exists config(default,hostgroup_stack)] && \ + [llength $config(default,hostgroup_stack)] > 0} { + unset -nocomplain config(option,hostgroup) + unset -nocomplain config(hosts) + array set config [lindex $config(default,hostgroup_stack) end] + if [info exists config(default,selected)] { + set config(hosts) $config(default,selected) } } + + if {[info exists config(option,hostgroup)]} { + read_hostgroup [config_option hostgroup] + } # Read in additional source file if {[info exists config(option,source)]} { @@ -2996,23 +3059,28 @@ proc rex_parse_cmdline {args} { } } -proc rex_command args { +proc rex_command_common args { global argv0 global argc global argv global config global env + set group {} + while {[llength $args] > 0} { switch -- [lshift args] { -copy { set config(option,copy) 1 } + -usage { set usage [lshift args] } + -docstring { set docstring [lshift args] } + -group { lappend group {-group} [lshift args] [lshift args] } default { error "bad arguments to rex_command: $args" } } } rex_parse_cmdline \ - -usage {rex run [OPTIONS] PROGRAM [ARGS...]} \ - -docstring {Runs PROGRAM on the given hosts.} \ + -usage $usage \ + -docstring $docstring \ -group global { group,g=NAME {select host group} @@ -3070,7 +3138,8 @@ proc rex_command args { zsh-quirk,Z {try to cope with hosts running zsh} { option_set zsh-quirk true } - } + } \ + {*}$group debug 2 "running prologue script" ::hostproc::prologue @@ -3177,6 +3246,170 @@ proc rex_command args { ::hostproc::epilogue } +proc rex_command args { + rex_command_common \ + -usage {rex run [OPTIONS] PROGRAM [ARGS...]} \ + -docstring {Runs PROGRAM on the given hosts.} +} + +proc readdefault {} { + global defaultfile + if [file exists $defaultfile] { + debug 2 "reading default file $defaultfile" + ::config::read $defaultfile + } +} + +proc savedefault {} { + global config + global defaultfile + + if {[catch {open "$defaultfile" "w"} fd] == 0} { + set key {default,hostgroup_stack} + if [info exists config($key)] { + puts $fd [list set ::config($key) $config($key)] + } + close $fd + } else { + terror "can't open $defaultfile: $fd" + exit 2 + } +} + +proc rex_group_select {} { + global config + + proc ::hostproc::logout {host ref status} { + if {$status == 0} { + lappend ::config(default,selected) $host + } + } + + unset -nocomplain config(default,selected) + rex_command_common \ + -usage {rex group select [OPTIONS] COMMAND [ARGS...]} \ + -docstring {Selects hosts on which COMMAND succeeds} + + if {![info exists config(default,selected)]} { + terror "no hosts match the predicate" + exit 1 + } + unset -nocomplain config(option,hostgroup) + ::config::push + + savedefault +} + +proc rex_group_drop {} { + global config + + rex_parse_cmdline \ + -usage {rex group drop} \ + -docstring {Deselect the hostgroup} + + ::config::pop + savedefault +} + +proc rex_group_push {} { + global argc + global argv + global config + + rex_parse_cmdline \ + -usage {rex group push GROUP} \ + -docstring {Select the hostgroup} + + if {$argc != 1} { + terror "bad arguments" + exit 1 + } + + unset -nocomplain config(default,selected) + option_set group [lindex $argv 0] + read_hostgroup [config_option hostgroup] + ::config::push +# ::config::export +# hostlist_setup + + savedefault +} + +proc rex_group_show {} { + global argc + global argv + global config + + set hosts 0 + + rex_parse_cmdline \ + -usage {rex group show [hosts]} \ + -docstring {Show selected hostgroup} + + if {$argc == 1} { + if {[lindex $argv 0] == "hosts"} { + set hosts 1 + } else { + terror "unrecognized argument: [lindex $argv 0]" + exit 2 + } + } elseif {$argc != 0} { + terror "too many arguments" + exit 2 + } + + if [info exists config(option,hostgroup)] { + puts $config(option,hostgroup) + } elseif [info exists config(default,selected)] { + puts "selected hosts" + } else { + exit 2 + } + if {$hosts && [info exists config(hosts)]} { + foreach h $config(hosts) { + puts $h + } + } +} + +proc rex_group {} { + global argc + global argv + global argv0 + global usrconfdir + + ::getopt::parse \ + -usage {rex group COMMAND [OPTIONS] [ARGS]} \ + -docstring {Manipulate hostgroups} \ + -footer {COMMANDs are: + select select hosts matching predicate command + show show selected hostgroup (and hosts) + push push new hostgroup + pop pop currently selected hostgroup off stack +} \ + -commit \ + argc argv + + if {[llength $argv] == 0} { + terror "missing subcommand; try 'rex group --help' for details" + exit 1 + } + set subcom [argcvshift] + + switch -- $subcom { + show rex_group_show + drop rex_group_drop + pop rex_group_drop + select rex_group_select + push rex_group_push + + default { + terror "unknown subcommand $subcom; try 'rex group --usage' for details" + exit 1 + } + } +} + proc rex_list {} { global argv0 global argc @@ -3534,7 +3767,7 @@ proc rex_login {} { denied { set sid $expect_out(spawn_id) - terror "bad password for [hostuser $host] on [job get $batch $sid hostname]" + terror "bad password for [hostuser [job get $batch $sid host]] on [job get $batch $sid hostname]" job close batch $sid } @@ -3669,6 +3902,10 @@ trap { switch -- $config(mode) { run rex_command + + group rex_group + select rex_select + deselect rex_deselect copy-to rex_copy_to |