F5 iControl REST API in Python

In F5 BIG-IP devices there is iControl REST API for interfacing with the devices programmatically. In this post I’ll briefly describe the basics of using the API in Python. I’ll be using BIG-IP version 17.1 here with a separately created non-administrator user account.

For starters

I’ll use a Python virtual environment to contain the separately-installed packages. Setup:

markku@devel:~$ python3 -m venv f5venv
markku@devel:~$ . f5venv/bin/activate
(f5venv) markku@devel:~$ pip install -U pip wheel
...
Successfully installed pip-25.3 wheel-0.45.1
(f5venv) markku@devel:~$ pip install requests
...
Successfully installed certifi-2025.11.12 charset_normalizer-3.4.4 idna-3.11 requests-2.32.5 urllib3-2.6.2
(f5venv) markku@devel:~$ python3
Python 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

I’ll be running the commands right in the interactive shell of Python (also known as REPL, Read-Evaluate-Print-Loop).

Logging in

First I’ll prepare a requests.Session object for handling the HTTP connections since it’s nice to reuse the existing TCP connections if possible.

Since my BIG-IP device doesn’t have a trusted TLS certificate, I’ll also disable the “untrusted certificate” warnings. Don’t do that if you have proper trusted certificates in use.

>>> import requests
>>> session = requests.Session()
>>> session.verify = False
>>> import urllib3
>>> urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
>>>

An authentication token is used for each authenticated call. Generating a token:

>>> import getpass
>>> f5_api_password = getpass.getpass()
Password:
>>> auth_data = {
... "username": "myusername",
... "password": f5_api_password,
... "loginProviderName": "tmos",
... }
>>> res = session.post("https://10.10.10.10/mgmt/shared/authn/login", json=auth_data)
>>> res.status_code
200
>>> # res.json() returns a dict but we will print it with nice indentations
>>> import json
>>> print(json.dumps(res.json(), indent=4))
{
"username": "myusername",
"loginReference": {
"link": "https://localhost/mgmt/cm/system/authn/providers/tmos/1f44a60e-11a7-3c51-a49f-82983026b41b/login"
},
"loginProviderName": "tmos",
"token": {
"token": "PVVVL7CUQMNIVIDV2PERXVMUUK",
"name": "PVVVL7CUQMNIVIDV2PERXVMUUK",
"userName": "myusername",
"authProviderName": "tmos",
"user": {
"link": "https://localhost/mgmt/cm/system/authn/providers/tmos/1f44a60e-11a7-3c51-a49f-82983026b41b/users/0a32cb9a-b028-3b85-a0b7-aca5cc21c0c6"
},
"timeout": 1200,
"startTime": "2025-12-14T11:05:35.905+0200",
"address": "10.10.10.200",
"partition": "[All]",
"generation": 1,
"lastUpdateMicros": 1765703135900413,
"expirationMicros": 1765704335905000,
"kind": "shared:authz:tokens:authtokenitemstate",
"selfLink": "https://localhost/mgmt/shared/authz/tokens/PVVVL7CUQMNIVIDV2PERXVMUUK"
},
"generation": 0,
"lastUpdateMicros": 0
}
>>> auth_token = res.json()["token"]["token"]
>>> auth_token
'PVVVL7CUQMNIVIDV2PERXVMUUK'
>>>

The links in the REST API call results always refer to localhost instead of the real management address or hostname of the device, be mindful of that if using those links for API calls later.

The authentication token is valid for 20 minutes only (unless deleted earlier), so you’ll need to create a new token if you continue using the API longer than that. Conveniently, the token data also includes the expirationMicros property, so it is easy to check if the token has been expired or not:

>>> token_expires = res.json()["token"]["expirationMicros"] / 1_000_000
>>> import time
>>> # Is the token expired yet?
>>> time.time() > token_expires
False
>>>

Now that we have the token (and it is still valid), we need to use it for the rest of the calls. Let’s add it to the session headers:

