These examples show how to use the F5 iControl interface with the Ruby SOAP4R library. Please note that this code requires at least SOAP4R version 1.5.8.

A blog post about this library can be found at F5 DevCentral.

Example: node-list

#!/usr/bin/env ruby
# vim:expandtab shiftwidth=2 softtabstop=2

require 'rubygems'
require 'getoptions'

$: << File.join(ENV['HOME'], '/ops/lib') << '/cc/ops/lib'
require 'f5'

PROGNAME = File.basename($0)

def usage
  Kernel.abort "Usage: #{PROGNAME} [--config_file <filename>] <hostname> ..."
end

opts = GetOptions.new(%w(config_file=s connect_timeout=i))

usage if ARGV.empty?

hostnames = ARGV

options = {
  :config_file     => opts.config_file,
  :connect_timeout => opts.connect_timeout,
}

hostnames.each do |hostname|
  lb = F5::LoadBalancer.new(hostname, options)
  begin
    puts lb.nodes
  rescue HTTPClient::ConnectTimeoutError => exc
    abort "#{PROGNAME}: connect to #{hostname}: #{exc.message}"
  end
end

exit 0

Output:

$ node-list 192.168.160.22
192.168.191.32
192.168.191.31
192.168.190.21
192.168.190.22
$ 

Example: members-per-pool

#!/usr/bin/env ruby
# vim:expandtab shiftwidth=2 softtabstop=2

PROGNAME = File.basename($0)

require 'rubygems'
require 'getoptions'
require 'yaml'

$: << File.join(ENV['HOME'], '/ops/lib') << '/cc/ops/lib'
require 'f5'

def usage
  Kernel.abort "Usage: #{PROGNAME} [--config_file <filename>] <hostname> [poolname ...]"
end

opts = GetOptions.new(%w(config_file=s connect_timeout=i))

usage if ARGV.empty?

hostname, *pools = ARGV

options = {
  :config_file     => opts.config_file,
  :connect_timeout => opts.connect_timeout,
}

lb = F5::LoadBalancer.new(hostname, options)

begin
  pools = pools.empty? ? lb.pools : pools.map {|pool| F5::LoadBalancer::Pool.new(lb, pool)}

  h = {}
  pools.each do |pool|
    members = pool.members
    unless members.empty?
      h[pool.name] = members.map {|member|
        member.refresh
        {
          'address' => member.address,
          'port' => member.port,
          'state' => {
            'session_enabled_state' => member.session_enabled_state,
            'session_status' => member.session_status,
            'monitor_status' => member.monitor_status,
          }
        }
      }
    end
  end
rescue HTTPClient::ConnectTimeoutError => exc
  abort "#{PROGNAME}: connect to #{hostname}: #{exc.message}"
end

puts h.to_yaml

exit 0

Output:

$ members-per-pool 192.168.160.22
---
sipproxy_5060:
- port: 5060
  address: 192.168.190.21
- port: 5060
  address: 192.168.190.22
redirect_5050:
- port: 5050
  address: 192.168.191.31
- port: 5050
  address: 192.168.191.32
sipproxy_tcp_pool:
- port: 0
  address: 192.168.190.21
- port: 0
  address: 192.168.190.22
redirect_tcp_pool:
- port: 0
  address: 192.168.191.31
- port: 0
  address: 192.168.191.32
eilab-test-keith-pool:
- port: 2380
  address: 192.168.214.108
$ members-per-pool 192.168.160.22 redirect_5050
---
redirect_5050:
- port: 5050
  address: 192.168.191.31
- port: 5050
  address: 192.168.191.32
$

Example: toggle-pool-member

#!/usr/bin/env ruby
# vim:expandtab shiftwidth=2 softtabstop=2

require 'f5'

Member = Struct.new(:address, :port) do
  def to_hash
    { 'address' => self.address, 'port' => self.port }
  end
end

def get_pool_member_states(lb, pools)
  lb.icontrol.locallb.pool_member.get_session_enabled_state(pools.map {|pool| pool.name})
end

def show_pool_members(lb, pools)
  member_state_lists = get_pool_member_states(lb, pools)

  puts "Available pool members"
  puts "======================"
  pools.each_with_index do |pool, i|
    puts "pool #{pool.name}"
    puts '{'
    member_state_list = member_state_lists[i]
    member_state_list.each do |member_state|
      member = member_state["member"]
      addr = member["address"]
      port = member["port"]

      session_state = member_state["session_state"]

      puts "    #{addr}:#{port} (#{session_state})"
    end
    puts '}'
  end
end

