Select pool member based on HTTP query string parameter

Problem this snippet solves:

The iRule allows clients to select a pool member based on a parameter set in the HTTP query string. The manual selection can be specified on any URI by appending member=1 to the query string. On responses, a session cookie is set to ensure the client requests are manually persisted to the same server as long as the browser is kept open. To have LTM clear the cookie, the member number can be set in the URI to 0.

For versions lower than 10.0, you could use a shell script run nightly to create a datagroup containing all of the pool members, regardless of state. This could be done on any LTM version. Then replace [active_members -list ..] with [lindex [lsort $::my_pool_members_class] $member_num] for 9.x or [lindex [lsort [class -get my_pool_members_class]] $member_num] for 10.x.

Code :

#
# Select pool member based on HTTP query string parameter
# v0.1 - 2010-03-15 - Aaron Hooley - hooleylists at gmail dot com
# v0.2 - 2010-12-29 - Aaron Hooley - hooleylists at gmail dot com - used 'members' command added in 10.0 instead of active_members
# 
# - Allow a client to select the pool member based on a parameter set in the query string
# - Uses a digit to select the 0th, 1st, 2nd, etc pool member from the active members of a pool
#
# See below for an alternative method for statically mapping 1 to the first pool membrer regardless of state.
# - Sets a session cookie so that the client will be persisted to the same pool member for the duration 
#   that the browser is kept open.
# - If the parameter is set to 0, then remove the cookie and load balance the request
#
# Requires 10.0+ to use members -list commnad
#   http://devcentral.f5.com/s/wiki/default.aspx/iRules/members
#
# For versions lower than 10.0 you could use a shell script run nightly to create a datagroup containing 
#   all of the pool members.  This could be done on any LTM version.
# Then replace [members -list] with [lindex [lsort $::my_pool_members_class] $member_num] for 9.x
#   or [lindex [lsort [class -get my_pool_members_class]] $member_num] for 10.x
#

when HTTP_REQUEST {

   # Log debug to /var/log/ltm? 1=yes, 0=no.
   set member_debug 1

   # Name of the URI parameter name used to manually select a specific pool member
   # Clients can append the member parameter to a query string in the format of:
   #   www.example.com/index.html?member=2
   # where member is the parameter name
   set member_param "member"

   # Track whether a member has been found in the query string or cookie
   set member_num ""

   # Debug logging
   if {$member_debug}{
      log local0. "[IP::client_addr]:[TCP::client_port]: Debug enabled on [HTTP::method] request for [HTTP::host][HTTP::uri]"
      log local0. "[IP::client_addr]:[TCP::client_port]: Members for pool [LB::server pool] (sorted):\
         [lsort [members -list [LB::server pool]]]"
   }

   # Check if query string contains "member=" before evaluating the value
   # Could replace this with a check of the URI that is only used when
   #   manually selecting a specific pool member.
   # Also check for a previously set cookie indicating a manually selected pool member.
   if {[HTTP::query] contains $member_param or [HTTP::cookie $member_param] ne ""}{

      # Have the query string parameter take precedence over the cookie.
      # So set the member_num based on the cookie and then overwrite it if the param value is set.
      set member_num [HTTP::cookie $member_param]
      if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Query contained the member parameter or member cookie was present.\
         Parsed member cookie value: $member_num"}

      # Parse the value of the parameter to get the pool member number being selected
      # Use a workaround to handle a bug with URI::query, described in:
      # CR137465: http://devcentral.f5.com/s/Default.aspx?tabid=53&forumid=5&tpage=1&view=topic&postid=1168257#1145270
      set query_member_num [URI::query "?&[HTTP::query]" "&${member_param}"]
      if {$query_member_num ne ""}{
         set member_num $query_member_num
         if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Using member number from URI: \$member_num: $member_num"}
      }

      # Check member number value
      switch -glob $member_num {
         "0" {
            # Exact match for 0 (client wants to clear cookie)
            # Delete the cookie in the response

            # Save the cookie value so we can delete it in the response
            set cookie_val [HTTP::cookie $member_param]

            if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Member 0 specified, will remove cookie in response."}
            return
         }
         "[0-9]*" {
            # The parameter had a value starting with a digit,
            # Use lindex to get the nth -1 pool member "IP port" (lindex is 0 based)
            # Use scan to parse the IP and port to separate variables (the pool command doesn't seem to handle them together)
            # Use catch to handle any errors trying to parse the pool member
            if {[catch {scan [lindex [lsort [members -list [LB::server pool]]] [expr {$member_num - 1}]] {%[^ ] %d} ip port} result]}{

               if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Error parsing pool member from $member_num"}

            } elseif {$result == 2}{

               if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Parsed IP port: $ip $port"}

               # Use catch to handle any errors trying to select the pool member
               if {not [catch {pool [LB::server pool] member $ip $port}]}{

                  # Selecting pool member succeeded so exit this event of this iRule
                  if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Successfully selected: $ip $port"}
                  return
               }
            }
         }
      }
      # If we're still in the iRule there was no match or there were errors trying to select the pool member
      # so load balance the request
      pool [LB::server pool]
      unset member_num
      if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: No/invalid member_num parsed, load balancing request"}
   }
}
when HTTP_RESPONSE {

   # Check if $member_num is set and has a value
   if {[info exists member_num] and $member_num ne ""}{

      switch $member_num {
         0 {
            if {$cookie_val ne ""}{
               # Expire the cookie by inserting the name/value with an expire time in the past
               if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Expiring cookie $member_param=$cookie_val"}
               HTTP::cookie insert name $member_param value $cookie_val path "/"
               HTTP::cookie expires $member_param 0 absolute
            }
         }
         default {
            if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Inserting cookie $member_param=$member_num"}
            HTTP::cookie insert name $member_param value $member_num path "/"
         }
      }
   }
}
# Debug events which can be removed once testing is complete
when LB_SELECTED {
   if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Selected IP:port: [LB::server]"}
}
when SERVER_CONNECTED {
   if {$member_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Server IP:port: [IP::server_addr]:[TCP::server_port]"}
}
Published Mar 18, 2015
Version 1.0