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
}
01. January 2017 · Comments Off on Custom 2x Orange Pi Zero Case · Categories: Cases, Design, Linux · Tags: , , , , , ,

The following is a 2x Orange Pi Zero case I designed in Inkscape and cut out in acrylic on my laser cutter… I have been incorporating a lot of single-board arm systems, like the Raspberry Pi, in my projects lately. Among one of my favorite boards to play with is the Orange Pi; due to its price, hardware design and power. I assisting some developers with one of their projects and wanted 2 headless systems. My solution was to get 2 Orange Pi Zero’s and set them up running armbian. Since a lot of people were handling the systems, I created a custom case to both protect and show off the systems.

Custom Orange Pi Zero Case

12. December 2016 · Comments Off on Convert Cisco APs from LWAP to Autonomous · Categories: Cisco, Networking, Wireless · Tags: , , ,

A lot of companies are pulling out their old Cisco wireless infrastructure to upgrade or replace it all together. As a result its pretty easy to get your hands on older Cisco AP’s, unfortunately by default they require a special controller in order to function. If you want to turn your old Cisco LWAP AP into something other than a paperweight, you either need to get an older controller or convert the AP to Autonomous. The following snippet is what I had to do to convert my Cisco AP from LWAP to Autonomous. Surprisingly they don’t cover this sort of thing on the CCNA wireless exam; or at least not back when I took it.

Before you begin, just make sure you get the proper “k9w7” autonomous code; bear in mind that “k9w8” is the lightweight code. In my case I had an old Aironet 1130 AG Access Point, so I had to get my hands on the c1130-k9w7-tar.124-25d.JA.tar code. I also needed to setup a TFTP server inside the same vlan as the AP. After I consoled into the AP and logged in to it I had to do the following…

debug lwapp console cli
debug lwapp client no-reload
 
config t
int fa 0
 ip address 10.0.0.1 255.255.255.224
end
 
archive download-sw /force-reload /overwrite tftp://10.0.0.2/c1130-k9w7-tar.124-25d.JA.tar

Over the last two years I have been involved in several successful migrations off of the Cisco ACE platform. While I do not consider myself an ACE expert I have discovered a nice filter option, albeit poorly documented, to display/isolate only the relevant configuration of a particular service-policy (EX: show running-config filter vip.example.com). This filter option is great for both migrations and copying configurations from one ACE to another; for when you need an identical DR VIP. Because of the general usefulness of being to quickly isolate parts of a configuration, I created a script to do the same for the F5. Just like the filter option on the ACE, this script will parse through an existing bigip.conf for a virtual server and display only required configuration items: virtual-address, pool, node, monitor, policy, profile & rules.

f5filter.sh

#!/bin/bash
## Filter out a single F5 virtual server config on a BigIP.
## 2016 (v1.0) - Script from www.davideaves.com
 
F5CONFIG="$1"
F5STANZA="$2"
 
### Print Syntax if arguments are not provided. ###
if [ ! -e "$F5CONFIG" ] || [ -z "$F5STANZA" ]
 then
 echo "Usage: $0 bigip.conf example.domain.com_80_vs"
 exit 0;
fi
 
### The function that does all the filtering. ###
F5FILTER() {
 sed -n -e '/^ltm .*'"$(echo $F5STANZA | sed 's/\//\\\//g')"' {$/,/^}$/ p' $F5CONFIG
}
 
