19. December 2018 · Comments Off on Ansible playbook to provision Netscaler VIPs. · Categories: Ansible, Linux, Linux Admin, Load Balancing, NetScaler, Networking · Tags: , , ,

The following playbook will create a fully functional VIP; including the supporting monitor, service-group (pool) and servers (nodes) on a netscaler loadbalancer. Additionally, the same playbook has the ability to fully deprovision a VIP and all its supporting artifacts. To do all this I use the native Netscaler Ansible modules. When it comes to using the netscaler_servicegroup module, since the number of servers are not always consistent; I create that task with a Jinja2 template, where its imported back into the play.

netscaler_provision.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text

#!/usr/bin/ansible-playbook -f 10
## Ansible playbook to provision Netscaler VIPs.
# Requires: nitrosdk-python
# 2018 (v.01) - Playbook from www.davideaves.com
---
- name: Netscaler VIP provision
  hosts: netscaler
  connection: local
  gather_facts: False

  vars:

    ansible_connection: "local"
    ansible_python_interpreter: "/usr/bin/env python"

    state: 'present'

    lbvip:
      name: testvip
      address: 203.0.113.1
      server:
        - name: 'server-1'
          address: '192.0.2.1'
          description: 'Ansible Test Server 1'
          disabled: 'true'
        - name: 'server-2'
          address: '192.0.2.2'
          description: 'Ansible Test Server 2'
          disabled: 'true'
        - name: 'server-3'
          address: '192.0.2.3'
          description: 'Ansible Test Server 3'
          disabled: 'true'
        - name: 'server-4'
          address: '192.0.2.4'
          description: 'Ansible Test Server 4'
          disabled: 'true'
        - name: 'server-5'
          address: '192.0.2.5'
          description: 'Ansible Test Server 5'
          disabled: 'true'
        - name: 'server-6'
          address: '192.0.2.6'
          description: 'Ansible Test Server 6'
          disabled: 'true'
        - name: 'server-7'
          address: '192.0.2.7'
          description: 'Ansible Test Server 7'
          disabled: 'true'
        - name: 'server-8'
          address: '192.0.2.8'
          description: 'Ansible Test Server 8'
          disabled: 'true'
      vserver:
        - port: '80'
          description: 'Generic service running on 80'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '443'
          description: 'Generic service running on 443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8080'
          description: 'Generic service running on 8080'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8081'
          description: 'Generic service running on 8081'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8443'
          description: 'Generic service running on 8443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'

  tasks:

    - name: Build lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
          ipaddress: "{{ item.address }}"
          comment: "{{ item.description | default('Ansible Created') }}"
          disabled: "{{ item.disabled | default('false') }}"
        with_items: "{{ lbvip.server }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
          destport: "{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
        no_log: false
      - local_action:
          module: copy
          content: "{{ lookup('template', 'templates/netscaler_servicegroup.j2') }}"
          dest: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
          mode: "0644"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - include_tasks: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: file
          state: absent
          path: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
          servicetype: "{{ item.type }}"
          ipv46: "{{ lbvip.address }}"
          port: "{{ item.port }}"
          lbmethod: "{{ item.method | default('LEASTCONNECTION') }}"
          persistencetype: "{{ item.persistence | default('SOURCEIP') }}"
          servicegroupbindings:
            - servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      when: state == "present"

    - name: Destroy lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_servicegroup
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
        with_items: "{{ lbvip.server }}"
      when: state == "absent"

The following is the Jinja2 template that creates the netscaler_servicegroup task. An important thing to note is my use of the RAW block. When the task is created and stored in /tmp it does not contain any account credentials, instead I preserve the variable in the raw to prevent leaking sensitive information to anyone who may be snooping around on the server while the playbook is running.

templates/netscaler_servicegroup.j2: ASCII text, with CRLF line terminators

