BIG-IP Config Cleaner

Problem this snippet solves:

This python script uses ssh/tmsh to access a BIG-IP and iterates through virtual servers looking for unused virtuals so that the virtual and associated configuration objects can be removed/cleaned from a BIG-IP system.

How to use this snippet:

Install onto a system that has python+paramiko

then execute and provide arguments.

./ssh_bigip_cleaner.py usage: ssh_bigip_cleaner.py [-h] (--scan | --remove | --scanandremove) --bigip BIGIP --user USER [--file FILE] [--vipNoDns] [--vipNoDnsVsEnabled] [--vipNoDnsVsDisabled] [--vipNoDnsVsAvailable] [--vipNoDnsVsOffline] [--vs0TotalConns] [--vs0CurConns] [--vsDisabled] [--vsOffline]

Code :

#!/usr/bin/python

## SSH/TMSH BIG-IP Cleaner
## Author: Chad Jenison (c.jenison@f5.com)
## Script that connects to BIG-IP over network (via SSH) and then uses TMSH commands to enumerate virtual servers and then identifies candidate virtual servers for deletion
## Version: 1.1 - Fixed operation on 10.2.x BIG-IPs (due to lack of support for "list ltm virtual one-line" and changes in "show ltm virtual field-fmt" between 10.x and 11.x+

import argparse
import sys
import socket
import getpass
import paramiko
import time


# Taken from http://code.activestate.com/recipes/577058/
def query_yes_no(question, default="no"):
    valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
    if default == None:
        prompt = " [y/n] "
    elif default == "yes":
        prompt = " [Y/n] "
    elif default == "no":
        prompt = " [y/N] "
    else:
        raise ValueError("invalid default answer: '%s'" % default)
    while 1:
        sys.stdout.write(question + prompt)
        choice = raw_input().lower() 
        if default is not None and choice == '':
            return valid[default]
        elif choice in valid.keys():
            return valid[choice]
        else:
            sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")

def removeVirtual(virtualName, vip, service, currentConns, totalConns, availabilityState, enabledState, reverseDns):
    #Gather Up Pool and iRules used by the virtual server being removed
    stdin, stdout, stderr = sshSession.exec_command('tmsh list ltm virtual %s' % (virtualName))
    inRules = False
    rules = []
    for line in stdout.read().splitlines():
        if line.lstrip().startswith('pool '):
            pool = line.lstrip().split(' ')[1].rstrip()
        elif inRules:
            if line.lstrip().rstrip() == '}':
                inRules = False
            else:
                if not line.lstrip().rstrip().startswith('_sys'):
                    rules.append(line.lstrip().rstrip())
        elif line.lstrip().startswith('rules {'):
            inRules = True 
    if args.remove:
       print ('--Status Information from CSV File - NOT CURRENT--')
    else:
       print ('--Status Information - CURRENT--')
    print ('Virtual Name: %s' % (virtualName))
    print ('VIP: %s' % (vip))
    print ('Service: %s' % (service))
    print ('Current Conns: %s' % (currentConns))
    print ('Total Conns: %s' % (totalConns))
    print ('Availability State: %s' % (availabilityState))
    print ('Enabled State: %s' % (enabledState))
    print ('Reverse DNS: %s' % (reverseDns))
    print ('Pool: %s' % (pool))
    if rules:
        for rule in rules:
             print ('iRule: %s' % (rule)) 
    queryString = ('Remove Virtual Server: %s?' % (virtualName))
    if query_yes_no(queryString, default="no"):
        stdin, stdout, stderr = sshSession.exec_command('tmsh delete ltm virtual %s' % (virtualName))
        #Check for Errors in deleting configuration objects
        for line in stderr.read().splitlines():
            if ": " in line:
                print ('Virtual Server Delete encountered error: %s' % (line))
        queryString = ('Remove Pool: %s?' % (pool))
        if query_yes_no(queryString, default="no"):
            stdin, stdout, stderr = sshSession.exec_command('tmsh delete ltm pool %s' % (pool))
            for line in stderr.read().splitlines():
                if ": " in line:
                    print ('Pool Delete encountered error: %s' % (line))
        for rule in rules:
            queryString = ('Remove Rule: %s?' % (rule))
            if query_yes_no(queryString, default="no"):
                stdin, stdout, stderr = sshSession.exec_command('tmsh delete ltm rule %s' % (rule))
                for line in stderr.read().splitlines():
                    if ": " in line:
                        print ('Rule %s Delete encountered error: %s' % (rule, line))

