namespace export cli_get_ttyname namespace export cli_run cli_run_interactive namespace export xml_pi_exec xml_pi_parse xml_pi_write xml_pi_read # cli_debug_extended can be set in user scripts to display # extended debug commands. debug ev man tcl cli must also # be enabled. Place this in user script to enable: # set ::cisco::eem::cli_debug_extended 1 variable cli_debug_extended 0 } # a typical usage of cli library: # set result [cli_open] # array set cli1 $result # set result [cli_exec $cli1(fd) "en"] # process result... # set result [cli_exec $cli1(fd) "config"] # set result [cli_exec $cli1(fd) "interface Ethernet1/0"] # set result [cli_exec $cli1(fd) "no shut"] # set result [cli_exec $cli1(fd) "end"] # cli_close $cli1(fd) $cli1(tty_id) # or # set result [cli_open] # array set cli1 $result # cli_write $cli1(fd) "en" # cli_write $cli1(fd) "show run" # next time before cli_exec: # cli_read_drain $cli1(fd) # set result [cli_exec $cli1(fd) "show run"] # cli_close $cli1(fd) $cli1(tty_id) # Spawn an exec, open a channel to it and return the slave side # channel handler. # # Possible errors raised: # 1. cannot get pty for exec # 2. cannot spawn exec # 3. error reading the first prompt proc ::cisco::eem::cli_open { { eem_no_scan 1 } } { global _cerrno _cerr_sub_num _cerr_sub_err _cerr_posix_err _cerr_str global lastprompt # Send a debug message if cli debug flag is on ::cisco::eem::cli_debug "CTL" "cli_open called." # Allocate a tty and connect to exec if [catch {::cisco::eem::tty_open $eem_no_scan} result] { return -code error "cannot get pty for exec: $result" } else { array set arr1 $result # returns # pty = pty device name # tty = tty device name # fd = tty number # tty_id = tty ID } # both read and write won't be buffered # fconfigure $arr1(fd) -buffering none # non-blocking read and write # fconfigure $arr1(fd) -blocking 0 # drain the first prompt set lastprompt "" if [catch {cli_read $arr1(fd)} result] { return -code error "error reading the first prompt: $result" } else { return [array get arr1] # arr1 contains # tty_id = tty ID # pty = pty device name # tty = tty device name # fd = tty number } } # Write the command 'cmd' to the channel whose handler is given # by 'fd' to execute the command. # # Possible errors raised: # None proc ::cisco::eem::cli_write {fd cmd} { global lastprompt # Send a debug message if cli debug flag is on set totalcmd $lastprompt append totalcmd $cmd ::cisco::eem::cli_debug "IN " "$totalcmd" ::cisco::eem::tty_write $fd $cmd return } # Read the command output from the CLI channel whose handler is given # by 'fd' until the pattern 'ptn' occurs in the contents read. Return # all the contents read up to the match. # # Possible errors raised: # None proc ::cisco::eem::cli_read_pattern {fd ptn} { global lastprompt set result "" set is_end 0 # set the string length used to perform the prompt check # only the previous pcheck_len characters will be checked for the prompt set pcheck_len 256 while {$is_end == 0} { set str [::cisco::eem::tty_read $fd] if {$str == ""} { after 100 continue } # double quotes (don't change to curly braces!) #action_syslog priority info msg --$str-- append result $str set len_result [string length $result] if {$len_result <= $pcheck_len} { set pcheck_off 0 } else { set pcheck_off [expr $len_result - $pcheck_len] } # look for a prompt string match from the offset in pcheck_off # to the end of the string Note: the source of a "regexp" operation # is always converted to unicode in Tcl (2 bytes per character) set match_str [string range $result $pcheck_off end] if {$::cisco::eem::cli_debug_extended == 1} { regsub -all {\n} $ptn "\\n" debug_prompt ::cisco::eem::cli_debug "CTL" "match pattern: $debug_prompt" } set is_end [regexp "[set ptn]" $match_str match rtr cfg pmt] if {$is_end == 1} { set lastprompt $rtr append lastprompt $cfg $pmt } after 100 } # Send a debug message if cli debug flag is on if {[string index $result 0] == "\n" || [string index $result 0] == "\r"} { ::cisco::eem::cli_debug "OUT" [string range $result 1 end] } else { ::cisco::eem::cli_debug "OUT" "$result" } return $result } # Read the command output from the CLI channel whose handler is given # by 'fd' until the pattern of the router prompt occurs in the contents # read. Return all the contents read up to the match. # # Possible errors raised: # 1. cannot get router name proc ::cisco::eem::cli_read {fd} { global _cerrno _cerr_sub_num _cerr_sub_err _cerr_posix_err _cerr_str global lastprompt set result "" set is_end 0 array set rname [sys_reqinfo_routername] set routername $rname(routername) if {[string match "" $routername]} { return -code error "Host name is not configured" } set routername_trunc [string range $routername 0 19] # Escape special characters in routername regsub -all {[][$^?+*()|\\.]} $routername \\\\& routername regsub -all {[][$^?+*()|\\.]} $routername_trunc \\\\& routername_trunc # wait a small amount of time to allow the prompt to change in tty. after 10 # set the string length used to perform the prompt check # only the previous pcheck_len characters will be checked for the prompt. set pcheck_len 256 while {$is_end == 0} { set str [::cisco::eem::tty_read $fd] if {$str == ""} { after 100 continue } append result $str set len_result [string length $result] if {$len_result <= $pcheck_len} { set pcheck_off 0 } else { set pcheck_off [expr $len_result - $pcheck_len] } # Look for a prompt string match from the offset in pcheck_off. set match_str [string range $result $pcheck_off end] if [catch {::cisco::eem::tty_prompt $fd} prompt_res] { set prompt [format "\n*(%s|%s)(\\(config\[^\n\]*\\))?(#|>)" \ $routername $routername_trunc] } else { set prompt [format \ "\n*(%s)|(\n*(%s|%s)(\\(config\[^\n\]*\\))?(#|>))" \ $prompt_res $routername $routername_trunc] } if {$::cisco::eem::cli_debug_extended == 1} { regsub -all {\n} $prompt "\\n" debug_prompt ::cisco::eem::cli_debug "CTL" "match pattern: $debug_prompt" } set is_end [regexp "[set prompt]" $match_str match rtr cfg pmt] if {$is_end == 1} { set lastprompt $rtr if {$pmt != ""} { append lastprompt $cfg $pmt } } after 100 } # Send a debug message if cli debug flag is on if {[string index $result 0] == "\n" || [string index $result 0] == "\r"} { ::cisco::eem::cli_debug "OUT" [string range $result 1 end] } else { ::cisco::eem::cli_debug "OUT" "$result" } return $result } # Write the command 'cmd' to the channel whose handler is given # by 'fd' to execute the command. Then read the output of the # command from the channel and return the output. # # Possible errors raised: # 1. error reading the channel proc ::cisco::eem::cli_exec {fd cmd} { cli_write $fd "$cmd" if [catch {cli_read $fd} result] { return -code error "error reading the channel: $result" } else { return $result } } # Close the exec process and the CLI channel connected to it given # the handler of the channel 'fd'. # # Possible errors raised: # 1. cannot close channel proc ::cisco::eem::cli_close {fd tty_id} { # Send a debug message if cli debug flag is on ::cisco::eem::cli_debug "CTL" "cli_close called." if [catch {::cisco::eem::tty_close $fd} result] { return -code error "cannot close the channel: $result" } } # Read one line of the command output from the CLI channel whose handler # is given by 'fd'. Return the line read. # # Possible errors raised: # None proc ::cisco::eem::cli_read_line {fd} { set result [cli_read_pattern $fd "\[^\n]*\n"] return $result } # Read and drain the CLI channel. proc ::cisco::eem::read_drain {fd} { set result "" for {set i 0} {$i < 1000} {incr i} { set str [::cisco::eem::tty_read $fd] append result $str # no need to check this line because automore is disabled for exec # if {[string match "*--More--*" $str]} { # puts -nonewline $fd " " # } } # Send a debug message if cli debug flag is on ::cisco::eem::cli_debug "OUT" "$result" return $result } # Read and drain the command output of the CLI channel whose handler # is given by 'fd'. Return all the contents read. # # Possible errors raised: # None proc ::cisco::eem::cli_read_drain { fd { wait_time 200 } } { set result [::cisco::eem::read_drain $fd] after $wait_time return $result } # Return both the real and pseudo tty names for this cli connection. # # Possible errors raised: # 1. cannot get tty name proc ::cisco::eem::cli_get_ttyname {tty_id} { if [catch {::cisco::eem::tty_get_ttyname $tty_id} result] { return -code error "cannot get tty name: $result" } # Send a debug message if cli debug flag is on ::cisco::eem::cli_debug "CTL" "cli_get_ttyname called." array set arr1 $result return [array get arr1] # arr1 contains # pty = pty device name # tty = tty device name } # Open a CLI channel, enter "enable" mode and execute each command in the # list 'clist' in succession. As each command is executed its output is # appended to a result variable. Upon exhaustion of the input list, the CLI # channel is closed and the aggregate result is returned. # # Example: To shutdown an interface use the following: # set cmdlist [list "config t" "int fa0/0" "shut"] # set rc [catch {cli_run $cmdlist} result] # # Possible errors raised: # 1. cannot get pty for exec # 2. cannot spawn exec # 3. error reading the first prompt # 4. error reading the channel # 5. cannot close channel proc cli_run { clist } { set rbuf "" if {[llength $clist] < 1} { return -code ok $rbuf } if {[catch {cli_open} result]} { return -code error $result } else { array set cliarr $result } if {[catch {cli_exec $cliarr(fd) "enable"} result]} { return -code error $result } foreach cmd $clist { if {[catch {cli_exec $cliarr(fd) $cmd} result]} { return -code error $result } append rbuf $result } if {[catch {cli_close $cliarr(fd) $cliarr(tty_id)} result]} { puts "WARNING: $result" } return -code ok $rbuf } # Open a CLI channel, enter "enable" mode and execute each command in the # list 'clist'. The format of each entry in the list is an array with three # members: # command - command to be executed # expect - a regular expression pattern match for the expected reply prompt # responses - a list of possible responses to the reply prompt constructed # as an array with two members: # expect - a regular expression pattern match for a possible reply prompt # reply - the replay for that expected prompt # As each command is executed its output is appended to a result variable. # Upon exhaustion of the input list, the CLI channel is closed and the # aggregate result is returned. # # Example: To clear counters for intrface fa0/0 use the following: # set cmdarr(command) "clear counters fa0/0" # set cmdarr(responses) [list] # set resps(expect) {[confirm]} # set resps(reply) "y" # lappend cmdarr(responses) [array get resps] # set rc [catch {cli_run_interactive [list [array get cmdarr]]} result] # # Possible errors raised: # 1. cannot get pty for exec # 2. cannot spawn exec # 3. error reading the first prompt # 4. error reading the channel # 5. cannot close channel proc cli_run_interactive { clist } { set rbuf "" if {[llength $clist] < 1} { return -code ok $rbuf } if {[catch {cli_open} result]} { return -code error $result } else { array set cliarr $result } if {[catch {cli_exec $cliarr(fd) "enable"} result]} { return -code error $result } foreach cmd $clist { array set sendexp $cmd if {[catch {cli_write $cliarr(fd) $sendexp(command)} result]} { return -code error $result } foreach response $sendexp(responses) { array set resp $response if {[catch {cli_read_pattern $cliarr(fd) $resp(expect)} result]} { return -code error $result } if {[catch {cli_write $cliarr(fd) $resp(reply)} result]} { return -code error $result } } if {[catch {cli_read $cliarr(fd)} result]} { return -code error $result } append rbuf $result } if {[catch {cli_close $cliarr(fd) $cliarr(tty_id)} result]} { puts "WARNING: $result" } return -code ok $rbuf } # Process the XML show command raw output passed into this function as # xml_data and retrieve those fields that are specified by xml_tags_list. # # The following processing takes place: # # Step 1: The XML tag list is validated as a Tcl list. An XML tag can be # specified as the low order XML tag name or as a fully qualified XML tag # name in case the low order name is ambiguous for a given command. # # Example tags: # # # # Step 2: The xml_data is validated as valid XML and parsed into an XML # parse tree. # # Step 3: A walk is made through the XML parse tree and each tag is # compared with entries in the XML tag list. # # When a match occurs it is determined if the tag name matches a Tcl # procedure defined within the current Tcl scope. If so, that Tcl # procedure will be called with the current result. If not, the tag name # and the data associated with that tag name will be appended to the # current result. # # Note: The current result is reset after Tcl procedure calls. # # Return the result in a Tcl array indexed by xml tag name. # # Possible errors raised: # 1. error splitting the XML tags list # 2. null XML tag list specified # 3. XML tag tree exceeds 20 levels # 4. called Tcl procedure returned an error # 5. memory allocation failure # 6. XML parse failure # 7. failed to create XML domain proc ::cisco::eem::xml_pi_parse {fd xml_data xml_tags_list} { # validate the fd even though it is unused if [catch {::cisco::eem::tty_prompt $fd} result] { return -code error "invalid fd: $result" } # for each item in the xml_tags_list locate the xml data and stuff it # into an array set xml [string trim $xml_data] return [xml_parse_data $xml $xml_tags_list] } # Write the XML-PI command 'cmd' to the channel whose handler is given # by 'fd' and spec file is given by 'spec_file'. # # Possible errors raised: # None proc ::cisco::eem::xml_pi_write {fd cmd spec_file} { cli_write $fd "$cmd | format $spec_file" return } # Read the XML-PI command output from the CLI channel whose handler is given # by 'fd' until the pattern of the router prompt occurs in the contents # read. Return all the contents read up to the match. # # Possible errors raised: # 1. cannot get router name # 2. command error proc ::cisco::eem::xml_pi_read {fd} { if [catch {cli_read $fd} result] { return -code error "error reading the channel: $result" } else { if [string match "*% Invalid input detected at *" $result] { return -code error "error reading the channel: $result" } return $result } } # Write the XML-PI command 'cmd' to the channel whose handler is given # by 'fd' and spec file is given by 'spec_file' to execute the command. # Then read the raw XML output data of the command from the channel and # return the output. # # Possible errors raised: # 1. error reading the channel proc ::cisco::eem::xml_pi_exec {fd cmd { spec_file "" } } { xml_pi_write $fd $cmd $spec_file if [catch {xml_pi_read $fd} result] { return -code error "error reading the channel: $result" } else { return $result } }