---
- local_action:
    module: netscaler_servicegroup
    nsip: {% raw %}"{{ inventory_hostname }}"
{% endraw %}
    nitro_user: {% raw %}"{{ nitro_user }}"
{% endraw %}
    nitro_pass: {% raw %}"{{ nitro_pass }}"
{% endraw %}
    nitro_protocol: "https"
    validate_certs: no

    state: "{{ state | default('present') }}"

    servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
    comment: "{{ item.description | default('Ansible Created') }}"
    servicetype: "{{ item.type }}"
    servicemembers:
{% for i in lbvip.server %}
      - servername: "{{ i.name }}"
        port: "{{ item.port }}"
{% endfor %}
    monitorbindings:
      - monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
04. December 2018 · Comments Off on Using Ansible to perform a Netscaler backup · Categories: Ansible, Load Balancing, NetScaler · Tags: , , , ,

The following Ansible playbook is a rewrite of a script from a long time ago to perform backups of a Netscaler. As far as I know, there are no native Ansible or Vendor modules to perform a system backup. Within the playbook I am simply performing a raw call using the URI module against the Nitro API and fetching the backup file.

The following Vendor links contain good/related reference information:

netscaler_systembackup.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text

#!/usr/bin/ansible-playbook -f 10
## Ansible playbook to perform a full backup of Netscaler systems
## 2018 (v.01) - Playbook from www.davideaves.com
---
- name: Netscaler full backup
  hosts: netscalers
  connection: local
  gather_facts: False

  vars:

    ansible_connection: "local"
    ansible_python_interpreter: "/usr/bin/env python"

    backup_location: "/srv/nsbackup"

    ns_sys_backup: "/var/ns_sys_backup"

  tasks:

    - name: Check backup file status
      local_action:
        module: stat
        path: "{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz"
      register: stat_result

    - name: Check backup directory location
      local_action:
        module: file
        path: "{{ backup_location }}"
        state: directory
        mode: 0775
        recurse: yes
      run_once: True
      when: stat_result.stat.exists == False

    - name: Full backup of Netscaler configuration.
      block:

      - name: Create Netscaler system backup
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systembackup?action=create"
          method: POST
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"
          body_format: json
          body: 
            systembackup:
              filename: "{{ inventory_hostname | hash('md5') }}"
              level: full
              comment: Ansible Generated Backup

      - name: Fetch Netscaler system backup
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systemfile?args=filename:{{ inventory_hostname | hash('md5') }}.tgz,filelocation:{{ ns_sys_backup | replace('/','%2F') }}"
          method: GET
          status_code: 200
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"
        register: result

      - name: Save Netscaler system backup to backup directory
        local_action: "shell echo '{{ result.json.systemfile[0].filecontent }}' | base64 -d > '{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz'"

      - name: Chmod saved backup file permissions
        local_action:
          module: file
          path: "{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz"
          mode: 0644

      always:

      - name: Delete system backup from Netscaler
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systembackup/{{ inventory_hostname | hash('md5') }}.tgz"
          method: DELETE
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"

      - name: Locate backup files older than 90 days
        local_action:
          module: find
          paths: "{{ backup_location }}"
          age: "1d"
        run_once: true
        register: files_matched

      - name: Purge old backup files
        local_action:
          module: file
          path: "{{ item.path }}"
          state: absent
        run_once: true
        with_items: "{{ files_matched.files }}"

      when: stat_result.stat.exists == False
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
09. November 2017 · Comments Off on YAML Templating Engine · Categories: Cisco, Firewall, Linux, Linux Scripts, Load Balancing, Networking · Tags: , , , ,

About a month ago I left my previous position and took a new engineering position. In my new role I am more focused on devops/orchestration for a new a network team. One major gap that I commonly see missing from most infrastructure/ops teams is the sharing of basic templates; ideally using a templating engine to maintain consistency. A couple years ago I stumbled upon a templating-engine by mxtommy on github that would read XML data and spit out a config. Aside from being a little rough, you would have to create your templates in a rough XML format plus it no longer seems to work with the latest version of PHP.

