v11 iControl: Transactions

Introduction

One of the most commonly requested features for iControl we’ve seen recently has been for transaction support. It was implemented in TMSH for Version 10 and is now available for iControl in Version 11. Transactions are super handy and anyone who has used them on other networking devices or databases can attest to their usefulness. There are many occasions where we want to make large sweeping changes, but want to interrupt the changes if any of them fails. This ensures that any changes made to the device will be done cleanly and if one step fails, subsequent actions will not fail as a result leaving the device in an unusable state.

Take for example a large web farm rollout: if we were building hundreds of virtual servers passing traffic to thousands of nodes, we probably would not want to do this manually, especially if we have to do it another 10 times at other datacenter locations. In a situation like this we would more than likely write an iControl script in the language of our choosing and loop through the creation of all these objects. This is works very well until we get halfway through our deployment and hit a snag. Now the device is in a semi-configured state. We will either need to fix our script to pickup where we left off or wipe the device and start over. Neither of which is an ideal situation. Wouldn’t it be great if we could tell the BIG-IP, “give me all or nothing?”

Transaction Behaviors

Last week we talked about iControl sessions and their importance in separating concurrent requests from the same user. They were useful for setting a variables (session timeout, query recursion, and active folder) on the BIG-IP, but didn’t provide for much else. This is where transactions enter the conversation. Within an iControl session, transactions can be initiated and a number of requests queued prior to submitting the transaction for processing. There are a few transaction behavior that you should be aware of before you start using them in all your iControl scripts.

Transactions are initiated via the ‘start_transaction’ method. Any iControl requests that involve a configuration modification (adds, edits, deletes, etc.) will be queued by the BIG-IP. If the request however is a query, the response will be returned immediately. If an invalid request (improper data structure, malformed SOAP request, etc.) is sent to the BIG-IP, the error will be returned immediately. If there is a modification request and the structure is valid, but there is an error in the content (pool doesn’t exists, invalid profile, etc.), the error will not be returned until the transaction is submitted via the ‘submit_transaction’ method. Let bulletize those behaviors:

  1. Only iControl requests that make modifications to the BIG-IP configuration are queued in transactions
  2. iControl queries (requests that do not make changed) are always served in real-time and not queued in transactions
  3. SOAP errors such as malformed requests will always be returned immediately
  4. iControl errors related to configuration changes will be returned only after the transaction is submitted
  5. Transactions will remain open until they are submitted or the session times out (default is 30 minutes)

Let’s walk through a couple scenarios to help us better visualize the behavior of transactions.

Creating And Deleting Pools Without Transactions

In this example we are going to forego the transaction and create 3 pools and immediately delete those 3 pools along with a random non-existent pool inserted in the middle of our delete requests. The non-existent pool will not be found on the LTM causing the iControl interface to throw an error and the script to exit with an error.

#!/usr/bin/ruby

require 'rubygems'
require 'f5-icontrol'

# initiate iControl interfaces

bigip = F5::IControl.new('10.0.0.1', 'admin', 'admin', \
  ['System.Session', 'LocalLB.Pool']).get_interfaces

# an array of the pools we will be working with

pools = [ \
  { 'name' => 'my_http_pool_1', 'members' => [ '10.0.0.100', '10.0.0.101' ] }, \
  { 'name' => 'my_http_pool_2', 'members' => [ '10.0.0.102', '10.0.0.103', '10.0.0.104' ] }, \
  { 'name' => 'my_http_pool_3', 'members' => [ '10.0.0.105' ] } \
]

# create pools

pools.each do |pool|
  # assemble members array to reflect Common::AddressPort struct

  members = pool['members'].collect do |member|
    { 'address' => member, 'port' => 80 }
  end

  puts "Creating pool #{pool['name']}..."

  bigip['LocalLB.Pool'].create_v2( \
    [ pool['name'] ], \
    ['LB_METHOD_ROUND_ROBIN'], \
    [ members ] \
  )
end

# collect list of pools to delete and insent a random pool name that doe not exist and shuffle them

pool_names = pools.collect { |pool| pool['name'] }.push('random_pool_that_doesnt_exist').shuffle

# delete pools

pool_names.each do |pool_name|
  puts "Deleting pool #{pool_name}..."

  bigip['LocalLB.Pool'].delete_pool(pool_name.to_a)
end

