04. February 2018 · Comments Off on Collect and archive all runtime information, statistics and status on F5 systems · Categories: F5, Linux, Linux Scripts, Load Balancing, Networking · Tags: , , , , , , , , , , ,

Last march I posted a TCL/Expect script (rtrinfo.exp) to backup configs and regularly collect runtime information via show commands on Cisco devices for archival purposes. It’s proven itself to be very useful and replaces the need to purchase convoluted commercial software to archive device configs. Not only do I use it, but I know of a few companies that have adopted it as well. Recently I needed something similar that could collect and archive all runtime information, statistics and status on F5 systems; the following is that script.

The script works by pushing a small, base64 encoded, command string to the F5 to be executed. The command string simply does a “tmsh -q show \?” to get a list of all show commands based on the enabled modules. The available runtime information is collected and piped to a for loop that runs all available show commands.

echo "Zm9yIE1PRCBpbiBgdG1zaCAtcSBzaG93IFw/IHwgc2VkIC1uIC1lICcvTW9kdWxlczovLC9PcHRpb25zOi9wJyB8IGF3ayAnL14gIC97cHJpbnQgJDF9J2A7IGRvIHRtc2ggLXEgc2hvdyAkTU9EIDI+IC9kZXYvbnVsbDsgZG9uZQ==" | base64 -d
for MOD in `tmsh -q show \? | sed -n -e '/Modules:/,/Options:/p' | awk '/^  /{print $1}'`; do tmsh -q show $MOD 2> /dev/null; done

All the output from the F5 is collected and some awk-foo is used to determine appropriate output destination on a per-line basis. The while loop appends the line to the appropriate file. Additionally all previous output archived to an appropriately named tar.gz file. I have also added the ability to silence the output, specify an output path override and to use root’s ssh private key instead of using the password (for running via cron).

f5info.sh: Bourne-Again shell script, ASCII text executable

#!/bin/bash
## Collect and archive all runtime information, statistics and status on F5 systems.
## 2018 (v1.0) - Script from www.davideaves.com
 
OUTDIR="."
 
### Script Functions ###
function USAGE () {
 # Display the script arguments.
 printf "Usage: $0 -d bigip -i id_rsa -p path\n\n"
 printf "Requires:\n"
 printf "\t-d: Target F5 system.\n"
 printf "Options:\n"
 printf "\t-i: Private id_rsa of root user.\n"
 printf "\t-p: Destination of output directory.\n"
 printf "\t-q: Quiet, do not show anything.\n"
}
 
function CLEANUP {
 # Cleanup after the script finishes.
 [ -e "${IDENTITY}" ] && { rm -rf "${IDENTITY}"; }
}
 
### Get CLI options ###
while getopts "d:i:p:q" ARG; do
 case "${ARG}" in
  d) F5="${OPTARG^^}";;
  i) trap CLEANUP EXIT
     IDENTITY="$(mktemp)"
     chmod 600 "${IDENTITY}" && cat "${OPTARG}" > "${IDENTITY}";;
  p) OUTDIR="${OPTARG}";;
  q) QUIET="YES";;
 esac
done 2> /dev/null
 
### Display USAGE if F5 not defined ###
[ -z "${F5}" ] && { USAGE && exit 1; }
 
### Archive & Create OUTDIR ###
if [ -d "${OUTDIR}/${F5}" ]
 then ARCHIVE="${OUTDIR}/${F5}_$(date +%Y%m%d -d @$(stat -c %Y "${OUTDIR}/${F5}")).tar.gz"
 
  [ -e "${ARCHIVE}" ] && { rm -f "${ARCHIVE}"; }
  [ -z "${QUIET}" ] && { echo "Archiving: ${ARCHIVE}"; }
  tar zcfP "${ARCHIVE}" "${OUTDIR}/${F5}" && rm -rf "${OUTDIR}/${F5}"
 
