# -*- coding: UTF-8 -*-
"""
This module is for performing privileged API calls to InsightIQ.
"""
import collections
from threading import Lock
import requests
[docs]class ConnectionError(Exception):
"""Unable to establish an connection to the OneFS API"""
pass
[docs]class InsightiqApi(object):
"""An authenticated connection to the InsightIQ API
This object is a simple wrapper around a `requests Session <http://docs.python-requests.org/en/master/user/advanced/#session-objects>`_.
The point of wrapping the requests Session object is to remove boiler plate
code in making API calls to InsightIQ, and to auto-handle authenticating
to the API. The most noteworthy changes to this object and how you use the
requests Session object is that you *must* provide the username and password
when instantiating the object, and when you make a request, you only supply
the URI end point (i.e. not the `http://my-host.org:8080` part).
Supports use of ``with`` statements, which will automatically handle creating
and closing the HTTP session with InsightIQ.
Example::
>>> with InsightiApi(username='administrator', password='foo') as iiq:
response = iiq.get('/api/clusters')
:param username: **Required** The name of the administrative account for InsightIQ
:type username: String
:param password: **Required** The password for the administrative account being used.
:type password: String
:param verify: Perform SSL/TLS cert validation using system certs. Setting
to True will likely cause issues when using a self-signed SSL/TLS cert.
Default is False.
:type verify: Boolean
"""
def __init__(self, username, password, verify=False):
self._username = username
self._password = password
self.verify = verify
self._url = 'https://localhost/' # 127.0.0.1 would break if IPv4 is disabled
self._session = None
self.renew_session()
[docs] def renew_session(self):
"""Create a new session to the InsightIQ API
:Returns: requests.Session
:Raises: ConnectionError
The InsightIQ API can be a bit fickle, so this method automatically retries
establishing upwards of 3 times.
"""
retries = 3
count = 0
for attempt in range(retries):
try:
iiq_session = self._get_session()
except requests.exceptions.ConnectionError:
continue
else:
self._session = iiq_session
break
else:
raise ConnectionError('Unable to connect to InsightIQ API')
def _get_session(self):
"""Obtain an authentication token being used for API calls"""
s = requests.Session()
resp = s.post(self._url + 'login', verify=self.verify,
data={'username' : self._username,
'password' : self._password,
'authform' : '+Log+in+'})
resp.raise_for_status()
return s
[docs] def end_session(self):
"""Logout of InsightIQ and close the connection to the server"""
# use of with statement might result in no session created, but still call this method
if self._session:
try:
self.get(self._url + '/logout')
except requests.exceptions.ConnectionError:
pass
finally:
self._session.close()
[docs] def get(self, endpoint, params=None, data=None, headers=None, **kwargs):
"""Perform an HTTP GET request
:Returns: PyObject
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param params: The HTTP parameters to send in the HTTP request
:type params: Dictionary
:param data: The HTTP body content to send in the request. The Python
object supplied (i.e. list, dict, etc) will be auto-converted
to JSON string.
:type data: PyObject
:param headers: Any additional HTTP headers to send in the request
:type headers: Dictionary
"""
return self._call(endpoint, method='get', params=params, data=data, headers=headers, **kwargs)
[docs] def post(self, endpoint, params=None, data=None, headers=None, **kwargs):
"""Perform an HTTP POST request
:Returns: PyObject
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param params: The HTTP parameters to send in the HTTP request
:type params: Dictionary
:param data: The HTTP body content to send in the request. The Python
object supplied (i.e. list, dict, etc) will be auto-converted
to JSON string.
:type data: PyObject
:param headers: Any additional HTTP headers to send in the request
:type headers: Dictionary
"""
return self._call(endpoint, method='post', params=params, data=data, headers=headers, **kwargs)
[docs] def put(self, endpoint, params=None, data=None, headers=None, **kwargs):
"""Perform an HTTP PUT request
:Returns: PyObject
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param params: The HTTP parameters to send in the HTTP request
:type params: Dictionary
:param data: The HTTP body content to send in the request. The Python
object supplied (i.e. list, dict, etc) will be auto-converted
to JSON string.
:type data: PyObject
:param headers: Any additional HTTP headers to send in the request
:type headers: Dictionary
"""
return self._call(endpoint, method='put', params=params, data=data, headers=headers, **kwargs)
[docs] def delete(self, endpoint, params=None, data=None, headers=None, **kwargs):
"""Perform an HTTP DELETE request
:Returns: PyObject
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param params: The HTTP parameters to send in the HTTP request
:type params: Dictionary
:param data: The HTTP body content to send in the request. The Python
object supplied (i.e. list, dict, etc) will be auto-converted
to JSON string.
:type data: PyObject
:param headers: Any additional HTTP headers to send in the request
:type headers: Dictionary
"""
return self._call(endpoint, method='delete', params=params, data=data, headers=headers, **kwargs)
[docs] def head(self, endpoint, params=None, data=None, headers=None, **kwargs):
"""Perform an HTTP HEAD request
:Returns: PyObject
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param params: The HTTP parameters to send in the HTTP request
:type params: Dictionary
:param data: The HTTP body content to send in the request. The Python
object supplied (i.e. list, dict, etc) will be auto-converted
to JSON string.
:type data: PyObject
:param headers: Any additional HTTP headers to send in the request
:type headers: Dictionary
"""
return self._call(endpoint, method='head', params=params, data=data, headers=headers, **kwargs)
def _call(self, endpoint, method, **kwargs):
"""Actually makes the HTTP API calls
The point of this abstraction is to keep our interactions with the
`Requests <http://docs.python-requests.org>`_ library more D.R.Y.
:Returns: requests.Response
:param endpoint: **Required** The URI end point of the InsightIQ API to call
:type endpoint: String
:param method: **Required** The HTTP method to invoke.
:type method: String
:param **kwargs: The key/value arguments for parameters, body, and headers.
:type **kwargs: Dictionary
"""
caller = getattr(self._session, method)
uri = self._build_uri(endpoint)
return caller(uri, verify=self.verify, **kwargs)
def _build_uri(self, endpoint):
"""Convert the supplied URI end point to a full URI
:Returns: String
:param endpoint: The URI resource to call
:type endpoint: String
"""
if endpoint.startswith('/'):
return self._url + endpoint[1:]
else:
return self._url + endpoint
def __enter__(self):
"""Enables use of ``with`` statement
Example::
with InsightiqApi(username='bob', password='1234') as iiq_api:
response = iiq_api.get('some/endpoint', params={'verbose' : True})
"""
return self
def __exit__(self, exec_type, exec_value, the_traceback):
self.end_session()
@property
def username(self):
# Setter so derps cannot change the username
return self._username
[docs]class Parameters(collections.MutableMapping):
"""Object for working with HTTP query parameters
This object supports the Python dictionary API, and lets you define the same
HTTP query parameter more than once. Additional definitions for the same
query parameter creates a new entry in the underlying list. This decision
makes it simple to iterate Parameters to build up the HTTP query string because
you do not have to iterate parameter values. Because you can define the same
parameter more than once, using the standard dictionary API will only impact
the first occurrence of that parameter. To modify a specific parameter, you
must use the methods in this class which extend the dictionary API.
This documentation is specific to how Parameters extends the normal Python
dictionary API. For documentation about the Python dictionary API, please
checkout their official page `here <https://docs.python.org/3/library/stdtypes.html#dict>`_.
Example creating duplicate parameters::
>>> params = Parameters()
>>> for value in range(3):
... params.add('myParam', value)
...
Parameters([['myParam', 0], ['myParam', 1], ['myParam', 2]])
What **NOT** to do::
>>> params = Parameters()
>>> for doh in range(3):
... params['homer'] = doh
...
>>> params
Parameters([['homer', 2], ['homer', 2], ['homer', 2]])
Iterating Parameters to build a query string::
>>> query = []
>>> params = Parameters(one=1, two=2)
>>> for name, value in params.items():
... query.append('%s=%s' % (name, value))
...
>>> query_str = '&'.join(query)
>>> query_str
'one=1&two=2'
:param args: Data to initialize the Parameters object with.
:type args: List, Tuple, or Dictionary
:param kwargs: Data to initialize the Parameters object with.
:type kwargs: Dictionary
"""
# Indexes for what a key and value are; avoids magic numbers
_KEY = 0
_VAL = 1
def __init__(self, *args, **kwargs):
self._data = []
self._lock = Lock()
# iterate the args first so we can fail as shallow as possible
for arg in args:
if isinstance(arg, collections.Mapping):
self._add_dict(arg)
continue
element, ok = self._arg_is_ok(arg)
if ok:
self._add_arg(arg)
else:
msg = 'Invalid element %s in arg %s' % (element, arg)
raise ValueError(msg)
# Now add any keyword args
self._add_dict(kwargs)
@property
def NAME(self):
"""The array index for a parameter name; avoids magic numbers"""
return self._KEY
@property
def VALUE(self):
"""The array index for a parameter value; avoids magic numbers"""
return self._VAL
def _add_dict(self, the_dictionary):
"""Update the Parameters object with the contents of a dictionary
:Returns: None
:param the_dictionary: **Required** The dictionary to add to the Parameters object
:type the_dictionary: Dictionary
"""
for key, value in the_dictionary.items():
self._data.append([key, value])
def _add_arg(self, the_arg):
"""Add the argument data to Parameters
:param the_arg: **Required** The specific argument (from __init__) to add
:type the_arg: List, Tuple, or Dictionary
"""
for element in the_arg:
if isinstance(element, collections.Mapping):
self._add_dict(element)
else:
self._data.append(list(element)) # Cast everything to list, b/c it might be a tuple
def _arg_is_ok(self, the_arg):
"""Test that the argument data structure is valid for Parameters
:param the_arg: **Required** The argument to test
:type the_arg: PyObject
"""
for element in the_arg:
if isinstance(element, collections.Mapping):
continue
if not (isinstance(element, tuple) or isinstance(element, list)):
return element, False
# query parameters are key/value pairs, so only 2 elements makes sense
if len(element) != 2:
return element, False
else:
return None, True
def __repr__(self):
"""A human friendly representation of the Parameters object"""
return 'Parameters(%s)' % self._data
def __getitem__(self, param):
"""Return the parameter name.
:param param: **Required** The name of the parameter
:type param: String
"""
found = [x[self._VAL] for x in self._data if x[self._KEY] == param]
if found:
return found[0]
else:
raise KeyError('No such parameter: %s' % param)
[docs] def items(self):
"""Iterate Parameters, and return the param/value pairs
:Returns: Generator
"""
for pair in self._data:
yield tuple(pair)
def __iter__(self):
"""Iterate the parameters and return the parameter name.
This approach makes Parameters behave like a standard Python dict()
"""
for pair in self._data:
yield pair[self._KEY]
def __len__(self):
"""The number of parameters"""
return len(self._data)
def __setitem__(self, key, value):
"""Create or update a parameter
If you want to add a duplicate parameter, use the ``.add()`` method
:param key: **Required** The parameter to create or update
:type key: String
:param value: **Required** The value for the parameter
:type value: PyObject
"""
for pair in self._data:
if pair[self._KEY] == key:
pair[self._VAL] = value
else:
self._data.append([key, value])
def __delitem__(self, key):
"""Delete the first occurrence of a parameter
:Raises: KeyError - when parameter does not exist
:Returns: None
:param key: **Required** The name of the parameter to delete
:type key: String
"""
for index, pair in enumerate(self._data):
if pair[self._KEY] == key:
value = self._data.pop(index)[self._VAL]
return value
else:
msg = 'No such parameter: %s' % key
raise KeyError(msg)
def _find_occurrence_index(self, name, occurrence):
"""Return the index of the N-th occurrence of a recurring parameter
:Raises: KeyError - if param or occurrence doesn't exist
:Returns: Integer
:param name: **Required** The name of the parameter to locate
:type name: String
:param occurrence: **Required** The N-th instance the parameter is defined. Zero-based numbering.
:type occurrence: Integer
"""
count = 0
param_exists = False
for index, pair in enumerate(self._data):
if pair[self._KEY] == name:
param_exists = True
if count == occurrence:
return index
else:
count += 1
# Param or occurrence not found :(
if param_exists:
msg = 'Parameter %s does not have an occurrence of %s' % (name, occurrence)
else:
msg = 'No such parameter: %s' % name
raise KeyError(msg)
[docs] def add(self, name, value):
"""Add a duplicate parameter
:Returns: None
:param name: **Required** The name of the parameter
:type name: String
:param value: **Required** The value for the duplicate parameter
:type value: PyObject
"""
self._data.append([name, value])
[docs] def delete_parameter(self, name, occurrence):
"""Delete a specific parameter that is defined more than once. Thread safe.
:Returns: None
:param name: **Required** The parameter to delete.
:type name: String
:param occurrence: **Required** The N-th instance of a parameter. Zero based numbering.
:type occurrence: Integer
"""
self._lock.acquire()
try:
index = self._find_occurrence_index(name, occurrence)
self._data.pop(index)
finally:
self._lock.release()
[docs] def modify_parameter(self, name, new_value, occurrence):
"""Change the value of a specific parameter that is defined more than once.
Thread safe.
:Returns: None
:param name: **Required** The parameter to delete.
:type name: String
:param new_value: **Required** The value for the parameter
:type new_value: PyObject
:param occurrence: **Required** The N-th instance of a parameter. Zero based numbering.
:type occurrence: Integer
"""
self._lock.acquire()
try:
index = self._find_occurrence_index(name, occurrence)
self._data[index] = [name, new_value]
finally:
self._lock.release()
[docs] def get_all(self, name):
"""Return the key/value pairs for a parameter. Order is maintained.
:Returns: List
:param name: **Required** The name of the query parameter
:type name: String
"""
return [x for x in self._data if x[self._KEY] == name]