mirror of https://github.com/ansible/ansible.git
Fortinet: FortiAnalyzer Plugin and Module Utils and FAZ_DEVICE update. (#58882)
* FortiAnalyzer Plugin and Module Utils and FAZ_DEVICE update. * Updated Version Added for plugin. * Removed stray characters (new keyboard... leaned on a key to hard...). DOH! * Minor fix to documentation * Added __metaclass__ = type per Sanity/1 test failure of last test. * Minor PEP8... should be last one * Same PEP8 Fix for Common... * Minor change to kick off another shippable after an obscure OS X error. * Last Shippable showed "unstable" but didn't give any other reasons. Kicking off YET ANOTHER. * Once again, shippable is erroring on unrelated issues and modules. Minor Docs change to kick it off again. * Shippable, again, failed to find the commit. Minor doc change to kick it off again. * Small doc change to kick off shippable. A single test is ending up "unstable" for RHEL...pull/61364/head
parent
b09db0ba9e
commit
6fb7073adc
@ -0,0 +1,292 @@
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||
# still belong to the author of the module, and may assign their own license
|
||||
# to the complete work.
|
||||
#
|
||||
# (c) 2017 Fortinet, Inc
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * 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 __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# BEGIN STATIC DATA AND MESSAGES
|
||||
class FAZMethods:
|
||||
GET = "get"
|
||||
SET = "set"
|
||||
EXEC = "exec"
|
||||
EXECUTE = "exec"
|
||||
UPDATE = "update"
|
||||
ADD = "add"
|
||||
DELETE = "delete"
|
||||
REPLACE = "replace"
|
||||
CLONE = "clone"
|
||||
MOVE = "move"
|
||||
|
||||
|
||||
BASE_HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
# FAZ RETURN CODES
|
||||
FAZ_RC = {
|
||||
"faz_return_codes": {
|
||||
0: {
|
||||
"msg": "OK",
|
||||
"changed": True,
|
||||
"stop_on_success": True
|
||||
},
|
||||
-100000: {
|
||||
"msg": "Module returned without actually running anything. "
|
||||
"Check parameters, and please contact the authors if needed.",
|
||||
"failed": True
|
||||
},
|
||||
-2: {
|
||||
"msg": "Object already exists.",
|
||||
"skipped": True,
|
||||
"changed": False,
|
||||
"good_codes": [0, -2]
|
||||
},
|
||||
-6: {
|
||||
"msg": "Invalid Url. Sometimes this can happen because the path is mapped to a hostname or object that"
|
||||
" doesn't exist. Double check your input object parameters."
|
||||
},
|
||||
-3: {
|
||||
"msg": "Object doesn't exist.",
|
||||
"skipped": True,
|
||||
"changed": False,
|
||||
"good_codes": [0, -3]
|
||||
},
|
||||
-10131: {
|
||||
"msg": "Object dependency failed. Do all named objects in parameters exist?",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-9998: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add. STOPPING. Use 'ignore_errors=yes' in playbook"
|
||||
"to override and mark successful.",
|
||||
},
|
||||
-20042: {
|
||||
"msg": "Device Unreachable.",
|
||||
"skipped": True
|
||||
},
|
||||
-10033: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add.",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-10000: {
|
||||
"msg": "Duplicate object. Try using mode='set', if using add.",
|
||||
"changed": False,
|
||||
"skipped": True
|
||||
},
|
||||
-20010: {
|
||||
"msg": "Device already added to FortiAnalyzer. Serial number already in use.",
|
||||
"good_codes": [0, -20010],
|
||||
"changed": False,
|
||||
"stop_on_failure": False
|
||||
},
|
||||
-20002: {
|
||||
"msg": "Invalid Argument -- Does this Device exist on FortiAnalyzer?",
|
||||
"changed": False,
|
||||
"skipped": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_RESULT_OBJ = (-100000, {"msg": "Nothing Happened. Check that handle_response is being called!"})
|
||||
FAIL_SOCKET_MSG = {"msg": "Socket Path Empty! The persistent connection manager is messed up. "
|
||||
"Try again in a few moments."}
|
||||
|
||||
|
||||
# BEGIN ERROR EXCEPTIONS
|
||||
class FAZBaseException(Exception):
|
||||
"""Wrapper to catch the unexpected"""
|
||||
|
||||
def __init__(self, msg=None, *args, **kwargs):
|
||||
if msg is None:
|
||||
msg = "An exception occurred within the fortianalyzer.py httpapi connection plugin."
|
||||
super(FAZBaseException, self).__init__(msg, *args)
|
||||
|
||||
# END ERROR CLASSES
|
||||
|
||||
|
||||
# BEGIN CLASSES
|
||||
class FAZCommon(object):
|
||||
|
||||
@staticmethod
|
||||
def format_request(method, url, *args, **kwargs):
|
||||
"""
|
||||
Formats the payload from the module, into a payload the API handler can use.
|
||||
|
||||
:param url: Connection URL to access
|
||||
:type url: string
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
:param kwargs: The payload dictionary from the module to be converted.
|
||||
|
||||
:return: Properly formatted dictionary payload for API Request via Connection Plugin.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
params = [{"url": url}]
|
||||
if args:
|
||||
for arg in args:
|
||||
params[0].update(arg)
|
||||
if kwargs:
|
||||
keylist = list(kwargs)
|
||||
for k in keylist:
|
||||
kwargs[k.replace("__", "-")] = kwargs.pop(k)
|
||||
if method == "get" or method == "clone":
|
||||
params[0].update(kwargs)
|
||||
else:
|
||||
if kwargs.get("data", False):
|
||||
params[0]["data"] = kwargs["data"]
|
||||
else:
|
||||
params[0]["data"] = kwargs
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def split_comma_strings_into_lists(obj):
|
||||
"""
|
||||
Splits a CSV String into a list. Also takes a dictionary, and converts any CSV strings in any key, to a list.
|
||||
|
||||
:param obj: object in CSV format to be parsed.
|
||||
:type obj: str or dict
|
||||
|
||||
:return: A list containing the CSV items.
|
||||
:rtype: list
|
||||
"""
|
||||
return_obj = ()
|
||||
if isinstance(obj, dict):
|
||||
if len(obj) > 0:
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str):
|
||||
new_list = list()
|
||||
if "," in v:
|
||||
new_items = v.split(",")
|
||||
for item in new_items:
|
||||
new_list.append(item.strip())
|
||||
obj[k] = new_list
|
||||
return_obj = obj
|
||||
elif isinstance(obj, str):
|
||||
return_obj = obj.replace(" ", "").split(",")
|
||||
|
||||
return return_obj
|
||||
|
||||
@staticmethod
|
||||
def cidr_to_netmask(cidr):
|
||||
"""
|
||||
Converts a CIDR Network string to full blown IP/Subnet format in decimal format.
|
||||
Decided not use IP Address module to keep includes to a minimum.
|
||||
|
||||
:param cidr: String object in CIDR format to be processed
|
||||
:type cidr: str
|
||||
|
||||
:return: A string object that looks like this "x.x.x.x/y.y.y.y"
|
||||
:rtype: str
|
||||
"""
|
||||
if isinstance(cidr, str):
|
||||
cidr = int(cidr)
|
||||
mask = (0xffffffff >> (32 - cidr)) << (32 - cidr)
|
||||
return (str((0xff000000 & mask) >> 24) + '.'
|
||||
+ str((0x00ff0000 & mask) >> 16) + '.'
|
||||
+ str((0x0000ff00 & mask) >> 8) + '.'
|
||||
+ str((0x000000ff & mask)))
|
||||
|
||||
@staticmethod
|
||||
def paramgram_child_list_override(list_overrides, paramgram, module):
|
||||
"""
|
||||
If a list of items was provided to a "parent" paramgram attribute, the paramgram needs to be rewritten.
|
||||
The child keys of the desired attribute need to be deleted, and then that "parent" keys' contents is replaced
|
||||
With the list of items that was provided.
|
||||
|
||||
:param list_overrides: Contains the response from the FortiAnalyzer.
|
||||
:type list_overrides: list
|
||||
:param paramgram: Contains the paramgram passed to the modules' local modify function.
|
||||
:type paramgram: dict
|
||||
:param module: Contains the Ansible Module Object being used by the module.
|
||||
:type module: classObject
|
||||
|
||||
:return: A new "paramgram" refactored to allow for multiple entries being added.
|
||||
:rtype: dict
|
||||
"""
|
||||
if len(list_overrides) > 0:
|
||||
for list_variable in list_overrides:
|
||||
try:
|
||||
list_variable = list_variable.replace("-", "_")
|
||||
override_data = module.params[list_variable]
|
||||
if override_data:
|
||||
del paramgram[list_variable]
|
||||
paramgram[list_variable] = override_data
|
||||
except BaseException as e:
|
||||
raise FAZBaseException("Error occurred merging custom lists for the paramgram parent: " + str(e))
|
||||
return paramgram
|
||||
|
||||
@staticmethod
|
||||
def syslog(module, msg):
|
||||
try:
|
||||
module.log(msg=msg)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
# RECURSIVE FUNCTIONS START
|
||||
def prepare_dict(obj):
|
||||
"""
|
||||
Removes any keys from a dictionary that are only specific to our use in the module. FortiAnalyzer will reject
|
||||
requests with these empty/None keys in it.
|
||||
|
||||
:param obj: Dictionary object to be processed.
|
||||
:type obj: dict
|
||||
|
||||
:return: Processed dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
list_of_elems = ["mode", "adom", "host", "username", "password"]
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj = dict((key, prepare_dict(value)) for (key, value) in obj.items() if key not in list_of_elems)
|
||||
return obj
|
||||
|
||||
|
||||
def scrub_dict(obj):
|
||||
"""
|
||||
Removes any keys from a dictionary that are EMPTY -- this includes parent keys. FortiAnalyzer doesn't
|
||||
like empty keys in dictionaries
|
||||
|
||||
:param obj: Dictionary object to be processed.
|
||||
:type obj: dict
|
||||
|
||||
:return: Processed dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return dict((k, scrub_dict(v)) for k, v in obj.items() if v and scrub_dict(v))
|
||||
else:
|
||||
return obj
|
@ -0,0 +1,477 @@
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||
# still belong to the author of the module, and may assign their own license
|
||||
# to the complete work.
|
||||
#
|
||||
# (c) 2017 Fortinet, Inc
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * 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 __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZ_RC
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZBaseException
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZCommon
|
||||
from ansible.module_utils.network.fortianalyzer.common import scrub_dict
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZMethods
|
||||
|
||||
|
||||
# ACTIVE BUG WITH OUR DEBUG IMPORT CALL - BECAUSE IT'S UNDER MODULE_UTILITIES
|
||||
# WHEN module_common.recursive_finder() runs under the module loader, it looks for this namespace debug import
|
||||
# and because it's not there, it always fails, regardless of it being under a try/catch here.
|
||||
# we're going to move it to a different namespace.
|
||||
# # check for debug lib
|
||||
# try:
|
||||
# from ansible.module_utils.network.fortianalyzer.fortianalyzer_debug import debug_dump
|
||||
# HAS_FAZ_DEBUG = True
|
||||
# except:
|
||||
# HAS_FAZ_DEBUG = False
|
||||
|
||||
|
||||
# BEGIN HANDLER CLASSES
|
||||
class FortiAnalyzerHandler(object):
|
||||
def __init__(self, conn, module):
|
||||
self._conn = conn
|
||||
self._module = module
|
||||
self._tools = FAZCommon
|
||||
self._uses_workspace = None
|
||||
self._uses_adoms = None
|
||||
self._locked_adom_list = list()
|
||||
self._lock_info = None
|
||||
|
||||
self.workspace_check()
|
||||
if self._uses_workspace:
|
||||
self.get_lock_info(adom=self._module.paramgram["adom"])
|
||||
|
||||
def process_request(self, url, datagram, method):
|
||||
"""
|
||||
Formats and Runs the API Request via Connection Plugin. Streamlined for use from Modules.
|
||||
|
||||
:param url: Connection URL to access
|
||||
:type url: string
|
||||
:param datagram: The prepared payload for the API Request in dictionary format
|
||||
:type datagram: dict
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
|
||||
:return: Dictionary containing results of the API Request via Connection Plugin.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
adom = self._module.paramgram["adom"]
|
||||
if self.uses_workspace and adom not in self._locked_adom_list and method != FAZMethods.GET:
|
||||
self.lock_adom(adom=adom)
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(err)
|
||||
|
||||
data = self._tools.format_request(method, url, **datagram)
|
||||
response = self._conn.send_request(method, data)
|
||||
|
||||
try:
|
||||
adom = self._module.paramgram["adom"]
|
||||
if self.uses_workspace and adom in self._locked_adom_list \
|
||||
and response[0] == 0 and method != FAZMethods.GET:
|
||||
self.commit_changes(adom=adom)
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(err)
|
||||
|
||||
# if HAS_FAZ_DEBUG:
|
||||
# try:
|
||||
# debug_dump(response, datagram, self._module.paramgram, url, method)
|
||||
# except BaseException:
|
||||
# pass
|
||||
|
||||
return response
|
||||
|
||||
def workspace_check(self):
|
||||
"""
|
||||
Checks FortiAnalyzer for the use of Workspace mode.
|
||||
"""
|
||||
url = "/cli/global/system/global"
|
||||
data = {"fields": ["workspace-mode", "adom-status"]}
|
||||
resp_obj = self.process_request(url, data, FAZMethods.GET)
|
||||
try:
|
||||
if resp_obj[1]["workspace-mode"] in ["workflow", "normal"]:
|
||||
self.uses_workspace = True
|
||||
elif resp_obj[1]["workspace-mode"] == "disabled":
|
||||
self.uses_workspace = False
|
||||
except KeyError:
|
||||
self.uses_workspace = False
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="Couldn't determine workspace-mode in the plugin. Error: " + str(err))
|
||||
try:
|
||||
if resp_obj[1]["adom-status"] in [1, "enable"]:
|
||||
self.uses_adoms = True
|
||||
else:
|
||||
self.uses_adoms = False
|
||||
except KeyError:
|
||||
self.uses_adoms = False
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="Couldn't determine adom-status in the plugin. Error: " + str(err))
|
||||
|
||||
def run_unlock(self):
|
||||
"""
|
||||
Checks for ADOM status, if locked, it will unlock
|
||||
"""
|
||||
for adom_locked in self._locked_adom_list:
|
||||
self.unlock_adom(adom_locked)
|
||||
|
||||
def lock_adom(self, adom=None):
|
||||
"""
|
||||
Locks an ADOM for changes
|
||||
"""
|
||||
if not adom or adom == "root":
|
||||
url = "/dvmdb/adom/root/workspace/lock"
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.EXEC, url, **datagram)
|
||||
resp_obj = self._conn.send_request(FAZMethods.EXEC, data)
|
||||
code = resp_obj[0]
|
||||
if code == 0 and resp_obj[1]["status"]["message"].lower() == "ok":
|
||||
self.add_adom_to_lock_list(adom)
|
||||
else:
|
||||
lockinfo = self.get_lock_info(adom=adom)
|
||||
self._module.fail_json(msg=("An error occurred trying to lock the adom. Error: "
|
||||
+ str(resp_obj) + ", LOCK INFO: " + str(lockinfo)))
|
||||
return resp_obj
|
||||
|
||||
def unlock_adom(self, adom=None):
|
||||
"""
|
||||
Unlocks an ADOM after changes
|
||||
"""
|
||||
if not adom or adom == "root":
|
||||
url = "/dvmdb/adom/root/workspace/unlock"
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/unlock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.EXEC, url, **datagram)
|
||||
resp_obj = self._conn.send_request(FAZMethods.EXEC, data)
|
||||
code = resp_obj[0]
|
||||
if code == 0 and resp_obj[1]["status"]["message"].lower() == "ok":
|
||||
self.remove_adom_from_lock_list(adom)
|
||||
else:
|
||||
self._module.fail_json(msg=("An error occurred trying to unlock the adom. Error: " + str(resp_obj)))
|
||||
return resp_obj
|
||||
|
||||
def get_lock_info(self, adom=None):
|
||||
"""
|
||||
Gets ADOM lock info so it can be displayed with the error messages. Or if determined to be locked by ansible
|
||||
for some reason, then unlock it.
|
||||
"""
|
||||
if not adom or adom == "root":
|
||||
url = "/dvmdb/adom/root/workspace/lockinfo"
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lockinfo/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lockinfo/".format(adom=adom)
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.GET, url, **datagram)
|
||||
resp_obj = self._conn.send_request(FAZMethods.GET, data)
|
||||
code = resp_obj[0]
|
||||
if code != 0:
|
||||
self._module.fail_json(msg=("An error occurred trying to get the ADOM Lock Info. Error: " + str(resp_obj)))
|
||||
elif code == 0:
|
||||
self._lock_info = resp_obj[1]
|
||||
return resp_obj
|
||||
|
||||
def commit_changes(self, adom=None, aux=False):
|
||||
"""
|
||||
Commits changes to an ADOM
|
||||
"""
|
||||
if not adom or adom == "root":
|
||||
url = "/dvmdb/adom/root/workspace/commit"
|
||||
else:
|
||||
if aux:
|
||||
url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/commit/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.EXEC, url, **datagram)
|
||||
resp_obj = self._conn.send_request(FAZMethods.EXEC, data)
|
||||
code = resp_obj[0]
|
||||
if code != 0:
|
||||
self._module.fail_json(msg=("An error occurred trying to commit changes to the adom. Error: "
|
||||
+ str(resp_obj)))
|
||||
|
||||
def govern_response(self, module, results, msg=None, good_codes=None,
|
||||
stop_on_fail=None, stop_on_success=None, skipped=None,
|
||||
changed=None, unreachable=None, failed=None, success=None, changed_if_success=None,
|
||||
ansible_facts=None):
|
||||
"""
|
||||
This function will attempt to apply default values to canned responses from FortiAnalyzer we know of.
|
||||
This saves time, and turns the response in the module into a "one-liner", while still giving us...
|
||||
the flexibility to directly use return_response in modules if we have too. This function saves repeated code.
|
||||
|
||||
:param module: The Ansible Module CLASS object, used to run fail/exit json
|
||||
:type module: object
|
||||
:param msg: An overridable custom message from the module that called this.
|
||||
:type msg: string
|
||||
:param results: A dictionary object containing an API call results
|
||||
:type results: dict
|
||||
:param good_codes: A list of exit codes considered successful from FortiAnalyzer
|
||||
:type good_codes: list
|
||||
:param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
|
||||
:type stop_on_fail: boolean
|
||||
:param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
|
||||
:type stop_on_success: boolean
|
||||
:param changed: If True, tells Ansible that object was changed (default: false)
|
||||
:type skipped: boolean
|
||||
:param skipped: If True, tells Ansible that object was skipped (default: false)
|
||||
:type skipped: boolean
|
||||
:param unreachable: If True, tells Ansible that object was unreachable (default: false)
|
||||
:type unreachable: boolean
|
||||
:param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param changed_if_success: If True, defaults to changed if successful if you specify or not"
|
||||
:type changed_if_success: boolean
|
||||
:param ansible_facts: A prepared dictionary of ansible facts from the execution.
|
||||
:type ansible_facts: dict
|
||||
"""
|
||||
if module is None and results is None:
|
||||
raise FAZBaseException("govern_response() was called without a module and/or results tuple! Fix!")
|
||||
# Get the Return code from results
|
||||
try:
|
||||
rc = results[0]
|
||||
except BaseException:
|
||||
raise FAZBaseException("govern_response() was called without the return code at results[0]")
|
||||
|
||||
# init a few items
|
||||
rc_data = None
|
||||
|
||||
# Get the default values for the said return code.
|
||||
try:
|
||||
rc_codes = FAZ_RC.get('faz_return_codes')
|
||||
rc_data = rc_codes.get(rc)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
if not rc_data:
|
||||
rc_data = {}
|
||||
# ONLY add to overrides if not none -- This is very important that the keys aren't added at this stage
|
||||
# if they are empty. And there aren't that many, so let's just do a few if then statements.
|
||||
if good_codes is not None:
|
||||
rc_data["good_codes"] = good_codes
|
||||
if stop_on_fail is not None:
|
||||
rc_data["stop_on_fail"] = stop_on_fail
|
||||
if stop_on_success is not None:
|
||||
rc_data["stop_on_success"] = stop_on_success
|
||||
if skipped is not None:
|
||||
rc_data["skipped"] = skipped
|
||||
if changed is not None:
|
||||
rc_data["changed"] = changed
|
||||
if unreachable is not None:
|
||||
rc_data["unreachable"] = unreachable
|
||||
if failed is not None:
|
||||
rc_data["failed"] = failed
|
||||
if success is not None:
|
||||
rc_data["success"] = success
|
||||
if changed_if_success is not None:
|
||||
rc_data["changed_if_success"] = changed_if_success
|
||||
if results is not None:
|
||||
rc_data["results"] = results
|
||||
if msg is not None:
|
||||
rc_data["msg"] = msg
|
||||
if ansible_facts is None:
|
||||
rc_data["ansible_facts"] = {}
|
||||
else:
|
||||
rc_data["ansible_facts"] = ansible_facts
|
||||
|
||||
return self.return_response(module=module,
|
||||
results=results,
|
||||
msg=rc_data.get("msg", "NULL"),
|
||||
good_codes=rc_data.get("good_codes", (0,)),
|
||||
stop_on_fail=rc_data.get("stop_on_fail", True),
|
||||
stop_on_success=rc_data.get("stop_on_success", False),
|
||||
skipped=rc_data.get("skipped", False),
|
||||
changed=rc_data.get("changed", False),
|
||||
changed_if_success=rc_data.get("changed_if_success", False),
|
||||
unreachable=rc_data.get("unreachable", False),
|
||||
failed=rc_data.get("failed", False),
|
||||
success=rc_data.get("success", False),
|
||||
ansible_facts=rc_data.get("ansible_facts", dict()))
|
||||
|
||||
def return_response(self, module, results, msg="NULL", good_codes=(0,),
|
||||
stop_on_fail=True, stop_on_success=False, skipped=False,
|
||||
changed=False, unreachable=False, failed=False, success=False, changed_if_success=True,
|
||||
ansible_facts=()):
|
||||
"""
|
||||
This function controls the logout and error reporting after an method or function runs. The exit_json for
|
||||
ansible comes from logic within this function. If this function returns just the msg, it means to continue
|
||||
execution on the playbook. It is called from the ansible module, or from the self.govern_response function.
|
||||
|
||||
:param module: The Ansible Module CLASS object, used to run fail/exit json
|
||||
:type module: object
|
||||
:param msg: An overridable custom message from the module that called this.
|
||||
:type msg: string
|
||||
:param results: A dictionary object containing an API call results
|
||||
:type results: dict
|
||||
:param good_codes: A list of exit codes considered successful from FortiAnalyzer
|
||||
:type good_codes: list
|
||||
:param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true)
|
||||
:type stop_on_fail: boolean
|
||||
:param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false)
|
||||
:type stop_on_success: boolean
|
||||
:param changed: If True, tells Ansible that object was changed (default: false)
|
||||
:type skipped: boolean
|
||||
:param skipped: If True, tells Ansible that object was skipped (default: false)
|
||||
:type skipped: boolean
|
||||
:param unreachable: If True, tells Ansible that object was unreachable (default: false)
|
||||
:type unreachable: boolean
|
||||
:param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false)
|
||||
:type unreachable: boolean
|
||||
:param changed_if_success: If True, defaults to changed if successful if you specify or not"
|
||||
:type changed_if_success: boolean
|
||||
:param ansible_facts: A prepared dictionary of ansible facts from the execution.
|
||||
:type ansible_facts: dict
|
||||
|
||||
:return: A string object that contains an error message
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
# VALIDATION ERROR
|
||||
if (len(results) == 0) or (failed and success) or (changed and unreachable):
|
||||
module.exit_json(msg="Handle_response was called with no results, or conflicting failed/success or "
|
||||
"changed/unreachable parameters. Fix the exit code on module. "
|
||||
"Generic Failure", failed=True)
|
||||
|
||||
# IDENTIFY SUCCESS/FAIL IF NOT DEFINED
|
||||
if not failed and not success:
|
||||
if len(results) > 0:
|
||||
if results[0] not in good_codes:
|
||||
failed = True
|
||||
elif results[0] in good_codes:
|
||||
success = True
|
||||
|
||||
if len(results) > 0:
|
||||
# IF NO MESSAGE WAS SUPPLIED, GET IT FROM THE RESULTS, IF THAT DOESN'T WORK, THEN WRITE AN ERROR MESSAGE
|
||||
if msg == "NULL":
|
||||
try:
|
||||
msg = results[1]['status']['message']
|
||||
except BaseException:
|
||||
msg = "No status message returned at results[1][status][message], " \
|
||||
"and none supplied to msg parameter for handle_response."
|
||||
|
||||
if failed:
|
||||
# BECAUSE SKIPPED/FAILED WILL OFTEN OCCUR ON CODES THAT DON'T GET INCLUDED, THEY ARE CONSIDERED FAILURES
|
||||
# HOWEVER, THEY ARE MUTUALLY EXCLUSIVE, SO IF IT IS MARKED SKIPPED OR UNREACHABLE BY THE MODULE LOGIC
|
||||
# THEN REMOVE THE FAILED FLAG SO IT DOESN'T OVERRIDE THE DESIRED STATUS OF SKIPPED OR UNREACHABLE.
|
||||
if failed and skipped:
|
||||
failed = False
|
||||
if failed and unreachable:
|
||||
failed = False
|
||||
if stop_on_fail:
|
||||
if self._uses_workspace:
|
||||
try:
|
||||
self.run_unlock()
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg=("Couldn't unlock ADOM! Error: " + str(err)))
|
||||
module.exit_json(msg=msg, failed=failed, changed=changed, unreachable=unreachable, skipped=skipped,
|
||||
results=results[1], ansible_facts=ansible_facts, rc=results[0],
|
||||
invocation={"module_args": ansible_facts["ansible_params"]})
|
||||
elif success:
|
||||
if changed_if_success:
|
||||
changed = True
|
||||
success = False
|
||||
if stop_on_success:
|
||||
if self._uses_workspace:
|
||||
try:
|
||||
self.run_unlock()
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg=("Couldn't unlock ADOM! Error: " + str(err)))
|
||||
module.exit_json(msg=msg, success=success, changed=changed, unreachable=unreachable,
|
||||
skipped=skipped, results=results[1], ansible_facts=ansible_facts, rc=results[0],
|
||||
invocation={"module_args": ansible_facts["ansible_params"]})
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
def construct_ansible_facts(response, ansible_params, paramgram, *args, **kwargs):
|
||||
"""
|
||||
Constructs a dictionary to return to ansible facts, containing various information about the execution.
|
||||
|
||||
:param response: Contains the response from the FortiAnalyzer.
|
||||
:type response: dict
|
||||
:param ansible_params: Contains the parameters Ansible was called with.
|
||||
:type ansible_params: dict
|
||||
:param paramgram: Contains the paramgram passed to the modules' local modify function.
|
||||
:type paramgram: dict
|
||||
:param args: Free-form arguments that could be added.
|
||||
:param kwargs: Free-form keyword arguments that could be added.
|
||||
|
||||
:return: A dictionary containing lots of information to append to Ansible Facts.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
facts = {
|
||||
"response": response,
|
||||
"ansible_params": scrub_dict(ansible_params),
|
||||
"paramgram": scrub_dict(paramgram),
|
||||
}
|
||||
|
||||
if args:
|
||||
facts["custom_args"] = args
|
||||
if kwargs:
|
||||
facts.update(kwargs)
|
||||
|
||||
return facts
|
||||
|
||||
@property
|
||||
def uses_workspace(self):
|
||||
return self._uses_workspace
|
||||
|
||||
@uses_workspace.setter
|
||||
def uses_workspace(self, val):
|
||||
self._uses_workspace = val
|
||||
|
||||
@property
|
||||
def uses_adoms(self):
|
||||
return self._uses_adoms
|
||||
|
||||
@uses_adoms.setter
|
||||
def uses_adoms(self, val):
|
||||
self._uses_adoms = val
|
||||
|
||||
def add_adom_to_lock_list(self, adom):
|
||||
if adom not in self._locked_adom_list:
|
||||
self._locked_adom_list.append(adom)
|
||||
|
||||
def remove_adom_from_lock_list(self, adom):
|
||||
if adom in self._locked_adom_list:
|
||||
self._locked_adom_list.remove(adom)
|
@ -0,0 +1,439 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
ANSIBLE_METADATA = {
|
||||
"metadata_version": "1.1",
|
||||
"status": ["preview"],
|
||||
"supported_by": "community"
|
||||
}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: faz_device
|
||||
version_added: "2.9"
|
||||
author: Luke Weighall (@lweighall)
|
||||
short_description: Add or remove device
|
||||
description:
|
||||
- Add or remove a device or list of devices to FortiAnalyzer Device Manager. ADOM Capable.
|
||||
|
||||
options:
|
||||
adom:
|
||||
description:
|
||||
- The ADOM the configuration should belong to.
|
||||
required: true
|
||||
default: root
|
||||
type: str
|
||||
|
||||
mode:
|
||||
description:
|
||||
- Add or delete devices. Or promote unregistered devices that are in the FortiAnalyzer "waiting pool"
|
||||
required: false
|
||||
default: add
|
||||
choices: ["add", "delete", "promote"]
|
||||
type: str
|
||||
|
||||
device_username:
|
||||
description:
|
||||
- The username of the device being added to FortiAnalyzer.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
device_password:
|
||||
description:
|
||||
- The password of the device being added to FortiAnalyzer.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
device_ip:
|
||||
description:
|
||||
- The IP of the device being added to FortiAnalyzer.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
device_unique_name:
|
||||
description:
|
||||
- The desired "friendly" name of the device being added to FortiAnalyzer.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
device_serial:
|
||||
description:
|
||||
- The serial number of the device being added to FortiAnalyzer.
|
||||
required: false
|
||||
type: str
|
||||
|
||||
os_type:
|
||||
description:
|
||||
- The os type of the device being added (default 0).
|
||||
required: true
|
||||
choices: ["unknown", "fos", "fsw", "foc", "fml", "faz", "fwb", "fch", "fct", "log", "fmg", "fsa", "fdd", "fac"]
|
||||
type: str
|
||||
|
||||
mgmt_mode:
|
||||
description:
|
||||
- Management Mode of the device you are adding.
|
||||
choices: ["unreg", "fmg", "faz", "fmgfaz"]
|
||||
required: true
|
||||
type: str
|
||||
|
||||
os_minor_vers:
|
||||
description:
|
||||
- Minor OS rev of the device.
|
||||
required: true
|
||||
type: str
|
||||
|
||||
os_ver:
|
||||
description:
|
||||
- Major OS rev of the device
|
||||
required: true
|
||||
choices: ["unknown", "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0"]
|
||||
type: str
|
||||
|
||||
platform_str:
|
||||
description:
|
||||
- Required for determine the platform for VM platforms. ie FortiGate-VM64
|
||||
required: false
|
||||
type: str
|
||||
|
||||
faz_quota:
|
||||
description:
|
||||
- Specifies the quota for the device in FAZ
|
||||
required: False
|
||||
type: str
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: DISCOVER AND ADD DEVICE A PHYSICAL FORTIGATE
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_username: "admin"
|
||||
device_password: "admin"
|
||||
device_ip: "10.10.24.201"
|
||||
device_unique_name: "FGT1"
|
||||
device_serial: "FGVM000000117994"
|
||||
state: "present"
|
||||
mgmt_mode: "faz"
|
||||
os_type: "fos"
|
||||
os_ver: "5.0"
|
||||
minor_rev: 6
|
||||
|
||||
|
||||
- name: DISCOVER AND ADD DEVICE A VIRTUAL FORTIGATE
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_username: "admin"
|
||||
device_password: "admin"
|
||||
device_ip: "10.10.24.202"
|
||||
device_unique_name: "FGT2"
|
||||
mgmt_mode: "faz"
|
||||
os_type: "fos"
|
||||
os_ver: "5.0"
|
||||
minor_rev: 6
|
||||
state: "present"
|
||||
platform_str: "FortiGate-VM64"
|
||||
|
||||
- name: DELETE DEVICE FGT01
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_unique_name: "ansible-fgt01"
|
||||
mode: "delete"
|
||||
|
||||
- name: DELETE DEVICE FGT02
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_unique_name: "ansible-fgt02"
|
||||
mode: "delete"
|
||||
|
||||
- name: PROMOTE FGT01 IN FAZ BY IP
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_password: "fortinet"
|
||||
device_ip: "10.7.220.151"
|
||||
device_username: "ansible"
|
||||
mgmt_mode: "faz"
|
||||
mode: "promote"
|
||||
|
||||
|
||||
- name: PROMOTE FGT02 IN FAZ
|
||||
faz_device:
|
||||
adom: "root"
|
||||
device_password: "fortinet"
|
||||
device_unique_name: "ansible-fgt02"
|
||||
device_username: "ansible"
|
||||
mgmt_mode: "faz"
|
||||
mode: "promote"
|
||||
|
||||
'''
|
||||
|
||||
RETURN = """
|
||||
api_result:
|
||||
description: full API response, includes status code and message
|
||||
returned: always
|
||||
type: str
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.connection import Connection
|
||||
from ansible.module_utils.network.fortianalyzer.fortianalyzer import FortiAnalyzerHandler
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZBaseException
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZCommon
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZMethods
|
||||
from ansible.module_utils.network.fortianalyzer.common import DEFAULT_RESULT_OBJ
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAIL_SOCKET_MSG
|
||||
|
||||
|
||||
def faz_add_device(faz, paramgram):
|
||||
"""
|
||||
This method is used to add devices to the faz or delete them
|
||||
"""
|
||||
|
||||
datagram = {
|
||||
"adom": paramgram["adom"],
|
||||
"device": {"adm_usr": paramgram["device_username"], "adm_pass": paramgram["device_password"],
|
||||
"ip": paramgram["ip"], "name": paramgram["device_unique_name"],
|
||||
"mgmt_mode": paramgram["mgmt_mode"], "os_type": paramgram["os_type"],
|
||||
"mr": paramgram["os_minor_vers"]}
|
||||
}
|
||||
|
||||
if paramgram["platform_str"] is not None:
|
||||
datagram["device"]["platform_str"] = paramgram["platform_str"]
|
||||
|
||||
if paramgram["sn"] is not None:
|
||||
datagram["device"]["sn"] = paramgram["sn"]
|
||||
|
||||
if paramgram["device_action"] is not None:
|
||||
datagram["device"]["device_action"] = paramgram["device_action"]
|
||||
|
||||
if paramgram["faz.quota"] is not None:
|
||||
datagram["device"]["faz.quota"] = paramgram["faz.quota"]
|
||||
|
||||
url = '/dvm/cmd/add/device/'
|
||||
response = faz.process_request(url, datagram, FAZMethods.EXEC)
|
||||
return response
|
||||
|
||||
|
||||
def faz_delete_device(faz, paramgram):
|
||||
"""
|
||||
This method deletes a device from the FAZ
|
||||
"""
|
||||
datagram = {
|
||||
"adom": paramgram["adom"],
|
||||
"device": paramgram["device_unique_name"],
|
||||
}
|
||||
|
||||
url = '/dvm/cmd/del/device/'
|
||||
response = faz.process_request(url, datagram, FAZMethods.EXEC)
|
||||
return response
|
||||
|
||||
|
||||
def faz_get_unknown_devices(faz):
|
||||
"""
|
||||
This method gets devices with an unknown management type field
|
||||
"""
|
||||
|
||||
faz_filter = ["mgmt_mode", "==", "0"]
|
||||
|
||||
datagram = {
|
||||
"filter": faz_filter
|
||||
}
|
||||
|
||||
url = "/dvmdb/device"
|
||||
response = faz.process_request(url, datagram, FAZMethods.GET)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def faz_approve_unregistered_device_by_ip(faz, paramgram):
|
||||
"""
|
||||
This method approves unregistered devices by ip.
|
||||
"""
|
||||
# TRY TO FIND DETAILS ON THIS UNREGISTERED DEVICE
|
||||
unknown_devices = faz_get_unknown_devices(faz)
|
||||
target_device = None
|
||||
if unknown_devices[0] == 0:
|
||||
for device in unknown_devices[1]:
|
||||
if device["ip"] == paramgram["ip"]:
|
||||
target_device = device
|
||||
else:
|
||||
return "No devices are waiting to be registered!"
|
||||
|
||||
# now that we have the target device details...fill out the datagram and make the call to promote it
|
||||
if target_device is not None:
|
||||
target_device_paramgram = {
|
||||
"adom": paramgram["adom"],
|
||||
"ip": target_device["ip"],
|
||||
"device_username": paramgram["device_username"],
|
||||
"device_password": paramgram["device_password"],
|
||||
"device_unique_name": paramgram["device_unique_name"],
|
||||
"sn": target_device["sn"],
|
||||
"os_type": target_device["os_type"],
|
||||
"mgmt_mode": paramgram["mgmt_mode"],
|
||||
"os_minor_vers": target_device["mr"],
|
||||
"os_ver": target_device["os_ver"],
|
||||
"platform_str": target_device["platform_str"],
|
||||
"faz.quota": target_device["faz.quota"],
|
||||
"device_action": paramgram["device_action"]
|
||||
}
|
||||
|
||||
add_device = faz_add_device(faz, target_device_paramgram)
|
||||
return add_device
|
||||
|
||||
return str("Couldn't find the desired device with ip: " + str(paramgram["device_ip"]))
|
||||
|
||||
|
||||
def faz_approve_unregistered_device_by_name(faz, paramgram):
|
||||
# TRY TO FIND DETAILS ON THIS UNREGISTERED DEVICE
|
||||
unknown_devices = faz_get_unknown_devices(faz)
|
||||
target_device = None
|
||||
if unknown_devices[0] == 0:
|
||||
for device in unknown_devices[1]:
|
||||
if device["name"] == paramgram["device_unique_name"]:
|
||||
target_device = device
|
||||
else:
|
||||
return "No devices are waiting to be registered!"
|
||||
|
||||
# now that we have the target device details...fill out the datagram and make the call to promote it
|
||||
if target_device is not None:
|
||||
target_device_paramgram = {
|
||||
"adom": paramgram["adom"],
|
||||
"ip": target_device["ip"],
|
||||
"device_username": paramgram["device_username"],
|
||||
"device_password": paramgram["device_password"],
|
||||
"device_unique_name": paramgram["device_unique_name"],
|
||||
"sn": target_device["sn"],
|
||||
"os_type": target_device["os_type"],
|
||||
"mgmt_mode": paramgram["mgmt_mode"],
|
||||
"os_minor_vers": target_device["mr"],
|
||||
"os_ver": target_device["os_ver"],
|
||||
"platform_str": target_device["platform_str"],
|
||||
"faz.quota": target_device["faz.quota"],
|
||||
"device_action": paramgram["device_action"]
|
||||
}
|
||||
|
||||
add_device = faz_add_device(faz, target_device_paramgram)
|
||||
return add_device
|
||||
|
||||
return str("Couldn't find the desired device with name: " + str(paramgram["device_unique_name"]))
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
adom=dict(required=False, type="str", default="root"),
|
||||
mode=dict(choices=["add", "delete", "promote"], type="str", default="add"),
|
||||
|
||||
device_ip=dict(required=False, type="str"),
|
||||
device_username=dict(required=False, type="str"),
|
||||
device_password=dict(required=False, type="str", no_log=True),
|
||||
device_unique_name=dict(required=False, type="str"),
|
||||
device_serial=dict(required=False, type="str"),
|
||||
|
||||
os_type=dict(required=False, type="str", choices=["unknown", "fos", "fsw", "foc", "fml",
|
||||
"faz", "fwb", "fch", "fct", "log", "fmg",
|
||||
"fsa", "fdd", "fac"]),
|
||||
mgmt_mode=dict(required=False, type="str", choices=["unreg", "fmg", "faz", "fmgfaz"]),
|
||||
os_minor_vers=dict(required=False, type="str"),
|
||||
os_ver=dict(required=False, type="str", choices=["unknown", "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0"]),
|
||||
platform_str=dict(required=False, type="str"),
|
||||
faz_quota=dict(required=False, type="str")
|
||||
)
|
||||
|
||||
required_if = [
|
||||
['mode', 'delete', ['device_unique_name']],
|
||||
['mode', 'add', ['device_serial', 'device_username',
|
||||
'device_password', 'device_unique_name', 'device_ip', 'mgmt_mode', 'platform_str']]
|
||||
|
||||
]
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=True, required_if=required_if, )
|
||||
|
||||
# START SESSION LOGIC
|
||||
paramgram = {
|
||||
"adom": module.params["adom"],
|
||||
"mode": module.params["mode"],
|
||||
"ip": module.params["device_ip"],
|
||||
"device_username": module.params["device_username"],
|
||||
"device_password": module.params["device_password"],
|
||||
"device_unique_name": module.params["device_unique_name"],
|
||||
"sn": module.params["device_serial"],
|
||||
"os_type": module.params["os_type"],
|
||||
"mgmt_mode": module.params["mgmt_mode"],
|
||||
"os_minor_vers": module.params["os_minor_vers"],
|
||||
"os_ver": module.params["os_ver"],
|
||||
"platform_str": module.params["platform_str"],
|
||||
"faz.quota": module.params["faz_quota"],
|
||||
"device_action": None
|
||||
}
|
||||
# INSERT THE PARAMGRAM INTO THE MODULE SO WHEN WE PASS IT TO MOD_UTILS.FortiManagerHandler IT HAS THAT INFO
|
||||
|
||||
if paramgram["mode"] == "add":
|
||||
paramgram["device_action"] = "add_model"
|
||||
elif paramgram["mode"] == "promote":
|
||||
paramgram["device_action"] = "promote_unreg"
|
||||
module.paramgram = paramgram
|
||||
|
||||
# TRY TO INIT THE CONNECTION SOCKET PATH AND FortiManagerHandler OBJECT AND TOOLS
|
||||
faz = None
|
||||
if module._socket_path:
|
||||
connection = Connection(module._socket_path)
|
||||
faz = FortiAnalyzerHandler(connection, module)
|
||||
faz.tools = FAZCommon()
|
||||
else:
|
||||
module.fail_json(**FAIL_SOCKET_MSG)
|
||||
|
||||
# BEGIN MODULE-SPECIFIC LOGIC -- THINGS NEED TO HAPPEN DEPENDING ON THE ENDPOINT AND OPERATION
|
||||
results = DEFAULT_RESULT_OBJ
|
||||
|
||||
try:
|
||||
if paramgram["mode"] == "add":
|
||||
results = faz_add_device(faz, paramgram)
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="An error occurred trying to add the device. Error: " + str(err))
|
||||
|
||||
try:
|
||||
if paramgram["mode"] == "promote":
|
||||
if paramgram["ip"] is not None:
|
||||
results = faz_approve_unregistered_device_by_ip(faz, paramgram)
|
||||
elif paramgram["device_unique_name"] is not None:
|
||||
results = faz_approve_unregistered_device_by_name(faz, paramgram)
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="An error occurred trying to promote the device. Error: " + str(err))
|
||||
|
||||
try:
|
||||
if paramgram["mode"] == "delete":
|
||||
results = faz_delete_device(faz, paramgram)
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="An error occurred trying to delete the device. Error: " + str(err))
|
||||
|
||||
# PROCESS RESULTS
|
||||
try:
|
||||
faz.govern_response(module=module, results=results,
|
||||
ansible_facts=faz.construct_ansible_facts(results, module.params, paramgram))
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg="An error occurred with govern_response(). Error: " + str(err))
|
||||
|
||||
# This should only be hit if faz.govern_response is missed or failed somehow. In fact. It should never be hit.
|
||||
# But it's here JIC.
|
||||
return module.exit_json(**results[1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,449 @@
|
||||
# Copyright (c) 2018 Fortinet and/or its affiliates.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
author:
|
||||
- Luke Weighall (@lweighall)
|
||||
- Andrew Welsh (@Ghilli3)
|
||||
- Jim Huber (@p4r4n0y1ng)
|
||||
httpapi : fortianalyzer
|
||||
short_description: HttpApi Plugin for Fortinet FortiAnalyzer Appliance or VM
|
||||
description:
|
||||
- This HttpApi plugin provides methods to connect to Fortinet FortiAnalyzer Appliance or VM via JSON RPC API
|
||||
version_added: "2.9"
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from ansible.plugins.httpapi import HttpApiBase
|
||||
from ansible.module_utils.basic import to_text
|
||||
from ansible.module_utils.network.fortianalyzer.common import BASE_HEADERS
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZBaseException
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZCommon
|
||||
from ansible.module_utils.network.fortianalyzer.common import FAZMethods
|
||||
|
||||
|
||||
class HttpApi(HttpApiBase):
|
||||
def __init__(self, connection):
|
||||
super(HttpApi, self).__init__(connection)
|
||||
self._req_id = 0
|
||||
self._sid = None
|
||||
self._url = "/jsonrpc"
|
||||
self._host = None
|
||||
self._tools = FAZCommon
|
||||
self._debug = False
|
||||
self._connected_faz = None
|
||||
self._last_response_msg = None
|
||||
self._last_response_code = None
|
||||
self._last_data_payload = None
|
||||
self._last_url = None
|
||||
self._last_response_raw = None
|
||||
self._locked_adom_list = list()
|
||||
self._locked_adoms_by_user = list()
|
||||
self._uses_workspace = False
|
||||
self._uses_adoms = False
|
||||
self._adom_list = list()
|
||||
self._logged_in_user = None
|
||||
|
||||
def set_become(self, become_context):
|
||||
"""
|
||||
ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED
|
||||
:param become_context: Unused input.
|
||||
:return: None
|
||||
"""
|
||||
return None
|
||||
|
||||
def update_auth(self, response, response_data):
|
||||
"""
|
||||
TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH
|
||||
:param response: Unused input.
|
||||
:param response_data Unused_input.
|
||||
:return: None
|
||||
"""
|
||||
return None
|
||||
|
||||
def login(self, username, password):
|
||||
"""
|
||||
This function will log the plugin into FortiAnalyzer, and return the results.
|
||||
:param username: Username of FortiAnalyzer Admin
|
||||
:param password: Password of FortiAnalyzer Admin
|
||||
|
||||
:return: Dictionary of status, if it logged in or not.
|
||||
"""
|
||||
|
||||
self._logged_in_user = username
|
||||
self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, "sys/login/user",
|
||||
passwd=password, user=username,))
|
||||
|
||||
if "FortiAnalyzer object connected to FortiAnalyzer" in self.__str__():
|
||||
# If Login worked, then inspect the FortiAnalyzer for Workspace Mode, and it's system information.
|
||||
self.inspect_faz()
|
||||
return
|
||||
else:
|
||||
raise FAZBaseException(msg="Unknown error while logging in...connection was lost during login operation...."
|
||||
" Exiting")
|
||||
|
||||
def inspect_faz(self):
|
||||
# CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS
|
||||
status = self.get_system_status()
|
||||
if status[0] == -11:
|
||||
# THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN
|
||||
self.logout()
|
||||
raise FAZBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors."
|
||||
" Exiting")
|
||||
elif status[0] == 0:
|
||||
try:
|
||||
self.check_mode()
|
||||
if self._uses_adoms:
|
||||
self.get_adom_list()
|
||||
if self._uses_workspace:
|
||||
self.get_locked_adom_list()
|
||||
self._connected_faz = status[1]
|
||||
self._host = self._connected_faz["Hostname"]
|
||||
except BaseException:
|
||||
pass
|
||||
return
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
This function will logout of the FortiAnalyzer.
|
||||
"""
|
||||
if self.sid is not None:
|
||||
# IF WE WERE USING WORKSPACES, THEN CLEAN UP OUR LOCKS IF THEY STILL EXIST
|
||||
if self.uses_workspace:
|
||||
self.get_lock_info()
|
||||
self.run_unlock()
|
||||
ret_code, response = self.send_request(FAZMethods.EXEC,
|
||||
self._tools.format_request(FAZMethods.EXEC, "sys/logout"))
|
||||
self.sid = None
|
||||
return ret_code, response
|
||||
|
||||
def send_request(self, method, params):
|
||||
"""
|
||||
Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well.
|
||||
:param params: A formatted dictionary that was returned by self.common_datagram_params()
|
||||
before being called here.
|
||||
:param method: The preferred API Request method (GET, ADD, POST, etc....)
|
||||
:type method: basestring
|
||||
|
||||
:return: Dictionary of status, if it logged in or not.
|
||||
"""
|
||||
|
||||
try:
|
||||
if self.sid is None and params[0]["url"] != "sys/login/user":
|
||||
raise FAZBaseException("An attempt was made to login with the SID None and URL != login url.")
|
||||
except IndexError:
|
||||
raise FAZBaseException("An attempt was made at communicating with a FAZ with "
|
||||
"no valid session and an incorrectly formatted request.")
|
||||
except Exception:
|
||||
raise FAZBaseException("An attempt was made at communicating with a FAZ with "
|
||||
"no valid session and an unexpected error was discovered.")
|
||||
|
||||
self._update_request_id()
|
||||
json_request = {
|
||||
"method": method,
|
||||
"params": params,
|
||||
"session": self.sid,
|
||||
"id": self.req_id,
|
||||
"verbose": 1
|
||||
}
|
||||
data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\')
|
||||
try:
|
||||
# Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins
|
||||
response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data),
|
||||
headers=BASE_HEADERS)
|
||||
# Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below
|
||||
result = json.loads(to_text(response_data.getvalue()))
|
||||
self._update_self_from_response(result, self._url, data)
|
||||
return self._handle_response(result)
|
||||
except Exception as err:
|
||||
raise FAZBaseException(err)
|
||||
|
||||
def _handle_response(self, response):
|
||||
self._set_sid(response)
|
||||
if isinstance(response["result"], list):
|
||||
result = response["result"][0]
|
||||
else:
|
||||
result = response["result"]
|
||||
if "data" in result:
|
||||
return result["status"]["code"], result["data"]
|
||||
else:
|
||||
return result["status"]["code"], result
|
||||
|
||||
def _update_self_from_response(self, response, url, data):
|
||||
self._last_response_raw = response
|
||||
if isinstance(response["result"], list):
|
||||
result = response["result"][0]
|
||||
else:
|
||||
result = response["result"]
|
||||
if "status" in result:
|
||||
self._last_response_code = result["status"]["code"]
|
||||
self._last_response_msg = result["status"]["message"]
|
||||
self._last_url = url
|
||||
self._last_data_payload = data
|
||||
|
||||
def _set_sid(self, response):
|
||||
if self.sid is None and "session" in response:
|
||||
self.sid = response["session"]
|
||||
|
||||
def return_connected_faz(self):
|
||||
"""
|
||||
Returns the data stored under self._connected_faz
|
||||
|
||||
:return: dict
|
||||
"""
|
||||
try:
|
||||
if self._connected_faz:
|
||||
return self._connected_faz
|
||||
except BaseException:
|
||||
raise FAZBaseException("Couldn't Retrieve Connected FAZ Stats")
|
||||
|
||||
def get_system_status(self):
|
||||
"""
|
||||
Returns the system status page from the FortiAnalyzer, for logging and other uses.
|
||||
return: status
|
||||
"""
|
||||
status = self.send_request(FAZMethods.GET, self._tools.format_request(FAZMethods.GET, "sys/status"))
|
||||
return status
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
return self._debug
|
||||
|
||||
@debug.setter
|
||||
def debug(self, val):
|
||||
self._debug = val
|
||||
|
||||
@property
|
||||
def req_id(self):
|
||||
return self._req_id
|
||||
|
||||
@req_id.setter
|
||||
def req_id(self, val):
|
||||
self._req_id = val
|
||||
|
||||
def _update_request_id(self, reqid=0):
|
||||
self.req_id = reqid if reqid != 0 else self.req_id + 1
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
return self._sid
|
||||
|
||||
@sid.setter
|
||||
def sid(self, val):
|
||||
self._sid = val
|
||||
|
||||
def __str__(self):
|
||||
if self.sid is not None and self.connection._url is not None:
|
||||
return "FortiAnalyzer object connected to FortiAnalyzer: " + str(self.connection._url)
|
||||
return "FortiAnalyzer object with no valid connection to a FortiAnalyzer appliance."
|
||||
|
||||
##################################
|
||||
# BEGIN DATABASE LOCK CONTEXT CODE
|
||||
##################################
|
||||
|
||||
@property
|
||||
def uses_workspace(self):
|
||||
return self._uses_workspace
|
||||
|
||||
@uses_workspace.setter
|
||||
def uses_workspace(self, val):
|
||||
self._uses_workspace = val
|
||||
|
||||
@property
|
||||
def uses_adoms(self):
|
||||
return self._uses_adoms
|
||||
|
||||
@uses_adoms.setter
|
||||
def uses_adoms(self, val):
|
||||
self._uses_adoms = val
|
||||
|
||||
def add_adom_to_lock_list(self, adom):
|
||||
if adom not in self._locked_adom_list:
|
||||
self._locked_adom_list.append(adom)
|
||||
|
||||
def remove_adom_from_lock_list(self, adom):
|
||||
if adom in self._locked_adom_list:
|
||||
self._locked_adom_list.remove(adom)
|
||||
|
||||
def check_mode(self):
|
||||
"""
|
||||
Checks FortiAnalyzer for the use of Workspace mode
|
||||
"""
|
||||
url = "/cli/global/system/global"
|
||||
code, resp_obj = self.send_request(FAZMethods.GET,
|
||||
self._tools.format_request(FAZMethods.GET,
|
||||
url,
|
||||
fields=["workspace-mode", "adom-status"]))
|
||||
try:
|
||||
if resp_obj["workspace-mode"] == "workflow":
|
||||
self.uses_workspace = True
|
||||
elif resp_obj["workspace-mode"] == "disabled":
|
||||
self.uses_workspace = False
|
||||
except KeyError:
|
||||
self.uses_workspace = False
|
||||
except BaseException:
|
||||
raise FAZBaseException(msg="Couldn't determine workspace-mode in the plugin")
|
||||
try:
|
||||
if resp_obj["adom-status"] in [1, "enable"]:
|
||||
self.uses_adoms = True
|
||||
else:
|
||||
self.uses_adoms = False
|
||||
except KeyError:
|
||||
self.uses_adoms = False
|
||||
except BaseException:
|
||||
raise FAZBaseException(msg="Couldn't determine adom-status in the plugin")
|
||||
|
||||
def run_unlock(self):
|
||||
"""
|
||||
Checks for ADOM status, if locked, it will unlock
|
||||
"""
|
||||
for adom_locked in self._locked_adoms_by_user:
|
||||
adom = adom_locked["adom"]
|
||||
self.unlock_adom(adom)
|
||||
|
||||
def lock_adom(self, adom=None, *args, **kwargs):
|
||||
"""
|
||||
Locks an ADOM for changes
|
||||
"""
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/lock"
|
||||
code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.add_adom_to_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def unlock_adom(self, adom=None, *args, **kwargs):
|
||||
"""
|
||||
Unlocks an ADOM after changes
|
||||
"""
|
||||
if adom:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/unlock/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/unlock"
|
||||
code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
|
||||
if code == 0 and respobj["status"]["message"].lower() == "ok":
|
||||
self.remove_adom_from_lock_list(adom)
|
||||
return code, respobj
|
||||
|
||||
def commit_changes(self, adom=None, aux=False, *args, **kwargs):
|
||||
"""
|
||||
Commits changes to an ADOM
|
||||
"""
|
||||
if adom:
|
||||
if aux:
|
||||
url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/commit/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
|
||||
else:
|
||||
url = "/dvmdb/adom/root/workspace/commit"
|
||||
return self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
|
||||
|
||||
def get_lock_info(self, adom=None):
|
||||
"""
|
||||
Gets ADOM lock info so it can be displayed with the error messages. Or if determined to be locked by ansible
|
||||
for some reason, then unlock it.
|
||||
"""
|
||||
if not adom or adom == "root":
|
||||
url = "/dvmdb/adom/root/workspace/lockinfo"
|
||||
else:
|
||||
if adom.lower() == "global":
|
||||
url = "/dvmdb/global/workspace/lockinfo/"
|
||||
else:
|
||||
url = "/dvmdb/adom/{adom}/workspace/lockinfo/".format(adom=adom)
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.GET, url, **datagram)
|
||||
resp_obj = self.send_request(FAZMethods.GET, data)
|
||||
code = resp_obj[0]
|
||||
if code != 0:
|
||||
self._module.fail_json(msg=("An error occurred trying to get the ADOM Lock Info. Error: " + str(resp_obj)))
|
||||
elif code == 0:
|
||||
try:
|
||||
if resp_obj[1]["status"]["message"] == "OK":
|
||||
self._lock_info = None
|
||||
except BaseException:
|
||||
self._lock_info = resp_obj[1]
|
||||
return resp_obj
|
||||
|
||||
def get_adom_list(self):
|
||||
"""
|
||||
Gets the list of ADOMs for the FortiAnalyzer
|
||||
"""
|
||||
if self.uses_adoms:
|
||||
url = "/dvmdb/adom"
|
||||
datagram = {}
|
||||
data = self._tools.format_request(FAZMethods.GET, url, **datagram)
|
||||
resp_obj = self.send_request(FAZMethods.GET, data)
|
||||
code = resp_obj[0]
|
||||
if code != 0:
|
||||
self._module.fail_json(msg=("An error occurred trying to get the ADOM Info. Error: " + str(resp_obj)))
|
||||
elif code == 0:
|
||||
num_of_adoms = len(resp_obj[1])
|
||||
append_list = ['root', ]
|
||||
for adom in resp_obj[1]:
|
||||
if adom["tab_status"] != "":
|
||||
append_list.append(str(adom["name"]))
|
||||
self._adom_list = append_list
|
||||
return resp_obj
|
||||
|
||||
def get_locked_adom_list(self):
|
||||
"""
|
||||
Gets the list of locked adoms
|
||||
"""
|
||||
try:
|
||||
locked_list = list()
|
||||
locked_by_user_list = list()
|
||||
for adom in self._adom_list:
|
||||
adom_lock_info = self.get_lock_info(adom=adom)
|
||||
try:
|
||||
if adom_lock_info[1]["status"]["message"] == "OK":
|
||||
continue
|
||||
except BaseException:
|
||||
pass
|
||||
try:
|
||||
if adom_lock_info[1][0]["lock_user"]:
|
||||
locked_list.append(str(adom))
|
||||
if adom_lock_info[1][0]["lock_user"] == self._logged_in_user:
|
||||
locked_by_user_list.append({"adom": str(adom), "user": str(adom_lock_info[1][0]["lock_user"])})
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(err)
|
||||
self._locked_adom_list = locked_list
|
||||
self._locked_adoms_by_user = locked_by_user_list
|
||||
|
||||
except BaseException as err:
|
||||
raise FAZBaseException(msg=("An error occurred while trying to get the locked adom list. Error: "
|
||||
+ str(err)))
|
||||
|
||||
################################
|
||||
# END DATABASE LOCK CONTEXT CODE
|
||||
################################
|
Loading…
Reference in New Issue