fi && ssh -q -o StrictHostKeyChecking=no `[ -r "${IDENTITY}" ] && { echo -i "${IDENTITY}"; }` root@${F5} \
'bash -c "$(base64 -di <<< Zm9yIE1PRCBpbiBgdG1zaCAtcSBzaG93IFw/IHwgc2VkIC1uIC1lICcvTW9kdWxlczovLC9PcHRpb25zOi9wJyB8IGF3ayAnL14gIC97cHJpbnQgJDF9J2A7IGRvIHRtc2ggLXEgc2hvdyAkTU9EIDI+IC9kZXYvbnVsbDsgZG9uZQ==)"' |\
 awk 'BEGIN{
  FS=": "
 }
 // {
  gsub(/[ \t]+$/, "")
 
  # LN buffer
  LN[1]=LN[0]
  LN[0]=$0
 
  # Build OUTPUT variable
  ## LN ends with "{" - special case header
  if(substr(LN[0],length(LN[0]),1) == "{") {
   COUNT=split(LN[0],FN," ") - "1"
   for (i = 1; i <= COUNT; i++) FILE=FILE FN[i] "_"
   OUTPUT=substr(FILE, 1, length(FILE)-1)
  }
  ## LN does not contain "::" but is a header
  else if(OUTPUT == "" && LN[0] ~ /^[a-z,A-Z]/) {
   OUTPUT=LN[0]
  }
  ## LN contains "::" and is a header
  else if(LN[0] ~ /^[A-Z].*::[A-Z]/) {
   gsub(/::/,"-")
   DIR=gensub(/\ /, "_", "g", tolower($1))
   FILE=gensub(/[ ,:].*/, "", "g", $2)
   if(FILE != "") {
    OUTPUT=DIR"/"FILE
   } else {
    OUTPUT=DIR
   }
  }
 
  # Print OUTPUT & LN buffer
  if(OUTPUT != "") print(OUTPUT"<"LN[1])
 }
 END{
  print(OUTPUT"<"LN[0])
 }' | while IFS="<" read OUTPUT LN
  do
 
     if [ ! -w "${OUTDIR}/${F5}/${OUTPUT}" ]
      then [ -z "${QUIET}" ] && { echo "Saving: ${OUTDIR}/${F5}/${OUTPUT}"; }
           install -D /dev/null -m 644 "${OUTDIR}/${F5}/${OUTPUT}"
     fi && echo "${LN}" >> "${OUTDIR}/${F5}/${OUTPUT}"
 
 done

About a year ago I wrote two F5 scripts for mapping and filtering VIPs on an F5 BigIP. I use those scripts often and the f5filter script I continue to tweak/correct minor bugs; which I post in the comments. The following script is cut from same cloth as the previous 2 scripts, except this script search for an F5 VIP by Node; even if the VIP directs traffic to the node via a Policy or iRule.

f5node.sh: Bourne-Again shell script text executable

#!/bin/bash
## Reverse filter a single Node on a BigIP.
## 2017 (v1.0) - Script from www.davideaves.com
 
F5CONFIG="$1"
F5NODE="$2"
 
# Fix broken TERM variable if using screen.
[ "$TERM" == "screen-256color" ] && { export TERM="xterm-256color"; }
 
# FUNCTION: End Script if error.
DIE() {
 echo "ERROR: Validate \"$_\" is installed and working on your system."
 exit 0
}
 
# 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"
 
### Print Syntax if arguments are not provided. ###
if [ ! -e "$F5CONFIG" ] || [ -z "$F5NODE" ]
 then
 echo "Usage: $0 bigip.conf 10.1.1.10"
 exit 0;
fi
 
### Check to see if we are running this on an F5. ###
type -p tmsh > /dev/null && if [ "$(cat /var/prompt/ps1)" == "Active" ]
  then STATE=$cbGREEN
  else STATE=$cbRED
fi
 
### The function that does all the filtering. ###
F5FILTER() {
 if [[ "$(file "$F5CONFIG")" == *"ASCII"* ]]
  then cat "$F5CONFIG"
 elif [[ "$(file "$F5CONFIG")" == *"gzip compressed data"* ]]
  then tar -xOvf "$F5CONFIG" config/bigip.conf 2> /dev/null
 fi | sed -n -e '/^ltm '"$(echo $F5STANZA)"'.*/,/^}$/p' | sed 's/^}/}|/g' | tr -d '[:cntrl:]' |\
  sed 's/|/\n/g;s/ \{1,\}/ /g' | grep "$F5DIG"
}
 
### Display status icon back to the user. ###
STATUS() {
 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
}
 
### Display the final result back to the user. ###
PRINT() {
 if [ ! -z "$STATE" ]
  then ### We are running this script on the F5. ###
   printf "$STATE$cfBLACK$(cat /var/prompt/hostname)$cCLS "
 
   # Collect status of VIRTUAL and print.
   printf ">>> "
   STATUS="$(tmsh show ltm virtual $VIRTUAL 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
   STATUS
   printf "$c1BOLD$VIRTUAL$cCLS < << $cf$POLICY$cCLS\n  "
 
   # Collect status of POOL and print.
   STATUS="$(tmsh show ltm pool $POOL 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
   STATUS
   printf "$cfCYAN$POOL$cCLS\n"
 
   # Collect status of Nodes and print.
   for NODE in $NODES;
    do STATUS="$(tmsh show ltm node $NODE 2> /dev/null | grep -e "Availability" -e "State" | awk -F':' '{printf $NF" "}' | sed 's/ \+/ /g;s/^[ \t]*//;s/[ \t]*$//')"
       STATUS && printf "$NODE "
   done | sed 's/\] /\]_/g' | xargs -n4 | column -t | awk '{print "  ",$0}' |\
   sed 's/'"$F5NODE"'/'"$cfYELLOW$F5NODE$cCLS "'/g;s/\]_/\] /g'
 
 else ### We are not Running on an F5 ###
   printf ">>> $c1BOLD$VIRTUAL$cCLS < << $cf$POLICY$cCLS\n  $cfCYAN$POOL$cCLS\n"
   printf "$NODES\n" | xargs -n4 | column -t | awk '{print "  ",$0}' |\
   sed 's/'"$F5NODE"'/'"$cfYELLOW$F5NODE$cCLS "'/g'
 fi; echo
}
 