I decided that it was time to rewrite it; this time making it so it would read a YAML data input structure. YAML is a human-readable data serialization standard and is commonly used in orchestration tools like Ansible. Just like before this web script searches the current directory for template files (*.yaml). It then parses the questions to ask and creates the appropriate html input fields. The user answers the questions and submits them back to the script. The script then replaces the user responses with the placeholders to create a constant and repeatable configuration… For example, last year I posted a config snippet for configuring a Cisco UCS-E module on a 4000 series router. A template created for that config would make for constant and rapid deployments of UCS-E blades by infrastructure staff. Additionally you could use it to create a bootstrap config to quickly bring a host online so Ansible can take over management of it.

This web script requires the YAML-1.1 parser and emitter for PHP…

  • Debian: apt-get install php-yaml
  • RHEL: yum install php-pecl-yaml

template.php: Template Engine / PHP script.

< ?php
/*
   # yaml-templating-engine
 
   This web script searches the current directory for .yaml files. It will then ask
   questions based on questions defined in the template file, and then insert the info
   at the appropriate places in the template.
 
   Concept based on: https://github.com/mxtommy/templating-engine
 
   Rewritten to consume YAML and works with PHP7+.
 
   *** This is script not intended for use on a public server. ***
 
   2017 (v1.0) - Script from www.davideaves.com
*/
 
if (basename($_SERVER["PHP_SELF"]) == basename(__FILE__) && isset($_GET["source"])) {
  header("Content-type: text/plain");
  exit(file_get_contents(basename($_SERVER["PHP_SELF"])));
}
 
?>
< !DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>< ?php echo gethostname(); ?>: YAML Template Generator</title>
    <link href="template.css" rel="stylesheet"/>
  </head>
<body>
 
<table align="center"><tr>
	<td><h1>YAML Templating Engine</h1></td>
</tr></table>
 
<div id="TEMPLATE">
<fieldset>< ?php
 
// SELECT THE TEMPLATE
if (!isset($_REQUEST['template'])) {
  echo "<legend>Template Select\n";
  echo "<p>Please select one of the following templates:</p>\n<ul class=\"twocols\">\n";
  $templates = glob("*.yaml");
  foreach ($templates as $file) {
	print ("\t<li><a href='".$_SERVER['PHP_SELF']."?template=".$file."'>$file</a></li>\n");
  }
  echo "</ul>";
 
}
 
// ASK TEMPLATE QUESTIONS
elseif (!isset($_REQUEST['submitted'])) {
  echo "<legend>Template Input</legend>";
  echo "<h2>" . $_REQUEST['template'] . "</h2>\n";
 
  $yaml_template = yaml_parse_file($_REQUEST['template']);
  //print_r($yaml_template);
 
  echo "<form action=\"" . $_SERVER['PHP_SELF'] . "\" method=\"post\">\n";
  echo "<input type=\"hidden\" name=\"template\" value=\"" . $_REQUEST['template'] . "\"/>\n";	
 
  foreach($yaml_template['questions'] as $question) {
 
    // type is text
    if ($question['type'] == 'text') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><input type=\"text\" name=\"" . $question['name'] . "\" value=\"" . $question['default'] . "\"/></div>\n";
    }
 
    // type is select
    elseif($question['type'] == 'select') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><select name=\"" . $question['name'] . "\">\n";
      foreach ($question['option'] as $option) {
        echo "\t<option value=\"" . $option['name'] . "\"";
          if ($question['default'] == $option['name']) { echo " selected"; }
          echo ">" . $option['description'] . "</option>\n";
      }
      echo "</select></div>\n";
    }
 
    // type is checkbox
    elseif($question['type'] == 'checkbox') {
      echo "<div class=\"line\"><label>" . $question['description'] . ":</label><input type=\"checkbox\" name=\"" . $question['name'] . "\"";
      if ( isset($question['default']) ) { echo " checked"; }
      echo "/></div>\n";
    }
  }
  echo "<br /><input type=\"submit\" name=\"submitted\"/>\n</form>\n";
} 
 
