From 6fb7073adc9626fcbeb13478fc20cae6b93f91c1 Mon Sep 17 00:00:00 2001 From: Fortinet Core CSE NA <43451990+ftntcorecse@users.noreply.github.com> Date: Mon, 26 Aug 2019 21:52:42 -0700 Subject: [PATCH] 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... --- .../network/fortianalyzer/__init__.py | 0 .../network/fortianalyzer/common.py | 292 +++++++++++ .../network/fortianalyzer/fortianalyzer.py | 477 ++++++++++++++++++ .../modules/network/fortianalyzer/__init__.py | 0 .../network/fortianalyzer/faz_device.py | 439 ++++++++++++++++ lib/ansible/plugins/httpapi/fortianalyzer.py | 449 +++++++++++++++++ 6 files changed, 1657 insertions(+) create mode 100644 lib/ansible/module_utils/network/fortianalyzer/__init__.py create mode 100644 lib/ansible/module_utils/network/fortianalyzer/common.py create mode 100644 lib/ansible/module_utils/network/fortianalyzer/fortianalyzer.py create mode 100644 lib/ansible/modules/network/fortianalyzer/__init__.py create mode 100644 lib/ansible/modules/network/fortianalyzer/faz_device.py create mode 100644 lib/ansible/plugins/httpapi/fortianalyzer.py diff --git a/lib/ansible/module_utils/network/fortianalyzer/__init__.py b/lib/ansible/module_utils/network/fortianalyzer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortianalyzer/common.py b/lib/ansible/module_utils/network/fortianalyzer/common.py new file mode 100644 index 00000000000..546f71aa129 --- /dev/null +++ b/lib/ansible/module_utils/network/fortianalyzer/common.py @@ -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 diff --git a/lib/ansible/module_utils/network/fortianalyzer/fortianalyzer.py b/lib/ansible/module_utils/network/fortianalyzer/fortianalyzer.py new file mode 100644 index 00000000000..a018c0c9406 --- /dev/null +++ b/lib/ansible/module_utils/network/fortianalyzer/fortianalyzer.py @@ -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) diff --git a/lib/ansible/modules/network/fortianalyzer/__init__.py b/lib/ansible/modules/network/fortianalyzer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/fortianalyzer/faz_device.py b/lib/ansible/modules/network/fortianalyzer/faz_device.py new file mode 100644 index 00000000000..074892c6e2f --- /dev/null +++ b/lib/ansible/modules/network/fortianalyzer/faz_device.py @@ -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 . +# + +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() diff --git a/lib/ansible/plugins/httpapi/fortianalyzer.py b/lib/ansible/plugins/httpapi/fortianalyzer.py new file mode 100644 index 00000000000..059cf91dfec --- /dev/null +++ b/lib/ansible/plugins/httpapi/fortianalyzer.py @@ -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 . +# + +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 + ################################