def toggle_pool_member(lb, pool, member_def)
  node_ip, node_port = member_def.split(/:/, 2)
  node_port = 0 if node_port.nil? or node_port.empty?
  member = Member.new(node_ip, node_port)

  pool_member_state = get_pool_member_state(lb, pool, member)

  # Set the state to be toggled to.
  toggle_state = 'STATE_DISABLED'
  toggle_state = case pool_member_state
  when 'STATE_DISABLED'; 'STATE_ENABLED'
  when 'STATE_ENABLED'; 'STATE_DISABLED'
  else
    raise "unable to find member #{member_def} in pool #{pool.name}"
  end

  member_session_state = {
    'member'        => member.to_hash,
    'session_state' => toggle_state,
  }
  member_session_state_list = [member_session_state]
  member_session_state_lists = [member_session_state_list]

  lb.icontrol.locallb.pool_member.set_session_enabled_state([pool.name], member_session_state_lists)
  puts "Pool #{pool.name} member {#{node_ip}:#{node_port}} state set from '#{pool_member_state}' to '#{toggle_state}'"
end

def get_pool_member_state(lb, pool, member_def)
  state = nil
  member_state_lists = get_pool_member_states(lb, [pool])
  member_state_list = member_state_lists.first
  member_state_list.each do |member_state|
    member = member_state['member']
    if member.address == member_def.address and
      member.port.to_i == member_def.port.to_i
      state = member_state['session_state']
    end
  end
  state
end

Kernel.abort "Usage: #{$0} <hostname> [pool [member]]" if ARGV.empty?

hostname, pool, member = ARGV

lb = F5::LoadBalancer.new(hostname, :config_file => 'config-admin.yaml', :connect_timeout => 10)

if pool
  pool = F5::LoadBalancer::Pool.new(lb, pool)
  if member
    toggle_pool_member(lb, pool, member)
  else
    show_pool_members(lb, [pool])
  end
else
  show_pool_members(lb, lb.pools)
end

exit

Example: node-state

#!/usr/bin/env ruby
# vim:expandtab shiftwidth=2 softtabstop=2

require 'rubygems'
require 'yaml'
require 'getoptions'

$: << File.join(ENV['HOME'], '/ops/lib') << '/cc/ops/lib'
require 'f5'

PROGNAME = File.basename($0)

def usage(msg=nil)
  Kernel.warn "#{PROGNAME}: #{msg}" if msg
  Kernel.abort "Usage: #{PROGNAME} [--config_file <filename>] <hostname> show|enable|disable <node> [...]"
end

opts = GetOptions.new(%w(config_file=s connect_timeout=i))

hostname, cmd, *nodes = ARGV

usage if nodes.empty?

options = {
  :config_file     => opts.config_file,
  :connect_timeout => opts.connect_timeout,
}

ObjectStatus = [:availability_status, :enabled_status, :status_description]
CMD_MAP = {
  'disable' => lambda {|node|
		 state = 'STATE_DISABLED'
                 node.set_state('STATE_DISABLED')
               },
  'enable'  => lambda {|node|
		 node.set_state('STATE_ENABLED')
               },
  'show'    => lambda {|node|
                 h = {
                   node.address => {
                     'monitor_status' => node.get_monitor_status,
                     'session_enabled_state' => node.get_session_enabled_state,
                     'object_status' => Hash[
                       *ObjectStatus.map {|what|
                         [what.to_s, node.get_object_status.send(what)]
                       }.flatten
                     ],
                   }
                 }
                 puts h.to_yaml
               },
}

usage("invalid command: #{cmd}") unless CMD_MAP.has_key? cmd

lb = F5::LoadBalancer.new(hostname, options)

nodes.map {|node| F5::LoadBalancer::Node.new(lb, node)}.each do |node|
  begin
    CMD_MAP[cmd].call(node)
  rescue HTTPClient::ConnectTimeoutError => exc
    abort "#{PROGNAME}: connect to #{hostname}: #{exc.message}"
  end
end

exit 0

The configuration file

username: guest
password: 'pass'
wsdl:
  LocalLB:
    pool: LocalLB.Pool.wsdl
    pool_member: LocalLB.PoolMember.wsdl
    virtual_server: LocalLB.VirtualServer.wsdl
    node_address: LocalLB.NodeAddress.wsdl

