Python 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.01 Comment
- Shamatulskii
Nimbostratus
Hi Mark_Lloyd,
Could you please help me find the rest_avr library? Also, could you confirm whether the script is working correctly? I would really appreciate your assistance with this!
Thank you!