1#!/usr/bin/python
2#
3# Ansible module to manage IP addresses on fortios devices
4# (c) 2016, Benjamin Jolivot <bjolivot@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15DOCUMENTATION = """
16---
17module: fortios_address
18version_added: "2.4"
19author: "Benjamin Jolivot (@bjolivot)"
20short_description: Manage fortios firewall address objects
21description:
22  - This module provide management of firewall addresses on FortiOS devices.
23extends_documentation_fragment: fortios
24options:
25  state:
26    description:
27      - Specifies if address need to be added or deleted.
28    required: true
29    choices: ['present', 'absent']
30  name:
31    description:
32      - Name of the address to add or delete.
33    required: true
34  type:
35    description:
36      - Type of the address.
37    choices: ['iprange', 'fqdn', 'ipmask', 'geography']
38  value:
39    description:
40      - Address value, based on type.
41        If type=fqdn, something like www.google.com.
42        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).
43  start_ip:
44    description:
45      - First ip in range (used only with type=iprange).
46  end_ip:
47    description:
48      - Last ip in range (used only with type=iprange).
49  country:
50    description:
51      - 2 letter country code (like FR).
52  interface:
53    description:
54      - interface name the address apply to.
55    default: any
56  comment:
57    description:
58      - free text to describe address.
59notes:
60  - This module requires netaddr python library.
61"""
62
63EXAMPLES = """
64- name: Register french addresses
65  fortios_address:
66    host: 192.168.0.254
67    username: admin
68    password: p4ssw0rd
69    state: present
70    name: "fromfrance"
71    type: geography
72    country: FR
73    comment: "French geoip address"
74
75- name: Register some fqdn
76  fortios_address:
77    host: 192.168.0.254
78    username: admin
79    password: p4ssw0rd
80    state: present
81    name: "Ansible"
82    type: fqdn
83    value: www.ansible.com
84    comment: "Ansible website"
85
86- name: Register google DNS
87  fortios_address:
88    host: 192.168.0.254
89    username: admin
90    password: p4ssw0rd
91    state: present
92    name: "google_dns"
93    type: ipmask
94    value: 8.8.8.8
95
96"""
97
98RETURN = """
99firewall_address_config:
100  description: full firewall addresses config string.
101  returned: always
102  type: str
103change_string:
104  description: The commands executed by the module.
105  returned: only if config changed
106  type: str
107"""
108
109from ansible.module_utils.network.fortios.fortios import fortios_argument_spec, fortios_required_if
110from ansible.module_utils.network.fortios.fortios import backup, AnsibleFortios
111
112from ansible.module_utils.basic import AnsibleModule
113
114
115# check for netaddr lib
116try:
117    from netaddr import IPNetwork
118    HAS_NETADDR = True
119except Exception:
120    HAS_NETADDR = False
121
122
123# define valid country list for GEOIP address type
124FG_COUNTRY_LIST = (
125    'ZZ', 'A1', 'A2', 'O1', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO',
126    'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE',
127    'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT',
128    'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL',
129    'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK',
130    'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'EU', 'FI', 'FJ',
131    'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL',
132    'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN',
133    'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT',
134    'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW',
135    'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY',
136    'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP',
137    'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE',
138    'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF',
139    'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE',
140    'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ',
141    'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC',
142    'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV',
143    'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI',
144    'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW'
145)
146
147
148def get_formated_ipaddr(input_ip):
149    """
150    Format given ip address string to fortigate format (ip netmask)
151    Args:
152        * **ip_str** (string) : string representing ip address
153        accepted format:
154          - ip netmask  (ex: 192.168.0.10 255.255.255.0)
155          - ip (ex: 192.168.0.10)
156          - CIDR (ex: 192.168.0.10/24)
157
158    Returns:
159        formated ip if ip is valid (ex: "192.168.0.10 255.255.255.0")
160        False if ip is not valid
161    """
162    try:
163        if " " in input_ip:
164            # ip netmask format
165            str_ip, str_netmask = input_ip.split(" ")
166            ip = IPNetwork(str_ip)
167            mask = IPNetwork(str_netmask)
168            return "%s %s" % (str_ip, str_netmask)
169        else:
170            ip = IPNetwork(input_ip)
171            return "%s %s" % (str(ip.ip), str(ip.netmask))
172    except Exception:
173        return False
174
175    return False
176
177
178def main():
179    argument_spec = dict(
180        state=dict(required=True, choices=['present', 'absent']),
181        name=dict(required=True),
182        type=dict(choices=['iprange', 'fqdn', 'ipmask', 'geography'], default='ipmask'),
183        value=dict(),
184        start_ip=dict(),
185        end_ip=dict(),
186        country=dict(),
187        interface=dict(default='any'),
188        comment=dict(),
189    )
190
191    # merge argument_spec from module_utils/fortios.py
192    argument_spec.update(fortios_argument_spec)
193
194    # Load module
195    module = AnsibleModule(
196        argument_spec=argument_spec,
197        required_if=fortios_required_if,
198        supports_check_mode=True,
199    )
200    result = dict(changed=False)
201
202    if not HAS_NETADDR:
203        module.fail_json(msg='Could not import the python library netaddr required by this module')
204
205    # check params
206    if module.params['state'] == 'absent':
207        if module.params['type'] != "ipmask":
208            module.fail_json(msg='Invalid argument type=%s when state=absent' % module.params['type'])
209        if module.params['value'] is not None:
210            module.fail_json(msg='Invalid argument `value` when state=absent')
211        if module.params['start_ip'] is not None:
212            module.fail_json(msg='Invalid argument `start_ip` when state=absent')
213        if module.params['end_ip'] is not None:
214            module.fail_json(msg='Invalid argument `end_ip` when state=absent')
215        if module.params['country'] is not None:
216            module.fail_json(msg='Invalid argument `country` when state=absent')
217        if module.params['interface'] != "any":
218            module.fail_json(msg='Invalid argument `interface` when state=absent')
219        if module.params['comment'] is not None:
220            module.fail_json(msg='Invalid argument `comment` when state=absent')
221    else:
222        # state=present
223        # validate IP
224        if module.params['type'] == "ipmask":
225            formated_ip = get_formated_ipaddr(module.params['value'])
226            if formated_ip is not False:
227                module.params['value'] = get_formated_ipaddr(module.params['value'])
228            else:
229                module.fail_json(msg="Bad ip address format")
230
231        # validate country
232        if module.params['type'] == "geography":
233            if module.params['country'] not in FG_COUNTRY_LIST:
234                module.fail_json(msg="Invalid country argument, need to be in `diagnose firewall ipgeo country-list`")
235
236        # validate iprange
237        if module.params['type'] == "iprange":
238            if module.params['start_ip'] is None:
239                module.fail_json(msg="Missing argument 'start_ip' when type is iprange")
240            if module.params['end_ip'] is None:
241                module.fail_json(msg="Missing argument 'end_ip' when type is iprange")
242
243    # init forti object
244    fortigate = AnsibleFortios(module)
245
246    # Config path
247    config_path = 'firewall address'
248
249    # load config
250    fortigate.load_config(config_path)
251
252    # Absent State
253    if module.params['state'] == 'absent':
254        fortigate.candidate_config[config_path].del_block(module.params['name'])
255
256    # Present state
257    if module.params['state'] == 'present':
258        # define address params
259        new_addr = fortigate.get_empty_configuration_block(module.params['name'], 'edit')
260
261        if module.params['comment'] is not None:
262            new_addr.set_param('comment', '"%s"' % (module.params['comment']))
263
264        if module.params['type'] == 'iprange':
265            new_addr.set_param('type', 'iprange')
266            new_addr.set_param('start-ip', module.params['start_ip'])
267            new_addr.set_param('end-ip', module.params['end_ip'])
268
269        if module.params['type'] == 'geography':
270            new_addr.set_param('type', 'geography')
271            new_addr.set_param('country', '"%s"' % (module.params['country']))
272
273        if module.params['interface'] != 'any':
274            new_addr.set_param('associated-interface', '"%s"' % (module.params['interface']))
275
276        if module.params['value'] is not None:
277            if module.params['type'] == 'fqdn':
278                new_addr.set_param('type', 'fqdn')
279                new_addr.set_param('fqdn', '"%s"' % (module.params['value']))
280            if module.params['type'] == 'ipmask':
281                new_addr.set_param('subnet', module.params['value'])
282
283        # add the new address object to the device
284        fortigate.add_block(module.params['name'], new_addr)
285
286    # Apply changes (check mode is managed directly by the fortigate object)
287    fortigate.apply_changes()
288
289
290if __name__ == '__main__':
291    main()
292