From 7307339a7e6ae26baae0348ecf5ab6672ace364b Mon Sep 17 00:00:00 2001 From: Adharsh Srivats R Date: Mon, 2 Mar 2020 06:19:28 -0500 Subject: [PATCH] NX-OS ACLs module (#67558) * Added nxos_acls module * Adding tests * Added integration tests * Integration tests update * Updated documentation * Replaced state changes * Added warning detection * Added port-protocol mapping * Added change * Merge update changes * Completed integration tests, rtt * Added unit tests * Linting Added metaclass info * Changed port protocol to str * Fixed shippable errors, added examples * Fixed type error, updated examples --- .../network/nxos/argspec/acls/__init__.py | 0 .../network/nxos/argspec/acls/acls.py | 425 +++++++++ .../network/nxos/config/acls/__init__.py | 0 .../network/nxos/config/acls/acls.py | 690 +++++++++++++++ .../network/nxos/facts/acls/__init__.py | 0 .../network/nxos/facts/acls/acls.py | 236 +++++ .../module_utils/network/nxos/facts/facts.py | 2 + lib/ansible/modules/network/nxos/nxos_acls.py | 825 ++++++++++++++++++ .../targets/nxos_acls/defaults/main.yaml | 2 + .../targets/nxos_acls/meta/main.yml | 2 + .../targets/nxos_acls/tasks/cli.yaml | 20 + .../targets/nxos_acls/tasks/main.yaml | 2 + .../targets/nxos_acls/tests/cli/deleted.yml | 69 ++ .../targets/nxos_acls/tests/cli/gathered.yml | 34 + .../targets/nxos_acls/tests/cli/merged.yml | 108 +++ .../nxos_acls/tests/cli/overridden.yml | 99 +++ .../targets/nxos_acls/tests/cli/parsed.yml | 45 + .../nxos_acls/tests/cli/populate_config.yaml | 15 + .../nxos_acls/tests/cli/remove_config.yaml | 9 + .../targets/nxos_acls/tests/cli/rendered.yml | 56 ++ .../targets/nxos_acls/tests/cli/replaced.yml | 65 ++ .../targets/nxos_acls/tests/cli/rtt.yml | 87 ++ .../modules/network/nxos/test_nxos_acls.py | 370 ++++++++ 23 files changed, 3161 insertions(+) create mode 100644 lib/ansible/module_utils/network/nxos/argspec/acls/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/argspec/acls/acls.py create mode 100644 lib/ansible/module_utils/network/nxos/config/acls/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/config/acls/acls.py create mode 100644 lib/ansible/module_utils/network/nxos/facts/acls/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/facts/acls/acls.py create mode 100644 lib/ansible/modules/network/nxos/nxos_acls.py create mode 100644 test/integration/targets/nxos_acls/defaults/main.yaml create mode 100644 test/integration/targets/nxos_acls/meta/main.yml create mode 100644 test/integration/targets/nxos_acls/tasks/cli.yaml create mode 100644 test/integration/targets/nxos_acls/tasks/main.yaml create mode 100644 test/integration/targets/nxos_acls/tests/cli/deleted.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/gathered.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/merged.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/overridden.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/parsed.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/populate_config.yaml create mode 100644 test/integration/targets/nxos_acls/tests/cli/remove_config.yaml create mode 100644 test/integration/targets/nxos_acls/tests/cli/rendered.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/replaced.yml create mode 100644 test/integration/targets/nxos_acls/tests/cli/rtt.yml create mode 100644 test/units/modules/network/nxos/test_nxos_acls.py diff --git a/lib/ansible/module_utils/network/nxos/argspec/acls/__init__.py b/lib/ansible/module_utils/network/nxos/argspec/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/argspec/acls/acls.py b/lib/ansible/module_utils/network/nxos/argspec/acls/acls.py new file mode 100644 index 00000000000..01e13f18936 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/argspec/acls/acls.py @@ -0,0 +1,425 @@ +# +# -*- 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 nxos_acls module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class AclsArgs(object): # pylint: disable=R0903 + """The arg spec for the nxos_acls module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'acls': { + 'elements': 'dict', + 'options': { + 'aces': { + 'elements': 'dict', + 'mutually_exclusive': [['grant', 'remark']], + 'options': { + 'destination': { + 'mutually_exclusive': + [['address', 'any', 'host', 'prefix'], + [ + 'wildcard_bits', 'any', 'host', + 'prefix' + ]], + 'options': { + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'host': { + 'type': 'str' + }, + 'port_protocol': { + 'mutually_exclusive': [[ + 'eq', 'lt', 'neq', 'gt', + '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' + }, + 'prefix': { + 'type': 'str' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': + [['address', 'wildcard_bits']], + 'type': 'dict' + }, + 'dscp': { + 'type': 'str' + }, + 'fragments': { + 'type': 'bool' + }, + 'grant': { + 'choices': ['permit', 'deny'], + 'type': 'str' + }, + 'log': { + 'type': 'bool' + }, + 'precedence': { + 'type': 'str' + }, + 'protocol': { + 'type': 'str' + }, + 'protocol_options': { + 'mutually_exclusive': + [['icmp', 'igmp', 'tcp']], + 'options': { + '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' + }, + 'message_code': { + 'type': 'int' + }, + 'message_type': { + 'type': 'int' + }, + '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': { + 'mutually_exclusive': [[ + 'dvmrp', 'host_query', + 'host_report' + ]], + 'options': { + 'dvmrp': { + 'type': 'bool' + }, + 'host_query': { + 'type': 'bool' + }, + 'host_report': { + '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', 'host', 'any', + 'prefix' + ]], + 'options': { + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'host': { + 'type': 'str' + }, + 'port_protocol': { + 'mutually_exclusive': + [['eq', 'lt', 'neq', 'range'], + ['eq', 'gt', 'neq', 'range']], + 'options': { + 'eq': { + 'type': 'str' + }, + 'gt': { + 'type': 'str' + }, + 'lt': { + 'type': 'str' + }, + 'neq': { + 'type': 'str' + }, + 'range': { + 'options': { + 'end': { + 'type': 'str' + }, + 'start': { + 'type': 'str' + } + }, + 'type': 'dict' + } + }, + 'type': + 'dict' + }, + 'prefix': { + 'type': 'str' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': + [['address', 'wildcard_bits']], + 'type': + 'dict' + } + }, + 'type': 'list' + }, + 'name': { + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'running_config': { + 'type': 'str' + }, + 'state': { + 'choices': [ + 'deleted', 'gathered', 'merged', 'overridden', 'rendered', + 'replaced', 'parsed' + ], + 'default': + 'merged', + 'type': + 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/nxos/config/acls/__init__.py b/lib/ansible/module_utils/network/nxos/config/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/config/acls/acls.py b/lib/ansible/module_utils/network/nxos/config/acls/acls.py new file mode 100644 index 00000000000..37c8b3d23a9 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/config/acls/acls.py @@ -0,0 +1,690 @@ +# +# -*- 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 nxos_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 + +import socket +import re +from copy import deepcopy +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list, remove_empties, dict_diff +from ansible.module_utils.network.nxos.facts.facts import Facts +from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.nxos.utils.utils import flatten_dict, search_obj_in_list, get_interface_type, normalize_interface + + +class Acls(ConfigBase): + """ + The nxos_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 edit_config(self, commands): + """Wrapper method for `_connection.edit_config()` + This exists solely to allow the unit test framework to mock device connection calls. + """ + return self._connection.edit_config(commands) + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + state = self._module.params['state'] + action_states = ['merged', 'replaced', 'deleted', 'overridden'] + + if state == 'gathered': + result['gathered'] = self.get_acls_facts() + elif state == 'rendered': + result['rendered'] = self.set_config({}) + elif state == 'parsed': + result['parsed'] = self.set_config({}) + else: + existing_acls_facts = self.get_acls_facts() + commands.extend(self.set_config(existing_acls_facts)) + if commands and state in action_states: + if not self._module.check_mode: + self._connection.edit_config(commands) + result['changed'] = True + result['before'] = existing_acls_facts + result['commands'] = commands + + changed_acls_facts = self.get_acls_facts() + if result['changed']: + result['after'] = 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 + """ + config = self._module.params['config'] + want = [] + if config: + for w in config: + want.append(remove_empties(w)) + have = existing_acls_facts + if want: + want = self.convert_values(want) + resp = self.set_state(want, have) + return to_list(resp) + + def convert_values(self, want): + ''' + This method is used to map and convert the user given values with what will actually be present in the device configuation + ''' + port_protocol = { + 515: 'lpd', + 517: 'talk', + 7: 'echo', + 9: 'discard', + 12: 'exec', + 13: 'login', + 14: 'cmd', + 109: 'pop2', + 19: 'chargen', + 20: 'ftp-data', + 21: 'ftp', + 23: 'telnet', + 25: 'smtp', + 540: 'uucp', + 543: 'klogin', + 544: 'kshell', + 37: 'time', + 43: 'whois', + 49: 'tacacs', + 179: 'bgp', + 53: 'domain', + 194: 'irc', + 70: 'gopher', + 79: 'finger', + 80: 'www', + 101: 'hostname', + 3949: 'drip', + 110: 'pop3', + 111: 'sunrpc', + 496: 'pim-auto-rp', + 113: 'ident', + 119: 'nntp' + } + protocol = { + 1: 'icmp', + 2: 'igmp', + 4: 'ip', + 6: 'tcp', + 103: 'pim', + 108: 'pcp', + 47: 'gre', + 17: 'udp', + 50: 'esp', + 51: 'ahp', + 88: 'eigrp', + 89: 'ospf', + 94: 'nos' + } + precedence = { + 0: 'routine', + 1: 'priority', + 2: 'immediate', + 3: 'flash', + 4: 'flash-override', + 5: 'critical', + 6: 'internet', + 7: 'network' + } + dscp = { + 10: 'AF11', + 12: 'AF12', + 14: 'AF13', + 18: 'AF21', + 20: 'AF22', + 22: 'AF23', + 26: 'AF31', + 28: 'AF32', + 30: 'AF33', + 34: 'AF41', + 36: 'AF42', + 38: 'AF43', + 8: 'CS1', + 16: 'CS2', + 24: 'CS3', + 32: 'CS4', + 40: 'CS5', + 48: 'CS6', + 56: 'CS7', + 0: 'Default', + 46: 'EF' + } + # port_pro_num = list(protocol.keys()) + for afi in want: + if 'acls' in afi.keys(): + for acl in afi['acls']: + if 'aces' in acl.keys(): + for ace in acl['aces']: + if 'dscp' in ace.keys(): + if ace['dscp'].isdigit(): + ace['dscp'] = dscp[int(ace['dscp'])] + ace['dscp'] = ace['dscp'].lower() + if 'precedence' in ace.keys(): + if ace['precedence'].isdigit(): + ace['precedence'] = precedence[int( + ace['precedence'])] + if 'protocol' in ace.keys( + ) and ace['protocol'].isdigit() and int( + ace['protocol']) in protocol.keys(): + ace['protocol'] = protocol[int( + ace['protocol'])] + # convert number to name + if 'protocol' in ace.keys( + ) and ace['protocol'] in ['tcp', 'udp']: + for end in ['source', 'destination']: + if 'port_protocol' in ace[end].keys(): + key = list(ace[end] + ['port_protocol'].keys())[0] + # key could be eq,gt,lt,neq or range + if key != 'range': + val = ace[end]['port_protocol'][ + key] + if val.isdigit() and int(val) in port_protocol.keys( + ): + ace[end]['port_protocol'][ + key] = port_protocol[int( + val)] + else: + st = int(ace[end]['port_protocol'] + ['range']['start']) + + end = int(ace[end]['port_protocol'] + ['range']['end']) + + if st in port_protocol.keys(): + ace[end]['port_protocol'][ + 'range'][ + 'start'] = port_protocol[ + st] + if end in port_protocol.keys(): + ace[end]['port_protocol'][ + 'range'][ + 'end'] = port_protocol[ + end] + return want + + 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 == 'overridden': + commands = (self._state_overridden(want, have)) + elif state == 'deleted': + commands = (self._state_deleted(want, have)) + elif state == 'rendered': + commands = self._state_rendered(want) + elif state == 'parsed': + want = self._module.params['running_config'] + commands = self._state_parsed(want) + else: + for w in want: + if state == 'merged': + commands.extend(self._state_merged(w, have)) + elif state == 'replaced': + commands.extend(self._state_replaced(w, have)) + if state != 'parsed': + commands = [c.strip() for c in commands] + return commands + + def _state_parsed(self, want): + return self.get_acls_facts(want) + + def _state_rendered(self, want): + commands = [] + for w in want: + commands.extend(self.set_commands(w, {})) + 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 = [] + have_afi = search_obj_in_list(want['afi'], have, 'afi') + del_dict = {'acls': []} + want_names = [] + if have_afi != want: + if have_afi: + del_dict.update({'afi': have_afi['afi'], 'acls': []}) + if want.get('acls'): + want_names = [w['name'] for w in want['acls']] + have_names = [h['name'] for h in have_afi['acls']] + want_acls = want.get('acls') + for w in want_acls: + acl_commands = [] + if w['name'] not in have_names: + # creates new ACL in replaced state + merge_dict = {'afi': want['afi'], 'acls': [w]} + commands.extend( + self._state_merged(merge_dict, have)) + else: + # acl in want exists in have + have_name = search_obj_in_list( + w['name'], have_afi['acls'], 'name') + have_aces = have_name.get('aces') if have_name.get( + 'aces') else [] + merge_aces = [] + del_aces = [] + w_aces = w.get('aces') if w.get('aces') else [] + + for ace in have_aces: + if ace not in w_aces: + del_aces.append(ace) + for ace in w_aces: + if ace not in have_aces: + merge_aces.append(ace) + merge_dict = { + 'afi': want['afi'], + 'acls': [{ + 'name': w['name'], + 'aces': merge_aces + }] + } + del_dict = { + 'afi': want['afi'], + 'acls': [{ + 'name': w['name'], + 'aces': del_aces + }] + } + if del_dict['acls']: + acl_commands.extend( + self._state_deleted([del_dict], have)) + acl_commands.extend( + self._state_merged(merge_dict, have)) + + for i in range(1, len(acl_commands)): + if acl_commands[i] == acl_commands[0]: + acl_commands[i] = '' + commands.extend(acl_commands) + else: + acls = [] + # no acls given in want, so delete all have acls + for acl in have_afi['acls']: + acls.append({'name': acl['name']}) + del_dict['acls'] = acls + if del_dict['acls']: + commands.extend(self._state_deleted([del_dict], have)) + + else: + # want_afi is not present in have + commands.extend(self._state_merged(want, have)) + + commands = list(filter(None, commands)) + 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 = [] + want_afi = [w['afi'] for w in want] + for h in have: + if h['afi'] in want_afi: + w = search_obj_in_list(h['afi'], want, 'afi') + for h_acl in h['acls']: + w_acl = search_obj_in_list(h_acl['name'], w['acls'], + 'name') + if not w_acl: + del_dict = { + 'afi': h['afi'], + 'acls': [{ + 'name': h_acl['name'] + }] + } + commands.extend(self._state_deleted([del_dict], have)) + else: + # if afi is not in want + commands.extend(self._state_deleted([{'afi': h['afi']}], have)) + for w in want: + commands.extend(self._state_replaced(w, have)) + 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 + """ + return self.set_commands(want, have) + + 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 want: # and have != want: + for w in want: + ip = 'ipv6' if w['afi'] == 'ipv6' else 'ip' + acl_names = [] + have_afi = search_obj_in_list(w['afi'], have, 'afi') + # if want['afi] not in have, ignore + if have_afi: + if w.get('acls'): + for acl in w['acls']: + if 'aces' in acl.keys(): + have_name = search_obj_in_list( + acl['name'], have_afi['acls'], 'name') + if have_name: + ace_commands = [] + flag = 0 + for ace in acl['aces']: + if list(ace.keys()) == ['sequence']: + # only sequence number is specified to be deleted + if 'aces' in have_name.keys(): + for h_ace in have_name['aces']: + if h_ace[ + 'sequence'] == ace[ + 'sequence']: + ace_commands.append( + 'no ' + + str(ace['sequence'] + )) + flag = 1 + else: + if 'aces' in have_name.keys(): + for h_ace in have_name['aces']: + # when want['ace'] does not have seq number + if 'sequence' not in ace.keys( + ): + del h_ace['sequence'] + if ace == h_ace: + ace_commands.append( + 'no ' + + self.process_ace( + ace)) + flag = 1 + if flag: + ace_commands.insert( + 0, + ip + ' access-list ' + acl['name']) + commands.extend(ace_commands) + else: + # only name given + for h in have_afi['acls']: + if h['name'] == acl['name']: + acl_names.append(acl['name']) + for name in acl_names: + commands.append('no ' + ip + ' access-list ' + + name) + + else: + # 'only afi is given' + if have_afi.get('acls'): + for h in have_afi['acls']: + acl_names.append(h['name']) + for name in acl_names: + commands.append('no ' + ip + ' access-list ' + + name) + else: + v6 = [] + v4 = [] + v6_local = v4_local = None + for h in have: + if h['afi'] == 'ipv6': + v6 = (acl['name'] for acl in h['acls']) + if 'match_local_traffic' in h.keys(): + v6_local = True + else: + v4 = (acl['name'] for acl in h['acls']) + if 'match_local_traffic' in h.keys(): + v4_local = True + + self.no_commands(v4, commands, v4_local, 'ip') + self.no_commands(v6, commands, v6_local, 'ipv6') + + for name in v6: + commands.append('no ipv6 access-list ' + name) + if v4_local: + commands.append('no ipv6 access-list match-local-traffic') + + return commands + + def no_commands(self, v_list, commands, match_local, ip): + for name in v_list: + commands.append('no ' + ip + ' access-list ' + name) + if match_local: + commands.append('no ' + ip + ' access-list match-local-traffic') + + def set_commands(self, want, have): + commands = [] + have_afi = search_obj_in_list(want['afi'], have, 'afi') + ip = '' + if 'v6' in want['afi']: + ip = 'ipv6 ' + else: + ip = 'ip ' + + if have_afi: + if want.get('acls'): + for w_acl in want['acls']: + have_acl = search_obj_in_list(w_acl['name'], + have_afi['acls'], 'name') + name = w_acl['name'] + flag = 0 + ace_commands = [] + if have_acl != w_acl: + if have_acl: + ace_list = [] + if w_acl.get('aces') and have_acl.get('aces'): + # case 1 --> sequence number not given in want --> new ace + # case 2 --> new sequence number in want --> new ace + # case 3 --> existing sequence number given --> update rule (only for merged state. + # For replaced and overridden, rule is deleted in the state's config) + + ace_list = [ + item for item in w_acl['aces'] + if 'sequence' not in item.keys() + ] # case 1 + + want_seq = [ + item['sequence'] for item in w_acl['aces'] + if 'sequence' in item.keys() + ] + + have_seq = [ + item['sequence'] + for item in have_acl['aces'] + ] + + new_seq = list(set(want_seq) - set(have_seq)) + common_seq = list( + set(want_seq).intersection(set(have_seq))) + + temp_list = [ + item for item in w_acl['aces'] + if 'sequence' in item.keys() + and item['sequence'] in new_seq + ] # case 2 + ace_list.extend(temp_list) + for w in w_acl['aces']: + self.argument_spec = AclsArgs.argument_spec + params = utils.validate_config( + self.argument_spec, { + 'config': [{ + 'afi': + want['afi'], + 'acls': [{ + 'name': name, + 'aces': ace_list + }] + }] + }) + if 'sequence' in w.keys( + ) and w['sequence'] in common_seq: + temp_obj = search_obj_in_list( + w['sequence'], have_acl['aces'], + 'sequence') # case 3 + if temp_obj != w: + for key, val in w.items(): + temp_obj[key] = val + ace_list.append(temp_obj) + if self._module.params[ + 'state'] == 'merged': + ace_commands.append( + 'no ' + str(w['sequence'])) + # remove existing rule to update it + elif w_acl.get('aces'): + # 'have' has ACL defined without any ACE + ace_list = [item for item in w_acl['aces']] + for w_ace in ace_list: + ace_commands.append( + self.process_ace(w_ace).strip()) + flag = 1 + + if flag: + ace_commands.insert(0, + ip + 'access-list ' + name) + + else: + commands.append(ip + 'access-list ' + name) + if 'aces' in w_acl.keys(): + for w_ace in w_acl['aces']: + commands.append( + self.process_ace(w_ace).strip()) + commands.extend(ace_commands) + else: + if want.get('acls'): + for w_acl in want['acls']: + name = w_acl['name'] + commands.append(ip + 'access-list ' + name) + if 'aces' in w_acl.keys(): + for w_ace in w_acl['aces']: + commands.append(self.process_ace(w_ace).strip()) + + return commands + + def process_ace(self, w_ace): + command = '' + ace_keys = w_ace.keys() + if 'remark' in ace_keys: + command += 'remark ' + w_ace['remark'] + ' ' + else: + command += w_ace['grant'] + ' ' + if 'protocol' in ace_keys: + command += w_ace['protocol'] + ' ' + src = self.get_address(w_ace['source'], w_ace['protocol']) + dest = self.get_address(w_ace['destination'], + w_ace['protocol']) + command += src + dest + if 'protocol_options' in ace_keys: + pro = list(w_ace['protocol_options'].keys())[0] + if pro != w_ace['protocol']: + self._module.fail_json( + msg='protocol and protocol_options mismatch') + flags = '' + for k in w_ace['protocol_options'][pro].keys(): + k = re.sub('_', '-', k) + flags += k + ' ' + command += flags + if 'dscp' in ace_keys: + command += 'dscp ' + w_ace['dscp'] + ' ' + if 'fragments' in ace_keys: + command += 'fragments ' + if 'precedence' in ace_keys: + command += 'precedence ' + w_ace['precedence'] + ' ' + if 'log' in ace_keys: + command += 'log ' + if 'sequence' in ace_keys: + command = str(w_ace['sequence']) + ' ' + command + return command + + def get_address(self, endpoint, pro=''): + ret_addr = '' + keys = list(endpoint.keys()) + if 'address' in keys: + if 'wildcard_bits' not in keys: + self._module.fail_json( + msg='wildcard bits not specified for address') + else: + ret_addr = endpoint['address'] + \ + ' ' + endpoint['wildcard_bits'] + ' ' + elif 'any' in keys: + ret_addr = 'any ' + elif 'host' in keys: + ret_addr = 'host ' + endpoint['host'] + ' ' + elif 'prefix' in keys: + ret_addr = endpoint['prefix'] + ' ' + + if pro in ['tcp', 'udp']: + if 'port_protocol' in keys: + options = self.get_options(endpoint['port_protocol']) + ret_addr += options + return ret_addr + + def get_options(self, item): + com = '' + subkey = list(item.keys()) + if 'range' in subkey: + com = 'range ' + item['range']['start'] + \ + ' ' + item['range']['end'] + ' ' + else: + com = subkey[0] + ' ' + item[subkey[0]] + ' ' + return com diff --git a/lib/ansible/module_utils/network/nxos/facts/acls/__init__.py b/lib/ansible/module_utils/network/nxos/facts/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/facts/acls/acls.py b/lib/ansible/module_utils/network/nxos/facts/acls/acls.py new file mode 100644 index 00000000000..e9b97b2456b --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/facts/acls/acls.py @@ -0,0 +1,236 @@ +# +# -*- 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 nxos 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 ansible.module_utils.network.common import utils +from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs + + +class AclsFacts(object): + """ The nxos 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 running-config | section 'ip(v6)* access-list'") + + 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) + data = re.split('\nip', data) + v6 = [] + v4 = [] + + for i in range(len(data)): + if str(data[i]): + if 'v6' in str(data[i]).split()[0]: + v6.append(data[i]) + else: + v4.append(data[i]) + + resources = [] + resources.append(v6) + resources.append(v4) + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('acls', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, + {'config': objs}) + params = utils.remove_empties(params) + facts['acls'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def get_endpoint(self, ace, pro): + ret_dict = {} + option = ace.split()[0] + if option == 'any': + ret_dict.update({'any': True}) + else: + # it could be a.b.c.d or a.b.c.d/x or a.b.c.d/32 + if '/' in option: # or 'host' in option: + ip = re.search(r'(.*)/(\d+)', option) + if int(ip.group(2)) < 32 or 32 < int(ip.group(2)) < 128: + ret_dict.update({'prefix': option}) + else: + ret_dict.update({'host': ip.group(1)}) + else: + ret_dict.update({'address': option}) + wb = ace.split()[1] + ret_dict.update({'wildcard_bits': wb}) + ace = re.sub('{0}'.format(wb), '', ace, 1) + ace = re.sub(option, '', ace, 1) + if pro in ['tcp', 'udp']: + keywords = ['eq', 'lt', 'gt', 'neq', 'range'] + if len(ace.split()) and ace.split()[0] in keywords: + port_protocol = {} + port_pro = re.search(r'(eq|lt|gt|neq) (\w*)', ace) + if port_pro: + port_protocol.update( + {port_pro.group(1): port_pro.group(2)}) + ace = re.sub(port_pro.group(1), '', ace, 1) + ace = re.sub(port_pro.group(2), '', ace, 1) + else: + limit = re.search(r'(range) (\w*) (\w*)', ace) + if limit: + port_protocol.update({ + 'range': { + 'start': limit.group(2), + 'end': limit.group(3) + } + }) + ace = re.sub(limit.group(2), '', ace, 1) + ace = re.sub(limit.group(3), '', ace, 1) + if port_protocol: + ret_dict.update({'port_protocol': port_protocol}) + return ace, ret_dict + + 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) + protocol_options = { + 'tcp': ['fin', 'established', 'psh', 'rst', 'syn', 'urg', 'ack'], + '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' + ], + 'igmp': ['dvmrp', 'host_query', 'host_report'], + } + if conf: + if 'v6' in conf[0].split()[0]: + config['afi'] = 'ipv6' + else: + config['afi'] = 'ipv4' + config['acls'] = [] + for acl in conf: + acls = {} + if 'match-local-traffic' in acl: + config['match_local_traffic'] = True + continue + acl = acl.split('\n') + acl = [a.strip() for a in acl] + acl = list(filter(None, acl)) + acls['name'] = re.match(r'(ip)?(v6)?\s?access-list (.*)', + acl[0]).group(3) + acls['aces'] = [] + for ace in list(filter(None, acl[1:])): + if re.search(r'ip(.*)access-list.*', ace): + break + entry = {} + ace = ace.strip() + seq = re.match(r'(\d*)', ace).group(0) + entry.update({'sequence': seq}) + ace = re.sub(seq, '', ace, 1) + grant = ace.split()[0] + rem = '' + if grant != 'remark': + entry.update({'grant': grant}) + else: + rem = re.match('.*remark (.*)', ace).group(1) + entry.update({'remark': rem}) + + if not rem: + ace = re.sub(grant, '', ace, 1) + pro = ace.split()[0] + entry.update({'protocol': pro}) + ace = re.sub(pro, '', ace, 1) + ace, source = self.get_endpoint(ace, pro) + entry.update({'source': source}) + ace, dest = self.get_endpoint(ace, pro) + entry.update({'destination': dest}) + + dscp = re.search(r'dscp (\w*)', ace) + if dscp: + entry.update({'dscp': dscp.group(1)}) + + frag = re.search(r'fragments', ace) + if frag: + entry.update({'fragments': True}) + + prec = re.search(r'precedence (\w*)', ace) + if prec: + entry.update({'precedence': prec.group(1)}) + + log = re.search('log', ace) + if log: + entry.update({'log': True}) + + if pro == 'tcp' or pro == 'icmp' or pro == 'igmp': + pro_options = {} + options = {} + for option in protocol_options[pro]: + option = re.sub('_', '-', option) + if option in ace: + option = re.sub('-', '_', option) + options.update({option: True}) + if options: + pro_options.update({pro: options}) + if pro_options: + entry.update({'protocol_options': pro_options}) + acls['aces'].append(entry) + config['acls'].append(acls) + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/nxos/facts/facts.py b/lib/ansible/module_utils/network/nxos/facts/facts.py index 9fab13c9ee3..301652b3855 100644 --- a/lib/ansible/module_utils/network/nxos/facts/facts.py +++ b/lib/ansible/module_utils/network/nxos/facts/facts.py @@ -24,6 +24,7 @@ from ansible.module_utils.network.nxos.facts.lacp_interfaces.lacp_interfaces imp from ansible.module_utils.network.nxos.facts.lldp_global.lldp_global import Lldp_globalFacts from ansible.module_utils.network.nxos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts from ansible.module_utils.network.nxos.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts +from ansible.module_utils.network.nxos.facts.acls.acls import AclsFacts FACT_LEGACY_SUBSETS = dict( @@ -48,6 +49,7 @@ FACT_RESOURCE_SUBSETS = dict( l2_interfaces=L2_interfacesFacts, lldp_interfaces=Lldp_interfacesFacts, acl_interfaces=Acl_interfacesFacts, + acls=AclsFacts, ) diff --git a/lib/ansible/modules/network/nxos/nxos_acls.py b/lib/ansible/modules/network/nxos/nxos_acls.py new file mode 100644 index 00000000000..82d25b96a34 --- /dev/null +++ b/lib/ansible/modules/network/nxos/nxos_acls.py @@ -0,0 +1,825 @@ +#!/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 nxos_acls +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +DOCUMENTATION = """ +--- +module: nxos_acls +version_added: '2.10' +short_description: Manage named IP ACLs on the Cisco NX-OS platform +description: Manage named IP ACLs on the Cisco NX-OS platform +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: + - Tested against NX-OS 7.3.(0)D1(1) on VIRL + - As NX-OS allows configuring a rule again with different sequence numbers, + the user is expected to provide sequence numbers for the access control entries to preserve idempotency. + If no sequence number is given, the rule will be added as a new rule by the device. + - To parse configuration text, provide the output of show running-config | section access-list or a mocked up config +options: + running_config: + description: + - Parse given commands into structured format. Required if I(state=parsed). + type: str + config: + description: A dictionary of ACL options. + type: list + elements: dict + suboptions: + afi: + description: The Address Family Indicator (AFI) for the ACL. + type: str + required: true + choices: ['ipv4', 'ipv6'] + acls: + description: A list of the ACLs. + type: list + elements: dict + suboptions: + name: + description: Name of the ACL. + type: str + required: true + aces: + description: The entries within the ACL. + type: list + elements: dict + suboptions: + grant: + description: Action to be applied on the rule. + type: str + choices: ['permit', 'deny'] + destination: + description: Specify the packet destination. + type: dict + suboptions: + address: + description: Destination network address. + type: str + any: + description: Any destination address. + type: bool + host: + description: Host IP address. + type: str + port_protocol: + description: Specify the destination port or protocol (only for TCP and UDP). + 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 + prefix: + description: Destination network prefix. Only for prefixes of value less than 31 for ipv4 and 127 for ipv6. + Prefixes of 32 (ipv4) and 128 (ipv6) should be given in the 'host' key. + type: str + wildcard_bits: + description: Destination wildcard bits. + type: str + + dscp: + description: Match packets with given DSCP value. + type: str + + fragments: + description: Check non-initial fragments. + type: bool + + remark: + description: Access list entry comment. + type: str + + sequence: + description: Sequence number. + type: int + + source: + description: Specify the packet source. + type: dict + suboptions: + address: + description: Source network address. + type: str + any: + description: Any source address. + type: bool + host: + description: Host IP address. + type: str + port_protocol: + description: Specify the destination port or protocol (only for TCP and UDP). + 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 + prefix: + description: Source network prefix. Only for prefixes of mask value less than 31 for ipv4 and 127 for ipv6. + Prefixes of mask 32 (ipv4) and 128 (ipv6) should be given in the 'host' key. + type: str + wildcard_bits: + description: Source wildcard bits. + type: str + + log: + description: Log matches against this entry. + type: bool + + precedence: + description: Match packets with given precedence value. + type: str + + protocol: + description: Specify the protocol. + type: str + + protocol_options: + description: All possible suboptions for the protocol chosen. + type: dict + suboptions: + icmp: + description: ICMP protocol options. + 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 + message_code: + description: ICMP message code + type: int + message_type: + description: ICMP message type + type: int + 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 exceeded. + 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: TCP 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: IGMP protocol options. + type: dict + suboptions: + dvmrp: + description: Distance Vector Multicast Routing Protocol + type: bool + host_query: + description: Host Query + type: bool + host_report: + description: Host Report + type: bool + + state: + description: + - The state the configuration should be left in + type: str + choices: + - deleted + - gathered + - merged + - overridden + - rendered + - replaced + - parsed + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# + +- name: Merge new ACLs configuration + nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 55 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + prefix: 2001:db8:12::/32 + protocol: sctp + state: merged + +# After state: +# ------------ +# +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any + +# Using replaced + +# Before state: +# ---------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Replace existing ACL configuration with provided configuration + nxos_acls: + config: + - afi: ipv4 + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - sequence: 20 + grant: permit + source: + any: true + destination: + any: true + protocol: pip + + - remark: Replaced ACE + + - name: ACL2v6 + state: replaced + +# After state: +# --------------- +# +# ipv6 access-list ACL1v6 +# 20 permit pip any any +# 30 remark Replaced ACE +# ipv6 access-list ACL2v6 + +# Using overridden + +# Before state: +# ---------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Override existing configuration with provided configuration + nxos_acls: + config: + - afi: ipv4 + acls: + - name: NewACL + aces: + - grant: deny + source: + address: 192.0.2.0 + wildcard_bits: 0.0.255.255 + destination: + any: true + protocol: eigrp + - remark: Example for overridden state + state: overridden + +# After state: +# ------------ +# +# ip access-list NewACL +# 10 deny eigrp 192.0.2.0 0.0.255.255 any +# 20 remark Example for overridden state + +# Using deleted: +# +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete all ACLs + nxos_acls: + config: + state: deleted + +# After state: +# ----------- +# + + +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete all ACLs in given AFI + nxos_acls: + config: + - afi: ipv4 + state: deleted + +# After state: +# ------------ +# +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + + + +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete specific ACLs + nxos_acls: + config: + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + any: true + protocol: sctp + + - sequence: 20 + state: deleted + +# After state: +# ------------ +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACl1v6 +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +# Using parsed + +- name: Parse given config to structured data + nxos_acls: + running_config: | + ip access-list ACL1v4 + 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin + ipv6 access-list ACL1v6 + 10 permit sctp any any + state: parsed + +# returns: +# parsed: +# - afi: ipv4 +# acls: +# - name: ACL1v4 +# aces: +# - grant: deny +# destination: +# address: 192.0.2.64 +# wildcard_bits: 0.0.0.255 +# source: +# any: true +# port_protocol: +# lt: 55 +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# fin: true +# sequence: 50 +# +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# aces: +# - grant: permit +# sequence: 10 +# source: +# any: true +# destination: +# prefix: 2001:db8:12::/32 +# protocol: sctp + + +# Using gathered: + +# Before state: +# ------------ +# +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any + +- name: Gather existing configuration + nxos_acls: + state: gathered + +# returns: +# gathered: +# - afi: ipv4 +# acls: +# - name: ACL1v4 +# aces: +# - grant: deny +# destination: +# address: 192.0.2.64 +# wildcard_bits: 0.0.0.255 +# source: +# any: true +# port_protocol: +# lt: 55 +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# fin: true +# sequence: 50 + +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# aces: +# - grant: permit +# sequence: 10 +# source: +# any: true +# destination: +# prefix: 2001:db8:12::/32 +# protocol: sctp + + +# Using rendered + +- name: Render required configuration to be pushed to the device + nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 55 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + prefix: 2001:db8:12::/32 + protocol: sctp + state: rendered + +# returns: +# rendered: +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + 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: dict + 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: ['ip access-list ACL1v4', '10 permit ip any any precedence critical log', '20 deny tcp any lt smtp host 192.0.2.64 ack fin'] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.nxos.config.acls.acls import Acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=AclsArgs.argument_spec, + supports_check_mode=True) + + result = Acls(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/nxos_acls/defaults/main.yaml b/test/integration/targets/nxos_acls/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/nxos_acls/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/nxos_acls/meta/main.yml b/test/integration/targets/nxos_acls/meta/main.yml new file mode 100644 index 00000000000..ae741cbdc71 --- /dev/null +++ b/test/integration/targets/nxos_acls/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nxos_tests diff --git a/test/integration/targets/nxos_acls/tasks/cli.yaml b/test/integration/targets/nxos_acls/tasks/cli.yaml new file mode 100644 index 00000000000..f1c20c1b78e --- /dev/null +++ b/test/integration/targets/nxos_acls/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- name: collect cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yml" + connection: local + register: test_cases + +- set_fact: + test_cases: + files: "{{ test_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli connection={{ cli }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_acls/tasks/main.yaml b/test/integration/targets/nxos_acls/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/nxos_acls/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/nxos_acls/tests/cli/deleted.yml b/test/integration/targets/nxos_acls/tests/cli/deleted.yml new file mode 100644 index 00000000000..d85b1250347 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/deleted.yml @@ -0,0 +1,69 @@ +--- +- debug: + msg: Start nxos_acls deleted integration tests connection={{ansible_connection}}" + +- include_tasks: populate_config.yaml + +- block: + - name: Deleted (All ACLs) + nxos_acls: + config: + state: deleted + + - name: Gather acls facts + nxos_facts: &facts + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - assert: + that: + - "ansible_facts.network_resources == {}" + + - include_tasks: populate_config.yaml + + - name: Deleted + nxos_acls: &deleted + config: + - afi: ipv4 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + any: true + protocol: sctp + + - sequence: 20 + state: deleted + register: result + + - assert: + that: + - "result.changed==True" + - "'no ip access-list ACL1v4' in result.commands" + - "'no ip access-list ACL2v4' in result.commands" + - "'ipv6 access-list ACL1v6' in result.commands" + - "'no 10 permit sctp any any' in result.commands" + - "'no 20' in result.commands" + - "result.commands | length == 5" + + - name: Gather acls facts + nxos_facts: *facts + + - name: Idempotence - deleted + nxos_acls: *deleted + register: result + + - assert: + that: + - "result.changed == false" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/gathered.yml b/test/integration/targets/nxos_acls/tests/cli/gathered.yml new file mode 100644 index 00000000000..c2ff83c9c54 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/gathered.yml @@ -0,0 +1,34 @@ +--- +- debug: + msg: Start nxos_acls gathered integration tests connection={{ansible_connection}}" + +- include_tasks: populate_config.yaml + +- block: + - name: Gather acls facts + nxos_facts: &facts + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - name: Gathered + nxos_acls: &gathered + state: gathered + register: result + + - assert: + that: + - "result.changed == false" + - "ansible_facts.network_resources.acls == result.gathered" + + - name: Idempotence - Gathered + nxos_acls: *gathered + register: result + + - assert: + that: + - "result.changed == false" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/merged.yml b/test/integration/targets/nxos_acls/tests/cli/merged.yml new file mode 100644 index 00000000000..b22b568adf5 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/merged.yml @@ -0,0 +1,108 @@ +--- +- debug: + msg: Start nxos_acls merged integration tests connection={{ansible_connection}}" + +- include_tasks: remove_config.yaml + +- block: + - name: Merged + nxos_acls: &merged + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 25 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + - grant: permit + protocol: ip + source: + any: true + destination: + any: true + fragments: true + log: true + sequence: 20 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + host: 2001:db8:12::128 + protocol: sctp + state: merged + register: result + + - assert: + that: + - "result.changed == True" + - "'ip access-list ACL1v4' in result.commands" + - "'20 permit ip any any fragments log' in result.commands" + - "'50 deny tcp any lt smtp 192.0.2.64 0.0.0.255 ack fin' in result.commands" + - "'ipv6 access-list ACL1v6' in result.commands" + - "'10 permit sctp any host 2001:db8:12::128' in result.commands" + - "result.commands | length == 5 " + + - name: Gather acls facts + nxos_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - assert: + that: + - "ansible_facts.network_resources.acls == result.after" + + - name: Idempotence - Merged + nxos_acls: *merged + register: result + + - assert: + that: + - "result.changed == false" + + - name: Update one parameter of an ACE + nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: permit + protocol: tcp + source: + any: true + destination: + any: true + sequence: 20 + precedence: 5 + state: merged + register: result + + - assert: + that: + - "result.changed == True" + - "'ip access-list ACL1v4' in result.commands" + - "'no 20' in result.commands" + - "'20 permit tcp any any fragments precedence critical log' in result.commands" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/overridden.yml b/test/integration/targets/nxos_acls/tests/cli/overridden.yml new file mode 100644 index 00000000000..27cca65d8b5 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/overridden.yml @@ -0,0 +1,99 @@ +--- +- debug: + msg: Start nxos_acls overridden integration tests connection={{ansible_connection}}" + +- include_tasks: populate_config.yaml + +- block: + - name: Overridden (first test) + nxos_acls: + config: + - afi: ipv4 + acls: + - name: NewACL + aces: + - grant: deny + source: + address: 192.0.2.0 + wildcard_bits: 0.0.255.255 + destination: + any: true + protocol: eigrp + - remark: Example for overridden state + state: overridden + register: result + + - assert: + that: + - "result.changed==True" + - "'no ip access-list ACL1v4' in result.commands" + - "'no ip access-list ACL2v4' in result.commands" + - "'no ipv6 access-list ACL1v6' in result.commands" + - "'no ipv6 access-list ACL2v6' in result.commands" + - "'ip access-list NewACL' in result.commands" + - "'deny eigrp 192.0.2.0 0.0.255.255 any' in result.commands" + - "'remark Example for overridden state' in result.commands" + - "result.commands|length==7" + + - name: Gather acls post facts + nxos_facts: &facts + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - assert: + that: + - "ansible_facts.network_resources.acls == result.after" + + - include_tasks: populate_config.yaml + + - name: Overridden (second test) + nxos_acls: &overridden + config: + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: deny + protocol: udp + destination: + any: true + source: + host: 2001:db8:3431::12 + port_protocol: + lt: 35 + sequence: 10 + state: overridden + register: result + + - assert: + that: + - "result.changed==True" + - "'no ip access-list ACL1v4' in result.commands" + - "'no ip access-list ACL2v4' in result.commands" + - "'no ipv6 access-list ACL2v6' in result.commands" + - "'no ip access-list NewACL' in result.commands" + - "'ipv6 access-list ACL1v6' in result.commands" + - "'no 10 permit sctp any any' in result.commands" + - "'no 20 remark IPv6 ACL' in result.commands" + - "'10 deny udp host 2001:db8:3431::12 lt 35 any' in result.commands" + - "result.commands|length==8" + + - name: Gather acls post facts + nxos_facts: *facts + + - assert: + that: + - "ansible_facts.network_resources.acls == result.after" + + - name: Idempotence - overridden + nxos_acls: *overridden + register: result + + - assert: + that: + - "result.changed == false" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/parsed.yml b/test/integration/targets/nxos_acls/tests/cli/parsed.yml new file mode 100644 index 00000000000..8a2efd5270e --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/parsed.yml @@ -0,0 +1,45 @@ +--- +- debug: + msg: Start nxos_acls gathered integration tests connection={{ansible_connection}}" + +- include_tasks: populate_config.yaml + +- block: + - name: Gather acls facts + nxos_facts: &facts + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - name: Parsed + nxos_acls: &parsed + running_config: | + ip access-list ACL1v4 + 10 permit ip any any + 20 deny udp any any + ip access-list ACL2v4 + 10 permit ahp 192.0.2.0 0.0.0.255 any + ipv6 access-list ACL1v6 + 10 permit sctp any any + 20 remark IPv6 ACL + ipv6 access-list ACL2v6 + 10 deny ipv6 any 2001:db8:3000::36/128 + 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + state: parsed + register: result + + - assert: + that: + - "result.changed == false" + - "ansible_facts.network_resources.acls == result.parsed" + + - name: Idempotence - Parsed + nxos_acls: *parsed + register: result + + - assert: + that: "result.changed == false" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/populate_config.yaml b/test/integration/targets/nxos_acls/tests/cli/populate_config.yaml new file mode 100644 index 00000000000..81b72e11c05 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/populate_config.yaml @@ -0,0 +1,15 @@ +--- +- name: Add configuration + cli_config: + config: | + ip access-list ACL1v4 + 10 permit ip any any + 20 deny udp any any + ip access-list ACL2v4 + 10 permit ahp 192.0.2.0 0.0.0.255 any + ipv6 access-list ACL1v6 + 10 permit sctp any any + 20 remark IPv6 ACL + ipv6 access-list ACL2v6 + 10 deny ipv6 any host 2001:db8:3000::36 + 20 permit tcp host 2001:db8:2000:2::2 host 2001:db8:2000:ab::2 diff --git a/test/integration/targets/nxos_acls/tests/cli/remove_config.yaml b/test/integration/targets/nxos_acls/tests/cli/remove_config.yaml new file mode 100644 index 00000000000..a6f3320051e --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/remove_config.yaml @@ -0,0 +1,9 @@ +--- +- name: Remove config + cli_config: + config: | + no ip access-list ACL1v4 + no ip access-list ACL2v4 + no ipv6 access-list ACL1v6 + no ipv6 access-list ACL2v6 + no ip access-list NewACL diff --git a/test/integration/targets/nxos_acls/tests/cli/rendered.yml b/test/integration/targets/nxos_acls/tests/cli/rendered.yml new file mode 100644 index 00000000000..5419a7086b1 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/rendered.yml @@ -0,0 +1,56 @@ +--- +- debug: + msg: "Start nxos_acls rendered tests connection={{ ansible_connection }}" + +- name: Rendered + nxos_acls: &rendered + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + eq: 43 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + prefix: 2001:db8:12::/32 + protocol: sctp + state: rendered + register: result + +- assert: + that: + - "result.changed == false" + - "'ip access-list ACL1v4' in result.rendered" + - "'50 deny tcp any eq whois 192.0.2.64 0.0.0.255 ack fin' in result.rendered" + - "'ipv6 access-list ACL1v6' in result.rendered" + - "'10 permit sctp any 2001:db8:12::/32' in result.rendered" + - "result.rendered | length == 4" + +- name: Idempotence - Rendered + nxos_acls: *rendered + register: result + +- assert: + that: + - "result.changed == false" diff --git a/test/integration/targets/nxos_acls/tests/cli/replaced.yml b/test/integration/targets/nxos_acls/tests/cli/replaced.yml new file mode 100644 index 00000000000..24513474914 --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/replaced.yml @@ -0,0 +1,65 @@ +--- +- debug: + msg: Start nxos_acls replaced integration tests connection={{ansible_connection}}" + +- include_tasks: populate_config.yaml + +- block: + - name: Replaced + nxos_acls: &replaced + config: + - afi: ipv4 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - sequence: 30 + grant: permit + source: + any: true + destination: + any: true + protocol: pim + + - sequence: 40 + remark: Replaced ACE + - name: ACL2v6 + state: replaced + register: result + + - assert: + that: + - "'no ip access-list ACL1v4' in result.commands" + - "'no ip access-list ACL2v4' in result.commands" + - "'ipv6 access-list ACL1v6' in result.commands" + - "'no 10 permit sctp any any' in result.commands" + - "'no 20 remark IPv6 ACL' in result.commands" + - "'30 permit pim any any' in result.commands" + - "'40 remark Replaced ACE' in result.commands" + - "'ipv6 access-list ACL2v6' in result.commands" + - "'no 10 deny ipv6 any host 2001:db8:3000::36' in result.commands" + - "'no 20 permit tcp host 2001:db8:2000:2::2 host 2001:db8:2000:ab::2' in result.commands" + - "result.commands|length == 10" + + - name: Gather static_routes post facts + nxos_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: acls + + - assert: + that: + - "ansible_facts.network_resources.acls == result.after" + + - name: Idempotence - Replaced + nxos_acls: *replaced + register: result + + - assert: + that: + - "result.changed == false" + + always: + - include_tasks: remove_config.yaml diff --git a/test/integration/targets/nxos_acls/tests/cli/rtt.yml b/test/integration/targets/nxos_acls/tests/cli/rtt.yml new file mode 100644 index 00000000000..d3878ec149d --- /dev/null +++ b/test/integration/targets/nxos_acls/tests/cli/rtt.yml @@ -0,0 +1,87 @@ +--- +- debug: + msg: "Start nxos_acls round trip integration tests connection = {{ansible_connection}}" + +- block: + - name: RTT - Apply provided configuration + nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 25 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - grant: permit + protocol: ip + source: + any: true + destination: + any: true + fragments: true + log: true + sequence: 20 + state: merged + + - name: Gather interfaces facts + nxos_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: + - acls + + - name: Apply configuration to be reverted + nxos_acls: + config: + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + host: 2001:db8:12::128 + protocol: sctp + state: overridden + register: result + + - assert: + that: + - "result.changed == True" + - "'no ip access-list ACL1v4' in result.commands" + - "'ipv6 access-list ACL1v6' in result.commands" + - "'10 permit sctp any host 2001:db8:12::128' in result.commands" + - "result.commands | length == 3 " + + - name: Revert back to base configuration using facts round trip + nxos_acls: + config: "{{ ansible_facts['network_resources']['acls'] }}" + state: overridden + register: result + + - assert: + that: + - "result.changed == True" + - "'ip access-list ACL1v4' in result.commands" + - "'20 permit ip any any fragments log' in result.commands" + - "'50 deny tcp any lt smtp 192.0.2.64 0.0.0.255 fin ack' in result.commands" + - "'no ipv6 access-list ACL1v6' in result.commands" + - "result.commands | length == 4 " + always: + - include_tasks: remove_config.yaml diff --git a/test/units/modules/network/nxos/test_nxos_acls.py b/test/units/modules/network/nxos/test_nxos_acls.py new file mode 100644 index 00000000000..32cfbdc1689 --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_acls.py @@ -0,0 +1,370 @@ +# +# (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 ansible.modules.network.nxos import nxos_acls +from units.compat.mock import patch, MagicMock +from units.modules.utils import set_module_args +from .nxos_module import TestNxosModule, load_fixture + + +class TestNxosAclsModule(TestNxosModule): + + module = nxos_acls + + def setUp(self): + super(TestNxosAclsModule, 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_edit_config = patch( + 'ansible.module_utils.network.nxos.config.acls.acls.Acls.edit_config' + ) + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch( + 'ansible.module_utils.network.nxos.facts.acls.acls.AclsFacts.get_device_data' + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestNxosAclsModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_edit_config.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None, device=''): + def load_from_file(*args, **kwargs): + v4 = '''\nip access-list ACL1v4\n 10 permit ip any any\n 20 deny udp any any''' + v6 = '''\nipv6 access-list ACL1v6\n 10 permit sctp any any''' + return v4 + v6 + + self.execute_show_command.side_effect = load_from_file + + def test_nxos_acls_merged(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL2v4", + aces=[ + dict( + grant="deny", + destination=dict(any=True), + source=dict(any=True), + fragments=True, + sequence=20, + protocol="tcp", + protocol_options=dict( + tcp=dict(ack=True)) + ) + ] + ) + ] + ), + dict(afi="ipv6", + acls=[ + dict(name="ACL2v6") + ]) + ], state="merged")) + commands = ['ip access-list ACL2v4', + '20 deny tcp any any ack fragments', + 'ipv6 access-list ACL2v6'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_merged_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL1v4", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="ip" + ), + dict( + grant="deny", + destination=dict(any=True), + source=dict(any=True), + sequence=20, + protocol="udp") + ] + ), + ] + ), + dict(afi="ipv6", + acls=[ + dict(name="ACL1v6", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="sctp", + ) + ]) + ]) + ], state="merged")) + self.execute_module(changed=False, commands=[]) + + def test_nxos_acls_replaced(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL1v4", + aces=[ + dict( + grant="permit", + destination=dict(host="192.0.2.28"), + source=dict(any=True), + log=True, + sequence=50, + protocol="icmp", + protocol_options=dict( + icmp=dict(administratively_prohibited=True)) + ) + ] + ) + ] + ) + ], state="replaced")) + commands = ['ip access-list ACL1v4', 'no 20 deny udp any any', + 'no 10 permit ip any any', + '50 permit icmp any host 192.0.2.28 administratively-prohibited log'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_replaced_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL1v4", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="ip", + ), + dict( + grant="deny", + destination=dict(any=True), + source=dict(any=True), + sequence=20, + protocol="udp") + ] + ), + ] + ), + dict(afi="ipv6", + acls=[ + dict(name="ACL1v6", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="sctp", + ) + ]) + ]) + ], state="replaced")) + self.execute_module(changed=False, commands=[]) + + def test_nxos_acls_overridden(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL2v4", + aces=[ + dict( + grant="permit", + destination=dict(host="192.0.2.28"), + source=dict(any=True), + log=True, + sequence=50, + protocol="icmp", + protocol_options=dict( + icmp=dict(administratively_prohibited=True)) + ), + dict( + remark="Overridden ACL" + ) + ] + ) + ] + ) + ], state="overridden")) + commands = ['no ip access-list ACL1v4', 'no ipv6 access-list ACL1v6', 'ip access-list ACL2v4', + '50 permit icmp any host 192.0.2.28 administratively-prohibited log', 'remark Overridden ACL'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_overridden_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL1v4", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="ip", + ), + dict( + grant="deny", + destination=dict(any=True), + source=dict(any=True), + sequence=20, + protocol="udp") + ] + ), + ] + ), + dict(afi="ipv6", + acls=[ + dict(name="ACL1v6", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="sctp", + ) + ]) + ]) + ], state="overridden")) + self.execute_module(changed=False, commands=[]) + + def test_nxos_acls_deletedafi(self): + set_module_args( + dict(config=[dict(afi="ipv4")], state="deleted")) + commands = ['no ip access-list ACL1v4'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_deletedace(self): + set_module_args( + dict(config=[dict(afi="ipv6", + acls=[ + dict(name="ACL1v6", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="sctp", + ) + ]) + ])], state="deleted")) + commands = ['ipv6 access-list ACL1v6', 'no 10 permit sctp any any'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_deletedall(self): + set_module_args(dict(config=[], state='deleted')) + commands = ['no ipv6 access-list ACL1v6', 'no ip access-list ACL1v4'] + self.execute_module(changed=True, commands=commands) + + def test_nxos_acls_rendered(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="ACL1v4", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="ip", + ), + dict( + grant="deny", + destination=dict(any=True), + source=dict(any=True), + sequence=20, + protocol="udp") + ] + ), + ] + ), + dict(afi="ipv6", + acls=[ + dict(name="ACL1v6", + aces=[ + dict( + grant="permit", + destination=dict(any=True), + source=dict(any=True), + sequence=10, + protocol="sctp", + ) + ]) + ]) + ], state="rendered")) + commands = ['ip access-list ACL1v4', '10 permit ip any any', '20 deny udp any any', + 'ipv6 access-list ACL1v6', '10 permit sctp any any'] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), sorted( + commands), result['rendered']) + + def test_nxos_acls_parsed(self): + set_module_args(dict(running_config='''\nip access-list ACL1v4\n 10 permit ip any any\n 20 deny udp any any dscp AF23 precedence critical''', + state="parsed")) + result = self.execute_module(changed=False) + compare_list = [{'afi': 'ipv4', 'acls': [{'name': 'ACL1v4', + 'aces': [{'grant': 'permit', 'sequence': 10, 'protocol': 'ip', 'source': {'any': True}, + 'destination': {'any': True}}, {'grant': 'deny', 'sequence': 20, + 'protocol': 'udp', 'source': {'any': True}, + 'destination': {'any': True}, + 'dscp': 'AF23', 'precedence': 'critical'}]}]}] + self.assertEqual(result['parsed'], compare_list, result['parsed']) + + def test_nxos_acls_gathered(self): + set_module_args(dict(config=[], state="gathered")) + result = self.execute_module(changed=False) + compare_list = [{'acls': [{'aces': [{'destination': {'any': True}, 'sequence': 10, 'protocol': 'sctp', 'source': {'any': True}, 'grant': 'permit'}], + 'name': 'ACL1v6'}], 'afi': 'ipv6'}, {'acls': [{'aces': [{'destination': {'any': True}, 'sequence': 10, 'protocol': 'ip', + 'source': {'any': True}, 'grant': 'permit'}, + {'destination': {'any': True}, 'sequence': 20, 'protocol': 'udp', + 'source': {'any': True}, 'grant': 'deny'}], 'name': 'ACL1v4'}], + 'afi': 'ipv4'}] + self.assertEqual(result['gathered'], + compare_list, result['gathered'])