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...
This commit is contained in:
Fortinet Core CSE NA 2019-08-26 21:52:42 -07:00 committed by Nilashish Chakraborty
parent b09db0ba9e
commit 6fb7073adc
6 changed files with 1657 additions and 0 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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
################################