diff --git a/lib/ansible/module_utils/network/eos/argspec/acls/__init__.py b/lib/ansible/module_utils/network/eos/argspec/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/argspec/acls/acls.py b/lib/ansible/module_utils/network/eos/argspec/acls/acls.py new file mode 100644 index 00000000000..f9989824043 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/acls/acls.py @@ -0,0 +1,468 @@ +# +# -*- 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 eos_acls module +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class AclsArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_acls module + """ + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'acls': { + 'elements': 'dict', + 'options': { + 'aces': { + 'elements': 'dict', + 'options': { + 'destination': { + 'mutually_exclusive': + [['address', 'subnet_address', 'any', 'host'], + ['wildcard_bits', 'subnet_address', 'any', 'host']], + 'options': { + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'host': { + 'type': 'str' + }, + 'port_protocol': { + 'type': 'dict' + }, + 'subnet_address': { + 'type': 'str' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': + [['address', 'wildcard_bits']], + 'type': + 'dict' + }, + 'fragment_rules': { + 'type': 'bool' + }, + 'fragments': { + 'type': 'bool' + }, + 'grant': { + 'choices': ['permit', 'deny'], + 'type': 'str' + }, + 'line': { + 'type': 'str', + 'aliases': ['ace'] + }, + 'hop_limit': { + 'type': 'dict' + }, + 'log': { + 'type': 'bool' + }, + 'protocol': { + 'type': 'str' + }, + 'protocol_options': { + '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_num': { + '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' + }, + 'icmpv6': { + 'options': { + 'address_unreachable': { + 'type': 'bool' + }, + 'beyond_scope': { + 'type': 'bool' + }, + 'echo_reply': { + 'type': 'bool' + }, + 'echo_request': { + 'type': 'bool' + }, + 'erroneous_header': { + 'type': 'bool' + }, + 'fragment_reassembly_exceeded': + { + 'type': 'bool' + }, + 'hop_limit_exceeded': { + 'type': 'bool' + }, + 'neighbor_advertisement': { + 'type': 'bool' + }, + 'neighbor_solicitation': { + 'type': 'bool' + }, + 'no_admin': { + 'type': 'bool' + }, + 'no_route': { + 'type': 'bool' + }, + 'packet_too_big': { + 'type': 'bool' + }, + 'parameter_problem': { + 'type': 'bool' + }, + 'port_unreachable': { + 'type': 'bool' + }, + 'redirect_message': { + 'type': 'bool' + }, + 'reject_route': { + 'type': 'bool' + }, + 'router_advertisement': { + 'type': 'bool' + }, + 'router_solicitation': { + 'type': 'bool' + }, + 'source_address_failed': { + 'type': 'bool' + }, + 'source_routing_error': { + 'type': 'bool' + }, + 'time_exceeded': { + 'type': 'bool' + }, + 'unreachable': { + 'type': 'bool' + }, + 'unrecognized_ipv6_option': { + 'type': 'bool' + }, + 'unrecognized_next_header': { + 'type': 'bool' + } + }, + 'type': 'dict' + }, + 'ip': { + 'options': { + 'nexthop_group': { + 'type': 'str' + } + }, + 'type': 'dict' + }, + 'ipv6': { + 'options': { + 'nexthop_group': { + 'type': 'str' + } + }, + 'type': 'dict' + }, + 'tcp': { + 'options': { + 'flags': { + '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' + } + }, + 'type': 'dict' + }, + 'remark': { + 'type': 'str' + }, + 'sequence': { + 'type': 'int' + }, + 'source': { + 'mutually_exclusive': + [['address', 'subnet_address', 'any', 'host'], + ['wildcard_bits', 'subnet_address', 'any', 'host']], + 'options': { + 'address': { + 'type': 'str' + }, + 'any': { + 'type': 'bool' + }, + 'host': { + 'type': 'str' + }, + 'port_protocol': { + 'type': 'dict' + }, + 'subnet_address': { + 'type': 'str' + }, + 'wildcard_bits': { + 'type': 'str' + } + }, + 'required_together': [['address', 'wildcard_bits']], + 'type': 'dict' + }, + 'tracked': { + 'type': 'bool' + }, + 'ttl': { + 'options': { + 'eq': { + 'type': 'int' + }, + 'gt': { + 'type': 'int' + }, + 'lt': { + 'type': 'int' + }, + 'neq': { + 'type': 'int' + } + }, + 'type': 'dict' + }, + 'vlan': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'name': { + 'required': True, + 'type': 'str' + }, + 'standard': { + 'type': 'bool' + } + }, + 'type': 'list' + }, + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'running_config': { + 'type': 'str' + }, + 'state': { + 'choices': [ + 'deleted', 'merged', 'overridden', 'replaced', 'gathered', + 'rendered', 'parsed' + ], + 'default': + 'merged', + 'type': + 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/eos/config/acls/__init__.py b/lib/ansible/module_utils/network/eos/config/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/config/acls/acls.py b/lib/ansible/module_utils/network/eos/config/acls/acls.py new file mode 100644 index 00000000000..90fee80b04d --- /dev/null +++ b/lib/ansible/module_utils/network/eos/config/acls/acls.py @@ -0,0 +1,483 @@ +# +# -*- 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 eos_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 +import itertools + +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.common.utils import remove_empties, dict_diff +from ansible.module_utils.network.eos.facts.facts import Facts + + +class Acls(ConfigBase): + """ + The eos_acls class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'acls', + ] + + def __init__(self, module): + super(Acls, self).__init__(module) + + def get_acls_facts(self, data=None): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources, data=data) + acls_facts = facts['ansible_network_resources'].get('acls') + if not acls_facts: + return [] + return acls_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + changed = False + + if self.state in self.ACTION_STATES: + existing_acls_facts = self.get_acls_facts() + else: + existing_acls_facts = [] + if self.state in self.ACTION_STATES or self.state == 'rendered': + commands.extend(self.set_config(existing_acls_facts)) + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(commands) + changed = True + if changed: + result['changed'] = True + if self.state in self.ACTION_STATES: + result['commands'] = commands + if self.state in self.ACTION_STATES or self.state == 'gathered': + changed_acls_facts = self.get_acls_facts() + elif self.state == 'rendered': + commands = list(itertools.chain(*commands)) + result['rendered'] = commands + elif self.state == 'parsed': + if not self._module.params['running_config']: + self._module.fail_json(msg="Value of running_config parameter must not be empty for state parsed") + result['parsed'] = self.get_acls_facts(data=self._module.params['running_config']) + else: + changed_acls_facts = [] + if self.state in self.ACTION_STATES: + result['before'] = existing_acls_facts + if result['changed']: + result['after'] = changed_acls_facts + elif self.state == 'gathered': + result['gathered'] = changed_acls_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_acls_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + config = self._module.params.get('config') + want = [] + onbox_configs = [] + for h in existing_acls_facts: + have_configs = add_commands(remove_empties(h)) + onbox_configs.append(have_configs) + if config: + for w in config: + want.append(remove_empties(w)) + have = existing_acls_facts + resp = self.set_state(want, have) + if self.state == 'merged': + to_config = self.compare_configs(onbox_configs, to_list(resp)) + else: + to_config = resp + return to_config + + def compare_configs(self, have, want): + commands = [] + want = list(itertools.chain(*want)) + have = list(itertools.chain(*have)) + h_index = 0 + config = list(want) + for w in want: + access_list = re.findall(r'(ip.*) access-list (.*)', w) + if access_list: + if w in have: + h_index = have.index(w) + else: + for num, h in enumerate(have, start=h_index + 1): + if "access-list" not in h: + seq_num = re.search(r'(\d+) (.*)', w) + if seq_num: + have_seq_num = re.search(r'(\d+) (.*)', h) + if seq_num.group(1) == have_seq_num.group(1) and have_seq_num.group(2) != seq_num.group(2): + negate_cmd = "no " + seq_num.group(1) + config.insert(config.index(w), negate_cmd) + if w in h: + config.pop(config.index(w)) + break + for c in config: + access_list = re.findall(r'(ip.*) access-list (.*)', c) + if access_list: + acl_index = config.index(c) + else: + if config[acl_index] not in commands: + commands.append(config[acl_index]) + commands.append(c) + return commands + + 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 + """ + commands = [] + if self.state in ('merged', 'replaced', 'overridden', 'rendered') and not want: + self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(self.state)) + state = self._module.params['state'] + if state == 'overridden': + commands = self._state_overridden(want, have) + elif state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged' or self.state == 'rendered': + commands = self._state_merged(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(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_commands = [] + remove_cmds = [] + diff = {} + present = False + diff_present = False + for w in want: + afi = "ipv6" if w["afi"] == "ipv6" else "ipv4" + for acl in w["acls"]: + name = acl["name"] + want_ace = acl["aces"] + for h in have: + if h["afi"] == afi: + for h_acl in h["acls"]: + if h_acl["name"] == name: + present = True + h = {"afi": afi, "acls": [{"name": name}]} + for h_ace in h_acl['aces']: + diff = get_ace_diff(h_ace, want_ace) + if diff: + diff_present = True + h = {"afi": afi, "acls": [{"name": name, "aces": [h_ace]}]} + remove_cmds.append(del_commands(h, have)) + if diff_present or not present: + config_cmds = set_commands(want, have) + config_cmds = list(itertools.chain(*config_cmds)) + for cmd in have: + have_configs = add_commands(cmd) + have_commands.append(have_configs) + have_commands = list(itertools.chain(*have_commands)) + if remove_cmds: + remove_cmds = list(itertools.chain(*remove_cmds)) + commands.append(remove_cmds) + commands.append(config_cmds) + commands = list(itertools.chain(*commands)) + commandset = [] + [commandset.append(cmd) for cmd in commands if cmd not in commandset] + return commandset + + @staticmethod + def _state_overridden(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 = [] + ace_diff = {} + h_afi_list = [] + w_afi_list = [] + diff = False + for h in have: + h_afi_list.append(h["afi"]) + for w in want: + w_afi_list.append(w["afi"]) + for hafi in h_afi_list: + if hafi not in w_afi_list: + h = {"afi": hafi} + remove_cmds = del_commands(h, have) + commands.append(remove_cmds) + for w in want: + w_names = [] + for h in have: + h_names = [] + if w["afi"] == h["afi"]: + for w_acl in w["acls"]: + w_names.append(w_acl["name"]) + for h_acl in h["acls"]: + h_names.append(h_acl["name"]) + if h_acl["name"] == w_acl["name"]: + for w_ace in w_acl['aces']: + ace_diff = get_ace_diff(w_ace, h_acl["aces"]) + if ace_diff: + diff = True + h = {"afi": h["afi"], "acls": [{"name": h_acl["name"], "aces": h_acl["aces"]}]} + remove_cmds = del_commands(h, have) + commands.append(remove_cmds) + for hname in h_names: + if hname not in w_names: + h = {"afi": h["afi"], "acls": [{"name": hname}]} + remove_cmds = del_commands(h, have) + if remove_cmds not in commands: + commands.append(remove_cmds) + + if diff: + config_cmds = set_commands(want, have) + config_cmds = list(itertools.chain(*config_cmds)) + commands.append(config_cmds) + if commands: + commands = list(itertools.chain(*commands)) + return commands + + @staticmethod + def _state_merged(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 set_commands(want, have) + + @staticmethod + def _state_deleted(want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + if not want: + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + commands.append(command) + else: + for w in want: + return_command = del_commands(w, have) + commands.append(return_command) + commands = list(itertools.chain(*commands)) + return commands + + +def set_commands(want, have): + commands = [] + for w in want: + wace_updated = [] + for h in have: + if w['afi'] == h['afi']: + for wacl in w["acls"]: + for hacl in h["acls"]: + if wacl['name'] == hacl['name']: + want_aces = wacl['aces'] + for wace in wacl['aces']: + for hace in hacl['aces']: + if 'sequence' in wace.keys() and 'sequence' in hace.keys(): + if wace['sequence'] == hace['sequence']: + wace_updated = get_updated_ace(wace, hace) + if wace_updated: + want_aces.pop(want_aces.index(wace)) + want_aces.append(wace_updated) + return_command = add_commands(w) + commands.append(return_command) + return commands + + +def get_updated_ace(w, h): + # gives the ace to be updated in case of merge update. + w_updated = w.copy() + for hkey in h.keys(): + if hkey not in w.keys(): + w_updated.update({hkey: h[hkey]}) + else: + w_updated.update({hkey: w[hkey]}) + return w_updated + + +def add_commands(want): + commandset = [] + protocol_name = {"51": "ahp", "47": "gre", "1": "icmp", "2": "igmp", + "4": "ip", "89": "ospf", "103": "pim", "6": "tcp", + "17": "udp", "112": "vrrp"} + if not want: + return commandset + command = "" + afi = "ip" if want["afi"] == "ipv4" else "ipv6" + for acl in want["acls"]: + if "standard" in acl.keys() and acl["standard"]: + command = afi + " access-list standard " + acl["name"] + else: + command = afi + " access-list " + acl["name"] + commandset.append(command) + if "aces" not in acl.keys(): + continue + for ace in acl["aces"]: + command = "" + if "sequence" in ace.keys(): + command = str(ace["sequence"]) + if "remark" in ace.keys(): + command = command + " remark " + ace["remark"] + if "fragment_rules" in ace.keys() and ace["fragment_rules"]: + command = command + " fragment-rules" + if "grant" in ace.keys(): + command = command + " " + ace["grant"] + if "vlan" in ace.keys(): + command = command + " vlan " + ace["vlan"] + if "protocol" in ace.keys(): + protocol = ace["protocol"] + if protocol.isdigit(): + if protocol in protocol_name.keys(): + protocol = protocol_name[protocol] + command = command + " " + protocol + if "source" in ace.keys(): + if "any" in ace["source"].keys(): + command = command + " any" + elif "subnet_address" in ace["source"].keys(): + command = command + " " + ace["source"]["subnet_address"] + elif "host" in ace["source"].keys(): + command = command + " host " + ace["source"]["host"] + elif "address" in ace["source"].keys(): + command = command + " " + ace["source"]["address"] + " " + ace["source"]["wildcard_bits"] + if "port_protocol" in ace["source"].keys(): + for op, val in ace["source"]["port_protocol"].items(): + if val.isdigit(): + val = socket.getservbyport(int(val)) + command = command + " " + op + " " + val + if "destination" in ace.keys(): + if "any" in ace["destination"].keys(): + command = command + " any" + elif "subnet_address" in ace["destination"].keys(): + command = command + " " + ace["destination"]["subnet_address"] + elif "host" in ace["destination"].keys(): + command = command + " host " + ace["destination"]["host"] + elif "address" in ace["destination"].keys(): + command = command + " " + ace["destination"]["address"] + " " + ace["destination"]["wildcard_bits"] + if "port_protocol" in ace["destination"].keys(): + for op in ace["destination"]["port_protocol"].keys(): + command = command + " " + op + " " + ace["destination"]["port_protocol"][op] + if "protocol_options" in ace.keys(): + for proto in ace["protocol_options"].keys(): + if proto == "icmp" or proto == "icmpv6": + for icmp_msg in ace["protocol_options"][proto].keys(): + command = command + " " + icmp_msg + elif proto == "ip" or proto == "ipv6": + command = command + " nexthop-group " + ace["protocol_options"][proto]["nexthop_group"] + elif proto == "tcp": + for flag, val in ace["prtocol_options"][proto]["flags"].items(): + command = command + " " + val + if "hop_limit" in ace.keys(): + for op, val in ace["hop_limit"].items(): + command = command + " hop-limit " + op + " " + val + if "tracked" in ace.keys() and ace["tracked"]: + command = command + " tracked" + if "ttl" in ace.keys(): + for op, val in ace["ttl"].items(): + command = command + " ttl " + op + " " + str(val) + if "fragments" in ace.keys(): + command = command + " fragments" + if "log" in ace.keys(): + command = command + " log" + commandset.append(command.strip()) + return commandset + + +def del_commands(want, have): + commandset = [] + command = "" + have_command = [] + for h in have: + have_configs = add_commands(h) + have_command.append(have_configs) + have_command = list(itertools.chain(*have_command)) + afi = "ip" if want["afi"] == "ipv4" else "ipv6" + if "acls" not in want.keys(): + for have_cmd in have_command: + access_list = re.search(r'(ip.*)\s+access-list .*', have_cmd) + if access_list and access_list.group(1) == afi: + commandset.append("no " + have_cmd) + return commandset + + for acl in want["acls"]: + ace_present = True + if "standard" in acl.keys() and acl["standard"]: + command = afi + " access-list standard " + acl["name"] + else: + command = afi + " access-list " + acl["name"] + if "aces" not in acl.keys(): + ace_present = False + commandset.append("no " + command) + if ace_present: + return_command = add_commands(want) + for cmd in return_command: + if "access-list" in cmd: + commandset.append(cmd) + continue + seq = re.search(r'(\d+) (permit|deny|fragment-rules|remark) .*', cmd) + if seq: + commandset.append("no " + seq.group(1)) + else: + commandset.append("no " + cmd) + return commandset + + +def get_ace_diff(want_ace, have_ace): + # gives the diff of the aces passed. + for h_a in have_ace: + d = dict_diff(want_ace, h_a) + if not d: + break + return d diff --git a/lib/ansible/module_utils/network/eos/facts/acls/__init__.py b/lib/ansible/module_utils/network/eos/facts/acls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/facts/acls/acls.py b/lib/ansible/module_utils/network/eos/facts/acls/acls.py new file mode 100644 index 00000000000..edd7752c13c --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/acls/acls.py @@ -0,0 +1,300 @@ +# +# -*- 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 eos 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.eos.argspec.acls.acls import AclsArgs + + +class AclsFacts(object): + """ The eos 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 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) + + # split the config into instances of the resource + find_pattern = r'(?:^|\n)(?:ip|ipv6) access\-list.*?(?=(?:^|\n)(?:ip|ipv6) access\-list|$)' + resources = [p for p in re.findall(find_pattern, + data, + re.DOTALL)] + + objs = [] + ipv4list = [] + ipv6list = [] + for resource in resources: + if "ipv6" in resource: + ipv6list.append(resource) + else: + ipv4list.append(resource) + ipv4list = ["\n".join(ipv4list)] + ipv6list = ["\n".join(ipv6list)] + for resource in ipv4list: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + for resource in ipv6list: + 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: + facts['acls'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['acls'].append(utils.remove_empties(cfg)) + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + afi_list = [] + acls_list = [] + name_dict = {} + standard = 0 + operator = ['eq', 'lt', 'neq', 'range', 'gt'] + flags = ['ack', 'established', 'fin', 'psh', 'rst', 'syn', 'urg'] + others = ['hop_limit', 'log', 'ttl', 'fragments', 'tracked'] + for dev_config in conf.split('\n'): + ace_dict = {} + if not dev_config: + continue + if dev_config == '!': + continue + dev_config = dev_config.strip() + matches = re.findall(r'(ip.*?) access-list (.*)', dev_config) + if matches: + afi = "ipv4" if matches[0][0] == "ip" else "ipv6" + ace_list = [] + if bool(name_dict): + acls_list.append(name_dict.copy()) + name_dict = {} + if afi not in afi_list: + afi_list.append(afi) + config.update({"afi": afi}) + if "standard" in matches[0][1]: + standard = 1 + name = matches[0][1].split() + name_dict.update({"name": name[1]}) + name_dict.update({"standard": True}) + else: + name_dict.update({"name": matches[0][1]}) + else: + source_dict = {} + dest_dict = {} + dev_config = re.sub('-', '_', dev_config) + dev_config_remainder = dev_config.split() + if "fragment_rules" in dev_config: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update({"fragment_rules": True}) + if "remark" in dev_config: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update({"remark": ' '.join(dev_config_remainder[1:])}) + seq = re.search(r'\d+ (permit|deny) .*', dev_config) + if seq: + ace_dict.update({"sequence": dev_config_remainder.pop(0)}) + ace_dict.update({"grant": dev_config_remainder.pop(0)}) + if dev_config_remainder[0] == "vlan": + vlan_str = "" + dev_config_remainder.pop(0) + if dev_config_remainder[0] == "inner": + vlan_str = dev_config_remainder.pop(0) + " " + vlan_str = dev_config_remainder.pop(0) + " " + dev_config_remainder.pop(0) + ace_dict.update({"vlan": vlan_str}) + if not standard: + protocol = dev_config_remainder[0] + ace_dict.update({"protocol": dev_config_remainder.pop(0)}) + src_prefix = re.search(r'/', dev_config_remainder[0]) + src_address = re.search(r'[a-z\d:\.]+', dev_config_remainder[0]) + if dev_config_remainder[0] == "host": + source_dict.update({"host": dev_config_remainder.pop(1)}) + dev_config_remainder.pop(0) + elif dev_config_remainder[0] == "any": + source_dict.update({"any": True}) + dev_config_remainder.pop(0) + elif src_prefix: + source_dict.update({"subnet_address": dev_config_remainder.pop(0)}) + elif src_address: + source_dict.update({"address": dev_config_remainder.pop(0)}) + source_dict.update({"wildcard_bits": dev_config_remainder.pop(0)}) + if dev_config_remainder: + if dev_config_remainder[0] in operator: + port_dict = {} + src_port = "" + src_opr = dev_config_remainder.pop(0) + portlist = dev_config_remainder[:] + for config_remainder in portlist: + addr = re.search(r'[\.\:]', config_remainder) + if config_remainder == "any" or config_remainder == "host" or addr: + break + else: + src_port = src_port + " " + config_remainder + dev_config_remainder.pop(0) + src_port = src_port.strip() + port_dict.update({src_opr: src_port}) + source_dict.update({"port_protocol": port_dict}) + ace_dict.update({"source": source_dict}) + if not dev_config_remainder or standard: + if dev_config_remainder and "log" in dev_config_remainder: + ace_dict.update({"log": True}) + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + # acls_list.append(name_dict) + continue + dest_prefix = re.search(r'/', dev_config_remainder[0]) + dest_address = re.search(r'[a-z\d:\.]+', dev_config_remainder[0]) + if dev_config_remainder[0] == "host": + dest_dict.update({"host": dev_config_remainder.pop(1)}) + dev_config_remainder.pop(0) + elif dev_config_remainder[0] == "any": + dest_dict.update({"any": True}) + dev_config_remainder.pop(0) + elif dest_prefix: + dest_dict.update({"subnet_address": dev_config_remainder.pop(0)}) + elif dest_address: + dest_dict.update({"address": dev_config_remainder.pop(0)}) + dest_dict.update({"wildcard_bits": dev_config_remainder.pop(0)}) + if dev_config_remainder: + if dev_config_remainder[0] in operator: + port_dict = {} + dest_port = "" + dest_opr = dev_config_remainder.pop(0) + portlist = dev_config_remainder[:] + for config_remainder in portlist: + if config_remainder in operator or config_remainder in others: + break + else: + dest_port = dest_port + " " + config_remainder + dev_config_remainder.pop(0) + dest_port = dest_port.strip() + port_dict.update({dest_opr: dest_port}) + dest_dict.update({"port_protocol": port_dict}) + ace_dict.update({"destination": dest_dict}) + protocol_option_dict = {} + tcp_dict = {} + icmp_dict = {} + ip_dict = {} + if not dev_config_remainder: + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + # acls_list.append(name_dict) + continue + if protocol == "tcp" or "6": + protocol = "tcp" + flags_dict = {} + if dev_config_remainder[0] in flags: + flaglist = dev_config_remainder.copy() + for config_remainder in flaglist: + if config_remainder not in flags: + break + else: + flags_dict.update({config_remainder: True}) + dev_config_remainder.pop(0) + if bool(flags_dict): + tcp_dict.update({"flags": flags_dict}) + if bool(tcp_dict): + protocol_option_dict.update({"tcp": tcp_dict}) + if protocol == "icmp" or protocol == "icmpv6" \ + or protocol == "1" or protocol == "58": + if protocol == "1": + protocol = "icmp" + elif protocol == "58": + protocol = "icmpv6" + if dev_config_remainder[0] not in others: + icmp_dict.update({dev_config_remainder[0]: True}) + dev_config_remainder.pop(0) + if bool(icmp_dict): + protocol_option_dict.update({protocol: icmp_dict}) + if protocol == "ip" or "ipv6": + if dev_config_remainder[0] == "nexthop_group": + dev_config_remainder.pop(0) + ip_dict.update({"nexthop_group": dev_config_remainder.pop(0)}) + if bool(ip_dict): + protocol_option_dict.update({protocol: ip_dict}) + if bool(protocol_option_dict): + ace_dict.update({"protocol_options": protocol_option_dict}) + if dev_config_remainder[0] == "ttl": + dev_config_remainder.pop(0) + op = dev_config_remainder.pop(0) + ttl_dict = {op: dev_config_remainder.pop(0)} + ace_dict.update({"ttl": ttl_dict}) + for config_remainder in dev_config_remainder: + if config_remainder in others: + if config_remainder == "hop_limit": + hop_index = dev_config_remainder.index(config_remainder) + hoplimit_dict = {dev_config_remainder[hop_index + 1]: dev_config_remainder[hop_index + 2]} + ace_dict.update({"hop_limit": hoplimit_dict}) + dev_config_remainder.pop(0) + continue + ace_dict.update({config_remainder: True}) + dev_config_remainder.pop(0) + if dev_config_remainder: + config.update({"line": dev_config}) + return utils.remove_empties(config) + if bool(ace_dict): + ace_list.append(ace_dict.copy()) + if len(ace_list): + name_dict = name_dict.copy() + name_dict.update({"aces": ace_list[:]}) + acls_list.append(name_dict.copy()) + config.update({"acls": acls_list}) + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/eos/facts/facts.py b/lib/ansible/module_utils/network/eos/facts/facts.py index a40da912edd..c82997a1321 100644 --- a/lib/ansible/module_utils/network/eos/facts/facts.py +++ b/lib/ansible/module_utils/network/eos/facts/facts.py @@ -22,6 +22,7 @@ from ansible.module_utils.network.eos.facts.lldp_interfaces.lldp_interfaces impo from ansible.module_utils.network.eos.facts.vlans.vlans import VlansFacts from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces from ansible.module_utils.network.eos.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts +from ansible.module_utils.network.eos.facts.acls.acls import AclsFacts FACT_LEGACY_SUBSETS = dict( @@ -41,6 +42,7 @@ FACT_RESOURCE_SUBSETS = dict( lldp_interfaces=Lldp_interfacesFacts, vlans=VlansFacts, acl_interfaces=Acl_interfacesFacts, + acls=AclsFacts, ) diff --git a/lib/ansible/modules/network/eos/eos_acls.py b/lib/ansible/modules/network/eos/eos_acls.py new file mode 100644 index 00000000000..0dffd2c6dfd --- /dev/null +++ b/lib/ansible/modules/network/eos/eos_acls.py @@ -0,0 +1,925 @@ +#!/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 eos_acls +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: eos_acls +version_added: '2.10' +short_description: 'Manages IP access-list attributes of Arista EOS interfaces' +description: This module manages the IP access-list attributes of Arista EOS interfaces. +author: Gomathiselvi S (@GomathiselviS) +notes: +- Tested against Arista vEOS v4.20.10M +options: + config: + description: A dictionary of IP access-list options + type: list + elements: dict + suboptions: + afi: + description: + - The Address Family Indicator (AFI) for the Access Control Lists (ACL). + type: str + required: true + choices: ['ipv4', 'ipv6'] + acls: + description: + - A list of Access Control Lists (ACL). + type: list + elements: dict + suboptions: + standard: + description: standard access-list or not + type: bool + default: False + name: + description: Name of the acl-list + type: str + required: true + aces: + description: Filtering data + type: list + elements: dict + suboptions: + sequence: + description: sequence number for the ordered list of rules + type: int + remark: + description: Specify a comment + type: str + fragment_rules: + description: Add fragment rules + type: bool + grant: + description: Action to be applied on the rule + type: str + choices: ['permit', 'deny'] + line: + description: For fact gathering, any ACE that is not fully parsed, while show up as a value of this attribute. + type: str + aliases: ['ace'] + protocol: + description: + - Specify the protocol to match. + - Refer to vendor documentation for valid values. + type: str + vlan: + description: Vlan options + type: str + protocol_options: + description: All the possible sub options for the protocol chosen. + type: dict + suboptions: + tcp: + description: Options for tcp protocol. + type: dict + suboptions: + flags: + description: Match TCP packet flags + type: dict + suboptions: + ack: + description: Match on the ACK bit + type: bool + established: + description: Match established connections + type: bool + fin: + description: Match on the FIN bit + type: bool + psh: + description: Match on the PSH bit + type: bool + rst: + description: Match on the RST bit + type: bool + syn: + description: Match on the SYN bit + type: bool + urg: + description: Match on the URG bit + type: bool + icmp: + description: + - Internet Control Message Protocol settings. + type: dict + suboptions: + administratively_prohibited: + description: Administratively prohibited + type: bool + alternate_address: + description: Alternate address + type: bool + conversion_error: + description: Datagram conversion + type: bool + dod_host_prohibited: + description: Host prohibited + type: bool + dod_net_prohibited: + description: Net prohibited + type: bool + echo: + description: Echo (ping) + type: bool + echo_reply: + description: Echo reply + type: bool + general_parameter_problem: + description: Parameter problem + type: bool + host_isolated: + description: Host isolated + type: bool + host_precedence_unreachable: + description: Host unreachable for precedence + type: bool + host_redirect: + description: Host redirect + type: bool + host_tos_redirect: + description: Host redirect for TOS + type: bool + host_tos_unreachable: + description: Host unreachable for TOS + type: bool + host_unknown: + description: Host unknown + type: bool + host_unreachable: + description: Host unreachable + type: bool + information_reply: + description: Information replies + type: bool + information_request: + description: Information requests + type: bool + mask_reply: + description: Mask replies + type: bool + mask_request: + description: Mask requests + type: bool + 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 exceededs + type: bool + timestamp_reply: + description: Timestamp replies + type: bool + timestamp_request: + description: Timestamp requests + type: bool + traceroute: + description: Traceroute + type: bool + ttl_exceeded: + description: TTL exceeded + type: bool + unreachable: + description: All unreachables + type: bool + message_num: + description: icmp msg type number. + type: int + icmpv6: + description: Options for icmpv6. + type: dict + suboptions: + address_unreachable: + description: address unreachable + type: bool + beyond_scope: + description: beyond_scope + type: bool + echo_reply: + description: echo_reply + type: bool + echo_request: + description: echo reques + type: bool + erroneous_header: + description: erroneous header + type: bool + fragment_reassembly_exceeded: + description: fragment_reassembly_exceeded + type: bool + hop_limit_exceeded: + description: hop limit exceeded + type: bool + neighbor_advertisement: + description: neighbor advertisement + type: bool + neighbor_solicitation: + description: neighbor_solicitation + type: bool + no_admin: + description: no admin + type: bool + no_route: + description: no route + type: bool + packet_too_big: + description: packet too big + type: bool + parameter_problem: + description: parameter problem + type: bool + port_unreachable: + description: port unreachable + type: bool + redirect_message: + description: redirect message + type: bool + reject_route: + description: reject route + type: bool + router_advertisement: + description: router_advertisement + type: bool + router_solicitation: + description: router_solicitation + type: bool + source_address_failed: + description: source_address_failed + type: bool + source_routing_error: + description: source_routing_error + type: bool + time_exceeded: + description: time_exceeded + type: bool + unreachable: + description: unreachable + type: bool + unrecognized_ipv6_option: + description: unrecognized_ipv6_option + type: bool + unrecognized_next_header: + description: unrecognized_next_header + type: bool + ip: + description : Internet Protocol. + type: dict + suboptions: + nexthop_group: + description: Nexthop-group name. + type: str + ipv6: + description : Internet V6 Protocol. + type: dict + suboptions: + nexthop_group: + description: Nexthop-group name. + type: str + source: + description: The packet's source address + type: dict + suboptions: + address: + description: dotted decimal notation of IP address + type: str + wildcard_bits: + description: Source wildcard bits + type: str + subnet_address: + description: A subnet address + type: str + host: + description: Host IP address + type: str + any: + description: Rule matches all source addresses + type: bool + port_protocol: + description: Specify source port/protocoli, along with operator. (comes with tcp/udp). + type: dict + destination: + description: The packet's destination address + type: dict + suboptions: + address: + description: dotted decimal notation of IP address + type: str + wildcard_bits: + description: Source wildcard bits + type: str + subnet_address: + description: A subnet address + type: str + host: + description: Host IP address + type: str + any: + description: Rule matches all source addresses + type: bool + port_protocol: + description: Specify dest port/protocol, along with operator . (comes with tcp/udp). + type: dict + ttl: + description: Compares the TTL (time-to-live) value in the packet to a specified value + type: dict + suboptions: + eq: + description: Match a single TTL value + type: int + lt: + description: Match TTL lesser than this number + type: int + gt: + description: Match TTL greater than this number + type: int + neq: + description: Match TTL not equal to this value + type: int + fragments: + description: Match non-head fragment packets + type: bool + log: + description: Log matches against this rule + type: bool + tracked: + description: Match packets in existing ICMP/UDP/TCP connections + type: bool + hop_limit: + description: Hop limit value. + type: dict + 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 + version_added: "2.10" + type: str + + state: + description: + - The state the configuration should be left in. + type: str + choices: + ['deleted', 'merged', 'overridden', 'replaced', 'gathered', 'rendered', 'parsed'] + default: + merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +- name: Merge provided configuration with device configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "ospf" + source: + subnet_address: 20.0.0.0/8 + destnation: + any: true + state: merged + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 35 deny ospf 20.0.0.0/8 any +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# Using merged + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +- name: Merge to update the given configuration with an existing ace + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + log : true + ttl: + eq: 33 + state: merged + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 35 deny ospf 20.0.0.0/8 any ttl eq 33 log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# Using replaced + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + + +- name: Replace device configuration with provided configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "permit" + protocol: "ospf" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + state: replaced + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 35 permit ospf 20.0.0.0/8 any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + +# Using overridden + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ! +# ip access-list test3 +# 10 permit ip 35.33.0.0/16 any log +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + + +- name: override device configuration with provided configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + action: "permit" + protocol: "ospf" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + state: overridden + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 35 permit ospf 20.0.0.0/8 any +# ! + + +# Using deleted + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + +- name: Delete provided configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 30 + state: deleted + +# After state: +# ------------ +# +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 40 permit ip any any +# ! +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + + +# Before state: +# ------------- +# show running-config | section access-list +# ip access-list test1 +# 10 permit ip 10.10.10.0/24 any ttl eq 200 +# 20 permit ip 10.30.10.0/24 host 10.20.10.1 +# 30 deny tcp host 10.10.20.1 eq finger www any syn log +# 40 permit ip any any +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + +# ! + +- name: Delete provided configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + state: deleted + +# After state: +# ------------ +# +# show running-config | section access-list + +# ipv6 access-list test2 +# 10 deny icmpv6 any any reject-route hop-limit eq 20 + + +# using gathered + +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# ip access-list test2 +# 40 permit vlan 55 0xE2 icmpv6 any any log + +- name: Gather the exisitng condiguration + eos_acls: + state: gathered + +# returns: + + +# eos_acls: +# config: +# - afi: "ipv4" +# acls: +# - name: test1 +# aces: +# - sequence: 35 +# grant: "deny" +# protocol: "ospf" +# source: +# subnet_address: 20.0.0.0/8 +# destination: +# any: true +# - afi: "ipv6" +# acls: +# - name: test2 +# aces: +# - sequence: 40 +# grant: "permit" +# vlan: "55 0xE2" +# protocol: "icmpv6" +# log: true +# source: +# any: true +# destination: +# any: true + + +# using rendered + +- name: Delete provided configuration + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "ospf" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + - afi: "ipv6" + acls: + - name: test2 + aces: + - sequence: 40 + grant: "permit" + vlan: "55 0xE2" + protocol: "icmpv6" + log: true + source: + any: true + destination: + any: true + state: rendered + +# returns: + +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# ip access-list test2 +# 40 permit vlan 55 0xE2 icmpv6 any any log + + +# Using Parsed + +# parsed_acls.cfg + +# ipv6 access-list standard test2 +# 10 permit any log +# ! +# ip access-list test1 +# 35 deny ospf 20.0.0.0/8 any +# 45 remark Run by ansible +# 55 permit tcp any any +# ! + +- name: parse configs + eos_acls: + running_config: "{{ lookup('file', './parsed_acls.cfg') }}" + state: parsed + +# returns +# "parsed": [ +# { +# "acls": [ +# { +# "aces": [ +# { +# "destination": { +# "any": true +# }, +# "grant": "deny", +# "protocol": "ospf", +# "sequence": 35, +# "source": { +# "subnet_address": "20.0.0.0/8" +# } +# }, +# { +# "remark": "Run by ansible", +# "sequence": 45 +# }, +# { +# "destination": { +# "any": true +# }, +# "grant": "permit", +# "protocol": "tcp", +# "sequence": 55, +# "source": { +# "any": true +# } +# } +# ], +# "name": "test1" +# } +# ], +# "afi": "ipv4" +# }, +# { +# "acls": [ +# { +# "aces": [ +# { +# "grant": "permit", +# "log": true, +# "sequence": 10, +# "source": { +# "any": true +# } +# } +# ], +# "name": "test2", +# "standard": true +# } +# ], +# "afi": "ipv6" +# } +# ] + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - ipv6 access-list standard test2 + - 10 permit any log + - ip access-list test1 + - 35 deny ospf 20.0.0.0/8 any + - 45 remark Run by ansible + - 55 permit tcp any any +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.eos.argspec.acls.acls import AclsArgs +from ansible.module_utils.network.eos.config.acls.acls import Acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [('state', 'merged', ('config',)), + ('state', 'replaced', ('config',)), + ('state', 'overridden', ('config',)), + ('state', 'rendered', ('config',)), + ('state', 'parsed', ('running_config',))] + mutually_exclusive = [('config', 'running_config')] + + module = AnsibleModule(argument_spec=AclsArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive) + + result = Acls(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/eos/eos_facts.py b/lib/ansible/modules/network/eos/eos_facts.py index 05151319083..a410c5640d4 100644 --- a/lib/ansible/modules/network/eos/eos_facts.py +++ b/lib/ansible/modules/network/eos/eos_facts.py @@ -50,7 +50,7 @@ options: not be collected. Valid subsets are 'all', 'interfaces', 'l2_interfaces', 'l3_interfaces', 'lacp', 'lacp_interfaces', 'lag_interfaces', 'lldp_global', 'lldp_interfaces', - 'vlans'. + 'vlans', 'acls'. required: false type: list version_added: "2.9" diff --git a/test/integration/targets/eos_acls/defaults/main.yaml b/test/integration/targets/eos_acls/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/eos_acls/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/eos_acls/meta/main.yaml b/test/integration/targets/eos_acls/meta/main.yaml new file mode 100644 index 00000000000..e5c8cd02f04 --- /dev/null +++ b/test/integration/targets/eos_acls/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_eos_tests diff --git a/test/integration/targets/eos_acls/tasks/cli.yaml b/test/integration/targets/eos_acls/tasks/cli.yaml new file mode 100644 index 00000000000..66941b1f491 --- /dev/null +++ b/test/integration/targets/eos_acls/tasks/cli.yaml @@ -0,0 +1,18 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/common" + 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 }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }} 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/eos_acls/tasks/eapi.yaml b/test/integration/targets/eos_acls/tasks/eapi.yaml new file mode 100644 index 00000000000..cb5f04d80c9 --- /dev/null +++ b/test/integration/targets/eos_acls/tasks/eapi.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all eapi test cases + find: + paths: "{{ role_path }}/tests/common" + patterns: "{{ testcase }}.yaml" + delegate_to: localhost + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include: "{{ test_case_to_run }} ansible_connection=httpapi" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_acls/tasks/main.yaml b/test/integration/targets/eos_acls/tasks/main.yaml new file mode 100644 index 00000000000..970e74171ea --- /dev/null +++ b/test/integration/targets/eos_acls/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- { include: cli.yaml, tags: ['cli'] } +- { include: eapi.yaml, tags: ['eapi'] } diff --git a/test/integration/targets/eos_acls/tests/common/_parsed.cfg b/test/integration/targets/eos_acls/tests/common/_parsed.cfg new file mode 100644 index 00000000000..11758ce538f --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/_parsed.cfg @@ -0,0 +1,4 @@ +ip access-list test1 +35 deny tcp 20.0.0.0/8 any log +45 remark Run by ansible +55 permit tcp any any diff --git a/test/integration/targets/eos_acls/tests/common/_parsed_cfg.yaml b/test/integration/targets/eos_acls/tests/common/_parsed_cfg.yaml new file mode 100644 index 00000000000..5a655ce534d --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/_parsed_cfg.yaml @@ -0,0 +1,11 @@ +--- +- name: Setup + cli_config: + config: "{{ lines }}" + become: yes + vars: + lines: | + ip access-list test1 + 35 deny tcp 20.0.0.0/8 any log + 45 remark Run by ansible + 55 permit tcp any any diff --git a/test/integration/targets/eos_acls/tests/common/_populate.yaml b/test/integration/targets/eos_acls/tests/common/_populate.yaml new file mode 100644 index 00000000000..07ed5b96734 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/_populate.yaml @@ -0,0 +1,49 @@ +--- +- name: Setup + eos_acls: &merged + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + - grant: "permit" + protocol: "6" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + source: + any: true + port_protocol: + eq: "25" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + log: "true" + source: + any: true + state: merged + become: yes + register: result diff --git a/test/integration/targets/eos_acls/tests/common/_remove_config.yaml b/test/integration/targets/eos_acls/tests/common/_remove_config.yaml new file mode 100644 index 00000000000..a8a351d80b1 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/_remove_config.yaml @@ -0,0 +1,8 @@ +--- +- name: Setup + eos_acls: + config: + - afi: "ipv4" + - afi: "ipv6" + state: deleted + become: yes diff --git a/test/integration/targets/eos_acls/tests/common/deleted.yaml b/test/integration/targets/eos_acls/tests/common/deleted.yaml new file mode 100644 index 00000000000..750e214d494 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/deleted.yaml @@ -0,0 +1,168 @@ +--- +- debug: + msg: "Start eos_acls deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config1: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 55 + grant: "permit" + protocol: "tcp" + source: + any: true + destination: + any: true + - remark: "Run by ansible" + sequence: 45 + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + sequence: 10 + log: "true" + source: + any: true + +- set_fact: + config2: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + sequence: 45 + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" +- set_fact: + config3: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + sequence: 45 + +- block: + - name: Delete attributes of given acls. + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + state: deleted + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 2" + - "result.changed == true" + - "ansible_facts.network_resources.acls|symmetric_difference(result.after) == [] " + become: yes + + - name: Delete afi of given acls. + eos_acls: + config: + - afi: "ipv6" + state: deleted + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 1" + - "result.changed == true" + - "ansible_facts.network_resources.acls|symmetric_difference(result.after) == [] " + become: yes + + - name: Delete attributes of given named acl. + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test4 + state: deleted + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 1" + - "result.changed == true" + - "ansible_facts.network_resources.acls|symmetric_difference(result.after) == [] " + become: yes + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/gathered.yaml b/test/integration/targets/eos_acls/tests/common/gathered.yaml new file mode 100644 index 00000000000..0baedb13aff --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/gathered.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: "START eos_acls gathered integration tests on connection={{ ansible_connection }}" + + +- include_tasks: _populate.yaml + +- block: + - name: Gathered the provided configuration with the exisiting running configuration + eos_acls: &gathered + config: + state: gathered + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - name: Assert + assert: + that: + - "ansible_facts.network_resources.acls | symmetric_difference(result.gathered) == []" + + + - name: Gather the existing running configuration (IDEMPOTENT) + eos_acls: *gathered + become: yes + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/merged.yaml b/test/integration/targets/eos_acls/tests/common/merged.yaml new file mode 100644 index 00000000000..9bfd233805f --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/merged.yaml @@ -0,0 +1,152 @@ +--- +- debug: + msg: "Start eos_acls merged integration tests ansible_connection={{ ansible_connection }}" + + +- set_fact: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + sequence: 45 + - grant: "permit" + sequence: 55 + protocol: "tcp" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + sequence: 10 + log: "true" + source: + any: true + +- block: + - name: merge attributes of given acls. + eos_acls: &merged + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + - grant: "permit" + protocol: "6" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + source: + any: true + port_protocol: + eq: "25" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + log: "true" + source: + any: true + state: merged + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + become: yes + + - name: Idempotency check + eos_acls: *merged + become: yes + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + - "ansible_facts.network_resources.acls|symmetric_difference(result.before) == []" + + - name: merge attributes with an existing ace + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + log: true + ttl: + eq: 33 + source: + any: true + state: merged + become: yes + register: result + + - assert: + that: + - "result.changed == true" + - "result.commands|length == 3" + - "'no 35' in result.commands" + - "'35 deny tcp any any ttl eq 33 log' in result.commands" + + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/overridden.yaml b/test/integration/targets/eos_acls/tests/common/overridden.yaml new file mode 100644 index 00000000000..4c8e51998eb --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/overridden.yaml @@ -0,0 +1,71 @@ +--- +- debug: + msg: "Start eos_acls merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 10 + grant: "permit" + protocol: "ospf" + source: + any: true + destination: + any: true + log: true + +- block: + - name: overriden attributes with given acls. + eos_acls: &overridden + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - grant: "permit" + sequence: 10 + protocol: "ospf" + source: + any: true + destination: + any: true + log: true + state: overridden + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + - "'ip access-list test1' in result.commands" + - "'10 permit ospf any any log' in result.commands" + - "ansible_facts.network_resources.acls|symmetric_difference(result.after) == []" + become: yes + + - name: Idempotency check + eos_acls: *overridden + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + - "ansible_facts.network_resources.acls|symmetric_difference(result.before) == []" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/parsed.yaml b/test/integration/targets/eos_acls/tests/common/parsed.yaml new file mode 100644 index 00000000000..ffadce84ed6 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/parsed.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: "START eos_acls parsed integration tests on connection={{ ansible_connection }}" + +- include_tasks: _parsed_cfg.yaml + +- name: Gather acls facts + eos_facts: + gather_subset: + - default + gather_network_resources: + - acls + become: yes + register: acls_facts + +- name: Provide the running configuration for parsing (config to be parsed) + eos_acls: &parsed + running_config: + "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "ansible_facts.network_resources.acls|symmetric_difference(result.parsed) == []" + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/rendered.yaml b/test/integration/targets/eos_acls/tests/common/rendered.yaml new file mode 100644 index 00000000000..f447c52ac04 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/rendered.yaml @@ -0,0 +1,80 @@ +--- +- debug: + msg: "START eos_acls rendered integration tests on connection={{ ansible_connection }}" + + +- block: + - name: Structure provided configuration into device specific commands + eos_acls: &rendered + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + - grant: "permit" + protocol: "6" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + source: + any: true + port_protocol: + eq: "25" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + log: "true" + source: + any: true + state: rendered + become: yes + register: result + + + - name: Assert that correct set of commands were generated + vars: + lines: + - ip access-list test1 + - 35 deny tcp 20.0.0.0/8 any log + - remark Run by ansible + - permit tcp any any + - ip access-list test4 + - permit tcp any eq smtp any eq www ttl eq 55 + - ipv6 access-list standard test2 + - permit any log + + assert: + that: + - "{{ lines | symmetric_difference(result['rendered']) |length == 0 }}" + + - name: Structure provided configuration into device specific commands (IDEMPOTENT) + eos_acls: *rendered + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" diff --git a/test/integration/targets/eos_acls/tests/common/replaced.yaml b/test/integration/targets/eos_acls/tests/common/replaced.yaml new file mode 100644 index 00000000000..068e177ef51 --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/replaced.yaml @@ -0,0 +1,94 @@ +--- +- debug: + msg: "Start eos_acls replaced integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 10 + grant: "permit" + protocol: "ospf" + source: + any: true + destination: + any: true + log: true + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + sequence: 10 + log: "true" + source: + any: true + +- block: + - name: replace attributes with given acls. + eos_acls: &replaced + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - grant: "permit" + sequence: 10 + protocol: "ospf" + source: + any: true + destination: + any: true + log: true + state: replaced + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.commands|length == 5" + - "result.changed == true" + - "ansible_facts.network_resources.acls|symmetric_difference(result.after) == []" + become: yes + + - name: Idempotency check + eos_acls: *replaced + become: yes + register: result + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + - "ansible_facts.network_resources.acls|symmetric_difference(result.before) == []" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/tests/common/rtt.yaml b/test/integration/targets/eos_acls/tests/common/rtt.yaml new file mode 100644 index 00000000000..4b78a8e782e --- /dev/null +++ b/test/integration/targets/eos_acls/tests/common/rtt.yaml @@ -0,0 +1,101 @@ +--- +- debug: + msg: "Start eos_acls round trip integration tests ansible_connection={{ ansible_connection }}" + + +- block: + - name: merge attributes of given acls(apply base config). + eos_acls: &merged + config: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + - grant: "permit" + protocol: "6" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + source: + any: true + port_protocol: + eq: "25" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + log: "true" + source: + any: true + state: merged + become: yes + register: base_config + + - eos_facts: + gather_network_resources: acls + become: yes + + - assert: + that: + - "base_config.commands|length == 8" + - "base_config.changed == true" + - "ansible_facts.network_resources.acls|symmetric_difference(base_config.after) == []" + + - name: Apply the provided configuration (config to be reverted) + eos_acls: + config: + - afi: "ipv4" + acls: + - name: test3 + aces: + - sequence: 100 + grant: "permit" + protocol: "icmp" + source: + any: true + destination: + any: true + log: true + become: yes + register: result + + - name: Assert that changes were applied + assert: + that: + - "{{ round_trip['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Revert back to base config using facts round trip + eos_acls: + config: "{{ ansible_facts['network_resources']['acls'] }}" + state: overridden + become: yes + register: revert + + - name: Assert that config was reverted + assert: + that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_acls/vars/main.yaml b/test/integration/targets/eos_acls/vars/main.yaml new file mode 100644 index 00000000000..3efbfb76fa9 --- /dev/null +++ b/test/integration/targets/eos_acls/vars/main.yaml @@ -0,0 +1,110 @@ +round_trip: + after: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + sequence: 45 + - grant: "permit" + sequence: 55 + protocol: "tcp" + source: + any: true + destination: + any: true + - name: test3 + aces: + - sequence: 100 + grant: "permit" + protocol: "icmp" + source: + any: true + destination: + any: true + log: true + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + sequence: 10 + log: true + source: + any: true + + commands: + - "ip access-list test3" + - "100 permit icmp any any log" + +base_config: + after: + - afi: "ipv4" + acls: + - name: test1 + aces: + - sequence: 35 + grant: "deny" + protocol: "tcp" + source: + subnet_address: 20.0.0.0/8 + destination: + any: true + log: true + - remark: "Run by ansible" + sequence: 45 + - grant: "permit" + sequence: 55 + protocol: "tcp" + source: + any: true + destination: + any: true + - name: test4 + aces: + - grant: "permit" + sequence: 10 + source: + any: true + port_protocol: + eq: "smtp" + destination: + any: true + port_protocol: + eq: "www" + protocol: "tcp" + ttl: + eq: "55" + - afi: "ipv6" + acls: + - name: test2 + standard: true + aces: + - grant: "permit" + log: "true" + source: + any: true diff --git a/test/units/modules/network/eos/eos_module.py b/test/units/modules/network/eos/eos_module.py index 12f84af41a1..ddc92b04970 100644 --- a/test/units/modules/network/eos/eos_module.py +++ b/test/units/modules/network/eos/eos_module.py @@ -23,6 +23,7 @@ import json import os from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible.module_utils.network.common.utils import dict_diff, param_list_to_dict fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') @@ -37,7 +38,6 @@ def load_fixture(name): with open(path) as f: data = f.read() - try: data = json.loads(data) except Exception: @@ -62,7 +62,6 @@ class TestEosModule(ModuleTestCase): else: result = self.changed(changed) self.assertEqual(result['changed'], changed, result) - if commands is not None: if transport == 'eapi': cmd = [] diff --git a/test/units/modules/network/eos/fixtures/eos_acls_config.cfg b/test/units/modules/network/eos/fixtures/eos_acls_config.cfg new file mode 100644 index 00000000000..fa4e4aae572 --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_acls_config.cfg @@ -0,0 +1,3 @@ +ip access-list test1 + 35 deny tcp 20.0.0.0/8 any log + 45 permit tcp any any diff --git a/test/units/modules/network/eos/test_eos_acls.py b/test/units/modules/network/eos/test_eos_acls.py new file mode 100644 index 00000000000..6a0947d2d2d --- /dev/null +++ b/test/units/modules/network/eos/test_eos_acls.py @@ -0,0 +1,278 @@ +# +# (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.eos import eos_acls +from ansible.module_utils.network.eos.config.acls.acls import add_commands +from units.modules.utils import set_module_args +from .eos_module import TestEosModule, load_fixture +import itertools + + +class TestEosAclsModule(TestEosModule): + module = eos_acls + + def setUp(self): + super(TestEosAclsModule, 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.eos.providers.providers.CliProvider.edit_config' + ) + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch( + 'ansible.module_utils.network.eos.facts.acls.acls.AclsFacts.get_device_data' + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestEosAclsModule, 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, transport='cli', filename=None): + if filename is None: + filename = 'eos_acls_config.cfg' + + def load_from_file(*args, **kwargs): + output = load_fixture(filename) + return output + + self.execute_show_command.side_effect = load_from_file + + def test_eos_acls_merged(self): + set_module_args( + dict(config=[ + dict(afi="ipv6", + acls=[ + dict(name="test2", + standard="true", + aces=[ + dict(sequence="10", + grant="permit", + protocol="ospf", + source=dict(subnet_address="30.2.0.0/8"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], state="merged")) + commands = ['ipv6 access-list standard test2', '10 permit ospf 30.2.0.0/8 any log'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_merged_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(sequence="35", + grant="deny", + protocol="tcp", + source=dict(subnet_address="20.0.0.0/8"), + destination=dict(any="true"), + log="true"), + dict(grant="permit", + source=dict(any="true"), + destination=dict(any="true"), + protocol=6) + ]) + ]) + ], state="merged")) + self.execute_module(changed=False, commands=[]) + + def test_eos_acls_replaced(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(sequence="10", + grant="permit", + protocol="ospf", + source=dict(subnet_address="30.2.0.0/8"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], state="replaced")) + commands = ['ip access-list test1', 'no 35', 'no 45', '10 permit ospf 30.2.0.0/8 any log'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_replaced_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(sequence="35", + grant="deny", + protocol="tcp", + source=dict(subnet_address="20.0.0.0/8"), + destination=dict(any="true"), + log="true"), + dict(grant="permit", + source=dict(any="true"), + destination=dict(any="true"), + sequence="45", + protocol="tcp") + ]) + ]) + ], state="replaced")) + self.execute_module(changed=False, commands=[]) + + def test_eos_acls_overridden(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(sequence="10", + grant="permit", + protocol="ospf", + source=dict(subnet_address="30.2.0.0/8"), + destination=dict(any="true"), + log="true") + ]) + ]) + ], state="overridden")) + commands = ['ip access-list test1', 'no 35', 'no 45', 'ip access-list test1', '10 permit ospf 30.2.0.0/8 any log'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_overridden_idempotent(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(sequence="35", + grant="deny", + protocol="tcp", + source=dict(subnet_address="20.0.0.0/8"), + destination=dict(any="true"), + log="true"), + dict(grant="permit", + source=dict(any="true"), + destination=dict(any="true"), + sequence="45", + protocol="tcp") + ]) + ]) + ], state="overridden")) + self.execute_module(changed=False, commands=[]) + + def test_eos_acls_deletedaces(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(grant="permit", + sequence="45", + source=dict(any="true"), + destination=dict(any="true"), + protocol=6) + ]) + ]) + ], state="deleted")) + commands = ['ip access-list test1', 'no 45'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_deletedacls(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1") + ]) + ], state="deleted")) + commands = ['no ip access-list test1'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_deletedafis(self): + set_module_args( + dict(config=[ + dict(afi="ipv4") + ], state="deleted")) + commands = ['no ip access-list test1'] + self.execute_module(changed=True, commands=commands) + + def test_eos_acls_gathered(self): + set_module_args( + dict(config=[], + state="gathered")) + result = self.execute_module(changed=False, filename='eos_acls_config.cfg') + commands = [] + for gathered_cmds in result['gathered']: + cfg = add_commands(gathered_cmds) + commands.append(cfg) + commands = list(itertools.chain(*commands)) + config_commands = ['ip access-list test1', '35 deny tcp 20.0.0.0/8 any log', '45 permit tcp any any'] + self.assertEqual(sorted(config_commands), sorted(commands), result['gathered']) + + def test_eos_acls_rendered(self): + set_module_args( + dict(config=[ + dict(afi="ipv4", + acls=[ + dict(name="test1", + aces=[ + dict(grant="permit", + sequence="45", + source=dict(any="true"), + destination=dict(any="true"), + protocol=6) + ]) + ]) + ], state="rendered")) + commands = ['ip access-list test1', '45 permit tcp any any'] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), sorted(commands), result['rendered']) + + def test_eos_acls_parsed(self): + set_module_args( + dict(running_config="ipv6 access-list test2\n 10 permit icmpv6 host 10.2.33.1 any ttl eq 25", + state="parsed")) + commands = ['ipv6 access-list test2', '10 permit icmpv6 host 10.2.33.1 any ttl eq 25'] + result = self.execute_module(changed=False) + parsed_commands = [] + for cmds in result['parsed']: + cfg = add_commands(cmds) + parsed_commands.append(cfg) + parsed_commands = list(itertools.chain(*parsed_commands)) + self.assertEqual(sorted(parsed_commands), sorted(commands), result['parsed'])