iControlREST
1095 TopicsPython module to post and retrieve IControl Rest JSON objects for AVR statistics
Problem this snippet solves: This module simplifies making Python dictionary objects that are converted to IControl rest AVR JSON objects. It also handles making AVR requests and retrieving results as well allowing multiple AVR requests to be queued, posted and retrieved. It also has some basis type checking for the elements of a AVR request. This module requires Bigip 12.1 on the target that statistics are retrieved. How to use this snippet: The main class is rest_avr.avr_req. It is a dictionary class that maps directly to an IControl Rest AVR JSON request as translated by json.dumps. Each dictionary element is an object derived from a customer class for each part of the request. The element classes have add() and clear() functions. if the element class only allows one entry the add() function will replace the existing entry, otherwise it will append the entry to the request element. The rest_avr.avr_req class also has functions to populate the HTTP host and authentication values for the target system. rest_avr.avr_req.post_and_response returns the Python representation of the JSON result of the query. rest_avr.avr_req.add_to_queue() adds the currently constructed request to a queue of requests to post. rest_avr.avr_req.post_and_response_queue() returns a python list of results of queued queries. The following code sample constructs, posts and returns results for an AVR statistics request for specific DNS records and a specificrecord type, then queues multiple quests and posts and returns results. #!/usr/bin/python import json import sys import time import rest_avr #print rest_avr.ShowAVRJsonApi #Populate the url avr_dns_req=rest_avr.avr_req() avr_dns_req.auth('admin','admin') avr_dns_req.url_base('10.10.2.113','dns') #Populate the json object avr_dns_req['analyticsModule'].add('dns') avr_dns_req['reportFeatures'].add('time-aggregated') avr_dns_req['entityFilters'].add('domain-name', 'OPERATOR_TYPE_EQUAL', ['test2.test1.com','test1.test1.com']) avr_dns_req['entityFilters'].add('query-type', 'OPERATOR_TYPE_EQUAL', ['a']) avr_dns_req['viewMetrics'].add('packets') avr_dns_req['viewDimensions'].add('domain-name') avr_dns_req['metricFilters'].add('packets', 'OPERATOR_TYPE_GREATER_THAN', 0) avr_dns_req['sortByMetrics'].add('packets', 'ascending') avr_dns_req['pagination'].add(20, 0) avr_dns_req['timeRange'].add(1461778251000000, None) #Post and retrieve results. result_py=avr_dns_req.post_and_response() if result_py != None: print ('\n' + result_py['results']['timeAggregated'][0]['dimensions'][0]['value'] + " " + result_py['results']['timeAggregated'][0]['metricValues'][0]['value'] + '\n') else: print result_py.error_layer print result_py.error_code print result_py.error_text # Now add multiple requests to a queue avr_dns_req.add_to_queue() avr_dns_req['entityFilters'].clear() avr_dns_req['entityFilters'].add('query-type', 'OPERATOR_TYPE_EQUAL', ['aaaa']) avr_dns_req.add_to_queue() #post and retrieve queued results result_py_q=avr_dns_req.post_and_response_queue() for result_py in result_py_q: if result_py != None: print ('\n' + result_py['results']['timeAggregated'][0]['dimensions'][0]['value'] + " " + result_py['results']['timeAggregated'][0]['metricValues'][0]['value'] + '\n') else: print result_py.error_layer print result_py.error_code print result_py.error_text Code : """ rest_avr provides a python interface to Bigip AVR statistics using the REST API. The main Python rest_avr.avr_req object is a Python dictionary that maps to a JSON object that can be processed with the json.dumps() function An IControl Rest AVR JSON request and response can be initiated with avr_req.post_and_response The simple description of the API can is available at avr_req.ShowJsonApi() Each of these modules has a method to add single or multiple elements as appropriate to the specific module. Once these elements are are populated a RestAPI request can be made with results returned as a python representation. avr_req.auth(user, passw) avr_req.url_base(host, module) avr_req['analyticsModule'].add(module) avr_req['analyticsModule'].clear() avr_req['reportFeatures'].add(metric_name, predicate, value) avr_req['reportFeatures'].clear() avr_req['entityFilters'].add(dimension_name, predicate, values) avr_req['entityFilters'].clear() avr_req['viewMetrics'].add(metric_name) avr_req['viewMetrics'].clear() avr_req['viewDimensions'].add(metric_name, order) avr_req['viewDimensions'].clear() avr_req['metricFilters'].add(metric_name, predicate, valu) avr_req['metricFilters'].clear() avr_req['sortByMetrics'].add(metric_name, orde) avr_req['sortByMetrics'].clear() avr_req['pagination'].add(num_results, skip_result) avr_req['pagination'].clear() avr_req['timeRange'].add(t_from, t_to) avr_req['timeRange'].clear() After a request in constructed a REST API call is initiated with initiated with: avr_req.post_and_response() The response is a python dictionary data structure of the results as processed by json.loads """ from copy import deepcopy import requests import json import sys import time import warnings __author__ = 'Mark Lloyd' __version__ = '1.0' # 05/24/2016 import json import requests import time class BadDictElement(Exception): def __init__(self, key, value, expl): Exception.__init__(self, '{0} {1} {2} '.format(key, value, expl)) class BadTime(Exception): def __init__(self, variable, value): Exception.__init__(self, '{0} {1} should be 16 char decimal in microseconds '.format('a', 'b')) class RequestFailure(Exception): def __init__(self, key, value): Exception.__init__(self, '{0} {1} '.format(key, value)) class analyticsModule(str): """ This class is tied to the structure of the parent class. parent() get's the parent object so we can make the string pseudo-mutable. accessed from within an avr request ['analyticsModule'].add(module) Adds a single string to analyticsModule element . If one exists it is replaced. ['analyticsModule'].clear() Send a null value to the analyticsModule element. See rest_avr.ShowAVRJsonApi for more details """ def parent(self, parent): self.parent = parent def add(self, module): """ avr_req.['analyticsModule'].add(module) Adds a single string to analyticsModule element . If one already exists it is replaced. This should be the same as the module string in avr_req.url_base. """ self.parent['analyticsModule'] = analyticsModule(module) self.parent['analyticsModule'].parent = self.parent def clear(self): """ avr_req.['analyticsModule'].add(module) replaces the analyticsModule mddule with a null string """ self.parent['analyticsModule'] = analyticsModule('') self.parent['analyticsModule'].parent = self.parent class metricFilters(list): """ avr_req.['metricFilters'].add(metric_name, predicate, value) metric name is a string, value is an integer Valid predicates strings are ['OPERATOR_TYPE_EQUAL', 'OPERATOR_TYPE_NOT_EQUAL', 'OPERATOR_TYPE_GREATER_THAN', OPERATOR_TYPE_LOWER_THAN','OPERATOR_TYPE_GREATER_THAN_OR_EQUAL', 'OPERATOR_TYPE_LOWER_THAN_OR_EQUAL']) avr_req['metricFilters'].clear() Clears metricFilters elements See rest_avr.ShowAVRJsonApi for more details. """ def __init__(self): self.append([]) self.valid_metric_predicate = ( ['OPERATOR_TYPE_EQUAL', 'OPERATOR_TYPE_NOT_EQUAL', 'OPERATOR_TYPE_GREATER_THAN', 'OPERATOR_TYPE_LOWER_THAN', 'OPERATOR_TYPE_GREATER_THAN_OR_EQUAL', 'OPERATOR_TYPE_LOWER_THAN_OR_EQUAL']) def add(self, metric_name, predicate, value): """ avr_req.['metricFilters'].add(metric_name, predicate, value) metric name is a string, value is an integer Valid predicates strings are ['OPERATOR_TYPE_EQUAL', 'OPERATOR_TYPE_NOT_EQUAL', 'OPERATOR_TYPE_GREATER_THAN', OPERATOR_TYPE_LOWER_THAN','OPERATOR_TYPE_GREATER_THAN_OR_EQUAL', 'OPERATOR_TYPE_LOWER_THAN_OR_EQUAL'] """ if type(value) is not int: raise BadDictElement(metric_name, value, 'value should be integer') if predicate in self.valid_metric_predicate: # first check if it is already there for metric in self[0]: if metric['metricName'] == metric_name: metric['predicate'] = predicate metric['value'] = value return 0 # if it is not there then just add it. self[0].append({'metricName': metric_name, 'predicate': predicate, 'value': value}) else: raise BadDictElement(metric_name, predicate, 'invalid predicate') def clear(self): """ avr_req['metricFilters'].clear() Clears metricFilters elements """ del self[0][:] class entityFilters(list): """ avr_req.['entityFilters'].add(dimension_name, predicate, values): All values are strings valid predicate is 'OPERATOR_TYPE_EQUAL' ['entityFilters'].clear() Clears the entityFilters element See rest_avr.ShowJsonApi for more details """ def __init__(self): self.append([]) def add(self, dimension_name, predicate, values): """ avr_req.['entityFilters'].add(dimension_name, predicate, values): All values are strings valid predicate is 'OPERATOR_TYPE_EQUAL' """ if predicate is 'OPERATOR_TYPE_EQUAL': # then loop throuth to see if the dimenson name already exists, if so replace for entity in self[0]: if entity['dimensionName'] == dimension_name: entity['predicate'] = predicate entity['values'] = values return 0 # if it is not there then just add it. self[0].append({'dimensionName': dimension_name, 'predicate': predicate, 'values': values}) else: raise BadDictElement(dimension_name, predicate, 'predicate must be OPERATOR_TYPE_EQUAL') def clear(self): """ ['entityFilters'].clear() Clears the entityFilters element """ del self[0][:] class reportFeatures(list): """ avr_req.['reportFeatures'].add( feature) adds report feature string. Multiple features are permitted. ['reportFeatures'].clear() Clears the analyticsModule element. See rest_avr.ShowAVRJsonApi for more details. """ def add(self, feature): """ avr_req.['reportFeatures'].add( feature) adds report feature string. Multiple features are permitted .""" if feature not in self: self.append(feature) def clear(self): """ ['reportFeatures'].clear() Clears the entityFilters element """ del self[:] class sortByMetrics(list): """ avr_req.['sortByMetrics'].add(metric_name, order) valid order names are 'ascending' and 'descending' sortByMetrics is optional in an AVR request. avr_req['sortByMetrics'].clear() Clears the sortByMetrics element. See rest_avr.ShowAVRJsonApi for more details. """ def __init__(self): self.metric_list = [] def add(self, metric_name, order): if metric_name not in self.metric_list: self.append({'metricName': metric_name, 'order': order}) self.metric_list.append(metric_name) def clear(self): """ ['sortByMetrics'].clear() Clears the sortByMetrics element """ del self[:] del self.metric_list[:] class viewDimensions(list): """ avr_req.['viewDimensions'].add(dimension_name): adds view dimension, only one dimension is allowed add will replace element if it already exists avr_req['viewDimensions'].clear() Clears the viewDimensions element. See rest_avr.ShowAVRJsonApi for more details. """ def __init__(self): self.append([]) self[0] = {} def add(self, dimension_name): """ avr_req.['viewDimensions'].add(dimension_name): adds view dimension string, only one dimension is allowed add will replace element if it already exists """ self[0]['dimensionName'] = dimension_name def clear(self, dimension_name): """ ['viewDimensions'].clear() Clears the viewDimensions element """ del self[0][:] class viewMetrics(list): """ avr_req.['viewMetrics'].add(metric_name): appends metric_name string to list. The specification allows multiple view metric elements avr_req['viewMetrics'].clear() Clears the viewMetrics elements See rest_avr.ShowAVRJsonApi for more details. """ def __init__(self): self.metric_list = [] def add(self, metric_name): """ avr_req.['viewMetrics'].add(metric_name): appends metric_name string to list. The specification allows multiple viewMetric elements """ if metric_name not in self.metric_list: self.append({'metricName': metric_name}) self.metric_list.append(metric_name) def clear(self): """ ['viewMetrics'].clear() Clears the viewMetrics elements """ del self[:] del self.metric_list[:] class timeRange(dict): """ avr_req.['timeRange'].add( t_from, t_to) both values are 16 digit numeric value in microseconds of unix/linux time. t_to is optional and can be replace by None timeRange is an optional. avr_req['timeRange'].clear() Clears the timeRange elements See rest_avr.ShowAVRJsonApi for more details. """ def add(self, t_from, t_to): """ avr_req.['timeRange'].add( t_from, t_to) both values are 16 digit numeric value in microseconds of unix/linux time. t_to is optional and can be replace by None timeRange is optional. """ if type(t_from) is long and len(str(t_from)) == 16: self['from'] = t_from else: raise BadTime(t_from + " is 16 digit numeric value in microseconds") if t_to != '' and t_to != 0 and t_to != None: if type(t_to) is long and len(str(t_from)) == 16: self['to'] = t_to else: raise BadTime(t_to + " is 16 digit numeric value in microseconds") else: if 'to' in self.keys(): del self['to'] def clear(self): """ ['timeRange'].clear() Clears the timeRange element """ del self[:] class pagination(dict): """ avr_req.['pagination'].add(num_results, skip_results) both are integer values. avr_req['pagination'].clear() Clears the pagination elements See rest_avr.ShowAVRJsonApi for more details. """ def add(self, num_results, skip_results): """ avr_req.['pagination'].add(num_results, skip_results) both arguments are integers. """ if type(num_results) is int: self['numberOfResults'] = num_results else: raise BadDictElement('number of Results ', num_results, 'must be integer') if type(skip_results) is int: self['skipResults'] = skip_results else: raise BadDictElement('skipResults ', skip_results, 'must be integer') def clear(self): """ ['pagination'].clear() Clears the pagination element """ del self[:] class avr_resp(dict): """ python response error is applicable. """ def __init__(self): self.error_layer = None self.error_code = None self.error_text = None class avr_req(dict): """ The main class for rest_avr. avr_req contains a dictionary that maps to the elements of a Icontrol REST AVR request along with capability of posting that request and receiving a response. The dictionary values are object instances of python classes that correspond to the the JSON values of the object's name/value pair. Each value has two public methods: avr_req.['objectName']add(): adds an element to the appropriate object with type checking. If an element allows more then one instance the add function will append the element If an element allows only one instance the add function will replace the element avr_req.['objectName'].clear()r: clears all elements in the object. printing rest_avr.ShowAVRJsonApi provides documentation for the AVR JASON elements. Further documentation is available on devcentral.f5.com To post an AVR Rest request there are two functions to populate the HTTP/HTTPS request. avr_req.auth(user, passw): provides the username and password avr_req.url_base(host, module) provides the host and the bigip module AVR queries to construct the URL to make the request. Then to post the request and return results in a python representation of the JSON response. avr_req.post_and_response() """ def __init__(self): self['analyticsModule'] = analyticsModule() self['analyticsModule'].parent = self self['pagination'] = pagination() self['metricFilters'] = metricFilters() self['entityFilters'] = entityFilters() self['reportFeatures'] = reportFeatures() self['sortByMetrics'] = sortByMetrics() self['viewDimensions'] = viewDimensions() self['viewMetrics'] = viewMetrics() self['timeRange'] = timeRange() self.avr_session = requests.session() self.avr_session.verify = False self.avr_session.headers.update({'Content-Type': 'application/json'}) # for multiple queued request handling. self.req_queue = [] self.generate_id = None self.done = None self.result = None self.num_requests = 0 self.res_queue = [] def post_and_response(self): """ returns a python representation of the json response to the request. failure returns array ['ERROR','component',error] """ warnings.filterwarnings("ignore") self.generate_request = self.avr_session.post(self.req_url_base + "/generate-report/", data=json.dumps(self)) self.generate_request_py = json.loads(self.generate_request.text) self.result_guid = self.generate_request_py['id'] self.results_status_url = self.req_url_base + "/generate-report/" + self.result_guid + "/?$select=status,reportResultsLink" self.results_url = self.req_url_base + "/report-results/" + self.result_guid self.sleeptime = .5 for i in range(5): time.sleep(self.sleeptime) self.sleeptime *= 2 # double backoff period each time. self.status_results_json = self.avr_session.get(self.results_status_url) self.status_results = json.loads(self.status_results_json.text) if self.status_results['status'] == 'FAILED': self.result = avr_resp() self.result_error_layer = 'REST' self.result_error_code = self.status_results['status'] self.result.error_text = self.status_results if self.status_results['status'] == 'FINISHED': self.raw_results_url = self.status_results['reportResultsLink'] self.results_url = self.raw_results_url.replace('localhost', self.host_name) self.results = self.avr_session.get(self.results_url) if self.results.status_code == 200: self.result = avr_resp() self.result.update(json.loads(self.results.text)) return self.result else: self.result = avr_resp() self.result.error_layer = 'HTTP' self.result.error_code = self.results.status_code self.result.error_text = self.results return self.result else: continue self.result = avr_resp() self.result.error_layer = 'REST_AVR' self.result.error_code = '408' self.result.error_text = 'TIMEOUT' def auth(self, user, passw): """ avr_req.auth(user, passw): username and password """ self.avr_session.auth = (user, passw) def url_base(self, host, module): """ avr_req.url_base(host, module) host and bigip module AVR queries to construct the URL to make the request. """ self.host_name = host self.req_url_base = 'https://%s/mgmt/tm/analytics/%s' % (host, module) self.module_py = {'analyticsModule': module} def add_to_queue(self): "adds request as currently constructed to queue" self.req_queue.append(deepcopy(self)) def clear_queue(self): """" clears request queue """ del self.req_queue[:] def post_and_response_queue(self): """ posts and sends response to from queue of requests. """ warnings.filterwarnings("ignore") for req in self.req_queue: req.generate_request = req.avr_session.post(req.req_url_base + "/generate-report/", data=json.dumps(req)) req.generate_request_py = json.loads(req.generate_request.text) req.generate_id = (req.generate_request_py['id']) req.results_status_url = self.req_url_base + "/generate-report/" + req.generate_id + "/?$select=status,reportResultsLink" self.sleeptime = .5 self.num_requests = len(self.req_queue) for i in range(5): for req in self.req_queue: if req.done is None: time.sleep(self.sleeptime) self.sleeptime *= 2 # double backoff period each time. req.status_results_json = req.avr_session.get(req.results_status_url) req.status_results = json.loads(req.status_results_json.text) if req.status_results['status'] == 'FAILED': req.result = avr_resp() req.result_error['layer'] = 'REST' req.result_error['error'] = req.status_results['status'] req.result_error['text'] = req.status_results if req.status_results['status'] == 'FINISHED': req.raw_results_url = req.status_results['reportResultsLink'] req.results_url = req.raw_results_url.replace('localhost', self.host_name) req.results = self.avr_session.get(req.results_url) if req.results.status_code == 200: req.result = avr_resp() req.result.update(json.loads(req.results.text)) req.done = True self.res_queue.append(req.result) self.num_requests -= 1 else: req.result = avr_resp() req.result_error.layer = 'HTTP' req.result_error.code = req.results.status_code req.result_error.text = req.results self.res_queue.append(req.result) if i == 5: if req.result == False: req.result = avr_resp() req.result.error_layer = 'REST_AVR' req.result.error_error = '408' req.result.error_text = 'TIMEOUT' if self.num_requests == 0: break return self.res_queue ShowAVRJsonApi = """ reportFeatures -------------- Specifies the kind of information that appears in a response from AVR. You may specify one or more of the following values: existing-entities time-aggregated time-series entities-count viewDimensions -------------- Specifies the dimensions for which to calculate a report, such as: {"dimensionName": "domain-name"} You may only specify a single dimension. You may omit this field in a report generation request. viewMetrics ----------- Specifies the list of metrics by which to sort results, such as: { "metricName": "average-tps" }, { "metricName": "transactions" } If you specify either time-aggregated or time-series features, you must specify one metric in a report generation request. sortByMetrics -------------- Specifies the list of metrics to sort by, such as: [{ metricName: "average-tps", order:"descending" } ] Valid values are ascending and descending. Sorting only applies to the time-aggregated feature. You do not need to specify this field in a report generation request. timeRange --------- Specifies the time range, in microseconds, for which to calculate a report, such as: {"from": 1410420888000000, "to": 1410424488000000 } You do not need to specify this field in a report generation request. entityFilters ============= Specifies the entities and values for which to calculate a report. You can specify a single entity with a second level of dimension filters that describe an aspect of the entity. If you specify multiple entity types, the results include only the entities that match all of the criteria. You do not need to specify this field in a report generation request. The following snippet contains two entities with corresponding values: [[{ "dimensionName" : "virtual", "predicate": "OPERATOR_TYPE_EQUAL", "values : ["phpAuction_VS_1"] }, { "dimensionName : "response-code", "predicate": "OPERATOR_TYPE_EQUAL", "values" : ["200"] } ]] metricFilters ------------- Specifies the metric filters for which to calculate a report, such as: [{ "metricName": "transactions", "predicate" : metricFilters "OPERATOR_TYPE_GREATER_THAN" "value": 100 }] You do not need to specify this field in a report generation request. For the existing-entities feature, AVR supports the OPERATOR_TYPE_LIKE predicate. AVR also supports the following predicates: OPERATOR_TYPE_EQUAL OPERATOR_TYPE_NOT_EQUAL OPERATOR_TYPE_GREATER_THAN OPERATOR_TYPE_LOWER_THAN OPERATOR_TYPE_GREATER_THAN_OR_EQUAL OPERATOR_TYPE_LOWER_THAN_OR_EQUAL pagination ---------- Specifies the number of results to return, and the number of results to skip, such as: { numberOfResults : 10, skipResults : 10} To see the second set of ten results, use the example shown here. AVR does not implement the OData query parameters top or skip. In order to see a specific set of results, you must set the number of results to return and then determine how many results to skip. You do not need to specify this field in a report generation request. """ Tested this on version: 12.0362Views0likes1CommentiControl REST Cookbook - Virtual Server (ltm virtual)
This cookbook lists selected ready-to-use iControl REST curl commands for virtual-server related resources. Each recipe consists of the curl command, it's tmsh equivalent, and sample output. In this cookbook, the following curl options are used. Option Meaning ______________________________________________________________________________________ -s Suppress progress meter. Handy when you want to pipe the output. ______________________________________________________________________________________ -k Allows "insecure" SSL connections. ______________________________________________________________________________________ -u Specify user ID and password. For the start, you should use the "admin" account that you normally use to access the Configuration Utility. When you specify the password at the same time, concatenate with ":". e.g., admin:admin. ______________________________________________________________________________________ -X <method> Specify the HTTP method. When omitted, the default is GET. In the REST framework, POST means create (tmsh create), PATCH means overwriting the existing resource with the data sent (tmsh modify), and PATCH is for merging (ditto). ______________________________________________________________________________________ -H <Header> Specify the request header. When you send (POST, PATCH, PUT) data, you need to tell the server that the data is in JSON format. i.e., -H "Content-Type: application/json. ______________________________________________________________________________________ -d 'data' The JSON data to send. Note that you need to quote the entire json blob, and each "name":"value" pairs must be quoted. When you have nested quotes, make sure you escape (\) them. Get information of the virtual <vs> tmsh list ltm <vs> curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs> Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', fullPath: 'vs', generation: 1109, selfLink: 'https://localhost/mgmt/tm/ltm/virtual/vs?ver=12.1.0', addressStatus: 'yes', autoLasthop: 'default', cmpEnabled: 'yes', connectionLimit: 0, description: 'TestData', destination: '/Common/192.168.184.226:80', enabled: true, gtmScore: 0, ipProtocol: 'tcp', mask: '255.255.255.255', mirror: 'disabled', mobileAppTunnel: 'disabled', nat64: 'disabled', pool: '/Common/vs-pool', poolReference: { link: 'https://localhost/mgmt/tm/ltm/pool/~Common~vs-pool?ver=12.1.0' }, rateLimit: 'disabled', rateLimitDstMask: 0, rateLimitMode: 'object', rateLimitSrcMask: 0, serviceDownImmediateAction: 'none', source: '0.0.0.0/0', sourceAddressTranslation: { type: 'automap' }, sourcePort: 'preserve', synCookieStatus: 'not-activated', translateAddress: 'enabled', translatePort: 'enabled', vlansDisabled: true, vsIndex: 4, rules: [ '/Common/irule' ], rulesReference: [ { link: 'https://localhost/mgmt/tm/ltm/rule/~Common~iRuleTest?ver=12.1.0' } ], policiesReference: { link: 'https://localhost/mgmt/tm/ltm/virtual/~Common~vs/policies?ver=12.1.0', isSubcollection: true }, profilesReference: { link: 'https://localhost/mgmt/tm/ltm/virtual/~Common~vs/profiles?ver=12.1.0', isSubcollection: true } } Get only specfic field of the virtual <vs> The naming convension for the parameters is slightly different from the ones on tmsh, so look for the familiar names in the GET response above. The example below queris the Default Pool (pool). tmsh list ltm <vs> pool curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs>?options=pool Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', fullPath: 'vs', generation: 1, selfLink: 'https://localhost/mgmt/tm/ltm/virtual/vs?options=pool&ver=12.1.1', pool: '/Common/vs-pool', poolReference: { link: 'https://localhost/mgmt/tm/ltm/pool/~Common~vs-pool?ver=12.1.1' } } Get all the information of the virtual <vs> Unlike the tmsh equivalent, iControl REST GET does not return the configuration information of the attached policies and profiles. To see them, use expandSubcollections tmsh list ltm <vs> curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs>?expandSubcollections=true Sample Output { "addressStatus": "yes", "autoLasthop": "default", "cmpEnabled": "yes", "connectionLimit": 0, "destination": "/Common/192.168.184.240:80", "enabled": true, "fullPath": "vs", "generation": 291, "gtmScore": 0, "ipProtocol": "tcp", "kind": "tm:ltm:virtual:virtualstate", "mask": "255.255.255.255", "mirror": "disabled", "mobileAppTunnel": "disabled", "name": "vs", "nat64": "disabled", "policiesReference": { "isSubcollection": true, "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~vs/policies?ver=13.1.0" }, "pool": "/Common/CentOS-all80", "poolReference": { "link": "https://localhost/mgmt/tm/ltm/pool/~Common~CentOS-all80?ver=13.1.0" }, "profilesReference": { "isSubcollection": true, "items": [ { "context": "all", "fullPath": "/Common/http", "generation": 291, "kind": "tm:ltm:virtual:profiles:profilesstate", "name": "http", "nameReference": { "link": "https://localhost/mgmt/tm/ltm/profile/http/~Common~http?ver=13.1.0" }, "partition": "Common", "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~vs/profiles/~Common~http?ver=13.1.0" }, { "context": "all", "fullPath": "/Common/tcp", "generation": 287, "kind": "tm:ltm:virtual:profiles:profilesstate", "name": "tcp", "nameReference": { "link": "https://localhost/mgmt/tm/ltm/profile/tcp/~Common~tcp?ver=13.1.0" }, "partition": "Common", "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~vs/profiles/~Common~tcp?ver=13.1.0" } ], "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~vs/profiles?ver=13.1.0" }, "rateLimit": "disabled", "rateLimitDstMask": 0, "rateLimitMode": "object", "rateLimitSrcMask": 0, "selfLink": "https://localhost/mgmt/tm/ltm/virtual/vs?expandSubcollections=true&ver=13.1.0", "serviceDownImmediateAction": "none", "source": "0.0.0.0/0", "sourceAddressTranslation": { "type": "automap" }, "sourcePort": "preserve", "synCookieStatus": "not-activated", "translateAddress": "enabled", "translatePort": "enabled", "vlansDisabled": true, "vsIndex": 2 } Get stats of the virtual <vs> tmsh show ltm <vs> curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs>/stats Sample Output { kind: 'tm:ltm:virtual:virtualstats', generation: 1109, selfLink: 'https://localhost/mgmt/tm/ltm/virtual/vs/stats?ver=12.1.0', entries: { 'https://localhost/mgmt/tm/ltm/virtual/vs/~Common~vs/stats': { nestedStats: { kind: 'tm:ltm:virtual:virtualstats', selfLink: 'https://localhost/mgmt/tm/ltm/virtual/vs/~Common~vs/stats?ver=12.1.0', entries: { 'clientside.bitsIn': { value: 12880 }, 'clientside.bitsOut': { value: 34592 }, 'clientside.curConns': { value: 0 }, 'clientside.evictedConns': { value: 0 }, 'clientside.maxConns': { value: 2 }, 'clientside.pktsIn': { value: 26 }, 'clientside.pktsOut': { value: 26 }, 'clientside.slowKilled': { value: 0 }, 'clientside.totConns': { value: 6 }, cmpEnableMode: { description: 'all-cpus' }, cmpEnabled: { description: 'enabled' }, csMaxConnDur: { value: 37 }, csMeanConnDur: { value: 29 }, csMinConnDur: { value: 17 }, destination: { description: '192.168.184.226:80' }, 'ephemeral.bitsIn': { value: 0 }, 'ephemeral.bitsOut': { value: 0 }, 'ephemeral.curConns': { value: 0 }, 'ephemeral.evictedConns': { value: 0 }, 'ephemeral.maxConns': { value: 0 }, 'ephemeral.pktsIn': { value: 0 }, 'ephemeral.pktsOut': { value: 0 }, 'ephemeral.slowKilled': { value: 0 }, 'ephemeral.totConns': { value: 0 }, fiveMinAvgUsageRatio: { value: 0 }, fiveSecAvgUsageRatio: { value: 0 }, tmName: { description: '/Common/vs' }, oneMinAvgUsageRatio: { value: 0 }, 'status.availabilityState': { description: 'available' }, 'status.enabledState': { description: 'enabled' }, 'status.statusReason': { description: 'The virtual server is available' }, syncookieStatus: { description: 'not-activated' }, 'syncookie.accepts': { value: 0 }, 'syncookie.hwAccepts': { value: 0 }, 'syncookie.hwSyncookies': { value: 0 }, 'syncookie.hwsyncookieInstance': { value: 0 }, 'syncookie.rejects': { value: 0 }, 'syncookie.swsyncookieInstance': { value: 0 }, 'syncookie.syncacheCurr': { value: 0 }, 'syncookie.syncacheOver': { value: 0 }, 'syncookie.syncookies': { value: 0 }, totRequests: { value: 4 } } } } } } Change one of the configuration options of the virtual <vs> The command below changes the Description field of the virtual ("description" in tmsh and iControl REST). tmsh modify ltm virtual <vs> description "Hello World!" curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs> \ -X PATCH -H "Content-Type: application/json" \ -d '{"description": "Hello World!"}' Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', ... description: 'Hello World!', <==== Changed. ... } Disable the virtual <vs> The command syntax is same as above: To change the state of a virtual from "enabled" to "disabled", send "disabled":true. For enabling the virtual, use "enabled":true. Note that the Boolean type true/false does not require quotations. tmsh modify ltm virtual <vs> disabled curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs> \ -X PATCH -H "Content-Type: application/json" \ -d '{"disabled": true}' \ Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', fullPath: 'vs', ... disabled: true, <== Changed ... } Add another iRule to <vs> When the virtual has iRules already attached, you need to send the existing ones too along with the additional one. For example, to add /Common/testRule1 to the virtual with /Common/testRule1, specify both in an array (square brackets). Note that the /Common/testRule2 iRule object should be already created. tmsh modify ltm virtual <vs> rules {testRule1 testRule2} curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual/<vs> \ -X PATCH -H "Content-Type: application/json" \ -d '{"rules": ["/Common/testRule1", "/Common/testRule2"] }' Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', fullPath: 'vs', ... rules: [ '/Common/test1', '/Common/test2' ], <== Changed rulesReference: [ { link: 'https://localhost/mgmt/tm/ltm/rule/~Common~test1?ver=12.1.1' }, { link: 'https://localhost/mgmt/tm/ltm/rule/~Common~test2?ver=12.1.1' } ], ... } Create a new virtual <vs> You can create a skeleton virtual by specifying only Destination Address and Mask. The remaining parameters such as profiles are set to default. You can later modify the parameters by PATCH-ing. tmsh create ltm virtual <vs> destination <ip:port> mask <ip> curl -sku admin:admin -X POST -H "Content-Type: application/json" \ -d '{"name": "vs", "destination":"192.168.184.230:80", "mask":"255.255.255.255"}' \ https://<host>/mgmt/tm/ltm/virtual Sample Output { kind: 'tm:ltm:virtual:virtualstate', name: 'vs', partition: 'Common', fullPath: '/Common/vs', ... destination: '/Common/192.168.184.230:80', <== Created ... mask: '255.255.255.255', <== Created ... } Create a new virtual <vs> with a lot of parameters You can specify all the essential parameters upon creation. This example creates a new virtual with pool, default persistence profile, profiles, iRule, and source address translation. The call fails if any of the parameters conflicts. For example, you cannot specify "Cookie Persistence" without specifying appropriate profiles. If you do not specify any profile, it falls back to the default fastL4 , which is not compatible with Cookie Persistence. tmsh create ltm virtual <vs> destination <ip:port> mask <ip> pool <pool> persist replace-all-with { cookie } profiles add { tcp http clientssl } rules { <rule> } source-address-translation { type automap } curl -sku admin:admin https://<host>/mgmt/tm/ltm/virtual -H "Content-Type: application/json" -X POST -d '{"name": "vs", \ "destination": "10.10.10.10:10", \ "mask": "255.255.255.255", \ "pool": "CentOS-all80", \ "persist": [ {"name": "cookie"} ], \ "profilesReference": {"items": [ {"context": "all", "name": "http"}, {"context": "all", "name": "tcp"}, {"context": "clientside", "name": "clientssl"}] }, \ "rules": [ "ShowVersion" ], \ "sourceAddressTranslation": {"type": "automap"} }' Sample Output { "addressStatus": "yes", "autoLasthop": "default", "cmpEnabled": "yes", "connectionLimit": 0, "destination": "/Common/10.10.10.10:10", "enabled": true, "fullPath": "/Common/test", "generation": 592, "gtmScore": 0, "ipProtocol": "tcp", "kind": "tm:ltm:virtual:virtualstate", "mask": "255.255.255.255", "mirror": "disabled", "mobileAppTunnel": "disabled", "name": "vs", "nat64": "disabled", "partition": "Common", "persist": [ { "name": "cookie", "nameReference": { "link": "https://localhost/mgmt/tm/ltm/persistence/cookie/~Common~cookie?ver=13.1.0" }, "partition": "Common", "tmDefault": "yes" } ], "policiesReference": { "isSubcollection": true, "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~test/policies?ver=13.1.0" }, "pool": "/Common/CentOS-all80", "poolReference": { "link": "https://localhost/mgmt/tm/ltm/pool/~Common~CentOS-all80?ver=13.1.0" }, "profilesReference": { "isSubcollection": true, "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~test/profiles?ver=13.1.0" }, "rateLimit": "disabled", "rateLimitDstMask": 0, "rateLimitMode": "object", "rateLimitSrcMask": 0, "rules": [ "/Common/ShowVersion" ], "rulesReference": [ { "link": "https://localhost/mgmt/tm/ltm/rule/~Common~ShowVersion?ver=13.1.0" } ], "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~test?ver=13.1.0", "serviceDownImmediateAction": "none", "source": "0.0.0.0/0", "sourceAddressTranslation": { "type": "automap" }, "sourcePort": "preserve", "synCookieStatus": "not-activated", "translateAddress": "enabled", "translatePort": "enabled", "vlansDisabled": true, "vsIndex": 52 } Delete a virtual <vs> tmsh delete ltm virtual <vs> curl -sku admin:admin https://192.168.226.55/mgmt/tm/ltm/virtual/<vs> -X DELETE Sample Output No output (just 200 OK and no response body) References curl.1 the man page curl Releases and Downloads ... including the port for Windows Jason Rahm's "Demystifying iControl REST" series(DevCentral) -- This is Part I of 7 at the time of this article. iControl REST API reference (DevCentral) iControl® REST API User Guide (DevCentral) -- Link is for 12.1. Search for the older versions.17KViews3likes13CommentsDemystifying iControl REST Part 7 - Understanding Transactions
iControl REST. It’s iControl SOAP’s baby, brother, introduced back in TMOS version 11.4 as an early access feature but released fully in version 11.5. Several articles on basic usage have been written about the rest interface so the intent here isn’t basic use, but rather to demystify some of the finer details of using the API. A few months ago, a question in Q&A from community member spirrello asking how to update a tcp profile on a virtual. He was using bigsuds, the python wrapper for the soap interface. For the rest interface on this particular object, this is easy; just use the put method and supply the payload mapping the updated profile. But for soap, this requires a transaction. There are some changes to BIG-IP via the rest interface, however, like updating an ssl cert or key, that likewise will require a transaction to accomplish. In this article, I’ll show you how to use transactions with the rest interface. The Fine Print From the iControl REST user guide, the life cycle of a transaction progresses through three phases: Creation - This phase occurs when the transaction is created using a POST command. Modification - This phase occurs when commands are added to the transaction, or changes are made to the sequence of commands in the transaction. Commit - This phase occurs when iControl REST runs the transaction. To create a transaction, post to /tm/transaction POST https://192.168.25.42/mgmt/tm/transaction {} Response: { "transId":1389812351, "state":"STARTED", "timeoutSeconds":30, "kind":"tm:transactionstate", "selfLink":"https://localhost/mgmt/tm/transaction/1389812351?ver=11.5.0" } Note the transId, the state, and the timeoutSeconds. You'll need the transId to add or re-sequence commands within the transaction, and the transaction will expire after 30 seconds if no commands are added. You can list all transactions, or the details of a specific transaction with a get request. GET https://192.168.25.42/mgmt/tm/transaction GET https://192.168.25.42/mgmt/tm/transaction/transId To add a command to the transaction, you use the normal method uris, but include the X-F5-REST-Coordination-Id header. This example creates a pool with a single member. POST https://192.168.25.42/mgmt/tm/ltm/pool X-F5-REST-Coordination-Id:1389812351 { "name":"tcb-xact-pool", "members": [ {"name":"192.168.25.32:80","description":"First pool for transactions"} ] } Not a great example because there is no need for a transaction here, but we'll roll with it! There are several other option methods for interrogating the transaction itself, see the user guide for details. Now we can commit the transaction. To do that, you reference the transaction id in the URI, remove the X-F5-REST-Coordination-Id header and use the patch method with payload key/value state: VALIDATING . PATCH https://localhost/mgmt/tm/transaction/1389812351 { "state":"VALIDATING" } That's all there is to it! Now that you've seen the nitty gritty details, let's take a look at some code samples. Roll Your Own In this example, I am needing to update and ssl key and certificate. If you try to update the cert or the key, it will complain that they do not match, so you need to update both at the same time. Assuming you are writing all your code from scratch, this is all it takes in python. Note on line 21 I post with an empty payload, and then on line 23, I add the header with the transaction id. I make my modifications and then in line 31, I remove the header, and finally on line 32, I patch to the transaction id with the appropriate payload. import json import requests btx = requests.session() btx.auth = (f5_user, f5_password) btx.verify = False btx.headers.update({'Content-Type':'application/json'}) urlb = 'https://{0}/mgmt/tm'.format(f5_host) domain = 'mydomain.local_sslobj' chain = 'mychain_sslobj try: key = btx.get('{0}/sys/file/ssl-key/~Common~{1}'.format(urlb, domain)) cert = btx.get('{0}/sys/file/ssl-cert/~Common~{1}'.format(urlb, domain)) chain = btx.get('{0}/sys/file/ssl-cert/~Common~{1}'.format(urlb, 'chain')) if (key.status_code == 200) and (cert.status_code == 200) and (chain.status_code == 200): # use a transaction txid = btx.post('{0}/transaction'.format(urlb), json.dumps({})).json()['transId'] # set the X-F5-REST-Coordination-Id header with the transaction id btx.headers.update({'X-F5-REST-Coordination-Id': txid}) # make modifications modkey = btx.put('{0}/sys/file/ssl-key/~Common~{1}'.format(urlb, domain), json.dumps(keyparams)) modcert = btx.put('{0}/sys/file/ssl-cert/~Common~{1}'.format(urlb, domain), json.dumps(certparams)) modchain = btx.put('{0}/sys/file/ssl-cert/~Common~{1}'.format(urlb, 'le-chain'), json.dumps(chainparams)) # remove header and patch to commit the transaction del btx.headers['X-F5-REST-Coordination-Id'] cresult = btx.patch('{0}/transaction/{1}'.format(urlb, txid), json.dumps({'state':'VALIDATING'})).json() A Little Help from a Friend The f5-common-python library was released a few months ago to relieve you of a lot of the busy work with building requests. This is great, especially for transactions. To simplify the above code just to the transaction steps, consider: # use a transaction txid = btx.post('{0}/transaction'.format(urlb), json.dumps({})).json()['transId'] # set the X-F5-REST-Coordination-Id header with the transaction id btx.headers.update({'X-F5-REST-Coordination-Id': txid}) # do stuff here # remove header and patch to commit the transaction del btx.headers['X-F5-REST-Coordination-Id'] cresult = btx.patch('{0}/transaction/{1}'.format(urlb, txid), json.dumps({'state':'VALIDATING'})).json() With the library, it's simplified to: tx = b.tm.transactions.transaction with TransactionContextManager(tx) as api: # do stuff here api.do_stuff Yep, it's that simple. So if you haven't checked out the f5-common-python library, I highly suggest you do! I'll be writing about how to get started using it next week, and perhaps a follow up on how to contribute to it as well, so stay tuned!2.9KViews2likes9CommentsConnection list via iControlREST API
Problem this snippet solves: This python script will retrieve a filtered list of active connections via the iControl REST API. It supports both the default connection list as well as the detailed list you get by specifying 'all-properties'. It has options to generate output on STDOUT as displayed by TMSH (raw), as a JSON formatted dictionary, or written to an Excel spreadsheet. Currently tested in python 2.7 and python 3.5 against BIGIP 11.6.0. How to use this snippet: $ ./conn-list.py -h usage: conn-list.py [-h] [-a] [-x FILE | -j | -r] -f P=V host positional arguments: host Host name to connect. Specified as [<username>@]<hostname> optional arguments: -h, --help show this help message and exit -a, --all-properties Get detailed connection information -f P=V, --filter P=V You must have at least one filter argument, but may have multiple. Output Type: -x FILE, --xlout FILE Excel workbook to be created. -j, --json JSON formatted output to STDOUT -r, --raw RAW ouptut from API request (default) P=V: P = Connection Property (below), V = Value to match. Multiple -f options are joined as logical AND. age Specifies the age, in seconds, of a connection cs-client-addr Specifies the clientside remote address of the active connections cs-client-port Specifies the clientside remote port of the active connections cs-server-addr Specifies the clientside local address of the active connections cs-server-port Specifies the clientside local port of the active connections protocol Specifies the protocol used for specified connections (for example: tcp, udp) ss-client-addr Specifies the serverside local address of the active connections ss-client-port Specifies the serverside local port of the active connections ss-server-addr Specifies the serverside remote address of the active connections ss-server-port Specifies the serverside remote port of the active connections type Specifies the connnection type used for specified connections (for example: any, mirror, self) $ ./conn-list.py -r -f cs-server-port=80 admin@192.0.2.45 Password: Sys::Connections 192.0.2.31:55345 192.0.2.20:80 192.0.2.31:55345 198.51.100.66:80 tcp 6 (tmm: 1) none Total records returned: 1 $ ./conn-list.py -j -f cs-server-port=80 admin@192.0.62.45 Password: [{"acceleration": "none", "cs-server": "192.0.2.20:80", "protocol": "tcp", "cs-client": "192.0.2.31:55613", "idle": 1, "ss-server": "198.51.100.66:80", "tmm": 1, "ss-client": "192.0.2.31:55613"}] Code : #!/usr/bin/env python """ This script will use F5's iControl REST API to collect current connection data. It enforces the use a at least one filter criteria. This has only been tested on 11.6.0 so far. Should be extendable to 11.5.x and 12.x by adding to or duplicating Record Definitions and RE sections. """ import json import sys import getpass import argparse import re try: # Py3 from urllib.parse import urlparse, parse_qs except ImportError: # Py2 from urlparse import urlparse, parse_qs import requests from openpyxl import Workbook RE_LAST = re.compile('(\w+)$') RE_LAST_TWO = re.compile('([\w/:\.]+)\s+([\w/:\.]+)$') DETAILED_DEF = { '11.6.0': {'rec-sep': '---', # actually second line in record. 'fields': {'Slot': {'re': RE_LAST, 'id': ['slot']}, 'TMM': {'re': RE_LAST, 'id': ['tmm']}, 'Acceleration': {'re': RE_LAST, 'id': ['acceleration']}, 'Protocol': {'re': RE_LAST, 'id': ['protocol']}, 'Idle Time ': {'re': RE_LAST, 'id': ['idle']}, 'Idle Timeout': {'re': RE_LAST, 'id': ['idle_timeout']}, 'Lasthop': {'re': RE_LAST_TWO, 'id': ['lasthop-vlan', 'lasthop-mac']}, 'Client Addr': {'re': RE_LAST_TWO, 'id': ['cs-client', 'ss-client']}, 'Server Addr': {'re': RE_LAST_TWO, 'id': ['cs-server', 'ss-server']}, 'Bits In': {'re': RE_LAST_TWO, 'id': ['cs-bits-in', 'ss-bits-in']}, 'Bits Out': {'re': RE_LAST_TWO, 'id': ['cs-bits-out', 'ss-bits-out']}, 'Packets In': {'re': RE_LAST_TWO, 'id': ['cs-packets-in', 'ss-packets-in']}, 'Packets Out': {'re': RE_LAST_TWO, 'id': ['cs-packets-out', 'ss-packets-out']}, } } } SHRT_RE_AP = \ re.compile( '([\w:\.]+)\s+([\w:\.]+)\s+([\w:\.]+)\s+([\w:\.]+)\s+(\w+)\s+(\d+)\s+\(tmm: (\d+)\)\s+(\w+)') SHRT_RE_CH = \ re.compile( '([\w:\.]+)\s+([\w:\.]+)\s+([\w:\.]+)\s+([\w:\.]+)\s+(\w+)\s+(\d+)\s+\(slot/tmm: (\d+)/(\d+)\)\s+(\w+)') SHORT_DEF = { '11.6.0': {'chs-indc': 'slot', 'fields': {'appliance': {'re': SHRT_RE_AP, 'id': ['cs-client', 'cs-server', 'ss-client', 'ss-server', 'protocol', 'idle', 'tmm', 'acceleration']}, 'chassis': {'re': SHRT_RE_CH, 'id': ['cs-client', 'cs-server', 'ss-client', 'ss-server', 'protocol', 'idle', 'slot', 'tmm', 'acceleration']}, } } } requests.packages.urllib3.disable_warnings() try: dict.iteritems except AttributeError: # Py3 def itervalues(d): return iter(d.values()) def iteritems(d): return iter(d.items()) else: # Py2 def itervalues(d): return d.itervalues() def iteritems(d): return d.iteritems() def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('-a', '--all-properties', dest='detail', action='store_true', help='Get detailed connection information') outype = parser.add_argument_group("Output Type") outputs = outype.add_mutually_exclusive_group() outputs.add_argument('-x', '--xlout', action='store', metavar="FILE", help='Excel workbook to be created.') outputs.add_argument('-j', '--json', action='store_true', help='JSON formatted output to STDOUT') outputs.add_argument('-r', '--raw', action='store_true', help='RAW ouptut from API request (default)') parser.add_argument('-f', '--filter', required=True, action='append', metavar='P=V', help='You must have at least one filter argument, but may have multiple.') parser.add_argument('host', help='Host name to connect. Specified as [@]') parser.epilog = """ P=V: P = Connection Property (below), V = Value to match. Multiple -f options are joined as logical AND. age Specifies the age, in seconds, of a connection cs-client-addr Specifies the clientside remote address of the active connections cs-client-port Specifies the clientside remote port of the active connections cs-server-addr Specifies the clientside local address of the active connections cs-server-port Specifies the clientside local port of the active connections protocol Specifies the protocol used for specified connections (for example: tcp, udp) ss-client-addr Specifies the serverside local address of the active connections ss-client-port Specifies the serverside local port of the active connections ss-server-addr Specifies the serverside remote address of the active connections ss-server-port Specifies the serverside remote port of the active connections type Specifies the connnection type used for specified connections (for example: any, mirror, self) """ args = parser.parse_args() (username, unused, host) = args.host.rpartition('@') if not username: username = raw_input('Username: ') password = getpass.getpass('Password: ') filter_prop = '+'.join(args.filter) filter_prop = filter_prop.replace('=', '+') (ctext, ver) = get_conn_list(host, username, password, filter_prop, args.detail) if not ctext: print('No connection list was returned from {}.'.format(host)) quit() if not args.xlout and not args.json: raw_output(ctext) else: if args.detail: conns, patterns = process_detailed_conns(ctext, ver) else: conns, patterns = process_short_conns(ctext, ver) if args.xlout: excel_output(conns, args.xlout, patterns) elif args.json: json_output(conns) # noinspection PyBroadException def get_conn_list(host, uname, pw, options, detail): conntext = [] version = '' b = requests.session() b.auth = (uname, pw) b.verify = False b.headers.update({'Content-Type': 'application/json'}) b_url = 'https://{}/mgmt/tm'.format(host) if detail: options += "+all-properties" try: resp = b.get(b_url + '/sys/connection/?options=' + options, timeout=6.05) if resp.status_code == requests.codes.ok: j = json.loads(resp.text) version = parse_qs(urlparse(j['selfLink']).query)['ver'][0] conntext = j['apiRawValues']['apiAnonymous'].splitlines() else: sys.stderr.write( 'Error: {} status returned from: {}\n {}\n'.format(resp.status_code, host, resp.reason)) except: sys.stderr.write('Error: Could not get data from {}: {}\n'.format(host, sys.exc_info()[0])) return conntext, version def process_detailed_conns(conntext, ver): connlist = [] patterns = {} detailed_record_length = 0 more_than_one_record = False for ndx, val in enumerate(conntext): if val.startswith(DETAILED_DEF[ver]['rec-sep']): if detailed_record_length == 0: detailed_record_length = ndx else: detailed_record_length = ndx - detailed_record_length more_than_one_record = True break else: for p in DETAILED_DEF[ver]['fields']: if val.lstrip().startswith(p): patterns[ndx - 1] = DETAILED_DEF[ver]['fields'][p] break if not more_than_one_record: # only one record returned detailed_record_length = ndx - 1 ndx = 0 while ndx < len(conntext) - 1: if ndx % detailed_record_length == 1: obj = {} for offset, data_def in iteritems(patterns): match = data_def['re'].search(conntext[ndx + offset]) for ndx2, col_heading in enumerate(data_def['id']): grouping = match.group(ndx2 + 1) obj[col_heading] = int(grouping) if grouping.isnumeric() else grouping connlist.append(obj) ndx += detailed_record_length else: ndx += 1 return connlist, patterns def process_short_conns(ctext, ver): connlist = [] patterns = {} if len(ctext) > 2: if SHORT_DEF[ver]['chs-indc'] in ctext[1]: patterns = SHORT_DEF[ver]['fields']['chassis'] else: patterns = SHORT_DEF[ver]['fields']['appliance'] for row in ctext: obj = {} match = patterns['re'].search(row) if match: for ndx, col_heading in enumerate(patterns['id']): grouping = match.group(ndx + 1) if grouping: obj[col_heading] = int(grouping) if grouping.isnumeric() else grouping connlist.append(obj) return connlist, {1: patterns} def excel_output(conns, fname, patterns): wb = Workbook() ws = wb.active header = [] for h in patterns.values(): header += h['id'] ws.append(header) for r in conns: row = [] for h in header: row.append(r[h]) ws.append(row) if not fname.endswith('.xlsx'): fname += '.xlsx' wb.save(fname) def json_output(conns): print(json.dumps(conns)) def raw_output(data): for l in data: print(l) if __name__ == '__main__': main() Tested this on version: 11.6956Views0likes1CommentGenerate private key w/ CSR via iControl REST
Problem this snippet solves: Generate a private key w/ CSR How to use this snippet: To create a private key with a CSR via iControl REST: POST URL:https://10.1.1.165/mgmt/tm/sys/crypto/key Use the data below as your payload. For the name field, it must end in .key or you will get a false 404! Code : { "name":"www.testing.com.key", "commonName":"www.testing.com", "keySize":"4096", "keyType":"rsa-private", "options":[{"gen-csr":"www.testing.com"}], "organization":"Let It Snow Corp.", "ou":"Ice Engineering", "city":"Calhoun", "state":"AZ", "admin-email-address":"jerry@letit.snow", "email-address":"beth@letit.snow", "subject-alternative-name":"DNS:www.testing.com", "challenge-password":"myP4ssword" } Tested this on version: 13.02KViews3likes11CommentsPowerShell module for the F5 LTM REST API
Problem this snippet solves: To report an issue with the F5-LTM or F5-BIGIP modules, please use the Issues sections of the GitHub repos (here and here) instead of commenting here. Thanks! This PowerShell module uses the iControlREST API to manipulate and query pools, pool members, virtual servers, and iRules. It aims to support version 11.5.1 and higher, and to conform to the schedule for technical support of versions, though this may eventually prove to become difficult. The module currently includes some functionality that, strictly speaking, is outside the scope of the LTM module. Hence, there is an active effort to wrap this LTM module into a larger BIG-IP module, and relocate that functionality elsewhere within that parent module, as well as expand the scope of functionality to include BIG-IP DNS (formerly GTM) and possibly other areas. Both the LTM module and the parent BIG-IP module are projects on github. Please use these projects to report any issues you discover. Thanks! The module contains the following functions. Add-iRuleToVirtualServer Add-iRuleToVirtualServer Add-PoolMember Add-PoolMonitor Disable-PoolMember Disable-VirtualServer Enable-PoolMember Enable-VirtualServer Get-CurrentConnectionCount (deprecated; use Get-PoolMemberStats | Select-Object -ExpandProperty 'serverside.curConns') Get-F5Session (will be deprecated in future versions. use New-F5Session) Get-F5Status Get-HealthMonitor Get-HealthMonitorType Get-iRule Get-iRuleCollection (deprecated; use Get-iRule) Get-Node Get-BIGIPPartition Get-Pool Get-PoolList (deprecated; use Get-Pool) Get-PoolMember Get-PoolMemberCollection (deprecated; use Get-PoolMember) Get-PoolMemberCollectionStatus Get-PoolMemberDescription (deprecated; use Get-PoolMember) Get-PoolMemberIP (deprecated; use Get-PoolMember) Get-PoolMembers (deprecated; use Get-PoolMember) Get-PoolMemberStats Get-PoolMemberStatus (deprecated; use Get-PoolMember) Get-PoolMonitor Get-PoolsForMember Get-StatusShape Get-VirtualServer Get-VirtualServeriRuleCollection (deprecated; use Get-VirtualServer | Where rules | Select -ExpandProperty rules) Get-VirtualServerList (deprecated; use Get-VirtualServer) Invoke-RestMethodOverride New-F5Session New-HealthMonitor New-Node New-Pool New-VirtualServer Remove-HealthMonitor Remove-iRule Remove-iRuleFromVirtualServer Remove-Pool Remove-PoolMember Remove-PoolMonitor Remove-ProfileRamCache Remove-Node Remove-VirtualServer Set-iRule Set-PoolLoadBalancingMode (deprecated; use Set-Pool) Set-PoolMemberDescription Set-Pool Set-VirtualServer Sync-DeviceToGroup Test-F5Session Test-Functionality Test-HealthMonitor Test-Node Test-Pool Test-VirtualServer How to use this snippet: To use the module, click 'Download Zip', extract the files, and place them in a folder named F5-LTM beneath your PowerShell modules folder. By default, this is %USERPROFILE%\Documents\WindowsPowerShell\Modules. The WindowsPowerShell and Modules folders may need to be created. You will most likely need to unblock the files after extracting them. Use the Unblock-File PS cmdlet to accomplish this. The Validation.cs class file (based on code posted by Brian Scholer) allows for using the REST API with LTM devices with self-signed SSL certificates. Nearly all of the functions require an F5 session object as a parameter, which contains the base URL for the F5 LTM and a credential object for a user with privileges to manipulate the F5 LTM via the REST API. Use the New-F5session function to create this object. This function expects the following parameters: The name or IP address of the F5 LTM device A credential object for a user with rights to use the REST API An optional TokenLifespan value for extending the life of the authentication token past the default 20 minutes You can create a credential object using Get-Credential and entering the username and password at the prompts, or programmatically like this: $secpasswd = ConvertTo-SecureString "PlainTextPassword" -AsPlainText -Force $mycreds = New-Object System.Management.Automation.PSCredential "username", $secpasswd Thanks to Kotesh Bandhamravuri and his blog entry for this snippet. There is a function called Test-Functionality that takes an F5Session object, a new pool name, a new virtual server, an IP address for the virtual server, and a computer name as a pool member, and validates nearly all the functions in the module. I've also contributed this code sample for how to gather some basic info about your LTM with this PS module. The module has been tested on: 11.5.1 Build 8.0.175 Hotfix 8 and later 11.6.0 Build 5.0.429 Hotfix 4 and later 12.0 / 12.1 13.0 Code : https://github.com/joel74/POSH-LTM-Rest Tested this on version: 11.519KViews2likes150CommentsiCR Python Module for iControl REST
Problem this snippet solves: This is a python module to simplify using iControl REST. Install using pip: pip install iCR or retrieve from https://pypi.python.org/pypi?:action=display&name=iCR&version=2.1 As simple as: #!/usr/bin/env python from iCR import iCR bigip = iCR("172.24.9.132","admin","admin") virtuals = bigip.get("ltm/virtual") for vs in virtuals['items']: print vs['name'] This prints out a list of Virtual Servers. Supported methods: init(hostname,username,password,[timeout,port,icontrol_version,folder,token,debug]) get(url,[select,top,skip,filter]) -> returns data or False getlarge(url,size,[select]) -> Used to retrieve large datasets in chunks. Returns data or False create(url,data) -> returns data or False modify(url,data,[patch=True]) -> returns data or False delete(url) -> returns True or False upload(file) -> file is a local file eg /var/tmp/test.txt, returns True or False download(file) -> files are located in /shared/images, returns True or False create_cert(files) -> files is an array containing paths to cert and key. Returns name of cert or False get_asm_id(name) -> name is the name of a policy. Returns an array of IDs or False create_hash(name) -> name is the name of the partition and policy. eg /Common/test_policy. This reduces the need to retrieve an array of hashes from the BIG-IP. Returns a string. get_token() -> this retrieves a BIG-IP token based on the username and password and sets it as the token in use. Returns the token ID or False delete_token() -> This deletes the object token from the BIG-IP and from the object create_transaction() -> creates a transaction and returns the transaction number ID as a string, or False. Subsequent requests will be added to thetransaction until commit_transaction is called. Transaction ID is stored in object.transaction commit_transaction() -> Commits the transaction stored in object.transaction. Returns True or False command(args,[cmd]) -> Runs a command using the arguments string args. Returns the returned output or True on success or False on failure. Note:Be sure to double-escape single quotes eg \\' and single escape double quotes eg \" cmd options are ping/save/load/restart/reboot Module Variables: icr_session - the link to the requests session raw - the raw returned JSON code - the returned HTTP Status Code eg 200 error - in the case of error, the exception error string headers - the response headers icontrol_version - set this to specify a specific version of iControl debug - boolean True or False to set debugging on or off port - set the port ( 443 by default ) folder - set this to create in a specific partition token - use this to set a specific token. If this is set, it will be used instead of basic auth select - use this with get to select the returned data top - use this with get to return a set number of records skip - use this to skip to a specific record number transaction - stores the Transaction ID How to use this snippet: Examples Setup a REST connection to a device #!/usr/bin/env python from iCR import iCR bigip = iCR("172.24.9.132","admin","admin",timeout=10) Create a Virtual Server vs_config = {'name':'test_vs'} createvs = bigip.create("ltm/virtual",vs_config,timeout=5) Retrieve the VS we just created virt = bigip.get("ltm/virtual/test_vs",select="name") print "Virtual Server created: " + virt['name'] Set the timeout bigip.timeout = 20 Now delete the VS we just created delvs = bigip.delete("ltm/virtual/test_vs") Retrieve ASM policy to ID mapping policies = bigip.get("asm/policies",select="name,id") Print a table of ASM policies with learning mode print print "Policy Name Learning Mode" print "------------------------------------------" for item in policies['items']: enabled = bigip.get("asm/policies/" + item['id'] + "/policy-builder",select="learningMode") print '{:32}'.format(item['name']) + enabled['learningMode'] File upload fp = "/home/pwhite/input.csv" if bigip.upload(fp): print "File " + fp + " uploaded" File download file="BIGIP-12.1.2.0.0.249.iso" download = bigip.download(file) if not download: print "File " + file + " download error" SSL Certificate creation In different folder bigip.folder = "TestFolder" files = ("TestCert.crt","TestCert.key") cert = bigip.create_cert(files) if cert: print "Certificate " + cert + " created" Turn on debugging bigip.debug = True Retrieve ASM policy IDs asm = bigip.get_asm_id("dummy_policy") print len(asm) + " IDs returned" print "ID: " + str(asm[0]) Convert an ASM policy name to hash hash = bigip.create_hash("/Common/test-policy") enabled = bigip.get("asm/policies/" + hash + "/policy-builder",select="learningMode") print '{:32}'.format(item['name']) + enabled['learningMode'] Retrieve and use a token bigip.get_token() Delete the token bigip.delete_token() Developed on Python 2.7 but works with v3. Works on TMOS 11.6 onwards though some features may not be implemented, such as tokens. If you use this and have found bugs, would like to discuss it or suggest features then please PM me on DevCentral. Tested this on version: 13.01.2KViews0likes19CommentsOverwriting or adding LTM SSL Traffic cert and key using iControlREST
Hi, I am trying to overwrite an existing cert and key within the LTM SSL Traffic cert and key using iControlREST. Here is the basic process, and result of each step. Upload key and cert PEM files to the uploads directory. I have tried this step both inside and outside of a transaction with the same result. This works fine. Create a transaction using the transaction REST endpoint. This works fine. Add a command to install the key over the desired SSL Traffic key referencing the local path from step 1 with the transaction id in the header. The command is set to install and from-local-file. Successfully added to the transaction commands. Add a command to install the key over the desired SSL Traffic cert referencing the local path from step 1 with the transaction id in the header. The command is set to install and from-local-file. Successfully added to the transaction commands. Get the transaction commands just to observe the contents. The commands are present, and the paths are correct per steps 3 & 4 above. Attempt to commit the transaction, and receive the failure with a message like the one below. message=transaction failed:01070712:3: file (/var/system/tmp/tmsh/GexeqO/IIS-F5v13.key) expected to exist. As you can see, F5 is looking in a different directory than specified in steps 3 & 4. I've closely examined all requests and responses using Fiddler, and there's no way to determine the randomly generated sub directory name ('GexeqO' in this particular case). It is different each transaction. Also note, this happens even when not overwriting existing entries. But I am using a transaction so that I don't get the 'key and certificate do not match' message. Any insights would be tremendously helpful. Best, Gary687Views2likes6CommentsHow BIG-IP Token/Authentication works ?
I'm unable to find anywhere here/documentation/articles anyone that could explain a little bit better the authentication token when you get the response from the Rest. I'm sending the POST to the Rest, and the Rest is returning the Authentication. Here is an example: token : AD2GKZPXKVTE4WKJEQUZTIPOM3 name : AD2GKZPXKVTE4WKJEQUZTIPOM3 userName : admin authProviderName : tmos user : ... groupReferences : ... timeout : 1200 startTime : 2016-07-22T09:24:11.808-0500 address : 10.10.10.10 partition : [All] generation : 1 lastUpdateMicros : 1469197451807722 expirationMicros : 1469198651808000 kind : shared:authz:tokens:authtokenitemstate selfLink : https://localhost/mgmt/shared/authz/tokens/AD2GKZPXKVTE4WKJEQUZTIPOM3 Does anyone knows what is "lastUpdateMicros", "ExpirationMicros" and what is Timeout actually means ? I'm having several issues in my scripts when I call the Rest and the call just fail. If I try to get a new token the call works. I wonder if could be due the token is expired after is used once. Will the token expire only after 1200 seconds or that is not true ?2.5KViews1like15CommentsWorking with subsets of data-group records via iControl REST
The BIG-IP iControl REST interface method for data-groups does not define the records as a subcollection (like pool members.) This is problematic for many because the records are just a list attribute in the data-group object. This means that if you want to add, modify, or delete a single record in a data-group, you have replace the entire list. A short while back I was on a call with iRule extraordinaire John Alam, and he was showing me a management tool he was working on where he could change individual records in a data-group via REST. I was intrigued so we dug into the details and I was floored at how simple the solution to this problem is! UPDATE: Chris L mentioned in the comments below that working with subsets IS possible without the tmsh script, as helearned in this thread in the Q&A section. The normal endpoint (/mgmt/tm/ltm/data-group/internal/yourDGname) works just fine, but instead of trying to change a subset of the records attribute, which only results in the replace-all-with behavior, you can use theoptions query parameterand then pass the normal tmsh command records data as arguments. An example of this request would be to PATCHwith an empty json payload ({})to urlhttps://{{host}}/mgmt/tm/ltm/data-group/internal/mydg?options=records%20modify%20%7B%20k3%20%7Bdata%20v3%20%7D%20%7D(without the encoding, that query value format is “records modify { k3 { data v3 } }”). As this article is still a good learning exercise on how to use tmsh scripts with iControl REST, I’ll keep the article as is, but an updated script for the specific problem we’re solving can befound in this gist on Github. Enter the tmsh script! Even though the iControl REST doesn’t treat data-group records as individual objects, the tmsh cli does. So if you can create a tmsh script to manage the local manipulation of the records, pass your record data into that script, and execute it from REST, well, that’s where the gold is, people. Let’s start with the tmsh script, written by John Alam but modified very slightly by me. cli script dgmgmt { proc script::init {} { } proc script::run {} { set record_data [lindex $tmsh::argv 3] switch [lindex $tmsh::argv 1] { "add-record" { tmsh::modify ltm data-group internal [lindex $tmsh::argv 2] type string records add $record_data puts "Record [lindex $tmsh::argv 3] added." } "modify-record" { tmsh::modify ltm data-group internal [lindex $tmsh::argv 2] type string records modify $record_data puts "Record changed [lindex $tmsh::argv 3]." } "delete-record" { tmsh::modify ltm data-group internal [lindex $tmsh::argv 2] type string records delete $record_data puts "Record [lindex $tmsh::argv 3] deleted." } "save" { tmsh::save sys config puts "Config saved." } } } proc script::help {} { } proc script::tabc {} { } total-signing-status not-all-signed } This script is installed on the BIG-IP and is a regular object in the BIG-IP configuration, stored in the bigip_script.conf file. There are four arguments passed. The first (arg 0) is always the script name. The other args we pass to the script are: arg 1 - action. Are we adding, modifying, or deleting records? arg 2 - data-group name arg 3 - data-group records to be changed The commands are pretty straight forward. Notice, however, that the record data at the tail end of each of those commands is just the data passed to the script, so the required tmsh format is left to the remote side of this transaction. Since I’m writing that side of the solution, that’s ok, but if I were to put my best practices hat on, the record formatting work should really be done in the tmsh script, so that all I have to do on the remote side is pass the key/value data. Executing the script! Now that we have a shiny new tmsh script for the BIG-IP, we have two issues. We need to install that script on the BIG-IP in order to use it We need to be able to run that script remotely, and pass data to it This is where you grab your programming language of choice and go at it! For me, that would be python. And I’ll be using the BIGREST SDK to interact with BIG-IP. Let’s start with the program flow: if __name__ == "__main__": args = build_parser() b = instantiate_bigip(args.host, args.user) if not b.exist("/mgmt/tm/cli/script/dgmgmt"): print( "\n\tThe data-group management tmsh script is not yet on your system, installing..." ) deploy_tmsh_script(b) sleep(2) if not b.exist(f"/mgmt/tm/ltm/data-group/internal/{args.datagroup}"): print( f"\n\tThe {args.datagroup} data-group doesn't exist. Please specify an existing data-group.\n" ) sys.exit() cli_arguments = format_records(args.action, args.datagroup, args.dgvalues) dg_update(b, cli_arguments) dg_listing(b, args.datagroup) This is a cli script, so we need to create a parser to handle the arguments. After collecting the data, we instantiate BIG-IP. Next, we check for the existence of the tmsh script on BIG-IP and install it if it is not present. We then format the record data and proceed to supply that output as arguments when we make the REST call to run the tmsh script. Finally, we print the results. This last step is probably not something you'd want to do for large data sets, but it's included here for validation. Now, let's look at each step of the flow. The imports and tmsh script # Imports used in this script from bigrest.bigip import BIGIP from time import sleep import argparse import getpass import sys # The tmsh script DGMGMT_SCRIPT = 'proc script::init {} {\n}\n\nproc script::run {} {\nset record_data [lindex $tmsh::argv 3]\n\n' \ 'switch [lindex $tmsh::argv 1] {\n "add-record" {\n tmsh::modify ltm data-group internal ' \ '[lindex $tmsh::argv 2] type string records add $record_data\n ' \ 'puts "Record [lindex $tmsh::argv 3] added."\n }\n "modify-record" {\n ' \ 'tmsh::modify ltm data-group internal [lindex $tmsh::argv 2] type string records modify' \ ' $record_data\n puts "Record changed [lindex $tmsh::argv 3]."\n }\n "delete-record" {\n' \ ' tmsh::modify ltm data-group internal [lindex $tmsh::argv 2] type string records delete' \ ' $record_data\n puts "Record [lindex $tmsh::argv 3] deleted."\n }\n "save" {\n ' \ ' tmsh::save sys config\n puts "Config saved."\n }\n}\n}\n' \ 'proc script::help {} {\n}\n\nproc script::tabc {} {\n}\n' These are defined at the top of the script and are necessary to the appropriate functions defined in the below sections. You could move the script into a file and load it, but it's small enough that it doesn't clutter the script and makes it easier not to have to manage multiple files. The parser def build_parser(): parser = argparse.ArgumentParser() parser.add_argument("host", help="BIG-IP IP/FQDN") parser.add_argument("user", help="BIG-IP Username") parser.add_argument( "action", help="add | modify | delete", choices=["add", "modify", "delete"] ) parser.add_argument("datagroup", help="Data-Group name you wish to change") parser.add_argument( "dgvalues", help='Key or KV Pairs, in this format: "k1,k2,k3=v3,k4=v4,k5"' ) return parser.parse_args() This is probably the least interesting part, but I'm including it here to be thorough. The one thing to note is the cli format to supply the key/value pairs for the data-group records. I could have also added an alternate option to load a file instead, but I'll leave that as an exercise for future development. If you supply no arguments or the optional -h/--help, you'll get the help message. % python dgmgmt.py -h usage: dgmgmt.py [-h] host user {add,modify,delete} datagroup dgvalues positional arguments: host BIG-IP IP/FQDN user BIG-IP Username {add,modify,delete} add | modify | delete datagroup Data-Group name you wish to change dgvalues Key or KV Pairs, in this format: "k1,k2,k3=v3,k4=v4,k5" optional arguments: -h, --help show this help message and exit Instantiation def instantiate_bigip(host, user): pw = getpass.getpass(prompt=f"\n\tWell hello {user}, please enter your password: ") try: obj = BIGIP(host, user, pw) except Exception as e: print(f"Failed to connect to {args.host} due to {type(e).__name__}:\n") print(f"{e}") sys.exit() return obj I don't like typing out my passwords on the cli so I use getpass here to ask for it after I kick off the script. You'll likely want to add an argument for the password if you automate this script with any of your tooling. This function makes a request to BIG-IP and builds a local python object to be used for future requests. Uploading the tmsh script def deploy_tmsh_script(bigip): try: cli_script = {"name": "dgmgmt", "apiAnonymous": DGMGMT_SCRIPT} bigip.create("/mgmt/tm/cli/script", cli_script) except Exception as e: print(f"Failed to create the tmsh script due to {type(e).__name__}:\n") print(f"{e}") sys.exit() Because tmsh scripts are BIG-IP objects, we don't have to interact with the file system. It's just a simple object creation like creating a pool. I have taken the liberty to hardcode the script name to limit the number of arguments required to pass on the cli, but that can be updated if you so desire by either changing the name in the script, or adding arguments. Formatting the records def format_records(action, name, records): recs = "" for record in records.split(","): x = record.split("=") record_key = x[0] if len(x) == 1 and action != 'modify': recs += f"{record_key} " elif len(x) == 1 and action == 'modify': recs += f'{record_key} {{ data \\\"\\\" }} ' elif len(x) == 2: record_value = x[1] recs += f'{record_key} {{ data \\\"{record_value}\\\" }} ' else: raise ValueError("Max record items is 2: key or key/value pair.") return f"{action}-record {name} '{{ {recs} }}'" This is the function I spent the most time ironing out. As I pointed out earlier, it would be better handled in the tmsh script, but since that work was already completed by John, I focused on the python side of things. The few things that I fleshed out in testing that I didn't consider while making it work the first time: Escaping all the special characters that make iControl REST unhappy. Handling whitespace in the data value. This requires quotes around the data value. Modifying a key by removing an existing value. This requires you to provide an empty data reference. Executing the script def dg_update(bigip, cli_args): try: dg_mods = {"command": "run", "name": "/Common/dgmgmt", "utilCmdArgs": cli_args} bigip.command("/mgmt/tm/cli/script", dg_mods) except Exception as e: print(f"Failed to modify the data-group due to {type(e).__name__}:\n") print(f"{e}") sys.exit() With all the formatting out of the way, the update is actually anticlimactic. iControl REST requires a json payload for the command, which is running the cli script. The cli arguments for that script are passed in the utilCmdArgs attribute. Validating the results def dg_listing(bigip, dgname): dg = b.load(f'/mgmt/tm/ltm/data-group/internal/{dgname}') print(f'\n\t{args.datagroup}\'s updated record set: ') for i in dg.properties['records']: print(f'\t\tkey: {i["name"]}, value: {i["data"]}') print('\n\n') And finally, we bask in the validity of our updates! Like the update function, this one doesn't have much to do. It grabs the data-group contents from BIG-IP and prints out each of the key/value pairs. As I indicated earlier, this may not be desirable on large data sets. You could modify the function by passing the keys you changed and compare that to the full results returned from BIG-IP and only print the updates, but I'll leave that as another exercise for future development. This is a cool workaround to the non-subcollection problem with data-groups that I wish I'd thought of years ago! The full script is in the codeshare. I hope you got something out of this article, drop a comment below and let me know!1.5KViews0likes2Comments