diff --git a/lib/ansible/plugins/httpapi/fortimanager.py b/lib/ansible/plugins/httpapi/fortimanager.py index c8f793a70e5..0e41a4c19b4 100644 --- a/lib/ansible/plugins/httpapi/fortimanager.py +++ b/lib/ansible/plugins/httpapi/fortimanager.py @@ -17,6 +17,7 @@ # from __future__ import (absolute_import, division, print_function) + __metaclass__ = type DOCUMENTATION = """ @@ -26,11 +27,10 @@ author: - Andrew Welsh (@Ghilli3) - Jim Huber (@p4r4n0y1ng) httpapi : fortimanager -short_description: HttpApi Plugin for Fortinet FortiManager Appliance or VM +short_description: HttpApi Plugin for Fortinet FortiManager Appliance or VM. description: - - This HttpApi plugin provides methods to connect to Fortinet FortiManager Appliance or VM via JSON RPC API + - This HttpApi plugin provides methods to connect to Fortinet FortiManager Appliance or VM via JSON RPC API. version_added: "2.8" - """ import json @@ -58,12 +58,15 @@ class HttpApi(HttpApiBase): 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 + ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED. :param become_context: Unused input. :return: None """ @@ -71,7 +74,7 @@ class HttpApi(HttpApiBase): def update_auth(self, response, response_data): """ - TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH + TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH. :param response: Unused input. :param response_data Unused_input. :return: None @@ -79,15 +82,17 @@ class HttpApi(HttpApiBase): return None def login(self, username, password): + """ This function will log the plugin into FortiManager, and return the results. :param username: Username of FortiManager Admin :param password: Password of FortiManager Admin - :return: Dictionary of status, if it logged in or not. + :return: Dictionary of status if it logged in or not. """ + self._logged_in_user = username self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, "sys/login/user", - passwd=password, user=username,)) + passwd=password, user=username, )) if "FortiManager object connected to FortiManager" in self.__str__(): # If Login worked, then inspect the FortiManager for Workspace Mode, and it's system information. @@ -99,9 +104,6 @@ class HttpApi(HttpApiBase): def inspect_fmgr(self): # CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS - - self.check_mode() - # CHECK FOR SYSTEM STATUS -- SHOULD RETURN 0 status = self.get_system_status() if status[0] == -11: # THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN @@ -110,6 +112,11 @@ class HttpApi(HttpApiBase): " 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_fmgr = status[1] self._host = self._connected_fmgr["Hostname"] except BaseException: @@ -121,7 +128,9 @@ class HttpApi(HttpApiBase): This function will logout of the FortiManager. """ 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(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, "sys/logout")) @@ -130,7 +139,7 @@ class HttpApi(HttpApiBase): def send_request(self, method, params): """ - Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well. + Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting too. :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....) @@ -138,16 +147,20 @@ class HttpApi(HttpApiBase): :return: Dictionary of status, if it logged in or not. """ - try: if self.sid is None and params[0]["url"] != "sys/login/user": - raise FMGBaseException("An attempt was made to login with the SID None and URL != login url.") + try: + self.connection._connect() + except Exception as err: + raise FMGBaseException( + msg="An problem happened with the httpapi plugin self-init connection process. " + "Error: " + to_text(err)) except IndexError: raise FMGBaseException("An attempt was made at communicating with a FMG with " "no valid session and an incorrectly formatted request.") - except Exception: + except Exception as err: raise FMGBaseException("An attempt was made at communicating with a FMG with " - "no valid session and an unexpected error was discovered.") + "no valid session and an unexpected error was discovered. \n Error: " + to_text(err)) self._update_request_id() json_request = { @@ -205,7 +218,7 @@ class HttpApi(HttpApiBase): try: if self._connected_fmgr: return self._connected_fmgr - except BaseException: + except Exception: raise FMGBaseException("Couldn't Retrieve Connected FMGR Stats") def get_system_status(self): @@ -245,7 +258,7 @@ class HttpApi(HttpApiBase): def __str__(self): if self.sid is not None and self.connection._url is not None: - return "FortiManager object connected to FortiManager: " + str(self.connection._url) + return "FortiManager object connected to FortiManager: " + to_text(self.connection._url) return "FortiManager object with no valid connection to a FortiManager appliance." ################################## @@ -281,26 +294,32 @@ class HttpApi(HttpApiBase): Checks FortiManager for the use of Workspace mode """ url = "/cli/global/system/global" - code, resp_obj = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, url, - fields=["workspace-mode", - "adom-status"])) + code, resp_obj = self.send_request(FMGRMethods.GET, + self._tools.format_request(FMGRMethods.GET, + url, + fields=["workspace-mode", "adom-status"])) try: - if resp_obj["workspace-mode"] != 0: + 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 + raise FMGBaseException(msg="Couldn't determine workspace-mode in the plugin") try: - if resp_obj["adom-status"] == 1: + if resp_obj["adom-status"] in [1, "enable"]: self.uses_adoms = True + else: + self.uses_adoms = False except KeyError: - self.uses_adoms = False + raise FMGBaseException(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_adom_list: - self.unlock_adom(adom_locked) + 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): """ @@ -350,6 +369,84 @@ class HttpApi(HttpApiBase): url = "/dvmdb/adom/root/workspace/commit" return self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.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(FMGRMethods.GET, url, **datagram) + resp_obj = self.send_request(FMGRMethods.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: " + to_text(resp_obj))) + elif code == 0: + try: + if resp_obj[1]["status"]["message"] == "OK": + self._lock_info = None + except Exception: + self._lock_info = resp_obj[1] + return resp_obj + + def get_adom_list(self): + """ + Gets the list of ADOMs for the FortiManager + """ + if self.uses_adoms: + url = "/dvmdb/adom" + datagram = {} + data = self._tools.format_request(FMGRMethods.GET, url, **datagram) + resp_obj = self.send_request(FMGRMethods.GET, data) + code = resp_obj[0] + if code != 0: + self._module.fail_json(msg=("An error occurred trying to get the ADOM Info. " + "Error: " + to_text(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(to_text(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 IndexError as err: + pass + try: + if adom_lock_info[1][0]["lock_user"]: + locked_list.append(to_text(adom)) + if adom_lock_info[1][0]["lock_user"] == self._logged_in_user: + locked_by_user_list.append({"adom": to_text(adom), + "user": to_text(adom_lock_info[1][0]["lock_user"])}) + except Exception as err: + raise FMGBaseException(err) + self._locked_adom_list = locked_list + self._locked_adoms_by_user = locked_by_user_list + + except Exception as err: + raise FMGBaseException(msg=("An error occurred while trying to get the locked adom list. Error: " + + to_text(err))) + ################################ # END DATABASE LOCK CONTEXT CODE ################################