Make sure the `wsdl’ key values point to the required WSDL files.

The f5 library

# vim:expandtab shiftwidth=2 softtabstop=2

require 'icontrol'

module F5

  class LoadBalancer
    attr_reader :hostname, :icontrol

    class Node
      attr_reader :address
      attr_reader :session_enabled_state, :monitor_status

      def initialize(lb, address)
        @lb, @address = lb, address
        clear
      end

      def clear
        @session_enabled_state = 'UNKNOWN'
        @monitor_status = 'UNKNOWN'
      end

      def refresh
        get_session_enabled_state
        get_monitor_status
      end

      def to_s
        "#{@address} (#{@session_enabled_state},#{@monitor_status})"
      end

      def set_monitor_state(what)
        @lb.icontrol.locallb.node_address.set_monitor_state([@address], [what])
      end

      def get_monitor_status
        @monitor_status = @lb.icontrol.locallb.node_address.get_monitor_status([@address]).first
      end

      def set_session_enabled_state(what)
        @lb.icontrol.locallb.node_address.set_session_enabled_state([@address], [what])
      end

      def get_session_enabled_state
        @session_enabled_state = @lb.icontrol.locallb.node_address.get_session_enabled_state([@address]).first
      end

      def get_object_status
        @lb.icontrol.locallb.node_address.get_object_status([@address]).first
      end

      def set_state(what)
        set_session_enabled_state(what)
        set_monitor_state(what)
      end
    end # class Node

    class Pool
      attr_reader :name, :lb

      def initialize(lb, name)
        @lb, @name = lb, name
        @members = nil
      end

      def to_s
        "#{@lb} #{@name}"
      end

      def members
        @members ||= @lb.icontrol.locallb.pool.get_member(@name).first.map do |member|
          PoolMember.new(self, member.address, member.port)
        end
      end

      def find_member(a_member)
        members.find {|member| member.address == a_member.address and member.port == a_member.port}
      end

      def find_node(a_node)
        members.find {|member| member.address == a_node.address}
      end

      def clear_members
        members.each do |member| member.clear end
      end

      # Fetch/update session_enabled_state for the PoolMembers in this Pool
      def get_session_enabled_state
        @lb.icontrol.locallb.pool_member.get_session_enabled_state(@name).first.each do |member_state|
          if m = find_member(member_state.member)
            m.session_enabled_state = member_state.session_state
          end
        end
      end

      # Fetch/update session_status for the PoolMembers in this Pool
      def get_session_status
        @lb.icontrol.locallb.pool_member.get_session_status(@name).first.each do |member_status|
          if m = find_member(member_status.member)
            m.session_status = member_status.session_status
          end
        end
      end

      def get_monitor_instance
        @lb.icontrol.locallb.pool.get_monitor_instance([@name]).first
      end

      def get_monitor_association
        @lb.icontrol.locallb.pool.get_monitor_association([@name]).first
      end

      def get_object_status
        @lb.icontrol.locallb.pool.get_object_status([@name]).first
      end

    end # class Pool

    class PoolMember
      attr_accessor :address, :port
      attr_reader :session_enabled_state, :session_status, :monitor_status

      def initialize(pool, address, port)
        @pool, @address, @port = pool, address, port
        clear
      end

      def clear
        @session_enabled_state = 'UNKNOWN'
        @session_status = 'UNKNOWN'
        @monitor_status = 'UNKNOWN'
      end

      def refresh
        get_session_status
        get_session_enabled_state
        get_monitor_status
      end

      def enabled?
        @session_enabled_state == 'STATE_ENABLED' and
        @session_status == 'SESSION_STATUS_ENABLED' and
        @monitor_status == 'MONITOR_STATUS_UP'
      end

      def to_s
        "#{@address}:#{@port} (#{@session_enabled_state},#{@session_status},#{@monitor_status})"
      end

      def to_hash
        {
          'address' => @address,
          'port'    => @port,
          'state'   => {
            'session_enabled_state' => @session_enabled_state,
            'session_status' => @session_status,
            'monitor_status' => @monitor_status,
          }
        }
      end

      def get_session_status
        @session_status = @pool.lb.icontrol.locallb.pool_member.get_session_status([@pool.name]).first.select {|state|
          state.member.address == @address and state.member.port == @port
        }.map {|state| state.session_status}.first
      end

      def get_session_enabled_state
        @session_enabled_state = @pool.lb.icontrol.locallb.pool_member.get_session_enabled_state([@pool.name]).first.select {|state|
          state.member.address == @address and state.member.port == @port
        }.map {|state| state.session_state}.first
      end

      def set_session_enabled_state(what)
        @pool.lb.icontrol.locallb.pool_member.set_session_enabled_state([@pool.name], [[{'member' => to_hash, 'session_state' => what}]])
        get_session_enabled_state
      end

      def get_monitor_status
        @monitor_status = @pool.lb.icontrol.locallb.pool_member.get_monitor_status([@pool.name]).first.select {|state|
          state.member.address == @address and state.member.port == @port
        }.map {|state| state.monitor_status}.first
      end

      def set_monitor_state(what)
        @pool.lb.icontrol.locallb.pool_member.set_monitor_state([@pool.name], [[{'member' => to_hash, 'monitor_state' => what}]])
        get_monitor_status
      end

      def set_state(what)
        set_session_enabled_state(what)
        set_monitor_state(what)
      end

      def get_monitor_instance
        @pool.lb.icontrol.locallb.pool_member.get_monitor_instance([@pool.name]).first
      end

      def get_monitor_association
        @pool.lb.icontrol.locallb.pool_member.get_monitor_association([@pool.name]).first
      end

    end # class PoolMember

    def initialize(hostname, opts={})
      @hostname = hostname
      @icontrol = F5::IControl.new(@hostname, opts)
    end

    def to_s
      "#{@hostname}"
    end

    def nodes
      @icontrol.locallb.node_address.get_list.map {|node| Node.new(self, node)}
    end

    def pools
      @icontrol.locallb.pool.get_list.map {|pool| Pool.new(self, pool)}
    end

    def pool(pool)
      pools.find {|p| p.name == pool.name}
    end

    def pool_by_name(pool_name)
      pools.find {|p| p.name == pool_name}
    end

    class Management

      class DBVariable
        attr_reader :name

        def initialize(name)
          @name = name
        end

        def query(lb)
          lb.icontrol.management.db_variable.query([@name]).first.value
        end

        def modify(lb, value)
          lb.icontrol.management.db_variable.modify([ { 'name' => @name, 'value' => value } ])
        end

        def delete(lb)
          lb.icontrol.management.db_variable.delete_variable([@name])
        end

        def available?(lb)
          lb.icontrol.management.db_variable.is_variable_available([@name]).first
        end

      end # class DBVariable

    end # class Management

    class System

      class Failover
        
        def get_failover_mode(lb)
          lb.icontrol.system.failover.get_failover_mode
        end
        
        def get_failover_state(lb)
          lb.icontrol.system.failover.get_failover_state
        end

        def get_peer_address(lb)
          lb.icontrol.system.failover.get_peer_address
        end

        def get_version(lb)
          lb.icontrol.system.failover.get_version
        end

        def is_redundant?(lb)
          lb.icontrol.system.failover.is_redundant
        end

      end # class Failover

    end # class System

  end # class LoadBalancer

end # module F5

The icontrol library

# vim:expandtab shiftwidth=2 softtabstop=2

require 'rubygems'
require 'soap/wsdlDriver'

module F5

  DEBUG = false

  class IControl
    attr_reader :endpoint_url, :basic_auth, :connect_timeout

    def initialize(endpoint, opts={})
      config_file      = opts[:config_file] || 'config.yaml'
      method           = opts[:method]      || 'https'
      @connect_timeout = opts[:connect_timeout]

      begin
        configuration = YAML::load_file(config_file)
      rescue Exception => exc
        raise "error loading configuration from '#{config_file}': #{exc.message}"
      end

      @wsdl = configuration['wsdl']
      username = configuration['username']
      password = configuration['password']

      @endpoint_url = "#{method}://#{endpoint}/iControl/iControlPortal.cgi"
      @basic_auth = [@endpoint_url, username, password]

      @modules = {}
      @wsdl.each do |module_name, interfaces|
        @modules[module_name] = Module.new(self, module_name, interfaces)
        class << self; self; end.module_eval do
          define_method(module_name.downcase) { @modules[module_name] }
        end
      end
    end

    class Module
      attr_reader :base

      def initialize(base, name, interfaces)
        puts "loading module #{name}" if DEBUG
        @base, @name = base, name
        @interfaces = Hash.new {|h, interface_name|
          if interfaces.has_key? interface_name
            h[interface_name] = Interface.new(self, interface_name, interfaces[interface_name])
            class << self; self; end.module_eval do
              define_method(interface_name) { h[interface_name] }
            end
            h[interface_name]
          else
            raise "module #{@name}: unknown interface: #{interface_name}"
          end
        }
      end

      def method_missing(meth, *args)
        @interfaces[meth.to_s] # Instantiate RPC driver
      end

      class Interface
        def initialize(mod, name, wsdl_name)
          puts "loading interface #{name}" if DEBUG
          @name = name
          @driver = SOAP::WSDLDriverFactory.new(wsdl_name).create_rpc_driver

          verify_mode = OpenSSL::SSL::VERIFY_NONE
          @driver.options['protocol.http.ssl_config.verify_mode'] = verify_mode
          @driver.options['protocol.http.ssl_config.verify_callback'] = lambda {|is_ok, ctx| true}
          @driver.options['protocol.http.basic_auth'] << mod.base.basic_auth
          if mod.base.connect_timeout
            @driver.options["protocol.http.connect_timeout"] = mod.base.connect_timeout
          end

          # Override WSDL service endpoint
          @driver.endpoint_url = mod.base.endpoint_url
        end

        def method_missing(meth, *args)
          if @driver.respond_to? meth
            @driver.send(meth, *args)
          else
            raise "interface #{@name}: unknown method: #{meth}"
          end
        end
      end # class Interface

    end # class Module

  end # class IControl

end # module F5