diff --git a/lib/ansible/module_utils/network/iosxr/argspec/acls/__init__.py b/lib/ansible/module_utils/network/iosxr/argspec/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/argspec/acls/acls.py b/lib/ansible/module_utils/network/iosxr/argspec/acls/acls.py new file mode 100644 index 00000000000..2a2fa6b2610 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/argspec/acls/acls.py @@ -0,0 +1,644 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The arg spec for the iosxr_acls module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class AclsArgs(object): # pylint: disable=R0903 + """The arg spec for the iosxr_acls module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'running_config': { + 'type': 'str' + }, + 'config': { + 'elements': 'dict', + 'options': { + 'acls': { + 'elements': 'dict', + 'options': { + 'name': { + 'type': 'str' + }, + 'aces': { + 'elements': 'dict', + 'mutually_exclusive': [['grant', 'remark', 'line']], + 'options': { + 'destination': { + 'mutually_exclusive': [['address', 'any', 'host', 'prefix'], ['wildcard_bits', 'any', 'host', 'prefix']], + 'options': { + 'host': { + 'type': 'str' + }, + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'prefix': { + 'type': 'str' + }, + 'port_protocol': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']], + 'options': { + 'eq': { + 'type': 'str' + }, + 'gt': { + 'type': 'str' + }, + 'lt': { + 'type': 'str' + }, + 'neq': { + 'type': 'str' + }, + 'range': { + 'options': { + 'end': { + 'type': 'str' + }, + 'start': { + 'type': 'str' + } + }, + 'required_together': [['start', 'end']], + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': [['address', 'wildcard_bits']], + 'type': 'dict' + }, + 'dscp': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']], + 'type': 'dict', + 'options': { + 'eq': { + 'type': 'str' + }, + 'gt': { + 'type': 'str' + }, + 'lt': { + 'type': 'str' + }, + 'neq': { + 'type': 'str' + }, + 'range': { + 'options': { + 'end': { + 'type': 'str' + }, + 'start': { + 'type': 'str' + } + }, + 'required_together': [['start', 'end']], + 'type': 'dict' + } + }, + }, + 'fragments': { + 'type': 'bool' + }, + 'capture': { + 'type': 'bool' + }, + 'destopts': { + 'type': 'bool' + }, + 'authen': { + 'type': 'bool' + }, + 'routing': { + 'type': 'bool' + }, + 'hop_by_hop': { + 'type': 'bool' + }, + 'grant': { + 'type': 'str', + 'choices': ['permit', 'deny'], + }, + 'icmp_off': { + 'type': 'bool' + }, + 'log': { + 'type': 'bool' + }, + 'log_input': { + 'type': 'bool' + }, + 'line': { + 'type': 'str', + 'aliases': ['ace'] + }, + 'packet_length': { + 'mutually_exclusive': [['eq', 'lt', 'neq', 'range'], ['eq', 'gt', 'neq', 'range']], + 'options': { + 'eq': { + 'type': 'int' + }, + 'gt': { + 'type': 'int' + }, + 'lt': { + 'type': 'int' + }, + 'neq': { + 'type': 'int' + }, + 'range': { + 'options': { + 'end': { + 'type': 'int' + }, + 'start': { + 'type': 'int' + } + }, + 'type': 'dict' + } + }, + 'type': + 'dict' + }, + 'precedence': { + 'type': 'str' + }, + 'protocol': { + 'type': 'str' + }, + 'protocol_options': { + 'mutually_exclusive': [['icmp', 'tcp', 'igmp', 'icmpv6']], + 'options': { + 'icmpv6': { + 'type': 'dict', + 'options': { + 'address_unreachable': { + 'type': 'bool' + }, + 'administratively_prohibited': + { + 'type': 'bool' + }, + 'beyond_scope_of_source_address': + { + 'type': 'bool' + }, + 'destination_unreachable': { + 'type': 'bool' + }, + 'echo': { + 'type': 'bool' + }, + 'echo_reply': { + 'type': 'bool' + }, + 'erroneous_header_field': { + 'type': 'bool' + }, + 'group_membership_query': { + 'type': 'bool' + }, + 'group_membership_report': { + 'type': 'bool' + }, + 'group_membership_termination': + { + 'type': 'bool' + }, + 'host_unreachable': { + 'type': 'bool' + }, + 'nd_na': { + 'type': 'bool' + }, + 'nd_ns': { + 'type': 'bool' + }, + 'neighbor_redirect': { + 'type': 'bool' + }, + 'no_route_to_destination': { + 'type': 'bool' + }, + 'node_information_request_is_refused': + { + 'type': 'bool' + }, + 'node_information_successful_reply': + { + 'type': 'bool' + }, + 'packet_too_big': { + 'type': 'bool' + }, + 'parameter_problem': { + 'type': 'bool' + }, + 'port_unreachable': { + 'type': 'bool' + }, + 'query_subject_is_IPv4address': + { + 'type': 'bool' + }, + 'query_subject_is_IPv6address': + { + 'type': 'bool' + }, + 'query_subject_is_domainname': { + 'type': 'bool' + }, + 'reassembly_timeout': { + 'type': 'bool' + }, + 'redirect': { + 'type': 'bool' + }, + 'router_advertisement': { + 'type': 'bool' + }, + 'router_renumbering': { + 'type': 'bool' + }, + 'router_solicitation': { + 'type': 'bool' + }, + 'rr_command': { + 'type': 'bool' + }, + 'rr_result': { + 'type': 'bool' + }, + 'rr_seqnum_reset': { + 'type': 'bool' + }, + 'time_exceeded': { + 'type': 'bool' + }, + 'ttl_exceeded': { + 'type': 'bool' + }, + 'unknown_query_type': { + 'type': 'bool' + }, + 'unreachable': { + 'type': 'bool' + }, + 'unrecognized_next_header': { + 'type': 'bool' + }, + 'unrecognized_option': { + 'type': 'bool' + }, + 'whoareyou_reply': { + 'type': 'bool' + }, + 'whoareyou_request': { + 'type': 'bool' + } + } + }, + 'icmp': { + 'options': { + 'administratively_prohibited': + { + 'type': 'bool' + }, + 'alternate_address': { + 'type': 'bool' + }, + 'conversion_error': { + 'type': 'bool' + }, + 'dod_host_prohibited': { + 'type': 'bool' + }, + 'dod_net_prohibited': { + 'type': 'bool' + }, + 'echo': { + 'type': 'bool' + }, + 'echo_reply': { + 'type': 'bool' + }, + 'general_parameter_problem': { + 'type': 'bool' + }, + 'host_isolated': { + 'type': 'bool' + }, + 'host_precedence_unreachable': + { + 'type': 'bool' + }, + 'host_redirect': { + 'type': 'bool' + }, + 'host_tos_redirect': { + 'type': 'bool' + }, + 'host_tos_unreachable': { + 'type': 'bool' + }, + 'host_unknown': { + 'type': 'bool' + }, + 'host_unreachable': { + 'type': 'bool' + }, + 'information_reply': { + 'type': 'bool' + }, + 'information_request': { + 'type': 'bool' + }, + 'mask_reply': { + 'type': 'bool' + }, + 'mask_request': { + 'type': 'bool' + }, + 'mobile_redirect': { + 'type': 'bool' + }, + 'net_redirect': { + 'type': 'bool' + }, + 'net_tos_redirect': { + 'type': 'bool' + }, + 'net_tos_unreachable': { + 'type': 'bool' + }, + 'net_unreachable': { + 'type': 'bool' + }, + 'network_unknown': { + 'type': 'bool' + }, + 'no_room_for_option': { + 'type': 'bool' + }, + 'option_missing': { + 'type': 'bool' + }, + 'packet_too_big': { + 'type': 'bool' + }, + 'parameter_problem': { + 'type': 'bool' + }, + 'port_unreachable': { + 'type': 'bool' + }, + 'precedence_unreachable': { + 'type': 'bool' + }, + 'protocol_unreachable': { + 'type': 'bool' + }, + 'reassembly_timeout': { + 'type': 'bool' + }, + 'redirect': { + 'type': 'bool' + }, + 'router_advertisement': { + 'type': 'bool' + }, + 'router_solicitation': { + 'type': 'bool' + }, + 'source_quench': { + 'type': 'bool' + }, + 'source_route_failed': { + 'type': 'bool' + }, + 'time_exceeded': { + 'type': 'bool' + }, + 'timestamp_reply': { + 'type': 'bool' + }, + 'timestamp_request': { + 'type': 'bool' + }, + 'traceroute': { + 'type': 'bool' + }, + 'ttl_exceeded': { + 'type': 'bool' + }, + 'unreachable': { + 'type': 'bool' + } + }, + 'type': 'dict' + }, + 'igmp': { + 'options': { + 'dvmrp': { + 'type': 'bool' + }, + 'host_query': { + 'type': 'bool' + }, + 'host_report': { + 'type': 'bool' + }, + 'mtrace': { + 'type': 'bool' + }, + 'mtrace_response': { + 'type': 'bool' + }, + 'pim': { + 'type': 'bool' + }, + 'trace': { + 'type': 'bool' + } + }, + 'type': 'dict' + }, + 'tcp': { + 'options': { + 'ack': { + 'type': 'bool' + }, + 'established': { + 'type': 'bool' + }, + 'fin': { + 'type': 'bool' + }, + 'psh': { + 'type': 'bool' + }, + 'rst': { + 'type': 'bool' + }, + 'syn': { + 'type': 'bool' + }, + 'urg': { + 'type': 'bool' + } + }, + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'remark': { + 'type': 'str' + }, + 'sequence': { + 'type': 'int' + }, + 'source': { + 'mutually_exclusive': [['address', 'any', 'host', 'prefix'], ['wildcard_bits', 'any', 'host', 'prefix']], + 'options': { + 'host': { + 'type': 'str' + }, + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'prefix': { + 'type': 'str' + }, + 'port_protocol': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']], + 'options': { + 'eq': { + 'type': 'str' + }, + 'gt': { + 'type': 'str' + }, + 'lt': { + 'type': 'str' + }, + 'neq': { + 'type': 'str' + }, + 'range': { + 'options': { + 'end': { + 'type': 'str' + }, + 'start': { + 'type': 'str' + } + }, + 'required_together': [['start', 'end']], + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': [['address', 'wildcard_bits']], + 'type': 'dict' + }, + 'ttl': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']], + 'options': { + 'eq': { + 'type': 'int' + }, + 'gt': { + 'type': 'int' + }, + 'lt': { + 'type': 'int' + }, + 'neq': { + 'type': 'int' + }, + 'range': { + 'options': { + 'end': { + 'type': 'int' + }, + 'start': { + 'type': 'int' + } + }, + 'type': 'dict' + } + }, + 'type': 'dict' + } + }, + 'type': 'list' + }, + }, + 'type': 'list' + }, + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': [ + 'merged', 'replaced', 'overridden', 'deleted', 'gathered', + 'rendered', 'parsed' + ], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/iosxr/config/acls/__init__.py b/lib/ansible/module_utils/network/iosxr/config/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/config/acls/acls.py b/lib/ansible/module_utils/network/iosxr/config/acls/acls.py new file mode 100644 index 00000000000..ef3f24d5366 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/config/acls/acls.py @@ -0,0 +1,442 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The iosxr_acls class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.iosxr.utils.utils \ + import ( + flatten_dict, + prefix_to_address_wildcard, + is_ipv4_address + ) +from ansible.module_utils.network.iosxr.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.common.utils \ + import ( + to_list, + search_obj_in_list, + dict_diff, + remove_empties, + dict_merge, + ) +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.iosxr.facts.facts import Facts + + +class Acls(ConfigBase): + """ + The iosxr_acls class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'acls', + ] + + def __init__(self, module): + super(Acls, self).__init__(module) + + def get_acls_facts(self, data=None): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts( + self.gather_subset, self.gather_network_resources, data=data) + acls_facts = facts["ansible_network_resources"].get("acls") + if not acls_facts: + return [] + return acls_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + + if self.state in self.ACTION_STATES: + existing_acls_facts = self.get_acls_facts() + else: + existing_acls_facts = [] + + if self.state in self.ACTION_STATES or self.state == "rendered": + commands.extend(self.set_config(existing_acls_facts)) + + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + result["changed"] = True + + if self.state in self.ACTION_STATES: + result["commands"] = commands + + if self.state in self.ACTION_STATES or self.state == "gathered": + changed_acls_facts = self.get_acls_facts() + + elif self.state == "rendered": + result["rendered"] = commands + + elif self.state == "parsed": + running_config = self._module.params["running_config"] + if not running_config: + self._module.fail_json( + msg="value of running_config parameter must not be empty for state parsed" + ) + result["parsed"] = self.get_acls_facts(data=running_config) + + if self.state in self.ACTION_STATES: + result["before"] = existing_acls_facts + if result["changed"]: + result["after"] = changed_acls_facts + + elif self.state == "gathered": + result["gathered"] = changed_acls_facts + + result["warnings"] = warnings + return result + + def set_config(self, existing_acls_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params['config'] + have = existing_acls_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params['state'] + commands = [] + + if state in ('overridden', 'merged', 'replaced', + 'rendered') and not want: + self._module.fail_json( + msg='value of config parameter must not be empty for state {0}' + .format(state)) + + if state == 'overridden': + commands.extend(self._state_overridden(want, have)) + + elif state == 'deleted': + commands.extend(self._state_deleted(want, have)) + + else: + # Instead of passing entire want and have + # list of dictionaries to the respective + # _state_* methods we are passing the want + # and have dictionaries per AFI + for item in want: + afi = item['afi'] + obj_in_have = search_obj_in_list(afi, have, key='afi') + + if state == 'merged' or self.state == 'rendered': + commands.extend( + self._state_merged(remove_empties(item), obj_in_have)) + + elif state == 'replaced': + commands.extend( + self._state_replaced(remove_empties(item), + obj_in_have)) + + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + for want_acl in want['acls']: + have_acl = search_obj_in_list(want_acl['name'], have['acls']) or {} + acl_updates = [] + + for have_ace in have_acl.get('aces', []): + want_ace = search_obj_in_list(have_ace['sequence'], want_acl['aces'], key='sequence') or {} + if not want_ace: + acl_updates.append('no {0}'.format(have_ace['sequence'])) + + for want_ace in want_acl.get('aces', []): + have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {} + set_cmd = self._set_commands(want_ace, have_ace) + if set_cmd: + acl_updates.append(set_cmd) + + if acl_updates: + acl_updates.insert(0, '{0} access-list {1}'.format(want['afi'], want_acl['name'])) + commands.extend(acl_updates) + + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + # Remove extraneous AFI that are present in config but not + # specified in `want` + for have_afi in have: + want_afi = search_obj_in_list(have_afi['afi'], want, key='afi') or {} + if not want_afi: + for acl in have_afi.get('acls', []): + commands.append('no {0} access-list {1}'.format(have_afi['afi'], acl['name'])) + + # First we remove the extraneous ACLs from the AFIs that + # are present both in `want` and in `have` and then + # we call `_state_replaced` to update the ACEs within those ACLs + for want_afi in want: + want_afi = remove_empties(want_afi) + have_afi = search_obj_in_list(want_afi['afi'], have, key='afi') or {} + if have_afi: + for have_acl in have_afi.get('acls', []): + want_acl = search_obj_in_list(have_acl['name'], want_afi.get('acls', [])) or {} + if not want_acl: + commands.append('no {0} access-list {1}'.format(have_afi['afi'], have_acl['name'])) + + commands.extend(self._state_replaced(want_afi, have_afi)) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + if not have: + have = {} + + for want_acl in want['acls']: + have_acl = search_obj_in_list(want_acl['name'], have.get('acls', {})) or {} + + acl_updates = [] + for want_ace in want_acl['aces']: + have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {} + set_cmd = self._set_commands(want_ace, have_ace) + if set_cmd: + acl_updates.append(set_cmd) + + if acl_updates: + acl_updates.insert(0, '{0} access-list {1}'.format(want['afi'], want_acl['name'])) + commands.extend(acl_updates) + + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + + if not want: + want = [{'afi': 'ipv4'}, {'afi': 'ipv6'}] + + for item in want: + item = remove_empties(item) + have_item = search_obj_in_list(item['afi'], have, key='afi') or {} + if 'acls' not in item: + if have_item: + for acl in have_item['acls']: + commands.append('no {0} access-list {1}'.format(have_item['afi'], acl['name'])) + else: + for want_acl in item['acls']: + have_acl = search_obj_in_list(want_acl['name'], have_item.get('acls', [])) or {} + if have_acl: + if 'aces' not in want_acl: + commands.append('no {0} access-list {1}'.format(have_item['afi'], have_acl['name'])) + else: + acl_updates = [] + for want_ace in want_acl['aces']: + have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {} + if have_ace: + acl_updates.append('no {0}'.format(have_ace['sequence'])) + + if acl_updates: + acl_updates.insert(0, '{0} access-list {1}'.format(have_item['afi'], have_acl['name'])) + commands.extend(acl_updates) + + return commands + + def _compute_commands(self, want_ace): + """This command creates an ACE line from an ACE dictionary + + :rtype: A string + :returns: An ACE generated from a structured ACE dictionary + """ + def __compute_src_dest(dir_dict): + cmd = "" + if 'any' in dir_dict: + cmd += 'any ' + elif 'host' in dir_dict: + cmd += 'host {0} '.format(dir_dict['host']) + elif 'prefix' in dir_dict: + cmd += '{0} '.format(dir_dict['prefix']) + else: + cmd += '{0} {1} '.format(dir_dict['address'], + dir_dict['wildcard_bits']) + + if 'port_protocol' in dir_dict: + protocol_range = dir_dict['port_protocol'].get('range') + if protocol_range: + cmd += 'range {0} {1} '.format(protocol_range['start'], + protocol_range['end']) + else: + for key, value in iteritems(dir_dict['port_protocol']): + cmd += '{0} {1} '.format(key, value) + + return cmd + + def __compute_protocol_options(protocol_dict): + cmd = "" + for value in protocol_options.values(): + for subkey, subvalue in iteritems(value): + if subvalue: + cmd += '{0} '.format(subkey.replace('_', '-')) + return cmd + + def __compute_match_options(want_ace): + cmd = "" + + if 'precedence' in want_ace: + cmd += 'precedence {0} '.format(want_ace['precedence']) + + for x in ['dscp', 'packet_length', 'ttl']: + if x in want_ace: + opt_range = want_ace[x].get('range') + if opt_range: + cmd += '{0} range {1} {2} '.format( + x.replace('_', '-'), opt_range['start'], + opt_range['end']) + else: + for key, value in iteritems(want_ace[x]): + cmd += '{0} {1} {2} '.format( + x.replace('_', '-'), key, value) + + for x in ('authen', 'capture', 'fragments', 'routing', 'log', + 'log_input', 'icmp_off', 'destopts', 'hop_by_hop'): + if x in want_ace: + cmd += '{0} '.format(x.replace('_', '-')) + + return cmd + + cmd = "" + if 'sequence' in want_ace: + cmd += '{0} '.format(want_ace['sequence']) + + if 'remark' in want_ace: + cmd += 'remark {0}'.format(want_ace['remark']) + + elif 'line' in want_ace: + cmd += want_ace['line'] + + else: + cmd += '{0} '.format(want_ace['grant']) + if 'protocol' in want_ace: + cmd += '{0} '.format(want_ace['protocol']) + + cmd += __compute_src_dest(want_ace['source']) + cmd += __compute_src_dest(want_ace['destination']) + + protocol_options = want_ace.get('protocol_options', {}) + if protocol_options: + cmd += __compute_protocol_options(protocol_options) + + cmd += __compute_match_options(want_ace) + + return cmd.strip() + + def _set_commands(self, want_ace, have_ace): + """A helped method that checks if there is + a delta between the `have_ace` and `want_ace`. + If there is a delta then it calls `_compute_commands` + to create the ACE line. + + :rtype: A string + :returns: An ACE generated from a structured ACE dictionary + via a call to `_compute_commands` + """ + + if 'line' in want_ace: + if want_ace['line'] != have_ace.get('line'): + return self._compute_commands(want_ace) + + else: + if ('prefix' in want_ace.get('source', {})) or ('prefix' in want_ace.get('destination', {})): + self._prepare_for_diff(want_ace) + + protocol_opt_delta = {} + delta = dict_diff(have_ace, want_ace) + + # `dict_diff` doesn't work properly for `protocol_options` diff, + # so we need to handle it separately + if want_ace.get('protocol_options', {}): + protocol_opt_delta = set(flatten_dict(have_ace.get('protocol_options', {}))) ^ \ + set(flatten_dict(want_ace.get('protocol_options', {}))) + + if delta or protocol_opt_delta: + want_ace = self._dict_merge(have_ace, want_ace) + return self._compute_commands(want_ace) + + def _prepare_for_diff(self, ace): + """This method prepares the want ace dict + for diff calculation against the have ace dict. + + :param ace: The want ace to prepare for diff calculation + """ + # Convert prefixes to "address wildcard bits" format for IPv4 addresses + # Not valid for IPv6 addresses because those can only be specified as prefixes + # and are always rendered in running-config as prefixes too + for x in ['source', 'destination']: + prefix = ace.get(x, {}).get('prefix') + if prefix and is_ipv4_address(prefix): + del ace[x]['prefix'] + ace[x]['address'], ace[x]['wildcard_bits'] = prefix_to_address_wildcard(prefix) + + def _dict_merge(self, have_ace, want_ace): + for x in want_ace: + have_ace[x] = want_ace[x] + return have_ace diff --git a/lib/ansible/module_utils/network/iosxr/facts/acls/__init__.py b/lib/ansible/module_utils/network/iosxr/facts/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/facts/acls/acls.py b/lib/ansible/module_utils/network/iosxr/facts/acls/acls.py new file mode 100644 index 00000000000..f67a3e26d0a --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/facts/acls/acls.py @@ -0,0 +1,381 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The iosxr acls fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +from copy import deepcopy + +from collections import deque +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.iosxr.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.iosxr.utils.utils import isipaddress + +PROTOCOL_OPTIONS = { + 'tcp': ( + 'ack', + 'fin', + 'psh', + 'rst', + 'syn', + 'urg', + 'established', + ), + 'igmp': ('dvmrp', 'host_query', 'host_report', 'mtrace', 'mtrace_response', + 'pim', 'trace', 'v2_leave', 'v2_report', 'v3_report'), + 'icmp': + ('administratively_prohibited', 'alternate_address', 'conversion_error', + 'dod_host_prohibited', 'dod_net_prohibited', 'echo', 'echo_reply', + 'general_parameter_problem', 'host_isolated', + 'host_precedence_unreachable', 'host_redirect', 'host_tos_redirect', + 'host_tos_unreachable', 'host_unknown', 'host_unreachable', + 'information_reply', 'information_request', 'mask_reply', 'mask_request', + 'mobile_redirect', 'net_redirect', 'net_tos_redirect', + 'net_tos_unreachable', 'net_unreachable', 'network_unknown', + 'no_room_for_option', 'option_missing', 'packet_too_big', + 'parameter_problem', 'port_unreachable', 'precedence_unreachable', + 'protocol_unreachable', 'reassembly_timeout', 'redirect', + 'router_advertisement', 'router_solicitation', 'source_quench', + 'source_route_failed', 'time_exceeded', 'timestamp_reply', + 'timestamp_request', 'traceroute', 'ttl_exceeded', 'unreachable'), + 'icmpv6': + ('address_unreachable', 'administratively_prohibited', + 'beyond_scope_of_source_address', 'destination_unreachable', 'echo', + 'echo_reply', 'erroneous_header_field', 'group_membership_query', + 'group_membership_report', 'group_membership_termination', + 'host_unreachable', 'nd_na', 'nd_ns', 'neighbor_redirect', + 'no_route_to_destination', 'node_information_request_is_refused', + 'node_information_successful_reply', 'packet_too_big', + 'parameter_problem', 'port_unreachable', 'query_subject_is_IPv4address', + 'query_subject_is_IPv6address', 'query_subject_is_domainname', + 'reassembly_timeout', 'redirect', 'router_advertisement', + 'router_renumbering', 'router_solicitation', 'rr_command', 'rr_result', + 'rr_seqnum_reset', 'time_exceeded', 'ttl_exceeded', 'unknown_query_type', + 'unreachable', 'unrecognized_next_header', 'unrecognized_option', + 'whoareyou_reply', 'whoareyou_request') +} + + +class AclsFacts(object): + """ The iosxr acls fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = AclsArgs.argument_spec + spec = deepcopy(self.argument_spec) + + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_device_data(self, connection): + return connection.get('show access-lists afi-all') + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for acls + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + objs = [] + + acl_lines = data.splitlines() + + # We iterate through the data and create a list of ACLs + # where each ACL is a dictionary in the following format: + # {'afi': 'ipv4', 'name': 'acl_1', 'aces': ['10 permit 172.16.0.0 0.0.255.255', '20 deny 192.168.34.0 0.0.0.255']} + if acl_lines: + acl, acls = {}, [] + for line in acl_lines: + if line.startswith('ip'): + if acl: + acls.append(acl) + acl = {'aces': []} + acl['afi'], acl['name'] = line.split()[0], line.split()[2] + else: + acl['aces'].append(line.strip()) + acls.append(acl) + + # Here we group the ACLs based on AFI + # { + # 'ipv6': [{'aces': ['10 permit ipv6 2000::/12 any'], 'name': 'acl_2'}], + # 'ipv4': [{'aces': ['10 permit 172.16.0.0 0.0.255.255', '20 deny 192.168.34.0 0.0.0.255'], 'name': 'acl_1'}, + # {'aces': ['20 deny 10.0.0.0/8 log'], 'name': 'acl_3'}] + # } + + grouped_acls = {'ipv4': [], 'ipv6': []} + for acl in acls: + acl_copy = deepcopy(acl) + del acl_copy['afi'] + grouped_acls[acl['afi']].append(acl_copy) + + # Now that we have the ACLs in a fairly structured format, + # we pass it on to render_config to convert it to model spec + for key, value in iteritems(grouped_acls): + obj = self.render_config(self.generated_spec, value) + if obj: + obj['afi'] = key + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('acls', None) + facts = {} + + facts['acls'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['acls'].append(utils.remove_empties(cfg)) + + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + config['acls'] = [] + + for item in conf: + acl = {'name': item['name']} + aces = item.get('aces', []) + if aces: + acl['aces'] = [] + for ace in aces: + acl['aces'].append(self._render_ace(ace)) + config['acls'].append(acl) + + return utils.remove_empties(config) + + def _render_ace(self, ace): + """ + Parses an Access Control Entry (ACE) and converts it + into model spec + + :param ace: An ACE in device specific format + :rtype: dictionary + :returns: The ACE in structured format + """ + + def __parse_src_dest(rendered_ace, ace_queue, direction): + """ + Parses the ACE queue and populates address, wildcard_bits, + host or any keys in the source/destination dictionary of + ace dictionary, i.e., `rendered_ace`. + + :param rendered_ace: The dictionary containing the ACE in structured format + :param ace_queue: The ACE queue + :param direction: Specifies whether to populate `source` or `destination` + dictionary + """ + element = ace_queue.popleft() + if element == 'host': + rendered_ace[direction] = {'host': ace_queue.popleft()} + + elif element == 'any': + rendered_ace[direction] = {'any': True} + + elif '/' in element: + rendered_ace[direction] = { + 'prefix': element + } + + elif isipaddress(element): + rendered_ace[direction] = { + 'address': element, + 'wildcard_bits': ace_queue.popleft() + } + + def __parse_port_protocol(rendered_ace, ace_queue, direction): + """ + Parses the ACE queue and populates `port_protocol` dictionary in the + ACE dictionary, i.e., `rendered_ace`. + + :param rendered_ace: The dictionary containing the ACE in structured format + :param ace_queue: The ACE queue + :param direction: Specifies whether to populate `source` or `destination` + dictionary + """ + if len(ace_queue) > 0 and ace_queue[0] in ('eq', 'gt', 'lt', 'neq', + 'range'): + element = ace_queue.popleft() + port_protocol = {} + + if element == 'range': + port_protocol['range'] = { + 'start': ace_queue.popleft(), + 'end': ace_queue.popleft() + } + else: + port_protocol[element] = ace_queue.popleft() + + rendered_ace[direction]['port_protocol'] = port_protocol + + def __parse_protocol_options(rendered_ace, ace_queue, protocol): + """ + Parses the ACE queue and populates protocol specific options + of the required dictionary and updates the ACE dictionary, i.e., + `rendered_ace`. + + :param rendered_ace: The dictionary containing the ACE in structured format + :param ace_queue: The ACE queue + :param protocol: Specifies the protocol that will be populated under + `protocol_options` dictionary + """ + if len(ace_queue) > 0: + protocol_options = {protocol: {}} + + for match_bit in PROTOCOL_OPTIONS.get(protocol, ()): + if match_bit.replace('_', '-') in ace_queue: + protocol_options[protocol][match_bit] = True + ace_queue.remove(match_bit.replace('_', '-')) + + rendered_ace['protocol_options'] = protocol_options + + def __parse_match_options(rendered_ace, ace_queue): + """ + Parses the ACE queue and populates remaining options in the ACE dictionary, + i.e., `rendered_ace` + + :param rendered_ace: The dictionary containing the ACE in structured format + :param ace_queue: The ACE queue + """ + if len(ace_queue) > 0: + # We deepcopy the actual queue and iterate through the + # copied queue. However, we pop off the elements from + # the actual queue. Then, in every pass we update the copied + # queue with the current state of the original queue. + # This is done because a queue cannot be mutated during iteration. + copy_ace_queue = deepcopy(ace_queue) + + for element in copy_ace_queue: + if element == 'precedence': + ace_queue.popleft() + rendered_ace['precedence'] = ace_queue.popleft() + + elif element == 'dscp': + ace_queue.popleft() + dscp = {} + operation = ace_queue.popleft() + + if operation in ('eq', 'gt', 'neq', 'lt', 'range'): + if operation == 'range': + dscp['range'] = { + 'start': ace_queue.popleft(), + 'end': ace_queue.popleft() + } + else: + dscp[operation] = ace_queue.popleft() + else: + # `dscp` can be followed by either the dscp value itself or + # the same thing can be represented using "dscp eq ". + # In both cases, it would show up as {'dscp': {'eq': "dscp_value"}}. + dscp['eq'] = operation + + rendered_ace['dscp'] = dscp + + elif element in ('packet-length', 'ttl'): + ace_queue.popleft() + element_dict = {} + operation = ace_queue.popleft() + + if operation == 'range': + element_dict['range'] = { + 'start': ace_queue.popleft(), + 'end': ace_queue.popleft() + } + else: + element_dict[operation] = ace_queue.popleft() + + rendered_ace[element.replace('-', '_')] = element_dict + + elif element in ('log', 'log-input', 'fragments', + 'icmp-off', 'capture', 'destopts', + 'authen', 'routing', 'hop-by-hop'): + rendered_ace[element.replace('-', '_')] = True + ace_queue.remove(element) + + copy_ace_queue = deepcopy(ace_queue) + + rendered_ace = {} + split_ace = ace.split() + + # Create a queue with each word in the ace + # We parse each element and pop it off the queue + ace_queue = deque(split_ace) + + # An ACE will always have a sequence number, even if + # it is not explicitly provided while configuring + sequence = int(ace_queue.popleft()) + rendered_ace['sequence'] = sequence + operation = ace_queue.popleft() + + if operation == 'remark': + rendered_ace['remark'] = ' '.join(split_ace[2:]) + + else: + rendered_ace['grant'] = operation + + # If the entry is a non-remark entry, the third element + # will always be the protocol specified. By default, it's + # the AFI. + rendered_ace['protocol'] = ace_queue.popleft() + + # Populate source dictionary + __parse_src_dest(rendered_ace, ace_queue, direction='source') + # Populate port_protocol key in source dictionary + __parse_port_protocol(rendered_ace, ace_queue, direction='source') + # Populate destination dictionary + __parse_src_dest(rendered_ace, ace_queue, direction='destination') + # Populate port_protocol key in destination dictionary + __parse_port_protocol(rendered_ace, + ace_queue, + direction='destination') + # Populate protocol_options dictionary + __parse_protocol_options(rendered_ace, + ace_queue, + protocol=rendered_ace['protocol']) + # Populate remaining match options' dictionaries + __parse_match_options(rendered_ace, ace_queue) + + # At this stage the queue should be empty + # If the queue is not empty, it means that + # we haven't been able to parse the entire ACE + # In this case, we add the whole unprocessed ACE + # to a key called `line` and send it back + if len(ace_queue) > 0: + rendered_ace = { + 'sequence': sequence, + 'line': ' '.join(split_ace[1:]) + } + + return utils.remove_empties(rendered_ace) diff --git a/lib/ansible/module_utils/network/iosxr/facts/facts.py b/lib/ansible/module_utils/network/iosxr/facts/facts.py index b8af928a730..312682f1ad2 100644 --- a/lib/ansible/module_utils/network/iosxr/facts/facts.py +++ b/lib/ansible/module_utils/network/iosxr/facts/facts.py @@ -24,6 +24,8 @@ from ansible.module_utils.network.iosxr.facts.lag_interfaces.lag_interfaces impo from ansible.module_utils.network.iosxr.facts.l2_interfaces.l2_interfaces import L2_InterfacesFacts from ansible.module_utils.network.iosxr.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts from ansible.module_utils.network.iosxr.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts +from ansible.module_utils.network.iosxr.facts.acls.acls import AclsFacts + FACT_LEGACY_SUBSETS = dict( default=Default, @@ -40,7 +42,8 @@ FACT_RESOURCE_SUBSETS = dict( l2_interfaces=L2_InterfacesFacts, lag_interfaces=Lag_interfacesFacts, l3_interfaces=L3_InterfacesFacts, - acl_interfaces=Acl_interfacesFacts + acl_interfaces=Acl_interfacesFacts, + acls=AclsFacts ) diff --git a/lib/ansible/module_utils/network/iosxr/utils/utils.py b/lib/ansible/module_utils/network/iosxr/utils/utils.py index c90a99260ae..73589601abf 100644 --- a/lib/ansible/module_utils/network/iosxr/utils/utils.py +++ b/lib/ansible/module_utils/network/iosxr/utils/utils.py @@ -8,6 +8,8 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +from ansible.module_utils._text import to_text +from ansible.module_utils.compat import ipaddress from ansible.module_utils.six import iteritems from ansible.module_utils.network.common.utils import dict_diff, is_masklen, to_netmask, search_obj_in_list @@ -303,3 +305,51 @@ def get_interface_type(interface): return 'preconfigure' else: return 'unknown' + + +def isipaddress(data): + """ + Checks if the passed string is + a valid IPv4 or IPv6 address + """ + isipaddress = True + + try: + ipaddress.ip_address(data) + except ValueError: + isipaddress = False + + return isipaddress + + +def is_ipv4_address(data): + """ + Checks if the passed string is + a valid IPv4 address + """ + if '/' in data: + data = data.split('/')[0] + + if not isipaddress(to_text(data)): + raise ValueError('{0} is not a valid IP address'.format(data)) + + return (ipaddress.ip_address(to_text(data)).version == 4) + + +def prefix_to_address_wildcard(prefix): + """ Converts a IPv4 prefix into address and + wildcard mask + + :returns: IPv4 address and wildcard mask + """ + wildcard = [] + + subnet = to_text(ipaddress.IPv4Network(to_text(prefix)).netmask) + + for x in subnet.split('.'): + component = 255 - int(x) + wildcard.append(str(component)) + + wildcard = '.'.join(wildcard) + + return prefix.split('/')[0], wildcard diff --git a/lib/ansible/modules/network/iosxr/iosxr_acls.py b/lib/ansible/modules/network/iosxr/iosxr_acls.py new file mode 100644 index 00000000000..045f7317703 --- /dev/null +++ b/lib/ansible/modules/network/iosxr/iosxr_acls.py @@ -0,0 +1,1480 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for iosxr_acls +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +DOCUMENTATION = """ +--- +module: iosxr_acls +version_added: "2.10" +short_description: Manage Access Control Lists (ACLs) on devices running IOS-XR. +description: + - This module manages Access Control Lists (ACLs) on devices running IOS-XR. +author: Nilashish Chakraborty (@NilashishC) +options: + config: + description: A list of dictionaries specifying ACL configurations. + type: list + elements: dict + suboptions: + afi: + description: + - The Address Family Indicator (AFI) for the Access Control Lists (ACL). + type: str + required: True + choices: ['ipv4', 'ipv6'] + acls: + description: + - A list of Access Control Lists (ACLs). + type: list + elements: dict + suboptions: + name: + description: + - The name of the Access Control List (ACL). + type: str + aces: + description: + - List of Access Control Entries (ACEs) for this Access Control List (ACL). + type: list + elements: dict + suboptions: + sequence: + description: + - Sequence number for the Access Control Entry (ACE). + type: int + grant: + description: + - Forward or drop packets matching the Access Control Entry (ACE). + type: str + choices: ['permit', 'deny'] + remark: + description: + - Comments or a description for the access list. + type: str + line: + description: + - An ACE excluding the sequence number. + - This key is mutually exclusive with all the other attributes except 'sequence'. + - When used with other attributes, the value of this key will get precedence and the + other keys will be ignored. + - This should only be used when an attribute doesn't exist in the argspec but is valid for the device. + - For fact gathering, any ACE that is not fully parsed, will show up as a value of this attribute, + excluding the sequence number, which will be populated as value of the sequence key. + type: str + aliases: ['ace'] + source: + description: + - Specifies the packet source. + type: dict + suboptions: + host: + description: + - The host IP address to match. + type: str + address: + description: + - The source IP address to match. + type: str + wildcard_bits: + description: + - The Wildcard bits to apply to source address. + type: str + any: + description: + - Match any source address. + type: bool + prefix: + description: + - Source network prefix. + type: str + port_protocol: + description: + - Specify the source port or protocol. + type: dict + suboptions: + eq: + description: + - Match only packets on a given port number. + type: str + gt: + description: + - Match only packets with a greater port number. + type: str + lt: + description: + - Match only packets with a lower port number. + type: str + neq: + description: + - Match only packets not on a given port number. + type: str + range: + description: + - Match only packets in the range of port numbers + type: dict + suboptions: + start: + description: + - Specify the start of the port range + type: str + end: + description: + - Specify the end of the port range + type: str + destination: + description: + - Specifies the packet destination. + type: dict + suboptions: + host: + description: + - The host IP address to match. + type: str + address: + description: + - The destination IP address to match. + type: str + wildcard_bits: + description: + - The Wildcard bits to apply to destination address. + type: str + any: + description: + - Match any destination address. + type: bool + prefix: + description: + - Destination network prefix. + type: str + port_protocol: + description: + - Specify the source port or protocol. + type: dict + suboptions: + eq: + description: + - Match only packets on a given port number. + type: str + gt: + description: + - Match only packets with a greater port number. + type: str + lt: + description: + - Match only packets with a lower port number. + type: str + neq: + description: + - Match only packets not on a given port number. + type: str + range: + description: + - Match only packets in the range of port numbers + type: dict + suboptions: + start: + description: + - Specify the start of the port range + type: str + end: + description: + - Specify the end of the port range + type: str + protocol: + description: + - Specify the protocol to match. + - Refer to vendor documentation for valid values. + type: str + protocol_options: + description: + - Additional suboptions for the protocol. + type: dict + suboptions: + icmpv6: + description: Internet Control Message Protocol settings for IPv6. + type: dict + suboptions: + address_unreachable: + description: Address Unreachable + type: bool + administratively_prohibited: + description: Administratively Prohibited + type: bool + beyond_scope_of_source_address: + description: Administratively Prohibited + type: bool + destination_unreachable: + description: Destination Unreachable + type: bool + echo: + description: Echo + type: bool + echo_reply: + description: Echo Reply + type: bool + erroneous_header_field: + description: Erroneous Header Field + type: bool + group_membership_query: + description: Group Membership Query + type: bool + group_membership_report: + description: Group Membership Report + type: bool + group_membership_termination: + description: Group Membership Termination + type: bool + host_unreachable: + description: Host Unreachable + type: bool + nd_na: + description: Neighbor Discovery - Neighbor Advertisement + type: bool + nd_ns: + description: Neighbor Discovery - Neighbor Solicitation + type: bool + neighbor_redirect: + description: Neighbor Redirect + type: bool + no_route_to_destination: + description: No Route To Destination + type: bool + node_information_request_is_refused: + description: Node Information Request Is Refused + type: bool + node_information_successful_reply: + description: Node Information Successful Reply + type: bool + packet_too_big: + description: Packet Too Big + type: bool + parameter_problem: + description: Parameter Problem + type: bool + port_unreachable: + description: Port Unreachable + type: bool + query_subject_is_IPv4address: + description: Query Subject Is IPv4 address + type: bool + query_subject_is_IPv6address: + description: Query Subject Is IPv6 address + type: bool + query_subject_is_domainname: + description: Query Subject Is Domain name + type: bool + reassembly_timeout: + description: Reassembly Timeout + type: bool + redirect: + description: Redirect + type: bool + router_advertisement: + description: Router Advertisement + type: bool + router_renumbering: + description: Router Renumbering + type: bool + router_solicitation: + description: Router Solicitation + type: bool + rr_command: + description: RR Command + type: bool + rr_result: + description: RR Result + type: bool + rr_seqnum_reset: + description: RR Seqnum Reset + type: bool + time_exceeded: + description: Time Exceeded + type: bool + ttl_exceeded: + description: TTL Exceeded + type: bool + unknown_query_type: + description: Unknown Query Type + type: bool + unreachable: + description: Unreachable + type: bool + unrecognized_next_header: + description: Unrecognized Next Header + type: bool + unrecognized_option: + description: Unrecognized Option + type: bool + whoareyou_reply: + description: Whoareyou Reply + type: bool + whoareyou_request: + description: Whoareyou Request + type: bool + icmp: + description: Internet Control Message Protocol settings. + type: dict + suboptions: + administratively_prohibited: + description: Administratively prohibited + type: bool + alternate_address: + description: Alternate address + type: bool + conversion_error: + description: Datagram conversion + type: bool + dod_host_prohibited: + description: Host prohibited + type: bool + dod_net_prohibited: + description: Net prohibited + type: bool + echo: + description: Echo (ping) + type: bool + echo_reply: + description: Echo reply + type: bool + general_parameter_problem: + description: Parameter problem + type: bool + host_isolated: + description: Host isolated + type: bool + host_precedence_unreachable: + description: Host unreachable for precedence + type: bool + host_redirect: + description: Host redirect + type: bool + host_tos_redirect: + description: Host redirect for TOS + type: bool + host_tos_unreachable: + description: Host unreachable for TOS + type: bool + host_unknown: + description: Host unknown + type: bool + host_unreachable: + description: Host unreachable + type: bool + information_reply: + description: Information replies + type: bool + information_request: + description: Information requests + type: bool + mask_reply: + description: Mask replies + type: bool + mask_request: + description: Mask requests + type: bool + mobile_redirect: + description: Mobile host redirect + type: bool + net_redirect: + description: Network redirect + type: bool + net_tos_redirect: + description: Net redirect for TOS + type: bool + net_tos_unreachable: + description: Network unreachable for TOS + type: bool + net_unreachable: + description: Net unreachable + type: bool + network_unknown: + description: Network unknown + type: bool + no_room_for_option: + description: Parameter required but no room + type: bool + option_missing: + description: Parameter required but not present + type: bool + packet_too_big: + description: Fragmentation needed and DF set + type: bool + parameter_problem: + description: All parameter problems + type: bool + port_unreachable: + description: Port unreachable + type: bool + precedence_unreachable: + description: Precedence cutoff + type: bool + protocol_unreachable: + description: Protocol unreachable + type: bool + reassembly_timeout: + description: Reassembly timeout + type: bool + redirect: + description: All redirects + type: bool + router_advertisement: + description: Router discovery advertisements + type: bool + router_solicitation: + description: Router discovery solicitations + type: bool + source_quench: + description: Source quenches + type: bool + source_route_failed: + description: Source route failed + type: bool + time_exceeded: + description: All time exceededs + type: bool + timestamp_reply: + description: Timestamp replies + type: bool + timestamp_request: + description: Timestamp requests + type: bool + traceroute: + description: Traceroute + type: bool + ttl_exceeded: + description: TTL exceeded + type: bool + unreachable: + description: All unreachables + type: bool + tcp: + description: Match TCP packet flags + type: dict + suboptions: + ack: + description: Match on the ACK bit + type: bool + established: + description: Match established connections + type: bool + fin: + description: Match on the FIN bit + type: bool + psh: + description: Match on the PSH bit + type: bool + rst: + description: Match on the RST bit + type: bool + syn: + description: Match on the SYN bit + type: bool + urg: + description: Match on the URG bit + type: bool + igmp: + description: Internet Group Management Protocol (IGMP) settings. + type: dict + suboptions: + dvmrp: + description: Match Distance Vector Multicast Routing Protocol + type: bool + host_query: + description: Match Host Query + type: bool + host_report: + description: Match Host Report + type: bool + pim: + description: Match Protocol Independent Multicast + type: bool + trace: + description: Multicast trace + type: bool + mtrace: + description: Match mtrace + type: bool + mtrace_response: + description: Match mtrace response + type: bool + dscp: + description: + - Match packets with given DSCP value. + type: dict + suboptions: + eq: + description: Match only packets on a given dscp value + type: str + gt: + description: Match only packets with a greater dscp value + type: str + lt: + description: Match only packets with a lower dscp value + type: str + neq: + description: Match only packets not on a given dscp value + type: str + range: + description: Match only packets in the range of dscp values + type: dict + suboptions: + start: + description: Start of the dscp range + type: str + end: + description: End of the dscp range + type: str + fragments: + description: + - Check non-intial fragments. + type: bool + packet_length: + description: + - Match packets given packet length. + type: dict + suboptions: + eq: + description: Match only packets on a given packet length + type: int + gt: + description: Match only packets with a greater packet length + type: int + lt: + description: Match only packets with a lower packet length + type: int + neq: + description: Match only packets not on a given packet length + type: int + range: + description: Match only packets in the range of packet lengths + type: dict + suboptions: + start: + description: Start of the packet length range + type: int + end: + description: End of the packet length range + type: int + precedence: + description: Match packets with given precedence value + type: str + ttl: + description: Match against specified TTL value. + type: dict + suboptions: + eq: + description: Match only packets with exact TTL value. + type: int + gt: + description: Match only packets with a greater TTL value. + type: int + lt: + description: Match only packets with a lower TTL value. + type: int + neq: + description: Match only packets that won't have the given TTL value. + type: int + range: + description: Match only packets in the range of given TTL values. + type: dict + suboptions: + start: + description: Start of the TTL range. + type: int + end: + description: End of the TTL range. + type: int + log: + description: + - Enable/disable log matches against this entry. + type: bool + log_input: + description: + - Enable/disable log matches against this entry, including input interface. + type: bool + icmp_off: + description: + - Enable/disable the ICMP message for this entry. + type: bool + capture: + description: + - Capture matched packet. + type: bool + destopts: + description: + - Match if destination opts header is present. + type: bool + authen: + description: + - Match if authentication header is present. + type: bool + routing: + description: + - Match if routing header is present. + type: bool + hop_by_hop: + description: + - Match if hop-by-hop opts header is present. + type: bool + running_config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(running_config) argument allows the + implementer to pass in the configuration to use as the base + config for comparison. This value of this option should be the + output received from device by executing command + B(show running-config router static). + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ +# Using merged to add new ACLs + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:07:45.767 UTC +# RP/0/RP0/CPU0:ios# + +- name: Merge the provided configuration with the exisiting running configuration + iosxr_acls: + config: + - afi: ipv6 + acls: + - name: acl6_1 + aces: + - sequence: 10 + grant: deny + protocol: tcp + source: + prefix: 2001:db8:1234::/48 + port_protocol: + range: + start: ftp + end: telnet + destination: + any: True + protocol_options: + tcp: + syn: True + ttl: + range: + start: 180 + end: 250 + routing: True + authen: True + log: True + + - sequence: 20 + grant: permit + protocol: icmpv6 + source: + any: True + destination: + any: True + protocol_options: + icmpv6: + router_advertisement: True + precedence: network + destopts: True + + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 16 + remark: TEST_ACL_1_REMARK + + - sequence: 21 + grant: permit + protocol: tcp + source: + host: 192.0.2.10 + port_protocol: + range: + start: pop3 + end: 121 + destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + protocol_options: + tcp: + rst: True + + - sequence: 23 + grant: deny + protocol: icmp + source: + any: True + destination: + prefix: 198.51.100.0/28 + protocol_options: + icmp: + reassembly_timeout: True + dscp: + lt: af12 + + - name: acl_2 + aces: + - sequence: 10 + remark: TEST_ACL_2_REMARK + state: merged + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +# Using merged to update existing ACLs + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Update existing ACEs + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 21 + source: + prefix: 198.51.100.32/28 + port_protocol: + range: + start: pop3 + end: 121 + protocol_options: + tcp: + syn: True + + - sequence: 23 + protocol_options: + icmp: + router_advertisement: True + dscp: + eq: af23 + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:47:18.711 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp 198.51.100.32 0.0.0.15 range pop3 121 198.51.100.0 0.0.0.15 syn +# 23 deny icmp any 198.51.100.0 0.0.0.15 router-advertisement dscp eq af23 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +# Using replaced to replace a whole ACL + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Replace device configurations of listed ACL with provided configurations + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_2 + aces: + - sequence: 11 + grant: permit + protocol: igmp + source: + host: 198.51.100.130 + destination: + any: True + ttl: + eq: 100 + + - sequence: 12 + grant: deny + source: + any: True + destination: + any: True + protocol: icmp + state: replaced + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 06:19:51.496 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp 198.51.100.32 0.0.0.15 range pop3 121 198.51.100.0 0.0.0.15 syn +# 23 deny icmp any 198.51.100.0 0.0.0.15 router-advertisement dscp eq af23 +# ipv4 access-list acl_2 +# 11 permit igmp host 198.51.100.130 any ttl eq 100 +# 12 deny icmp any any +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +# Using overridden to override all ACLs in the device + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Overridde all ACLs configuration with provided configuration + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 10 + grant: permit + source: + any: True + destination: + any: True + protocol: tcp + + - name: acl_2 + aces: + - sequence: 20 + grant: permit + source: + any: True + destination: + any: True + protocol: igmp + state: overridden + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 06:31:22.178 UTC +# ipv4 access-list acl_1 +# 10 permit tcp any any +# ipv4 access-list acl_2 +# 20 permit igmp any any + +# Using deleted to delete a single ACE from an ACL + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Delete a single ACE + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 23 + state: deleted + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +# Using deleted to delete an entire ACL + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Delete a single ACL + iosxr_acls: + config: + - afi: ipv6 + acls: + - name: acl6_1 + state: deleted + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK + +# Using deleted to delete all ACLs under one AFI + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Delete all ACLs under one AFI + iosxr_acls: + config: + - afi: ipv4 + state: deleted + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +# Using deleted to delete all ACLs from the device + +# Before state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:22:57.021 UTC +# ipv4 access-list acl_1 +# 16 remark TEST_ACL_1_REMARK +# 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst +# 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +# ipv4 access-list acl_2 +# 10 remark TEST_ACL_2_REMARK +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network destopts + +- name: Delete all ACLs from the device + iosxr_acls: + state: deleted + +# After state: +# ------------- + +# RP/0/RP0/CPU0:ios#sh access-lists afi-all +# Thu Feb 20 05:07:45.767 UTC +# RP/0/RP0/CPU0:ios# + +# Using gathered to gather ACL facts from the device + +- name: Gather ACL interfaces facts using gathered state + iosxr_acls: + state: gathered + +# Task Output (redacted) +# ----------------------- +# + +# "gathered": [ +# { +# "acls": [ +# { +# "aces": [ +# { +# "remark": "TEST_ACL_1_REMARK", +# "sequence": 16 +# }, +# { +# "destination": { +# "address": "198.51.100.0", +# "wildcard_bits": "0.0.0.15" +# }, +# "grant": "permit", +# "protocol": "tcp", +# "protocol_options": { +# "tcp": { +# "rst": true +# } +# }, +# "sequence": 21, +# "source": { +# "host": "192.0.2.10", +# "port_protocol": { +# "range": { +# "end": "121", +# "start": "pop3" +# } +# } +# } +# }, +# { +# "destination": { +# "address": "198.51.100.0", +# "wildcard_bits": "0.0.0.15" +# }, +# "dscp": { +# "lt": "af12" +# }, +# "grant": "deny", +# "protocol": "icmp", +# "protocol_options": { +# "icmp": { +# "reassembly_timeout": true +# } +# }, +# "sequence": 23, +# "source": { +# "any": true +# } +# } +# ], +# "name": "acl_1" +# }, +# { +# "aces": [ +# { +# "remark": "TEST_ACL_2_REMARK", +# "sequence": 10 +# } +# ], +# "name": "acl_2" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "aces": [ +# { +# "authen": true, +# "destination": { +# "any": true +# }, +# "grant": "deny", +# "log": true, +# "protocol": "tcp", +# "protocol_options": { +# "tcp": { +# "syn": true +# } +# }, +# "routing": true, +# "sequence": 10, +# "source": { +# "port_protocol": { +# "range": { +# "end": "telnet", +# "start": "ftp" +# } +# }, +# "prefix": "2001:db8:1234::/48" +# }, +# "ttl": { +# "range": { +# "end": 250, +# "start": 180 +# } +# } +# }, +# { +# "destination": { +# "any": true +# }, +# "destopts": true, +# "grant": "permit", +# "precedence": "network", +# "protocol": "icmpv6", +# "protocol_options": { +# "icmpv6": { +# "router_advertisement": true +# } +# }, +# "sequence": 20, +# "source": { +# "any": true +# } +# } +# ], +# "name": "acl6_1" +# } +# ], +# "afi": "ipv6" +# } +# ] + +# Using rendered + +- name: Render platform specific commands (without connecting to the device) + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_2 + aces: + - sequence: 11 + grant: permit + protocol: igmp + source: + host: 198.51.100.130 + destination: + any: True + ttl: + eq: 100 + + - sequence: 12 + grant: deny + source: + any: True + destination: + any: True + protocol: icmp + state: rendered + +# Task Output (redacted) +# ----------------------- + +# "rendered": [ +# "ipv4 access-list acl_2", +# "11 permit igmp host 198.51.100.130 any ttl eq 100", +# "12 deny icmp any any" + +# Using parsed + +# parsed.cfg +# ------------ +# +# ipv4 access-list acl_1 +# 10 remark TEST_ACL_2_REMARK +# ipv4 access-list acl_2 +# 11 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log +# 21 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts +# ipv6 access-list acl6_1 +# 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log +# 20 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts + +- name: Parse externally provided ACL config to agnostic model + iosxr_acls: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task Output (redacted) +# ----------------------- +# "parsed": [ +# { +# "acls": [ +# { +# "aces": [ +# { +# "remark": "TEST_ACL_2_REMARK", +# "sequence": 10 +# } +# ], +# "name": "acl_1" +# }, +# { +# "aces": [ +# { +# "authen": true, +# "destination": { +# "any": true +# }, +# "grant": "deny", +# "log": true, +# "protocol": "tcp", +# "protocol_options": { +# "tcp": { +# "syn": true +# } +# }, +# "routing": true, +# "sequence": 11, +# "source": { +# "port_protocol": { +# "range": { +# "end": "telnet", +# "start": "ftp" +# } +# }, +# "prefix": "2001:db8:1234::/48" +# }, +# "ttl": { +# "range": { +# "end": 250, +# "start": 180 +# } +# } +# }, +# { +# "destination": { +# "any": true +# }, +# "destopts": true, +# "grant": "permit", +# "packet_length": { +# "eq": 576 +# }, +# "precedence": "network", +# "protocol": "icmpv6", +# "protocol_options": { +# "icmpv6": { +# "router_advertisement": true +# } +# }, +# "sequence": 21, +# "source": { +# "any": true +# } +# } +# ], +# "name": "acl_2" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "aces": [ +# { +# "authen": true, +# "destination": { +# "any": true +# }, +# "grant": "deny", +# "log": true, +# "protocol": "tcp", +# "protocol_options": { +# "tcp": { +# "syn": true +# } +# }, +# "routing": true, +# "sequence": 10, +# "source": { +# "port_protocol": { +# "range": { +# "end": "telnet", +# "start": "ftp" +# } +# }, +# "prefix": "2001:db8:1234::/48" +# }, +# "ttl": { +# "range": { +# "end": 250, +# "start": 180 +# } +# } +# }, +# { +# "destination": { +# "any": true +# }, +# "destopts": true, +# "grant": "permit", +# "packet_length": { +# "eq": 576 +# }, +# "precedence": "network", +# "protocol": "icmpv6", +# "protocol_options": { +# "icmpv6": { +# "router_advertisement": true +# } +# }, +# "sequence": 20, +# "source": { +# "any": true +# } +# } +# ], +# "name": "acl6_1" +# } +# ], +# "afi": "ipv6" +# } +# ] +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - ipv6 access-list acl6_1 + - 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log + - 20 permit icmpv6 any any router-advertisement precedence network destopts + - ipv4 access-list acl_1 + - 16 remark TEST_ACL_1_REMARK + - 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst + - 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.iosxr.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.iosxr.config.acls.acls import Acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [('state', 'merged', ('config',)), + ('state', 'replaced', ('config',)), + ('state', 'overridden', ('config',)), + ('state', 'rendered', ('config',)), + ('state', 'parsed', ('running_config',))] + + module = AnsibleModule(argument_spec=AclsArgs.argument_spec, required_if=required_if, + supports_check_mode=True) + + result = Acls(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/iosxr/iosxr_facts.py b/lib/ansible/modules/network/iosxr/iosxr_facts.py index 8a1f685a5fc..244cb617810 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_facts.py +++ b/lib/ansible/modules/network/iosxr/iosxr_facts.py @@ -55,7 +55,7 @@ options: specific subset should not be collected. Valid subsets are 'all', 'lacp', 'lacp_interfaces', 'lldp_global', 'lldp_interfaces', 'interfaces', 'l2_interfaces', 'l3_interfaces', - 'lag_interfaces'. + 'lag_interfaces', 'acls'. required: false version_added: "2.9" """ diff --git a/test/integration/targets/iosxr_acls/defaults/main.yaml b/test/integration/targets/iosxr_acls/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/iosxr_acls/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/iosxr_acls/tasks/cli.yaml b/test/integration/targets/iosxr_acls/tasks/cli.yaml new file mode 100644 index 00000000000..337e34133b0 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- name: Collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + delegate_to: localhost + +- name: Run test case (connection=network_cli) + include: "{{ test_case_to_run }}" + vars: + ansible_connection: network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_acls/tasks/main.yaml b/test/integration/targets/iosxr_acls/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/iosxr_acls/tests/cli/_populate_config.yaml b/test/integration/targets/iosxr_acls/tests/cli/_populate_config.yaml new file mode 100644 index 00000000000..440943c864d --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/_populate_config.yaml @@ -0,0 +1,13 @@ +--- +- name: Setup + iosxr_config: + lines: | + ipv6 access-list acl6_1 + 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log + 20 permit icmpv6 any any router-advertisement precedence network destopts + ipv4 access-list acl_1 + 16 remark TEST_ACL_1_REMARK + 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst + 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 + ipv4 access-list acl_2 + 10 remark TEST_ACL_2_REMARK \ No newline at end of file diff --git a/test/integration/targets/iosxr_acls/tests/cli/_remove_config.yaml b/test/integration/targets/iosxr_acls/tests/cli/_remove_config.yaml new file mode 100644 index 00000000000..8ef7d1c3e58 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/_remove_config.yaml @@ -0,0 +1,14 @@ +--- +- name: Remove ACLs + cli_config: + config: "{{ lines }}" + vars: + lines: | + no ipv4 access-list acl_1 + no ipv4 access-list acl_2 + no ipv4 access-list acl_3 + no ipv4 access-list acl_3 + no ipv6 access-list acl6_1 + no ipv6 access-list acl6_2 + no ipv6 access-list acl6_3 + ignore_errors: yes diff --git a/test/integration/targets/iosxr_acls/tests/cli/deleted.yaml b/test/integration/targets/iosxr_acls/tests/cli/deleted.yaml new file mode 100644 index 00000000000..b3cc2d7a0b7 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/deleted.yaml @@ -0,0 +1,116 @@ +--- +- debug: + msg: "Start iosxr_lag_interfaces deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Delete a single ACE + iosxr_acls: &deleted_1 + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 23 + state: deleted + register: result + + - assert: + that: + - '"ipv4 access-list acl_1" in result.commands' + - '"no 23" in result.commands' + - "result.commands|length == 2" + + - name: Delete a single ACE (IDEMPOTENT) + iosxr_acls: *deleted_1 + register: result + + - name: Assert that the previous task was idempotent + assert: &unchanged + that: + - "result.changed == false" + - "result.commands|length == 0" + + - name: Delete a single ACL + iosxr_acls: &deleted_2 + config: + - afi: ipv6 + acls: + - name: acl6_1 + state: deleted + register: result + + - assert: + that: + - '"no ipv6 access-list acl6_1" in result.commands' + - "result.commands|length == 1" + + - name: Delete a single ACL (IDEMPOTENT) + iosxr_acls: *deleted_2 + register: result + + - name: Assert that the previous task was idempotent + assert: *unchanged + + - name: Delete all ACLs under one AFI + iosxr_acls: &deleted_3 + config: + - afi: ipv4 + state: deleted + register: result + + - assert: + that: + - '"no ipv4 access-list acl_1" in result.commands' + - '"no ipv4 access-list acl_2" in result.commands' + - "result.commands|length == 2" + + - name: Delete all ACLs under one AFI (IDEMPOTENT) + iosxr_acls: *deleted_3 + register: result + + - name: Assert that the previous task was idempotent + assert: *unchanged + + - include_tasks: _populate_config.yaml + + - name: Delete all ACLs from the device + iosxr_acls: &deleted_4 + state: deleted + register: result + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that the correct set of commands were generated + assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that the after dicts were correctly generated + assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Delete all ACLs from the device (IDEMPOTENT) + iosxr_lag_interfaces: *deleted_4 + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/tests/cli/empty_config.yaml b/test/integration/targets/iosxr_acls/tests/cli/empty_config.yaml new file mode 100644 index 00000000000..085a603fd80 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/empty_config.yaml @@ -0,0 +1,57 @@ +- debug: + msg: "START iosxr_acls empty_config integration tests on connection={{ ansible_connection }}" + +- name: Merged with empty config should give appropriate error message + iosxr_acls: + config: + state: merged + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state merged' + +- name: Replaced with empty config should give appropriate error message + iosxr_acls: + config: + state: replaced + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state replaced' + +- name: Overridden with empty config should give appropriate error message + iosxr_acls: + config: + state: overridden + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state overridden' + +- name: Rendered with empty config should give appropriate error message + iosxr_acls: + config: + state: rendered + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state rendered' + +- name: Parsed with empty config should give appropriate error message + iosxr_acls: + running_config: + state: parsed + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' diff --git a/test/integration/targets/iosxr_acls/tests/cli/fixtures/parsed.cfg b/test/integration/targets/iosxr_acls/tests/cli/fixtures/parsed.cfg new file mode 100644 index 00000000000..31155e23783 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/fixtures/parsed.cfg @@ -0,0 +1,8 @@ +ipv4 access-list acl_1 + 10 remark TEST_ACL_2_REMARK +ipv4 access-list acl_2 + 11 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log + 21 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts +ipv6 access-list acl6_1 + 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log + 20 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts diff --git a/test/integration/targets/iosxr_acls/tests/cli/gathered.yaml b/test/integration/targets/iosxr_acls/tests/cli/gathered.yaml new file mode 100644 index 00000000000..0c287726744 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: "START iosxr_acls gathered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Gather ACL interfaces facts using gathered state + iosxr_acls: + state: gathered + register: result + + - name: Assert that facts were correctly generated + assert: + that: "{{ merged['after'] | symmetric_difference(result['gathered']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/tests/cli/merged.yaml b/test/integration/targets/iosxr_acls/tests/cli/merged.yaml new file mode 100644 index 00000000000..0462502cec9 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/merged.yaml @@ -0,0 +1,168 @@ +--- +- debug: + msg: "START iosxr_acls merged integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge the provided configuration with the exisiting running configuration + iosxr_acls: &merged + config: + - afi: ipv6 + acls: + - name: acl6_1 + aces: + - sequence: 10 + grant: deny + protocol: tcp + source: + prefix: 2001:db8:1234::/48 + port_protocol: + range: + start: ftp + end: telnet + destination: + any: True + protocol_options: + tcp: + syn: True + ttl: + range: + start: 180 + end: 250 + routing: True + authen: True + log: True + + - sequence: 20 + grant: permit + protocol: icmpv6 + source: + any: True + destination: + any: True + protocol_options: + icmpv6: + router_advertisement: True + precedence: network + destopts: True + + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 16 + remark: TEST_ACL_1_REMARK + + - sequence: 21 + grant: permit + protocol: tcp + source: + host: 192.0.2.10 + port_protocol: + range: + start: pop3 + end: 121 + destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + protocol_options: + tcp: + rst: True + + - sequence: 23 + grant: deny + protocol: icmp + source: + any: True + destination: + prefix: 198.51.100.0/28 + protocol_options: + icmp: + reassembly_timeout: True + dscp: + lt: af12 + + - name: acl_2 + aces: + - sequence: 10 + remark: TEST_ACL_2_REMARK + state: merged + register: result + + - name: Assert that before dicts were correctly generated + assert: + that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that after dicts was correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + iosxr_acls: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dicts were correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Update existing ACEs + iosxr_acls: &update + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 21 + source: + prefix: 198.51.100.32/28 + port_protocol: + range: + start: pop3 + end: 121 + protocol_options: + tcp: + syn: True + + - sequence: 23 + protocol_options: + icmp: + router_advertisement: True + dscp: + eq: af23 + state: merged + register: result + + - name: Assert that the correct set of commands were generated + assert: + that: + - '"ipv4 access-list acl_1" in result.commands' + - '"21 permit tcp 198.51.100.32 0.0.0.15 range pop3 121 198.51.100.0 0.0.0.15 syn" in result.commands' + - '"23 deny icmp any 198.51.100.0 0.0.0.15 router-advertisement dscp eq af23" in result.commands' + - "result.commands|length == 3" + + - name: Update existing ACEs (IDEMPOTENT) + iosxr_acls: *update + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/tests/cli/overridden.yaml b/test/integration/targets/iosxr_acls/tests/cli/overridden.yaml new file mode 100644 index 00000000000..2e4c3cc0042 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/overridden.yaml @@ -0,0 +1,68 @@ +--- +- debug: + msg: "START iosxr_acls overridden integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Overridde all ACLs configuration with provided configuration + iosxr_acls: &overridden + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 10 + grant: permit + source: + any: True + destination: + any: True + protocol: tcp + + - name: acl_2 + aces: + - sequence: 20 + grant: permit + source: + any: True + destination: + any: True + protocol: igmp + state: overridden + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Overridde all interface LAG interface configuration with provided configuration (IDEMPOTENT) + iosxr_acls: *overridden + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dict is correctly generated + assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/tests/cli/parsed.yaml b/test/integration/targets/iosxr_acls/tests/cli/parsed.yaml new file mode 100644 index 00000000000..5e6b4609fa9 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/parsed.yaml @@ -0,0 +1,14 @@ +--- +- debug: + msg: "START iosxr_acls parsed integration tests on connection={{ ansible_connection }}" + +- name: Parse externally provided ACL config to agnostic model + iosxr_acls: + running_config: "{{ lookup('file', './fixtures/parsed.cfg') }}" + state: parsed + register: result + +- name: Assert that config was correctly parsed + assert: + that: + - "{{ parsed | symmetric_difference(result['parsed']) |length == 0 }}" diff --git a/test/integration/targets/iosxr_acls/tests/cli/rendered.yaml b/test/integration/targets/iosxr_acls/tests/cli/rendered.yaml new file mode 100644 index 00000000000..d5dd00ce7b9 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/rendered.yaml @@ -0,0 +1,93 @@ +--- +- debug: + msg: "START iosxr_acls rendered integration tests on connection={{ ansible_connection }}" + +- name: Render platform specific commands from task input using rendered state + iosxr_acls: + config: + - afi: ipv6 + acls: + - name: acl6_1 + aces: + - sequence: 10 + grant: deny + protocol: tcp + source: + prefix: 2001:db8:1234::/48 + port_protocol: + range: + start: ftp + end: telnet + destination: + any: True + protocol_options: + tcp: + syn: True + ttl: + range: + start: 180 + end: 250 + routing: True + authen: True + log: True + + - sequence: 20 + grant: permit + protocol: icmpv6 + source: + any: True + destination: + any: True + protocol_options: + icmpv6: + router_advertisement: True + precedence: network + destopts: True + + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 16 + remark: TEST_ACL_1_REMARK + + - sequence: 21 + grant: permit + protocol: tcp + source: + host: 192.0.2.10 + port_protocol: + range: + start: pop3 + end: 121 + destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + protocol_options: + tcp: + rst: True + + - sequence: 23 + grant: deny + protocol: icmp + source: + any: True + destination: + prefix: 198.51.100.0/28 + protocol_options: + icmp: + reassembly_timeout: True + dscp: + lt: af12 + + - name: acl_2 + aces: + - sequence: 10 + remark: TEST_ACL_2_REMARK + state: rendered + register: result + +- name: Assert that correct set of commands were rendered + assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['rendered']) |length == 0 }}" diff --git a/test/integration/targets/iosxr_acls/tests/cli/replaced.yaml b/test/integration/targets/iosxr_acls/tests/cli/replaced.yaml new file mode 100644 index 00000000000..d9137908e4d --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/replaced.yaml @@ -0,0 +1,68 @@ +--- +- debug: + msg: "START iosxr_acl_interfaces replaced integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Replace device configurations of listed ACL with provided configurations + iosxr_acls: &replaced + config: + - afi: ipv4 + acls: + - name: acl_2 + aces: + - sequence: 11 + grant: permit + protocol: igmp + source: + host: 198.51.100.130 + destination: + any: True + ttl: + eq: 100 + + - sequence: 12 + grant: deny + source: + any: True + destination: + any: True + protocol: icmp + state: replaced + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT) + iosxr_acls: *replaced + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dict is correctly generated + assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/tests/cli/rtt.yaml b/test/integration/targets/iosxr_acls/tests/cli/rtt.yaml new file mode 100644 index 00000000000..dd3044dcba5 --- /dev/null +++ b/test/integration/targets/iosxr_acls/tests/cli/rtt.yaml @@ -0,0 +1,85 @@ +--- +- debug: + msg: "START iosxr_acls round trip integration tests on connection={{ ansible_connection }}" + +- block: + - include_tasks: _remove_config.yaml + + - name: Apply the provided configuration (base config) + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_2 + aces: + - sequence: 11 + grant: permit + protocol: igmp + source: + host: 198.51.100.130 + destination: + any: True + ttl: + eq: 100 + + - sequence: 12 + grant: deny + source: + any: True + destination: + any: True + protocol: icmp + state: merged + register: base_config + + - name: Gather interfaces facts + iosxr_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: + - acls + + - name: Apply the provided configuration (config to be reverted) + iosxr_acls: + config: + - afi: ipv4 + acls: + - name: acl_1 + aces: + - sequence: 10 + grant: permit + source: + any: True + destination: + any: True + protocol: tcp + + - name: acl_2 + aces: + - sequence: 20 + grant: permit + source: + any: True + destination: + any: True + protocol: igmp + state: overridden + register: result + + - name: Assert that changes were applied + assert: + that: "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Revert back to base config using facts round trip + iosxr_acls: + config: "{{ ansible_facts['network_resources']['acls'] }}" + state: overridden + register: revert + + - name: Assert that config was reverted + assert: + that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_acls/vars/main.yaml b/test/integration/targets/iosxr_acls/vars/main.yaml new file mode 100644 index 00000000000..651b1bc912a --- /dev/null +++ b/test/integration/targets/iosxr_acls/vars/main.yaml @@ -0,0 +1,338 @@ +--- +merged: + before: [] + + commands: + - ipv6 access-list acl6_1 + - 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log + - 20 permit icmpv6 any any router-advertisement precedence network destopts + - ipv4 access-list acl_1 + - 16 remark TEST_ACL_1_REMARK + - 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst + - 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12 + - ipv4 access-list acl_2 + - 10 remark TEST_ACL_2_REMARK + + after: + - acls: + - aces: + - remark: TEST_ACL_1_REMARK + sequence: 16 + + - destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + grant: permit + protocol: tcp + protocol_options: + tcp: + rst: true + sequence: 21 + source: + host: 192.0.2.10 + port_protocol: + range: + end: '121' + start: pop3 + + - destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + dscp: + lt: af12 + grant: deny + protocol: icmp + protocol_options: + icmp: + reassembly_timeout: true + sequence: 23 + source: + any: true + name: acl_1 + + - aces: + - remark: TEST_ACL_2_REMARK + sequence: 10 + name: acl_2 + afi: ipv4 + + - acls: + - aces: + - authen: true + destination: + any: true + grant: deny + log: true + protocol: tcp + protocol_options: + tcp: + syn: true + routing: true + sequence: 10 + source: + port_protocol: + range: + end: telnet + start: ftp + prefix: 2001:db8:1234::/48 + ttl: + range: + end: 250 + start: 180 + + - destination: + any: true + destopts: true + grant: permit + precedence: network + protocol: icmpv6 + protocol_options: + icmpv6: + router_advertisement: true + sequence: 20 + source: + any: true + name: acl6_1 + afi: ipv6 + + +replaced: + commands: + - ipv4 access-list acl_2 + - no 10 + - 11 permit igmp host 198.51.100.130 any ttl eq 100 + - 12 deny icmp any any + + after: + - acls: + - aces: + - remark: TEST_ACL_1_REMARK + sequence: 16 + - destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + grant: permit + protocol: tcp + protocol_options: + tcp: + rst: true + sequence: 21 + source: + host: 192.0.2.10 + port_protocol: + range: + end: '121' + start: pop3 + - destination: + address: 198.51.100.0 + wildcard_bits: 0.0.0.15 + dscp: + lt: af12 + grant: deny + protocol: icmp + protocol_options: + icmp: + reassembly_timeout: true + sequence: 23 + source: + any: true + name: acl_1 + + - aces: + - destination: + any: true + grant: permit + protocol: igmp + sequence: 11 + source: + host: 198.51.100.130 + ttl: + eq: 100 + - destination: + any: true + grant: deny + protocol: icmp + sequence: 12 + source: + any: true + name: acl_2 + afi: ipv4 + + - acls: + - aces: + - authen: true + destination: + any: true + grant: deny + log: true + protocol: tcp + protocol_options: + tcp: + syn: true + routing: true + sequence: 10 + source: + port_protocol: + range: + end: telnet + start: ftp + prefix: 2001:db8:1234::/48 + ttl: + range: + end: 250 + start: 180 + + - destination: + any: true + destopts: true + grant: permit + precedence: network + protocol: icmpv6 + protocol_options: + icmpv6: + router_advertisement: true + sequence: 20 + source: + any: true + name: acl6_1 + afi: ipv6 + +overridden: + commands: + - no ipv6 access-list acl6_1 + - ipv4 access-list acl_1 + - no 16 + - no 21 + - no 23 + - 10 permit tcp any any + - ipv4 access-list acl_2 + - no 10 + - 20 permit igmp any any + + after: + - acls: + - aces: + - destination: + any: true + grant: permit + protocol: tcp + sequence: 10 + source: + any: true + name: acl_1 + + - aces: + - destination: + any: true + grant: permit + protocol: igmp + sequence: 20 + source: + any: true + name: acl_2 + afi: ipv4 + +deleted: + commands: + - no ipv4 access-list acl_1 + - no ipv4 access-list acl_2 + - no ipv6 access-list acl6_1 + + after: [] + +parsed: + - acls: + - aces: + - remark: TEST_ACL_2_REMARK + sequence: 10 + name: acl_1 + - aces: + - authen: true + destination: + any: true + grant: deny + log: true + protocol: tcp + protocol_options: + tcp: + syn: true + routing: true + sequence: 11 + source: + port_protocol: + range: + end: telnet + start: ftp + prefix: 2001:db8:1234::/48 + ttl: + range: + end: 250 + start: 180 + - destination: + any: true + destopts: true + grant: permit + packet_length: + eq: 576 + precedence: network + protocol: icmpv6 + protocol_options: + icmpv6: + router_advertisement: true + sequence: 21 + source: + any: true + name: acl_2 + afi: ipv4 + - acls: + - aces: + - authen: true + destination: + any: true + grant: deny + log: true + protocol: tcp + protocol_options: + tcp: + syn: true + routing: true + sequence: 10 + source: + port_protocol: + range: + end: telnet + start: ftp + prefix: 2001:db8:1234::/48 + ttl: + range: + end: 250 + start: 180 + - destination: + any: true + destopts: true + grant: permit + packet_length: + eq: 576 + precedence: network + protocol: icmpv6 + protocol_options: + icmpv6: + router_advertisement: true + sequence: 20 + source: + any: true + name: acl6_1 + afi: ipv6 + +round_trip: + after: + - members: + - member: GigabitEthernet0/0/0/8 + mode: passive + - member: GigabitEthernet0/0/0/9 + mode: active + name: Bundle-Ether10 + + - mode: active + name: Bundle-Ether11 + \ No newline at end of file diff --git a/test/units/modules/network/iosxr/fixtures/iosxr_acls_config.cfg b/test/units/modules/network/iosxr/fixtures/iosxr_acls_config.cfg new file mode 100644 index 00000000000..d9c32dd691e --- /dev/null +++ b/test/units/modules/network/iosxr/fixtures/iosxr_acls_config.cfg @@ -0,0 +1,5 @@ +ipv4 access-list acl_2 + 10 deny ipv4 any any + 20 permit tcp host 192.168.1.100 any +ipv6 access-list acl6_1 + 10 deny icmpv6 any any diff --git a/test/units/modules/network/iosxr/test_iosxr_acls.py b/test/units/modules/network/iosxr/test_iosxr_acls.py new file mode 100644 index 00000000000..39058210f64 --- /dev/null +++ b/test/units/modules/network/iosxr/test_iosxr_acls.py @@ -0,0 +1,247 @@ +# +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.iosxr import iosxr_acls +from units.modules.utils import set_module_args +from .iosxr_module import TestIosxrModule, load_fixture +import itertools + + +class TestIosxrAclsModule(TestIosxrModule): + module = iosxr_acls + + def setUp(self): + super(TestIosxrAclsModule, self).setUp() + + self.mock_get_config = patch( + 'ansible.module_utils.network.common.network.Config.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + 'ansible.module_utils.network.common.network.Config.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_get_resource_connection_config = patch( + 'ansible.module_utils.network.common.cfg.base.get_resource_connection' + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start( + ) + + self.mock_get_resource_connection_facts = patch( + 'ansible.module_utils.network.common.facts.facts.get_resource_connection' + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start( + ) + + self.mock_execute_show_command = patch( + 'ansible.module_utils.network.iosxr.facts.acls.acls.AclsFacts.get_device_data' + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosxrAclsModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None): + def load_from_file(*args, **kwargs): + return load_fixture('iosxr_acls_config.cfg') + + self.execute_show_command.side_effect = load_from_file + + def test_iosxr_acls_merged(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_1", + aces=[ + dict(sequence="10", + grant="permit", + protocol="ospf", + source=dict(prefix="192.168.1.0/24"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], + state="merged")) + commands = [ + 'ipv4 access-list acl_1', + '10 permit ospf 192.168.1.0 0.0.0.255 any log' + ] + result = self.execute_module(changed=True, commands=commands) + + def test_iosxr_acls_merged_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(sequence="10", + grant="deny", + protocol='ipv4', + destination=dict(any='true'), + source=dict(any="true")), + ]) + ]) + ], + state="merged")) + result = self.execute_module(changed=False, commands=[]) + + def test_iosxr_acls_replaced(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(sequence="30", + grant="permit", + protocol="ospf", + source=dict(prefix="10.0.0.0/8"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], + state="replaced")) + commands = [ + 'ipv4 access-list acl_2', 'no 10', 'no 20', + '30 permit ospf 10.0.0.0 0.255.255.255 any log' + ] + result = self.execute_module(changed=True, commands=commands) + + def test_iosxr_acls_replaced_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(sequence="10", + grant="deny", + protocol='ipv4', + destination=dict(any='true'), + source=dict(any="true")), + dict(sequence="20", + grant="permit", + protocol="tcp", + destination=dict(any='true'), + source=dict(host="192.168.1.100")), + ]) + ]) + ], + state="replaced")) + result = self.execute_module(changed=False, commands=[]) + + def test_iosxr_acls_overridden(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(sequence="40", + grant="permit", + protocol="ospf", + source=dict(any="true"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], + state="overridden")) + commands = [ + 'no ipv6 access-list acl6_1', 'ipv4 access-list acl_2', 'no 10', + 'no 20', '40 permit ospf any any log' + ] + result = self.execute_module(changed=True, commands=commands) + + def test_iosxr_acls_overridden_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(sequence="10", + grant="deny", + protocol='ipv4', + destination=dict(any='true'), + source=dict(any="true")), + dict(sequence="20", + grant="permit", + protocol="tcp", + destination=dict(any='true'), + source=dict(host="192.168.1.100")), + ]) + ]), + dict(afi='ipv6', + acls=[ + dict(name="acl6_1", + aces=[ + dict( + sequence="10", + grant="deny", + protocol="icmpv6", + destination=dict(any='true'), + source=dict(any='true'), + ) + ]) + ]) + ], + state="overridden")) + result = self.execute_module(changed=False, commands=[]) + + def test_iosxr_acls_deletedaces(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[dict(name="acl_2", aces=[dict(sequence="20")])]) + ], + state="deleted")) + commands = ['ipv4 access-list acl_2', 'no 20'] + result = self.execute_module(changed=True, commands=commands) + + def test_iosxr_acls_deletedacls(self): + set_module_args( + dict(config=[dict(afi="ipv6", acls=[dict(name="acl6_1")])], + state="deleted")) + commands = ['no ipv6 access-list acl6_1'] + result = self.execute_module(changed=True, commands=commands) + + def test_iosxr_acls_deletedafis(self): + set_module_args(dict(config=[dict(afi="ipv4")], state="deleted")) + commands = ['no ipv4 access-list acl_2'] + result = self.execute_module(changed=True, commands=commands) + + def test_eos_acls_rendered(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="acl_2", + aces=[ + dict(grant="permit", + source=dict(any="true"), + destination=dict(any="true"), + protocol='igmp') + ]) + ]) + ], + state="rendered")) + commands = ['ipv4 access-list acl_2', 'permit igmp any any'] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), sorted(commands), + result['rendered'])