Tinkering with the BIGREST Python SDK - Part 1

A couple months back, Leonardo Souza (one of our MVPs) released a new SDK for interacting with the iControl REST interface. A write up right here on DevCentral covers the high level basics of why he rolled his own when we already had one (f5-common-python). In this article, I'm going to compare and contrast the basic functionality of the old SDK with Leo's new SDK, and then add some color commentary after each section.

Instantiating BIG-IP

This first task is required to build a session with the BIG-IP (or BIG-IQ, but this article will focus on BIG-IP.) This can be done with basic authentication or with tokens, the latter of which you’ll need for remote authentication servers. For this article, the device objects will be labeled fcp for the f5-common-python SDK and br for the bigrest SDK.

f5-common-python

from f5.bigip import ManagementRoot
# Basic Auth
fcp = ManagementRoot('10.0.2.15', 'admin', 'admin')
# Token Auth
fcp = ManagementRoot('10.0.2.15', 'admin', 'admin', token=True)

bigrest

from bigrest.bigip import BIGIP
# Basic Auth
br = BIGIP('10.0.2.15', 'admin', 'admin')
# Token Auth
br = BIGIP('10.0.2.15', 'admin', 'admin', request_token=True)

Conclusion

For basic auth, instantiation is a wash with the exception of the method itself. In the f5-common-python SDK, ManagementRoot is used for both BIG-IP and BIG-IQ systems, whereas in BIGREST, it is system specific. Being explicit is a good thing, as with BIGREST you’ll always know if you are on BIG-IP or BIG-IQ, but if you are automating tests, using a single nomenclature would likely reduce lines of code.

(Updated 9/25) For token authentication, the token and request_token parameters exist to handle exactly that. No differentiator in functionality between the two at all, both work as advertised.

What you don’t see in either of these is how they handle SSL certificates. Ideally, from a security standpoint you should have to opt out of a secure transaction, but both libraries are set to ignore certificate errors. f5-common-python has an attribute (verify) that is defaulted to false and it doesn’t look configurable in bigrest. In my view both should be refactored to default to a secure posture so insecurity is an explicit decision by the programmer.

The final observation I have on instantiation is proxy support, which is built-in for f5-common-python but not bigrest.

Retrieving a List of Pools

In this quick comparison, I just want to see how easy it is to grab a list of objects and display their names. It could be anything, but I’ve chosen the pool here. We’ll assume instantiation has already taken place.

f5-common-python

p1 = fcp.tm.ltm.pools.get_collection()
for p in p1:
...     print(p.name)
...
NewPool
p_tcpopt
testpool

bigrest

p2 = br.load('/mgmt/tm/ltm/pool')
for p in p2:
...     print(p.properties.get('name'))
...
NewPool
p_tcpopt
testpool

Conclusion

Here is where you can start to see some differences in design choices. In the f5-common-python approach, the SDK methods are associated to defined iControl REST interfaces. With bigrest, that approach is avoided so that no development effort is needed to support missing or new interfaces in the SDK. Whereas that’s a smart choice in my opinion, it does put a little more effort on the programmer to know the interfaces, but on the flip side, the programmer doesn’t need to learn another somewhat different nomenclature in the SDK if they are already familiar with the tmsh endpoints. 

As far as the data itself is concerned, I do like the easy access to the key/value pairs in f5-common-python (p.name) versus bigrest (p.properties.get(‘name’), or p.properties[’name’] if you prefer), but that’s a minor gripe given the flexibility of loading any endpoint with a single simple load command. 

CRUD Operations for a Pool and its Members

f5-common-python

Creating a pool and a pool member

pool = fcp.tm.ltm.pools.pool.create(name='fcp_pool', partition='Common')
fcp.tm.ltm.pools.pool.exists(name='fcp_pool', partition='Common')
True

pool_member = pool.members_s.members.create(name='192.168.102.44:80', partition='Common')
pool.members_s.members.exists(name='192.168.102.44:80', partition='Common')
True

Refreshing and updating a pool

# Load and refresh without updating
p1 = fcp.tm.ltm.pools.pool.load(name='fcp_pool', partition='Common')
p1.description
Traceback (most recent call last):
  File "", line 1, in
  File "/Users/rahm/Documents/PycharmProjects/scripts/py38/lib/python3.8/site-packages/f5/bigip/mixins.py", line 102, in __getattr__
    raise AttributeError(error_message)
