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 <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.1",
+    "status": ["preview"],
+    "supported_by": "community"
+}
+
+DOCUMENTATION = '''
+---
+module: faz_device
+version_added: "2.9"
+author: Luke Weighall (@lweighall)
+short_description: Add or remove device
+description:
+  - Add or remove a device or list of devices to FortiAnalyzer Device Manager. ADOM Capable.
+
+options:
+  adom:
+    description:
+      - The ADOM the configuration should belong to.
+    required: true
+    default: root
+    type: str
+
+  mode:
+    description:
+      - Add or delete devices. Or promote unregistered devices that are in the FortiAnalyzer "waiting pool"
+    required: false
+    default: add
+    choices: ["add", "delete", "promote"]
+    type: str
+
+  device_username:
+    description:
+      - The username of the device being added to FortiAnalyzer.
+    required: false
+    type: str
+
+  device_password:
+    description:
+      - The password of the device being added to FortiAnalyzer.
+    required: false
+    type: str
+
+  device_ip:
+    description:
+      - The IP of the device being added to FortiAnalyzer.
+    required: false
+    type: str
+
+  device_unique_name:
+    description:
+      - The desired "friendly" name of the device being added to FortiAnalyzer.
+    required: false
+    type: str
+
+  device_serial:
+    description:
+      - The serial number of the device being added to FortiAnalyzer.
+    required: false
+    type: str
+
+  os_type:
+    description:
+      - The os type of the device being added (default 0).
+    required: true
+    choices: ["unknown", "fos", "fsw", "foc", "fml", "faz", "fwb", "fch", "fct", "log", "fmg", "fsa", "fdd", "fac"]
+    type: str
+
+  mgmt_mode:
+    description:
+      - Management Mode of the device you are adding.
+    choices: ["unreg", "fmg", "faz", "fmgfaz"]
+    required: true
+    type: str
+
+  os_minor_vers:
+    description:
+      - Minor OS rev of the device.
+    required: true
+    type: str
+
+  os_ver:
+    description:
+      - Major OS rev of the device
+    required: true
+    choices: ["unknown", "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0"]
+    type: str
+
+  platform_str:
+    description:
+      - Required for determine the platform for VM platforms. ie FortiGate-VM64
+    required: false
+    type: str
+
+  faz_quota:
+    description:
+      - Specifies the quota for the device in FAZ
+    required: False
+    type: str
+'''
+
+EXAMPLES = '''
+- name: DISCOVER AND ADD DEVICE A PHYSICAL FORTIGATE
+  faz_device:
+    adom: "root"
+    device_username: "admin"
+    device_password: "admin"
+    device_ip: "10.10.24.201"
+    device_unique_name: "FGT1"
+    device_serial: "FGVM000000117994"
+    state: "present"
+    mgmt_mode: "faz"
+    os_type: "fos"
+    os_ver: "5.0"
+    minor_rev: 6
+
+
+- name: DISCOVER AND ADD DEVICE A VIRTUAL FORTIGATE
+  faz_device:
+    adom: "root"
+    device_username: "admin"
+    device_password: "admin"
+    device_ip: "10.10.24.202"
+    device_unique_name: "FGT2"
+    mgmt_mode: "faz"
+    os_type: "fos"
+    os_ver: "5.0"
+    minor_rev: 6
+    state: "present"
+    platform_str: "FortiGate-VM64"
+
+- name: DELETE DEVICE FGT01
+  faz_device:
+    adom: "root"
+    device_unique_name: "ansible-fgt01"
+    mode: "delete"
+
+- name: DELETE DEVICE FGT02
+  faz_device:
+    adom: "root"
+    device_unique_name: "ansible-fgt02"
+    mode: "delete"
+
+- name: PROMOTE FGT01 IN FAZ BY IP
+  faz_device:
+    adom: "root"
+    device_password: "fortinet"
+    device_ip: "10.7.220.151"
+    device_username: "ansible"
+    mgmt_mode: "faz"
+    mode: "promote"
+
+
+- name: PROMOTE FGT02 IN FAZ
+  faz_device:
+    adom: "root"
+    device_password: "fortinet"
+    device_unique_name: "ansible-fgt02"
+    device_username: "ansible"
+    mgmt_mode: "faz"
+    mode: "promote"
+
+'''
+
+RETURN = """
+api_result:
+  description: full API response, includes status code and message
+  returned: always
+  type: str
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.connection import Connection
+from ansible.module_utils.network.fortianalyzer.fortianalyzer import FortiAnalyzerHandler
+from ansible.module_utils.network.fortianalyzer.common import FAZBaseException
+from ansible.module_utils.network.fortianalyzer.common import FAZCommon
+from ansible.module_utils.network.fortianalyzer.common import FAZMethods
+from ansible.module_utils.network.fortianalyzer.common import DEFAULT_RESULT_OBJ
+from ansible.module_utils.network.fortianalyzer.common import FAIL_SOCKET_MSG
+
+
+def faz_add_device(faz, paramgram):
+    """
+    This method is used to add devices to the faz or delete them
+    """
+
+    datagram = {
+        "adom": paramgram["adom"],
+        "device": {"adm_usr": paramgram["device_username"], "adm_pass": paramgram["device_password"],
+                   "ip": paramgram["ip"], "name": paramgram["device_unique_name"],
+                   "mgmt_mode": paramgram["mgmt_mode"], "os_type": paramgram["os_type"],
+                   "mr": paramgram["os_minor_vers"]}
+    }
+
+    if paramgram["platform_str"] is not None:
+        datagram["device"]["platform_str"] = paramgram["platform_str"]
+
+    if paramgram["sn"] is not None:
+        datagram["device"]["sn"] = paramgram["sn"]
+
+    if paramgram["device_action"] is not None:
+        datagram["device"]["device_action"] = paramgram["device_action"]
+
+    if paramgram["faz.quota"] is not None:
+        datagram["device"]["faz.quota"] = paramgram["faz.quota"]
+
+    url = '/dvm/cmd/add/device/'
+    response = faz.process_request(url, datagram, FAZMethods.EXEC)
+    return response
+
+
+def faz_delete_device(faz, paramgram):
+    """
+    This method deletes a device from the FAZ
+    """
+    datagram = {
+        "adom": paramgram["adom"],
+        "device": paramgram["device_unique_name"],
+    }
+
+    url = '/dvm/cmd/del/device/'
+    response = faz.process_request(url, datagram, FAZMethods.EXEC)
+    return response
+
+
+def faz_get_unknown_devices(faz):
+    """
+    This method gets devices with an unknown management type field
+    """
+
+    faz_filter = ["mgmt_mode", "==", "0"]
+
+    datagram = {
+        "filter": faz_filter
+    }
+
+    url = "/dvmdb/device"
+    response = faz.process_request(url, datagram, FAZMethods.GET)
+
+    return response
+
+
+def faz_approve_unregistered_device_by_ip(faz, paramgram):
+    """
+    This method approves unregistered devices by ip.
+    """
+    # TRY TO FIND DETAILS ON THIS UNREGISTERED DEVICE
+    unknown_devices = faz_get_unknown_devices(faz)
+    target_device = None
+    if unknown_devices[0] == 0:
+        for device in unknown_devices[1]:
+            if device["ip"] == paramgram["ip"]:
+                target_device = device
+    else:
+        return "No devices are waiting to be registered!"
+
+    # now that we have the target device details...fill out the datagram and make the call to promote it
+    if target_device is not None:
+        target_device_paramgram = {
+            "adom": paramgram["adom"],
+            "ip": target_device["ip"],
+            "device_username": paramgram["device_username"],
+            "device_password": paramgram["device_password"],
+            "device_unique_name": paramgram["device_unique_name"],
+            "sn": target_device["sn"],
+            "os_type": target_device["os_type"],
+            "mgmt_mode": paramgram["mgmt_mode"],
+            "os_minor_vers": target_device["mr"],
+            "os_ver": target_device["os_ver"],
+            "platform_str": target_device["platform_str"],
+            "faz.quota": target_device["faz.quota"],
+            "device_action": paramgram["device_action"]
+        }
+
+        add_device = faz_add_device(faz, target_device_paramgram)
+        return add_device
+
+    return str("Couldn't find the desired device with ip: " + str(paramgram["device_ip"]))
+
+
+def faz_approve_unregistered_device_by_name(faz, paramgram):
+    # TRY TO FIND DETAILS ON THIS UNREGISTERED DEVICE
+    unknown_devices = faz_get_unknown_devices(faz)
+    target_device = None
+    if unknown_devices[0] == 0:
+        for device in unknown_devices[1]:
+            if device["name"] == paramgram["device_unique_name"]:
+                target_device = device
+    else:
+        return "No devices are waiting to be registered!"
+
+    # now that we have the target device details...fill out the datagram and make the call to promote it
+    if target_device is not None:
+        target_device_paramgram = {
+            "adom": paramgram["adom"],
+            "ip": target_device["ip"],
+            "device_username": paramgram["device_username"],
+            "device_password": paramgram["device_password"],
+            "device_unique_name": paramgram["device_unique_name"],
+            "sn": target_device["sn"],
+            "os_type": target_device["os_type"],
+            "mgmt_mode": paramgram["mgmt_mode"],
+            "os_minor_vers": target_device["mr"],
+            "os_ver": target_device["os_ver"],
+            "platform_str": target_device["platform_str"],
+            "faz.quota": target_device["faz.quota"],
+            "device_action": paramgram["device_action"]
+        }
+
+        add_device = faz_add_device(faz, target_device_paramgram)
+        return add_device
+
+    return str("Couldn't find the desired device with name: " + str(paramgram["device_unique_name"]))
+
+
+def main():
+    argument_spec = dict(
+        adom=dict(required=False, type="str", default="root"),
+        mode=dict(choices=["add", "delete", "promote"], type="str", default="add"),
+
+        device_ip=dict(required=False, type="str"),
+        device_username=dict(required=False, type="str"),
+        device_password=dict(required=False, type="str", no_log=True),
+        device_unique_name=dict(required=False, type="str"),
+        device_serial=dict(required=False, type="str"),
+
+        os_type=dict(required=False, type="str", choices=["unknown", "fos", "fsw", "foc", "fml",
+                                                          "faz", "fwb", "fch", "fct", "log", "fmg",
+                                                          "fsa", "fdd", "fac"]),
+        mgmt_mode=dict(required=False, type="str", choices=["unreg", "fmg", "faz", "fmgfaz"]),
+        os_minor_vers=dict(required=False, type="str"),
+        os_ver=dict(required=False, type="str", choices=["unknown", "0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0"]),
+        platform_str=dict(required=False, type="str"),
+        faz_quota=dict(required=False, type="str")
+    )
+
+    required_if = [
+        ['mode', 'delete', ['device_unique_name']],
+        ['mode', 'add', ['device_serial', 'device_username',
+                         'device_password', 'device_unique_name', 'device_ip', 'mgmt_mode', 'platform_str']]
+
+    ]
+
+    module = AnsibleModule(argument_spec, supports_check_mode=True, required_if=required_if, )
+
+    # START SESSION LOGIC
+    paramgram = {
+        "adom": module.params["adom"],
+        "mode": module.params["mode"],
+        "ip": module.params["device_ip"],
+        "device_username": module.params["device_username"],
+        "device_password": module.params["device_password"],
+        "device_unique_name": module.params["device_unique_name"],
+        "sn": module.params["device_serial"],
+        "os_type": module.params["os_type"],
+        "mgmt_mode": module.params["mgmt_mode"],
+        "os_minor_vers": module.params["os_minor_vers"],
+        "os_ver": module.params["os_ver"],
+        "platform_str": module.params["platform_str"],
+        "faz.quota": module.params["faz_quota"],
+        "device_action": None
+    }
+    # INSERT THE PARAMGRAM INTO THE MODULE SO WHEN WE PASS IT TO MOD_UTILS.FortiManagerHandler IT HAS THAT INFO
+
+    if paramgram["mode"] == "add":
+        paramgram["device_action"] = "add_model"
+    elif paramgram["mode"] == "promote":
+        paramgram["device_action"] = "promote_unreg"
+    module.paramgram = paramgram
+
+    # TRY TO INIT THE CONNECTION SOCKET PATH AND FortiManagerHandler OBJECT AND TOOLS
+    faz = None
+    if module._socket_path:
+        connection = Connection(module._socket_path)
+        faz = FortiAnalyzerHandler(connection, module)
+        faz.tools = FAZCommon()
+    else:
+        module.fail_json(**FAIL_SOCKET_MSG)
+
+    # BEGIN MODULE-SPECIFIC LOGIC -- THINGS NEED TO HAPPEN DEPENDING ON THE ENDPOINT AND OPERATION
+    results = DEFAULT_RESULT_OBJ
+
+    try:
+        if paramgram["mode"] == "add":
+            results = faz_add_device(faz, paramgram)
+    except BaseException as err:
+        raise FAZBaseException(msg="An error occurred trying to add the device. Error: " + str(err))
+
+    try:
+        if paramgram["mode"] == "promote":
+            if paramgram["ip"] is not None:
+                results = faz_approve_unregistered_device_by_ip(faz, paramgram)
+            elif paramgram["device_unique_name"] is not None:
+                results = faz_approve_unregistered_device_by_name(faz, paramgram)
+    except BaseException as err:
+        raise FAZBaseException(msg="An error occurred trying to promote the device. Error: " + str(err))
+
+    try:
+        if paramgram["mode"] == "delete":
+            results = faz_delete_device(faz, paramgram)
+    except BaseException as err:
+        raise FAZBaseException(msg="An error occurred trying to delete the device. Error: " + str(err))
+
+    # PROCESS RESULTS
+    try:
+        faz.govern_response(module=module, results=results,
+                            ansible_facts=faz.construct_ansible_facts(results, module.params, paramgram))
+    except BaseException as err:
+        raise FAZBaseException(msg="An error occurred with govern_response(). Error: " + str(err))
+
+    # This should only be hit if faz.govern_response is missed or failed somehow. In fact. It should never be hit.
+    # But it's here JIC.
+    return module.exit_json(**results[1])
+
+
+if __name__ == "__main__":
+    main()
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 <http://www.gnu.org/licenses/>.
+#
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+---
+author:
+    - Luke Weighall (@lweighall)
+    - Andrew Welsh (@Ghilli3)
+    - Jim Huber (@p4r4n0y1ng)
+httpapi : fortianalyzer
+short_description: HttpApi Plugin for Fortinet FortiAnalyzer Appliance or VM
+description:
+  - This HttpApi plugin provides methods to connect to Fortinet FortiAnalyzer Appliance or VM via JSON RPC API
+version_added: "2.9"
+
+"""
+
+import json
+from ansible.plugins.httpapi import HttpApiBase
+from ansible.module_utils.basic import to_text
+from ansible.module_utils.network.fortianalyzer.common import BASE_HEADERS
+from ansible.module_utils.network.fortianalyzer.common import FAZBaseException
+from ansible.module_utils.network.fortianalyzer.common import FAZCommon
+from ansible.module_utils.network.fortianalyzer.common import FAZMethods
+
+
+class HttpApi(HttpApiBase):
+    def __init__(self, connection):
+        super(HttpApi, self).__init__(connection)
+        self._req_id = 0
+        self._sid = None
+        self._url = "/jsonrpc"
+        self._host = None
+        self._tools = FAZCommon
+        self._debug = False
+        self._connected_faz = None
+        self._last_response_msg = None
+        self._last_response_code = None
+        self._last_data_payload = None
+        self._last_url = None
+        self._last_response_raw = None
+        self._locked_adom_list = list()
+        self._locked_adoms_by_user = list()
+        self._uses_workspace = False
+        self._uses_adoms = False
+        self._adom_list = list()
+        self._logged_in_user = None
+
+    def set_become(self, become_context):
+        """
+        ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED
+        :param become_context: Unused input.
+        :return: None
+        """
+        return None
+
+    def update_auth(self, response, response_data):
+        """
+        TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH
+        :param response: Unused input.
+        :param response_data Unused_input.
+        :return: None
+        """
+        return None
+
+    def login(self, username, password):
+        """
+        This function will log the plugin into FortiAnalyzer, and return the results.
+        :param username: Username of FortiAnalyzer Admin
+        :param password: Password of FortiAnalyzer Admin
+
+        :return: Dictionary of status, if it logged in or not.
+        """
+
+        self._logged_in_user = username
+        self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, "sys/login/user",
+                                                                      passwd=password, user=username,))
+
+        if "FortiAnalyzer object connected to FortiAnalyzer" in self.__str__():
+            # If Login worked, then inspect the FortiAnalyzer for Workspace Mode, and it's system information.
+            self.inspect_faz()
+            return
+        else:
+            raise FAZBaseException(msg="Unknown error while logging in...connection was lost during login operation...."
+                                       " Exiting")
+
+    def inspect_faz(self):
+        # CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS
+        status = self.get_system_status()
+        if status[0] == -11:
+            # THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN
+            self.logout()
+            raise FAZBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors."
+                                       " Exiting")
+        elif status[0] == 0:
+            try:
+                self.check_mode()
+                if self._uses_adoms:
+                    self.get_adom_list()
+                if self._uses_workspace:
+                    self.get_locked_adom_list()
+                self._connected_faz = status[1]
+                self._host = self._connected_faz["Hostname"]
+            except BaseException:
+                pass
+        return
+
+    def logout(self):
+        """
+        This function will logout of the FortiAnalyzer.
+        """
+        if self.sid is not None:
+            # IF WE WERE USING WORKSPACES, THEN CLEAN UP OUR LOCKS IF THEY STILL EXIST
+            if self.uses_workspace:
+                self.get_lock_info()
+                self.run_unlock()
+            ret_code, response = self.send_request(FAZMethods.EXEC,
+                                                   self._tools.format_request(FAZMethods.EXEC, "sys/logout"))
+            self.sid = None
+            return ret_code, response
+
+    def send_request(self, method, params):
+        """
+        Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well.
+        :param params: A formatted dictionary that was returned by self.common_datagram_params()
+        before being called here.
+        :param method: The preferred API Request method (GET, ADD, POST, etc....)
+        :type method: basestring
+
+        :return: Dictionary of status, if it logged in or not.
+        """
+
+        try:
+            if self.sid is None and params[0]["url"] != "sys/login/user":
+                raise FAZBaseException("An attempt was made to login with the SID None and URL != login url.")
+        except IndexError:
+            raise FAZBaseException("An attempt was made at communicating with a FAZ with "
+                                   "no valid session and an incorrectly formatted request.")
+        except Exception:
+            raise FAZBaseException("An attempt was made at communicating with a FAZ with "
+                                   "no valid session and an unexpected error was discovered.")
+
+        self._update_request_id()
+        json_request = {
+            "method": method,
+            "params": params,
+            "session": self.sid,
+            "id": self.req_id,
+            "verbose": 1
+        }
+        data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\')
+        try:
+            # Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins
+            response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data),
+                                                           headers=BASE_HEADERS)
+            # Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below
+            result = json.loads(to_text(response_data.getvalue()))
+            self._update_self_from_response(result, self._url, data)
+            return self._handle_response(result)
+        except Exception as err:
+            raise FAZBaseException(err)
+
+    def _handle_response(self, response):
+        self._set_sid(response)
+        if isinstance(response["result"], list):
+            result = response["result"][0]
+        else:
+            result = response["result"]
+        if "data" in result:
+            return result["status"]["code"], result["data"]
+        else:
+            return result["status"]["code"], result
+
+    def _update_self_from_response(self, response, url, data):
+        self._last_response_raw = response
+        if isinstance(response["result"], list):
+            result = response["result"][0]
+        else:
+            result = response["result"]
+        if "status" in result:
+            self._last_response_code = result["status"]["code"]
+            self._last_response_msg = result["status"]["message"]
+            self._last_url = url
+            self._last_data_payload = data
+
+    def _set_sid(self, response):
+        if self.sid is None and "session" in response:
+            self.sid = response["session"]
+
+    def return_connected_faz(self):
+        """
+        Returns the data stored under self._connected_faz
+
+        :return: dict
+        """
+        try:
+            if self._connected_faz:
+                return self._connected_faz
+        except BaseException:
+            raise FAZBaseException("Couldn't Retrieve Connected FAZ Stats")
+
+    def get_system_status(self):
+        """
+        Returns the system status page from the FortiAnalyzer, for logging and other uses.
+        return: status
+        """
+        status = self.send_request(FAZMethods.GET, self._tools.format_request(FAZMethods.GET, "sys/status"))
+        return status
+
+    @property
+    def debug(self):
+        return self._debug
+
+    @debug.setter
+    def debug(self, val):
+        self._debug = val
+
+    @property
+    def req_id(self):
+        return self._req_id
+
+    @req_id.setter
+    def req_id(self, val):
+        self._req_id = val
+
+    def _update_request_id(self, reqid=0):
+        self.req_id = reqid if reqid != 0 else self.req_id + 1
+
+    @property
+    def sid(self):
+        return self._sid
+
+    @sid.setter
+    def sid(self, val):
+        self._sid = val
+
+    def __str__(self):
+        if self.sid is not None and self.connection._url is not None:
+            return "FortiAnalyzer object connected to FortiAnalyzer: " + str(self.connection._url)
+        return "FortiAnalyzer object with no valid connection to a FortiAnalyzer appliance."
+
+    ##################################
+    # BEGIN DATABASE LOCK CONTEXT CODE
+    ##################################
+
+    @property
+    def uses_workspace(self):
+        return self._uses_workspace
+
+    @uses_workspace.setter
+    def uses_workspace(self, val):
+        self._uses_workspace = val
+
+    @property
+    def uses_adoms(self):
+        return self._uses_adoms
+
+    @uses_adoms.setter
+    def uses_adoms(self, val):
+        self._uses_adoms = val
+
+    def add_adom_to_lock_list(self, adom):
+        if adom not in self._locked_adom_list:
+            self._locked_adom_list.append(adom)
+
+    def remove_adom_from_lock_list(self, adom):
+        if adom in self._locked_adom_list:
+            self._locked_adom_list.remove(adom)
+
+    def check_mode(self):
+        """
+        Checks FortiAnalyzer for the use of Workspace mode
+        """
+        url = "/cli/global/system/global"
+        code, resp_obj = self.send_request(FAZMethods.GET,
+                                           self._tools.format_request(FAZMethods.GET,
+                                                                      url,
+                                                                      fields=["workspace-mode", "adom-status"]))
+        try:
+            if resp_obj["workspace-mode"] == "workflow":
+                self.uses_workspace = True
+            elif resp_obj["workspace-mode"] == "disabled":
+                self.uses_workspace = False
+        except KeyError:
+            self.uses_workspace = False
+        except BaseException:
+            raise FAZBaseException(msg="Couldn't determine workspace-mode in the plugin")
+        try:
+            if resp_obj["adom-status"] in [1, "enable"]:
+                self.uses_adoms = True
+            else:
+                self.uses_adoms = False
+        except KeyError:
+            self.uses_adoms = False
+        except BaseException:
+            raise FAZBaseException(msg="Couldn't determine adom-status in the plugin")
+
+    def run_unlock(self):
+        """
+        Checks for ADOM status, if locked, it will unlock
+        """
+        for adom_locked in self._locked_adoms_by_user:
+            adom = adom_locked["adom"]
+            self.unlock_adom(adom)
+
+    def lock_adom(self, adom=None, *args, **kwargs):
+        """
+        Locks an ADOM for changes
+        """
+        if adom:
+            if adom.lower() == "global":
+                url = "/dvmdb/global/workspace/lock/"
+            else:
+                url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom)
+        else:
+            url = "/dvmdb/adom/root/workspace/lock"
+        code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
+        if code == 0 and respobj["status"]["message"].lower() == "ok":
+            self.add_adom_to_lock_list(adom)
+        return code, respobj
+
+    def unlock_adom(self, adom=None, *args, **kwargs):
+        """
+        Unlocks an ADOM after changes
+        """
+        if adom:
+            if adom.lower() == "global":
+                url = "/dvmdb/global/workspace/unlock/"
+            else:
+                url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom)
+        else:
+            url = "/dvmdb/adom/root/workspace/unlock"
+        code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
+        if code == 0 and respobj["status"]["message"].lower() == "ok":
+            self.remove_adom_from_lock_list(adom)
+        return code, respobj
+
+    def commit_changes(self, adom=None, aux=False, *args, **kwargs):
+        """
+        Commits changes to an ADOM
+        """
+        if adom:
+            if aux:
+                url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom)
+            else:
+                if adom.lower() == "global":
+                    url = "/dvmdb/global/workspace/commit/"
+                else:
+                    url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom)
+        else:
+            url = "/dvmdb/adom/root/workspace/commit"
+        return self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url))
+
+    def get_lock_info(self, adom=None):
+        """
+        Gets ADOM lock info so it can be displayed with the error messages. Or if determined to be locked by ansible
+        for some reason, then unlock it.
+        """
+        if not adom or adom == "root":
+            url = "/dvmdb/adom/root/workspace/lockinfo"
+        else:
+            if adom.lower() == "global":
+                url = "/dvmdb/global/workspace/lockinfo/"
+            else:
+                url = "/dvmdb/adom/{adom}/workspace/lockinfo/".format(adom=adom)
+        datagram = {}
+        data = self._tools.format_request(FAZMethods.GET, url, **datagram)
+        resp_obj = self.send_request(FAZMethods.GET, data)
+        code = resp_obj[0]
+        if code != 0:
+            self._module.fail_json(msg=("An error occurred trying to get the ADOM Lock Info. Error: " + str(resp_obj)))
+        elif code == 0:
+            try:
+                if resp_obj[1]["status"]["message"] == "OK":
+                    self._lock_info = None
+            except BaseException:
+                self._lock_info = resp_obj[1]
+        return resp_obj
+
+    def get_adom_list(self):
+        """
+        Gets the list of ADOMs for the FortiAnalyzer
+        """
+        if self.uses_adoms:
+            url = "/dvmdb/adom"
+            datagram = {}
+            data = self._tools.format_request(FAZMethods.GET, url, **datagram)
+            resp_obj = self.send_request(FAZMethods.GET, data)
+            code = resp_obj[0]
+            if code != 0:
+                self._module.fail_json(msg=("An error occurred trying to get the ADOM Info. Error: " + str(resp_obj)))
+            elif code == 0:
+                num_of_adoms = len(resp_obj[1])
+                append_list = ['root', ]
+                for adom in resp_obj[1]:
+                    if adom["tab_status"] != "":
+                        append_list.append(str(adom["name"]))
+                self._adom_list = append_list
+            return resp_obj
+
+    def get_locked_adom_list(self):
+        """
+        Gets the list of locked adoms
+        """
+        try:
+            locked_list = list()
+            locked_by_user_list = list()
+            for adom in self._adom_list:
+                adom_lock_info = self.get_lock_info(adom=adom)
+                try:
+                    if adom_lock_info[1]["status"]["message"] == "OK":
+                        continue
+                except BaseException:
+                    pass
+                try:
+                    if adom_lock_info[1][0]["lock_user"]:
+                        locked_list.append(str(adom))
+                    if adom_lock_info[1][0]["lock_user"] == self._logged_in_user:
+                        locked_by_user_list.append({"adom": str(adom), "user": str(adom_lock_info[1][0]["lock_user"])})
+                except BaseException as err:
+                    raise FAZBaseException(err)
+            self._locked_adom_list = locked_list
+            self._locked_adoms_by_user = locked_by_user_list
+
+        except BaseException as err:
+            raise FAZBaseException(msg=("An error occurred while trying to get the locked adom list. Error: "
+                                        + str(err)))
+
+    ################################
+    # END DATABASE LOCK CONTEXT CODE
+    ################################