1#!/usr/bin/python 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 22ANSIBLE_METADATA = { 23 "metadata_version": "1.1", 24 "status": ["preview"], 25 "supported_by": "community" 26} 27 28DOCUMENTATION = ''' 29--- 30module: fmgr_query 31version_added: "2.8" 32notes: 33 - Full Documentation at U(https://ftnt-ansible-docs.readthedocs.io/en/latest/). 34author: Luke Weighall (@lweighall) 35short_description: Query FortiManager data objects for use in Ansible workflows. 36description: 37 - Provides information on data objects within FortiManager so that playbooks can perform conditionals. 38 39options: 40 adom: 41 description: 42 - The ADOM the configuration should belong to. 43 required: false 44 default: root 45 46 object: 47 description: 48 - The data object we wish to query (device, package, rule, etc). Will expand choices as improves. 49 required: true 50 choices: 51 - device 52 - cluster_nodes 53 - task 54 - custom 55 56 custom_endpoint: 57 description: 58 - ADVANCED USERS ONLY! REQUIRES KNOWLEDGE OF FMGR JSON API! 59 - The HTTP Endpoint on FortiManager you wish to GET from. 60 required: false 61 62 custom_dict: 63 description: 64 - ADVANCED USERS ONLY! REQUIRES KNOWLEDGE OF FMGR JSON API! 65 - DICTIONARY JSON FORMAT ONLY -- Custom dictionary/datagram to send to the endpoint. 66 required: false 67 68 device_ip: 69 description: 70 - The IP of the device you want to query. 71 required: false 72 73 device_unique_name: 74 description: 75 - The desired "friendly" name of the device you want to query. 76 required: false 77 78 device_serial: 79 description: 80 - The serial number of the device you want to query. 81 required: false 82 83 task_id: 84 description: 85 - The ID of the task you wish to query status on. If left blank and object = 'task' a list of tasks are returned. 86 required: false 87 88 nodes: 89 description: 90 - A LIST of firewalls in the cluster you want to verify i.e. ["firewall_A","firewall_B"]. 91 required: false 92''' 93 94 95EXAMPLES = ''' 96- name: QUERY FORTIGATE DEVICE BY IP 97 fmgr_query: 98 object: "device" 99 adom: "ansible" 100 device_ip: "10.7.220.41" 101 102- name: QUERY FORTIGATE DEVICE BY SERIAL 103 fmgr_query: 104 adom: "ansible" 105 object: "device" 106 device_serial: "FGVM000000117992" 107 108- name: QUERY FORTIGATE DEVICE BY FRIENDLY NAME 109 fmgr_query: 110 adom: "ansible" 111 object: "device" 112 device_unique_name: "ansible-fgt01" 113 114- name: VERIFY CLUSTER MEMBERS AND STATUS 115 fmgr_query: 116 adom: "ansible" 117 object: "cluster_nodes" 118 device_unique_name: "fgt-cluster01" 119 nodes: ["ansible-fgt01", "ansible-fgt02", "ansible-fgt03"] 120 121- name: GET STATUS OF TASK ID 122 fmgr_query: 123 adom: "ansible" 124 object: "task" 125 task_id: "3" 126 127- name: USE CUSTOM TYPE TO QUERY AVAILABLE SCRIPTS 128 fmgr_query: 129 adom: "ansible" 130 object: "custom" 131 custom_endpoint: "/dvmdb/adom/ansible/script" 132 custom_dict: { "type": "cli" } 133''' 134 135RETURN = """ 136api_result: 137 description: full API response, includes status code and message 138 returned: always 139 type: str 140""" 141 142from ansible.module_utils.basic import AnsibleModule, env_fallback 143from ansible.module_utils.connection import Connection 144from ansible.module_utils.network.fortimanager.fortimanager import FortiManagerHandler 145from ansible.module_utils.network.fortimanager.common import FMGBaseException 146from ansible.module_utils.network.fortimanager.common import FMGRCommon 147from ansible.module_utils.network.fortimanager.common import FMGRMethods 148from ansible.module_utils.network.fortimanager.common import DEFAULT_RESULT_OBJ 149from ansible.module_utils.network.fortimanager.common import FAIL_SOCKET_MSG 150 151 152def fmgr_get_custom(fmgr, paramgram): 153 """ 154 :param fmgr: The fmgr object instance from fortimanager.py 155 :type fmgr: class object 156 :param paramgram: The formatted dictionary of options to process 157 :type paramgram: dict 158 :return: The response from the FortiManager 159 :rtype: dict 160 """ 161 # IF THE CUSTOM DICTIONARY (OFTEN CONTAINING FILTERS) IS DEFINED CREATED THAT 162 if paramgram["custom_dict"] is not None: 163 datagram = paramgram["custom_dict"] 164 else: 165 datagram = dict() 166 167 # SET THE CUSTOM ENDPOINT PROVIDED 168 url = paramgram["custom_endpoint"] 169 # MAKE THE CALL AND RETURN RESULTS 170 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 171 return response 172 173 174def fmgr_get_task_status(fmgr, paramgram): 175 """ 176 :param fmgr: The fmgr object instance from fortimanager.py 177 :type fmgr: class object 178 :param paramgram: The formatted dictionary of options to process 179 :type paramgram: dict 180 :return: The response from the FortiManager 181 :rtype: dict 182 """ 183 # IF THE TASK_ID IS DEFINED, THEN GET THAT SPECIFIC TASK 184 # OTHERWISE, GET ALL RECENT TASKS IN A LIST 185 if paramgram["task_id"] is not None: 186 187 datagram = { 188 "adom": paramgram["adom"] 189 } 190 url = '/task/task/{task_id}'.format(task_id=paramgram["task_id"]) 191 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 192 else: 193 datagram = { 194 "adom": paramgram["adom"] 195 } 196 url = '/task/task' 197 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 198 return response 199 200 201def fmgr_get_device(fmgr, paramgram): 202 """ 203 This method is used to get information on devices. This will not work on HA_SLAVE nodes, only top level devices. 204 Such as cluster objects and standalone devices. 205 206 :param fmgr: The fmgr object instance from fortimanager.py 207 :type fmgr: class object 208 :param paramgram: The formatted dictionary of options to process 209 :type paramgram: dict 210 :return: The response from the FortiManager 211 :rtype: dict 212 """ 213 # FIRST TRY TO RUN AN UPDATE ON THE DEVICE 214 # RUN A QUICK CLUSTER REFRESH/UPDATE ATTEMPT TO ENSURE WE'RE GETTING THE LATEST INFORMOATION 215 response = DEFAULT_RESULT_OBJ 216 url = "" 217 datagram = {} 218 219 update_url = '/dvm/cmd/update/device' 220 update_dict = { 221 "adom": paramgram['adom'], 222 "device": paramgram['device_unique_name'], 223 "flags": "create_task" 224 } 225 # DO THE UPDATE CALL 226 fmgr.process_request(update_url, update_dict, FMGRMethods.EXEC) 227 228 # SET THE URL 229 url = '/dvmdb/adom/{adom}/device'.format(adom=paramgram["adom"]) 230 device_found = 0 231 response = [] 232 233 # TRY TO FIND IT FIRST BY SERIAL NUMBER 234 if paramgram["device_serial"] is not None: 235 datagram = { 236 "filter": ["sn", "==", paramgram["device_serial"]] 237 } 238 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 239 if len(response[1]) >= 0: 240 device_found = 1 241 242 # CHECK IF ANYTHING WAS RETURNED, IF NOT TRY DEVICE NAME PARAMETER 243 if device_found == 0 and paramgram["device_unique_name"] is not None: 244 datagram = { 245 "filter": ["name", "==", paramgram["device_unique_name"]] 246 } 247 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 248 if len(response[1]) >= 0: 249 device_found = 1 250 251 # CHECK IF ANYTHING WAS RETURNED, IF NOT TRY DEVICE IP ADDRESS 252 if device_found == 0 and paramgram["device_ip"] is not None: 253 datagram = { 254 "filter": ["ip", "==", paramgram["device_ip"]] 255 } 256 response = fmgr.process_request(url, datagram, FMGRMethods.GET) 257 if len(response[1]) >= 0: 258 device_found = 1 259 260 return response 261 262 263def fmgr_get_cluster_nodes(fmgr, paramgram): 264 """ 265 This method is used to get information on devices. This WILL work on HA_SLAVE nodes, but NOT top level standalone 266 devices. 267 Such as cluster objects and standalone devices. 268 269 :param fmgr: The fmgr object instance from fortimanager.py 270 :type fmgr: class object 271 :param paramgram: The formatted dictionary of options to process 272 :type paramgram: dict 273 :return: The response from the FortiManager 274 :rtype: dict 275 """ 276 response = DEFAULT_RESULT_OBJ 277 url = "" 278 datagram = {} 279 # USE THE DEVICE METHOD TO GET THE CLUSTER INFORMATION SO WE CAN SEE THE HA_SLAVE NODES 280 response = fmgr_get_device(fmgr, paramgram) 281 # CHECK FOR HA_SLAVE NODES, IF CLUSTER IS MISSING COMPLETELY THEN QUIT 282 try: 283 returned_nodes = response[1][0]["ha_slave"] 284 num_of_nodes = len(returned_nodes) 285 except Exception: 286 error_msg = {"cluster_status": "MISSING"} 287 return error_msg 288 289 # INIT LOOP RESOURCES 290 loop_count = 0 291 good_nodes = [] 292 expected_nodes = list(paramgram["nodes"]) 293 missing_nodes = list(paramgram["nodes"]) 294 bad_status_nodes = [] 295 296 # LOOP THROUGH THE NODES AND GET THEIR STATUS TO BUILD THE RETURN JSON OBJECT 297 # WE'RE ALSO CHECKING THE NODES IF THEY ARE BAD STATUS, OR PLAIN MISSING 298 while loop_count < num_of_nodes: 299 node_append = { 300 "node_name": returned_nodes[loop_count]["name"], 301 "node_serial": returned_nodes[loop_count]["sn"], 302 "node_parent": returned_nodes[loop_count]["did"], 303 "node_status": returned_nodes[loop_count]["status"], 304 } 305 # IF THE NODE IS IN THE EXPECTED NODES LIST AND WORKING THEN ADD IT TO GOOD NODES LIST 306 if node_append["node_name"] in expected_nodes and node_append["node_status"] == 1: 307 good_nodes.append(node_append["node_name"]) 308 # IF THE NODE IS IN THE EXPECTED NODES LIST BUT NOT WORKING THEN ADDED IT TO BAD_STATUS_NODES 309 # IF THE NODE STATUS IS NOT 1 THEN ITS BAD 310 if node_append["node_name"] in expected_nodes and node_append["node_status"] != 1: 311 bad_status_nodes.append(node_append["node_name"]) 312 # REMOVE THE NODE FROM MISSING NODES LIST IF NOTHING IS WRONG WITH NODE -- LEAVING US A LIST OF 313 # NOT WORKING NODES 314 missing_nodes.remove(node_append["node_name"]) 315 loop_count += 1 316 317 # BUILD RETURN OBJECT FROM NODE LISTS 318 nodes = { 319 "good_nodes": good_nodes, 320 "expected_nodes": expected_nodes, 321 "missing_nodes": missing_nodes, 322 "bad_nodes": bad_status_nodes, 323 "query_status": "good", 324 } 325 if len(nodes["good_nodes"]) == len(nodes["expected_nodes"]): 326 nodes["cluster_status"] = "OK" 327 else: 328 nodes["cluster_status"] = "NOT-COMPLIANT" 329 return nodes 330 331 332def main(): 333 argument_spec = dict( 334 adom=dict(required=False, type="str", default="root"), 335 object=dict(required=True, type="str", choices=["device", "cluster_nodes", "task", "custom"]), 336 custom_endpoint=dict(required=False, type="str"), 337 custom_dict=dict(required=False, type="dict"), 338 device_ip=dict(required=False, type="str"), 339 device_unique_name=dict(required=False, type="str"), 340 device_serial=dict(required=False, type="str"), 341 nodes=dict(required=False, type="list"), 342 task_id=dict(required=False, type="str") 343 ) 344 345 module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False, ) 346 paramgram = { 347 "adom": module.params["adom"], 348 "object": module.params["object"], 349 "device_ip": module.params["device_ip"], 350 "device_unique_name": module.params["device_unique_name"], 351 "device_serial": module.params["device_serial"], 352 "nodes": module.params["nodes"], 353 "task_id": module.params["task_id"], 354 "custom_endpoint": module.params["custom_endpoint"], 355 "custom_dict": module.params["custom_dict"] 356 } 357 module.paramgram = paramgram 358 fmgr = None 359 if module._socket_path: 360 connection = Connection(module._socket_path) 361 fmgr = FortiManagerHandler(connection, module) 362 fmgr.tools = FMGRCommon() 363 else: 364 module.fail_json(**FAIL_SOCKET_MSG) 365 366 results = DEFAULT_RESULT_OBJ 367 368 try: 369 # IF OBJECT IS DEVICE 370 if paramgram["object"] == "device" and any(v is not None for v in [paramgram["device_unique_name"], 371 paramgram["device_serial"], 372 paramgram["device_ip"]]): 373 results = fmgr_get_device(fmgr, paramgram) 374 if results[0] not in [0]: 375 module.fail_json(msg="Device query failed!") 376 elif len(results[1]) == 0: 377 module.exit_json(msg="Device NOT FOUND!") 378 else: 379 module.exit_json(msg="Device Found", **results[1][0]) 380 except Exception as err: 381 raise FMGBaseException(err) 382 383 try: 384 # IF OBJECT IS CLUSTER_NODES 385 if paramgram["object"] == "cluster_nodes" and paramgram["nodes"] is not None: 386 results = fmgr_get_cluster_nodes(fmgr, paramgram) 387 if results["cluster_status"] == "MISSING": 388 module.exit_json(msg="No cluster device found!", **results) 389 elif results["query_status"] == "good": 390 module.exit_json(msg="Cluster Found - Showing Nodes", **results) 391 elif results is None: 392 module.fail_json(msg="Query FAILED -- Check module or playbook syntax") 393 except Exception as err: 394 raise FMGBaseException(err) 395 396 try: 397 # IF OBJECT IS TASK 398 if paramgram["object"] == "task": 399 results = fmgr_get_task_status(fmgr, paramgram) 400 if results[0] != 0: 401 module.fail_json(**results[1]) 402 if results[0] == 0: 403 module.exit_json(**results[1]) 404 except Exception as err: 405 raise FMGBaseException(err) 406 407 try: 408 # IF OBJECT IS CUSTOM 409 if paramgram["object"] == "custom": 410 results = fmgr_get_custom(fmgr, paramgram) 411 if results[0] != 0: 412 module.fail_json(msg="QUERY FAILED -- Please check syntax check JSON guide if needed.") 413 if results[0] == 0: 414 results_len = len(results[1]) 415 if results_len > 0: 416 results_combine = dict() 417 if isinstance(results[1], dict): 418 results_combine["results"] = results[1] 419 if isinstance(results[1], list): 420 results_combine["results"] = results[1][0:results_len] 421 module.exit_json(msg="Custom Query Success", **results_combine) 422 else: 423 module.exit_json(msg="NO RESULTS") 424 except Exception as err: 425 raise FMGBaseException(err) 426 427 return module.exit_json(**results[1]) 428 429 430if __name__ == "__main__": 431 main() 432