AttributeError: '<class 'f5.bigip.tm.ltm.pool.Pool'>' object has no attribute 'description'
p1.description = 'updating description'
p1.description
'updating description'
p1.refresh()
p1.description
Traceback (most recent call last):
  File "", line 1, in
  File "/Users/rahm/Documents/PycharmProjects/scripts/py38/lib/python3.8/site-packages/f5/bigip/mixins.py", line 102, in __getattr__
    raise AttributeError(error_message)
AttributeError: '<class 'f5.bigip.tm.ltm.pool.Pool'>' object has no attribute 'description'

# Load, update, and refresh
p1 = fcp.tm.ltm.pools.pool.load(name='fcp_pool', partition='Common')
p2 = fcp.tm.ltm.pools.pool.load(name='fcp_pool', partition='Common')
assert p1.name == p2.name
p1.description = 'updating description'
p1.update()
p1.description
'updating description'
p2.description
Traceback (most recent call last):
  File "", line 1, in
  File "/Users/rahm/Documents/PycharmProjects/scripts/py38/lib/python3.8/site-packages/f5/bigip/mixins.py", line 102, in __getattr__
    raise AttributeError(error_message)
AttributeError: '<class 'f5.bigip.tm.ltm.pool.Pool'>' object has no attribute 'description'
p2.refresh()
p2.description
'updating description'

Deleting a pool

p1 = fcp.tm.ltm.pools.pool.load(name='fcp_pool', partition='Common')
p1.delete()
fcp.tm.ltm.pools.pool.exists(name='fcp_pool', partition='Common')
False

bigrest

Creating a pool and a pool member

pool_data = {}
pool_data['name'] = 'br_pool'
pool = br.create('/mgmt/tm/ltm/pool', pool_data)
br.exist('/mgmt/tm/ltm/pool/br_pool')
True

pm_data = {}
pm_data['name'] = '192.168.102.44:80'
pool_member = br.create('/mgmt/tm/ltm/pool/br_pool/members', pm_data)
br.exist('/mgmt/tm/ltm/pool/br_pool/members/~Common~192.168.102.44:80')
True

Refreshing and updating a pool

# Load and refresh without updating
p1 = br.load('/mgmt/tm/ltm/pool/br_pool')[0]
p1.properties['description']
Traceback (most recent call last):
  File "", line 1, in
KeyError: 'description'
p1.properties['description'] = 'updating description'
p1.properties['description']
'updating description'
p1 = br.load('/mgmt/tm/ltm/pool/br_pool')[0]
p1.properties['description']
Traceback (most recent call last):
  File "", line 1, in
KeyError: 'description'

# Load, update and refresh
p1 = br.load('/mgmt/tm/ltm/pool/br_pool')[0]
p2 = br.load('/mgmt/tm/ltm/pool/br_pool')[0]
assert p1.properties['name'] == p2.properties['name']
p1.properties['description'] = 'updating description'
p1 = br.save(p1)
p1.properties['description']
'updating description'
p2.properties['description']
Traceback (most recent call last):
  File "", line 1, in
KeyError: 'description'
p2 = br.load('/mgmt/tm/ltm/pool/br_pool')[0]
p2.properties['description']
'updating description'

Deleting a pool

br.delete('/mgmt/tm/ltm/pool/br_pool')
br.exist('/mgmt/tm/ltm/pool/br_pool')
False

Conclusion

With both approaches, you need to understand the tmsh hierarchy of BIG-IP objects. That said, with bigrest, you only need to know that. With f5-common-python, you need to understand as well the nuances of the SDK authors and how it was built (see pools.pool.members_s.members above in the code.) I also like with bigrest that you can delete an object with one command; in f5-common-python you have to load the object before deleting it. I don’t like that bigrest doesn’t distinguish between collections and single objects, so that means you have to do a little extra repetitive work with isolating the single list object instead of that being handled for you. Being able to work with any collection or single object, though, is super handy and a big differentiator, as bigrest isn’t bogged down with defining the endpoints. I’m sure there are some (BIG-IP) version specific gotchas with the bigrest save command throwing the whole object back to the REST interface, as I recall coding around many of the nuances with f5-common-python where we’d have to strip some attributes before updating, but that’s a design choice where keeping things super “dumb” makes everything more flexible. There is no refresh method in bigrest, but reloading the object works just as well, and it should be clear by now that bigrest’s update method is called save and works at the device level instead of the local python object level but that’s also a design choice that doesn’t bother me at all. My final observation here is the requirement in bigrest to build a data object. I don't hate it, but I do like with f5-common-python how I can just pass an attribute to the method and it creates the data object for me.