# Main Loop.
F5STANZA="pool"
F5DIG="$F5NODE "
 
for i in $(F5FILTER)
 do
  if [ "$i" == "pool" ]
   then echo; FLAG="$i"
  elif [ "$i" == "address" ]
   then FLAG="$i"
  elif [ ! -z "${FLAG}" ]
   then printf "$i " | sed 's/^\/.*\///'
   unset FLAG
  fi
done | grep -v ^$ | while read POOL NODES
 do
 
  # Dig polices and iRules binding the pool to VIPs.
  for F5STANZA in policy profile rule
   do F5DIG="$POOL"
 
      if [ "$F5STANZA" == "policy" ]
       then COLOR="$c1BOLD$cfGREEN"
      elif [ "$F5STANZA" == "rule" ]
       then COLOR="$c0BOLD$cfGREEN"
      else
       COLOR="$c0BOLD$cfMAGENTA"
      fi
 
      F5FILTER | sed 's/profile httpclass/httpclass/g' | awk '{print $3}' | sed 's/\/.*\///g' | while read POLICY;
       do F5STANZA="virtual "
          F5DIG="$POLICY"
          F5FILTER | awk '{print $3}' | sed 's/\/.*\///g' | while read VIRTUAL
            do POLICY="$COLOR$POLICY"
               PRINT
          done
      done
  done
 
  # Dig VIPs that are bound to the pool.
  F5STANZA="virtual "
  F5DIG="$POOL"
  F5FILTER | awk '{print $3}' | sed 's/\/.*\///g' | while read VIRTUAL
   do PRINT
  done
 
done

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"
26. August 2016 · Comments Off on Backing up your F5 load balancers. · Categories: F5, Linux, Linux Scripts, Load Balancing, Networking · Tags: , , , , ,

The following script is for performing scheduled backups of F5 load balancers. The Script initiates a backup against the F5 via SSH and then SCP’s the UCS output file off the box. It is meant to be ran in the crontab, on a Linux box, against the F5’s in an environment.

For further reading please reference the following F5 Support Documentation:

Feel free to review, modify or use this script however you see fit. Remember you do so at your own risk!

#!/bin/bash
## Create/Backup a UCS file against a list of F5 loadbalancers.
## 2016 (v1.0) - Script from www.davideaves.com
 
F5HOSTS="bigip01 bigip02"
BACKUPDIR="/srv/f5backup"
 
# FUNCTION: End Script if error.
DIE() {
 echo "ERROR: Validate \"$_\" is installed and working on your system."
 exit 0
}
 
# FUNCTION: Fetch the UCS or private id_rsa keyfile.
UCSFETCH() {
 if [ -e "$BACKUPDIR/.$F5.identity" ]
  then
        printf "$F5 "
 
        # Delete backup files older than 90 days.
        find "$BACKUPDIR" -maxdepth 1 -type f -name "$F5*.ucs" -mtime +90 -exec rm {} \;
 
        # Create the UCS backup file.
        ssh -q -o StrictHostKeyChecking=no -i "$BACKUPDIR/.$F5.identity" root@$F5 "tmsh save /sys ucs $(echo $F5) > /dev/null 2>&1"
 
        # Copy down the UCS backup file.
        scp -q -o StrictHostKeyChecking=no -i "$BACKUPDIR/.$F5.identity" root@$F5:/var/local/ucs/$F5.ucs "$BACKUPDIR/" && UCSRENAME
 else
        printf "\n$F5 "
 
        # Copy down the F5's private id_rsa keyfile for root user.
        scp -o StrictHostKeyChecking=no root@$F5:/var/ssh/root/identity "$BACKUPDIR/.$F5.identity" 2> /dev/null
 fi
}
 
# FUNCTION: Rename the UCS file.
UCSRENAME() {
 mv "$BACKUPDIR/$F5.ucs" "$BACKUPDIR/$F5$(echo $F5 | cksum | awk '{print "_"$1}') ($(date +%F -d "$(file "$BACKUPDIR/$F5.ucs" | awk -F': ' '{print $NF}' | awk -F',' '{print $1}')")).ucs"
}
 
# Validate script requirements are meet.
type -p scp > /dev/null || DIE
 
### Main Loop ###
for F5 in $(echo $F5HOSTS | tr [:lower:] [:upper:]); do
 
 # Validate host is pingable before fetching UCS file.
 ping -c1 $F5 > /dev/null 2>&1 && UCSFETCH
 
done; echo