From 2ee3a5aa073551af21b4ebee1feaac594534f78e Mon Sep 17 00:00:00 2001 From: Benjamin Jolivot Date: Wed, 1 Mar 2017 23:11:36 +0100 Subject: [PATCH] Fortios ipv4 policy (#21849) * New module fortios_address_group * New module fortios_ipv4_policy * New module fortios_ipv4_policy * Fix pep8 * Fix alias doc problem * Fix string format for 2.5 compat + close cnx * Forgoten if string != "" * Fix doc, change action to policy_action & add action as alias * fix doc + bug in timeout + duplicate code for config compare * Create class AnsibleFortios in module_utils/forios.py + use in ipv4_policy module * remove json import * python3 error handling compatibility bad examples for srcadd or dstaddr s/any/all/ remove pyFG dependency in module (moved to module_utils) id type is int but casted as string call fortiosansible object sooner typo in doc --- lib/ansible/module_utils/fortios.py | 111 +++++++ .../network/fortios/fortios_ipv4_policy.py | 289 ++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 lib/ansible/modules/network/fortios/fortios_ipv4_policy.py diff --git a/lib/ansible/module_utils/fortios.py b/lib/ansible/module_utils/fortios.py index ce1cf7296b..cbb0bd9249 100644 --- a/lib/ansible/module_utils/fortios.py +++ b/lib/ansible/module_utils/fortios.py @@ -29,6 +29,17 @@ import os import time +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + +#check for pyFG lib +try: + from pyFG import FortiOS, FortiConfig + from pyFG.exceptions import CommandExecutionException, FailedCommit + HAS_PYFG=True +except: + HAS_PYFG=False fortios_argument_spec = dict( host = dict(required=True ), @@ -45,6 +56,13 @@ fortios_required_if = [ ['backup', True , ['backup_path'] ], ] + +fortios_error_codes = { + '-3':"Object not found", + '-61':"Command error" +} + + def backup(module,running_config): backup_path = module.params['backup_path'] if not os.path.exists(backup_path): @@ -58,3 +76,96 @@ def backup(module,running_config): open(filename, 'w').write(running_config) except: module.fail_json(msg="Can't create backup file {0} Permission denied ?".format(filename)) + + + + +class AnsibleFortios(object): + def __init__(self, module): + if not HAS_PYFG: + module.fail_json(msg='Could not import the python library pyFG required by this module') + + self.result = { + 'changed': False, + } + self.module = module + + + def _connect(self): + host = self.module.params['host'] + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + vdom = self.module.params['vdom'] + + self.forti_device = FortiOS(host, username=username, password=password, timeout=timeout, vdom=vdom) + + try: + self.forti_device.open() + except Exception: + e = get_exception() + self.module.fail_json(msg='Error connecting device. %s' % e) + + + def load_config(self, path): + self._connect() + self.path = path + #get config + try: + self.forti_device.load_config(path=path) + self.result['running_config'] = self.forti_device.running_config.to_text() + except Exception: + self.forti_device.close() + e = get_exception() + self.module.fail_json(msg='Error reading running config. %s' % e) + + #backup if needed + if self.module.params['backup']: + backup(self.module, self.result['running_config']) + + self.candidate_config = self.forti_device.candidate_config + + + def apply_changes(self): + change_string = self.forti_device.compare_config() + if change_string: + self.result['change_string'] = change_string + self.result['changed'] = True + + #Commit if not check mode + if change_string and not self.module.check_mode: + try: + self.forti_device.commit() + except FailedCommit: + #Something's wrong (rollback is automatic) + self.forti_device.close() + e = get_exception() + error_list = self.get_error_infos(e) + self.module.fail_json(msg_error_list=error_list, msg="Unable to commit change, check your args, the error was %s" % e.message ) + + self.forti_device.close() + self.module.exit_json(**self.result) + + + def del_block(self, block_id): + self.forti_device.candidate_config[self.path].del_block(block_id) + + + def add_block(self, block_id, block): + self.forti_device.candidate_config[self.path][block_id] = block + + + def get_error_infos(self, cli_errors): + error_list = [] + for errors in cli_errors.args: + for error in errors: + error_code = error[0] + error_string = error[1] + error_type = fortios_error_codes.get(error_code,"unknown") + error_list.append(dict(error_code=error_code, error_type=error_type, error_string= error_string)) + + return error_list + + def get_empty_configuration_block(self, block_name, block_type): + return FortiConfig(block_name, block_type) + diff --git a/lib/ansible/modules/network/fortios/fortios_ipv4_policy.py b/lib/ansible/modules/network/fortios/fortios_ipv4_policy.py new file mode 100644 index 0000000000..af6b179657 --- /dev/null +++ b/lib/ansible/modules/network/fortios/fortios_ipv4_policy.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# +# Ansible module to manage IPv4 policy objects in fortigate devices +# (c) 2017, Benjamin Jolivot +# +# 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 . +# + +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0' +} + +DOCUMENTATION = """ +--- +module: fortios_ipv4_policy +version_added: "2.3" +author: "Benjamin Jolivot (@bjolivot)" +short_description: Manage fortios firewall IPv4 policy objects +description: + - This module provides management of firewall IPv4 policies on FortiOS devices. +extends_documentation_fragment: fortios +options: + id: + description: + - Policy ID. + required: true + state: + description: + - Specifies if address need to be added or deleted. + choices: ['present', 'absent'] + default: present + src_intf: + description: + - Specifies source interface name. + default: any + dst_intf: + description: + - Specifies destination interface name. + default: any + src_addr: + description: + - Specifies source address (or group) object name(s). + required: true + src_addr_negate: + description: + - Negate source address param. + default: false + choices: ["true", "false"] + dst_addr: + description: + - Specifies destination address (or group) object name(s). + required: true + dst_addr_negate: + description: + - Negate destination address param. + default: false + choices: ["true", "false"] + policy_action: + description: + - Specifies accept or deny action policy. + choices: ['accept', 'deny'] + required: true + aliases: ['action'] + service: + description: + - "Specifies policy service(s), could be a list (ex: ['MAIL','DNS'])." + required: true + aliases: + - services + service_negate: + description: + - Negate policy service(s) defined in service value. + default: false + choices: ["true", "false"] + schedule: + description: + - defines policy schedule. + default: 'always' + nat: + description: + - Enable or disable Nat. + default: false + choices: ["true", "false"] + fixedport: + description: + - Use fixed port for nat. + default: false + choices: ["true", "false"] + poolname: + description: + - Specifies NAT pool name. + av_profile: + description: + - Specifies Antivirus profile name. + webfilter_profile: + description: + - Specifies Webfilter profile name. + ips_sensor: + description: + - Specifies IPS Sensor profile name. + application_list: + description: + - Specifies Application Control name. + comment: + description: + - free text to describe policy. +notes: + - This module requires pyFG library. +""" + +EXAMPLES = """ +- name: Allow external DNS call + fortios_ipv4_policy: + host: 192.168.0.254 + username: admin + password: password + id: 42 + srcaddr: internal_network + dstaddr: all + service: dns + nat: True + state: present + policy_action: accept + +- name: Public Web + fortios_ipv4_policy: + host: 192.168.0.254 + username: admin + password: password + id: 42 + srcaddr: all + dstaddr: webservers + services: + - http + - https + state: present + policy_action: accept +""" + +RETURN = """ +firewall_address_config: + description: full firewall adresses config string + returned: always + type: string +change_string: + description: The commands executed by the module + returned: only if config changed + type: string +msg_error_list: + description: "List of errors returned by CLI (use -vvv for better readability)." + returned: only when error + type: string +""" + +from ansible.module_utils.fortios import fortios_argument_spec, fortios_required_if +from ansible.module_utils.fortios import backup, AnsibleFortios + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + +def main(): + argument_spec = dict( + comment = dict(type='str'), + id = dict(type='int', required=True), + src_intf = dict(default='any'), + dst_intf = dict(default='any'), + state = dict(choices=['present', 'absent'], default='present'), + src_addr = dict(required=True, type='list'), + dst_addr = dict(required=True, type='list'), + src_addr_negate = dict(type='bool', default=False), + dst_addr_negate = dict(type='bool', default=False), + policy_action = dict(choices=['accept','deny'], required=True, aliases=['action']), + service = dict(aliases=['services'], required=True, type='list'), + service_negate = dict(type='bool', default=False), + schedule = dict(type='str', default='always'), + nat = dict(type='bool', default=False), + fixedport = dict(type='bool', default=False), + poolname = dict(type='str'), + av_profile = dict(type='str'), + webfilter_profile = dict(type='str'), + ips_sensor = dict(type='str'), + application_list = dict(type='str'), + ) + + #merge global required_if & argument_spec from module_utils/fortios.py + argument_spec.update(fortios_argument_spec) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=fortios_required_if, + ) + + #init forti object + fortigate = AnsibleFortios(module) + + #test params + #NAT related + if not module.params['nat']: + if module.params['poolname']: + module.fail_json(msg='Poolname param requires NAT to be true.') + if module.params['fixedport']: + module.fail_json(msg='Fixedport param requires NAT to be true.') + + #id must be str(int) for pyFG to work + policy_id = str(module.params['id']) + + #load config + fortigate.load_config('firewall policy') + + #Absent State + if module.params['state'] == 'absent': + fortigate.candidate_config[path].del_block(policy_id) + + #Present state + elif module.params['state'] == 'present': + new_policy = fortigate.get_empty_configuration_block(policy_id, 'edit') + + #src / dest / service / interfaces + new_policy.set_param('srcintf', '"%s"' % (module.params['src_intf'])) + new_policy.set_param('dstintf', '"%s"' % (module.params['dst_intf'])) + + + new_policy.set_param('srcaddr', " ".join('"' + item + '"' for item in module.params['src_addr'])) + new_policy.set_param('dstaddr', " ".join('"' + item + '"' for item in module.params['dst_addr'])) + new_policy.set_param('service', " ".join('"' + item + '"' for item in module.params['service'])) + + # negate src / dest / service + if module.params['src_addr_negate']: + new_policy.set_param('srcaddr-negate', 'enable') + if module.params['dst_addr_negate']: + new_policy.set_param('dstaddr-negate', 'enable') + if module.params['service_negate']: + new_policy.set_param('service-negate', 'enable') + + # action + new_policy.set_param('action', '%s' % (module.params['policy_action'])) + + # Schedule + new_policy.set_param('schedule', '%s' % (module.params['schedule'])) + + #NAT + if module.params['nat']: + new_policy.set_param('nat', 'enable') + if module.params['fixedport']: + new_policy.set_param('fixedport', 'enable') + if module.params['poolname'] is not None: + new_policy.set_param('ippool', 'enable') + new_policy.set_param('poolname', '"%s"' % (module.params['poolname'])) + + #security profiles: + if module.params['av_profile'] is not None: + new_policy.set_param('av-profile', '"%s"' % (module.params['av_profile'])) + if module.params['webfilter_profile'] is not None: + new_policy.set_param('webfilter-profile', '"%s"' % (module.params['webfilter_profile'])) + if module.params['ips_sensor'] is not None: + new_policy.set_param('ips-sensor', '"%s"' % (module.params['ips_sensor'])) + if module.params['application_list'] is not None: + new_policy.set_param('application-list', '"%s"' % (module.params['application_list'])) + + # comment + if module.params['comment'] is not None: + new_policy.set_param('comment', '"%s"' % (module.params['comment'])) + + #add the new policy to the device + fortigate.add_block(policy_id, new_policy) + + #Apply changes + fortigate.apply_changes() + +if __name__ == '__main__': + main() +