f5-common-python
5 TopicsGetting Started with the f5-common-python SDK
If you have dabbled with python and iControl over the years, you might be familiar with some of my other “Getting Stared with …” articles on python libraries. I started my last, on Bigsuds, this way: I imagine the progression for you, the reader, will be something like this in the first six- or seven-hundred milliseconds after reading the title: Oh cool! Wait, what? Don’t we already have like two libraries for python? Really, a third library for python? It’s past time to update those numbers as the forth library in our python support evolution, the f5-common-python SDK, has been available since March of last year!I still love Bigsuds, but it only supports the iControl SOAP interface. The f5-common-python SDK is under continuous development in support of the iControl REST interface, and like Bigsuds, does a lot of the API heavy lifting for you so you can just focus on the logic of bending BIG-IP configuration to your will. Not all endpoints are supported yet, but please feel free to open an issue on the GitHub repo if there’s something missing you need for your project.In this article, I’ll cover the basics of installing the SDK and how to utilize the core functionality. Installing the SDK This section is going to be really short, as the SDK is uploaded to PyPI after reach release, though you can clone the GitHub project and run the development branch with latest features if you so desire. I'd recommend installing in a virtual environment to keep your system python uncluttered, but YMMV. pip install f5-sdk A simple one-liner and we're done! Moving on... Instantiating BIG-IP The first thing you’ll want to do with your shiny new toy is authenticate to the BIG-IP. You can use basic or token authentication to do so. I disable the certificate security warnings on my test boxes, but the first two lines in the sample code below are not necessary if you are using valid certificates >>> import requests >>> requests.packages.urllib3.disable_warnings() >>> from f5.bigip import ManagementRoot >>> # Basic Authentication >>> b = ManagementRoot('ltm3.test.local', 'admin', 'admin') >>> # Token Authentication >>> b = ManagementRoot('ltm3.test.local', 'admin', 'admin', token=True) >>> b.tmos_version u'12.1.0' The b object has credentials attached and various other attributes as well, such as the tmos_version attribute shown above. This is the root object you’ll use (of course you don’t have to call it b, you can call it plutoWillAlwaysBeAPlanetToMe if you want to, but that’s a lot more typing) for all the modules you might interact with on the system. Nomenclature The method mappings are tied to the tmsh and REST URL ids. Consider the tmsh command tmsh list /ltm pool . In the URL, this would be https://ip/mgmt/tm/ltm/pool. For the SDK, at the collection level the command would be b.tm.ltm.pools . It's plural here because we are signifying the collection. If there is a collection already ending in an s, like the subcollection of a pool in members, it would be addressed as members_s. This will be more clear as we work through examples in later articles, but I wanted to provide a little guidance before moving on. Working with Collections There are two types of collections (well three if you include subcollections, but we’ll cover those in a later article,) organizing collections and collections. An organizing collection is a superset of other collections. For example, the ltm or net module listing would be an organizing collection, whereas ltm/pool or net/vlan would be collections. To retrieve either type, you use the get_collection method as shown below, with abbreviated output. # The LTM Organizing Collection >>> for x in b.tm.ltm.get_collection(): ... print x ... {u'reference': {u'link': u'https://localhost/mgmt/tm/ltm/auth?ver=12.1.0'}} {u'reference': {u'link': u'https://localhost/mgmt/tm/ltm/data-group?ver=12.1.0'}} {u'reference': {u'link': u'https://localhost/mgmt/tm/ltm/dns?ver=12.1.0'}} # The Net/Vlan Collection: >>> vlans = b.tm.net.vlans.get_collection() >>> for vlan in vlans: ... print vlan.name ... vlan10 vlan102 vlan103 Working with Named Resources A named resource, like a pool, vip, or vlan, is a fully configurable object for which the CURDLE methods are supported. These methods are: create() update() refresh() delete() load() exists() Let’s work through all these methods with a pool object. >>> b.tm.ltm.pools.pool.exists(name='mypool2017', partition='Common') False >>> p1 = b.tm.ltm.pools.pool.create(name='mypool2017', partition='Common') >>> p2 = b.tm.ltm.pools.pool.load(name='mypool2017', partition='Common') >>> p1.loadBalancingMode = 'least-connections-member' >>> p1.update() >>> assert p1.loadBalancingMode == p2.loadBalancingMode Traceback (most recent call last): File "", line 1, in AssertionError >>> p2.refresh() >>> assert p1.loadBalancingMode == p2.loadBalancingMode >>> p1.delete() >>> b.tm.ltm.pools.pool.exists(name='mypool2017', partition='Common') False Notice in line 1, I am looking to see if the pool called mypool2017 exists, to which I get a return value of False. So I can go ahead and create that pool as shown in line 3. In line 4, I load the same pool so I have two local python objects (p1, p2) that reference the same BIG-IP pool (mypool2017.) In line 5, I update the load balancing algorithm from the default of round robin to least connections member. But at this point, only the local python object has been updated. To update the BIG-IP, in line 6 I apply that method to the object. Now if I assert the LB algorithm between the local p1 and p2 python objects as shown in line 7, it fails, because we have updated p1, but p2 is still as it was when I initially loaded it. Refreshing p2 as shown in line 11 will update it (the local python object, not the BIG-IP pool.) Now I assert again in line 12, and it does not fail. As this was just an exercise, I delete the new pool (could be done on p1 or p2 since they reference the same BIG-IP object) in line 13, and a quick check to see if it exists in line 14 returns false. The great thing is that even though the endpoints change from pool to virtual to rule and so on, the methods used for them do not. Next Steps This is just the tip of the iceberg! There is much more to cover, so come back for the next installment, where we’ll cover unnamed resources and commands. If you can't wait, feel free to dig into the SDK documentation.13KViews3likes70CommentsGetting started with the python SDK part 6: transactions
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: 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. 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. 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.1KViews0likes3CommentsPython SDK Cookbook: Encoding URLs and Using Proxies
Every now and then I get an inquiry if the Python SDK can do something I haven't personally used or seen a need for. In this article, I'll cover two such use cases: encoding URLs and using proxies to connect to BIG-IP. Both of these features aren’t technically supported by thef5-common-pythonSDK, but rather the underlying f5-icontrol-rest-python module that builds the requests and communicates with BIG-IP. Encoding URLs In this issue, a user was trying to submit a get request to BIG-IP with a node name of 172.16.1.5%0. The actual request with a partition of test included was (in this example using the underlying iControlRESTSession): from f5.bigip import ManagementRoot b = ManagementRoot('localhost', 'admin', 'admin', port=13443) name = '172.16.1.5%0' b.tm.ltm.nodes.node.load(name=name, partition='test') Traceback (most recent call last): ...... raise iControlUnexpectedHTTPError(error_message, response=response) iControlUnexpectedHTTPError: 400 Unexpected Error: Bad Request for uri: https://localhost:13443/mgmt/tm/ltm/node/~test~172.16.1.5%0 This resulted in a 400 error from BIG-IP because of the percent sign. The easy end-user fix (read: workaround) for this is to simply encode the node name with the urllib module before making the request: from f5.bigip import ManagementRoot import urllib name = '172.16.1.5%0' encoded_name = urllib.quote(name) b.tm.ltm.nodes.node.load(name=encoded_name, partition='test') b.tm.ltm.nodes.node.load(name=encoded_name, partition='test').raw {u'kind': u'tm:ltm:node:nodestate', u'logging': u'disabled', u'name': u'172.16.1.5%0', u'state': u'unchecked', u'generation': 1443, u'partition': u'test', ...removed for clarity..., u'monitor': u'default'} We could also fix the problem in the iControlREST module, but if this turns out to be a rare one-off where users name their objects with special characters I'll focus on bigger issues that need attention. Using a Proxy With this use case, my first response was “yeah, this is possible but it’s likely going to take some significant development time.” But when I had a little more time to investigate, I suspected that the iControlREST library already supported this with the native python requests module. But in order to confirm my suspicions, I needed a proxy on my development workstation. Enter Burp Suite Community Edition. I use this on my Mac a lot for various intercept and packet crafting and analysis purposes, but had yet to have a reason to install on my windows box where I have migrated most of my BIG-IP python-related development. To activate the proxy, I just start the executable and intercept is on by default. To use a proxy from the SDK, you just need to supply the proxies kwargwith a json blob specifying your proxy properties, for example: (#hattip Stack) pxy = { "http": "http://10.10.1.10:3128", "https": "https://10.10.1.10:1080", } Burp Suite defaults to loopback address 127.0.0.1 and port 8080 for the proxy. So with that in mind, we can build the same query as show above with url encoding from the SDK, but this time with a proxy configuration like so: proxy = { "https":"https://127.0.0.1:8080" } b.tm.ltm.nodes.node.load(name=encoded_name, partition='test', proxies=proxy).raw Here is the request Burp Suite intercepted: GET /mgmt/tm/ltm/node/~test~172.16.1.5%250 HTTP/1.1 Host: localhost:13443 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: python-requests/2.21.0 f5-icontrol-rest-python/1.3.12 Content-Type: application/json Cookie: BIGIPAuthUsernameCookie=admin; BIGIPAuthCookie=C74B069117329FE004C7840047A1DA3707965613 Authorization: Basic YWRtaW46YWRtaW4= Immediately after I click forward in Burp Suite, I get my returned json blob from the BIG-IP: {u'kind': u'tm:ltm:node:nodestate', u'logging': u'disabled', u'name': u'172.16.1.5%0', u'state': u'unchecked', u'generation': 1443, u'partition': u'test', ...removed for clarity..., u'monitor': u'default'} That's all well and good, but the keen observers among you might notice that the proxies kwarg in my example above was applied to an existing session, not at instantiation with ManagementRoot. So whereas it's helpful to know that proxy support is native in the requests module, it's not supported in f5-icontrol-rest versions through 1.3.12 and f5-sdk versions through 3.0.20. But wait! Effective today, you can upgrade your SDK to the latest version, 3.0.21, for proxy support.Assuming as I've shown above I have a local BIG-IP Virtual Edition at localhost:13443 and a proxy at 127.0.0.1:8080, here's how you activate the proxy: >>>from f5.bigip import ManagementRoot >>>proxy = {"https": "https://127.0.0.1:8080} >>>b = ManagementRoot('localhost', 'admin', 'admin', proxies=proxy) I made sure to include the proxy configuration in the session object metadata, so you can reference as needed: >>> b._meta_data['proxies'] {'https': 'https://127.0.0.1:8080'} I love learning new things! What issues have you stymied with the SDK that I can investigate or help fill in the gaps? Drop a line in the comments below.576Views0likes1CommentDebugging API calls with the python sdk
In version 3.0.10 of the iControl REST python SDK, we introduced a new feature that allows you to debug the API call tracing. In this article, I’ll show you how to use it to debug an issue with pool members. Consider the following existing pool on my BIG-IP: ltm pool testpool { members { 192.168.103.20:http { address 192.168.103.20 session monitor-enabled state up } } monitor http } You can see that there is a single pool member in this pool, 192.168.103.20:80. So when I check from the SDK to see if the pool and pool member exist, I expect the answer to be true on both counts. >>> mgmt.tm.ltm.pools.pool.exists(name='testpool') True >>> pool = mgmt.tm.ltm.pools.pool.load(name='testpool') >>> pool.members_s.members.exists(name='192.168.103.20:80') False Both should be true, but the pool member exists check is returning false. This is where we can engage the debug data to see what is going on with the requests.You can set the debug attribute to true when you instantiate BIG-IP, or enable it only when you need it. >>> mgmt = ManagementRoot('ltm3.test.local', 'admin', 'admin', debug=True) # set on instantiation >>> mgmt.debug = True # turn on after instantiation >>> mgmt.debug = False # turn off Once you enable the debug attribute, the debug_output attribute will begin appending to a list the API calls made to the BIG-IP, in the format of the curl command. Note that the API is not using curl, but the debug output is formatted this way to allow you to grab the command and run it directly from the command line. For the instantiation, pool exists check, loading the pool, and then the pool member exists check, this debug output looks like this: >>> for x in mgmt.debug_output: ... print x ... curl -k -X GET https://ltm3.test.local:443/mgmt/tm/sys/ -H 'Connection: keep-alive' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'User-Agent: python-requests/2.18.4 f5-icontrol-rest-python/1.3.8' -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' curl -k -X GET https://ltm3.test.local:443/mgmt/tm/ltm/pool/testpool -H 'Connection: keep-alive' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'User-Agent: python-requests/2.18.4 f5-icontrol-rest-python/1.3.8' -H 'Content-Type: application/json' -H 'Cookie: BIGIPAuthUsernameCookie=admin; BIGIPAuthCookie=3CF752E8B6DC27B20D42B9C2BBF152B10B400407' -H 'Authorization: Basic YWRtaW46YWRtaW4=' curl -k -X GET https://ltm3.test.local:443/mgmt/tm/ltm/pool/testpool -H 'Connection: keep-alive' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'User-Agent: python-requests/2.18.4 f5-icontrol-rest-python/1.3.8' -H 'Content-Type: application/json' -H 'Cookie: BIGIPAuthUsernameCookie=admin; BIGIPAuthCookie=3CF752E8B6DC27B20D42B9C2BBF152B10B400407' -H 'Authorization: Basic YWRtaW46YWRtaW4=' curl -k -X GET https://ltm3.test.local:443/mgmt/tm/ltm/pool/testpool/members/192.168.103.20:80 -H 'Connection: keep-alive' -H 'Accept-Encoding: gzip, deflate' -H 'Accept: */*' -H 'User-Agent: python-requests/2.18.4 f5-icontrol-rest-python/1.3.8' -H 'Content-Type: application/json' -H 'Cookie: BIGIPAuthUsernameCookie=admin; BIGIPAuthCookie=3CF752E8B6DC27B20D42B9C2BBF152B10B400407' -H 'Authorization: Basic YWRtaW46YWRtaW4=' After running the pool member curl command I get this response: {"code":404,"message":"Object not found - 192.168.103.20:80","errorStack":[],"apiError":1} Now we know why the SDK returned false, but we know this pool member exists, so what gives? If we back off and just make an API call via curl to the members collection, we can investigate the response data for clues. {"kind":"tm:ltm:pool:members:memberscollectionstate","selfLink":"https://localhost/mgmt/tm/ltm/pool/testpool/members?ver=13.1.0.5","items":[{"kind":"tm:ltm:pool:members:membersstate","name":"192.168.103.20:80","partition":"Common","fullPath":"/Common/192.168.103.20:80","generation":175,"selfLink":"https://localhost/mgmt/tm/ltm/pool/testpool/members/~Common~192.168.103.20:80?ver=13.1.0.5","address":"192.168.103.20","connectionLimit":0,"dynamicRatio":1,"ephemeral":"false","fqdn":{"autopopulate":"disabled"},"inheritProfile":"enabled","logging":"disabled","monitor":"default","priorityGroup":0,"rateLimit":"disabled","ratio":1,"session":"monitor-enabled","state":"up"}]} Notice in the selfLink field there, the pool member has the partition as part of the name of the pool member. If we update our curl command to include the partition (changing 192.168.103.20:80 to ~Common~192.168.103.20:80,) let's see if that fixes our 404 error: {"kind":"tm:ltm:pool:members:membersstate","name":"192.168.103.20:80","partition":"Common","fullPath":"/Common/192.168.103.20:80","generation":175,"selfLink":"https://localhost/mgmt/tm/ltm/pool/testpool/members/~Common~192.168.103.20:80?ver=13.1.0.5","address":"192.168.103.20","connectionLimit":0,"dynamicRatio":1,"ephemeral":"false","fqdn":{"autopopulate":"disabled"},"inheritProfile":"enabled","logging":"disabled","monitor":"default","priorityGroup":0,"rateLimit":"disabled","ratio":1,"session":"monitor-enabled","state":"up"} Woot! Now we are getting the pool member data. It turns out that the pool member call requires the partition to be submitted, even if you do not use partitions on your BIG-IP. So now, if we update our python code to include the partition parameter, it should now return true, and sure enough, that is what we get: pool.members_s.members.exists(name='192.168.103.20:80', partition='Common') True The debug tracing details are documented in the SDK documentation linked at the top of this article, happy coding!634Views0likes1CommentGetting started with the python SDK part 2: unnamed resources and commands
In the first article in this series we looked at the sdk installation steps, nomenclature, and basic BIG-IP instantiation. In this article, we’ll focus on unnamed resources and commands. Working with Unnamed Resources Unlike named resources like virtual servers and pools, an unnamed resource cannot be created or deleted, but otherwise can be configured similarly. A good example of an unnamed resource would be the BIG-IP system DNS settings. The path for this object is mgmt->tm->sys->dns. First, we need to load the object. I'll instantiate in this first example and assume it for the remaining ones. import requests from f5.bigip import ManagementRoot from pprint import pprint as pp # for pretty print of json data requests.packages.urllib3.disable_warnings() # ignore cert warnings b = ManagementRoot('ltm3.test.local', 'admin', 'admin') obj_dns = b.tm.sys.dns.load() pp(obj_dns.raw) {'_meta_data': {'allowed_commands': [], 'bigip': , 'container': , 'exclusive_attributes': [], 'icontrol_version': '', 'icr_session': , 'minimum_version': '11.5.0', 'object_has_stats': True, 'required_command_parameters': set([]), 'required_json_kind': 'tm:sys:dns:dnsstate', 'required_load_parameters': set([]), 'uri': 'https://ltm3.test.local:443/mgmt/tm/sys/dns/'}, u'description': u'configured-by-dhcp', u'kind': u'tm:sys:dns:dnsstate', u'nameServers': [u'10.10.10.1'], u'numberOfDots': 0, u'search': [u'localdomain'], u'selfLink': u'https://localhost/mgmt/tm/sys/dns?ver=12.1.0'} The metadata is added by the sdk, but the important attributes for configuration purposes here are nameServers and search. If we want to add a google cache resolver and a test.local domain to the seach field, we set the attributes appropriately as lists as shown below. obj_dns.nameServers = ['10.10.10.1', '8.8.8.8'] obj_dns.search = ['localdomain', 'test.local'] obj_dns.update() obj_dns.refresh() obj_dns.nameServers [u'10.10.10.1', u'8.8.8.8'] obj_dns.search [u'localdomain', u'test.local'] Once the attributes are set, we call the updatemethod. This will execute a refresh of the local object as well, but for demonstration purposes let’s call the refreshmethod as well so we can validate our new name servers and search list are accurate, which they are. There are named resources in the API that behave like unnamed resources, in that they cannot be created or deleted, just updated. Examples would include authentication sources like tacacs, and the management-ip. But they still need to be loaded by name, unlike unnamed resources. Working with Commands Commands are interesting as we’re not working with configuration objects, we’re executing tasks like saving or loading the configuration, or kicking off a qkview for diagnostics. The method for commands is exec_cmd. Most commands, like the tmsh utilities, use the keyword run, but for configuration it would be loador save.There is usually a keyword argument required but not always. For our first example, let's look at a simple save of the configuration. b.tm.sys.config.exec_cmd('save') That’s it! Pretty simple and straight forward. A load is the same way, unless you are merging files, in which case you need to do a little extra work. options = {} options['file'] = '/var/config/rest/downloads/myfile.txt' options['merge'] = True b.tm.sys.config.exec_cmd('load', options=[options]) I'm looking into updating the sdk to make a merge cleaner. Also note that the ability to load has been merged to development but isn't yet in an official release. Utility commands to work with files, ciphers, or dns clients are also available via the sdk. Here are a few examples: ## Checking DNS resolution from BIG-IP >>> digresults = b.tm.util.dig.exec_cmd('run', utilCmdArgs='@8.8.8.8 www.google.com') >>> digresults.commandResult u'\n; DiG 9.9.8-P4 @8.8.8.8 www.google.com\n; (1 server found)\n;; global options: +cmd\n;; Got answer:\n;; -HEADER- opcode: QUERY, status: NOERROR, id: 253\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 512\n;; QUESTION SECTION:\n;www.google.com.\t\t\tIN\tA\n\n;; ANSWER SECTION:\nwww.google.com.\t\t225\tIN\tA\t216.58.192.228\n\n;; Query time: 29 msec\n;; SERVER: 8.8.8.8#53(8.8.8.8)\n;; WHEN: Thu Aug 24 14:12:30 CDT 2017\n;; MSG SIZE rcvd: 59\n\n' > Note: data returned from utility commands isn't formatted as objects, it's just standard console output (note the newlines above) so any parsing required is up to you. In this next example, we use the python print command to format the newlines properly in displaying the data. ## Checking the Client Ciphers with a specified string >>> cciphers = b.tm.util.clientssl_ciphers.exec_cmd('run', utilCmdArgs='TLSv1+HIGH:!SSLv2:!aNULL:!eNULL:@STRENGTH') >>> print cciphers.commandResult ID SUITE BITS PROT METHOD CIPHER MAC KEYX 0: 49172 ECDHE-RSA-AES256-CBC-SHA 256 TLS1 Native AES SHA ECDHE_RSA 1: 49162 ECDHE-ECDSA-AES256-SHA 256 TLS1 Native AES SHA ECDHE_ECDSA 2: 57 DHE-RSA-AES256-SHA 256 TLS1 Native AES SHA EDH/RSA 3: 56 DHE-DSS-AES256-SHA 256 TLS1 Native AES SHA DHE/DSS 4: 58 ADH-AES256-SHA 256 TLS1 Native AES SHA ADH 5: 49167 ECDH-RSA-AES256-SHA 256 TLS1 Native AES SHA ECDH_RSA 6: 49157 ECDH-ECDSA-AES256-SHA 256 TLS1 Native AES SHA ECDH_ECDSA 7: 53 AES256-SHA 256 TLS1 Native AES SHA RSA 8: 136 DHE-RSA-CAMELLIA256-SHA 256 TLS1 Native CAMELLIA SHA EDH/RSA 9: 135 DHE-DSS-CAMELLIA256-SHA 256 TLS1 Native CAMELLIA SHA DHE/DSS 10: 132 CAMELLIA256-SHA 256 TLS1 Native CAMELLIA SHA RSA 11: 49170 ECDHE-RSA-DES-CBC3-SHA 168 TLS1 Native DES SHA ECDHE_RSA 12: 49160 ECDHE-ECDSA-DES-CBC3-SHA 168 TLS1 Native DES SHA ECDHE_ECDSA 13: 22 DHE-RSA-DES-CBC3-SHA 168 TLS1 Native DES SHA EDH/RSA 14: 27 ADH-DES-CBC3-SHA 168 TLS1 Native DES SHA ADH 15: 49165 ECDH-RSA-DES-CBC3-SHA 168 TLS1 Native DES SHA ECDH_RSA 16: 49155 ECDH-ECDSA-DES-CBC3-SHA 168 TLS1 Native DES SHA ECDH_ECDSA 17: 10 DES-CBC3-SHA 168 TLS1 Native DES SHA RSA I haven't finished adding all of the REST endpoints available yet, but any missing ones can be worked around using the bash command. For example, if you want to get the current status of the routing table, you can pass netstat -rn via the bash command like this: >>> rtstat = b.tm.util.bash.exec_cmd('run', utilCmdArgs='-c "netstat -rn"') >>> print rtstat.commandResult Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 127.1.1.0 0.0.0.0 255.255.255.0 U 0 0 0 tmm 192.168.102.0 0.0.0.0 255.255.255.0 U 0 0 0 vlan102 192.168.103.0 0.0.0.0 255.255.255.0 U 0 0 0 vlan103 10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 10.10.10.0 0.0.0.0 255.255.255.0 U 0 0 0 vlan10 127.7.0.0 127.1.1.253 255.255.0.0 UG 0 0 0 tmm 127.20.0.0 0.0.0.0 255.255.0.0 U 0 0 0 tmm_bp 0.0.0.0 10.10.10.1 0.0.0.0 UG 0 0 0 vlan10 Note the extra quotes within the utilCmdArgs string around netstat -rn. These are necessary to pass the argument to the command you want bash to run, rather than to bash itself. Join me next time where we will explore transactions!1.1KViews0likes0Comments