Updating Unnamed Objects

Most objects are created and thus have names, but there are many that exist in the system that are not named and cannot be created or deleted.

f5-common-python

Adding DNS servers to the BIG-IP configuration

settings = fcp.tm.sys.dns.load()
settings.nameServers = ['8.8.8.8']
settings.update()

bigrest

Adding DNS servers to the BIG-IP configuration

settings = br.load('/mgmt/tm/sys/dns')[0]
settings.properties['nameServers'] = ['8.8.8.8']
settings = br.save(settings)

Conclusion

There are no real challenging or intriguing differences here that I haven’t already covered, so let’s just move on!

More to come...

In part two I’ll move beyond the basic building blocks of configuration to look at how to run commands, upload and download files, and work with stats, transactions, and task. Afterward, I’ll weigh in on my overall thoughts for both projects and where to go from here.

Published Sep 01, 2020
Version 1.0
  • Let me add a few things, that explain the design options and challenges...

     

    Token

    This is how you would normally use the SDK with token:

    device = BIGIP(ip, username, password, request_token=True)

    The SDK uses the username and password to request a token, and uses that token while is valid and automatic requests a new one when it expires.

     

    There are 2 token functions that can be used to get tokens.

    token() and refresh_token()

     

    Both BIG-IP and BIG-IQ can use token, but only BIG-IQ uses refresh token.

    BIG-IQ refresh token is valid for many hours, and is used to request a token.

     

    Those functions can be used for any scenario where you don't have the username or password, and you only got a token or refresh token to use.

    Very unusual use case, but they are there as options, as the REST API supports it.

     

    SSL and Proxy

    Yes, the SDK is hardcoded to skip SSL certificate check.

    No proxy support at the moment, maybe in the future.

     

    Properties

    The first versions of the SDK I had with obj.name.

    The problem is when you use the show command, because the properties have names like "clientside.maxConns", and you can't have an attribute with a dot in the name.

    As far as remember, f5-common-python did not implement /stats part of the REST API, so is not a problem there.

     

    Single or Multiple Objects

    For the load command, I want to be sure it returns always the same type of object, to avoid errors with returning different type of objects.

    So, this is why it returns a list even when the device has a single virtual server for example.

    I thought about using 2 methods, load and list_load, but it would be confusing and duplication of code.

     

    REST API Extra Properties

    Initially I thought I had to remove extra properties when updating the object, but it turns out the REST API just ignore them.

    So, you can load a object with the extra properties from the REST API, and send everything back to the API, and it will not give any error (just ignore it).

    The only thing I had to do is convert to enabled=True or disabled=True, for cases where it has enabled=False or disabled=False.

  • Thanks  for the additional context! I'll cover stats in part 2. The other SDK does have a utility, but it's not awesome.

  • Great article  and awesome new SDK  that i found out right on time.

    I was struggling with "f5-common-python" to add/remove server side ssl to a virtual server.

    I found BIGREST cleaner syntax and easy to understand.

    Below is the snippet i used to do what I was looking for:

     

    domain_name = input()
    # Connect to BigIP
    b = BIGIP(ip, username, password)
     
    # Load the Profiles on a virtual server
    profiles = b.load(f"/mgmt/tm/ltm/virtual/{rest_format(domain_name)}/profiles")
     
    print(f"List of Profiles attached to {domain_name}")
    profile_context_list = []
    for p in profiles:
        profile_context_list.append(p.properties["context"])
    print(profile_context_list)
     
    if "serverside" in profile_context_list:
        print("Serverside SSL applied")
        print("Deleting Serverside SSL profile")
        path = (
          f"/mgmt/tm/ltm/virtual/{rest_format(domain_name)}/profiles/{rest_format(profile_name)}"
        )
        b.delete(path)
    else:
        print("Serverside SSL doesn't applied")
        print("Adding Serverside SSL Profile")
        data = {}
        data["name"] = profile_name
        data["context"] = "serverside"
        b.create(f"/mgmt/tm/ltm/virtual/{rest_format(domain_name)}/profiles", data)

    Complete code : https://github.com/mshoaibshafi/nre-tools/tree/main/f5

     

    Thank you,

    Muhammad