parser = argparse.ArgumentParser(description='A tool to identify and remove stale virtual servers from F5 BIG-IP Systems (should work with any BIG-IP that runs 10.0+ and has TMSH)', epilog='Use this tool with caution')
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument('--scan', action='store_true', help='scan for unused virtual servers and output to file')
mode.add_argument('--remove', action='store_true', help='remove unused virtual servers based on input file')
mode.add_argument('--scanandremove', action='store_true', help='scan for unused virtual servers and immediately remove')
parser.add_argument('--bigip', help='IP or hostname of BIG-IP Management or Self IP', required=True)
parser.add_argument('--user', help='username to use for authentication', required=True)
parser.add_argument('--file', help='filename in cwd CSV formatted; file is output filename if scanning; file is input filename if removing', default='ssh_bigip_cleaner.csv')
virtualselector = parser.add_argument_group(title='Criteria for Selecting Stale Virtual Servers')
virtualselector.add_argument('--vipNoDns', action='store_true', help='select virtual server for removal solely based on VIP Reverse DNS Unknown')
virtualselector.add_argument('--vipNoDnsVsEnabled', action='store_true', help='select virtual server for removal based on VIP Reverse DNS Unknown AND Virtual Enabled')
virtualselector.add_argument('--vipNoDnsVsDisabled', action='store_true', help='select virtual server for removal based on VIP Reverse DNS Unknown AND Virtual Disabled')
virtualselector.add_argument('--vipNoDnsVsAvailable', action='store_true', help='select virtual server for removal based on VIP Reverse DNS Unknown AND Virtual has Available Status')
virtualselector.add_argument('--vipNoDnsVsOffline', action='store_true', help='select virtual server for removal based on VIP Reverse DNS Unknown AND Virtual has Offline Status')
virtualselector.add_argument('--vs0TotalConns', action='store_true', help='select virtual server for removal based on Virtual 0 Total Conns')
virtualselector.add_argument('--vs0CurConns', action='store_true', help='select virtual server for removal based on Virtual 0 Current Conns')
virtualselector.add_argument('--vsDisabled', action='store_true', help='select virtual server for removal based on Virtual Server Disabled')
virtualselector.add_argument('--vsOffline', action='store_true', help='select virtual server for removal based on Virtual Server Offline')

args = parser.parse_args()

passwd = getpass.getpass("Password for " + args.user + ":")

sshSession=paramiko.SSHClient()
sshSession.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sshSession.connect(args.bigip, username=args.user, password=passwd, look_for_keys=False, allow_agent=False)

if args.scan or args.scanandremove:
    stdin, stdout, stderr = sshSession.exec_command("tmsh list ltm virtual")
    allvirtuals = []
    for line in stdout.read().splitlines():
        if line.startswith('ltm virtual'):
            virtual = []
            virtual.append(line.split(' ')[2])
            allvirtuals.append(virtual)

    for virtual in allvirtuals:
        stdin, stdout, stderr = sshSession.exec_command("tmsh list ltm virtual %s" % (virtual[0]))
        for line in stdout.read().splitlines():
            if line.lstrip().startswith('destination'):
                virtual.append(line.lstrip().split(' ')[1]) 
                virtual.append(line.lstrip().split(' ')[1].split(':')[0]) 
                virtual.append(line.lstrip().split(' ')[1].split(':')[1].rstrip()) 
        stdin, stdout, stderr = sshSession.exec_command("tmsh show ltm virtual %s field-fmt" % (virtual[0]))
        for line in stdout.read().splitlines():
            if line.lstrip().startswith('clientside.cur-conns'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
            elif line.lstrip().startswith('clientside.tot-conns'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
            #11.x+ Format
            elif line.lstrip().startswith('status.availability-state'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
            #10.x format
            elif line.lstrip().startswith('virtual-server.status.availability-state'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
            #11.x+ Format
            elif line.lstrip().startswith('status.enabled-state'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
            #10.x Format
            elif line.lstrip().startswith('virtual-server.status.enabled-state'):
        virtual.append(line.lstrip().split(' ')[1].rstrip())
        try:
            name, alias, addresslist = socket.gethostbyaddr(virtual[2])
            virtual.append(name)
        except socket.error:
            virtual.append('unknown')

    if args.scan:
        fileOut = open('%s' % (args.file), 'w')
    for virtual in allvirtuals:
        if (args.vipNoDns and virtual[8] == 'unknown') or (args.vipNoDnsVsEnabled and virtual[8] == 'unknown' and virtual[7] == 'enabled') or (args.vipNoDnsVsDisabled and virtual[8] == 'unknown' and virtual[7] == 'disabled') or (args.vipNoDnsVsAvailable and virtual[8] == 'unknown' and virtual[6] == 'available') or (args.vipNoDnsVsOffline and virtual[8] == 'unknown' and virtual[6] == 'offline') or (args.vs0TotalConns and virtual[5] == '0') or (args.vs0CurConns and virtual[4] == '0') or (args.vsDisabled and virtual[7] == 'disabled') or (args.vsOffline and virtual[6] == 'offline'):
            if args.scan:
                fileOut.write('VirtualName,%s,VIP,%s,Service,%s,CurrentConns,%s,TotalConns,%s,AvailabilityState,%s,EnabledState,%s,ReverseDNS,%s\n' % (virtual[0], virtual[2], virtual[3], virtual[4], virtual[5], virtual[6], virtual[7], virtual[8]))
            else:
removeVirtual(virtual[0], virtual[2], virtual[3], virtual[4], virtual[5], virtual[6], virtual[7], virtual[8])
    if args.scan:
        fileOut.close()

if args.remove:
   fileIn = open('%s' % (args.file), 'r')
   for line in fileIn:
       removeVirtual(line.split(',')[1], line.split(',')[3], line.split(',')[5], line.split(',')[7], line.split(',')[9], line.split(',')[11], line.split(',')[13], line.split(',')[15].rstrip())

##SUBLIST FIELD NUMBERS BELOW
#            print ('Virtual Name: %s' % (virtual[0]))
#            print ('VIP: %s' % (virtual[2]))
#            print ('Service: %s' % (virtual[3]))
#            print ('Current Conns: %s' % (virtual[4]))
#            print ('Total Conns: %s' % (virtual[5]))
#            print ('Availability State: %s' % (virtual[6]))
#            print ('Enabled State: %s' % (virtual[7]))
#     print ('Reverse DNS: %s' % (virtual[8]))

Tested this on version:

11.5
Published Jan 31, 2018
Version 1.0