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