davideaves.com

Live in a world of your own, but always welcome visitors.

View on GitHub
31 March 2017

TCL/Expect script to backup Cisco device configs.

by deaves

I am not a software developer, but I do like challenges and am interested in learning about different software languages. For this project I decided to practice some TCL/Expect so I rewrote a poorly written Perl script I came across. This script will back up Cisco device configurations by reading 2 files: command.db and device.db … It loads them into a data dictionary and iterates through it using a control loop. It then logs into the device, by shelling out to rancid, and executes all its commands. Its a little hacky, but works. I even wrote a shell script to parse the log output into separate files.

/srv/rtrinfo/rtrinfo.exp: a expect script, ASCII text executable

#!/usr/bin/expect -f
# Login to a list of devices and collect show output.
#
## Requires: clogin (rancid)

exp_version -exit 5.0
set timeout 5

set DEVDB "[lindex $argv 0]"
set LOGDIR "/var/log/rtrinfo"
set OUTLOG "/srv/rtrinfo/output.log"

## Validate input files or print usage.
if {0==[llength $DEVDB]} {
    send_user "usage: $argv0 -device.db-\n"
    exit
} else {
   if {[file isfile "cmd.db"] == "1"} {
      set CMDDB "cmd.db"
   } elseif {[file isfile "[file dirname $argv0]/cmd.db"] == "1"} {
      set CMDDB "[file dirname $argv0]/cmd.db"
   } else {
    send_user "Unable to find cmd.db file, can not start...\n"
    exit 1
   }
}

################################################################

### Procedure to create 3 column dictionary ###
proc addDICT {dbVar field1 field2 field3} {

   # Initialize the DEVICE dictionary
   if {![info exists $dbVar]} {
      dict set $dbVar ID 0
   }

   upvar 1 $dbVar db

   # Create a new ID
   dict incr db ID
   set id [dict get $db ID]

   # Add columns into dictionary
   dict set db $id "\"$field1\" \"$field2\" \"$field3\""
}

### Build the CMD and DEVICE dicts from db files ###
foreach DB [list $CMDDB $DEVDB] {
   set DBFILE [open $DB]
   set file [read $DBFILE]
   close $DBFILE

   ## Split into records on newlines
   set records [split $file "\n"]

   ## Load records for dictionary
   foreach rec $records {
      ## split into fields on colons
      set fields [split $rec ";"]
      lassign $fields field1 field2 field3

      if {"[file tail $DB]" == "cmd.db"} {
         # Cols: OUTPUT TYPE CMD
         foreach field2 [split $field2 ","] {
            addDICT CMDS $field2 $field1 $field3
         }
      } else {
         # Cols: HOST TYPE STATE DESC
         addDICT DEVICES $field1 $field2 $field3
      }
   }
}

################################################################

### Open $OUTLOG to be used for post parcing.
set OUTLOG [open "$OUTLOG" w 0664]

### Itterate the DEVICES dictionary ###
dict for {id row} $DEVICES {

   ## Assign field names
   lassign $row DEVICE DEVTYPE STATUS

   ## Process device status
   if {"$STATUS" == "up"} {

      ## Create log output directory if does not exist
      if {[file isdirectory "$LOGDIR"] != "1"} {
         file mkdir "$LOGDIR"
      }

      log_file
      log_file -noappend "$LOGDIR/$DEVTYPE\_$DEVICE.log"

      ## Run rancid's clogin with a 5min timeout.
      spawn timeout 300 clogin $DEVICE

      expect "*#" {

      ## Set proper terminal length ##
      if {$DEVTYPE != "asa"} {
         send "terminal length 0\r"
      } else {
         send "terminal pager 0\r"
      }

      ### Itterate the CMDS dictionary ###
      dict for {id row} $CMDS {
         ## Assign field names
         lassign $row CMDTYPE OUTPUT CMD

         ## Push commands to device & update $OUTLOG
         if {($DEVTYPE == $CMDTYPE)&&($OUTPUT != "")} {
            puts $OUTLOG "$LOGDIR/$DEVTYPE\_$DEVICE.log;$OUTPUT;$CMD"
            expect "*#" { send "$CMD\r" }
         }
      }

      ## We are done! logout
      expect "*#" { send "exit\r" }
      expect EOF
      }

   }
}

close $OUTLOG

### Run a shell script to parse the output.log ###
#exec "[file dirname $argv0]/rtrparse.sh"

/srv/rtrinfo/cmd.db: ASCII text

