#! /usr/local/bin/expect -- ## ## rancid 3.11 ## Copyright (c) 2023 by Nick Peelman ## All rights reserved. ## ## This code is derived from software contributed to and maintained by ## Henry Kilmer, John Heasley, Andrew Partan, ## Pete Whiting, Austin Schutz, and Andrew Fort. ## ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions ## are met: ## 1. Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## 2. Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the distribution. ## 3. Neither the name of RANCID nor the names of its ## contributors may be used to endorse or promote products derived from ## this software without specific prior written permission. ## ## THIS SOFTWARE IS PROVIDED BY Henry Kilmer, John Heasley AND CONTRIBUTORS ## ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED ## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ## PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COMPANY OR CONTRIBUTORS ## BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE ## POSSIBILITY OF SUCH DAMAGE. ## ## It is the request of the authors, but not a condition of license, that ## parties packaging or redistributing RANCID NOT distribute altered versions ## of the etc/rancid.types.base file nor alter how this file is processed nor ## when in relation to etc/rancid.types.conf. The goal of this is to help ## suppress our support costs. If it becomes a problem, this could become a ## condition of license. # # The expect login scripts were based on Erik Sherk's gwtn, by permission. # # The original looking glass software was written by Ed Kern, provided by # permission and modified beyond recognition. # # noklogin - Nokia (Alcatel-Lucent) SR OS login # # Most options are intuitive for logging into a Cisco router. # The default username password is the same as the vty password. # # Sometimes routers take awhile to answer (the default is 10 sec) set timeoutdflt 45 # Some CLIs having problems if we write too fast (Extreme, PIX, Cat) set send_human {.2 .1 .4 .2 1} # Usage line set usage "Usage: $argv0 \[-dhiSV\] \[-m|M\] \[-autoenable\] \[-noenable\] \ \[-c command\] \[-Evar=x\] \[-e enable-password\] \[-f cloginrc-file\] \ \[-p user-password\] \[-r passphrase\] \[-s script-file\] \[-t timeout\] \ \[-u username\] \[-v vty-password\] \[-w enable-username\] \[-x command-file\] \ \[-y ssh_cypher_type\] router \[router...\]\n" # Password file set password_file $env(HOME)/.cloginrc # Default is to login to the router set do_command 0 set do_interact 0 set do_script 0 # The default is to automatically enable if {! [info exists avenable]} { set avenable 0 } # The default is that you login non-enabled (tacacs can have you login already # enabled) set avautoenable 0 # The default is to look in the password file to find the passwords. This # tracks if we receive them on the command line. set do_passwd 1 set do_enapasswd 1 # Save config, if prompted set do_saveconfig 0 # cloginrc debugging knob set do_cloginrcdbg 0 # Sometimes routers take awhile to answer (the default is 10 sec) if {! [info exists timeoutdflt]} { set timeoutdflt 10 } # spawn tty options set spawnopts {} # intialize cloginrc parsing stacks set int_file {} set int_lineno {} # env(CLOGIN) may contain: # x == do not set xterm banner or name # Find the user in the ENV, or use the unix userid. if {![info exists default_user] && [info exists env(CISCO_USER)]} { if {[string length $env(CISCO_USER)]} { set default_user $env(CISCO_USER) } } if {![info exists default_user] && [info exists env(USER)]} { if {[string length $env(USER)]} { set default_user $env(USER) } } if {![info exists default_user] && [info exists env(LOGNAME)]} { if {[string length $env(LOGNAME)]} { set default_user $env(LOGNAME) } } if (![info exists default_user]) { # This uses "id" which I think is portable. At least it has existed # (without options) on all machines/OSes I've been on recently - # unlike whoami or id -nu. if [catch {exec id} reason] { send_error "\nError: could not exec id: $reason\n" exit 1 } regexp {\(([^)]*)} "$reason" junk default_user } if {[info exists env(CLOGINRC)]} { set password_file $env(CLOGINRC) } # Process the command line for {set i 0} {$i < $argc} {incr i} { set arg [lindex $argv $i] switch -glob -- $arg { # Expect debug mode -d* { exp_internal 1 # Help } -h* { send_user "$usage" exit 0 # Command to run. } -c* { if {! [regexp .\[cC\](.+) $arg ignore command]} { incr i set command [lindex $argv $i] } set do_command 1 # Enable Password } -e* { if {! [regexp .\[e\](.+) $arg ignore enapasswd]} { incr i set enapasswd [lindex $argv $i] } set do_enapasswd 0 # Environment variable to pass to -s scripts } -E* { if {[regexp .\[E\](.+)=(.+) $arg ignore varname varvalue]} { set E$varname $varvalue } else { send_user "\nError: invalid format for -E in $arg\n" exit 1 } # alternate cloginrc file } -f* { if {! [regexp .\[fF\](.+) $arg ignore password_file]} { incr i set password_file [lindex $argv $i] } # interactive } -i* { set do_interact 1 # user Password } -p* { if {! [regexp .\[pP\](.+) $arg ignore userpasswd]} { incr i set userpasswd [lindex $argv $i] } set do_passwd 0 # cloginrc debugging knobs } -m* { set do_cloginrcdbg 1 } -M* { set do_cloginrcdbg 2 # ssh passphrase } -r* { if {! [regexp .\[rR\](.+) $arg ignore passphrase]} { incr i set avpassphrase [lindex $argv $i] } # Expect script to run. } -s* { if {! [regexp .\[sS\](.+) $arg ignore sfile]} { incr i set sfile [lindex $argv $i] } if {! [file readable $sfile]} { send_user "\nError: Can't read $sfile\n" exit 1 } set do_script 1 # save config on exit } -S* { # may not be supported by the script and may not be applicable to # the platform set do_saveconfig 1 # Timeout } -t* { if {! [regexp .\[tT\](.+) $arg ignore timeout]} { incr i set timeoutdflt [lindex $argv $i] } # Username } -u* { if {! [regexp .\[uU\](.+) $arg ignore user]} { incr i set username [lindex $argv $i] } # VTY Password } -v* { # some scripts ignore -v, like jlogin if {! [regexp .\[vV\](.+) $arg ignore passwd]} { incr i set passwd [lindex $argv $i] } set do_passwd 0 # Version string } -V* { send_user "rancid 3.11\n" exit 0 # Enable Username } -w* { # may not be supported by the script and may not be applicable to # the platform if {! [regexp .\[wW\](.+) $arg ignore enauser]} { incr i set enausername [lindex $argv $i] } # Command file } -x* { if {! [regexp .\[xX\](.+) $arg ignore cmd_file]} { incr i set cmd_file [lindex $argv $i] } if [catch {set cmd_fd [open $cmd_file r]} reason] { send_user "\nError: $reason\n" exit 1 } set cmd_text [read $cmd_fd] close $cmd_fd set command [join [split $cmd_text \n] \;] set do_command 1 # 'ssh -c' cypher type } -y* { if {! [regexp .\[eE\](.+) $arg ignore cypher]} { incr i set cypher [lindex $argv $i] } # Do we enable? } -noenable { set avenable 0 # Does tacacs automatically enable us? } -autoenable { set avautoenable 1 set avenable 0 } -* { send_user "\nError: Unknown argument! $arg\n" send_user $usage exit 1 } default { break } } } # Process routers...no routers listed is an error. if {$i == $argc} { send_user "\nError: $usage" } # Only be quiet if we are running a script (it can log its output # on its own) if {$do_script} { log_user 0 } else { log_user 1 } # # Done configuration/variable setting. Now run with it... # # Sets Xterm title if interactive...if its an xterm and the user cares proc label {host} { global env # if CLOGIN has an 'x' in it, don't set the xterm name/banner if [info exists env(CLOGIN)] { if {[string first "x" $env(CLOGIN)] != -1} { return } } # take host from ENV(TERM) if [info exists env(TERM)] { if [regexp \^(xterm|vs) $env(TERM) ignore] { send_user "\033]1;[lindex [split $host "."] 0]\a" send_user "\033]2;$host\a" } } } # This is a helper function to make the password file easier to # maintain. Using this the password file has the form: # add password sl* pete cow # add password at* steve # add password * hanky-pie proc add {var args} { global int_file int_lineno int_$var set file [lindex $int_file 0] set lineno [lindex $int_lineno 0] lappend int_$var "$var:$file:$lineno: $args" } proc include {args} { global env regsub -all "(^{|}$)" $args {} args if {[regexp "^/" $args ignore] == 0} { set args $env(HOME)/$args } source_password_file $args } proc find {var router} { global do_cloginrcdbg upvar int_$var list if {[info exists list]} { foreach line $list { if {[string match -nocase [lindex $line 1] $router]} { if {$do_cloginrcdbg > 0} { send_error -- [join [list [lindex $line 0] [lrange $line 1 end] "\r\n"]] } if {$do_cloginrcdbg == 2} { # save return value if {! [info exists result]} { set result [lrange $line 2 end] } } else { return [lrange $line 2 end] } } } } if {$do_cloginrcdbg == 2} { if {[info exists result]} { return $result } } return {} } # Loads the password file. Note that as this file is tcl, and that # it is sourced, the user better know what to put in there, as it # could install more than just password info... I will assume however, # that a "bad guy" could just as easy put such code in the clogin # script, so I will leave .cloginrc as just an extention of that script proc source_password_file {file} { global env int_file int_lineno if {! [file exists $file]} { send_user "\nError: password file ($file) does not exist\n" exit 1 } file stat $file fileinfo if {[expr ($fileinfo(mode) & 007)] != 0000} { send_user "\nError: $file must not be world readable/writable\n" exit 1 } if [catch {set fd [open $file "r"]} reason] { send_user "\nError: $reason\n" exit 1 } set int_file [linsert $int_file 0 $file] set int_lineno [linsert $int_lineno 0 0] while {[gets $fd line] >= 0} { set tmp [lindex $int_lineno 0]; incr tmp lset int_lineno 0 $tmp eval $line } set int_file [lrange $int_file 1 end] set int_lineno [lrange $int_lineno 1 end] close $fd } # Log into the router. # returns: 0 on success, 1 on failure, -1 if rsh was used successfully proc login { router user userpswd passwd cmethod cyphertype identfile } { global command spawn_id in_proc do_command do_script passphrase global prompt prompt_match u_prompt p_prompt e_prompt sshcmd telnetcmd global spawnopts set in_proc 1 set uprompt_seen 0 # try each of the connection methods in $cmethod until one is successful set progs [llength $cmethod] foreach prog [lrange $cmethod 0 end] { incr progs -1 if [string match "telnet*" $prog] { regexp {telnet(:([^[:space:]]+))*} $prog methcmd suffix port if {"$port" == ""} { set retval [catch {eval spawn $spawnopts [split "$telnetcmd $router"]} reason] } else { set retval [catch {eval spawn $spawnopts [split "$telnetcmd $router $port"]} reason] } if { $retval } { send_user "\nError: telnet failed: $reason\n" return 1 } } elseif [string match "ssh*" $prog] { # ssh to the router & try to login with or without an identfile. regexp {ssh(:([^[:space:]]+))*} $prog methcmd suffix port set cmd $sshcmd if {"$port" != ""} { set cmd "$cmd -p $port" } if {"$cyphertype" != ""} { set cmd "$cmd -c $cyphertype" } if {"$identfile" != ""} { set cmd "$cmd -i $identfile" } set retval [catch {eval spawn $spawnopts [split "$cmd -x -l $user $router" { }]} reason] if { $retval } { send_user "\nError: $cmd failed: $reason\n" return 1 } } elseif ![string compare $prog "rsh"] { if { ! $do_command } { if { [llength $cmethod] == 1 } { send_user "\nError: rsh is an invalid method for -x and " send_user "interactive logins\n" } if { $progs == 0 } { return 1 } continue; } # handle escaped ;s in commands, and ;; and ^; regsub -all {([^\\]);} $command "\\1\u0002;" esccommand regsub -all {([^\\]);;} $esccommand "\\1;\u0002;" command regsub {^;} $command "\u0002;" esccommand regsub -all {[\\];} $esccommand ";" command regsub -all {\u0002;} $command "\u0002" esccommand set sep "\u0002" set commands [split $esccommand $sep] set num_commands [llength $commands] set rshfail 0 for {set i 0} {$i < $num_commands && !$rshfail} { incr i} { log_user 0 set retval [catch {spawn rsh $user@$router [lindex $commands $i] } reason] if { $retval } { send_user "\nError: rsh failed: $reason\n" log_user 1; return 1 } send_user "$router# [lindex $commands $i]\n" # rcmd does not get a pager and no prompts, so we just have to # look for failures & lines. expect { "Connection refused" { catch {close}; catch {wait}; send_user "\nError: Connection\ Refused ($prog): $router\n" set rshfail 1 } -re "(Connection closed by|Connection to \[^\n\r]+ closed)" { catch {close}; catch {wait}; send_user "\nError: Connection\ closed ($prog): $router\n" set rshfail 1 } "Host is unreachable" { catch {close}; catch {wait}; send_user "\nError: Host Unreachable:\ $router\n" set rshfail 1 } "No address associated with" { catch {close}; catch {wait}; send_user "\nError: Unknown host\ $router\n" set rshfail 1 } -re "\b+" { exp_continue } -re "\[\n\r]+" { send_user -- "$expect_out(buffer)" exp_continue } timeout { catch {close}; catch {wait}; send_user "\nError: TIMEOUT reached\n" set rshfail 1 } eof { catch {close}; catch {wait}; } } log_user 1 } if { $rshfail } { if { !$progs } { return 1 } else { continue } } # fake the end of the session for rancid. send_user "$router# quit\n" # return rsh "success" return -1 } else { send_user "\nError: unknown connection method: $prog\n" return 1 } sleep 0.3 # This helps cleanup each expect clause. expect_after { timeout { global in_proc send_user "\nError: TIMEOUT reached\n" catch {close}; catch {wait}; if {$in_proc} { return 1 } else { continue } } eof { global in_proc send_user "\nError: EOF received\n" catch {close}; catch {wait}; if {$in_proc} { return 1 } else { continue } } } # Here we get a little tricky. There are several possibilities: # the router can ask for a username and passwd and then # talk to the TACACS server to authenticate you, or if the # TACACS server is not working, then it will use the enable # passwd. Or, the router might not have TACACS turned on, # then it will just send the passwd. # if telnet fails with connection refused, try ssh expect { -re "(Connection refused|Secure connection \[^\n\r]+ refused)" { catch {close}; catch {wait}; if !$progs { send_user "\nError: Connection Refused ($prog): $router\n" return 1 } } -re "(Connection closed by|Connection to \[^\n\r]+ closed)" { catch {close}; catch {wait}; if !$progs { send_user "\nError: Connection closed ($prog): $router\n" return 1 } } eof { send_user "\nError: Couldn't login: $router\n"; wait; return 1 } -nocase "unknown host\r" { send_user "\nError: Unknown host $router\n"; catch {close}; catch {wait}; return 1 } "Host is unreachable" { send_user "\nError: Host Unreachable: $router\n"; catch {close}; catch {wait}; return 1 } "No address associated with name" { send_user "\nError: Unknown host $router\n"; catch {close}; catch {wait}; return 1 } -re "(Host key not found |The authenticity of host .* be established)" { expect { -re "\\(yes\/no\[^\\)]*\\)\\?" { send "yes\r"; send_user "\nHost $router added to the list of known hosts.\n" } -re "\[^\r\n]*\[\r\n]+" { exp_continue; } } exp_continue } -re "HOST IDENTIFICATION HAS CHANGED" { send_user "\nError: The host key for $router has changed. Update the SSH known_hosts file accordingly.\n" expect { -re "\\(yes\/no\\)\\?" { send "no\r" } -re " strict checking\.\[\r\n]+" { } -re "\[^\r\n]*\[\r\n]+" { exp_continue; } } catch {close}; catch {wait}; return 1 } -re "Offending key for " { send_user "\nError: host key mismatch for $router. Update the SSH known_hosts file accordingly.\n" expect { -re "\\(yes\/no\\)\\?" { send "no\r" } -re "\[^\r\n]*\[\r\n]+" { exp_continue; } } catch {close}; catch {wait}; return 1 } -nocase -re "^warning: remote host denied authentication agent forwarding." { exp_continue; } -re "(denied|Sorry)" { send_user "\nError: Check your passwd for $router\n" catch {close}; catch {wait}; return 1 } "Login failed" { send_user "\nError: Check your passwd for $router\n" catch {close}; catch {wait}; return 1 } -re "% (Bad passwords|Authentication failed)" { send_user "\nError: Check your passwd for $router\n" catch {close}; catch {wait}; return 1 } -re "Last login:" { exp_continue } -re "Press the key \[^\r\n]+\[\r\n]+" { exp_continue } -re "@\[^\r\n]+ $p_prompt" { # ssh pwd prompt sleep 1 send -- "$userpswd\r" exp_continue } -re "Enter passphrase.*: " { # sleep briefly to allow time for stty -echo sleep .3 send -- "$passphrase\r" exp_continue } -re "$u_prompt" { send -- "$user\r" set uprompt_seen 1 exp_continue } -re "$p_prompt" { sleep 1 if {$uprompt_seen == 1} { send -- "$userpswd\r" } else { send -- "$passwd\r" } exp_continue } -re "$prompt" { set prompt_match $expect_out(0,string); break; } "Login invalid" { send_user "\nError: Invalid login: $router\n"; catch {close}; catch {wait}; return 1 } -re "\[^\r\n]*\[\r\n]+" { exp_continue; } } } set in_proc 0 return 0 } # Run commands given on the command line. proc run_commands { prompt command } { global do_interact do_saveconfig in_proc set in_proc 1 # match sros config mode prompts too, such as A:name>mode>mode# # or *A:name>...#. regsub -all -- {#$} $prompt {[^\r\n# ]*#} reprompt # this is the only way i see to get rid of more prompts in o/p..grrrrr log_user 0 # handle escaped ;s in commands, and ;; and ^; regsub -all {([^\\]);} $command "\\1\u0002;" esccommand regsub -all {([^\\]);;} $esccommand "\\1;\u0002;" command regsub {^;} $command "\u0002;" esccommand regsub -all {[\\];} $esccommand ";" command regsub -all {\u0002;} $command "\u0002" esccommand set sep "\u0002" set commands [split $esccommand $sep] set num_commands [llength $commands] for {set i 0} {$i < $num_commands} { incr i} { send -- "[subst -nocommands [lindex $commands $i]]\r" expect { -re "^\[^\n\r *]*$reprompt" { send_user -- "$expect_out(buffer)" } -re "^\[^\n\r]*$reprompt." { send_user -- "$expect_out(buffer)" } -re "\[^\r\n]*\[\n\r]+" { send_user -- "$expect_out(buffer)" exp_continue } -re "\[^\r\n]*Press any key to continue \\\(Q to quit\\\)" { send " " # bloody ^[[2K after " " expect { -re "^\[^\r\n]*\r" {} } exp_continue } } } log_user 1 if { $do_interact == 1 } { interact return 0 } send -h "quit\r" expect { -re "Connection to (.*) closed." { return 0 } -re "\[\n\r]+" { exp_continue } timeout { catch {close}; catch {wait}; return 1 } eof { return 0 } } set in_proc 0 } # # For each router... (this is main loop) # source_password_file $password_file set in_proc 0 set exitval 0 set prompt_match "" # if we have dont have a tty, we need some additional terminal settings if [catch {open /dev/tty w} ttyid] { # no tty, ie: cron set spawnopts "-nottycopy" set stty_init "cols 132" } else { catch {close ttyid} reason } foreach router [lrange $argv $i end] { set router [string tolower $router] send_user -- "$router\n" # device timeout set timeout [find timeout $router] if { [llength $timeout] == 0 } { set timeout $timeoutdflt } # Default prompt. set prompt [join [find prompt $router] ""] if { [llength $prompt] == 0 } { set prompt "#" } # Figure out passwords if { $do_passwd } { set pswd [find password $router] if { [llength $pswd] == 0 } { send_user -- "\nError: no password for $router in $password_file.\n" continue } set passwd [join [lindex $pswd 0] ""] } else { set passwd $userpasswd } # Figure out username if {[info exists username]} { # command line username set ruser $username } else { set ruser [join [find user $router] ""] if { "$ruser" == "" } { set ruser $default_user } } # Figure out username's password (if different from the vty password) if {[info exists userpasswd]} { # command line username set userpswd $userpasswd } else { set userpswd [join [find userpassword $router] ""] if { "$userpswd" == "" } { set userpswd $passwd } } # Figure out prompts set u_prompt [find userprompt $router] if { "$u_prompt" == "" } { set u_prompt "(\[Uu]sername|Login|login|user name|User):" } else { set u_prompt [join [lindex $u_prompt 0] ""] } set p_prompt [find passprompt $router] if { "$p_prompt" == "" } { set p_prompt "(\[Pp]assword|passwd|Enter password for \[^ :]+):" } else { set p_prompt [join [lindex $p_prompt 0] ""] } # Figure out identity file to use set identfile [join [lindex [find identity $router] 0] ""] # Figure out passphrase to use if {[info exists avpassphrase]} { set passphrase $avpassphrase } else { set passphrase [join [lindex [find passphrase $router] 0] ""] } if { ! [string length "$passphrase"]} { set passphrase $passwd } # Figure out cypher type if {[info exists cypher]} { # command line cypher type set cyphertype $cypher } else { set cyphertype [find cyphertype $router] } # Figure out connection method set cmethod [find method $router] if { "$cmethod" == "" } { set cmethod {{telnet} {ssh}} } # Figure out the SSH executable name set sshcmd [join [lindex [find sshcmd $router] 0] ""] if { "$sshcmd" == "" } { set sshcmd {ssh} } # Figure out the telnet executable name set telnetcmd [join [lindex [find telnetcmd $router] 0] ""] if { "$telnetcmd" == "" } { set telnetcmd "telnet -K" } # if [-mM], skip do not login if { $do_cloginrcdbg > 0 } { continue; } # Login to the router if {[login $router $ruser $userpswd $passwd $cmethod $cyphertype $identfile]} { incr exitval # if login failed or rsh was unsuccessful, move on to the next device continue } # we are logged in, now figure out the full prompt: # cli prompt: # A:some.domain.name# configure # cli prompt when config is unsaved: # *A:some.domain.name# configure # config prompt: # A:some.domain.name>config# vrrp # *A:some.domain.name>config>vrrp# send "\r" regsub -all {^(\^*)(.*)} $prompt {\2} reprompt regsub -all {\e\[\??\w+} $prompt {\2} reprompt expect { -re "\[\r\n]+" { exp_continue; } -re "^.+$reprompt" { set junk $expect_out(0,string); regsub -all "^\\\*" $junk {} junk regsub -all "\[\]\[\(\)+]" $junk {\\&} prompt; set prompt "\[*]?$prompt"; } } if { $do_command || $do_script } { # disable the fancy prompt send "environment cli-engine type basic\r" expect { -re "MINOR: CLI Command not allowed" { send_error "Error: environment no more command is not allowed\n" incr exitval continue } -re $prompt {} } send "environment save home\r" expect { -re "MINOR: CLI Command not allowed" { send_error "Error: //environment no more command is not allowed\n" incr exitval continue } -re $prompt {} } } if { $do_command } { if {[run_commands $prompt $command]} { incr exitval continue } } elseif { $do_script } { source $sfile catch {close}; } else { label $router log_user 1 interact } # End of for each router catch {wait}; sleep 0.3 } exit $exitval