// DISPLAY THE TEMPLATE
else {
  echo "<legend>Template Output</legend>";
  echo "<h2>" . $_REQUEST['template'] . "</h2>\n";
 
  $vars[] = "";
  $yaml_template = yaml_parse_file($_POST['template']);
 
  foreach ($yaml_template['questions'] as $i => $row) {
 
      $TMP = $yaml_template['questions'][$i];
 
      // Input type is text.
      if($TMP['type'] == "text") {
	$vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $_POST[$TMP['name']] . "</mark>" );
      }
 
      // Input type is select.
      elseif ($TMP['type'] == "select" ) {
        foreach ($TMP['option'] as $i => $row) {
          if($row['name'] == $_POST[$TMP['name']]) {
            foreach ($TMP['option'][$i]['option'] as $i => $row) {
              $vars = $vars + array( "{{ ".$row['name']." }}" => "<mark>" . $row['text'] . "</mark>" );
      } } } }
 
      // Input type is a checkbox.
      elseif ($TMP['type'] == "checkbox" ) {
        if(isset($_POST[$TMP['name']])) {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $TMP['checked'] . "</mark>" );
        } elseif (isset($TMP['unchecked'])) {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "<mark>" . $TMP['unchecked'] . "</mark>" );
        } else {
          $vars = $vars + array( "{{ ".$TMP['name']." }}" => "" );
      } }
  }
 
  array_shift($vars);
  echo "<span class=\"config\">";
  echo strtr($yaml_template['config'], $vars);
  echo "</span>";
 
}
 
?></fieldset>
</div>
 
</body>
</html>

template.css: Example CSS file for the template generator.

/* TEMPLATE CSS */
#TEMPLATE {width:800px; margin:20px auto; background: #EEE; border: solid 1px #999; position: float; vertical-align: middle; border-radius: 5px;}
#TEMPLATE fieldset {border-radius: 5px; border-width:2px; border-style:dotted; margin:5px;}
#TEMPLATE h2 {margin:0; font-family:Arial, Sans-Serif; background:#5f9be3; color:#fff; white-space: nowrap; font-weight:bold; padding:0px; text-align: center;}
#TEMPLATE span.config {background-color:black; color:white; font-family:monospace; white-space:pre; display:block;}
#TEMPLATE mark {background-color:black; color:yellow; font-style:italic;}
#TEMPLATE form {margin-top: 15px; margin-left: 15px; margin-right: 15px;}
#TEMPLATE form label {display:inline-block; width:48%;}
#TEMPLATE form a {color: blue; text-decoration: none; margin: 0px;}
#TEMPLATE form input[type="text"] {width:50%;}
#TEMPLATE form input[type="submit"] {width:100%;}
#TEMPLATE form select {width:50.6%;}
#TEMPLATE form .line {clear:both;}
 
/* twocols layout */
.twocols {
  -webkit-column-count: 2; /* Chrome, Safari, Opera */
  -moz-column-count: 2; /* Firefox */
  column-count: 2;
}

Overall the templates are very easy to read and create making it so even non-technical support staff can contribute to the template repository this web script reads from.

example.yaml: Example yaml template file.

---
questions:

  - name: "hostname"
    description: "Hostname"
    type: "text"
    default: ROUTER

  - name: "mgmt_net"
    description: "Management Network"
    type: "select"
    default: dev
    option:
    - name: corp
      description: "Corporate"
      option:
      - name: VLAN
        text: 1000
      - name: VLAN-NAME
        text: PROD_VLAN
      - name: GATEWAY
        text: 192.168.50.1
    - name: dev
      description: "Development"
      option:
      - name: VLAN
        text: 2000
      - name: VLAN-NAME
        text: DEV_VLAN
      - name: GATEWAY
        text: 192.168.100.1

  - name: "service_encrypt"
    description: "Password Encryption"
    type: "checkbox"
    default: checked
    checked: service password-encryption
    unchecked: no service password-encryption

config: |
  {{ service_encrypt }}
  !
  h0stname {{ hostname }}
  !
  vlan {{ VLAN }}
   name {{ VLAN-NAME }}
  !
  ip default gateway {{ GATEWAY }}