aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2016-09-02 15:11:43 +0300
committerSergey Poznyakoff <gray@gnu.org.ua>2016-09-02 15:23:15 +0300
commit2bd8f87dae1fb57c2641f8ecc2fb8632246fb56c (patch)
treecf933b052c75ed2761f8782dc54eaf2a26c0af47
parentff1b746d713629e87c4ef025c38bc9b4176df7c9 (diff)
downloadrex-2bd8f87dae1fb57c2641f8ecc2fb8632246fb56c.tar.gz
rex-2bd8f87dae1fb57c2641f8ecc2fb8632246fb56c.tar.bz2
Revamp scripting support.
The TCL script to use is given by the -s NAME option. The file NAME.tcl is searched in the library path. It is sourced in the namespace "hostproc". It is supposed to overwrite one or more of the following functions: prologue, epilogue, getline, transition, finish. Default implementations of these functions provide the basic rex output facilities. * rex.exp: Remove -x (--program) option. Look for file NAME.tcl if -s NAME option is given. Remove config keys: prologue, epilogue, silent. (hostproc): New namespace. (::hostproc::prologue, ::hostproc::epilogue) (::hostproc::getline, ::hostproc::finish) (::hostproc::transition): New functions. (runcmd): Use functions from the hostproc namespace for host processing. (main): Fix code modifying the libpath. Call ::hostproc::prologue after option processing, but before processing the remaining command line arguments. This way the function can modify config(argv) to set the default command. * rex.man8: Document scripting facilities.
-rwxr-xr-xrex.exp179
-rw-r--r--rex.man8171
2 files changed, 233 insertions, 117 deletions
diff --git a/rex.exp b/rex.exp
index 6d59fa5..848f752 100755
--- a/rex.exp
+++ b/rex.exp
@@ -358,12 +358,10 @@ proc prusage {} {
-p, --password=PWD set password (unsafe!)
-S, --script COMMAND is a script, copy it to each host prior to
executing
- -s NAME read .rex/$NAME.rex in addition to the config file
+ -s NAME read .rex/$NAME.tcl in addition to the config file
-u, --user=NAME log in as user NAME
-X, --exclude-host=HOST
remove HOST from the list
- -x, --program=SCRIPT run the expect SCRIPT after executing command on each
- host
-w, --confirm prompt and wait for confirmation before each host
-V, --version print program version and copyright statement
-Z, --zsh-quirk try to cope with hosts running zsh
@@ -1135,31 +1133,25 @@ proc remote_command {} {
}
}
+proc transition {sid state} {
+ upvar sidinfo sidinfo
+ set sidinfo($sid,state) $state
+ ::hostproc::transition $sidinfo($sid,host) $state
+}
+
proc runcmd {hosts command} {
global config
debug 2 "running $command on $hosts"
- if { [info exists config(silent)] } {
- set silent $config(silent)
- } else {
- set silent 0
- }
log_user [config_option log]
- if {[llength $hosts] == 1 &&\
- !$silent &&\
- ![config_option no-host-header] &&\
- ![config_option buffer-output]} {
- puts "[hostname [lindex $hosts 0]]:"
- }
-
foreach host $hosts {
spawn -noecho ssh [hostuser $host]@$host
lappend sidlist $spawn_id
set sidinfo($spawn_id,host) $host
debug 3 "$host on $spawn_id"
- set sidinfo($spawn_id,state) INIT
+ transition $spawn_id INIT
set sidinfo($spawn_id,zsh-quirk) [config_option zsh-quirk]
add_host_key_list $host earlycmd sidinfo($spawn_id,earlycmd)
@@ -1190,11 +1182,11 @@ proc runcmd {hosts command} {
set host $sidinfo($sid,host)
if {$sidinfo($sid,state) == "INIT"} {
send -i $sid "[hostpass $host]\r"
- set sidinfo($sid,state) PASS_SENT
+ transition $sid PASS_SENT
} elseif {$sidinfo($sid,state) == "COMMAND"} {
if {$config(sudo) != ""} {
send -i $sid "[hostpass $host]\r"
- set sidinfo($sid,state) SUDO
+ transition $sid SUDO
exp_continue
} else {
# FIXME
@@ -1202,7 +1194,7 @@ proc runcmd {hosts command} {
}
} elseif {$sidinfo($sid,state) == "SUDO2"} {
send -i $sid "[hostpass $host]\r"
- set sidinfo($sid,state) CLEANUP
+ transition $sid CLEANUP
exp_continue
} else {
# FIXME
@@ -1224,21 +1216,15 @@ proc runcmd {hosts command} {
}
} elseif {$sidinfo($sid,state) == "COMMAND"
|| $sidinfo($sid,state) == "SUDO"} {
- if $silent {
- continue
- }
set host $sidinfo($sid,host)
-
set text [regsub -all {\r} $expect_out(0,string) {}]
+
if [config_option buffer-output] {
lappend output($host) $text
- } else {
- if [config_option prefix] {
- puts -nonewline "[hostname $host]> "
- }
- puts -nonewline $text
}
+
+ ::hostproc::getline $host $text
} elseif {$sidinfo($sid,state) == "STTY"} {
regexp -- {REXDIR=(.+)\r} $expect_out(0,string) x sidinfo($sid,wd)
continue
@@ -1255,7 +1241,7 @@ proc runcmd {hosts command} {
set cmd [lshift sidinfo($sid,cmdlist)]
debug 3 "$sid: sending $cmd"
send -i $sid "$cmd\r"
- set sidinfo($sid,state) STTY
+ transition $sid STTY
} elseif {$sidinfo($sid,state) == "STTY"} {
if [llength $sidinfo($sid,cmdlist)] {
set cmd [lshift sidinfo($sid,cmdlist)]
@@ -1275,11 +1261,7 @@ proc runcmd {hosts command} {
} else {
debug 3 "$sid: sending $command"
send -i $sid "$command\r"
- set sidinfo($sid,state) COMMAND
-
- if [info exists config(program)] {
- eval $config(program)
- }
+ transition $sid COMMAND
}
} else {
if {[info exists sidinfo($sid,wd)] && $sidinfo($sid,state) == "COMMAND"} {
@@ -1287,13 +1269,13 @@ proc runcmd {hosts command} {
debug 3 "$sid: removing $host:$sidinfo($sid,wd)"
send -i $sid "$config(sudo)rm -r $sidinfo($sid,wd)\r"
if {$config(sudo) != ""} {
- set sidinfo($sid,state) SUDO2
+ transition $sid SUDO2
} else {
- set sidinfo($sid,state) CLEANUP
+ transition $sid CLEANUP
}
} else {
send -i $sid "exit\r"
- set sidinfo($sid,state) LOGOUT
+ transition $sid LOGOUT
}
}
}
@@ -1326,18 +1308,18 @@ proc runcmd {hosts command} {
eof {
set sid $expect_out(spawn_id)
if {$sidinfo($sid,state) == "LOGOUT"} {
- debug 2 "EOF from $sidinfo($sid,host), waiting for $sid"
+ set host $sidinfo($sid,host)
+ debug 2 "EOF from $host, waiting for $sid"
closeproc $sid
wait -i $sid
+
+ ::hostproc::finish $host output($host)
} elseif {$sidinfo($sid,state) == "COPY"} {
debug 3 "$sidinfo($sid,cmd) finished"
debug 3 "sending $command to $sidinfo($sid,master)"
send -i $sidinfo($sid,master) "$command\r"
- set sidinfo($sidinfo($sid,master),state) COMMAND
-
- if [info exists config(program)] {
- eval $config(program)
- }
+ transition $sidinfo($sid,master) COMMAND
+
closeproc $sid
wait -i $sid
} else {
@@ -1348,25 +1330,12 @@ proc runcmd {hosts command} {
}
}
debug 2 "finished $command on $hosts"
- if [config_option buffer-output] {
- foreach host $hosts {
- if ![config_option no-host-header] {
- puts "[hostname $host]:"
- }
- foreach text $output($host) {
- if [config_option prefix] {
- puts -nonewline "[hostname $host]> "
- }
- puts -nonewline $text
- }
- }
- }
}
# #######################################################################
# The game begins...
# #######################################################################
-set shortopts "bCcdD:eEfIij:g:H:hlNnPp:qSsu:X:x:VwZ"
+set shortopts "bCcdD:eEfIij:g:H:hlNnPp:qSsu:X:VwZ"
set longopts {
buffer-output b
config C
@@ -1394,7 +1363,6 @@ set longopts {
no-resolve n
password p
prefix P
- program x
script S
user u
version V
@@ -1438,13 +1406,6 @@ getopt -progname $argv0 -longopts $longopts $argc $argv $shortopts {
u { set config(user) $optarg }
N { set config(option,noop) 1 }
X { eval lappend config(exclude_hosts) [split $optarg ","] }
- x { if [catch {open "$optarg" r} fd] {
- puts stderr "$argv0: cannot open $optarg"
- exit 1
- }
- set config(program) [read $fd]
- close $fd
- }
V prversion
w { set config(option,confirm) 1 }
Z { set config(option,zsh-quirk) 1 }
@@ -1485,13 +1446,13 @@ if [config_option zsh-quirk] {
if [info exists config(option,hostgroup)] {
debug 1 "using hostgroup $config(option,hostgroup)"
- set libpath [linsert libpath 0 \
+ set libpath [linsert $libpath 0 \
"$sysconfdir/$config(option,hostgroup)/script"]
set dir "$usrconfdir/$config(option,hostgroup)"
if [file isdirectory $dir] {
- set config(home) [file normalize $dir]
- set libpath [linsert libpath 0 "$config(home)/script"]
- debug 1 "group home is $config(home)"
+ set config(home) [file normalize $dir]
+ set libpath [linsert $libpath 0 "$config(home)/script"]
+ debug 1 "group home is $config(home)"
}
} else {
set config(home) $usrconfdir
@@ -1508,8 +1469,7 @@ if [info exists config(option,encrypt)] {
exit 0
}
-set cfgvars { sudo hosts user password command program
- prologue epilogue silent}
+set cfgvars { sudo hosts user password command program }
namespace eval config {
variable initialized 0
@@ -1538,7 +1498,9 @@ proc scanlibpath {file} {
foreach dir $libpath {
set name "$dir/$file"
+ debug 2 "looking for $name"
if [file exists $name] {
+ debug 1 "found $name"
return $name
}
}
@@ -1581,18 +1543,69 @@ if [info exists config(option,hostgroup)] {
}
}
}
+
+# The hostproc namespace provides functions for processing output
+# from hosts.
+namespace eval hostproc {
+ # A prologue function. Called before processing the first batch of hosts.
+ proc prologue {} {}
+
+ # Epilogue function is called when all hosts have been processed.
+ proc epilogue {} {}
+
+ proc transition {host state} {
+ if {$state == "COMMAND" &&\
+ ![config_option no-host-header] &&\
+ ![config_option buffer-output]} {
+ puts "[hostname $host]:"
+ }
+ }
+ # Getline is invoked when next line of output has been received from the
+ # host. The default implementation provides basic output capability.
+ proc getline {host line} {
+ if [config_option buffer-output] {
+ return
+ }
+
+ if [config_option prefix] {
+ puts -nonewline "[hostname $host]> "
+ }
+ puts -nonewline $line
+ }
+
+ # 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} {
+ upvar $ref text
+ if [info exists text] {
+ if ![config_option no-host-header] {
+ puts "[hostname $host]:"
+ }
+ foreach line $text {
+ if [config_option prefix] {
+ puts -nonewline "[hostname $host]> "
+ }
+ puts -nonewline $line
+ }
+ }
+ }
+}
+
if [info exists config(script)] {
set config(script) [lindex $argv 0]
set argv [lrange $argv 1 end]
incr argc -1
- set name [scanlibpath $config(script).rex]
+ set name [scanlibpath $config(script).tcl]
if {$name == ""} {
- terror "the script $config(script).rex not found"
+ terror "the script $config(script).tcl not found"
exit 1
}
debug 2 "reading script file $name"
- ::config::readcfg $name
+ namespace eval hostproc {
+ source $name
+ }
}
if {$config::initialized} {
@@ -1615,6 +1628,9 @@ if {$config::initialized} {
}
}
+debug 2 "running prologue script"
+::hostproc::prologue
+
if [config_option noop] {
updatedb
exit 0
@@ -1634,14 +1650,14 @@ if [config_option noop] {
terror "too few arguments"
exit 1
} else {
- debug 2 "copy from mode"
+ debug 2 "copy-from mode"
}
} else {
if { $argc < 2 } {
terror "too few arguments"
exit 1
} else {
- debug 2 "copy mode"
+ debug 2 "copy-to mode"
}
}
} elseif { $argc == 0 } {
@@ -1830,11 +1846,6 @@ if [config_option copy] {
debug 1 "... on $config(hosts)"
}
-if [info exists config(prologue)] {
- debug 2 "running prologue script"
- eval $config(prologue)
-}
-
if {[config_option jobs] > 1} {
if ![config_option buffer-output] {
set config(option,prefix) 1
@@ -1861,10 +1872,8 @@ while {[llength $config(hosts)] > 0} {
}
debug 2 "left main loop"
-if [info exists config(epilogue)] {
- debug 2 "running epilogue script"
- eval $config(epilogue)
-}
+debug 2 "running epilogue script"
+::hostproc::epilogue
updatedb
diff --git a/rex.man8 b/rex.man8
index 4c66ca9..54381b4 100644
--- a/rex.man8
+++ b/rex.man8
@@ -13,7 +13,7 @@
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with Rex. If not, see <http://www.gnu.org/licenses/>.
-.TH REX 8 "September 1, 2016" "REX" "Rex User Reference"
+.TH REX 8 "September 2, 2016" "REX" "Rex User Reference"
.ds ET /etc
.SH NAME
rex \- remote execution utility
@@ -29,7 +29,6 @@ rex \- remote execution utility
[\fB\-s\fR \fINAME\fR]\
[\fB\-u\fR \fINAME\fR]\
[\fB\-X\fR \fIHOST[\fB,\fINAME\fR...]]\
- [\fB\-x\fR \fISCRIPT\fR]\
[\fB\-\-buffer\-output\fR]\
[\fB\-\-config=\fIFILE\fR]\
[\fB\-\-confirm\fR]\
@@ -287,25 +286,129 @@ more information.
Command to run on the hosts in this hostgroup (an array). It is used
if no command is given in the command line.
.TP
-.B program
-Optional \fBTCL\fR code to run right after successful authentication
-on each host.
-.TP
-.B prologue
-Optional \fBTCL\fR code that will be run before connecting to the
-first host.
-.TP
-.B epilogue
-Optional \fBTCL\fR code that will be run after finishing the last host
-from the hostgroup.
-.TP
-.B silent
-A boolean value. Disables output of the progress information.
-.TP
.B sudo
Boolean. \fB1\fR if the command must be run via
.BR sudo .
+.SS Scripting
+When given the \fB\-s \fINAME\fR option, \fBrex\fR looks for the file
+named \fINAME\fB\.tcl\fR in the library search path. The search path
+is scanned from left to right. The first encountered
+\fINAME\fB\.tcl\fR file is sourced in the namespace \fBhostproc\fR.
+.PP
+The default library search path is:
+.PP
+.RS
+.B ~/.rex/script
+.br
+.B \*(ET
+.RE
+.PP
+If the \fB\-g \fIGROUP\fR (\fB\-\-group=\fIGROUP\fR) option is given,
+the path is modified as follows:
+.PP
+.RS
+\fB~/.rex/\fIGROUP\fB/script\fR
+.br
+\fB\*(ET/\fIGROUP\fB/script\fR
+.br
+.B ~/.rex/script
+.br
+.B \*(ET
+.RE
.PP
+The following functions from the \fBhostproc\fR namespace are used by
+\fBrex\fR:
+.TP
+.B proc prologue {}
+Called at the very beginning of \fBrex\fR invocation, after analyzing
+the command line options, but before parsing the remaining command
+line arguments.
+
+This function can be used to initialize internal variables. It is
+also allowed to modify the global \fBconfig\fR array (see the section
+\fBCONFIG ARRAY\fR, below).
+
+In the following example, \fBprologue\fR is used to set up the command
+line:
+.sp
+.RS
+.EX
+proc prologue {} {
+ global config
+ set config(argv) {ls -ld /home}
+}
+.EE
+.RE
+.TP
+.B proc epilogue {}
+Called after processing all hosts.
+
+It an be used for application-specific cleanup.
+.TP
+.B proc transition {host state}
+Called when the session for \fI$host\fR is switched to the state
+\fI$state\fR. Currently implemented states are:
+.RS
+.TP
+.B INIT
+The initial state. It is entered after starting \fBssh\fR for that host.
+.TP
+.B PASS_SENT
+Entered after receiving password prompt from the remote side and
+responding to it with a password.
+.TP
+.B SUDO
+Entered after responding to the password prompt received in the
+\fBCOMMAND\fR state. This happens only in sudo mode, when \fBsudo\fR
+asks for credentials.
+.TP
+.B SUDO2
+Entered after sending a \fBsudo rm\fR command to remove the temporary
+directory.
+.TP
+.B STTY
+Entered after sending the initial \fBstty -echo\fR command, which
+happens after successful authentication. The session remains in this
+state while sending commands from the initial command list (see the
+description of \fBshrc\fR key in \fBCONFIG ARRAY\fR.
+.TP
+.B COMMAND
+Entered after sending the main command to the host.
+.TP
+.B LOGOUT
+The very last state in the session lifetime. It is entered after
+sending the terminating \fBexit\fR command to the host.
+
+The following implementation of the \fBtransition\fR function uses
+this information to display informational message about each host
+being processed:
+.sp
+.RS
+.EX
+proc transition {host state} {
+ if {$state == "INIT"} {
+ puts -nonewline stderr "$host... "
+ } elseif {$state == "LOGOUT"} {
+ puts "done"
+ }
+}
+.EE
+.RE
+.RE
+.TP
+.B proc getline {host line}
+Invoked each time a new line of output is received from the host. The
+\fBhost\fR parameter is the name of the host. The \fB$line\fR
+parameter is the line received from the host (with trailing \fBCRLF\fR
+characters).
+.TP
+.B proc finish {host refname}
+The \fBfinish\fR function is called when EOF has been received from
+the \fBhost\fR. The \fBrefname\fR parameter is a name of the variable
+keeping the entire text received from that host during the session.
+This variable exists only if \fBrex\fR has been instructed to buffer
+host output (see the \fB\-b\fR option). If it exists, the output text
+is stored as a list of lines.
.SS Debugging
To see what's going on during \fBrex\fR invocation. Use the
\fB\-d (\fB\-\-debug\fR) option. This option is incremental, that is
@@ -313,6 +416,10 @@ the more times you repeat it in the command line, the more debugging
information is output. Currently, to obtain the most detailed
information, it should be repeated three times (e.g. \B\-ddd\fR).
.PP
+Additional information is provided by the \fB\-l\fR
+(\fB\-\-log\fR) option, which enables logging of the send/expect
+dialogue to stdout.
+.PP
If you wish to get even more information and are familiar with
.BR expect (1)
internals, set the
@@ -436,9 +543,6 @@ Prompt and wait for confirmation before each host.
Remove \fIHOST\fR from the list. Multiple hosts can be specified as a
comma-delimited list (no whitespace around commas, please).
.TP
-\fB\-x\fR, \fB\-\-program=\fISCRIPT\fR
-Run \fBexpect\fR \fISCRIPT\fR after executing command on each host.
-.TP
\fB\-Z\fR, \fB\-\-zsh\-quirk\fR
Try to cope with hosts running
.BR zsh (1).
@@ -473,9 +577,6 @@ value is 3.
(list) A list of commands to send as early as the very first line of output
from the host is seen (see the note about \fBZSH\fR, below).
.TP
-.B epilogue
-\fBTCL\fR code to run when all hosts have been processed.
-.TP
.B exclude_hosts
List of host names to exclude from processing. Normally set via the
\fB\-X\fR exclude option.
@@ -504,10 +605,6 @@ credentials\fR).
\fBTCL\fR code to run on each host after receiving last line of
output from the command.
.TP
-.B prologue
-\fBTCL\fR code to run before starting the processing of the first
-batch of hosts.
-.TP
.B prompt
Regular expression for matching the command line prompt. Default is
\fB"(%|#|\\$) $"\fR .
@@ -515,9 +612,6 @@ Regular expression for matching the command line prompt. Default is
.\" .TP
.\" .B script
.TP
-.B silent
-(boolean) Suppress output from hosts.
-.TP
.B shrc
A list of commands to send after the initial command line prompt has been
received. See the note about \fBDisabling command history\fR for an
@@ -640,8 +734,18 @@ The simplest way, common for \fBbash\fR, \fBksh\fR, and \fBzsh\fR is:
set config(shrc) { {HISTFILE=/dev/null} }
.EE
.PP
-Its drawback is that the assignment itself, and all the built-in
-commands sent by \fBrex\fR itself are still retained in history.
+Similar setting for
+.BR csh (1)
+or
+.BR tcsh (1)
+is:
+.PP
+.EX
+set config(shrc) { {set histfile=/dev/null} }
+.EE
+.PP
+The drawback of this approach that the assignment itself, and all the
+built-in commands sent by \fBrex\fR itself are still retained in history.
.PP
A more sophisticated way, suitable for both \fBbash\fR and \fBzsh\fR
shells is:
@@ -663,6 +767,9 @@ further updating it.
.PP
(The most recent line contains the command \fBstty -echo\fR, sent by
\fBrex\fR at the beginning of each session.)
+.PP
+To set history cleaning on a per-host (or per-hostgroup) basis, use
+the \fBshrc\fR key in the rex database file.
.SS ZSH
Interaction with
.BR zsh (1)

Return to:

Send suggestions and report system problems to the System administrator.