Export Virtual Server Configuration in CSV - tmsh cli script
Problem this snippet solves: This is a simple cli script used to collect all the virtuals name, its VIP details, Pool names, members, all Profiles, Irules, persistence associated to each, in all partitions. A sample output would be like below, One can customize the code to extract other fields available too. The same logic can be allowed to pull information's from profiles stats, certificates etc. Update: 5th Oct 2020 Added Pool members capture in the code. After the Pool-Name, Pool-Members column will be found. If a pool does not have members - field not present: "members" will shown in the respective Pool-Members column. If a pool itself is not bound to the VS, then Pool-Name, Pool-Members will have none in the respective columns. Update: 21st Jan 2021 Added logic to look for multiple partitions & collect configs Update: 12th Feb 2021 Added logic to add persistence to sheet. Update: 26th May 2021 Added logic to add state & status to sheet. Update: 24th Oct 2023 Added logic to add hostname, Pool Status,Total-Connections & Current-Connections. Note: The codeshare has multiple version, use the latest version alone. The reason to keep the other versions is for end users to understand & compare, thus helping them to modify to their own requirements. Hope it helps. How to use this snippet: Login to the LTM, create your script by running the below commands and paste the code provided in snippet tmsh create cli script virtual-details So when you list it, it should look something like below, [admin@labltm:Active:Standalone] ~ # tmsh list cli script virtual-details cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all"context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],[tmsh::get_field_value $obj "pool"],$profilelist,[tmsh::get_field_value $obj "rules"]" } } total-signing-status not-all-signed } [admin@labltm:Active:Standalone] ~ # And you can run the script like below, tmsh run cli script virtual-details > /var/tmp/virtual-details.csv And get the output from the saved file, cat /var/tmp/virtual-details.csv Old Codes: cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],[tmsh::get_field_value $obj "pool"],$profilelist,[tmsh::get_field_value $obj "rules"]" } } total-signing-status not-all-signed } ###=================================================== ###2.0 ###UPDATED CODE BELOW ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } else { puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } total-signing-status not-all-signed } ###=================================================== ### Version 3.0 ### UPDATED CODE BELOW FOR MULTIPLE PARTITION ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } } total-signing-status not-all-signed } ###=================================================== ### Version 4.0 ### UPDATED CODE BELOW FOR CAPTURING PERSISTENCE ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules,Persist" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } } } } total-signing-status not-all-signed } ###=================================================== ### 5.0 ### UPDATED CODE BELOW ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules,Persist,Status,State" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { foreach { status } [tmsh::get_status ltm virtual [tmsh::get_name $obj]] { set vipstatus [tmsh::get_field_value $status "status.availability-state"] set vipstate [tmsh::get_field_value $status "status.enabled-state"] } set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } } } } total-signing-status not-all-signed } Latest Code: cli script virtual-details { proc script::run {} { set hostconf [tmsh::get_config /sys global-settings hostname] set hostname [tmsh::get_field_value [lindex $hostconf 0] hostname] puts "Hostname,Partition,Virtual Server,Destination,Pool-Name,Pool-Status,Pool-Members,Profiles,Rules,Persist,Status,State,Total-Conn,Current-Conn" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { foreach { status } [tmsh::get_status ltm virtual [tmsh::get_name $obj]] { set vipstatus [tmsh::get_field_value $status "status.availability-state"] set vipstate [tmsh::get_field_value $status "status.enabled-state"] set total_conn [tmsh::get_field_value $status "clientside.tot-conns"] set curr_conn [tmsh::get_field_value $status "clientside.cur-conns"] } set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ foreach { p_status } [tmsh::get_status ltm pool $poolname] { set pool_status [tmsh::get_field_value $p_status "status.availability-state"] } set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_status,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_status,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } } } else { puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } } } } } Tested this on version: 13.07.7KViews9likes25CommentsTMSH Cli Script - Virtual Server, State, Status, Pool Name Report in CSV
Problem this snippet solves: The below is a simple code to get a report on Virtuals present in the BigIP, with its name, state, status and the pool name associated to it. Should be helpful in building reports for management or auditing. The output can be seen in console (if needed) or can be written to a file with csv format, so when opened in excel it would look like below snapshot. The output file will be save in /var/tmp/virtual_stats-output.csv How to use this snippet: In the console, tmsh create cli script virtual_stats and paste the below code and save. Code : create script virtual_stats { proc script::run {} { exec echo Virtual,State,Status,Pool, > /var/tmp/virtual_stats-output.csv foreach { obj } [tmsh::get_status ltm virtual] { set l1 [tmsh::get_field_value $obj "name"] set l2 [tmsh::get_field_value $obj "status.enabled-state"] set l3 [tmsh::get_field_value $obj "status.availability-state"] set l4 [tmsh::get_config ltm virtual $l1 pool ] set poolname [lindex [split [lindex [split $l4 "\n"] 1] " "] 5] ###the below line can be commented to stop displaying output in console puts "$l1,$l2,$l3,$poolname" exec echo "$l1,$l2,$l3,$poolname" >> /var/tmp/virtual_stats-output.csv } } } Tested this on version: 13.01.2KViews0likes2CommentsRadware config translation
Problem this snippet solves: This is a simple Python script that translates Radware text configuration file and outputs as config snippet and certificate files. $ ./rad2f5 Usage: rad2f5 <filename> [partition] Example: ./rad2f5 radwareconfig.txt MyPartition Using partition /MyPartition/ IP routes:46 Non-floating IP interfaces:26 Floating IP interfaces:19 Monitors:349 Virtual Servers:430 Pools:152 Certificates: 31 SSL profiles: 17 WARNING! Found L7 Regular Expression at "appdirector l7 farm-selection method-table setCreate l7Rule-Redirect" Layer 7 rules:12 Layer 7 policies:4 !! Copy all certificate files to BIG-IP /var/tmp and use load_certs.sh to load !! # Configuration #--------------------------------------# ltm policy /MyPartition/l7rule-Redirect { controls { forwarding } requires { http } rules { Redirect { actions { 0 { forward select pool SORRYPOOL } } ordinal 1 } } net route /MyPartition/10.20.30.40_32 { network 10.20.30.40/32 gateway 10.1.1.1 } How to use this snippet: This includes translation of routes and non-floating self-IPs, monitors, pools, virtual servers, certificates, SSL profiles and some layer 7 rules. To install, either download the file attached here, extract and run it or use pip: pip install rad2f5 To load the configuration to the f5 device, output to a file eg ./rad2f5 filename>newcfg.txt , remove the statistics header from the file with a text editor, upload the file into /var/tmp on the BIG-IP and test loading with tmsh load sys config merge file /var/tmp/<filename> verify . Fix any issues and load with tmsh load sys config merge file /var/tmp/<filename> The script will also output the certificates and keys which should be uploaded to the BIG-IP /var/tmp directory and run file check_certs.sh and load_certs.sh to check and load the certs and keys. Works with Python v2 and v3. If you want something to be added then message me with details of the text and i will try to add it. Feel free to add or change anything Code : #!/usr/bin/env python # v1.1 14/3/2018 Deal with lack of text in monitors ( line 157 ) # v2 3/1/2019 Updated after changes suggested by Avinash Piare # v3 6/11/2019 Updated to deal with \\r\n at the end of the line # v3.1 6/11/2019 Minor changes to handle VS names with spaces # 3.4 18/4/2020 Fixed minor issues # Peter White 6/11/2019 # usage ./rad2f5 [partition] import sys import re import textwrap import ipaddress print ("Running with Python v" + sys.version) # Set debug to true to output the workings of the script ie the arrays etc debug = False def parse_options(options): # Function to split options and return a dict containing option and value # Example: -pl 27 -v 123 -pa 172.28.1.234 # Return: { 'pl':'27', 'v':'123', 'pa':'172.28.1.234' } output = dict() # Deal with non-quotes eg -b 12345 for r in re.finditer(r'-(\w{1,3}) (\S+)',options): output[r.group(1)] = r.group(2) # Deal with quotes eg -u "User Name" for v in re.finditer(r'-(\w{1,3}) (".+")',options): output[v.group(1)] = v.group(2) return output def splitvars(vars): # Split a string on | and then =, return a dict of ABC=XYZ # eg HDR=User-Agent|TKN=Googlebot-Video/1.0|TMATCH=EQ| # Return is { 'HDR':'User-Agent','TKN': 'Googlebot-Video/1.0', 'TMATCH': 'EQ' } vlist = vars.split("|") o = {} for v in vlist: if v == '': continue w = v.split("=") if len(w) > 1: o[w[0]] = w[1] else: o[w[0]] = '' return o def array_add(a,d): # Function to safely add to an array #global a if not len(a): a = [ d ] else: a.append( d ) return a def setup_pools(name): global pools if not name in pools: pools[name] = { 'destinations': [], 'member-disabled': [], 'member-ids': [], 'priority-group': [] } else: if not 'destinations' in pools[name]: pools[name]['destinations'] = [] if not 'member-disabled' in pools[name]: pools[name]['member-disabled'] = [] if not 'member-ids' in pools[name]: pools[name]['member-ids'] = [] if not 'priority-group' in pools[name]: pools[name]['priority-group'] = [] def getVsFromPool(poolname): # This retrieves the names of virtual servers that use a specific pool global farm2vs if poolname in farm2vs: return farm2vs[poolname] else: return False def is_ipv6(ip): if ':' in ip: return True else: return False # Check there is an argument given if not len(sys.argv) > 1: exit("Usage: rad2f5 [partition]") if len(sys.argv) > 2: # Second command-line variable is the partition partition = "/" + sys.argv[2] + "/" print ("Using partition " + partition) else: partition = "/Common/" # Check input file fh = open(sys.argv[1],"r") if not fh: exit("Cannot open file " + argv[1]) rawfile = fh.read() # v2 # Remove any instances of slash at the end of line # eg health-monitoring check create\ # HC_DNS -id 5 -m DNS -p 53 -a \ #HOST=ns.domain.com|ADDR=1.1.1.1| -d 2.2.2.2 #file = re.sub('^security certificate table\\\\\n','',rawfile) file = re.sub('\\\\\n','',rawfile) file = re.sub('\\\\\r\n','',file) ################ LACP trunks ########################## # # # ############################################################# #net linkaggr trunks set T-1 -lap G-13,G-14 -lam Manual -lat Fast -law 3 -laa 32767 trunks = {} for r in re.finditer(r'net linkaggr trunks set (\S+) (.+)',file): name = r.group(1) options = parse_options(r.group(2)) if 'lap' in options: # Manage interfaces ints = options['lap'].split(',') trunks[name] = { 'interfaces': ints } print ("LACP trunks:" + str(len(trunks))) if debug: print('DEBUG LACP trunks: ' + str(trunks)) ################ IP routes ########################## # # # ############################################################# routes = {} for r in re.finditer(r'net route table create (\S+) (\S+) (\S+) (\S+)',file): if r.group(1) == '0.0.0.0': name = 'default' else: name = r.group(1) + "_" + r.group(2) routes[name] = { 'network': r.group(1), 'mask': r.group(2), 'gateway': r.group(3), 'interface': r.group(4)} print ("IP routes:" + str(len(routes))) if debug: print('DEBUG routes: ' + str(routes)) ################ Non-floating self-IPs ############### # # # # ############################################################# selfIpNonFloating = {} for s in re.finditer(r'net ip-interface create (\S+) (\S+) (.+)',file): output = { 'interface': s.group(2) } opts = parse_options(s.group(3)) if 'pl' in opts: output['mask'] = opts['pl'] if 'v' in opts: output['vlan'] = opts['v'] if 'pa' in opts: output['peerAddress'] = opts['pa'] selfIpNonFloating[s.group(1)] = output print ("Non-floating IP interfaces:" + str(len(selfIpNonFloating))) if debug: print("DEBUG non-floating IPs: " + str(selfIpNonFloating)) ################ Floating self-IPs ################### # # # ############################################################# selfIpFloating = {} for s in re.finditer(r'redundancy vrrp virtual-routers create (\S+) (\S+) (\S+) (.+)',file): output = { 'version': s.group(1),'interface': s.group(3), 'ipAddresses': []} opts = parse_options(s.group(4)) #if 'as' in opts: # Enabled - assume all are enabled if 'pip' in opts: output['peerIpAddress'] = opts['pip'] selfIpFloating[s.group(2)] = output # Retrieve IP addresses for Floating self-IPs # for s in re.finditer(r'redundancy vrrp associated-ip create (\S+) (\S+) (\S+) (\S+)',file): selfIpFloating[s.group(2)]['ipAddresses'] = array_add(selfIpFloating[s.group(2)]['ipAddresses'],s.group(4)) print ("Floating IP interfaces:" + str(len(selfIpFloating))) if debug: print("DEBUG Floating IPs: " + str(selfIpFloating)) ################ Monitors ########################### # # -m XYZ is the monitor type may or may not be present ( not present for icmp type ) # -id is the monitor ID, -p is the port, -d is the destination # ############################################################# monitors = {} for m in re.finditer(r'health-monitoring check create (\S+) (.+)',file): output = { 'name': m.group(1) } opts = parse_options(m.group(2)) if 'id' in opts: id = opts['id'] else: print ("No ID for monitor " + m.group(1)) continue if 'm' in opts: output['type'] = opts['m'] else: output['type'] = 'icmp' if 'a' in opts: output['text'] = opts['a'] else: # Added in v2 output['text'] = '' if 'p' in opts: output['port'] = opts['p'] else: output['port'] = '' monitors[id] = output print ("Monitors:" + str(len(monitors))) if debug: print("DEBUG Monitors:" + str(monitors)) ################ Virtual Servers ##################### # # 0 = name, 1= IP, 2=protocol, 3=port, 4=source 5=options # -ta = type, -ht = http policy, -fn = pool name, -sl = ssl profile name, -ipt = translation? # -po = policy name # ############################################################# farm2vs = {} virtuals = {} snatPools = {} for v in re.finditer(r'appdirector l4-policy table create (\S+) (\S+) (\S+) (\S+) (\S+) (.*)',file): # Puke if VS has quotes if v.group(1).startswith('"'): name = v.group(1).strip('"').replace(' ','_') + v.group(2).strip('"').replace(' ','_') address,protocol,port = v.group(3),v.group(4),v.group(5) source = v.group(6).split(' ')[0] options = ' '.join(v.group(6).split(' ')[1:]) else: name,address,protocol,port,source,options = v.group(1),v.group(2),v.group(3),v.group(4),v.group(5),v.group(6) opts = parse_options(options) # Get rid of ICMP virtual servers if protocol == 'ICMP': continue print ("Name " + name + " has quotes, address: " + address + " protocol: " + protocol) if port == 'Any': port = '0' output = {'source': source, 'destination': address + ":" + port, 'protocol': protocol, 'port': port } if 'fn' in opts: output['pool'] = opts['fn'] if not opts['fn'] in farm2vs: farm2vs[opts['fn']] = [] farm2vs[opts['fn']].append(name) output['port'] = port if 'po' in opts: output['policy'] = opts['po'] if 'ipt' in opts: output['snat'] = 'snatpool_' + opts['ipt'] if not 'snatpool_' + opts['ipt'] in snatPools: #Create snatpool snatPools['snatpool_' + opts['ipt']] = { 'members': [opts['ipt']] } # Set the correct profiles profiles = [] # Layer 4 if protocol == "TCP" or protocol == "UDP": profiles.append(protocol.lower()) else: profiles.append('ipother') # http if port == '80' or port == '8080': profiles.append('http') profiles.append('oneconnect') # ftp if port == '21': profiles.append('ftp') # RADIUS if port == '1812' or port == '1813': profiles.append('radius') # SSL if 'sl' in opts: profiles.append(opts['sl']) profiles.append('http') profiles.append('oneconnect') output['profiles'] = profiles # virtuals[name] = output print ("Virtual Servers:" + str(len(virtuals))) if debug: print("DEBUG Virtual Servers:" + str(virtuals)) #print("DEBUG Farm to VS Mapping:" + str(farm2vs)) ################ Pools ############################### # # Pools config options are distributed across multiple tables # This sets the global config such as load balancing algorithm # -dm This sets the distribution method. cyclic = Round Robin, Fewest Number of Users = least conn, Weighted Cyclic = ratio, Response Time = fastest # -as this is the admin state # -cm is the checking method ie monitor # -at is the activation time ############################################################# pools = {} for p in re.finditer(r'appdirector farm table setCreate (\S+) (.*)',file): name = p.group(1) setup_pools(name) opts = parse_options(p.group(2)) output = {} # Admin state output['poolDisabled'] = False if 'as' in opts: if opts['as'] == 'Disabled': output['poolDisabled'] = True # Deal with distribution methods method = 'round-robin' if 'dm' in opts: if opts['dm'] == '"Fewest Number of Users"': method = 'least-conn' elif opts['dm'] == 'Hashing': method = 'hash' else: method = 'round-robin' output['lbMethod'] = method if 'at' in opts: output['slowRamp'] = opts['at'] pools[name] = output # This sets the pool members # 0=name, 1=node, 2=node address, 3=node port, 4=? -id =id -sd ? -as admin state eg Disable, -om operation mode eg Backup ( fallback server ), -rt backup server address 0.0.0.0 for p in re.finditer(r'appdirector farm server table create (\S+) (\S+) (\S+) (\S+) (\S+) (.*)',file): name,node,node_address,node_port,node_hostname = p.group(1),p.group(2),p.group(3),p.group(4),p.group(5) opts = parse_options(p.group(6)) setup_pools(name) if node_port == 'None': node_port = '0' pools[name]['destinations'] = array_add(pools[name]['destinations'],node_address + ":" + node_port) # Manage monitor if node_port == '80' or node_port == '8080': monitor = 'http' elif node_port == '443': monitor = 'https' elif node_port == '53': monitor = 'dns' elif node_port == '21': monitor = 'ftp' elif node_port == '0': monitor = 'gateway-icmp' else: monitor = 'tcp' pools[name]['monitor'] = monitor # Retrieve pool member ID if 'id' in opts: pools[name]['member-ids'] = array_add(pools[name]['member-ids'],opts['id']) else: print ("ID not found for pool " + name) # Check if member is disabled if 'as' in opts and opts['as'] == 'Disabled': # This pool member is disabled pools[name]['member-disabled'] = array_add(pools[name]['member-disabled'],True) else: pools[name]['member-disabled'] = array_add(pools[name]['member-disabled'],False) # Check if member is backup ie Priority Groups if 'om' in opts and opts['om'] == 'Backup': pools[name]['priority-group'] = array_add(pools[name]['priority-group'],20) else: pools[name]['priority-group'] = array_add(pools[name]['priority-group'],0) print ("Pools:" + str(len(pools)) ) if debug: print("DEBUG pools: " + str(pools)) ################ SSL certificates ################### # # #Name: certificate_name \ #Type: certificate \ #-----BEGIN CERTIFICATE----- \ #MIIEcyFCA7agAwIAAgISESFWs9QGF # ############################################################# certs = {} for r in re.finditer(r'Name: (\S+) Type: (\S+) (-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----)',file): name,type,text = r.group(1),r.group(2),r.group(3) certs[name] = { 'type': type,'text': text.replace(' \\\r\n','') } # Print out to file or something print ("SSL Certificates: " + str(len(certs))) if debug: print("DEBUG certs: " + str(certs)) ################ SSL Keys ################### # # #Name: New_Root_Cert Type: key Passphrase: -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: [key] [key] -----END RSA PRIVATE KEY----- \ # ############################################################# keys = {} # Keys with passphrase for r in re.finditer(r'Name: (\S+) Type: key Passphrase: (\S+) (-----BEGIN RSA PRIVATE KEY-----.+?-----END RSA PRIVATE KEY-----)',file): name,passphrase,text = r.group(1),r.group(2),r.group(3) keys[name] = { 'passphrase': passphrase,'text': text.replace(' \\\r\n','') } # Keys without passphrase for r in re.finditer(r'Name: (\S+) Type: key (-----BEGIN RSA PRIVATE KEY-----.+?-----END RSA PRIVATE KEY-----)',file): name,text = r.group(1),r.group(2) keys[name] = { 'text': text.replace(' \\\r\n','') } print ("SSL Keys: " + str(len(keys))) if debug: print("DEBUG SSL keys: " + str(keys)) ################ SSL profiles ######################## # # -c is cert, -u is ciphers, -t is chain cert, -fv - versions ############################################################## sslProfiles = {} for s in re.finditer(r'^appdirector l4-policy ssl-policy create (\S+) (.+)',file.replace('\\\r\n',''),re.MULTILINE): name = s.group(1) output = {} opts = parse_options(s.group(2)) if 'c' in opts: # Certificate output['certificate'] = opts['c'] if 't' in opts: # Chain cert output['chaincert'] = opts['t'] if 'fv' in opts: # TLS Version output['version'] = opts['fv'] if 'u' in opts: # User-defined Ciphers output['cipher'] = opts['u'] if 'lp' in opts: # ? output['lp'] = opts['lp'] if 'pa' in opts: # ? output['pa'] = opts['pa'] if 'cb' in opts: # ? output['cb'] = opts['cb'] if 'i' in opts: # Backend SSL Cipher -> Values: Low, Medium, High, User Defined output['i'] = opts['i'] if 'bs' in opts: # Backend SSL in use ie serverSSL output['serverssl'] = opts['bs'] sslProfiles[name] = output print ("SSL profiles: " + str(len(sslProfiles))) if debug: print("DEBUG SSL Profiles: " + str(sslProfiles)) ################ SNAT pools ########################## # # Note that this only works for addresses in the same /24 subnet ############################################################# for s in re.finditer(r'appdirector nat client address-range create (\S+) (\S+)',file): name = s.group(1) if sys.version.startswith('2'): start = ipaddress.ip_address(unicode(name,'utf_8')) end = ipaddress.ip_address(unicode(s.group(2),'utf_8')) else: start = ipaddress.ip_address(name) end = ipaddress.ip_address(s.group(2)) current = start ipAddresses = [] while current <= end: ipAddresses.append(str(current)) current += 1 snatPools['snatpool_' + name] = { 'members': ipAddresses } print ("SNAT Pools:" + str(len(snatPools))) if debug: print("DEBUG SNAT Pools: " + str(snatPools)) #################################################################### ################ Layer 7 functions ########################## # # #################################################################### l7rules = { } for r in re.finditer(r'appdirector l7 farm-selection method-table setCreate (\S+) (.+)',file): name = r.group(1) l7rules[name] = { 'match': [], 'action': [] } opts = parse_options(r.group(2)) if 'cm' in opts and opts['cm'] == '"Header Field"' and 'ma' in opts: # This is a rule to insert a header params = splitvars(opts['ma']) if 'HDR' in params and 'TKN' in params: if params['TKN'] == "$Client_IP": params['TKN'] = '[IP::client_addr]' l7rules[name]['action'].append("http-header\ninsert\nname " + params['HDR'] + "\nvalue " + params['TKN']) if 'cm' in opts and opts['cm'] == 'URL' and 'ma' in opts: # This does a match on Host and URL params = splitvars(opts['ma']) if 'HN' in params: l7rules[name]['match'].append("http-host\nhost\nvalues { " + params['HN'] + " }") if 'P' in params: l7rules[name]['match'].append("http-uri\npath\nvalues { " + params['P'] + " }") if 'cm' in opts and opts['cm'] == '"Regular Expression"' and 'ma' in opts: params = splitvars(opts['ma']) if 'EXP' in params and params['EXP'] == '.': # This is a regex which matches everything print ("REGEX: " + name) else: print ("WARNING! Found L7 Regular Expression at \"appdirector l7 farm-selection method-table setCreate " + name + "\". Manually set the match in output config.") # Note that there can be multiple entries ie multiple rules per policy l7policies = {} for p in re.finditer(r'appdirector l7 farm-selection policy-table setCreate (\S+) (\d+) (.+)',file): name = p.group(1) precedence = p.group(2) opts = parse_options(p.group(3)) if 'fn' in opts: farm = opts['fn'] else: farm = '' if 'm1' in opts: rule = opts['m1'] if 'pa' in opts: # Retain HTTP Persistency (PRSST)-If the argument is ON (or undefined), AppDirector maintains HTTP 1.1 # HTTP Redirect To (RDR)-Performs HTTP redirection to the specified name or IP address. # HTTPS Redirect To (RDRS)-AppDirector redirects the HTTP request to the specified name or IP address and modifies the request to a HTTPS request. # Redirection Code (RDRC)-Defines the code to be used for redirection. # RDRC=PRMN stand for Permanent I assume , as in HTTP 301 # SIP Redirect To (RDRSIP)-Performs SIP redirection to the specified name or IP address. params = splitvars(opts['pa']) if 'RDR' in params: url = 'http://' + params['RDR'] elif 'RDRS' in params: url = 'https://' + params['RDRS'] else: url = '' if not name in l7policies: l7policies[name] = [] if rule in l7rules: if farm != '': l7rules[rule]['action'].append("forward\nselect\npool " + farm) elif url != '': l7rules[rule]['action'].append("http-redirect\nhost " + url) l7policies[name].append({ 'precedence': precedence, 'farm': farm, 'rule': rule }) print ("Layer 7 rules:" + str(len(l7rules))) print ("Layer 7 policies:" + str(len(l7policies))) if debug: print("DEBUG L7 rules: " + str(l7rules)) print("DEBUG L7 policies: " + str(l7policies)) # # We have retrieved the required configuration from the input file # Now start outputting the config ################# output SSL certificates import script ###################### # # ######################################################################## if len(certs): print ("-- Creating SSL certs and keys --") with open("load_certs.sh",'w') as loadScript: loadScript.write("#!/bin/bash\n# Script to load SSL certs and keys from /var/tmp\n") ##### Manage Certificates ######## for cert in certs: # Write certs to load_certs.sh if certs[cert]['type'] == 'certificate' or certs[cert]['type'] == 'interm': loadScript.write("tmsh install sys crypto cert " + partition + cert + ".crt from-local-file /var/tmp/" + cert + ".crt\n") # Create the certificate files with open(cert + ".crt","w") as certFile: for m in re.finditer(r'(-----BEGIN CERTIFICATE-----)\s?(.+)\s?(-----END CERTIFICATE-----)',certs[cert]['text']): certFile.write(m.group(1) + "\n") certFile.write(textwrap.fill(m.group(2),64) + "\n") certFile.write(m.group(3) + "\n") print ("Created SSL certificate file " + cert + ".crt") ##### Manage Keys ################# for key in keys: # Write keys to load_certs.sh loadScript.write("tmsh install sys crypto key " + partition + key + ".key") if 'passphrase' in keys[key]: loadScript.write(" passphrase " + keys[key]['passphrase']) loadScript.write(" from-local-file /var/tmp/" + key + ".key\n") # Create the key files with open(key + ".key","w") as keyFile: for n in re.finditer(r'(-----BEGIN RSA PRIVATE KEY-----)(.+)(-----END RSA PRIVATE KEY-----)',keys[key]['text']): keyFile.write(n.group(1) + "\n") if 'Proc-Type:' in n.group(2) and 'DEK-Info:' in n.group(2): # File is encrypted, separate the first lines for o in re.finditer(r'(Proc-Type: \S+) (DEK-Info: \S+) (.+)',n.group(2)): keyFile.write(o.group(1) + "\n") keyFile.write(o.group(2) + "\n\n") keyFile.write(textwrap.fill(o.group(3),64) + "\n") else: # File is not encrypted, output as it is keyFile.write(textwrap.fill(n.group(2),64) + "\n") keyFile.write(n.group(3) + "\n") print ("Created SSL key file " + key + ".key") print ("-- Finished creating SSL certs and keys --") print ("!! Copy all .crt and .key SSL files to BIG-IP /var/tmp and use load_certs.sh to load !!" ) ####################################################################### ################# output SSL certificates checking script ###################### # # ######################################################################## with open("check_certs.sh",'w') as checkScript: checkScript.write("#!/bin/bash\n# Script to check SSL certs and keys\n") checkScript.write("\n# Check SSL Certs\n") for cert in certs: if certs[cert]['type'] == 'certificate' or certs[cert]['type'] == 'interm': checkScript.write("openssl x509 -in " + cert + ".crt -noout || echo \"Error with file " + cert + ".crt\"\n") checkScript.write("\n# Check SSL Keys\n") for key in keys: checkScript.write("openssl rsa -in " + key + ".key -check || echo \"Error with file " + key + ".key\"\n") print ("!! Run the check_certs.sh script to check the certificates are valid !!" ) ####################################################################### # # # # # # # # print ("\n\n\n\n\n\n# Configuration \n#--------------------------------------#") ################# output policy config ##################### # # ############################################################# #print ("L7 rules: " + str(l7rules)) for i in l7policies.keys(): output = "ltm policy " + partition + i + " {\n\tcontrols { forwarding }\n\trequires { http }\n\t" output += "rules {\n\t" ordinal = 1 for j in l7policies[i]: output += "\t" + j['rule'] + " { \n" if len(l7rules[j['rule']]['match']): # Deal with conditions output += "\t\t\tconditions { \n" l = 0 for k in l7rules[j['rule']]['match']: output += "\t\t\t\t" + str(l) + " { \n\t\t\t\t\t" + k.replace('\n','\n\t\t\t\t\t') l += 1 output += "\n\t\t\t\t }\n" output += "\n\t\t\t }\n" if len(l7rules[j['rule']]['action']): # Deal with actions output += "\t\t\tactions { \n" m = 0 for n in l7rules[j['rule']]['action']: output += "\t\t\t\t" + str(m) + " { \n\t\t\t\t\t" + n.replace('\n','\n\t\t\t\t\t') m += 1 output += "\n\t\t\t\t }\n" output += "\n\t\t\t }\n" output += "\t\tordinal " + str(ordinal) output += "\n\t\t}\n\t" ordinal += 1 output += "\n\t}\n}\n" print (output) ################# output LACP trunk config ############## # # ############################################################# for trunk in trunks.keys(): output = "net trunk " + partition + trunk + " {\n" if 'interfaces' in trunks[trunk]: output += "\tinterfaces {\n" for int in trunks[trunk]['interfaces']: output += "\t\t" + int + "\n" output += "\t}\n" output += "}\n" print (output) ################# output vlan config ############## # # ############################################################# for ip in selfIpNonFloating.keys(): output = "" if 'vlan' in selfIpNonFloating[ip]: vlanName = "VLAN-" + selfIpNonFloating[ip]['vlan'] output += "net vlan " + partition + vlanName + " {\n" if 'interface' in selfIpNonFloating[ip]: output += "\tinterfaces {\n" output += "\t\t" + selfIpNonFloating[ip]['interface'] + " {\n" output += "\t\t\ttagged\n\t\t}\n" output += "\t}\n" output += "\ttag " + selfIpNonFloating[ip]['vlan'] + "\n}\n" print(output) ################# output non-floating self-ip config ####### # # ############################################################# for ip in selfIpNonFloating.keys(): output = "" if 'mask' in selfIpNonFloating[ip]: mask = "/" + selfIpNonFloating[ip]['mask'] else: if is_ipv6(ip): mask = "/64" else: mask = "/32" if 'vlan' in selfIpNonFloating[ip]: vlanName = "VLAN-" + selfIpNonFloating[ip]['vlan'] else: continue output += "net self " + partition + "selfip_" + ip + " {\n\taddress " + ip + mask + "\n\t" output += "allow-service none\n\t" output += "traffic-group traffic-group-local-only\n" output += "\tvlan " + vlanName + "\n" output += "}\n" print (output) ################# output network route config ############## # # ############################################################# for route in routes.keys(): network,mask,gateway = routes[route]['network'],routes[route]['mask'],routes[route]['gateway'] output = "net route " + partition + route + " {\n\tnetwork " + network + "/" + mask + "\n\t" output += "gateway " + gateway + "\n}" print (output) ################# output SSL profiles config ########################## # # ######################################################################## for s in sslProfiles: #print (str(sslProfiles[s])) output = "ltm profile client-ssl " + partition + s + " {\n" output += "\tcert-key-chain { " + s + " { cert " output += sslProfiles[s]['certificate'] + ".crt " output += " key " output += sslProfiles[s]['certificate'] + ".key " if 'chaincert' in sslProfiles[s]: output += "chain " + sslProfiles[s]['chaincert'] output += " }\n\tdefaults-from /Common/clientssl\n}\n" print (output) if 'serverssl' in sslProfiles[s]: print("WARNING: VSs using Client SSL profile " + s + " should have Server SSL profile assigned") ################# output monitor config #################### # # ############################################################# for m in monitors.keys(): if monitors[m]['type'] == '"TCP Port"': output = "ltm monitor tcp " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/tcp\n}\n" if monitors[m]['type'] == '"UDP Port"': output = "ltm monitor udp " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/udp\n}\n" if monitors[m]['type'] == 'HTTP': ''' https://webhelp.radware.com/AppDirector/v214/214Traffic%20Management%20and%20Application%20Acceleration.03.001.htm https://webhelp.radware.com/AppDirector/v214/HM_Checks_Table.htm Arguments: Host name - HOST=10.10.10.53 path - PATH=/hc.htm HTTP Header HTTP method - MTD=G (G/H/P) send HTTP standard request or proxy request - PRX=N use of no-cache - NOCACHE=N text for search within a HTTP header and body, and an indication whether the text appears Username and Password for basic authentication NTLM authentication option - AUTH=B Up to four valid HTTP return codes - C1=200 -a PATH=/hc.htm|C1=200|MEXIST=Y|MTD=G|PRX=N|NOCACHE=N|AUTH=B| ''' output = "ltm monitor http " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/http\n" vars = splitvars(monitors[m]['text']) output += "\tsend \"" if 'MTD' in vars: if vars['MTD'] == 'G': output += "GET " elif vars['MTD'] == 'P': output += "POST " elif vars['MTD'] == 'H': output += "HEAD " else: output += "GET " if 'PATH' in vars: output += vars['PATH'] + ' HTTP/1.0\\r\\n\\r\\n"\n' if 'C1' in vars: output += "\trecv \"^HTTP/1\.. " + vars['C1'] + "\"\n" output += "}\n" if monitors[m]['type'] == 'HTTPS': output = "ltm monitor https " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/https\n" vars = splitvars(monitors[m]['text']) output += "\tsend \"" if 'MTD' in vars: if vars['MTD'] == 'G': output += "GET " elif vars['MTD'] == 'P': output += "POST " elif vars['MTD'] == 'H': output += "HEAD " else: output += "GET " if 'PATH' in vars: output += vars['PATH'] + ' HTTP/1.0\\r\\n\\r\\n"\n' output += "}\n" if monitors[m]['type'] == 'LDAP': ''' The Health Monitoring module enhances the health checks for LDAP servers by allowing performing searches in the LDAP server. Before Health Monitoring performs the search, it issues a Bind request command to the LDAP server. After performing the search, it closes the connection with the Unbind command. A successful search receives an answer from the server that includes a "searchResultEntry" message. An unsuccessful search receives an answer that only includes only a "searchResultDone" message. Arguments: Username A user with privileges to search the LDAP server. Password The password of the user. Base Object The location in the directory from which the LDAP search begins. Attribute Name The attribute to look for. For example, CN - Common Name. Search Value The value to search for. Search scope baseObject, singleLevel, wholeSubtree Search Deref Aliases neverDerefAliases, dereflnSearching, derefFindingBaseObj, derefAlways USER=cn=healthcheck,dc=domain|PASS=1234|BASE=dc=domain|ATTR=cn|VAL=healthcheck|SCP=1|DEREF=3| ''' output = "ltm monitor ldap " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/ldap\n" vars = splitvars(monitors[m]['text']) if 'BASE' in vars: output += "\tbase \"" + vars['BASE'] + "\"\n" if 'USER' in vars: output += "\tusername \"" + vars['USER'] + "\"\n" if 'PASS' in vars: output += "\tpassword \"" + vars['PASS'] + "\"\n" output += "}\n" if monitors[m]['type'] == 'icmp': output = "ltm monitor gateway-icmp " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/gateway-icmp\n}\n" if monitors[m]['type'] == 'DNS': output = "ltm monitor dns " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/dns\n" vars = splitvars(monitors[m]['text']) if 'HOST' in vars: output += "\tqname \"" + vars['HOST'] + "\"\n" if 'ADDR' in vars: output += "\trecv \"" + vars['ADDR'] + "\"\n" output += "}\n" if monitors[m]['type'] == '"Radius Accounting"': output = "ltm monitor radius-accounting " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/radius-accounting\n" vars = splitvars(monitors[m]['text']) if 'USER' in vars: output += "\tusername \"" + vars['USER'] + "\"\n" #if 'PASS' in vars: if 'SECRET' in vars: output += "\tsecret \"" + vars['SECRET'] + "\"\n" output += "}\n" if monitors[m]['type'] == '"Radius Authentication"': output = "ltm monitor radius " + partition + monitors[m]['name'] + " {\n\tdefaults-from /Common/radius\n" vars = splitvars(monitors[m]['text']) if 'USER' in vars: output += "\tusername \"" + vars['USER'] + "\"\n" #if 'PASS' in vars: if 'SECRET' in vars: output += "\tsecret \"" + vars['SECRET'] + "\"\n" output += "}\n" print (output) ################# output SNAT pool config ###################### # # ############################################################# for s in snatPools.keys(): output = "ltm snatpool " + partition + s + " {\n\t" output += "members {\n" #run through pool range and add members as individual IP addresses below each other for member in snatPools[s]['members']: output += "\t " + member + "\n" output += "\t}" output += "\n}" print (output) ################# output pool config ###################### # # ############################################################# for p in pools.keys(): output = "ltm pool " + partition + p + " {\n\t" if 'monitor' in pools[p]: output += "monitor min 1 of { " + pools[p]['monitor'] + " }\n\t" if 'lbMethod' in pools[p]: output += "load-balancing-mode " + pools[p]['lbMethod'] + "\n\t" output += "members {\n" if 'destinations' in pools[p] and len(pools[p]['destinations']): for i,v in enumerate(pools[p]['destinations']): d = re.sub(':\d+$','',v) output += "\t\t" + v + " {\n\t\t\taddress " + d + "\n" if pools[p]['member-disabled'][i]: output += "\t\t\tstate down\n" output += "\t\t\tpriority-group " + str(pools[p]['priority-group'][i]) + "\n" output += "\t\t}\n" output += "\t}\n}" print (output) ################# output virtual server config ###################### # # ###################################################################### for v in virtuals: output = "ltm virtual " + partition + v + " {\n" output += "\tdestination " + virtuals[v]['destination'] + "\n" output += "\tip-protocol " + virtuals[v]['protocol'].lower() + "\n" if 'pool' in virtuals[v] and virtuals[v]['pool']: output += "\tpool " + virtuals[v]['pool'] + "\n" if 'profiles' in virtuals[v] and len(virtuals[v]['profiles']): output += "\tprofiles {\n" for p in virtuals[v]['profiles']: output += "\t\t" + p + " { }\n" output += "\t}\n" if 'snat' in virtuals[v]: output += "\tsource-address-translation {\n\t" if virtuals[v]['snat'] == 'automap': output += "\ttype automap\n\t" else: output += "\ttype snat\n\t" output += "\tpool " + virtuals[v]['snat'] + "\n\t" output += "}\n" if 'policy' in virtuals[v]: output += "\tpolicies {\n\t" output += "\t" + virtuals[v]['policy'] + " { }\n\t}\n" output += "}" print (output) Tested this on version: 12.01.9KViews0likes30CommentsRewriting the URL for specific pool members
How to use this snippet: when RULE_INIT { set static::new_app_nodes "1.1.1.1 443 2.2.2.2 443 3.3.3.3 443" } when HTTP_REQUEST_SEND { if { $static::new_app_nodes contains "[LB::server addr] [LB::server port]" } then { log -noname local0.debug "Match! [members -list $static::new_app_nodes] contains [LB::server addr] [LB::server port]" clientside { HTTP::uri "/uri" } } else { log -noname local0.debug "Don't match! [members -list $static::new_app_nodes] contains [LB::server addr] [LB::server port]" } } Code : when RULE_INIT { set static::new_app_nodes "1.1.1.1 443 2.2.2.2 443 3.3.3.3 443" } when HTTP_REQUEST_SEND { if { $static::new_app_nodes contains "[LB::server addr] [LB::server port]" } then { log -noname local0.debug "Match! [members -list $static::new_app_nodes] contains [LB::server addr] [LB::server port]" clientside { HTTP::uri "/uri" } } else { log -noname local0.debug "Don't match! [members -list $static::new_app_nodes] contains [LB::server addr] [LB::server port]" } } Tested this on version: 11.4223Views0likes0CommentsConvert curl command to BIG-IP Monitor Send String
Problem this snippet solves: Convert curl commands into a HTTP or HTTPS monitor Send String. How to use this snippet: Save this code into a Python script and run as a replacement for curl. Related Article: How to create custom HTTP monitors with Postman, curl, and Python An example would be: % python curl_to_send_string.py -X POST -H "Host: api.example.com" -H "User-Agent: Custom BIG-IP Monitor" -H "Accept-Encoding: identity" -H "Connection: Close" -H "Content-Type: application/json" -d '{ "hello": "world" } ' "http://10.1.10.135:8080/post?show_env=1" SEND STRING: POST /post?show_env=1 HTTP/1.1\r\nHost: api.example.com\r\nUser-Agent: Custom BIG-IP Monitor\r\nAccept-Encoding: identity\r\nConnection: Close\r\nContent-Type: application/json\r\n\r\n{\n \"hello\":\n \"world\"\n}\n Code : Python 2.x import getopt import sys import urllib optlist, args = getopt.getopt(sys.argv[1:], 'X:H:d:') flat_optlist = dict(optlist) method = flat_optlist.get('-X','GET') (host,uri) = urllib.splithost(urllib.splittype(args[0])[1]) protocol = 'HTTP/1.1' headers = ["%s %s %s" %(method, uri, protocol)] headers.extend([h[1] for h in optlist if h[0] == '-H']) if not filter(lambda x: 'host:' in x.lower(),headers): headers.insert(1,'Host: %s' %(host)) send_string = "\\r\\n".join(headers) send_string += "\\r\\n\\r\\n" if '-d' in flat_optlist: send_string += flat_optlist['-d'].replace('\n','\\n') send_string = send_string.replace("\"", "\\\"") print "SEND STRING:" print send_string Python 3.x import getopt import sys from urllib.parse import splittype, urlparse optlist, args = getopt.getopt(sys.argv[1:], 'X:H:d:') flat_optlist = dict(optlist) method = flat_optlist.get('-X','GET') parts = urlparse(splittype(args[0])[1]) (host,uri) = (parts.hostname,parts.path) protocol = 'HTTP/1.1' headers = ["%s %s %s" %(method, uri, protocol)] headers.extend([h[1] for h in optlist if h[0] == '-H']) if not filter(lambda x: 'host:' in x.lower(),headers): headers.insert(1,'Host: %s' %(host)) send_string = "\\r\\n".join(headers) send_string += "\\r\\n\\r\\n" if '-d' in flat_optlist: send_string += flat_optlist['-d'].replace('\n','\\n') send_string = send_string.replace("\"", "\\\"") print("SEND STRING:") print(send_string) Tested this on version: 11.54.5KViews0likes17CommentsForm Based Authentication with external SOAP web services
Problem this snippet solves: 1- You need to authenticate users against an external authentication system relying on SOAP calls. 2- The session identifier must be provided by an external third party system. How to use this snippet: Installation Files You need to upload an html login page using ifiles. You need to upload the SOAP body of the external web service using ifiles. irule You need to install the irule on your Virtual Server you need to protect. Variables set static::holdtime 3600 # session timeout set static::login_url "/login" # login url set static::sideband_vs "VS_EXTERNAL_AUTH_PROVIDER" # Virtual Server that publish the web service Features Version 1.0 Form based login (provided by a custom ifile) Authentication against an external SOAP web service Manage Session timeout Backlog Improve logging Allow 2-factor authentication (Challenge) Encrypt Session cookie Provide internal mecanism to generate a session cookie accept Basic Authentication External links Github : https://github.com/e-XpertSolutions/f5 Code : when RULE_INIT { set static::holdtime 3600 set static::login_url "/login" set static::sideband_vs "VS_EXTERNAL_AUTH_PROVIDER" } when HTTP_REQUEST { if { [HTTP::cookie exists SessionCook] and [table lookup -subtable "active_sessions" [HTTP::cookie SessionCook]] != "" } { return } else { if { [HTTP::path] eq $static::login_url } { if { [HTTP::method] eq "POST" } { if {[HTTP::header "Content-Length"] ne "" && [HTTP::header "Content-Length"] <= 1048576}{ set content_length [HTTP::header "Content-Length"] } else { set content_length 1048576 } if { $content_length > 0} { HTTP::collect $content_length } } else { HTTP::respond 200 content [ifile get login.html] "Cache-Control" "no-cache, must-revalidate" "Content-Type" "text/html" } } else { HTTP::respond 302 noserver "Location" $static::login_url "Cache-Control" "no-cache, must-revalidate" Set-Cookie "SessionCook=$result;domain=[HTTP::host];path=/" } } } when HTTP_REQUEST_DATA { set payload [HTTP::payload] set username "" set password "" regexp {Login1\%3AtxtUserName\=(.*)\&Login1\%3AtxtPassword\=(.*)\&Login1\%3AbtnSubmit\=(.*)} $payload -> username password garbage if {[catch {connect -timeout 1000 -idle 30 -status conn_status $static::sideband_vs} conn_id] == 0 && $conn_id ne ""}{ log local0. "Connect returns: $conn_id and conn status: $conn_status" } else { log local0. "Connection could not be established to sideband_virtual_server" } set content [subst -nocommands -nobackslashes [ifile get soap_body]] set length [string length $content] set data "POST /apppath/webservicename.asmx HTTP/1.1\r\nHost: www.hostname.com\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: $length\r\nSOAPAction: http://schemas.microsoft.com/sqlserver/2004/SOAP\r\n\r\n$content" set send_bytes [send -timeout 1000 -status send_status $conn_id $data] set recv_data [recv -timeout 1000 $conn_id] # parse response to retrieve the authentication result, it gives 0 if authentication failed or a session_id if it succeed regexp { (.*) (.*)} $recv_data -> result garbage unset content unset length unset data unset recv_data close $conn_id # add a custom alert notification to the login page if { $result == 0 } { set alert " Invalid credentials. " HTTP::respond 200 content [subst -nocommands -nobackslashes [ifile get login.html]] "Cache-Control" "no-cache, must-revalidate" "Content-Type" "text/html" Set-Cookie "SessionCook=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;domain=[HTTP::host];path=/" } else { HTTP::respond 302 noserver "Location" "/" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "SessionCook=$result;domain=[HTTP::host];path=/" # save the cookie value in a cache for fast checking table add -subtable "active_sessions" $result $username indef $static::holdtime } } Tested this on version: 11.5437Views0likes1CommentGoogle Analytics script injection
Problem this snippet solves: Add google analytics script in the html content of the HTTP response. Works also for other Analytics providers like Piwik. How to use this snippet: Installation Files The code below has to be imported as an ifile. By default, you must name this ifile google.js but you can change it in the irule if required. Google Analytics code : <!-- Google Analytics --> <script> window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; ga('create', '$static::tracking_id', 'auto'); ga('send', 'pageview'); </script> <script async src='https://www.google-analytics.com/analytics.js'></script> <!-- End Google Analytics --> Piwik javascript code : <!-- Piwik --> <script type="text/javascript"> var _paq = _paq || []; _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); (function() { var u="//$static::piwik_url/"; _paq.push(['setTrackerUrl', u+'piwik.php']); _paq.push(['setSiteId', {$static::siteid}]); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); </script> <!-- End Piwik Code --> irule You need to install the irule on your Virtual Server. Variables set static::tracking_id "UA-XXXXX-Y" # replace the Google Tracking ID by your own set static::siteid "UA-XXXXX-Y" # replace the Piwik Site ID by your own set static::piwik_url "https://www.mypiwik.com/piwik/piwik" # replace the Piwik URL by your own Features Version 1.0 Insert Google Analytics JS code within html response support for Piwik JS insertion Manage Multiple TrackingID by hostname (see Multiple "hostname and TrackingID section") Backlog Add logging External links Github : https://github.com/e-XpertSolutions/f5 BONUS : Multiple hostname and TrackingID Prerequisite You need to add a string based Datagroup named HOST_TRACKING_MAPPING. ltm data-group internal HOST_TRACKING_MAPPING { records { blog.e-xpertsolutions.com { data UA-XXXXX-Z } www.e-xpertsolutions.com { data UA-XXXXX-Y } } type string } The google.js ifile need to be replaced by the following example : <!-- Google Analytics --> <script> window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; ga('create', '$tracking_id', 'auto'); ga('send', 'pageview'); </script> <script async src='https://www.google-analytics.com/analytics.js'></script> <!-- End Google Analytics --> Irule when RULE_INIT { set static::default_trackingid "UA-XXXXX-Y" } when HTTP_REQUEST { HTTP::header remove "Accept-Encoding" set host [HTTP::host] } when HTTP_RESPONSE { if { [HTTP::header Content-Type] contains "text/html" } { if { [HTTP::header exists "Content-Length"] } { set content_length [HTTP::header "Content-Length"] } else { set content_length 1000000 } if { $content_length > 0 } { HTTP::collect $content_length } } } when HTTP_RESPONSE_DATA { set search "</head>" set tracking_id [class match -value -- $host equals HOST_TRACKING_MAPPING] if { $tracking_id eq "" } { set tracking_id $static::default_trackingid } HTTP::payload replace 0 $content_length [string map [list $search "[subst -nocommands -nobackslashes [ifile get google.js]]</head>"] [HTTP::payload]] HTTP::release } Code : when RULE_INIT { set static::tracking_id "UA-XXXXX-Y" set static::siteid "XXXXX" set static::piwik_url "https://www.piwik.url/piwik/piwik" } when HTTP_REQUEST { HTTP::header remove "Accept-Encoding" } when HTTP_RESPONSE { if { [HTTP::header Content-Type] contains "text/html" } { if { [HTTP::header exists "Content-Length"] } { set content_length [HTTP::header "Content-Length"] } else { set content_length 1000000 } if { $content_length > 0 } { HTTP::collect $content_length } } } when HTTP_RESPONSE_DATA { set search "" HTTP::payload replace 0 $content_length [string map [list $search "[subst -nocommands -nobackslashes [ifile get google.js]]"] [HTTP::payload]] HTTP::release } Tested this on version: 11.51.6KViews0likes3CommentsF5 Automation - TCL & Bash
Problem this snippet solves: This is a really simple way to automate CLI command execution on multiple F5 devices using Bash & TCL scripting. How to use this snippet: On a linux machine that is utilized to connect to the F5 device: Create a directory mkdir F5_Check Within the "F5_Check" directory, create the following 3 files: F5_Host.txt (This file contains F5's IP address) F5_Bash_v1 (This is the bash script used to collect username/password for F5) F5_Out_v1.exp (This is the TCL script executes the relevant commands on F5) Explanation of the 3 files: File Content: F5_Out_v1.exp is provided as code share. This is the main TCL script that is utiliezd to execute CLI commands on multiple F5 devices. File Content: F5_Bash_v1 #!/bin/bash # Collect the username and password for F5 access echo -n "Enter the username " read -s -e user echo -ne '\n' echo -n "Enter the password " read -s -e password echo -ne '\n' # Feed the expect script a device list & the collected username & passwords for device in `cat ~/F5_Check/F5_Host.txt`; do ./F5_Out_v1.exp $device $password $user ; done File Contents: F5_Host.txt This contains the management IP of the F5 devices. Example: cat F5_Host.txt 10.12.12.200 10.12.12.201 10.12.12.202 10.12.12.203 Code : #!/usr/bin/expect -f # Set variables set hostname [lindex $argv 0] set password [lindex $argv 1] set username [lindex $argv 2] # Log results log_file -a ~/F5_Check/F5LOG.log # Announce which device we are working on and the time send_user "\n" send_user ">>>>> Working on $hostname @ [exec date] <<<<<\n" send_user "\n" # SSH access to device spawn ssh $username@$hostname expect { "no)? " { send "yes\n" expect "*assword: " sleep 1 send "$password\r" } "*assword: " { sleep 1 send "$password\r" } } expect "(tmos)#" send "sys\n" expect "(tmos.sys)#" send "show software\n" expect "#" send "exit\n" expect "#" send "quit\n" expect ":~\$" exit Tested this on version: 11.51.7KViews0likes2CommentsiRule to decode WebSocket negotiation and frames
Problem this snippet solves: WebSocket establishes a socket via HTTP upgrade and once socket is established subsequent messages are non-HTTP, but WebSocket frame. There might be a situation where you want to dump WebSocket negotiation and frame into log for troubleshooting purpose. This iRule dumps negotiation and WebSocket frame header fields, and payload (only text data). WebSocket frame format looks as below. RFC 6455 - 5.2. Base Framing Protocol 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ WebSocket frame is quite simple. The first 2 bytes are necessary and always present. Extended payload length exists only when Payload length is set to 126 (in this case Ext len is 16 bits) or 127 (in this case Ext len is 64 bits). Masking-key exists only if MASK bit is set to 1. FIN : If the frame is the last frame, set to 1. If payload is fragmented, the last frame should have FIN = 1. Other fragmented ones should have FIN = 0. RSV : If extension is not used, it is set to 0 opcode : This tells you if it is data frame (can be text or binary) or control frame. %x0:continuation frame %x1:text frame %x2:binary frame %x3-7:reserved for further %x8:connection close %x9:ping %xA:pong %xB-F:reserved for further MASK : When browser sends a data, this bit MUST be set to 1, which means data is masked using the Masking-key. When server sends a data, this bit MUST NOT set to 1 Payload len : If 0 - 125, this field represents the payload length If it is 126, then the Extended payload length (16bit) are used to tell the actual payload length (maximum data size is 65535 bytes) If it is 127, then the Extended payload length (64bit : MSB must be 0) are used to tell the actual payload length (maximum data size is 9223372036854775807 bytes) Extended payload length Only when Payload len is set to 126 Extended payload length_continued Only when Payload len is set to 127 Masking-key Used to mask data. Masking is to avoid proxy poisoning. Non-compliant HTTP proxy caches WebSocket data. If MASK bit is 1, this field is present. If MASK bit is 0, this field is not present. Client sets this key and it must be unpredictable. Payload Payload from client to server is masked using Masking-key. How to use this snippet: Here I am sending text data "DEAD BEEF" in WebSocket frame via WebSocket_cURL.py (https://github.com/jussmen/WebSocket_cURL). HTTP request and response (negotiation) look like below. $ python WebSocket_cURL.py 10.10.148.101 80 -s "DEAD BEEF" GET /ws HTTP/1.1 Host: 10.10.148.101 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: n5twxG/tNPf8h3po+pNrPA== User-Agent: IE HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: y9WDs+d4zDl+qvQ7H17KpnP0EhI= This is how iRule dumps the negotiation and subsequent WebSocket frames in /var/log/ltm <ws_request>: ============================================= <ws_request>: Client 10.10.1.2:47427 -> 10.10.148.101/ws (request) <ws_request>: Host: 10.10.148.101 <ws_request>: Connection: Upgrade <ws_request>: Upgrade: websocket <ws_request>: Sec-WebSocket-Version: 13 <ws_request>: Sec-WebSocket-Key: n5twxG/tNPf8h3po+pNrPA== <ws_request>: User-Agent: IE <ws_request>: ============================================= <ws_response>: ============================================= <ws_response>: Client 10.10.1.2:47427 -> 10.10.148.101/ws (response) <ws_response>: Upgrade: websocket <ws_response>: Connection: Upgrade <ws_response>: Sec-WebSocket-Accept: y9WDs+d4zDl+qvQ7H17KpnP0EhI= <ws_response>: ============================================= <ws_server_frame>: ============================================= <ws_server_frame>: FIN bit : 1 <ws_server_frame>: MASK bit : 1 <ws_server_frame>: MASK : 0 <ws_server_frame>: Type : Text - 1 <ws_server_frame>: ============================================= <ws_server_data>: ============================================= <ws_server_data>: The server says: 'Hello'. Connection was accepted. <ws_server_data>: ============================================= <ws_client_frame>: ============================================= <ws_client_frame>: FIN bit : 1 <ws_client_frame>: MASK bit : 1 <ws_client_frame>: MASK : 1178944834 <ws_client_frame>: Type : Text - 1 <ws_client_frame>: ============================================= <ws_client_data>: ============================================= <ws_client_data>: DEAD BEEF <ws_client_data>: ============================================= <ws_server_frame>: ============================================= <ws_server_frame>: FIN bit : 1 <ws_server_frame>: MASK bit : 1 <ws_server_frame>: MASK : 0 <ws_server_frame>: Type : Text - 1 <ws_server_frame>: ============================================= <ws_server_data>: ============================================= <ws_server_data>: The server says: DEAD BEEF back at you <ws_server_data>: ============================================= <ws_client_frame>: ============================================= <ws_client_frame>: FIN bit : 1 <ws_client_frame>: MASK bit : 1 <ws_client_frame>: MASK : 1178944834 <ws_client_frame>: Type : Connection close - 8 <ws_client_frame>: ============================================= <ws_server_frame>: ============================================= <ws_server_frame>: FIN bit : 1 <ws_server_frame>: MASK bit : 1 <ws_server_frame>: MASK : 0 <ws_server_frame>: Type : Connection close - 8 <ws_server_frame>: ============================================= Code : when WS_REQUEST { # Copied from : https://devcentral.f5.com/s/articles/log-http-headers set LogString "Client [IP::client_addr]:[TCP::client_port] -> [HTTP::host][HTTP::uri]" log local0. "=============================================" log local0. "$LogString (request)" foreach aHeader [HTTP::header names] { log local0. "$aHeader: [HTTP::header value $aHeader]" } log local0. "=============================================" } when WS_RESPONSE { # Copied from : https://devcentral.f5.com/s/articles/log-http-headers log local0. "=============================================" log local0. "$LogString (response)" foreach aHeader [HTTP::header names] { log local0. "$aHeader: [HTTP::header value $aHeader]" } log local0. "=============================================" } when WS_CLIENT_FRAME { log local0. "=============================================" log local0. "FIN bit : [WS::frame eom]" log local0. "MASK bit : [WS::frame orig_masked]" if { [WS::frame orig_masked] eq 0 } { log local0. "Not masked. Client frame MUST be masked." } if { [WS::frame orig_masked] eq 1 } { log local0. "MASK : [WS::frame mask]" } switch -glob [WS::frame type] { "0" { log local0. "Type : Continuatoin frame - 0" } "1" { log local0. "Type : Text - 1" WS::collect frame } "2" { log local0. "Type : Binary - 2" } "3" - "4" - "5" - "6" - "7" { log local0. "Type : Reserved type (3-7) - [WS::frame type]" } "8" { log local0. "Type : Connection close - 8" } "9" { log local0. "Type : ping - 9" } "10" { log local0. "Type : pong - 10" } "11" - "12" - "13" - "14" - "15" { log local0. "Type : Reserved type (11-15) - [WS::frame type]" } } log local0. "=============================================" } when WS_SERVER_FRAME { log local0. "=============================================" log local0. "FIN bit : [WS::frame eom]" log local0. "MASK bit : [WS::frame orig_masked]" if { [WS::frame orig_masked] eq 1 } { log local0. "MASK : [WS::frame mask]" } switch -glob [WS::frame type] { "0" { log local0. "Type : Continuatoin frame - 0" } "1" { log local0. "Type : Text - 1" WS::collect frame } "2" { log local0. "Type : Binary - 2" } "3" - "4" - "5" - "6" - "7" { log local0. "Type : Reserved type (3-7) - [WS::frame type]" } "8" { log local0. "Type : Connection close - 8" } "9" { log local0. "Type : ping - 9" } "10" { log local0. "Type : pong - 10" } "11" - "12" - "13" - "14" - "15" { log local0. "Type : Reserved type (11-15) - [WS::frame type]" } } log local0. "=============================================" } #when WS_CLIENT_FRAME_DONE { #log local0. "WS_CLIENT_FRAME_DONE" #} #when WS_SERVER_FRAME_DONE { #log local0. "WS_SERVER_FRAME_DONE" #} when WS_CLIENT_DATA { log local0. "=============================================" log local0. "[WS::payload]" log local0. "=============================================" WS::release } when WS_SERVER_DATA { log local0. "=============================================" log local0. "[WS::payload]" log local0. "=============================================" WS::release } Tested this on version: 12.0824Views0likes0Comments