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