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