From 050a2c51dd065b6152052ea75cdfcadd4d7c88e0 Mon Sep 17 00:00:00 2001 From: ftntcorecse <43451990+ftntcorecse@users.noreply.github.com> Date: Tue, 20 Nov 2018 23:33:38 -0700 Subject: [PATCH] Fortinet FortiManager Device Config Module (#46080) * fmgr_device_config PR candidate * fmgr_fwobj_address PR candidate * PR candidate * Resolving Edits * Resolving Edits * Fixing Authors --- .../fortimanager/fmgr_device_config.py | 288 ++++++++++++++++++ .../fixtures/test_fmgr_device_config.json | 173 +++++++++++ .../fortimanager/test_fmgr_device_config.py | 266 ++++++++++++++++ 3 files changed, 727 insertions(+) create mode 100644 lib/ansible/modules/network/fortimanager/fmgr_device_config.py create mode 100644 test/units/modules/network/fortimanager/fixtures/test_fmgr_device_config.json create mode 100644 test/units/modules/network/fortimanager/test_fmgr_device_config.py diff --git a/lib/ansible/modules/network/fortimanager/fmgr_device_config.py b/lib/ansible/modules/network/fortimanager/fmgr_device_config.py new file mode 100644 index 0000000000..0b7dc32101 --- /dev/null +++ b/lib/ansible/modules/network/fortimanager/fmgr_device_config.py @@ -0,0 +1,288 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community" +} + +DOCUMENTATION = ''' +--- +module: fmgr_device_config +version_added: "2.8" +author: + - Luke Weighall (@lweighall) + - Andrew Welsh (@Ghilli3) + - Jim Huber (@p4r4n0y1ng) +short_description: Edit device configurations +description: + - Edit device configurations from FortiManager Device Manager using JSON RPC API. + +options: + adom: + description: + - The ADOM the configuration should belong to. + required: false + default: root + host: + description: + - The FortiManager's address. + required: true + username: + description: + - The username used to authenticate with the FortiManager. + required: false + password: + description: + - The password associated with the username account. + required: false + + device_unique_name: + description: + - The unique device's name that you are editing. A.K.A. Friendly name of the device in FortiManager. + required: True + device_hostname: + description: + - The device's new hostname. + required: false + + install_config: + description: + - Tells FMGR to attempt to install the config after making it. + required: false + default: disable + interface: + description: + - The interface/port number you are editing. + required: false + interface_ip: + description: + - The IP and subnet of the interface/port you are editing. + required: false + interface_allow_access: + description: + - Specify what protocols are allowed on the interface, comma-sepeareted list (see examples). + required: false + +''' + +EXAMPLES = ''' +- name: CHANGE HOSTNAME + fmgr_device_config: + host: "{{inventory_hostname}}" + username: "{{ username }}" + password: "{{ password }}" + device_hostname: "ChangedbyAnsible" + device_unique_name: "FGT1" + +- name: EDIT INTERFACE INFORMATION + fmgr_device_config: + host: "{{inventory_hostname}}" + username: "{{ username }}" + password: "{{ password }}" + adom: "root" + device_unique_name: "FGT2" + interface: "port3" + interface_ip: "10.1.1.1/24" + interface_allow_access: "ping, telnet, https" + +- name: INSTALL CONFIG + fmgr_device_config: + host: "{{inventory_hostname}}" + username: "{{ username }}" + password: "{{ password }}" + adom: "root" + device_unique_name: "FGT1" + install_config: "enable" +''' + +RETURN = """ +api_result: + description: full API response, includes status code and message + returned: always + type: string +""" + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.network.fortimanager.fortimanager import AnsibleFortiManager + +# check for pyFMG lib +try: + from pyFMG.fortimgr import FortiManager + HAS_PYFMGR = True +except ImportError: + HAS_PYFMGR = False + + +def update_device_hostname(fmg, paramgram): + """ + Change a device's hostname + """ + datagram = { + "hostname": paramgram["device_hostname"] + } + + url = "pm/config/device/{device_name}/global/system/global".format(device_name=paramgram["device_unique_name"]) + response = fmg.update(url, datagram) + return response + + +def update_device_interface(fmg, paramgram): + """ + Update a device interface IP and allow access + """ + access_list = list() + allow_access_list = paramgram["interface_allow_access"].replace(' ', '') + access_list = allow_access_list.split(',') + + datagram = { + "allowaccess": access_list, + "ip": paramgram["interface_ip"] + } + + url = "/pm/config/device/{device_name}/global/system/interface" \ + "/{interface}".format(device_name=paramgram["device_unique_name"], interface=paramgram["interface"]) + response = fmg.update(url, datagram) + return response + + +def exec_config(fmg, paramgram): + """ + Update a device interface IP and allow access + """ + datagram = { + "scope": { + "name": paramgram["device_unique_name"] + }, + "adom": paramgram["adom"], + "flags": "none" + } + + url = "/securityconsole/install/device" + response = fmg.execute(url, datagram) + return response + + +# FUNCTION/METHOD FOR LOGGING OUT AND ANALYZING ERROR CODES +def fmgr_logout(fmg, module, msg="NULL", results=(), good_codes=(0,), logout_on_fail=True, logout_on_success=False): + """ + THIS METHOD CONTROLS THE LOGOUT AND ERROR REPORTING AFTER AN METHOD OR FUNCTION RUNS + """ + + # VALIDATION ERROR (NO RESULTS, JUST AN EXIT) + if msg != "NULL" and len(results) == 0: + try: + fmg.logout() + except: + pass + module.fail_json(msg=msg) + + # SUBMISSION ERROR + if len(results) > 0: + if msg == "NULL": + try: + msg = results[1]['status']['message'] + except: + msg = "No status message returned from pyFMG. Possible that this was a GET with a tuple result." + + if results[0] not in good_codes: + if logout_on_fail: + fmg.logout() + module.fail_json(msg=msg, **results[1]) + else: + return_msg = msg + " -- LOGOUT ON FAIL IS OFF, MOVING ON" + return return_msg + else: + if logout_on_success: + fmg.logout() + module.exit_json(msg=msg, **results[1]) + else: + return_msg = msg + " -- LOGOUT ON SUCCESS IS OFF, MOVING ON TO REST OF CODE" + return return_msg + + +def main(): + argument_spec = dict( + host=dict(required=True, type="str"), + adom=dict(required=False, type="str", default="root"), + password=dict(fallback=(env_fallback, ["ANSIBLE_NET_PASSWORD"]), no_log=True), + username=dict(fallback=(env_fallback, ["ANSIBLE_NET_USERNAME"])), + + device_unique_name=dict(required=True, type="str"), + device_hostname=dict(required=False, type="str"), + interface=dict(required=False, type="str"), + interface_ip=dict(required=False, type="str"), + interface_allow_access=dict(required=False, type="str"), + install_config=dict(required=False, type="str", default="disable"), + ) + + module = AnsibleModule(argument_spec, supports_check_mode=True,) + + # handle params passed via provider and insure they are represented as the data type expected by fortimanager + paramgram = { + "device_unique_name": module.params["device_unique_name"], + "device_hostname": module.params["device_hostname"], + "interface": module.params["interface"], + "interface_ip": module.params["interface_ip"], + "interface_allow_access": module.params["interface_allow_access"], + "install_config": module.params["install_config"], + "adom": module.params["adom"] + } + + # check if params are set + if module.params["host"] is None or module.params["username"] is None or module.params["password"] is None: + module.fail_json(msg="Host and username are required for connection") + + # CHECK IF LOGIN FAILED + fmg = AnsibleFortiManager(module, module.params["host"], module.params["username"], module.params["password"]) + response = fmg.login() + if response[1]['status']['code'] != 0: + module.fail_json(msg="Connection to FortiManager Failed") + else: + + # START SESSION LOGIC + + # if the device_hostname isn't null, then attempt the api call via method call, store results in variable + if paramgram["device_hostname"] is not None: + # add device + results = update_device_hostname(fmg, paramgram) + if results[0] != 0: + fmgr_logout(fmg, module, msg="Failed to set Hostname", results=results, good_codes=[0]) + + if paramgram["interface_ip"] is not None or paramgram["interface_allow_access"] is not None: + results = update_device_interface(fmg, paramgram) + if results[0] != 0: + fmgr_logout(fmg, module, msg="Failed to Update Device Interface", results=results, good_codes=[0]) + + if paramgram["install_config"] == "enable": + # attempt to install the config + results = exec_config(fmg, paramgram) + if results[0] != 0: + fmgr_logout(fmg, module, msg="Failed to Update Device Interface", results=results, good_codes=[0]) + + # logout, build in check for future logging capabilities + fmg.logout() + return module.exit_json(**results[1]) + + +if __name__ == "__main__": + main() diff --git a/test/units/modules/network/fortimanager/fixtures/test_fmgr_device_config.json b/test/units/modules/network/fortimanager/fixtures/test_fmgr_device_config.json new file mode 100644 index 0000000000..1ab30314d0 --- /dev/null +++ b/test/units/modules/network/fortimanager/fixtures/test_fmgr_device_config.json @@ -0,0 +1,173 @@ +{ + "update_device_interface": [ + { + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "/pm/config/device/FGT1/global/system/interface/port2" + }, + "paramgram_used": { + "adom": "ansible", + "install_config": "disable", + "device_unique_name": "FGT1", + "interface": "port2", + "device_hostname": null, + "interface_ip": "10.1.1.1/24", + "interface_allow_access": "ping, telnet, https, http" + }, + "post_method": "update" + }, + { + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "/pm/config/device/FGT2/global/system/interface/port2" + }, + "paramgram_used": { + "adom": "ansible", + "install_config": "disable", + "device_unique_name": "FGT2", + "interface": "port2", + "device_hostname": null, + "interface_ip": "10.1.2.1/24", + "interface_allow_access": "ping, telnet, https, http" + }, + "post_method": "update" + }, + { + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "/pm/config/device/FGT3/global/system/interface/port2" + }, + "paramgram_used": { + "adom": "ansible", + "install_config": "disable", + "device_unique_name": "FGT3", + "interface": "port2", + "device_hostname": null, + "interface_ip": "10.1.3.1/24", + "interface_allow_access": "ping, telnet, https, http" + }, + "post_method": "update" + } + ], + "update_device_hostname": [ + { + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "pm/config/device/FGT1/global/system/global" + }, + "paramgram_used": { + "adom": "ansible", + "install_config": "disable", + "device_unique_name": "FGT1", + "interface": null, + "device_hostname": "ansible-fgt01", + "interface_ip": null, + "interface_allow_access": null + }, + "post_method": "update" + }, + { + "paramgram_used": { + "adom": "ansible", + "interface": null, + "device_unique_name": "FGT1", + "install_config": "disable", + "device_hostname": "ansible-fgt01", + "interface_ip": null, + "interface_allow_access": null + }, + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "pm/config/device/FGT1/global/system/global" + }, + "post_method": "update" + }, + { + "paramgram_used": { + "adom": "ansible", + "interface": null, + "device_unique_name": "FGT2", + "install_config": "disable", + "device_hostname": "ansible-fgt02", + "interface_ip": null, + "interface_allow_access": null + }, + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "pm/config/device/FGT2/global/system/global" + }, + "post_method": "update" + }, + { + "paramgram_used": { + "adom": "ansible", + "interface": null, + "device_unique_name": "FGT3", + "install_config": "disable", + "device_hostname": "ansible-fgt03", + "interface_ip": null, + "interface_allow_access": null + }, + "raw_response": { + "status": { + "message": "OK", + "code": 0 + }, + "url": "pm/config/device/FGT3/global/system/global" + }, + "post_method": "update" + } + ], + "exec_config": [ + { + "url": "/securityconsole/install/device", + "paramgram_used": { + "adom": "ansible", + "interface": null, + "device_unique_name": "FGT1", + "install_config": "enable", + "device_hostname": null, + "interface_ip": null, + "interface_allow_access": null + }, + "raw_response": { + "task": 352 + }, + "post_method": "execute" + }, + { + "url": "/securityconsole/install/device", + "raw_response": { + "task": 353 + }, + "paramgram_used": { + "adom": "ansible", + "install_config": "enable", + "device_unique_name": "FGT2, FGT3", + "interface": null, + "device_hostname": null, + "interface_ip": null, + "interface_allow_access": null + }, + "post_method": "execute" + } + ] +} diff --git a/test/units/modules/network/fortimanager/test_fmgr_device_config.py b/test/units/modules/network/fortimanager/test_fmgr_device_config.py new file mode 100644 index 0000000000..4160ada37c --- /dev/null +++ b/test/units/modules/network/fortimanager/test_fmgr_device_config.py @@ -0,0 +1,266 @@ +# Copyright 2018 Fortinet, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +from pyFMG.fortimgr import FortiManager +import pytest + +try: + from ansible.modules.network.fortimanager import fmgr_device_config +except ImportError: + pytest.skip( + "Could not load required modules for testing", + allow_module_level=True) + +fmg_instance = FortiManager("1.1.1.1", "admin", "") + + +def load_fixtures(): + fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') + "/{filename}.json".format( + filename=os.path.splitext(os.path.basename(__file__))[0]) + try: + with open(fixture_path, "r") as fixture_file: + fixture_data = json.load(fixture_file) + except IOError: + return [] + return [fixture_data] + + +@pytest.fixture(scope="function", params=load_fixtures()) +def fixture_data(request): + func_name = request.function.__name__.replace("test_", "") + return request.param.get(func_name, None) + + +def test_update_device_hostname(fixture_data, mocker): + mocker.patch( + "pyFMG.fortimgr.FortiManager._post_request", + side_effect=fixture_data) + + paramgram_used = { + 'adom': 'ansible', + 'install_config': 'disable', + 'device_unique_name': 'FGT1', + 'interface': None, + 'device_hostname': 'ansible-fgt01', + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'update'} + output = fmgr_device_config.update_device_hostname( + fmg_instance, paramgram_used) + # + # adom: ansible + # install_config: disable + # device_unique_name: FGT1 + # interface: None + # device_hostname: ansible-fgt01 + # interface_ip: None + # interface_allow_access: None + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + paramgram_used = { + 'adom': 'ansible', + 'interface': None, + 'device_unique_name': 'FGT1', + 'install_config': 'disable', + 'device_hostname': 'ansible-fgt01', + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'update'} + output = fmgr_device_config.update_device_hostname( + fmg_instance, paramgram_used) + # + # adom: ansible + # interface: None + # device_unique_name: FGT1 + # install_config: disable + # device_hostname: ansible-fgt01 + # interface_ip: None + # interface_allow_access: None + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + paramgram_used = { + 'adom': 'ansible', + 'interface': None, + 'device_unique_name': 'FGT2', + 'install_config': 'disable', + 'device_hostname': 'ansible-fgt02', + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'update'} + output = fmgr_device_config.update_device_hostname( + fmg_instance, paramgram_used) + # + # adom: ansible + # interface: None + # device_unique_name: FGT2 + # install_config: disable + # device_hostname: ansible-fgt02 + # interface_ip: None + # interface_allow_access: None + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + paramgram_used = { + 'adom': 'ansible', + 'interface': None, + 'device_unique_name': 'FGT3', + 'install_config': 'disable', + 'device_hostname': 'ansible-fgt03', + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'update'} + output = fmgr_device_config.update_device_hostname( + fmg_instance, paramgram_used) + # + # adom: ansible + # interface: None + # device_unique_name: FGT3 + # install_config: disable + # device_hostname: ansible-fgt03 + # interface_ip: None + # interface_allow_access: None + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + + +def test_update_device_interface(fixture_data, mocker): + mocker.patch( + "pyFMG.fortimgr.FortiManager._post_request", + side_effect=fixture_data) + + paramgram_used = { + 'adom': 'ansible', + 'install_config': 'disable', + 'device_unique_name': 'FGT1', + 'interface': 'port2', + 'device_hostname': None, + 'interface_ip': '10.1.1.1/24', + 'interface_allow_access': 'ping, telnet, https, http', + 'mode': 'update'} + output = fmgr_device_config.update_device_interface( + fmg_instance, paramgram_used) + # + # adom: ansible + # install_config: disable + # device_unique_name: FGT1 + # interface: port2 + # device_hostname: None + # interface_ip: 10.1.1.1/24 + # interface_allow_access: ping, telnet, https, http + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + paramgram_used = { + 'adom': 'ansible', + 'install_config': 'disable', + 'device_unique_name': 'FGT2', + 'interface': 'port2', + 'device_hostname': None, + 'interface_ip': '10.1.2.1/24', + 'interface_allow_access': 'ping, telnet, https, http', + 'mode': 'update'} + output = fmgr_device_config.update_device_interface( + fmg_instance, paramgram_used) + # + # adom: ansible + # install_config: disable + # device_unique_name: FGT2 + # interface: port2 + # device_hostname: None + # interface_ip: 10.1.2.1/24 + # interface_allow_access: ping, telnet, https, http + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + paramgram_used = { + 'adom': 'ansible', + 'install_config': 'disable', + 'device_unique_name': 'FGT3', + 'interface': 'port2', + 'device_hostname': None, + 'interface_ip': '10.1.3.1/24', + 'interface_allow_access': 'ping, telnet, https, http', + 'mode': 'update'} + output = fmgr_device_config.update_device_interface( + fmg_instance, paramgram_used) + # + # adom: ansible + # install_config: disable + # device_unique_name: FGT3 + # interface: port2 + # device_hostname: None + # interface_ip: 10.1.3.1/24 + # interface_allow_access: ping, telnet, https, http + # mode: update + # + assert output['raw_response']['status']['code'] == 0 + + +def test_exec_config(fixture_data, mocker): + mocker.patch( + "pyFMG.fortimgr.FortiManager._post_request", + side_effect=fixture_data) + + paramgram_used = { + 'adom': 'ansible', + 'interface': None, + 'device_unique_name': 'FGT1', + 'install_config': 'enable', + 'device_hostname': None, + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'execute'} + output = fmgr_device_config.exec_config(fmg_instance, paramgram_used) + # + # adom: ansible + # interface: None + # device_unique_name: FGT1 + # install_config: enable + # device_hostname: None + # interface_ip: None + # interface_allow_access: None + # mode: execute + # + assert isinstance(output['raw_response'], dict) is True + paramgram_used = { + 'adom': 'ansible', + 'install_config': 'enable', + 'device_unique_name': 'FGT2, FGT3', + 'interface': None, + 'device_hostname': None, + 'interface_ip': None, + 'interface_allow_access': None, + 'mode': 'execute'} + output = fmgr_device_config.exec_config(fmg_instance, paramgram_used) + # + # adom: ansible + # install_config: enable + # device_unique_name: FGT2, FGT3 + # interface: None + # device_hostname: None + # interface_ip: None + # interface_allow_access: None + # mode: execute + # + assert isinstance(output['raw_response'], dict) is True