1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9ANSIBLE_METADATA = {'metadata_version': '1.1', 10 'status': ['preview'], 11 'supported_by': 'certified'} 12 13DOCUMENTATION = r''' 14--- 15module: aci_filter_entry 16short_description: Manage filter entries (vz:Entry) 17description: 18- Manage filter entries for a filter on Cisco ACI fabrics. 19options: 20 arp_flag: 21 description: 22 - The arp flag to use when the ether_type is arp. 23 - The APIC defaults to C(unspecified) when unset during creation. 24 type: str 25 choices: [ arp_reply, arp_request, unspecified ] 26 description: 27 description: 28 - Description for the Filter Entry. 29 type: str 30 aliases: [ descr ] 31 dst_port: 32 description: 33 - Used to set both destination start and end ports to the same value when ip_protocol is tcp or udp. 34 - Accepted values are any valid TCP/UDP port range. 35 - The APIC defaults to C(unspecified) when unset during creation. 36 type: str 37 dst_port_end: 38 description: 39 - Used to set the destination end port when ip_protocol is tcp or udp. 40 - Accepted values are any valid TCP/UDP port range. 41 - The APIC defaults to C(unspecified) when unset during creation. 42 type: str 43 dst_port_start: 44 description: 45 - Used to set the destination start port when ip_protocol is tcp or udp. 46 - Accepted values are any valid TCP/UDP port range. 47 - The APIC defaults to C(unspecified) when unset during creation. 48 type: str 49 entry: 50 description: 51 - Then name of the Filter Entry. 52 type: str 53 aliases: [ entry_name, filter_entry, name ] 54 ether_type: 55 description: 56 - The Ethernet type. 57 - The APIC defaults to C(unspecified) when unset during creation. 58 type: str 59 choices: [ arp, fcoe, ip, ipv4, ipv6, mac_security, mpls_ucast, trill, unspecified ] 60 filter: 61 description: 62 - The name of Filter that the entry should belong to. 63 type: str 64 aliases: [ filter_name ] 65 icmp_msg_type: 66 description: 67 - ICMPv4 message type; used when ip_protocol is icmp. 68 - The APIC defaults to C(unspecified) when unset during creation. 69 type: str 70 choices: [ dst_unreachable, echo, echo_reply, src_quench, time_exceeded, unspecified ] 71 icmp6_msg_type: 72 description: 73 - ICMPv6 message type; used when ip_protocol is icmpv6. 74 - The APIC defaults to C(unspecified) when unset during creation. 75 type: str 76 choices: [ dst_unreachable, echo_request, echo_reply, neighbor_advertisement, neighbor_solicitation, redirect, time_exceeded, unspecified ] 77 ip_protocol: 78 description: 79 - The IP Protocol type when ether_type is ip. 80 - The APIC defaults to C(unspecified) when unset during creation. 81 type: str 82 choices: [ eigrp, egp, icmp, icmpv6, igmp, igp, l2tp, ospfigp, pim, tcp, udp, unspecified ] 83 state: 84 description: 85 - present, absent, query 86 type: str 87 default: present 88 choices: [ absent, present, query ] 89 name_alias: 90 description: 91 - The alias for the current object. This relates to the nameAlias field in ACI. 92 type: str 93 stateful: 94 description: 95 - Determines the statefulness of the filter entry. 96 type: bool 97 tenant: 98 description: 99 - The name of the tenant. 100 type: str 101 aliases: [ tenant_name ] 102extends_documentation_fragment: 103- cisco.aci.aci 104 105notes: 106- The C(tenant) and C(filter) used must exist before using this module in your playbook. 107 The M(cisco.aci.aci_tenant) and M(cisco.aci.aci_filter) modules can be used for this. 108seealso: 109- module: cisco.aci.aci_tenant 110- module: cisco.aci.aci_filter 111- name: APIC Management Information Model reference 112 description: More information about the internal APIC class B(vz:Entry). 113 link: https://developer.cisco.com/docs/apic-mim-ref/ 114author: 115- Jacob McGill (@jmcgill298) 116''' 117 118# FIXME: Add more, better examples 119EXAMPLES = r''' 120- cisco.aci.aci_filter_entry: 121 host: "{{ inventory_hostname }}" 122 username: "{{ user }}" 123 password: "{{ pass }}" 124 state: "{{ state }}" 125 entry: "{{ entry }}" 126 tenant: "{{ tenant }}" 127 ether_name: "{{ ether_name }}" 128 icmp_msg_type: "{{ icmp_msg_type }}" 129 filter: "{{ filter }}" 130 descr: "{{ descr }}" 131 delegate_to: localhost 132''' 133 134RETURN = r''' 135current: 136 description: The existing configuration from the APIC after the module has finished 137 returned: success 138 type: list 139 sample: 140 [ 141 { 142 "fvTenant": { 143 "attributes": { 144 "descr": "Production environment", 145 "dn": "uni/tn-production", 146 "name": "production", 147 "nameAlias": "", 148 "ownerKey": "", 149 "ownerTag": "" 150 } 151 } 152 } 153 ] 154error: 155 description: The error information as returned from the APIC 156 returned: failure 157 type: dict 158 sample: 159 { 160 "code": "122", 161 "text": "unknown managed object class foo" 162 } 163raw: 164 description: The raw output returned by the APIC REST API (xml or json) 165 returned: parse error 166 type: str 167 sample: '<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1"><error code="122" text="unknown managed object class foo"/></imdata>' 168sent: 169 description: The actual/minimal configuration pushed to the APIC 170 returned: info 171 type: list 172 sample: 173 { 174 "fvTenant": { 175 "attributes": { 176 "descr": "Production environment" 177 } 178 } 179 } 180previous: 181 description: The original configuration from the APIC before the module has started 182 returned: info 183 type: list 184 sample: 185 [ 186 { 187 "fvTenant": { 188 "attributes": { 189 "descr": "Production", 190 "dn": "uni/tn-production", 191 "name": "production", 192 "nameAlias": "", 193 "ownerKey": "", 194 "ownerTag": "" 195 } 196 } 197 } 198 ] 199proposed: 200 description: The assembled configuration from the user-provided parameters 201 returned: info 202 type: dict 203 sample: 204 { 205 "fvTenant": { 206 "attributes": { 207 "descr": "Production environment", 208 "name": "production" 209 } 210 } 211 } 212filter_string: 213 description: The filter string used for the request 214 returned: failure or debug 215 type: str 216 sample: ?rsp-prop-include=config-only 217method: 218 description: The HTTP method used for the request to the APIC 219 returned: failure or debug 220 type: str 221 sample: POST 222response: 223 description: The HTTP response from the APIC 224 returned: failure or debug 225 type: str 226 sample: OK (30 bytes) 227status: 228 description: The HTTP status from the APIC 229 returned: failure or debug 230 type: int 231 sample: 200 232url: 233 description: The HTTP url used for the request to the APIC 234 returned: failure or debug 235 type: str 236 sample: https://10.11.12.13/api/mo/uni/tn-production.json 237''' 238 239from ansible.module_utils.basic import AnsibleModule 240from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec 241 242VALID_ARP_FLAGS = ['arp_reply', 'arp_request', 'unspecified'] 243VALID_ETHER_TYPES = ['arp', 'fcoe', 'ip', 'ipv4', 'ipv6', 'mac_security', 'mpls_ucast', 'trill', 'unspecified'] 244VALID_ICMP_TYPES = ['dst_unreachable', 'echo', 'echo_reply', 'src_quench', 'time_exceeded', 'unspecified'] 245VALID_ICMP6_TYPES = ['dst_unreachable', 'echo_request', 'echo_reply', 'neighbor_advertisement', 246 'neighbor_solicitation', 'redirect', 'time_exceeded', 'unspecified'] 247VALID_IP_PROTOCOLS = ['eigrp', 'egp', 'icmp', 'icmpv6', 'igmp', 'igp', 'l2tp', 'ospfigp', 'pim', 'tcp', 'udp', 'unspecified'] 248 249# mapping dicts are used to normalize the proposed data to what the APIC expects, which will keep diffs accurate 250ARP_FLAG_MAPPING = dict(arp_reply='reply', arp_request='req', unspecified=None) 251FILTER_PORT_MAPPING = {'443': 'https', '25': 'smtp', '80': 'http', '20': 'ftpData', '53': 'dns', '110': 'pop3', '554': 'rtsp'} 252ICMP_MAPPING = {'dst_unreachable': 'dst-unreach', 'echo': 'echo', 'echo_reply': 'echo-rep', 'src_quench': 'src-quench', 253 'time_exceeded': 'time-exceeded', 'unspecified': 'unspecified', 'echo-rep': 'echo-rep', 'dst-unreach': 'dst-unreach'} 254ICMP6_MAPPING = dict(dst_unreachable='dst-unreach', echo_request='echo-req', echo_reply='echo-rep', neighbor_advertisement='nbr-advert', 255 neighbor_solicitation='nbr-solicit', redirect='redirect', time_exceeded='time-exceeded', unspecified='unspecified') 256 257 258def main(): 259 argument_spec = aci_argument_spec() 260 argument_spec.update( 261 arp_flag=dict(type='str', choices=VALID_ARP_FLAGS), 262 description=dict(type='str', aliases=['descr']), 263 dst_port=dict(type='str'), 264 dst_port_end=dict(type='str'), 265 dst_port_start=dict(type='str'), 266 entry=dict(type='str', aliases=['entry_name', 'filter_entry', 'name']), # Not required for querying all objects 267 ether_type=dict(choices=VALID_ETHER_TYPES, type='str'), 268 filter=dict(type='str', aliases=['filter_name']), # Not required for querying all objects 269 icmp_msg_type=dict(type='str', choices=VALID_ICMP_TYPES), 270 icmp6_msg_type=dict(type='str', choices=VALID_ICMP6_TYPES), 271 ip_protocol=dict(choices=VALID_IP_PROTOCOLS, type='str'), 272 state=dict(type='str', default='present', choices=['absent', 'present', 'query']), 273 stateful=dict(type='bool'), 274 tenant=dict(type='str', aliases=['tenant_name']), # Not required for querying all objects 275 name_alias=dict(type='str'), 276 ) 277 278 module = AnsibleModule( 279 argument_spec=argument_spec, 280 supports_check_mode=True, 281 required_if=[ 282 ['state', 'absent', ['entry', 'filter', 'tenant']], 283 ['state', 'present', ['entry', 'filter', 'tenant']], 284 ], 285 ) 286 287 aci = ACIModule(module) 288 289 arp_flag = module.params.get('arp_flag') 290 if arp_flag is not None: 291 arp_flag = ARP_FLAG_MAPPING.get(arp_flag) 292 description = module.params.get('description') 293 dst_port = module.params.get('dst_port') 294 if FILTER_PORT_MAPPING.get(dst_port) is not None: 295 dst_port = FILTER_PORT_MAPPING.get(dst_port) 296 dst_end = module.params.get('dst_port_end') 297 if FILTER_PORT_MAPPING.get(dst_end) is not None: 298 dst_end = FILTER_PORT_MAPPING.get(dst_end) 299 dst_start = module.params.get('dst_port_start') 300 if FILTER_PORT_MAPPING.get(dst_start) is not None: 301 dst_start = FILTER_PORT_MAPPING.get(dst_start) 302 entry = module.params.get('entry') 303 ether_type = module.params.get('ether_type') 304 filter_name = module.params.get('filter') 305 icmp_msg_type = module.params.get('icmp_msg_type') 306 if icmp_msg_type is not None: 307 icmp_msg_type = ICMP_MAPPING.get(icmp_msg_type) 308 icmp6_msg_type = module.params.get('icmp6_msg_type') 309 if icmp6_msg_type is not None: 310 icmp6_msg_type = ICMP6_MAPPING.get(icmp6_msg_type) 311 ip_protocol = module.params.get('ip_protocol') 312 state = module.params.get('state') 313 stateful = aci.boolean(module.params.get('stateful')) 314 tenant = module.params.get('tenant') 315 name_alias = module.params.get('name_alias') 316 317 # validate that dst_port is not passed with dst_start or dst_end 318 if dst_port is not None and (dst_end is not None or dst_start is not None): 319 module.fail_json(msg="Parameter 'dst_port' cannot be used with 'dst_end' and 'dst_start'") 320 elif dst_port is not None: 321 dst_end = dst_port 322 dst_start = dst_port 323 324 aci.construct_url( 325 root_class=dict( 326 aci_class='fvTenant', 327 aci_rn='tn-{0}'.format(tenant), 328 module_object=tenant, 329 target_filter={'name': tenant}, 330 ), 331 subclass_1=dict( 332 aci_class='vzFilter', 333 aci_rn='flt-{0}'.format(filter_name), 334 module_object=filter_name, 335 target_filter={'name': filter_name}, 336 ), 337 subclass_2=dict( 338 aci_class='vzEntry', 339 aci_rn='e-{0}'.format(entry), 340 module_object=entry, 341 target_filter={'name': entry}, 342 ), 343 ) 344 345 aci.get_existing() 346 347 if state == 'present': 348 aci.payload( 349 aci_class='vzEntry', 350 class_config=dict( 351 arpOpc=arp_flag, 352 descr=description, 353 dFromPort=dst_start, 354 dToPort=dst_end, 355 etherT=ether_type, 356 icmpv4T=icmp_msg_type, 357 icmpv6T=icmp6_msg_type, 358 name=entry, 359 prot=ip_protocol, 360 stateful=stateful, 361 nameAlias=name_alias, 362 ), 363 ) 364 365 aci.get_diff(aci_class='vzEntry') 366 367 aci.post_config() 368 369 elif state == 'absent': 370 aci.delete_config() 371 372 aci.exit_json() 373 374 375if __name__ == "__main__": 376 main() 377