1# Copyright (c) 2018 Fortinet and/or its affiliates. 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17# 18 19from __future__ import (absolute_import, division, print_function) 20__metaclass__ = type 21 22DOCUMENTATION = """ 23--- 24author: 25 - Luke Weighall (@lweighall) 26 - Andrew Welsh (@Ghilli3) 27 - Jim Huber (@p4r4n0y1ng) 28httpapi : fortimanager 29short_description: HttpApi Plugin for Fortinet FortiManager Appliance or VM 30description: 31 - This HttpApi plugin provides methods to connect to Fortinet FortiManager Appliance or VM via JSON RPC API 32version_added: "2.8" 33 34""" 35 36import json 37from ansible.plugins.httpapi import HttpApiBase 38from ansible.module_utils.basic import to_text 39from ansible.module_utils.network.fortimanager.common import BASE_HEADERS 40from ansible.module_utils.network.fortimanager.common import FMGBaseException 41from ansible.module_utils.network.fortimanager.common import FMGRCommon 42from ansible.module_utils.network.fortimanager.common import FMGRMethods 43 44 45class HttpApi(HttpApiBase): 46 def __init__(self, connection): 47 super(HttpApi, self).__init__(connection) 48 self._req_id = 0 49 self._sid = None 50 self._url = "/jsonrpc" 51 self._host = None 52 self._tools = FMGRCommon 53 self._debug = False 54 self._connected_fmgr = None 55 self._last_response_msg = None 56 self._last_response_code = None 57 self._last_data_payload = None 58 self._last_url = None 59 self._last_response_raw = None 60 self._locked_adom_list = list() 61 self._uses_workspace = False 62 self._uses_adoms = False 63 64 def set_become(self, become_context): 65 """ 66 ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED 67 :param become_context: Unused input. 68 :return: None 69 """ 70 return None 71 72 def update_auth(self, response, response_data): 73 """ 74 TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH 75 :param response: Unused input. 76 :param response_data Unused_input. 77 :return: None 78 """ 79 return None 80 81 def login(self, username, password): 82 """ 83 This function will log the plugin into FortiManager, and return the results. 84 :param username: Username of FortiManager Admin 85 :param password: Password of FortiManager Admin 86 87 :return: Dictionary of status, if it logged in or not. 88 """ 89 self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, "sys/login/user", 90 passwd=password, user=username,)) 91 92 if "FortiManager object connected to FortiManager" in self.__str__(): 93 # If Login worked, then inspect the FortiManager for Workspace Mode, and it's system information. 94 self.inspect_fmgr() 95 return 96 else: 97 raise FMGBaseException(msg="Unknown error while logging in...connection was lost during login operation...." 98 " Exiting") 99 100 def inspect_fmgr(self): 101 # CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS 102 103 self.check_mode() 104 # CHECK FOR SYSTEM STATUS -- SHOULD RETURN 0 105 status = self.get_system_status() 106 if status[0] == -11: 107 # THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN 108 self.logout() 109 raise FMGBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors." 110 " Exiting") 111 elif status[0] == 0: 112 try: 113 self._connected_fmgr = status[1] 114 self._host = self._connected_fmgr["Hostname"] 115 except BaseException: 116 pass 117 return 118 119 def logout(self): 120 """ 121 This function will logout of the FortiManager. 122 """ 123 if self.sid is not None: 124 if self.uses_workspace: 125 self.run_unlock() 126 ret_code, response = self.send_request(FMGRMethods.EXEC, 127 self._tools.format_request(FMGRMethods.EXEC, "sys/logout")) 128 self.sid = None 129 return ret_code, response 130 131 def send_request(self, method, params): 132 """ 133 Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well. 134 :param params: A formatted dictionary that was returned by self.common_datagram_params() 135 before being called here. 136 :param method: The preferred API Request method (GET, ADD, POST, etc....) 137 :type method: basestring 138 139 :return: Dictionary of status, if it logged in or not. 140 """ 141 142 try: 143 if self.sid is None and params[0]["url"] != "sys/login/user": 144 raise FMGBaseException("An attempt was made to login with the SID None and URL != login url.") 145 except IndexError: 146 raise FMGBaseException("An attempt was made at communicating with a FMG with " 147 "no valid session and an incorrectly formatted request.") 148 except Exception: 149 raise FMGBaseException("An attempt was made at communicating with a FMG with " 150 "no valid session and an unexpected error was discovered.") 151 152 self._update_request_id() 153 json_request = { 154 "method": method, 155 "params": params, 156 "session": self.sid, 157 "id": self.req_id, 158 "verbose": 1 159 } 160 data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\') 161 try: 162 # Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins 163 response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data), 164 headers=BASE_HEADERS) 165 # Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below 166 result = json.loads(to_text(response_data.getvalue())) 167 self._update_self_from_response(result, self._url, data) 168 return self._handle_response(result) 169 except Exception as err: 170 raise FMGBaseException(err) 171 172 def _handle_response(self, response): 173 self._set_sid(response) 174 if isinstance(response["result"], list): 175 result = response["result"][0] 176 else: 177 result = response["result"] 178 if "data" in result: 179 return result["status"]["code"], result["data"] 180 else: 181 return result["status"]["code"], result 182 183 def _update_self_from_response(self, response, url, data): 184 self._last_response_raw = response 185 if isinstance(response["result"], list): 186 result = response["result"][0] 187 else: 188 result = response["result"] 189 if "status" in result: 190 self._last_response_code = result["status"]["code"] 191 self._last_response_msg = result["status"]["message"] 192 self._last_url = url 193 self._last_data_payload = data 194 195 def _set_sid(self, response): 196 if self.sid is None and "session" in response: 197 self.sid = response["session"] 198 199 def return_connected_fmgr(self): 200 """ 201 Returns the data stored under self._connected_fmgr 202 203 :return: dict 204 """ 205 try: 206 if self._connected_fmgr: 207 return self._connected_fmgr 208 except BaseException: 209 raise FMGBaseException("Couldn't Retrieve Connected FMGR Stats") 210 211 def get_system_status(self): 212 """ 213 Returns the system status page from the FortiManager, for logging and other uses. 214 return: status 215 """ 216 status = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, "sys/status")) 217 return status 218 219 @property 220 def debug(self): 221 return self._debug 222 223 @debug.setter 224 def debug(self, val): 225 self._debug = val 226 227 @property 228 def req_id(self): 229 return self._req_id 230 231 @req_id.setter 232 def req_id(self, val): 233 self._req_id = val 234 235 def _update_request_id(self, reqid=0): 236 self.req_id = reqid if reqid != 0 else self.req_id + 1 237 238 @property 239 def sid(self): 240 return self._sid 241 242 @sid.setter 243 def sid(self, val): 244 self._sid = val 245 246 def __str__(self): 247 if self.sid is not None and self.connection._url is not None: 248 return "FortiManager object connected to FortiManager: " + str(self.connection._url) 249 return "FortiManager object with no valid connection to a FortiManager appliance." 250 251 ################################## 252 # BEGIN DATABASE LOCK CONTEXT CODE 253 ################################## 254 255 @property 256 def uses_workspace(self): 257 return self._uses_workspace 258 259 @uses_workspace.setter 260 def uses_workspace(self, val): 261 self._uses_workspace = val 262 263 @property 264 def uses_adoms(self): 265 return self._uses_adoms 266 267 @uses_adoms.setter 268 def uses_adoms(self, val): 269 self._uses_adoms = val 270 271 def add_adom_to_lock_list(self, adom): 272 if adom not in self._locked_adom_list: 273 self._locked_adom_list.append(adom) 274 275 def remove_adom_from_lock_list(self, adom): 276 if adom in self._locked_adom_list: 277 self._locked_adom_list.remove(adom) 278 279 def check_mode(self): 280 """ 281 Checks FortiManager for the use of Workspace mode 282 """ 283 url = "/cli/global/system/global" 284 code, resp_obj = self.send_request(FMGRMethods.GET, self._tools.format_request(FMGRMethods.GET, url, 285 fields=["workspace-mode", 286 "adom-status"])) 287 try: 288 if resp_obj["workspace-mode"] != 0: 289 self.uses_workspace = True 290 except KeyError: 291 self.uses_workspace = False 292 try: 293 if resp_obj["adom-status"] == 1: 294 self.uses_adoms = True 295 except KeyError: 296 self.uses_adoms = False 297 298 def run_unlock(self): 299 """ 300 Checks for ADOM status, if locked, it will unlock 301 """ 302 for adom_locked in self._locked_adom_list: 303 self.unlock_adom(adom_locked) 304 305 def lock_adom(self, adom=None, *args, **kwargs): 306 """ 307 Locks an ADOM for changes 308 """ 309 if adom: 310 if adom.lower() == "global": 311 url = "/dvmdb/global/workspace/lock/" 312 else: 313 url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom) 314 else: 315 url = "/dvmdb/adom/root/workspace/lock" 316 code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url)) 317 if code == 0 and respobj["status"]["message"].lower() == "ok": 318 self.add_adom_to_lock_list(adom) 319 return code, respobj 320 321 def unlock_adom(self, adom=None, *args, **kwargs): 322 """ 323 Unlocks an ADOM after changes 324 """ 325 if adom: 326 if adom.lower() == "global": 327 url = "/dvmdb/global/workspace/unlock/" 328 else: 329 url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom) 330 else: 331 url = "/dvmdb/adom/root/workspace/unlock" 332 code, respobj = self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url)) 333 if code == 0 and respobj["status"]["message"].lower() == "ok": 334 self.remove_adom_from_lock_list(adom) 335 return code, respobj 336 337 def commit_changes(self, adom=None, aux=False, *args, **kwargs): 338 """ 339 Commits changes to an ADOM 340 """ 341 if adom: 342 if aux: 343 url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom) 344 else: 345 if adom.lower() == "global": 346 url = "/dvmdb/global/workspace/commit/" 347 else: 348 url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom) 349 else: 350 url = "/dvmdb/adom/root/workspace/commit" 351 return self.send_request(FMGRMethods.EXEC, self._tools.format_request(FMGRMethods.EXEC, url)) 352 353 ################################ 354 # END DATABASE LOCK CONTEXT CODE 355 ################################ 356