### Build Search commands to run after loop finishes ###
F5FILTER "$F5CONFIG" "$F5STANZA" | while read A B C D
 do
 
  ### Stanza: policy, profile, rule
  if [ -n "LCOUNT" -a "$(echo $A | cut -c1)" == "/" ]
   then echo "$LCOUNT|$A" | grep -v ":[0-9]"
        let LCOUNT++
 
  ### Stanza: virtual server ###
  elif [ "$A" == "ltm" -a "$B" == "virtual" ]
   then echo "80|$B $C"
 
  ### Stanza: pool ###
  elif [ "$A" == "pool" ]
   then F5STANZA="$(echo $B | awk -F'/' '{print $NF}')"
   echo "70|$A $B"
 
   # Dig inside of pool stanza #
   F5FILTER "$F5STANZA" | while read A B C D
    do  if [ "$A" == "monitor" ]
         then echo "40|$B"
        elif [ "$(echo $A | cut -c1)" == "/" -a "$B" == "{" ]
         then echo "50|node $A" | grep ":[0-9]$" | awk -F':' '{print $1}'
        fi
   done
 
  ### Stanza: virtual address ###
  elif [ "$A" == "destination" ]
   then echo "90|virtual-address $(echo $B | awk -F':' '{print $1}')"
 
  ### Stanza: LOOP ###
  elif [ "$B" == "{" -a -z "$C" ]
   then LCOUNT="10"
   [ "$A" == "policies" ] && { LCOUNT="20"; }
   [ "$A" == "rules" ] && { LCOUNT="30"; }
  fi
 
done | sort -n | uniq | while IFS="|" read SEQ F5STANZA
 do printf "#%.0s" {1..60}
    printf "\r### $SEQ: $F5STANZA \n"
    F5FILTER "$F5STANZA"
done

There are a few limitations with this script… It will not pull out any objects referenced in policies or irules. It will also not pull out inherited profiles.

02. November 2016 · Comments Off on Display a LTM Network Map at the bash shell on a F5 BigIP. · Categories: F5, Linux, Linux Scripts, Load Balancing · Tags: , , , ,

I have been doing a bunch of F5 migrations lately and have gotten fond of the visualization of the network map in the F5 GUI. From the CLI to get the status of a VIP you have to parse tmsh output to find the information your looking for. As a personal challenge I wanted to make a script to be ran on the F5 to provide the exact same summary you see in the GUI, but instead form a bash shell…

Network MAP from the GUI


f5_ltm-map-gui

Network Map from the F5 bash shell, using this script


f5_ltm-map-cli

MAP.sh

#!/bin/bash
## Display a LTM Network Map at the bash shell on a F5 BigIP.
## 2016 (v1.0) - Script from www.davideaves.com
 
DEBUG="N"
 
# Fix broken TERM variable if using screen.
[ "$TERM" == "screen-256color" ] && { export TERM="xterm-256color"; }
 
# Start: Collect time for runtime.
TIME=`date +%s`
 
# ANSI color variables.
esc="$(echo -e '\E')";  cCLS="${esc}[0m"
cfBLACK="${esc}[30m";   cbBLACK="${esc}[40m"
cfRED="${esc}[31m";     cbRED="${esc}[41m"
cfGREEN="${esc}[32m";   cbGREEN="${esc}[42m"
cfYELLOW="${esc}[33m";  cbYELLOW="${esc}[43m"
cfBLUE="${esc}[34m";    cbBLUE="${esc}[44m"
cfMAGENTA="${esc}[35m"; cbMAGENTA="${esc}[45m"
cfCYAN="${esc}[36m";    cbCYAN="${esc}[46m"
cfWHITE="${esc}[37m";   cbWHITE="${esc}[47m"
c1BOLD="${esc}[1m";     c0BOLD="${esc}[22m"
 
DIE() {
 ## End Script if error.
 ERR=$?
 echo "$c1BOLD""ERROR"" [$cbRED$ERR$cbBLACK]:$cCLS An error has been encountered."
 exit 1;
}
 
READID() {
 ## Return component & identifier of an array element.
 [ -z "$ID" ] && { echo "READID: Varable \"\$ID\" not found."; exit 1; }
 [ -z "$MAP" ] && { echo "READID: Varable \"\$MAP\" not found."; exit 1; }
 
 export COMPONENT="$(echo ${MAP[$ID]} | awk -F':' '{printf $1}')"
 export IDENTIFIER="$(echo ${MAP[$ID]} | awk -F':' '{printf $2}')"
}
 
