diff --git a/lib/ansible/module_utils/network/ios/argspec/acl_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/argspec/acl_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/argspec/acl_interfaces/acl_interfaces.py b/lib/ansible/module_utils/network/ios/argspec/acl_interfaces/acl_interfaces.py new file mode 100644 index 00000000000..608cf6e728b --- /dev/null +++ b/lib/ansible/module_utils/network/ios/argspec/acl_interfaces/acl_interfaces.py @@ -0,0 +1,66 @@ +# +# -*- 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 ios_acl_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Acl_InterfacesArgs(object): + """The arg spec for the ios_acl_interfaces module + """ + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'access_groups': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'afi': {'required': True, 'choices': ['ipv4', 'ipv6'], 'type': 'str'}, + 'acls': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'direction': {'required': True, 'choices': ['in', 'out'], 'type': 'str'} + } + } + } + } + }, + 'type': 'list' + }, + 'running_config': {'type': 'str'}, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted', 'gathered', 'rendered', 'parsed'], + 'default': 'merged', + 'type': 'str' + } + } diff --git a/lib/ansible/module_utils/network/ios/config/acl_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/config/acl_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/config/acl_interfaces/acl_interfaces.py b/lib/ansible/module_utils/network/ios/config/acl_interfaces/acl_interfaces.py new file mode 100644 index 00000000000..fa3352389c6 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/config/acl_interfaces/acl_interfaces.py @@ -0,0 +1,405 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The ios_acl_interfaces 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.common.utils import to_list +from ansible.module_utils.network.ios.facts.facts import Facts +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.ios.utils.utils import remove_duplicate_interface, normalize_interface + + +class Acl_Interfaces(ConfigBase): + """ + The ios_acl_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'acl_interfaces', + ] + + def __init__(self, module): + super(Acl_Interfaces, self).__init__(module) + + def get_acl_interfaces_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) + acl_interfaces_facts = facts['ansible_network_resources'].get('acl_interfaces') + if not acl_interfaces_facts: + return [] + + return acl_interfaces_facts + + def execute_module(self): + """ Execute the module + :rtype: A dictionary + :returns: The result from moduel execution + """ + result = {'changed': False} + commands = list() + warnings = list() + + if self.state in self.ACTION_STATES: + existing_acl_interfaces_facts = self.get_acl_interfaces_facts() + else: + existing_acl_interfaces_facts = [] + + if self.state in self.ACTION_STATES or self.state == 'rendered': + commands.extend(self.set_config(existing_acl_interfaces_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_acl_interfaces_facts = self.get_acl_interfaces_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_acl_interfaces_facts(data=running_config) + else: + changed_acl_interfaces_facts = [] + + if self.state in self.ACTION_STATES: + result['before'] = existing_acl_interfaces_facts + if result['changed']: + result['after'] = changed_acl_interfaces_facts + elif self.state == 'gathered': + result['gathered'] = changed_acl_interfaces_facts + + result['warnings'] = warnings + + return result + + def set_config(self, existing_acl_interfaces_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 deisred configuration + """ + want = self._module.params['config'] + if want: + for item in want: + item['name'] = normalize_interface(item['name']) + + have = existing_acl_interfaces_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 deisred configuration + """ + commands = [] + + state = self._module.params['state'] + 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 = self._state_overridden(want, have) + elif state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged' or state == 'rendered': + commands = self._state_merged(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + :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 deisred configuration + """ + commands = [] + + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + continue + commands.extend(self._clear_config(interface, each, 'replaced')) + commands.extend(self._set_config(interface, each)) + # Remove the duplicate interface call + commands = remove_duplicate_interface(commands) + + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :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 + """ + commands = [] + + for each in have: + for interface in want: + if each['name'] == interface['name']: + break + else: + # We didn't find a matching desired state, which means we can + # pretend we recieved an empty desired state. + interface = dict(name=each['name']) + commands.extend(self._clear_config(interface, each)) + continue + commands.extend(self._clear_config(interface, each, 'overridden')) + commands.extend(self._set_config(interface, each)) + # Remove the duplicate interface call + commands = remove_duplicate_interface(commands) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + :param want: the additive configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + # configuring non-existing interface + commands.extend(self._set_config(interface, dict())) + continue + commands.extend(self._set_config(interface, each)) + + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + :param want: the objects from which the configuration should be removed + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + + if want: + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + continue + commands.extend(self._clear_config(interface, each)) + else: + for each in have: + commands.extend(self._clear_config(dict(), each)) + + return commands + + def dict_to_set(self, input_dict, test_set, final_set, count=0): + # recursive function to convert input dict to set for comparision + test_dict = dict() + if isinstance(input_dict, dict): + input_dict_len = len(input_dict) + for k, v in sorted(iteritems(input_dict)): + count += 1 + if isinstance(v, list): + for each in v: + if isinstance(each, dict): + input_dict_len = len(each) + if [True for i in each.values() if type(i) == list]: + self.dict_to_set(each, set(), final_set, count) + else: + self.dict_to_set(each, test_set, final_set, 0) + else: + if v is not None: + test_dict.update({k: v}) + if tuple(iteritems(test_dict)) not in test_set and count == input_dict_len: + test_set.add(tuple(iteritems(test_dict))) + count = 0 + if count == input_dict_len + 1: + test_set.update(tuple(iteritems(test_dict))) + final_set.add(tuple(test_set)) + + def _set_config(self, want, have): + """ Function that sets the acls config based on the want and have config + :param want: want config + :param have: have config + :param acl_want: want acl config + :param afi: acl afi type + :rtype: A list + :returns: the commands generated based on input want/have params + """ + commands = [] + + want_set = set() + have_set = set() + self.dict_to_set(want, set(), want_set) + self.dict_to_set(have, set(), have_set) + + for w in want_set: + want_afi = dict(w).get('afi') + if have_set: + def common_diff_config_code(diff_list, cmd, commands): + for each in diff_list: + try: + temp = dict(each) + temp_cmd = cmd + ' {0} {1}'.format(temp['name'], temp['direction']) + if temp_cmd not in commands: + commands.append(temp_cmd) + except ValueError: + continue + for h in have_set: + have_afi = dict(h).get('afi') + if have_afi == want_afi: + if want_afi == 'ipv4': + diff = set(w) - set(h) + if diff: + cmd = 'ip access-group' + common_diff_config_code(diff, cmd, commands) + if want_afi == 'ipv6': + diff = set(w) - set(h) + if diff: + cmd = 'ipv6 traffic-filter' + common_diff_config_code(diff, cmd, commands) + break + else: + if want_afi == 'ipv4': + diff = set(w) - set(h) + if diff: + cmd = 'ip access-group' + common_diff_config_code(diff, cmd, commands) + if want_afi == 'ipv6': + diff = set(w) - set(h) + if diff: + cmd = 'ipv6 traffic-filter' + common_diff_config_code(diff, cmd, commands) + else: + def common_want_config_code(want, cmd, commands): + for each in want: + if each[0] == 'afi': + continue + temp = dict(each) + temp_cmd = cmd + ' {0} {1}'.format(temp['name'], temp['direction']) + commands.append(temp_cmd) + if want_afi == 'ipv4': + cmd = 'ip access-group' + common_want_config_code(w, cmd, commands) + if want_afi == 'ipv6': + cmd = 'ipv6 traffic-filter' + common_want_config_code(w, cmd, commands) + commands.sort() + if commands: + interface = want.get('name') + commands.insert(0, 'interface {0}'.format(interface)) + + return commands + + def _clear_config(self, want, have, state=''): + """ Function that deletes the acl config based on the want and have config + :param acl: acl config + :param config: config + :rtype: A list + :returns: the commands generated based on input acl/config params + """ + commands = [] + + if want.get('name'): + interface = 'interface ' + want['name'] + else: + interface = 'interface ' + have['name'] + + w_access_group = want.get('access_groups') + temp_want_afi = [] + temp_want_acl_name = [] + if w_access_group: + # get the user input afi and acls + for each in w_access_group: + want_afi = each.get('afi') + want_acls = each.get('acls') + if want_afi: + temp_want_afi.append(want_afi) + if want_acls: + for each in want_acls: + temp_want_acl_name.append(each.get('name')) + + h_access_group = have.get('access_groups') + if h_access_group: + for access_grp in h_access_group: + for acl in access_grp.get('acls'): + have_afi = access_grp.get('afi') + acl_name = acl.get('name') + acl_direction = acl.get('direction') + if temp_want_afi and state not in ['replaced', 'overridden']: + # if user want to delete acls based on afi + if 'ipv4' in temp_want_afi and have_afi == 'ipv4': + if acl_name in temp_want_acl_name: + continue + cmd = 'no ip access-group' + cmd += ' {0} {1}'.format(acl_name, acl_direction) + commands.append(cmd) + if 'ipv6' in temp_want_afi and have_afi == 'ipv6': + if acl_name in temp_want_acl_name: + continue + cmd = 'no ipv6 traffic-filter' + cmd += ' {0} {1}'.format(acl_name, acl_direction) + commands.append(cmd) + else: + # if user want to delete acls based on interface + if access_grp.get('afi') == 'ipv4': + if acl_name in temp_want_acl_name: + continue + cmd = 'no ip access-group' + cmd += ' {0} {1}'.format(acl_name, acl_direction) + commands.append(cmd) + elif access_grp.get('afi') == 'ipv6': + if acl_name in temp_want_acl_name: + continue + cmd = 'no ipv6 traffic-filter' + cmd += ' {0} {1}'.format(acl_name, acl_direction) + commands.append(cmd) + if commands: + # inserting the interface at first + commands.insert(0, interface) + + return commands diff --git a/lib/ansible/module_utils/network/ios/facts/acl_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/facts/acl_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/facts/acl_interfaces/acl_interfaces.py b/lib/ansible/module_utils/network/ios/facts/acl_interfaces/acl_interfaces.py new file mode 100644 index 00000000000..c80939aeba5 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/facts/acl_interfaces/acl_interfaces.py @@ -0,0 +1,122 @@ +# +# -*- 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 ios_acl_interfaces 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.ios.utils.utils import get_interface_type +from ansible.module_utils.network.ios.argspec.acl_interfaces.acl_interfaces import Acl_InterfacesArgs + + +class Acl_InterfacesFacts(object): + """ The ios_acl_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Acl_InterfacesArgs.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_acl_interfaces_data(self, connection): + return connection.get('sh running-config | include interface|ip access-group|ipv6 traffic-filter') + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + data = self.get_acl_interfaces_data(connection) + # operate on a collection of resource x + config = data.split('interface ') + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + facts = {} + if objs: + facts['acl_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + + for cfg in params['config']: + facts['acl_interfaces'].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) + match = re.search(r'^(\S+)', conf) + intf = match.group(1) + + if get_interface_type(intf) == 'unknown': + return {} + config['name'] = intf + config['access_groups'] = [] + acl_v4_config = {} + acl_v6_config = {} + + def common_iter_code(cmd, conf): + # Common code for IPV4 and IPV6 config parsing + acls = [] + re_cmd = cmd + ' (\\S+.*)' + ip_all = re.findall(re_cmd, conf) + for each in ip_all: + acl = {} + access_grp_config = each.split(' ') + acl['name'] = access_grp_config[0] + acl['direction'] = access_grp_config[1] + acls.append(acl) + return acls + + if 'ip' in conf: + acls = common_iter_code('ip access-group', conf) + acl_v4_config['afi'] = 'ipv4' + acl_v4_config['acls'] = acls + config['access_groups'].append(acl_v4_config) + if 'ipv6' in conf: + acls = common_iter_code('ipv6 traffic-filter', conf) + acl_v6_config['afi'] = 'ipv6' + acl_v6_config['acls'] = acls + config['access_groups'].append(acl_v6_config) + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/ios/facts/facts.py b/lib/ansible/module_utils/network/ios/facts/facts.py index 1c504425dd5..6f0163f98e2 100644 --- a/lib/ansible/module_utils/network/ios/facts/facts.py +++ b/lib/ansible/module_utils/network/ios/facts/facts.py @@ -23,6 +23,7 @@ from ansible.module_utils.network.ios.facts.lacp_interfaces.lacp_interfaces impo from ansible.module_utils.network.ios.facts.lldp_global.lldp_global import Lldp_globalFacts from ansible.module_utils.network.ios.facts.lldp_interfaces.lldp_interfaces import Lldp_InterfacesFacts from ansible.module_utils.network.ios.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts +from ansible.module_utils.network.ios.facts.acl_interfaces.acl_interfaces import Acl_InterfacesFacts from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config @@ -43,6 +44,7 @@ FACT_RESOURCE_SUBSETS = dict( lldp_global=Lldp_globalFacts, lldp_interfaces=Lldp_InterfacesFacts, l3_interfaces=L3_InterfacesFacts, + acl_interfaces=Acl_InterfacesFacts, ) diff --git a/lib/ansible/modules/network/ios/ios_acl_interfaces.py b/lib/ansible/modules/network/ios/ios_acl_interfaces.py new file mode 100644 index 00000000000..7286ac45a8d --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_acl_interfaces.py @@ -0,0 +1,633 @@ +#!/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 ios_acl_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: ios_acl_interfaces +version_added: '2.10' +short_description: Configure and manage access-control (ACL) attributes of interfaces on IOS devices. +description: This module configures and manages the access-control (ACL) attributes of interfaces on IOS platforms. +author: Sumit Jaiswal (@justjais) +notes: + - Tested against Cisco IOSv Version 15.2 on VIRL + - This module works with connection C(network_cli). + See L(IOS Platform Options,../network/user_guide/platform_ios.html). +options: + config: + description: A dictionary of ACL interfaces options + type: list + elements: dict + suboptions: + name: + description: Full name of the interface excluding any logical unit number, i.e. GigabitEthernet0/1. + type: str + required: True + access_groups: + description: Specify access-group for IP access list (standard or extended). + type: list + elements: dict + suboptions: + afi: + description: Specifies the AFI for the ACLs to be configured on this interface. + type: str + required: True + choices: + - ipv4 + - ipv6 + acls: + description: Specifies the ACLs for the provided AFI. + type: list + elements: dict + suboptions: + name: + description: Specifies the name of the IPv4/IPv4 ACL for the interface. + type: str + required: True + direction: + description: + - Specifies the direction of packets that the ACL will be applied on. + - With one direction already assigned, other acl direction cannot be same. + type: str + required: True + choices: + - in + - out + 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. + type: str + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged +""" + +EXAMPLES = """ +--- + +# Using Merged + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# interface GigabitEthernet0/2 +# ip access-group 123 out + +- name: "Merge module attributes of given access-groups" + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + - afi: ipv6 + acls: + - name: test_v6 + direction: out + - name: temp_v6 + direction: in + - name: GigabitEthernet0/2 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: in + state: merged + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 100 in +# ip access-group 123 out + + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +# Using Replaced + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: "Replace module attributes of given access-groups" + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: out + - name: 110 + direction: in + state: replaced + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# no ip access-group 123 out +# no ipv6 traffic-filter temp_v6 in +# no ipv6 traffic-filter test_v6 out +# ip access-group 100 out + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 100 out +# ip access-group 110 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +# Using Overridden + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: "Overridden module attributes of given access-groups" + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: out + - name: 110 + direction: in + state: overridden + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# no ip access-group 123 out +# no ipv6 traffic-filter test_v6 out +# no ipv6 traffic-filter temp_v6 in +# ip access-group 100 out +# interface GigabitEthernet0/2 +# no ip access-group 110 in +# no ip access-group 123 out + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 100 out +# ip access-group 110 in +# interface GigabitEthernet0/2 + +# Using Deleted + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: "Delete module attributes of given Interface" + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + state: deleted + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# no ip access-group 110 in +# no ip access-group 123 out +# no ipv6 traffic-filter test_v6 out +# no ipv6 traffic-filter temp_v6 in + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: "Delete module attributes of given Interface based on AFI" + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + state: deleted + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# no ip access-group 110 in +# no ip access-group 123 out + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +# Using DELETED without any config passed +#"(NOTE: This will delete all of configured resource module attributes from each configured interface)" + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: "Delete module attributes of given access-groups from ALL Interfaces" + ios_acl_interfaces: + config: + state: deleted + +# Commands Fired: +# --------------- +# +# interface GigabitEthernet0/1 +# no ip access-group 110 in +# no ip access-group 123 out +# no ipv6 traffic-filter test_v6 out +# no ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# no ip access-group 110 out +# no ip access-group 123 out + +# After state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# interface GigabitEthernet0/2 + +# Using Gathered + +# Before state: +# ------------- +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +- name: Gather listed acl interfaces with provided configurations + ios_acl_interfaces: + config: + state: gathered + +# Module Execution Result: +# ------------------------ +# +# "gathered": [ +# { +# "name": "Loopback888" +# }, +# { +# "name": "GigabitEthernet0/0" +# }, +# { +# "access_groups": [ +# { +# "acls": [ +# { +# "direction": "in", +# "name": "110" +# }, +# { +# "direction": "out", +# "name": "123" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "direction": "in", +# "name": "temp_v6" +# }, +# { +# "direction": "out", +# "name": "test_v6" +# } +# ], +# "afi": "ipv6" +# } +# ], +# "name": "GigabitEthernet0/1" +# }, +# { +# "access_groups": [ +# { +# "acls": [ +# { +# "direction": "in", +# "name": "100" +# }, +# { +# "direction": "out", +# "name": "123" +# } +# ], +# "afi": "ipv4" +# } +# ], +# "name": "GigabitEthernet0/2" +# } +# ] + +# After state: +# ------------ +# +# vios#sh running-config | include interface|ip access-group|ipv6 traffic-filter +# interface Loopback888 +# interface GigabitEthernet0/0 +# interface GigabitEthernet0/1 +# ip access-group 110 in +# ip access-group 123 out +# ipv6 traffic-filter test_v6 out +# ipv6 traffic-filter temp_v6 in +# interface GigabitEthernet0/2 +# ip access-group 110 in +# ip access-group 123 out + +# Using Rendered + +- name: Render the commands for provided configuration + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + - afi: ipv6 + acls: + - name: test_v6 + direction: out + - name: temp_v6 + direction: in + state: rendered + +# Module Execution Result: +# ------------------------ +# +# "rendered": [ +# "interface GigabitEthernet0/1", +# "ip access-group 110 in", +# "ip access-group 123 out", +# "ipv6 traffic-filter temp_v6 in", +# "ipv6 traffic-filter test_v6 out" +# ] + +# Using Parsed + +- name: Parse the commands for provided configuration + ios_acl_interfaces: + running_config: + "interface GigabitEthernet0/1 + ip access-group 110 in + ip access-group 123 out + ipv6 traffic-filter temp_v6 in + ipv6 traffic-filter test_v6 out" + state: parsed + +# Module Execution Result: +# ------------------------ +# +# "parsed": [ +# { +# "access_groups": [ +# { +# "acls": [ +# { +# "direction": "in", +# "name": "110" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "direction": "in", +# "name": "temp_v6" +# } +# ], +# "afi": "ipv6" +# } +# ], +# "name": "GigabitEthernet0/1" +# } +# ] + +""" + +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: The configuration returned will always be in the same format of the parameters above. +after: + description: The configuration as structured data after module completion. + 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: ['interface GigabitEthernet0/1', 'ip access-group 110 in', 'ipv6 traffic-filter test_v6 out'] +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.argspec.acl_interfaces.acl_interfaces import Acl_InterfacesArgs +from ansible.module_utils.network.ios.config.acl_interfaces.acl_interfaces import Acl_Interfaces + + +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',))] + + mutually_exclusive = [('config', 'running_config')] + + module = AnsibleModule(argument_spec=Acl_InterfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + result = Acl_Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/ios/ios_facts.py b/lib/ansible/modules/network/ios/ios_facts.py index 791762153fd..6a93dc7971e 100644 --- a/lib/ansible/modules/network/ios/ios_facts.py +++ b/lib/ansible/modules/network/ios/ios_facts.py @@ -58,7 +58,7 @@ options: a specific subset should not be collected. Valid subsets are 'all', 'interfaces', 'l2_interfaces', 'vlans', 'lag_interfaces', 'lacp', 'lacp_interfaces', 'lldp_global', - 'lldp_interfaces', 'l3_interfaces'. + 'lldp_interfaces', 'l3_interfaces', 'acl_interfaces'. version_added: "2.9" """ diff --git a/test/integration/targets/ios_acl_interfaces/defaults/main.yaml b/test/integration/targets/ios_acl_interfaces/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ios_acl_interfaces/tasks/cli.yaml b/test/integration/targets/ios_acl_interfaces/tasks/cli.yaml new file mode 100644 index 00000000000..242420aa7f5 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tasks/cli.yaml @@ -0,0 +1,21 @@ +--- +- 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 + tags: connection_network_cli diff --git a/test/integration/targets/ios_acl_interfaces/tasks/main.yaml b/test/integration/targets/ios_acl_interfaces/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/_parsed.cfg b/test/integration/targets/ios_acl_interfaces/tests/cli/_parsed.cfg new file mode 100644 index 00000000000..1462c135300 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/_parsed.cfg @@ -0,0 +1,8 @@ +interface GigabitEthernet0/1 +ip access-group 110 in +ip access-group 123 out +ipv6 traffic-filter temp_v6 in +ipv6 traffic-filter test_v6 out +interface GigabitEthernet0/2 +ip access-group 110 in +ip access-group 123 out diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/_populate_config.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/_populate_config.yaml new file mode 100644 index 00000000000..68ee0265f2c --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/_populate_config.yaml @@ -0,0 +1,14 @@ +--- +- name: Populate Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + interface GigabitEthernet 0/1 + ip access-group 110 in + ip access-group 123 out + ipv6 traffic-filter temp_v6 in + ipv6 traffic-filter test_v6 out + interface GigabitEthernet 0/2 + ip access-group 110 in + ip access-group 123 out diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/_remove_config.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/_remove_config.yaml new file mode 100644 index 00000000000..9560e2bbf7c --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/_remove_config.yaml @@ -0,0 +1,15 @@ +--- +- name: Remove Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + interface GigabitEthernet 0/1 + no ip access-group 110 in + no ip access-group 100 out + no ip access-group 123 out + no ipv6 traffic-filter temp_v6 in + no ipv6 traffic-filter test_v6 out + interface GigabitEthernet 0/2 + no ip access-group 110 in + no ip access-group 123 out diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/deleted.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/deleted.yaml new file mode 100644 index 00000000000..c101b6b8fa4 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/deleted.yaml @@ -0,0 +1,65 @@ +--- +- debug: + msg: "Start ios_acl_interfaces deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + + - name: Delete module attributes of given Interface based on AFI + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv6 + state: deleted + register: result + + - assert: + that: + - "result.commands|length == 3" + - "result.changed == true" + - "'no ipv6 traffic-filter temp_v6 in' in result.commands" + - "'no ipv6 traffic-filter test_v6 out' in result.commands" + + - name: Delete module attributes of given Interface based on AFI (IDEMPOTENT) + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv6 + state: deleted + register: result + + - assert: + that: + - "result.changed == false" + + - name: Delete module attributes of given Interface. + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + state: deleted + register: result + + - assert: + that: + - "result.commands|length == 3" + - "result.changed == true" + - "'no ip access-group 110 in' in result.commands" + - "'no ip access-group 123 out' in result.commands" + + - name: Delete module attributes of given Interface (IDEMPOTENT) + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + state: deleted + register: result + + - assert: + that: + - "result.changed == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/empty_config.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/empty_config.yaml new file mode 100644 index 00000000000..85e3bdf7aab --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/empty_config.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: "START ios_acl_interfaces empty_config.yaml integration tests on connection={{ ansible_connection }}" + +- name: Merged with empty config should give appropriate error message + ios_acl_interfaces: + 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 + ios_acl_interfaces: + 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 + ios_acl_interfaces: + 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 + ios_acl_interfaces: + 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 + ios_acl_interfaces: + 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/ios_acl_interfaces/tests/cli/gathered.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/gathered.yaml new file mode 100644 index 00000000000..e11c78adf14 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: "START ios_acl_interfaces gathered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _populate_config.yaml + +- block: + - name: Gather the provided configuration with the exisiting running configuration + ios_acl_interfaces: &gathered + config: + state: gathered + register: result + + - name: Assert + assert: + that: + - "gathered['config'] | symmetric_difference(result.gathered) == []" + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/merged.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/merged.yaml new file mode 100644 index 00000000000..46be05105f1 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/merged.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: "Start ios_acl_interfaces merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + + - name: Merge the provided configuration with the exisiting running configuration + ios_acl_interfaces: &merged + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + - afi: ipv6 + acls: + - name: temp_v6 + direction: in + - name: test_v6 + direction: out + - name: GigabitEthernet0/2 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + state: merged + register: result + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + - "result.commands|symmetric_difference(merged.commands) == []" + + - name: Merge the provided configuration with the exisiting running configuration (IDEMPOTENT) + ios_acl_interfaces: *merged + register: result + + - assert: + that: + - "result.commands|length == 0" + - "result.changed == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/overridden.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/overridden.yaml new file mode 100644 index 00000000000..89b303846fc --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/overridden.yaml @@ -0,0 +1,41 @@ +--- +- debug: + msg: "Start ios_acl_interfaces overridden integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate_config.yaml + +- block: + + - name: Override device configuration of all acl_interfaces with provided configuration + ios_acl_interfaces: &overridden + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: out + - name: 110 + direction: in + state: overridden + become: yes + register: result + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + - "result.commands|symmetric_difference(overridden.commands) == []" + + - name: Override device configuration of all acl_interfaces with provided configuration (IDEMPOTENT) + ios_acl_interfaces: *overridden + become: yes + register: result + + - assert: + that: + - "result.commands|length == 0" + - "result.changed == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/parsed.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/parsed.yaml new file mode 100644 index 00000000000..96efd878989 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/parsed.yaml @@ -0,0 +1,16 @@ +--- +- debug: + msg: "START ios_acl_interfaces parsed integration tests on connection={{ ansible_connection }}" + +- name: Parse the commands for provided configuration + ios_acl_interfaces: &parsed + running_config: + "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "parsed['config']|symmetric_difference(result.parsed) == []" diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/rendered.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/rendered.yaml new file mode 100644 index 00000000000..0d83ef0d221 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/rendered.yaml @@ -0,0 +1,39 @@ +--- +- debug: + msg: "Start ios_acl_interfaces rendered integration tests ansible_connection={{ ansible_connection }}" + +- block: + + - name: Render the commands for provided configuration + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + - afi: ipv6 + acls: + - name: test_v6 + direction: out + - name: temp_v6 + direction: in + - name: GigabitEthernet0/2 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + state: rendered + become: yes + register: result + + - assert: + that: + - "result.changed == false" + - "result.rendered|symmetric_difference(merged.commands) == []" diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/replaced.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/replaced.yaml new file mode 100644 index 00000000000..ef569ebac7c --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/replaced.yaml @@ -0,0 +1,41 @@ +--- +- debug: + msg: "Start ios_acl_interfaces replced integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate_config.yaml + +- block: + + - name: Replaces device configuration of listed acl_interfaces with provided configuration + ios_acl_interfaces: &replaced + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: out + - name: 110 + direction: in + state: replaced + become: yes + register: result + + - assert: + that: + - "result.commands|length == 5" + - "result.changed == true" + - "result.commands|symmetric_difference(replaced.commands) == []" + + - name: Replaces device configuration of listed acl_interfaces with provided configuration (IDEMPOTENT) + ios_acl_interfaces: *replaced + become: yes + register: result + + - assert: + that: + - "result.commands|length == 0" + - "result.changed == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/tests/cli/rtt.yaml b/test/integration/targets/ios_acl_interfaces/tests/cli/rtt.yaml new file mode 100644 index 00000000000..999901092fa --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/tests/cli/rtt.yaml @@ -0,0 +1,78 @@ +--- +- debug: + msg: "START ios_acl_interfaces round trip integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + + - name: Apply the provided configuration (Base config) + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + - afi: ipv6 + acls: + - name: temp_v6 + direction: in + - name: test_v6 + direction: out + - name: GigabitEthernet0/2 + access_groups: + - afi: ipv4 + acls: + - name: 110 + direction: in + - name: 123 + direction: out + state: merged + register: base_config + + - name: Gather acl interfaces facts + ios_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: + - acl_interfaces + + - name: Apply the provided configuration (config to be reverted) + ios_acl_interfaces: + config: + - name: GigabitEthernet0/1 + access_groups: + - afi: ipv4 + acls: + - name: 100 + direction: out + - name: 110 + direction: in + state: overridden + register: result + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + - "result.commands|symmetric_difference(overridden.commands) == []" + + - name: Revert back to base config using facts round trip + ios_acl_interfaces: + config: "{{ ansible_facts['network_resources']['acl_interfaces'] }}" + state: overridden + register: revert + + - assert: + that: + - "revert.commands|length == 8" + - "revert.changed == true" + - "revert.commands|symmetric_difference(rtt.commands) == []" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_acl_interfaces/vars/main.yaml b/test/integration/targets/ios_acl_interfaces/vars/main.yaml new file mode 100644 index 00000000000..424499b53a3 --- /dev/null +++ b/test/integration/targets/ios_acl_interfaces/vars/main.yaml @@ -0,0 +1,99 @@ +--- +interfaces: + int1: + GigabitEthernet0/1 + int2: + GigabitEthernet0/2 + + +merged: + commands: + - interface GigabitEthernet0/1 + - ip access-group 110 in + - ip access-group 123 out + - ipv6 traffic-filter temp_v6 in + - ipv6 traffic-filter test_v6 out + - interface GigabitEthernet0/2 + - ip access-group 110 in + - ip access-group 123 out + +replaced: + commands: + - interface GigabitEthernet0/1 + - no ip access-group 123 out + - no ipv6 traffic-filter temp_v6 in + - no ipv6 traffic-filter test_v6 out + - ip access-group 100 out + +overridden: + commands: + - interface GigabitEthernet0/1 + - no ip access-group 123 out + - no ipv6 traffic-filter test_v6 out + - no ipv6 traffic-filter temp_v6 in + - ip access-group 100 out + - interface GigabitEthernet0/2 + - no ip access-group 110 in + - no ip access-group 123 out + +gathered: + config: + - name: GigabitEthernet0/0 + - access_groups: + - acls: + - direction: 'in' + name: '110' + - direction: 'out' + name: '123' + afi: 'ipv4' + - acls: + - direction: 'in' + name: 'temp_v6' + - direction: 'out' + name: 'test_v6' + afi: 'ipv6' + name: GigabitEthernet0/1 + - access_groups: + - acls: + - direction: 'in' + name: '110' + - direction: 'out' + name: '123' + afi: ipv4 + name: GigabitEthernet0/2 + +parsed: + config: + - access_groups: + - acls: + - direction: in + name: '110' + - direction: out + name: '123' + afi: ipv4 + - acls: + - direction: in + name: temp_v6 + - direction: out + name: test_v6 + afi: ipv6 + name: GigabitEthernet0/1 + - access_groups: + - acls: + - direction: in + name: '110' + - direction: out + name: '123' + afi: ipv4 + name: GigabitEthernet0/2 + +rtt: + commands: + - interface GigabitEthernet0/1 + - no ip access-group 100 out + - ip access-group 123 out + - ipv6 traffic-filter temp_v6 in + - ipv6 traffic-filter test_v6 out + - interface GigabitEthernet0/2 + - ip access-group 110 in + - ip access-group 123 out diff --git a/test/units/modules/network/ios/fixtures/ios_acl_interfaces.cfg b/test/units/modules/network/ios/fixtures/ios_acl_interfaces.cfg new file mode 100644 index 00000000000..1462c135300 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_acl_interfaces.cfg @@ -0,0 +1,8 @@ +interface GigabitEthernet0/1 +ip access-group 110 in +ip access-group 123 out +ipv6 traffic-filter temp_v6 in +ipv6 traffic-filter test_v6 out +interface GigabitEthernet0/2 +ip access-group 110 in +ip access-group 123 out diff --git a/test/units/modules/network/ios/ios_module.py b/test/units/modules/network/ios/ios_module.py index 7a63d5d82ad..34df31c8127 100644 --- a/test/units/modules/network/ios/ios_module.py +++ b/test/units/modules/network/ios/ios_module.py @@ -50,7 +50,6 @@ def load_fixture(name): class TestIosModule(ModuleTestCase): def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): - self.load_fixtures(commands) if failed: diff --git a/test/units/modules/network/ios/test_ios_acl_interfaces.py b/test/units/modules/network/ios/test_ios_acl_interfaces.py new file mode 100644 index 00000000000..b3296062a56 --- /dev/null +++ b/test/units/modules/network/ios/test_ios_acl_interfaces.py @@ -0,0 +1,335 @@ +# +# (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.ios import ios_acl_interfaces +from units.modules.utils import set_module_args +from .ios_module import TestIosModule, load_fixture + + +class TestIosAclInterfacesModule(TestIosModule): + module = ios_acl_interfaces + + def setUp(self): + super(TestIosAclInterfacesModule, 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.ios.providers.providers.CliProvider.edit_config') + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch('ansible.module_utils.network.ios.facts.acl_interfaces.acl_interfaces.' + 'Acl_InterfacesFacts.get_acl_interfaces_data') + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosAclInterfacesModule, 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): + + def load_from_file(*args, **kwargs): + return load_fixture('ios_acl_interfaces.cfg') + self.execute_show_command.side_effect = load_from_file + + def test_ios_acl_interfaces_merged(self): + set_module_args( + dict(config=[ + dict(name="GigabitEthernet0/1", + access_groups=[ + dict(afi="ipv4", + acls=[ + dict(name="merge_110", + direction="in"), + dict(name="merge_123", + direction="out") + ]), + dict(afi="ipv6", + acls=[ + dict(name="merge_temp_v6", + direction="in"), + dict(name="merge_test_v6", + direction="out") + ]) + ]), + dict(name="GigabitEthernet0/2", + access_groups=[ + dict(afi="ipv4", + acls=[ + dict(name="merge_110", + direction="in"), + dict(name="merge_123", + direction="out") + ]) + ]) + ], state="merged")) + commands = ['interface GigabitEthernet0/1', + 'ip access-group merge_110 in', + 'ip access-group merge_123 out', + 'ipv6 traffic-filter merge_temp_v6 in', + 'ipv6 traffic-filter merge_test_v6 out', + 'interface GigabitEthernet0/2', + 'ip access-group merge_110 in', + 'ip access-group merge_123 out' + ] + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], commands) + + def test_ios_acl_interfaces_merged_idempotent(self): + set_module_args(dict( + config=[dict( + name="GigabitEthernet0/1", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="110", + direction="in" + ), dict( + name="123", + direction="out" + )] + ), dict( + afi="ipv6", + acls=[dict( + name="test_v6", + direction="out" + ), dict( + name="temp_v6", + direction="in" + )] + )] + ), dict( + name="GigabitEthernet0/2", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="110", + direction="in" + ), dict( + name="123", + direction="out" + )] + )] + )], state="merged" + )) + self.execute_module(changed=False, commands=[]) + + def test_ios_acl_interfaces_replaced(self): + set_module_args(dict( + config=[dict( + name="GigabitEthernet0/1", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="replace_100", + direction="out" + ), dict( + name="110", + direction="in" + )] + )] + )], state="replaced" + )) + commands = ['interface GigabitEthernet0/1', + 'no ip access-group 123 out', + 'no ipv6 traffic-filter temp_v6 in', + 'no ipv6 traffic-filter test_v6 out', + 'ip access-group replace_100 out' + ] + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], commands) + + def test_ios_acl_interfaces_replaced_idempotent(self): + set_module_args(dict( + config=[dict( + name="GigabitEthernet0/1", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="110", + direction="in" + ), dict( + name="123", + direction="out" + )] + ), dict( + afi="ipv6", + acls=[dict( + name="test_v6", + direction="out" + ), dict( + name="temp_v6", + direction="in" + )] + )] + )], state="replaced" + )) + self.execute_module(changed=False, commands=[]) + + def test_ios_acl_interfaces_overridden(self): + set_module_args(dict( + config=[dict( + name="GigabitEthernet0/1", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="100", + direction="out" + ), dict( + name="110", + direction="in" + )] + )] + )], state="overridden" + )) + + commands = [ + 'interface GigabitEthernet0/1', + 'no ip access-group 123 out', + 'no ipv6 traffic-filter test_v6 out', + 'no ipv6 traffic-filter temp_v6 in', + 'ip access-group 100 out', + 'interface GigabitEthernet0/2', + 'no ip access-group 110 in', + 'no ip access-group 123 out' + ] + self.execute_module(changed=True, commands=commands) + + def test_ios_acl_interfaces_overridden_idempotent(self): + set_module_args(dict( + config=[dict( + name="GigabitEthernet0/1", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="110", + direction="in" + ), dict( + name="123", + direction="out" + )] + ), dict( + afi="ipv6", + acls=[dict( + name="test_v6", + direction="out" + ), dict( + name="temp_v6", + direction="in" + )] + )] + ), dict( + name="GigabitEthernet0/2", + access_groups=[dict( + afi="ipv4", + acls=[dict( + name="110", + direction="in" + ), dict( + name="123", + direction="out" + )] + )] + )], state="overridden" + )) + self.execute_module(changed=False, commands=[]) + + def test_ios_acl_interfaces_deleted_interface(self): + set_module_args( + dict(config=[ + dict(name="GigabitEthernet0/1") + ], state="deleted")) + commands = ['interface GigabitEthernet0/1', + 'no ip access-group 110 in', + 'no ip access-group 123 out', + 'no ipv6 traffic-filter test_v6 out', + 'no ipv6 traffic-filter temp_v6 in', + ] + self.execute_module(changed=True, commands=commands) + + def test_ios_acl_interfaces_deleted_afi(self): + set_module_args( + dict(config=[ + dict(name="GigabitEthernet0/1", + access_groups=[ + dict(afi="ipv6")]) + ], state="deleted")) + commands = ['interface GigabitEthernet0/1', + 'no ipv6 traffic-filter test_v6 out', + 'no ipv6 traffic-filter temp_v6 in', + ] + self.execute_module(changed=True, commands=commands) + + def test_ios_acl_interfaces_parsed(self): + set_module_args( + dict( + running_config="interface GigabitEthernet0/1\nip access-group 110 in\nipv6 traffic-filter test_v6 out", + state="parsed" + ) + ) + result = self.execute_module(changed=False) + parsed_list = [ + {'access_groups': + [ + {'acls': + [ + {'direction': 'in', 'name': '110'} + ], 'afi': 'ipv4'}, + {'acls': + [ + {'direction': 'out', 'name': 'test_v6'} + ], + 'afi': 'ipv6'} + ], + 'name': 'GigabitEthernet0/1'}] + self.assertEqual(parsed_list, result['parsed']) + + def test_ios_acl_interfaces_rendered(self): + set_module_args( + dict(config=[ + dict(name="GigabitEthernet0/1", + access_groups=[ + dict(afi="ipv4", + acls=[ + dict(name="110", + direction="in"), + dict(name="123", + direction="out") + ]), + dict(afi="ipv6", + acls=[ + dict(name="temp_v6", direction="in"), + dict(name="test_v6", direction="out") + ]) + ]) + ], state="rendered")) + commands = ['interface GigabitEthernet0/1', + 'ip access-group 110 in', + 'ip access-group 123 out', + 'ipv6 traffic-filter temp_v6 in', + 'ipv6 traffic-filter test_v6 out' + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), commands)