1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2018, Kevin Breit (@kbreit) <kevin.breit@kevinbreit.net>
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
10ANSIBLE_METADATA = {
11    'metadata_version': '1.1',
12    'status': ['preview'],
13    'supported_by': 'community'
14}
15
16DOCUMENTATION = r'''
17---
18module: meraki_mx_site_to_site_firewall
19short_description: Manage MX appliance firewall rules for site-to-site VPNs
20version_added: "1.0.0"
21description:
22- Allows for creation, management, and visibility into firewall rules for site-to-site VPNs implemented on Meraki MX firewalls.
23notes:
24- Module assumes a complete list of firewall rules are passed as a parameter.
25options:
26    state:
27        description:
28        - Create or modify an organization.
29        choices: ['present', 'query']
30        default: present
31        type: str
32    rules:
33        description:
34        - List of firewall rules.
35        type: list
36        elements: dict
37        suboptions:
38            policy:
39                description:
40                - Policy to apply if rule is hit.
41                choices: [allow, deny]
42                type: str
43            protocol:
44                description:
45                - Protocol to match against.
46                choices: [any, icmp, tcp, udp]
47                type: str
48            dest_port:
49                description:
50                - Comma separated list of destination port numbers to match against.
51                - C(Any) must be capitalized.
52                type: str
53            dest_cidr:
54                description:
55                - Comma separated list of CIDR notation destination networks.
56                - C(Any) must be capitalized.
57                type: str
58            src_port:
59                description:
60                - Comma separated list of source port numbers to match against.
61                - C(Any) must be capitalized.
62                type: str
63            src_cidr:
64                description:
65                - Comma separated list of CIDR notation source networks.
66                - C(Any) must be capitalized.
67                type: str
68            comment:
69                description:
70                - Optional comment to describe the firewall rule.
71                type: str
72            syslog_enabled:
73                description:
74                - Whether to log hints against the firewall rule.
75                - Only applicable if a syslog server is specified against the network.
76                type: bool
77                default: False
78    syslog_default_rule:
79        description:
80        - Whether to log hits against the default firewall rule.
81        - Only applicable if a syslog server is specified against the network.
82        - This is not shown in response from Meraki. Instead, refer to the C(syslog_enabled) value in the default rule.
83        type: bool
84author:
85- Kevin Breit (@kbreit)
86extends_documentation_fragment: cisco.meraki.meraki
87'''
88
89EXAMPLES = r'''
90- name: Query firewall rules
91  meraki_mx_site_to_site_firewall:
92    auth_key: abc123
93    org_name: YourOrg
94    state: query
95  delegate_to: localhost
96
97- name: Set two firewall rules
98  meraki_mx_site_to_site_firewall:
99    auth_key: abc123
100    org_name: YourOrg
101    state: present
102    rules:
103      - comment: Block traffic to server
104        src_cidr: 192.0.1.0/24
105        src_port: any
106        dest_cidr: 192.0.2.2/32
107        dest_port: any
108        protocol: any
109        policy: deny
110      - comment: Allow traffic to group of servers
111        src_cidr: 192.0.1.0/24
112        src_port: any
113        dest_cidr: 192.0.2.0/24
114        dest_port: any
115        protocol: any
116        policy: permit
117  delegate_to: localhost
118
119- name: Set one firewall rule and enable logging of the default rule
120  meraki_mx_site_to_site_firewall:
121    auth_key: abc123
122    org_name: YourOrg
123    state: present
124    rules:
125      - comment: Block traffic to server
126        src_cidr: 192.0.1.0/24
127        src_port: any
128        dest_cidr: 192.0.2.2/32
129        dest_port: any
130        protocol: any
131        policy: deny
132    syslog_default_rule: yes
133  delegate_to: localhost
134'''
135
136RETURN = r'''
137data:
138    description: Firewall rules associated to network.
139    returned: success
140    type: complex
141    contains:
142        rules:
143            description: List of firewall rules associated to network.
144            returned: success
145            type: complex
146            contains:
147                comment:
148                    description: Comment to describe the firewall rule.
149                    returned: always
150                    type: str
151                    sample: Block traffic to server
152                src_cidr:
153                    description: Comma separated list of CIDR notation source networks.
154                    returned: always
155                    type: str
156                    sample: 192.0.1.1/32,192.0.1.2/32
157                src_port:
158                    description: Comma separated list of source ports.
159                    returned: always
160                    type: str
161                    sample: 80,443
162                dest_cidr:
163                    description: Comma separated list of CIDR notation destination networks.
164                    returned: always
165                    type: str
166                    sample: 192.0.1.1/32,192.0.1.2/32
167                dest_port:
168                    description: Comma separated list of destination ports.
169                    returned: always
170                    type: str
171                    sample: 80,443
172                protocol:
173                    description: Network protocol for which to match against.
174                    returned: always
175                    type: str
176                    sample: tcp
177                policy:
178                    description: Action to take when rule is matched.
179                    returned: always
180                    type: str
181                syslog_enabled:
182                    description: Whether to log to syslog when rule is matched.
183                    returned: always
184                    type: bool
185                    sample: true
186'''
187
188from ansible.module_utils.basic import AnsibleModule, json
189from ansible_collections.cisco.meraki.plugins.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec
190
191
192def assemble_payload(meraki):
193    params_map = {'policy': 'policy',
194                  'protocol': 'protocol',
195                  'dest_port': 'destPort',
196                  'dest_cidr': 'destCidr',
197                  'src_port': 'srcPort',
198                  'src_cidr': 'srcCidr',
199                  'syslog_enabled': 'syslogEnabled',
200                  'comment': 'comment',
201                  }
202    rules = []
203    for rule in meraki.params['rules']:
204        proposed_rule = dict()
205        for k, v in rule.items():
206            proposed_rule[params_map[k]] = v
207        rules.append(proposed_rule)
208    payload = {'rules': rules}
209    return payload
210
211
212def get_rules(meraki, org_id):
213    path = meraki.construct_path('get_all', org_id=org_id)
214    response = meraki.request(path, method='GET')
215    if meraki.status == 200:
216        return response
217
218
219def main():
220    # define the available arguments/parameters that a user can pass to
221    # the module
222
223    fw_rules = dict(policy=dict(type='str', choices=['allow', 'deny']),
224                    protocol=dict(type='str', choices=['tcp', 'udp', 'icmp', 'any']),
225                    dest_port=dict(type='str'),
226                    dest_cidr=dict(type='str'),
227                    src_port=dict(type='str'),
228                    src_cidr=dict(type='str'),
229                    comment=dict(type='str'),
230                    syslog_enabled=dict(type='bool', default=False),
231                    )
232
233    argument_spec = meraki_argument_spec()
234    argument_spec.update(state=dict(type='str', choices=['present', 'query'], default='present'),
235                         rules=dict(type='list', default=None, elements='dict', options=fw_rules),
236                         syslog_default_rule=dict(type='bool'),
237                         )
238
239    # the AnsibleModule object will be our abstraction working with Ansible
240    # this includes instantiation, a couple of common attr would be the
241    # args/params passed to the execution, as well as if the module
242    # supports check mode
243    module = AnsibleModule(argument_spec=argument_spec,
244                           supports_check_mode=True,
245                           )
246    meraki = MerakiModule(module, function='mx_site_to_site_firewall')
247
248    meraki.params['follow_redirects'] = 'all'
249
250    query_urls = {'mx_site_to_site_firewall': '/organizations/{org_id}/appliance/vpn/vpnFirewallRules/'}
251    update_urls = {'mx_site_to_site_firewall': '/organizations/{org_id}/appliance/vpn/vpnFirewallRules/'}
252
253    meraki.url_catalog['get_all'].update(query_urls)
254    meraki.url_catalog['update'] = update_urls
255
256    payload = None
257
258    # execute checks for argument completeness
259
260    # manipulate or modify the state as needed (this is going to be the
261    # part where your module will do what it needs to do)
262    org_id = meraki.params['org_id']
263    orgs = None
264    if org_id is None:
265        orgs = meraki.get_orgs()
266        for org in orgs:
267            if org['name'] == meraki.params['org_name']:
268                org_id = org['id']
269
270    if meraki.params['state'] == 'query':
271        meraki.result['data'] = get_rules(meraki, org_id)
272    elif meraki.params['state'] == 'present':
273        rules = get_rules(meraki, org_id)
274        path = meraki.construct_path('get_all', org_id=org_id)
275        if meraki.params['rules'] is not None:
276            payload = assemble_payload(meraki)
277        else:
278            payload = dict()
279        update = False
280        if meraki.params['syslog_default_rule'] is not None:
281            payload['syslogDefaultRule'] = meraki.params['syslog_default_rule']
282        try:
283            if meraki.params['rules'] is not None:
284                if len(rules['rules']) - 1 != len(payload['rules']):  # Quick and simple check to avoid more processing
285                    update = True
286            if meraki.params['syslog_default_rule'] is not None:
287                if rules['rules'][len(rules['rules']) - 1]['syslogEnabled'] != meraki.params['syslog_default_rule']:
288                    update = True
289            if update is False:
290                default_rule = rules['rules'][len(rules['rules']) - 1].copy()
291                # meraki.fail_json(msg=update)
292                del rules['rules'][len(rules['rules']) - 1]  # Remove default rule for comparison
293                if len(rules['rules']) - 1 == 0:
294                    if meraki.is_update_required(rules['rules'][0], payload['rules'][0]) is True:
295                        update = True
296                else:
297                    for r in range(len(rules) - 1):
298                        if meraki.is_update_required(rules['rules'][r], payload['rules'][r]) is True:
299                            update = True
300                rules['rules'].append(default_rule)
301        except KeyError:
302            pass
303        if update is True:
304            if meraki.check_mode is True:
305                if meraki.params['rules'] is not None:
306                    data = payload
307                    data['rules'].append(rules['rules'][len(rules['rules']) - 1])  # Append the default rule
308                    if meraki.params['syslog_default_rule'] is not None:
309                        data['rules'][len(payload['rules']) - 1]['syslog_enabled'] = meraki.params['syslog_default_rule']
310                else:
311                    if meraki.params['syslog_default_rule'] is not None:
312                        data = rules
313                        data['rules'][len(data['rules']) - 1]['syslogEnabled'] = meraki.params['syslog_default_rule']
314                meraki.result['data'] = data
315                meraki.result['changed'] = True
316                meraki.exit_json(**meraki.result)
317            response = meraki.request(path, method='PUT', payload=json.dumps(payload))
318            if meraki.status == 200:
319                meraki.result['data'] = response
320                meraki.result['changed'] = True
321        else:
322            meraki.result['data'] = rules
323
324    # in the event of a successful module execution, you will want to
325    # simple AnsibleModule.exit_json(), passing the key/value results
326    meraki.exit_json(**meraki.result)
327
328
329if __name__ == '__main__':
330    main()
331