1# This code is part of Ansible, but is an independent component. 2# This particular file snippet, and this file snippet only, is BSD licensed. 3# Modules you write using this snippet, which is embedded dynamically by Ansible 4# still belong to the author of the module, and may assign their own license 5# to the complete work. 6# 7# (c) 2017-2020 Fortinet, Inc 8# All rights reserved. 9# 10# Redistribution and use in source and binary forms, with or without modification, 11# are permitted provided that the following conditions are met: 12# 13# * Redistributions of source code must retain the above copyright 14# notice, this list of conditions and the following disclaimer. 15# * Redistributions in binary form must reproduce the above copyright notice, 16# this list of conditions and the following disclaimer in the documentation 17# and/or other materials provided with the distribution. 18# 19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 27# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28# 29from __future__ import (absolute_import, division, print_function) 30__metaclass__ = type 31 32from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGR_RC 33from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGBaseException 34from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import FMGRCommon 35from ansible_collections.fortinet.fortimanager.plugins.module_utils.common import scrub_dict 36 37# check for pyFMG lib - DEPRECATING 38try: 39 from pyFMG.fortimgr import FortiManager 40 HAS_PYFMGR = True 41except ImportError: 42 HAS_PYFMGR = False 43 44# ACTIVE BUG WITH OUR DEBUG IMPORT CALL -- BECAUSE IT'S UNDER MODULE_UTILITIES 45# WHEN module_common.recursive_finder() runs under the module loader, it looks for this namespace debug import 46# and because it's not there, it always fails, regardless of it being under a try/catch here. 47# we're going to move it to a different namespace. 48# # check for debug lib 49# try: 50# from ansible.module_utils.network.fortimanager.fortimanager_debug import debug_dump 51# HAS_FMGR_DEBUG = True 52# except: 53# HAS_FMGR_DEBUG = False 54 55 56# BEGIN HANDLER CLASSES 57class FortiManagerHandler(object): 58 def __init__(self, conn, module): 59 self._conn = conn 60 self._module = module 61 self._tools = FMGRCommon 62 self.process_workspace_lock() 63 64 def process_workspace_lock(self): 65 self._conn.process_workspace_locking(self._module.params) 66 67 def process_request(self, url, datagram, method): 68 """ 69 Formats and Runs the API Request via Connection Plugin. Streamlined for use FROM Modules. 70 71 :param url: Connection URL to access 72 :type url: string 73 :param datagram: The prepared payload for the API Request in dictionary format 74 :type datagram: dict 75 :param method: The preferred API Request method (GET, ADD, POST, etc....) 76 :type method: basestring 77 78 :return: Dictionary containing results of the API Request via Connection Plugin 79 :rtype: dict 80 """ 81 data = self._tools.format_request(method, url, **datagram) 82 response = self._conn.send_request(method, data) 83 84 # if HAS_FMGR_DEBUG: 85 # try: 86 # debug_dump(response, datagram, self._module.paramgram, url, method) 87 # except BaseException: 88 # pass 89 90 return response 91 92 def govern_response(self, module, results, msg=None, good_codes=None, 93 stop_on_fail=None, stop_on_success=None, skipped=None, 94 changed=None, unreachable=None, failed=None, success=None, changed_if_success=None, 95 ansible_facts=None): 96 """ 97 This function will attempt to apply default values to canned responses from FortiManager we know of. 98 This saves time, and turns the response in the module into a "one-liner", while still giving us... 99 the flexibility to directly use return_response in modules if we have too. This function saves repeated code. 100 101 :param module: The Ansible Module CLASS object, used to run fail/exit json 102 :type module: object 103 :param msg: An overridable custom message from the module that called this. 104 :type msg: string 105 :param results: A dictionary object containing an API call results 106 :type results: dict 107 :param good_codes: A list of exit codes considered successful from FortiManager 108 :type good_codes: list 109 :param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true) 110 :type stop_on_fail: boolean 111 :param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false) 112 :type stop_on_success: boolean 113 :param changed: If True, tells Ansible that object was changed (default: false) 114 :type skipped: boolean 115 :param skipped: If True, tells Ansible that object was skipped (default: false) 116 :type skipped: boolean 117 :param unreachable: If True, tells Ansible that object was unreachable (default: false) 118 :type unreachable: boolean 119 :param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false) 120 :type unreachable: boolean 121 :param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false) 122 :type unreachable: boolean 123 :param changed_if_success: If True, defaults to changed if successful if you specify or not" 124 :type changed_if_success: boolean 125 :param ansible_facts: A prepared dictionary of ansible facts from the execution. 126 :type ansible_facts: dict 127 """ 128 if module is None and results is None: 129 raise FMGBaseException("govern_response() was called without a module and/or results tuple! Fix!") 130 # Get the Return code from results 131 try: 132 rc = results[0] 133 except BaseException: 134 raise FMGBaseException("govern_response() was called without the return code at results[0]") 135 136 # init a few items 137 rc_data = None 138 139 # Get the default values for the said return code. 140 try: 141 rc_codes = FMGR_RC.get('fmgr_return_codes') 142 rc_data = rc_codes.get(rc) 143 except BaseException: 144 pass 145 146 if not rc_data: 147 rc_data = {} 148 # ONLY add to overrides if not none -- This is very important that the keys aren't added at this stage 149 # if they are empty. And there aren't that many, so let's just do a few if then statements. 150 if good_codes is not None: 151 rc_data["good_codes"] = good_codes 152 if stop_on_fail is not None: 153 rc_data["stop_on_fail"] = stop_on_fail 154 if stop_on_success is not None: 155 rc_data["stop_on_success"] = stop_on_success 156 if skipped is not None: 157 rc_data["skipped"] = skipped 158 if changed is not None: 159 rc_data["changed"] = changed 160 if unreachable is not None: 161 rc_data["unreachable"] = unreachable 162 if failed is not None: 163 rc_data["failed"] = failed 164 if success is not None: 165 rc_data["success"] = success 166 if changed_if_success is not None: 167 rc_data["changed_if_success"] = changed_if_success 168 if results is not None: 169 rc_data["results"] = results 170 if msg is not None: 171 rc_data["msg"] = msg 172 if ansible_facts is None: 173 rc_data["ansible_facts"] = {} 174 else: 175 rc_data["ansible_facts"] = ansible_facts 176 177 return self.return_response(module=module, 178 results=results, 179 msg=rc_data.get("msg", "NULL"), 180 good_codes=rc_data.get("good_codes", (0,)), 181 stop_on_fail=rc_data.get("stop_on_fail", True), 182 stop_on_success=rc_data.get("stop_on_success", False), 183 skipped=rc_data.get("skipped", False), 184 changed=rc_data.get("changed", False), 185 changed_if_success=rc_data.get("changed_if_success", False), 186 unreachable=rc_data.get("unreachable", False), 187 failed=rc_data.get("failed", False), 188 success=rc_data.get("success", False), 189 ansible_facts=rc_data.get("ansible_facts", dict())) 190 191 @staticmethod 192 def return_response(module, results, msg="NULL", good_codes=(0,), 193 stop_on_fail=True, stop_on_success=False, skipped=False, 194 changed=False, unreachable=False, failed=False, success=False, changed_if_success=True, 195 ansible_facts=()): 196 """ 197 This function controls the logout and error reporting after an method or function runs. The exit_json for 198 ansible comes from logic within this function. If this function returns just the msg, it means to continue 199 execution on the playbook. It is called from the ansible module, or from the self.govern_response function. 200 201 :param module: The Ansible Module CLASS object, used to run fail/exit json 202 :type module: object 203 :param msg: An overridable custom message from the module that called this. 204 :type msg: string 205 :param results: A dictionary object containing an API call results 206 :type results: dict 207 :param good_codes: A list of exit codes considered successful from FortiManager 208 :type good_codes: list 209 :param stop_on_fail: If true, stops playbook run when return code is NOT IN good codes (default: true) 210 :type stop_on_fail: boolean 211 :param stop_on_success: If true, stops playbook run when return code is IN good codes (default: false) 212 :type stop_on_success: boolean 213 :param changed: If True, tells Ansible that object was changed (default: false) 214 :type skipped: boolean 215 :param skipped: If True, tells Ansible that object was skipped (default: false) 216 :type skipped: boolean 217 :param unreachable: If True, tells Ansible that object was unreachable (default: false) 218 :type unreachable: boolean 219 :param failed: If True, tells Ansible that execution was a failure. Overrides good_codes. (default: false) 220 :type unreachable: boolean 221 :param success: If True, tells Ansible that execution was a success. Overrides good_codes. (default: false) 222 :type unreachable: boolean 223 :param changed_if_success: If True, defaults to changed if successful if you specify or not" 224 :type changed_if_success: boolean 225 :param ansible_facts: A prepared dictionary of ansible facts from the execution. 226 :type ansible_facts: dict 227 228 :return: A string object that contains an error message 229 :rtype: str 230 """ 231 232 # VALIDATION ERROR 233 if (len(results) == 0) or (failed and success) or (changed and unreachable): 234 module.exit_json(msg="Handle_response was called with no results, or conflicting failed/success or " 235 "changed/unreachable parameters. Fix the exit code on module. " 236 "Generic Failure", failed=True) 237 238 # IDENTIFY SUCCESS/FAIL IF NOT DEFINED 239 if not failed and not success: 240 if len(results) > 0: 241 if results[0] not in good_codes: 242 failed = True 243 elif results[0] in good_codes: 244 success = True 245 246 if len(results) > 0: 247 # IF NO MESSAGE WAS SUPPLIED, GET IT FROM THE RESULTS, IF THAT DOESN'T WORK, THEN WRITE AN ERROR MESSAGE 248 if msg == "NULL": 249 try: 250 msg = results[1]['status']['message'] 251 except BaseException: 252 msg = "No status message returned at results[1][status][message], " \ 253 "and none supplied to msg parameter for handle_response." 254 255 if failed: 256 # BECAUSE SKIPPED/FAILED WILL OFTEN OCCUR ON CODES THAT DON'T GET INCLUDED, THEY ARE CONSIDERED FAILURES 257 # HOWEVER, THEY ARE MUTUALLY EXCLUSIVE, SO IF IT IS MARKED SKIPPED OR UNREACHABLE BY THE MODULE LOGIC 258 # THEN REMOVE THE FAILED FLAG SO IT DOESN'T OVERRIDE THE DESIRED STATUS OF SKIPPED OR UNREACHABLE. 259 if failed and skipped: 260 failed = False 261 if failed and unreachable: 262 failed = False 263 if stop_on_fail: 264 module.exit_json(failed=failed, changed=changed, unreachable=unreachable, skipped=skipped, meta=results[1]) 265 elif success: 266 if changed_if_success: 267 changed = True 268 success = False 269 if stop_on_success: 270 module.exit_json(success=success, changed=changed, unreachable=unreachable, skipped=skipped, meta=results[1]) 271 return msg 272 273 def construct_ansible_facts(self, response, ansible_params, paramgram, *args, **kwargs): 274 """ 275 Constructs a dictionary to return to ansible facts, containing various information about the execution. 276 277 :param response: Contains the response from the FortiManager. 278 :type response: dict 279 :param ansible_params: Contains the parameters Ansible was called with. 280 :type ansible_params: dict 281 :param paramgram: Contains the paramgram passed to the modules' local modify function. 282 :type paramgram: dict 283 :param args: Free-form arguments that could be added. 284 :param kwargs: Free-form keyword arguments that could be added. 285 286 :return: A dictionary containing lots of information to append to Ansible Facts. 287 :rtype: dict 288 """ 289 290 facts = { 291 "response": response, 292 "ansible_params": scrub_dict(ansible_params), 293 "paramgram": scrub_dict(paramgram), 294 "connected_fmgr": self._conn.return_connected_fmgr() 295 } 296 297 if args: 298 facts["custom_args"] = args 299 if kwargs: 300 facts.update(kwargs) 301 302 return facts 303 304 305########################## 306# BEGIN DEPRECATED METHODS 307########################## 308 309# SOME OF THIS CODE IS DUPLICATED IN THE PLUGIN, BUT THOSE ARE PLUGIN SPECIFIC. THIS VERSION STILL ALLOWS FOR 310# THE USAGE OF PYFMG FOR CUSTOMERS WHO HAVE NOT YET UPGRADED TO ANSIBLE 2.7 311 312# LEGACY PYFMG METHODS START 313# USED TO DETERMINE LOCK CONTEXT ON A FORTIMANAGER. A DATABASE LOCKING CONCEPT THAT NEEDS TO BE ACCOUNTED FOR. 314 315class FMGLockContext(object): 316 """ 317 - DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE 318 - CONVERT ALL MODULES TO CONNECTION MANAGER METHOD. 319 - LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE 320 """ 321 def __init__(self, fmg): 322 self._fmg = fmg 323 self._locked_adom_list = list() 324 self._uses_workspace = False 325 self._uses_adoms = False 326 327 @property 328 def uses_workspace(self): 329 return self._uses_workspace 330 331 @uses_workspace.setter 332 def uses_workspace(self, val): 333 self._uses_workspace = val 334 335 @property 336 def uses_adoms(self): 337 return self._uses_adoms 338 339 @uses_adoms.setter 340 def uses_adoms(self, val): 341 self._uses_adoms = val 342 343 def add_adom_to_lock_list(self, adom): 344 if adom not in self._locked_adom_list: 345 self._locked_adom_list.append(adom) 346 347 def remove_adom_from_lock_list(self, adom): 348 if adom in self._locked_adom_list: 349 self._locked_adom_list.remove(adom) 350 351 def check_mode(self): 352 url = "/cli/global/system/global" 353 code, resp_obj = self._fmg.get(url, fields=["workspace-mode", "adom-status"]) 354 try: 355 if resp_obj["workspace-mode"] != 0: 356 self.uses_workspace = True 357 except KeyError: 358 self.uses_workspace = False 359 try: 360 if resp_obj["adom-status"] == 1: 361 self.uses_adoms = True 362 except KeyError: 363 self.uses_adoms = False 364 365 def run_unlock(self): 366 for adom_locked in self._locked_adom_list: 367 self.unlock_adom(adom_locked) 368 369 def lock_adom(self, adom=None, *args, **kwargs): 370 if adom: 371 if adom.lower() == "global": 372 url = "/dvmdb/global/workspace/lock/" 373 else: 374 url = "/dvmdb/adom/{adom}/workspace/lock/".format(adom=adom) 375 else: 376 url = "/dvmdb/adom/root/workspace/lock" 377 code, respobj = self._fmg.execute(url, {}, *args, **kwargs) 378 if code == 0 and respobj["status"]["message"].lower() == "ok": 379 self.add_adom_to_lock_list(adom) 380 return code, respobj 381 382 def unlock_adom(self, adom=None, *args, **kwargs): 383 if adom: 384 if adom.lower() == "global": 385 url = "/dvmdb/global/workspace/unlock/" 386 else: 387 url = "/dvmdb/adom/{adom}/workspace/unlock/".format(adom=adom) 388 else: 389 url = "/dvmdb/adom/root/workspace/unlock" 390 code, respobj = self._fmg.execute(url, {}, *args, **kwargs) 391 if code == 0 and respobj["status"]["message"].lower() == "ok": 392 self.remove_adom_from_lock_list(adom) 393 return code, respobj 394 395 def commit_changes(self, adom=None, aux=False, *args, **kwargs): 396 if adom: 397 if aux: 398 url = "/pm/config/adom/{adom}/workspace/commit".format(adom=adom) 399 else: 400 if adom.lower() == "global": 401 url = "/dvmdb/global/workspace/commit/" 402 else: 403 url = "/dvmdb/adom/{adom}/workspace/commit".format(adom=adom) 404 else: 405 url = "/dvmdb/adom/root/workspace/commit" 406 return self._fmg.execute(url, {}, *args, **kwargs) 407 408 409# DEPRECATED -- USE PLUGIN INSTEAD 410class AnsibleFortiManager(object): 411 """ 412 - DEPRECATING: USING CONNECTION MANAGER NOW INSTEAD. EVENTUALLY THIS CLASS WILL DISAPPEAR. PLEASE 413 - CONVERT ALL MODULES TO CONNECTION MANAGER METHOD. 414 - LEGACY pyFMG HANDLER OBJECT: REQUIRES A CHECK FOR PY FMG AT TOP OF PAGE 415 """ 416 417 def __init__(self, module, ip=None, username=None, passwd=None, use_ssl=True, verify_ssl=False, timeout=300): 418 self.ip = ip 419 self.username = username 420 self.passwd = passwd 421 self.use_ssl = use_ssl 422 self.verify_ssl = verify_ssl 423 self.timeout = timeout 424 self.fmgr_instance = None 425 426 if not HAS_PYFMGR: 427 module.fail_json(msg='Could not import the python library pyFMG required by this module') 428 429 self.module = module 430 431 def login(self): 432 if self.ip is not None: 433 self.fmgr_instance = FortiManager(self.ip, self.username, self.passwd, use_ssl=self.use_ssl, 434 verify_ssl=self.verify_ssl, timeout=self.timeout, debug=False, 435 disable_request_warnings=True) 436 return self.fmgr_instance.login() 437 438 def logout(self): 439 if self.fmgr_instance.sid is not None: 440 self.fmgr_instance.logout() 441 442 def get(self, url, data): 443 return self.fmgr_instance.get(url, **data) 444 445 def set(self, url, data): 446 return self.fmgr_instance.set(url, **data) 447 448 def update(self, url, data): 449 return self.fmgr_instance.update(url, **data) 450 451 def delete(self, url, data): 452 return self.fmgr_instance.delete(url, **data) 453 454 def add(self, url, data): 455 return self.fmgr_instance.add(url, **data) 456 457 def execute(self, url, data): 458 return self.fmgr_instance.execute(url, **data) 459 460 def move(self, url, data): 461 return self.fmgr_instance.move(url, **data) 462 463 def clone(self, url, data): 464 return self.fmgr_instance.clone(url, **data) 465 466########################## 467# END DEPRECATED METHODS 468########################## 469