Now, as we run our script you'll see that it isn't so happy trying to remove that pool that doesn't exist. Notice that it is the ‘delete_pool’ method that is throwing the exception. Here's the output:

Creating pool my_http_pool_1...
Creating pool my_http_pool_2...
Creating pool my_http_pool_3...
Deleting pool my_http_pool_2...
Deleting pool random_pool_that_doesnt_exist...
: Exception caught in LocalLB::urn:iControl:LocalLB/Pool::delete_pool() (SOAP::FaultError)
Exception: Common::OperationFailed
        primary_error_code   : 16908342 (0x01020036)
        secondary_error_code : 0
        error_string         : 01020036:3: The requested pool (/Common/random_pool_that_doesnt_exist) was not found.

shell returned 1

If we now go and check the pools on our LTM we’ll see that we have two pools left leaving us in an unfavorable half-broken state. This is where transaction shine, avoiding half-broken configurations.

Creating And Deleting Pools With One Transaction

Next we’ll submit all of our changes as a single transaction. We’ll again try and delete our ‘random_pool_that_doesnt_exist’, but this time the transaction should catch this error and prevent any of the changes from being submitted. While this may not be completely ideal because our pools won’t get created or deleted as we had hoped, it will prevent us from entering a bad configuration state. The end result will be that our configuration remains in the previous state before we executed our iControl script. Here’s the code:

#!/usr/bin/ruby

require 'rubygems'
require 'f5-icontrol'

# initiate iControl interfaces

bigip = F5::IControl.new('10.0.0.1', 'admin', 'admin', \
  ['System.Session', 'LocalLB.Pool']).get_interfaces

# an array of the pools we will be working with

pools = [ \
  { 'name' => 'my_http_pool_1', 'members' => [ '10.0.0.100', '10.0.0.101' ] }, \
  { 'name' => 'my_http_pool_2', 'members' => [ '10.0.0.102', '10.0.0.103', '10.0.0.104' ] }, \
  { 'name' => 'my_http_pool_3', 'members' => [ '10.0.0.105' ] } \
]

# start transaction

bigip['System.Session'].start_transaction

# create pools

pools.each do |pool|
  # assemble members array to reflect Common::AddressPort struct

  members = pool['members'].collect do |member|
    { 'address' => member, 'port' => 80 }
  end

  puts "Creating pool #{pool['name']}..."

  bigip['LocalLB.Pool'].create_v2( \
    [ pool['name'] ], \
    ['LB_METHOD_ROUND_ROBIN'], \
    [ members ] \
  )
end

# collect list of pools to delete and insent a random pool name that doe not exist and shuffle them

pool_names = pools.collect { |pool| pool['name'] }.push('random_pool_that_doesnt_exist').shuffle

# delete pools

pool_names.each do |pool_name|
  puts "Deleting pool #{pool_name}..."

  bigip['LocalLB.Pool'].delete_pool(pool_name.to_a)
end

# submit the transaction

bigip['System.Session'].submit_transaction

Look at the above error and notice which method threw the exception that caused our script to exit: System/Session::submit_transaction(). In the previous example the LocalLB/Pool::delete_pool() method was the culprit, now it's the transaction throwing the error. That's a good sign that it is doing its job. Here’s the script’s output:

Creating pool my_http_pool_1...
Creating pool my_http_pool_2...
Creating pool my_http_pool_3...
Deleting pool random_pool_that_doesnt_exist...
Deleting pool my_http_pool_1...
Deleting pool my_http_pool_2...
Deleting pool my_http_pool_3...
: Exception caught in System::urn:iControl:System/Session::submit_transaction() (SOAP::FaultError)
Exception: Common::OperationFailed
        primary_error_code   : 16908342 (0x01020036)
        secondary_error_code : 0
        error_string         : 01020036:3: The requested pool (/Common/random_pool_that_doesnt_exist) was not found.

shell returned 1

When we go and look at the pool on our LTM now we'll notice that nothing was actually created or deleted because our request to remove a pool that didn't exist failed.

Creating And Deleting Pools With Multiple Transactions

There may come a time when you want to submit different batches of changes as multiple transactions. With iControl it is as simple as submitting one and starting another. They will be executed linearly with the code. Just be careful that you don't start a transaction and inadvertently submit it prematurely elsewhere in your code.

In this example we will combine all of our pool creation statements into one transaction and our deletions into another. The net result should be that we have 3 pools on the LTM at the end of the script's execution as the 3 pools will be created without issue and the deletions will fail due once again to the non-existent pool.