acl;asa,router;show access-list
arp;ap,ace,asa,router,switch;show arp
arpinspection;ace;show arp inspection
arpstats;ace;show arp statistics
bgp;router;show ip bgp
bgpsumm;router;show ip bgp summary
boot;switch;show boot
cdpneighbors;ap,router,switch;show cdp neighbors
conferror;ace;sh ft config-error
controller;router;show controller
cpuhis;ap,router,switch;show process cpu history
debug;ap,router,switch;show debug
dot11ass;ap;show dot11 associations
envall;switch;show env all
env;router;show environment all
errdis;switch;show interface status err-disabled
filesys;router,switch;dir
flash;asa;show flashfs
intdesc;ap,router,switch;show interface description
interface;ap,asa,router,switch;show interface
intfbrie;ap,ace,router,switch;show ip interface brief
intipbrief;asa;show interface ip brief
intstatus;switch;show interface status
intsumm;router;show int summary
inventory;asa,router,switch;show inventory
iparp;ap,switch;show ip arp
ipint;router;show ip int
mac;switch;show mac address-table
nameif;asa;show nameif
ntpassoc;ap,asa,router,switch;show ntp assoc
plat;router;show platform
power;switch;show power inline
probe;ace;show probe
routes;asa;show route
routes;ap,router,switch;show ip route
rserver;ace;show rserver
running;ace;show running-config
running;ap,asa,router,switch;more system:running-config
serverfarm;ace;show serverfarm
service-policy;ace;show service-policy
service-pol-summ;ace;show service-policy summary
spantree;switch;show spanning-tree
srvfarmdetail;ace;show serverfarm detail
version;ap,ace,asa,router,switch;show version
vlan;switch;show vlan

/srv/rtrinfo/device.db: ASCII text

192.168.0.1;router;up;Site Router
192.168.0.2;ap;up;Atonomous AP
192.168.0.3;asa;ASA Firewall
192.168.0.5;switch;Site Switch
192.168.0.10;ace;Cisco ACE

/srv/rtrinfo/rtrparse.sh: Bourne-Again shell script, ASCII text executable

#!/bin/bash
# Parse the new rtrinfo output.log and create individual cmd output.
# 2016 (v.03) - Script from www.davideaves.com

OUTLOG="/srv/rtrinfo/output.log"
RTRPATH="$(dirname $OUTLOG)"

### Delete previous directories.
for DIR in ace asa router switch
 do [ -d "$RTRPATH/$DIR" ] && { rm -rf "$RTRPATH/$DIR"; }
done

### Itterate through $OUTLOG
grep "\.log" "$OUTLOG" | while IFS=';' read LOGFILE OUTPUT CMD
 do

 ### Get device name and type.
 TYPE="$(basename "$LOGFILE" | awk -F'_' '{print $1}')"
 DEVICE="$(basename "$LOGFILE" | awk -F'_' '{print $2}' | sed 's/\.log$//')"

 ### Create output directory.
 [ ! -d "$RTRPATH/$TYPE/$OUTPUT" ] && { mkdir -p "$RTRPATH/$TYPE/$OUTPUT"; }

 ### Extract rtrinfo:output logs and dump into individual files.
 # 1) sed identify $CMD output between prompts.
 # 2) awk drops X beginning line(s).
 # 3) sed to drop the last line.
 sed -n "/[^.].*[#, ]$CMD\(.\)\{1,2\}$/,/[^.].*#.*$/p" "$LOGFILE" \
 | awk 'NR > 0' | sed -n '$!p' > "$RTRPATH/$TYPE/$OUTPUT/$DEVICE.txt"

 ## EX: sed -n "/[^.]\([a-zA-Z]\)\{3\}[0-9].*[#, ]$CMD\(.\)\{1,2\}$/,/[^.]\([a-zA-Z]\)\{3\}[0-9].*#.*$/p"

done

Since this is something that would be collect nightly or weekly, I would probably kick this off using logrotate (as opposed to using crontab). The following would be what I would drop in my /etc/logrotate.d directory…

/etc/logrotate.d/rtrinfo: ASCII text

/var/log/rtrinfo/*.log {
        rotate 14
        daily
        missingok
        compress
        sharedscripts
        postrotate
                /srv/rtrinfo/rtrinfo.exp /srv/rtrinfo/device.db > /dev/null
        endscript
}
tags: bash - expect - linux - logrotate - rancid - rtrinfo - script - shell - tcl