Manage Fortios/Fortigate Address (#21542)

* New module fortios_address

* Add module_utils required_if + fix Doc

* Merge spec & required_if from module_utils

* Fix pep8

* Py2.5 compat , cosmetic changes

* Fix param timeout

* Fortios_address module + integration tests

* add netaddr library in requirements for integration tests

* Pep8 problems

* ANSIBLE_METADATA.version -> ANSIBLE_METADATA.metadata_version
This commit is contained in:
Benjamin Jolivot 2017-08-01 19:17:12 +02:00 committed by Chris Alfonso
parent 1b7ac73c85
commit 753b26ccf9
9 changed files with 6848 additions and 0 deletions

View file

@ -0,0 +1,304 @@
#!/usr/bin/python
#
# Ansible module to manage IP addresses on fortios devices
# (c) 2016, Benjamin Jolivot <bjolivot@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.0'
}
DOCUMENTATION = """
---
module: fortios_address
version_added: "2.4"
author: "Benjamin Jolivot (@bjolivot)"
short_description: Manage fortios firewall address objects
description:
- This module provide management of firewall addresses on FortiOS devices.
extends_documentation_fragment: fortios
options:
state:
description:
- Specifies if address need to be added or deleted.
required: true
choices: ['present', 'absent']
name:
description:
- Name of the address to add or delete.
required: true
type:
description:
- Type of the address.
choices: ['iprange', 'fqdn', 'ipmask', 'geography']
value:
description:
- Address value, based on type.
If type=fqdn, somthing like www.google.com.
If type=ipmask, you can use simple ip (192.168.0.1), ip+mask (192.168.0.1 255.255.255.0) or CIDR (192.168.0.1/32).
start_ip:
description:
- First ip in range (used only with type=iprange).
end_ip:
description:
- Last ip in range (used only with type=iprange).
country:
description:
- 2 letter country code (like FR).
interface:
description:
- interface name the address apply to.
default: any
comment:
description:
- free text to describe address.
notes:
- This module requires netaddr python library.
"""
EXAMPLES = """
- name: Register french addresses
fortios_address:
host: 192.168.0.254
username: admin
password: p4ssw0rd
state: present
name: "fromfrance"
type: geography
country: FR
comment: "French geoip address"
- name: Register some fqdn
fortios_address:
host: 192.168.0.254
username: admin
password: p4ssw0rd
state: present
name: "Ansible"
type: fqdn
value: www.ansible.com
comment: "Ansible website"
- name: Register google DNS
fortios_address:
host: 192.168.0.254
username: admin
password: p4ssw0rd
state: present
name: "google_dns"
type: ipmask
value: 8.8.8.8
"""
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
"""
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
# check for netaddr lib
try:
from netaddr import IPNetwork
HAS_NETADDR = True
except:
HAS_NETADDR = False
# define valid country list for GEOIP address type
FG_COUNTRY_LIST = (
'ZZ', 'A1', 'A2', 'O1', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO',
'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE',
'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT',
'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL',
'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK',
'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'EU', 'FI', 'FJ',
'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL',
'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT',
'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW',
'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY',
'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP',
'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE',
'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF',
'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ',
'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC',
'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV',
'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI',
'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW'
)
def get_formated_ipaddr(input_ip):
"""
Format given ip address string to fortigate format (ip netmask)
Args:
* **ip_str** (string) : string representing ip address
accepted format:
- ip netmask (ex: 192.168.0.10 255.255.255.0)
- ip (ex: 192.168.0.10)
- CIDR (ex: 192.168.0.10/24)
Returns:
formated ip if ip is valid (ex: "192.168.0.10 255.255.255.0")
False if ip is not valid
"""
try:
if " " in input_ip:
# ip netmask format
str_ip, str_netmask = input_ip.split(" ")
ip = IPNetwork(str_ip)
mask = IPNetwork(str_netmask)
return "%s %s" % (str_ip, str_netmask)
else:
ip = IPNetwork(input_ip)
return "%s %s" % (str(ip.ip), str(ip.netmask))
except:
return False
return False
def main():
argument_spec = dict(
state=dict(required=True, choices=['present', 'absent']),
name=dict(required=True),
type=dict(choices=['iprange', 'fqdn', 'ipmask', 'geography'], default='ipmask'),
value=dict(),
start_ip=dict(),
end_ip=dict(),
country=dict(),
interface=dict(default='any'),
comment=dict(),
)
# merge argument_spec from module_utils/fortios.py
argument_spec.update(fortios_argument_spec)
# Load module
module = AnsibleModule(
argument_spec=argument_spec,
required_if=fortios_required_if,
supports_check_mode=True,
)
result = dict(changed=False)
if not HAS_NETADDR:
module.fail_json(msg='Could not import the python library netaddr required by this module')
# check params
if module.params['state'] == 'absent':
if module.params['type'] != "ipmask":
module.fail_json(msg='Invalid argument type=%s when state=absent' % module.params['type'])
if module.params['value'] is not None:
module.fail_json(msg='Invalid argument `value` when state=absent')
if module.params['start_ip'] is not None:
module.fail_json(msg='Invalid argument `start_ip` when state=absent')
if module.params['end_ip'] is not None:
module.fail_json(msg='Invalid argument `end_ip` when state=absent')
if module.params['country'] is not None:
module.fail_json(msg='Invalid argument `country` when state=absent')
if module.params['interface'] != "any":
module.fail_json(msg='Invalid argument `interface` when state=absent')
if module.params['comment'] is not None:
module.fail_json(msg='Invalid argument `comment` when state=absent')
else:
# state=present
# validate IP
if module.params['type'] == "ipmask":
formated_ip = get_formated_ipaddr(module.params['value'])
if formated_ip is not False:
module.params['value'] = get_formated_ipaddr(module.params['value'])
else:
module.fail_json(msg="Bad ip address format")
# validate country
if module.params['type'] == "geography":
if module.params['country'] not in FG_COUNTRY_LIST:
module.fail_json(msg="Invalid country argument, need to be in `diagnose firewall ipgeo country-list`")
# validate iprange
if module.params['type'] == "iprange":
if module.params['start_ip'] is None:
module.fail_json(msg="Missing argument 'start_ip' when type is iprange")
if module.params['end_ip'] is None:
module.fail_json(msg="Missing argument 'end_ip' when type is iprange")
# init forti object
fortigate = AnsibleFortios(module)
# Config path
config_path = 'firewall address'
# load config
fortigate.load_config(config_path)
# Absent State
if module.params['state'] == 'absent':
fortigate.candidate_config[config_path].del_block(module.params['name'])
# Present state
if module.params['state'] == 'present':
# define address params
new_addr = fortigate.get_empty_configuration_block(module.params['name'], 'edit')
if module.params['comment'] is not None:
new_addr.set_param('comment', '"%s"' % (module.params['comment']))
if module.params['type'] == 'iprange':
new_addr.set_param('type', 'iprange')
new_addr.set_param('start-ip', module.params['start_ip'])
new_addr.set_param('end-ip', module.params['end_ip'])
if module.params['type'] == 'geography':
new_addr.set_param('type', 'geography')
new_addr.set_param('country', '"%s"' % (module.params['country']))
if module.params['interface'] != 'any':
new_addr.set_param('associated-interface', '"%s"' % (module.params['interface']))
if module.params['value'] is not None:
if module.params['type'] == 'fqdn':
new_addr.set_param('type', 'fqdn')
new_addr.set_param('fqdn', '"%s"' % (module.params['value']))
if module.params['type'] == 'ipmask':
new_addr.set_param('subnet', module.params['value'])
# add the new address object to the device
fortigate.add_block(module.params['name'], new_addr)
# Apply changes (check mode is managed directly by the fortigate object)
fortigate.apply_changes()
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
posix/ci/group1

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
pyfg>=0.50
netaddr

View file

@ -0,0 +1,14 @@
---
- name: install required libraries
pip:
requirements: "{{ role_path }}/files/requirements.txt"
become: True
- name: copy backup config file to config file
copy:
src: "{{ role_path }}/files/default_config.conf.backup"
dest: "{{ role_path }}/files/default_config.conf"
- { include: test_indempotency.yml }
- { include: test_params_state_absent.yml }
- { include: test_params_state_present.yml }

View file

@ -0,0 +1,82 @@
---
- name: Add address
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
value: 192.30.253.113
state: present
register: add_addr
- name: Assert
assert:
that:
- "add_addr.changed == true"
- name: Add the same address
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
value: 192.30.253.113
state: present
register: add_addr
- name: Assert
assert:
that:
- "add_addr.changed == false"
- name: change value
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
value: 192.1.2.3
state: present
register: change_addr
- name: Assert
assert:
that:
- "change_addr.changed == true"
- name: change value second time
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
value: 192.1.2.3
state: present
register: change_addr
- name: Assert
assert:
that:
- "change_addr.changed == false"
- name: Delete existing address
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
state: absent
register: del_addr
- name: Assert
assert:
that:
- "del_addr.changed == true"
- name: Delete same existing address
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: github
state: absent
register: del_addr
- name: Assert
assert:
that:
- "del_addr.changed == false"

View file

@ -0,0 +1,91 @@
---
# Check made for absent state
- name: missing name
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: absent
register: missing_name
ignore_errors: True
- name: not wanted type fqdn
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
type: fqdn
register: unwanted_fqdn
ignore_errors: True
- name: not wanted type geography
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
type: geography
register: unwanted_geography
ignore_errors: True
- name: not wanted param start_ip
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
start_ip: 10.1.1.1
register: unwanted_start_ip
ignore_errors: True
- name: not wanted param end_ip
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
end_ip: 10.1.1.1
register: unwanted_end_ip
ignore_errors: True
- name: not wanted param country
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
country: FR
register: unwanted_country
ignore_errors: True
- name: not wanted param comment
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
comment: blabla
register: unwanted_comment
ignore_errors: True
- name: not wanted param value
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
name: some name
state: absent
value: blabla
register: unwanted_value
ignore_errors: True
- name: Verify that all previous test have failed
assert:
that:
- "missing_name.failed == True"
- "unwanted_fqdn.failed == True"
- "unwanted_geography.failed == True"
- "unwanted_start_ip.failed == True"
- "unwanted_end_ip.failed == True"
- "unwanted_country.failed == True"
- "unwanted_comment.failed == True"
- "unwanted_value.failed == True"

View file

@ -0,0 +1,86 @@
---
# Check made for present state
# type ipmask
- name: missing name
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
value: blabla
register: missing_name
ignore_errors: True
- name: missing value
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
register: missing_value
ignore_errors: True
- name: bad ip mask value
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
value: pwet
register: bad_ipmask
ignore_errors: True
# type geography
- name: missing country
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
type: geography
register: missing_country
ignore_errors: True
- name: bad country
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
type: geography
country: FRA
register: bad_country
ignore_errors: True
# type iprange
- name: missing start_ip
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
type: iprange
end_ip: 10.10.10.10
register: missing_sart_ip
ignore_errors: True
- name: missing end_ip
fortios_address:
file_mode: true
config_file: "{{role_path}}/files/default_config.conf"
state: present
name: blabla
type: iprange
start_ip: 10.10.10.10
register: missing_end_ip
ignore_errors: True
- name: Verify that all previous test have failed
assert:
that:
- "missing_name.failed == True"
- "missing_value.failed == True"
- "bad_ipmask.failed == True"
- "missing_country.failed == True"
- "bad_country.failed == True"
- "missing_sart_ip.failed == True"
- "missing_end_ip.failed == True"