>>> session.headers
{'User-Agent': 'python-requests/2.32.5', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
>>> session.headers.update({"X-F5-Auth-Token": auth_token})
>>> session.headers
{'User-Agent': 'python-requests/2.32.5', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'X-F5-Auth-Token': 'PVVVL7CUQMNIVIDV2PERXVMUUK'}
>>>

Testing the access

For a demo, let’s get the system version information:

>>> res = session.get("https://10.10.10.10/mgmt/tm/sys/version")
>>> res.status_code
200
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:sys:version:versionstats",
"selfLink": "https://localhost/mgmt/tm/sys/version?ver=17.1.3",
"entries": {
"https://localhost/mgmt/tm/sys/version/0": {
"nestedStats": {
"entries": {
"Build": {
"description": "0.20.11"
},
"Date": {
"description": "Sun Oct 12 12:43:02 PDT 2025"
},
"Edition": {
"description": "Engineering Hotfix"
},
"Product": {
"description": "BIG-IP"
},
"Title": {
"description": "Main Package"
},
"Version": {
"description": "17.1.3"
}
}
}
},
"https://localhost/mgmt/tm/sys/version/1": {
"nestedStats": {
"entries": {
"ID1009161_3": {
"description": "ID1009161-3"
},
"ID1012009_4": {
"description": "ID1012009-4"
},
...
>>>

By the way, as shown in the selfLink property above, you can also use specific version of the API by adding ver query parameter in the API calls. The default is to use the version that the system is running.

In the collection level (like /mgmt/tm/sys above) you can also use the GET command to get a list of all possible options below that:

>>> res = session.get("https://10.10.10.10/mgmt/tm/sys")
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:sys:syscollectionstate",
"selfLink": "https://localhost/mgmt/tm/sys?ver=17.1.3",
"items": [
{
"reference": {
"link": "https://localhost/mgmt/tm/sys/application?ver=17.1.3"
}
},
{
"reference": {
"link": "https://localhost/mgmt/tm/sys/crypto?ver=17.1.3"
}
},
{
"reference": {
"link": "https://localhost/mgmt/tm/sys/daemon-log-settings?ver=17.1.3"
}
},
{
"reference": {
"link": "https://localhost/mgmt/tm/sys/diags?ver=17.1.3"
}
},
{
"reference": {
"link": "https://localhost/mgmt/tm/sys/disk?ver=17.1.3"
}
},
...
>>>

If you are unsure about the exact feature name you are looking for, listing the upper level can be handy. Note that with restricted (non-administrator) user roles it might not be possible to retrieve all information available.

Creating a new authentication token

While we were looking at the data above, the token has been expired and a new token must be created:

>>> time.time() > token_expires
True
>>> res = session.post("https://10.10.10.10/mgmt/shared/authn/login", json=data)
>>> res.status_code
401
>>> res.json()
{'code': 401, 'message': 'X-F5-Auth-Token has expired.', 'referer': '10.10.10.200', 'restOperationId': 166493543, 'kind': ':resterrorresponse'}
>>>

The problem with the call now is the old authentication token still in the session header. It kind of overrides our intention to just create a new token with the username and password. Let’s pop the old token out and retry:

>>> session.headers.pop("X-F5-Auth-Token")
'PVVVL7CUQMNIVIDV2PERXVMUUK'
>>> session.headers
{'User-Agent': 'python-requests/2.32.5', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
>>> res = session.post("https://10.10.10.10/mgmt/shared/authn/login", json=auth_data)
>>> res.status_code
200
>>> auth_token = res.json()["token"]["token"]
>>> session.headers.update({"X-F5-Auth-Token": auth_token})
>>> session.headers
{'User-Agent': 'python-requests/2.32.5', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'X-F5-Auth-Token': 'M5TBFC5NFLK372JKNRGZRCO4XM'}
>>> token_expires = res.json()["token"]["expirationMicros"] / 1_000_000
>>> time.time() > token_expires
False
>>>

Getting data

Now that we know how to access the API, let’s do something with the BIG-IP resources.

In the UI, I’ll first create a node “Test” with address 10.10.10.31 in LTM. Then I’ll get all the nodes from the system:

>>> res = session.get("https://10.10.10.10/mgmt/tm/ltm/node")
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:ltm:node:nodecollectionstate",
"selfLink": "https://localhost/mgmt/tm/ltm/node?ver=17.1.3",
"items": [
{
"kind": "tm:ltm:node:nodestate",
"name": "Test",
"partition": "Common",
"fullPath": "/Common/Test",
"generation": 109325,
"selfLink": "https://localhost/mgmt/tm/ltm/node/~Common~Test?ver=17.1.3",
"address": "10.10.10.31",
"connectionLimit": 0,
"dynamicRatio": 1,
"ephemeral": "false",
"fqdn": {
"addressFamily": "ipv4",
"autopopulate": "disabled",
"downInterval": 5,
"interval": "3600"
},
"logging": "disabled",
"monitor": "default",
"rateLimit": "disabled",
"ratio": 1,
"session": "user-enabled",
"state": "unchecked"
}
]
}
>>>

I can also fetch one specific node only:

>>> res = session.get("https://10.10.10.10/mgmt/tm/ltm/node/Test")
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:ltm:node:nodestate",
"name": "Test",
"fullPath": "Test",
"generation": 109348,
"selfLink": "https://localhost/mgmt/tm/ltm/node/Test?ver=17.1.3",
"address": "10.10.10.31",
"connectionLimit": 0,
"dynamicRatio": 1,
"ephemeral": "false",
"fqdn": {
"addressFamily": "ipv4",
"autopopulate": "disabled",
"downInterval": 5,
"interval": "3600"
},
"logging": "disabled",
"monitor": "default",
"rateLimit": "disabled",
"ratio": 1,
"session": "user-enabled",
"state": "unchecked"
}
>>>

Modifying the data

By fiddling in the UI with the various node settings, I see that the node can be disabled by setting the session property to “user-disabled”. Let’s do it with PATCH command:

>>> res = session.patch("https://10.10.10.10/mgmt/tm/ltm/node/Test", json={"session": "user-disabled"})
>>> res
<Response [200]>
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:ltm:node:nodestate",
"name": "Test",
"fullPath": "Test",
"generation": 109392,
"selfLink": "https://localhost/mgmt/tm/ltm/node/Test?ver=17.1.3",
"address": "10.10.10.31",
"connectionLimit": 0,
"dynamicRatio": 1,
"ephemeral": "false",
"fqdn": {
"addressFamily": "ipv4",
"autopopulate": "disabled",
"downInterval": 5,
"interval": "3600"
},
"logging": "disabled",
"monitor": "default",
"rateLimit": "disabled",
"ratio": 1,
"session": "user-disabled",
"state": "unchecked"
}
>>>

Creating new resource

This time create a new node “Test2” using the API, with mostly default properties:

>>> res = session.post("https://10.10.10.10/mgmt/tm/ltm/node", json={"name": "Test2", "address": "10.10.10.32"})
>>> res
<Response [200]>
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:ltm:node:nodestate",
"name": "Test2",
"partition": "Common",
"fullPath": "/Common/Test2",
"generation": 109403,
"selfLink": "https://localhost/mgmt/tm/ltm/node/~Common~Test2?ver=17.1.3",
"address": "10.10.10.32",
"connectionLimit": 0,
"dynamicRatio": 1,
"ephemeral": "false",
"fqdn": {
"addressFamily": "ipv4",
"autopopulate": "disabled",
"downInterval": 5,
"interval": "3600"
},
"logging": "disabled",
"monitor": "default",
"rateLimit": "disabled",
"ratio": 1,
"session": "user-enabled",
"state": "unchecked"
}

Getting selected properties only

When getting list of resources, you can use $select query parameter to limit the output to only specific properties you need:

>>> res = session.get("https://10.10.10.10/mgmt/tm/ltm/node", params={"$select": "name,address"})
>>> print(json.dumps(res.json(), indent=4))
{
"kind": "tm:ltm:node:nodecollectionstate",
"selfLink": "https://localhost/mgmt/tm/ltm/node?$select=name%2Caddress&ver=17.1.3",
"items": [
{
"name": "Test",
"address": "10.10.10.31"
},
{
"name": "Test2",
"address": "10.10.10.32"
}
]
}
>>>

Deleting data

If you want to delete a resource, you can use the DELETE call:

>>> res = session.delete("https://10.10.10.10/mgmt/shared/authz/tokens/M5TBFC5NFLK372JKNRGZRCO4XM")
>>> res
<Response [200]>
>>> res = session.get("https://10.10.10.10/mgmt/shared/authz/tokens")
>>> res
<Response [401]>
>>>

Note how deleting the current token caused the next call to fail with a 401 (Unauthorized) response.

Summary

That’s about it for the mechanism. The rest of using the REST API is about doing the things that you really want to do with the devices.

Key takeaways:

  • Ensure that you use a valid token in the X-F5-Auth-Token header (and remove expired token from the headers when requesting a new token in the same session)
  • Use GET calls to fetch data
    • Use also $select query parameter if you only need to get specific properties
  • Use POST calls and dictionaries to create resources
  • Use PATCH calls and dictionaries to modify specific properties of existing resources
  • Use DELETE calls to delete resources

References

For more information (like using transactions to aggregate multiple commands into a single atomic operation):

Updated: December 14, 2025 — 13:34

Leave a Reply