In the last article we looked at request parameter options. In this article we'll look at how to use transactions with the python SDK.

Before we dig into the SDK implementation, let’s take a look at what a transaction is, why you’d use one, and how the REST interface implements them.

What is a transaction?

In the context of BIG-IP configuration, a transaction is a sequence of individual commands performed as a single unit of work, similar to how a relational database transaction commit only updates the database if all commands within the commit are successful.

Why would I use a transaction?

Transactions exist for the situations where multiple commands are required, or a series of commands are dependent on other commands being successful. It is also useful simply as a desired state for configuration control, where if anything fails, roll it all back and evaluate and not leave partial configs hanging out there.

One situation that has dependencies is the ssl key/cert pair. Updating one or the other, but not both simultaneously, results in an error.

How does the iControl REST interface implement them?

There are three phases in the life of a transaction:

  1. Creation - This is pretty self explanatory. This is done with a POST to /mgmt/tm/transaction. The json data returned from BIG-IP includes the transaction id.
  2. Modification - In this phase, you can add, modify, reorder, and delete any commands to the transaction. In order to do so, you generate regular commands (e.g. creating a pool via POST /ltm/pool), but you must include the X-F5-REST-Coordination-Id header with a value of your transaction ID.
  3. Commit - When your transaction script is as you want it, you send a PATCH to /mgmt/tm/transaction/ID with the json blob {’state’: ‘VALIDATING’}. You can add the attribute validateOnly and set to True if you only want to validate your changes, not commit them.

The steps for the transaction flow without the SDK look like this:

import json
import requests

# Create your session object
b = requests.session()
b.auth = ('admin', 'admin')
b.verify = False
b.headers.update({'Content-Type':'application/json'})

# Create the transaction
tx = b.post('https://ltm3.test.local/mgmt/tm/transaction', json.dumps({}).json()['transId']

# Apply the transaction header
b.headers.update({'X-F5-REST-Coordination-Id': tx})

## Do your stuff here ##

# Remove the transaction header
del b.headers['X-F5-REST-Coordination-Id']

# Commit the transaction
Result = b.patch('https://ltm3.test.local/mgmt/tm/transaction/{}'.format(tx), json.dumps({'state':'VALIDATING'})).json()

How do I use them in the python SDK?

With the python SDK, you can use the transaction context manager to abstract all the inner workings of transactions and focus on the actual tasks at hand:

from f5.bigip import ManagementRoot
from f5.bigip.contexts import TransactionContextManager

# Create your session object
b = ManagementRoot('ltm3.test.local', 'admin', 'admin')

# Set up the transaction
tx = b.tm.transactions.transaction
with TransactionContextManager(tx) as api:
	## do your stuff here ##

Far more on the side of simple, no? The context manager has enter and exit states that manages all the transaction id, header, and json blob details for you. You can take a peek here on GitHub if you are interested.

For an example error should your transaction fail, below I try to create a pool and create a virtual server with that pool attached. Note that the pool should work, but the virtual will fail as I already have a virtual with that name. Finally, because this was completed within a transaction, the pool will not actually be created because the virtual server creation failed.

>>> with TransactionContextManager(tx) as api:
...    api.tm.ltm.pools.pool.create(name='testpool1')
...    api.tm.ltm.virtuals.virtual.create(name='testvip', pool='testpool', destination='192.168.102.99:80')
    
Traceback (most recent call last):
  File "/Users/rahm/Documents/PycharmProjects/f5-common-python/f5/bigip/contexts.py", line 97, in __exit__
    validateOnly=self.validate_only)
  File "/Users/rahm/Documents/PycharmProjects/f5-common-python/f5/bigip/resource.py", line 411, in modify
    self._modify(**patch)
  File "/Users/rahm/Documents/PycharmProjects/f5-common-python/f5/bigip/resource.py", line 404, in _modify
    response = session.patch(patch_uri, json=patch, **requests_params)
  File "/Users/rahm/Documents/PycharmProjects/f5-common-python/f5py3/lib/python3.7/site-packages/icontrol/session.py", line 284, in wrapper
    raise iControlUnexpectedHTTPError(error_message, response=response)
icontrol.exceptions.iControlUnexpectedHTTPError: 409 Unexpected Error: Conflict for uri: https://ltm3.test.local:443/mgmt/tm/transaction/1536877430032827/
Text: '{"code":409,"message":"transaction failed:01020066:3: The requested Virtual Server (/Common/testvip) already exists in partition Common.","errorStack":[],"apiError":2}'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File input, line 3, in 
  File "/Users/rahm/Documents/PycharmProjects/f5-common-python/f5/bigip/contexts.py", line 100, in __exit__
    raise TransactionSubmitException(e)
f5.sdk_exception.TransactionSubmitException: 409 Unexpected Error: Conflict for uri: https://ltm3.test.local:443/mgmt/tm/transaction/1536877430032827/
Text: '{"code":409,"message":"transaction failed:01020066:3: The requested Virtual Server (/Common/testvip) already exists in partition Common.","errorStack":[],"apiError":2}'

And we can also run a successful transaction, but use the validate_only so it doesn't update the configuration. Notice no error here and that the objects were not created:

>>> with TransactionContextManager(tx, validate_only=True) as api:
...    api.tm.ltm.pools.pool.create(name='testpool2')
...    api.tm.ltm.virtuals.virtual.create(name='testvip2', pool='testpool2', destination='192.168.102.99:80')

>>>
>>> b.tm.ltm.pools.pool.exists(name='testpool2')
False
>>> b.tm.ltm.virtuals.virtual.exists(name='testvip2')
False

Hopefully this gives you an idea of how to simplify your transaction updates with python utilizing the f5-common-python SDK. For a more concrete example of a useful transaction, you can see my Let's Encrypt hook script where I'm modifying the ssl key/cert. Happy coding out there!

Note that much of this information is presented strictly from the perspective of the REST interface instead of focusing on the python SDK in the earlier Demystifying the iControl Interface article on transactions.