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) 28name: fortianalyzer 29short_description: HttpApi Plugin for Fortinet FortiAnalyzer Appliance or VM. 30description: 31 - This HttpApi plugin provides methods to connect to Fortinet FortiAnalyzer Appliance or VM via JSON RPC API. 32 33''' 34 35import json 36from ansible.plugins.httpapi import HttpApiBase 37from ansible.module_utils.basic import to_text 38from ansible_collections.community.fortios.plugins.module_utils.fortianalyzer.common import BASE_HEADERS 39from ansible_collections.community.fortios.plugins.module_utils.fortianalyzer.common import FAZBaseException 40from ansible_collections.community.fortios.plugins.module_utils.fortianalyzer.common import FAZCommon 41from ansible_collections.community.fortios.plugins.module_utils.fortianalyzer.common import FAZMethods 42 43 44class HttpApi(HttpApiBase): 45 def __init__(self, connection): 46 super(HttpApi, self).__init__(connection) 47 self._req_id = 0 48 self._sid = None 49 self._url = "/jsonrpc" 50 self._host = None 51 self._tools = FAZCommon 52 self._debug = False 53 self._connected_faz = None 54 self._last_response_msg = None 55 self._last_response_code = None 56 self._last_data_payload = None 57 self._last_url = None 58 self._last_response_raw = None 59 self._locked_adom_list = list() 60 self._locked_adoms_by_user = list() 61 self._uses_workspace = False 62 self._uses_adoms = False 63 self._adom_list = list() 64 self._logged_in_user = None 65 66 def set_become(self, become_context): 67 """ 68 ELEVATION IS NOT REQUIRED ON FORTINET DEVICES - SKIPPED 69 :param become_context: Unused input. 70 :return: None 71 """ 72 return None 73 74 def update_auth(self, response, response_data): 75 """ 76 TOKENS ARE NOT USED SO NO NEED TO UPDATE AUTH 77 :param response: Unused input. 78 :param response_data Unused_input. 79 :return: None 80 """ 81 return None 82 83 def login(self, username, password): 84 """ 85 This function will log the plugin into FortiAnalyzer, and return the results. 86 :param username: Username of FortiAnalyzer Admin 87 :param password: Password of FortiAnalyzer Admin 88 89 :return: Dictionary of status if it logged in or not. 90 """ 91 92 self._logged_in_user = username 93 self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, "sys/login/user", 94 passwd=password, user=username,)) 95 96 if "FortiAnalyzer object connected to FortiAnalyzer" in self.__str__(): 97 # If Login worked then inspect the FortiAnalyzer for Workspace Mode, and it's system information. 98 self.inspect_faz() 99 return 100 else: 101 raise FAZBaseException(msg="Unknown error while logging in...connection was lost during login operation..." 102 " Exiting") 103 104 def inspect_faz(self): 105 # CHECK FOR WORKSPACE MODE TO SEE IF WE HAVE TO ENABLE ADOM LOCKS 106 status = self.get_system_status() 107 if status[0] == -11: 108 # THE CONNECTION GOT LOST SOMEHOW, REMOVE THE SID AND REPORT BAD LOGIN 109 self.logout() 110 raise FAZBaseException(msg="Error -11 -- the Session ID was likely malformed somehow. Contact authors." 111 " Exiting") 112 elif status[0] == 0: 113 try: 114 self.check_mode() 115 if self._uses_adoms: 116 self.get_adom_list() 117 if self._uses_workspace: 118 self.get_locked_adom_list() 119 self._connected_faz = status[1] 120 self._host = self._connected_faz["Hostname"] 121 except Exception: 122 pass 123 return 124 125 def logout(self): 126 """ 127 This function will logout of the FortiAnalyzer. 128 """ 129 if self.sid is not None: 130 # IF WE WERE USING WORKSPACES, THEN CLEAN UP OUR LOCKS IF THEY STILL EXIST 131 if self.uses_workspace: 132 self.get_lock_info() 133 self.run_unlock() 134 ret_code, response = self.send_request(FAZMethods.EXEC, 135 self._tools.format_request(FAZMethods.EXEC, "sys/logout")) 136 self.sid = None 137 return ret_code, response 138 139 def send_request(self, method, params): 140 """ 141 Responsible for actual sending of data to the connection httpapi base plugin. Does some formatting as well. 142 :param params: A formatted dictionary that was returned by self.common_datagram_params() 143 before being called here. 144 :param method: The preferred API Request method (GET, ADD, POST, etc....) 145 :type method: basestring 146 147 :return: Dictionary of status if it logged in or not. 148 """ 149 150 try: 151 if self.sid is None and params[0]["url"] != "sys/login/user": 152 try: 153 self.connection._connect() 154 except Exception as err: 155 raise FAZBaseException( 156 msg="An problem happened with the httpapi plugin self-init connection process. " 157 "Error: " + to_text(err)) 158 except IndexError: 159 raise FAZBaseException("An attempt was made at communicating with a FAZ with " 160 "no valid session and an incorrectly formatted request.") 161 except Exception: 162 raise FAZBaseException("An attempt was made at communicating with a FAZ with " 163 "no valid session and an unexpected error was discovered.") 164 165 self._update_request_id() 166 json_request = { 167 "method": method, 168 "params": params, 169 "session": self.sid, 170 "id": self.req_id, 171 "verbose": 1 172 } 173 data = json.dumps(json_request, ensure_ascii=False).replace('\\\\', '\\') 174 try: 175 # Sending URL and Data in Unicode, per Ansible Specifications for Connection Plugins 176 response, response_data = self.connection.send(path=to_text(self._url), data=to_text(data), 177 headers=BASE_HEADERS) 178 # Get Unicode Response - Must convert from StringIO to unicode first so we can do a replace function below 179 result = json.loads(to_text(response_data.getvalue())) 180 self._update_self_from_response(result, self._url, data) 181 return self._handle_response(result) 182 except Exception as err: 183 raise FAZBaseException(err) 184 185 def _handle_response(self, response): 186 self._set_sid(response) 187 if isinstance(response["result"], list): 188 result = response["result"][0] 189 else: 190 result = response["result"] 191 if "data" in result: 192 return result["status"]["code"], result["data"] 193 else: 194 return result["status"]["code"], result 195 196 def _update_self_from_response(self, response, url, data): 197 self._last_response_raw = response 198 if isinstance(response["result"], list): 199 result = response["result"][0] 200 else: 201 result = response["result"] 202 if "status" in result: 203 self._last_response_code = result["status"]["code"] 204 self._last_response_msg = result["status"]["message"] 205 self._last_url = url 206 self._last_data_payload = data 207 208 def _set_sid(self, response): 209 if self.sid is None and "session" in response: 210 self.sid = response["session"] 211 212 def return_connected_faz(self): 213 """ 214 Returns the data stored under self._connected_faz 215 216 :return: dict 217 """ 218 try: 219 if self._connected_faz: 220 return self._connected_faz 221 except Exception: 222 raise FAZBaseException("Couldn't Retrieve Connected FAZ Stats") 223 224 def get_system_status(self): 225 """ 226 Returns the system status page from the FortiAnalyzer, for logging and other uses. 227 return: status 228 """ 229 status = self.send_request(FAZMethods.GET, self._tools.format_request(FAZMethods.GET, "sys/status")) 230 return status 231 232 @property 233 def debug(self): 234 return self._debug 235 236 @debug.setter 237 def debug(self, val): 238 self._debug = val 239 240 @property 241 def req_id(self): 242 return self._req_id 243 244 @req_id.setter 245 def req_id(self, val): 246 self._req_id = val 247 248 def _update_request_id(self, reqid=0): 249 self.req_id = reqid if reqid != 0 else self.req_id + 1 250 251 @property 252 def sid(self): 253 return self._sid 254 255 @sid.setter 256 def sid(self, val): 257 self._sid = val 258 259 def __str__(self): 260 if self.sid is not None and self.connection._url is not None: 261 return "FortiAnalyzer object connected to FortiAnalyzer: " + to_text(self.connection._url) 262 return "FortiAnalyzer object with no valid connection to a FortiAnalyzer appliance." 263 264 ################################## 265 # BEGIN DATABASE LOCK CONTEXT CODE 266 ################################## 267 268 @property 269 def uses_workspace(self): 270 return self._uses_workspace 271 272 @uses_workspace.setter 273 def uses_workspace(self, val): 274 self._uses_workspace = val 275 276 @property 277 def uses_adoms(self): 278 return self._uses_adoms 279 280 @uses_adoms.setter 281 def uses_adoms(self, val): 282 self._uses_adoms = val 283 284 def add_adom_to_lock_list(self, adom): 285 if adom not in self._locked_adom_list: 286 self._locked_adom_list.append(adom) 287 288 def remove_adom_from_lock_list(self, adom): 289 if adom in self._locked_adom_list: 290 self._locked_adom_list.remove(adom) 291 292 def check_mode(self): 293 """ 294 Checks FortiAnalyzer for the use of Workspace mode 295 """ 296 url = "/cli/global/system/global" 297 code, resp_obj = self.send_request(FAZMethods.GET, 298 self._tools.format_request(FAZMethods.GET, 299 url, 300 fields=["workspace-mode", "adom-status"])) 301 try: 302 if resp_obj["workspace-mode"] == "workflow": 303 self.uses_workspace = True 304 elif resp_obj["workspace-mode"] == "disabled": 305 self.uses_workspace = False 306 except KeyError: 307 self.uses_workspace = False 308 except Exception: 309 raise FAZBaseException(msg="Couldn't determine workspace-mode in the plugin") 310 try: 311 if resp_obj["adom-status"] in [1, "enable"]: 312 self.uses_adoms = True 313 else: 314 self.uses_adoms = False 315 except KeyError: 316 self.uses_adoms = False 317 except Exception: 318 raise FAZBaseException(msg="Couldn't determine adom-status in the plugin") 319 320 def run_unlock(self): 321 """ 322 Checks for ADOM status, if locked, it will unlock 323 """ 324 for adom_locked in self._locked_adoms_by_user: 325 adom = adom_locked["adom"] 326 self.unlock_adom(adom) 327 328 def lock_adom(self, adom=None, *args, **kwargs): 329 """ 330 Locks an ADOM for changes 331 """ 332 if adom: 333 if adom.lower() == "global": 334 url = "/dvmdb/global/workspace/lock/" 335 else: 336 url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom) 337 else: 338 url = "/dvmdb/adom/root/workspace/lock" 339 code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url)) 340 if code == 0 and respobj["status"]["message"].lower() == "ok": 341 self.add_adom_to_lock_list(adom) 342 return code, respobj 343 344 def unlock_adom(self, adom=None, *args, **kwargs): 345 """ 346 Unlocks an ADOM after changes 347 """ 348 if adom: 349 if adom.lower() == "global": 350 url = "/dvmdb/global/workspace/unlock/" 351 else: 352 url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom) 353 else: 354 url = "/dvmdb/adom/root/workspace/unlock" 355 code, respobj = self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url)) 356 if code == 0 and respobj["status"]["message"].lower() == "ok": 357 self.remove_adom_from_lock_list(adom) 358 return code, respobj 359 360 def commit_changes(self, adom=None, aux=False, *args, **kwargs): 361 """ 362 Commits changes to an ADOM 363 """ 364 if adom: 365 if aux: 366 url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom) 367 else: 368 if adom.lower() == "global": 369 url = "/dvmdb/global/workspace/commit/" 370 else: 371 url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom) 372 else: 373 url = "/dvmdb/adom/root/workspace/commit" 374 return self.send_request(FAZMethods.EXEC, self._tools.format_request(FAZMethods.EXEC, url)) 375 376 def get_lock_info(self, adom=None): 377 """ 378 Gets ADOM lock info so it can be displayed with the error messages. Or if determined to be locked by ansible 379 for some reason, then unlock it. 380 """ 381 if not adom or adom == "root": 382 url = "/dvmdb/adom/root/workspace/lockinfo" 383 else: 384 if adom.lower() == "global": 385 url = "/dvmdb/global/workspace/lockinfo/" 386 else: 387 url = "/dvmdb/adom/{adom}/workspace/lockinfo/".format(adom=adom) 388 datagram = {} 389 data = self._tools.format_request(FAZMethods.GET, url, **datagram) 390 resp_obj = self.send_request(FAZMethods.GET, data) 391 code = resp_obj[0] 392 if code != 0: 393 self._module.fail_json(msg=("An error occurred trying to get the ADOM Lock Info. Error: " + to_text(resp_obj))) 394 elif code == 0: 395 try: 396 if resp_obj[1]["status"]["message"] == "OK": 397 self._lock_info = None 398 except Exception: 399 self._lock_info = resp_obj[1] 400 return resp_obj 401 402 def get_adom_list(self): 403 """ 404 Gets the list of ADOMs for the FortiAnalyzer 405 """ 406 if self.uses_adoms: 407 url = "/dvmdb/adom" 408 datagram = {} 409 data = self._tools.format_request(FAZMethods.GET, url, **datagram) 410 resp_obj = self.send_request(FAZMethods.GET, data) 411 code = resp_obj[0] 412 if code != 0: 413 self._module.fail_json(msg=("An error occurred trying to get the ADOM Info. Error: " + to_text(resp_obj))) 414 elif code == 0: 415 num_of_adoms = len(resp_obj[1]) 416 append_list = ['root', ] 417 for adom in resp_obj[1]: 418 if adom["tab_status"] != "": 419 append_list.append(to_text(adom["name"])) 420 self._adom_list = append_list 421 return resp_obj 422 423 def get_locked_adom_list(self): 424 """ 425 Gets the list of locked adoms 426 """ 427 try: 428 locked_list = list() 429 locked_by_user_list = list() 430 for adom in self._adom_list: 431 adom_lock_info = self.get_lock_info(adom=adom) 432 try: 433 if adom_lock_info[1]["status"]["message"] == "OK": 434 continue 435 except Exception: 436 pass 437 try: 438 if adom_lock_info[1][0]["lock_user"]: 439 locked_list.append(to_text(adom)) 440 if adom_lock_info[1][0]["lock_user"] == self._logged_in_user: 441 locked_by_user_list.append({"adom": to_text(adom), "user": to_text(adom_lock_info[1][0]["lock_user"])}) 442 except Exception as err: 443 raise FAZBaseException(err) 444 self._locked_adom_list = locked_list 445 self._locked_adoms_by_user = locked_by_user_list 446 447 except Exception as err: 448 raise FAZBaseException(msg=("An error occurred while trying to get the locked adom list. Error: " 449 + to_text(err))) 450 451 ################################# 452 # END DATABASE LOCK CONTEXT CODE 453 ################################# 454