#!/usr/local/bin/python3
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
'''
------------------------------------------------------------------------
Description:
Module to provide class hierachy to simplify access to the BloxOne APIs
Date Last Updated: 20210713
Todo:
Copyright (c) 2021 Chris Marrison / Infoblox
Redistribution and use in source and binary forms,
with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
------------------------------------------------------------------------
'''
from bloxone.b1oph import b1oph
import bloxone
import logging
import requests
import json
# ** Global Vars **
__version__ = '0.1.0'
__author__ = 'Chris Marrison'
__email__ = 'chris@infoblox.com'
__doc__ = 'https://python-bloxone.readthedocs.io/en/latest/'
__license__ = 'BSD'
class APIError_Unable_To_Retrieve_Commands(Exception):
'''
Exception for API call in __init__
'''
pass
class Command_Not_Supported(Exception):
'''
Exception for API call in __init__
'''
pass
class Unknown_Argument(Exception):
'''
Exception for API call in __init__
'''
pass
[docs]class b1diagnostics(bloxone.b1):
'''
Class to simplify access to the BloxOne Platform APIs
'''
def __init__(self, cfg_file='config.ini'):
'''
Call base __init__ and extend
'''
super().__init__(cfg_file)
# Instantiate b1oph class as need access to b1oph.get_ophid()
self.b1_oph = b1oph(cfg_file)
# Automatically get list of remote_commands
try:
response = self.get_remote_commands()
self.commands = response.json()['results']
except:
logging.error(f'Response code: {response.status_code}')
logging.error(f'Response body: {response.text}')
raise APIError_Unable_To_Retrieve_Commands()
return
[docs] def get(self, objpath, id="", action="", **params):
'''
Generic get object wrapper for platform calls
Parameters:
objpath (str): Swagger object path
id (str): Optional Object ID
action (str): Optional object action, e.g. "nextavailableip"
Returns:
response object: Requests response object
'''
# Build url
url = self.diagnostics_url + objpath
url = self._use_obj_id(url,id=id)
url = self._add_params(url, **params)
logging.debug("URL: {}".format(url))
response = self._apiget(url)
return response
[docs] def post(self, objpath, body=""):
'''
Generic create object wrapper for platform objects
Parameters:
objpath (str): Swagger object path
body (str): JSON formatted data payload
Returns:
response object: Requests response object
'''
# Build url
url = self.diagnostics_url + objpath
logging.debug("URL: {}".format(url))
# Make API Call
response = self._apipost(url, body)
return response
[docs] def delete(self, objpath, id=""):
'''
Generic delete object wrapper for platform objects
Parameters:
objpath (str): Swagger object path
id (str): Object id to delete
Returns:
response object: Requests response object
'''
# Build url
url = self.diagnostics_url + objpath
url = self._use_obj_id(url, id=id)
logging.debug("URL: {}".format(url))
# Make API Call
response = self._apidelete(url)
return response
[docs] def update(self, objpath, id="", body=""):
'''
Generic create object wrapper for ddi objects
Parameters:
objpath (str): Swagger object path
body (str): JSON formatted data payload
Returns:
response object: Requests response object
'''
# Build url
url = self.diagnostics_url + objpath
url = self._use_obj_id(url, id=id)
logging.debug("URL: {}".format(url))
# Make API Call
response = self._apiput(url, body)
return response
# *** Helper Methods ***
[docs] def get_id(self, objpath, *, key="", value="", include_path=False):
'''
Get object id using key/value pair
Parameters:
objpath (str): Swagger object path
key (str): name of key to match
value (str): value to match
include_path (bool): Include path to object id
Returns:
id (str): object id or ""
'''
# Local Variables
id = ""
filter = key+'=="'+value+'"'
fields = key + ',id'
# Make API Call
response = self.get(objpath, _filter=filter, _fields=fields)
# Process response
if response.status_code in self.return_codes_ok:
obj = response.json()
# Look for results
if "results" in obj.keys():
obj = obj['results']
if obj:
id = obj[0]['id']
if not include_path:
id = id.rsplit('/',1)[1]
else:
logging.debug("Key {} with value {} not found."
.format(key,value))
else:
id = ""
logging.debug("No results found.")
else:
id=""
logging.debug("HTTP Error occured. {}".format(response.status_code))
logging.debug("id: {}".format(id))
return id
# Helper Methods
'''
def get_ophid(self):
return ophid
'''
[docs] def get_remote_commands(self):
'''
Get set of possible remote commands and parameters
Returns:
response object: Requests response object
'''
return self.get('/remotecommands')
[docs] def is_command(self, command):
'''
Check whether command is valid
Parameters:
command(str): command to check
Returns:
boolean
'''
cmds = []
for cmd in self.commands:
cmds.append(cmd['name'])
if command in cmds:
status = True
else:
status = False
return status
[docs] def get_args(self, command):
'''
Check the args for a command
Parameters:
command(str): Command to retrieve argyments for
Returns:
Disctionary of arguments or empty dictionary if none.
Raises:
Command_Not_Supported Exception if command is not available
'''
args = {}
if self.is_command(command):
for cmd in self.commands:
if cmd['name'] == command:
if 'args' in cmd.keys():
args = cmd['args']
else:
args = {}
break
else:
raise Command_Not_Supported(f'Command: {command} not supported.')
return args
[docs] def execute_task(self, command, args={}, ophname=None,
ophid=None, id_only=True, priv=False):
'''
Execute remote command on an OPH
Parameters:
cmd(str): Command to execute
args(dict): Command arguments
ophname(str): Name of OPH to execute command on (or supply ophid)
ophid(str): (Optional) ophid of OPH for cmd execution
id_only(bool): default of True
priv(bool): Run privileged task, default of False
Returns:
id string of task if id_only=True (defult)
response object: Requests response object if id_only=False
Raises:
TypeError Exception if required options not supplied
KeyErro Exception if ophname is not found (and ophid not supplied)
Command_Not_Supported Exception if command is not valid
Unknown_Argument Exception if arguments do not match required
Todo:
[ ] Enhance logic to run /priviledgetask or /task
Awaiting API enhancement to determine priv versus non-priv
[ ] Enhance args check for required arguments
Awaiting API enhancement for arg to determine required versus
optional arguments
'''
# Check command is valid
if self.is_command(command):
# If ophid supplied then get this
if ophid or ophname:
if not ophid:
logging.debug(f'Getting ophid for OPH: {ophname}')
ophid = self.b1_oph.get_ophid(name=ophname)
if not ophid:
logging.error(f'OPH not found.')
raise KeyError(f'OPH {ophname} not found')
logging.debug(f'OPHID: {ophid}')
# Check args
arglist = self.get_args(command)
for arg in args.keys():
if arg not in arglist.keys():
raise Unknown_Argument(f'Argument: {arg} not recognised')
s_type = type(args[arg])
ex_type = type(arglist[arg])
if s_type != ex_type:
raise TypeError(f'Supplied argument type {s_type}' +
f' does not match expected type {ex_type}')
# Create command body
body = '{ "cmd": '
if len(args) > 0:
body = body + '{ "args": ' + json.dumps(args) + ', '
else:
body = body + '{ '
body = body + '"name": "' + command + '" }, '
body = body + '"ophid": "' + ophid + '" }'
logging.debug(f'Task body: {body}')
# Execute
if priv:
response = self.post('/privilegedtask', body=body)
else:
response = self.post('/task', body=body)
if id_only:
if response.status_code in self.return_codes_ok:
id = response.json()['result']['id']
else:
error_msg = ( 'Failed to create task, ' +
f'HTTP Code: {response.status_code}')
logging.error(error_msg)
id = None
result = id
else:
result = response
else:
logging.error('No ophname or ophid supplied.')
raise TypeError('Requires either ophname or ophid to be defined')
else:
logging.error(f'Command: {command} is not a supported command')
raise Command_Not_Supported(f'Command: {command} not supported.')
return result
[docs] def get_task_result(self, taskid):
'''
Get the results for specidied task
Parameters:
taskid(str): id of executed task
Returns:
response object: Requests response object if id_only=False
'''
id = '/task/' + taskid
return self.get(id)
[docs] def download_task_results(self, taskid):
'''
Get the results for specidied task
Parameters:
taskid(str): id of executed task
Returns:
response object: Requests response object if id_only=False
Note:
'''
id = '/task/' + taskid + '/download'
return self.get(id)