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