#!/usr/bin/ruby

require 'rubygems'
require 'f5-icontrol'

# initiate iControl interfaces

bigip = F5::IControl.new('10.0.0.1', 'admin', 'admin', \
  ['System.Session', 'LocalLB.Pool']).get_interfaces

# an array of the pools we will be working with

pools = [ \
  { 'name' => 'my_http_pool_1', 'members' => [ '10.0.0.100', '10.0.0.101' ] }, \
  { 'name' => 'my_http_pool_2', 'members' => [ '10.0.0.102', '10.0.0.103', '10.0.0.104' ] }, \
  { 'name' => 'my_http_pool_3', 'members' => [ '10.0.0.105' ] } \
]

# start transaction for pool creations

bigip['System.Session'].start_transaction
puts "Starting pool creation transaction..."

# create pools

pools.each do |pool|
  # assemble members array to reflect Common::AddressPort struct

  members = pool['members'].collect do |member|
    { 'address' => member, 'port' => 80 }
  end

  puts "Creating pool #{pool['name']}..."

  bigip['LocalLB.Pool'].create_v2( \
    [ pool['name'] ], \
    ['LB_METHOD_ROUND_ROBIN'], \
    [ members ] \
  )
end

# submit the transaction for pool creations

bigip['System.Session'].submit_transaction
puts "Submitting pool creation transaction..."

# start transaction for pool deletions

bigip['System.Session'].start_transaction
puts "Starting pool deletion transaction..."

# collect list of pools to delete and insent a random pool name that doe not exist and shuffle them

pool_names = pools.collect { |pool| pool['name'] }.push('random_pool_that_doesnt_exist').shuffle

# delete pools

pool_names.each do |pool_name|
  puts "Deleting pool #{pool_name}..."
  bigip['LocalLB.Pool'].delete_pool(pool_name.to_a)
end

# submit the transaction for pool deletions

bigip['System.Session'].submit_transaction
puts "Submitting pool deletion transaction..."

Now when we look at the output from our script, we'll notice that there are two separate tranactions occuring. The first executes without issue and creates the 3 pools on our LTM. The second transaction however fails due to trying to delete our now infamous 'random_pool_that_doesnt_exist'. Here’s the output from our script:

Starting pool creation transaction...
Creating pool my_http_pool_1...
Creating pool my_http_pool_2...
Creating pool my_http_pool_3...
Submitting pool creation transaction...
Starting pool deletion transaction...
Deleting pool my_http_pool_3...
Deleting pool my_http_pool_2...
Deleting pool my_http_pool_1...
Deleting pool random_pool_that_doesnt_exist...
: Exception caught in System::urn:iControl:System/Session::submit_transaction() (SOAP::FaultError)
Exception: Common::OperationFailed
        primary_error_code   : 16908342 (0x01020036)
        secondary_error_code : 0
        error_string         : 01020036:3: The requested pool (/Common/random_pool_that_doesnt_exist) was not found.

shell returned 1

If we examine our LTM configuration now we'll notice that there are three new pools configured. While our original instructions had been to create then subsequently remove them all, this is not a complete failure. We were able to isolate those failures to a transaction and ensure that our LTM remained in a working state throughout the modifications we were making.

Conclusion

Transactions are one of the most exciting features of Version 11. They give developers and administrators a new level of control over their iControl applications. Making use of transactions can give your iControl applications a new layer of insulation from making potential mission critical mistakes. They take a minimal amount of time to implement and can save developers and engineers from hours of headaches when things go haywire. Stay tuned for more Version 11 Tech Tips!

 
Updated Mar 18, 2022
Version 2.0
  • From the text I understand that transactions work only within a session. The example code does not maintain a session as described in https://devcentral.f5.com/s/articles/v11-icontrol-sessions. Is the example code simplified or a session is not mandatory in order to start a transaction? Quite confusing for beginners.
  • Bullet 3. says `SOAP errors such as malformed requests will always be returned immediately` - what's the effect of this on the current transaction? Is it rolled back or the faulty request is excluded but the transaction remains intact?
  • Is it Possible to rollback a transaction after a commit? We have two datacenters so I need to update two loadbalancers. I do first a validate on both loadbalancer transactions. If that is successfull I do a commit on both transactions.

     

    Can it happen that a transaction fails on one of the loadbalancers after a successfull validate? If yess I need to rollback the first commit.