STATUS() {
 ## Display status back to the user.
 [ -z "$STATUS" ] && { set > t; echo "STATUS: Varable \"\$STATUS\" not found."; exit 1; }
 
 if    [ "$STATUS" == "available enabled" ]; then printf -- "($c0BOLD$cfBLACK$cbGREEN@$cCLS) "
  elif [ "$STATUS" == "available disabled" ]; then printf -- "($c0BOLD$cfBLACK$cbBLUE@$cCLS) "
  elif [ "$STATUS" == "unknown enabled" ]; then printf -- "[$c0BOLD$cfBLACK$cbBLUE#$cCLS] "
  elif [ "$STATUS" == "offline disabled" ]; then printf -- "< $c0BOLD$cfBLACK$cbWHITE-$cCLS> "
  elif [ "$STATUS" == "offline enabled" ]; then printf -- "< $c0BOLD$cfBLACK$cbRED!$cCLS> "
  elif [ "$STATUS" == "available disabled-by-parent" ]; then printf -- "($c1BOLD$cfBLACK$cbWHITE@$cCLS) "
  elif [ "$STATUS" == "unknown disabled-by-parent" ]; then printf -- "[$c1BOLD$cfBLACK$cbWHITE#$cCLS] "
  elif [ "$STATUS" == "offline disabled-by-parent" ]; then printf -- "< $c1BOLD$cfBLACK$cbWHITE-$cCLS> "
  else printf "[ $STATUS ] "
 fi
}
 
# Validate script requirements are meet.
[ -f "/config/bigip.conf" ] && [ -x "/usr/bin/tmsh" ] || { DIE; }
 
### Main Loop ###
grep ^"ltm virtual .*"$1".* {$" "/config/bigip.conf" | awk '{print $(NF-1)}' | while read VS
 do ((ID=0))
 
  # Read VS into an array.
  MAP=( `tmsh show ltm virtual $VS detail | grep -e "Ltm::" -e "Availability" -e "State" | sed 's/|//g;s/ \+//g' | awk -F':' '{print $(NF-1)":"$NF}'` )
 
  # DEBUG #
  [ "$DEBUG" == "Y" ] && { echo "$cfBLACK$cbCYAN${MAP[*]}$cCLS"; echo; }
 
  # Display Virtual Server #
  STATUS="$(((ID=ID+1)); READID; printf "$IDENTIFIER") $(((ID=ID+2)); READID; printf "$IDENTIFIER")"
 
  READID
  printf -- "$(STATUS)"
  printf "$IDENTIFIER $(echo "${MAP[`expr ${#MAP[@]}-3`]}" | awk -F'(' '{print "("$NF}')\n"
 
  ### Iterate from right of MAP array ###
  ((ID=${#MAP[@]}))
  while [ "$ID" -ge "1" ]
   do ((ID--)); READID
 
     # Display iRules #
     [ "$IDENTIFIER" == "HTTP_REQUEST" ] && { printf "   (*) $COMPONENT\n"; }
 
  done
 
  ### Iterate from left of MAP array ###
  ((ID=2))
  while [ "$ID" -le "${#MAP[@]}" ]
   do ((ID++)); READID
 
     # Display Nodes #
     if [ "$COMPONENT" == "Node" ]
      then
        STATUS="$(((ID=ID-2)); READID; printf "$IDENTIFIER") $(((ID=ID-1)); READID; printf "$IDENTIFIER")"
 
        printf -- "      $(STATUS)"
        printf "$(((ID=ID-3)); READID; printf "$COMPONENT:$IDENTIFIER") "
        printf "$( echo $IDENTIFIER | awk -F'(' '{print "("$NF}')\n"
 
     # Display Pools #
     elif [ "$COMPONENT" == "Pool" ]
      then
        STATUS="$(((ID=ID+1)); READID; printf "$IDENTIFIER") $(((ID=ID+2)); READID; printf "$IDENTIFIER")"
 
        printf -- "   $(STATUS)"
        printf "$IDENTIFIER\n"
     fi
 
  done; echo
 
done 2> /dev/null || DIE
 
# Done: Print status & runtime.
printf "Completed in $B$(expr `date +%s` - $TIME)$B0 seconds...$CLS\n"