Add iosxr_acls RM (#66207)

Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
This commit is contained in:
Nilashish Chakraborty 2020-02-24 18:34:27 +05:30 committed by GitHub
parent 3acd8f6f7f
commit b818436c5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 4341 additions and 2 deletions

View file

@ -0,0 +1,644 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The arg spec for the iosxr_acls module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class AclsArgs(object): # pylint: disable=R0903
"""The arg spec for the iosxr_acls module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'running_config': {
'type': 'str'
},
'config': {
'elements': 'dict',
'options': {
'acls': {
'elements': 'dict',
'options': {
'name': {
'type': 'str'
},
'aces': {
'elements': 'dict',
'mutually_exclusive': [['grant', 'remark', 'line']],
'options': {
'destination': {
'mutually_exclusive': [['address', 'any', 'host', 'prefix'], ['wildcard_bits', 'any', 'host', 'prefix']],
'options': {
'host': {
'type': 'str'
},
'address': {
'type': 'str'
},
'any': {
'type': 'bool'
},
'prefix': {
'type': 'str'
},
'port_protocol': {
'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']],
'options': {
'eq': {
'type': 'str'
},
'gt': {
'type': 'str'
},
'lt': {
'type': 'str'
},
'neq': {
'type': 'str'
},
'range': {
'options': {
'end': {
'type': 'str'
},
'start': {
'type': 'str'
}
},
'required_together': [['start', 'end']],
'type': 'dict'
}
},
'type': 'dict'
},
'wildcard_bits': {
'type': 'str'
}
},
'required_together': [['address', 'wildcard_bits']],
'type': 'dict'
},
'dscp': {
'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']],
'type': 'dict',
'options': {
'eq': {
'type': 'str'
},
'gt': {
'type': 'str'
},
'lt': {
'type': 'str'
},
'neq': {
'type': 'str'
},
'range': {
'options': {
'end': {
'type': 'str'
},
'start': {
'type': 'str'
}
},
'required_together': [['start', 'end']],
'type': 'dict'
}
},
},
'fragments': {
'type': 'bool'
},
'capture': {
'type': 'bool'
},
'destopts': {
'type': 'bool'
},
'authen': {
'type': 'bool'
},
'routing': {
'type': 'bool'
},
'hop_by_hop': {
'type': 'bool'
},
'grant': {
'type': 'str',
'choices': ['permit', 'deny'],
},
'icmp_off': {
'type': 'bool'
},
'log': {
'type': 'bool'
},
'log_input': {
'type': 'bool'
},
'line': {
'type': 'str',
'aliases': ['ace']
},
'packet_length': {
'mutually_exclusive': [['eq', 'lt', 'neq', 'range'], ['eq', 'gt', 'neq', 'range']],
'options': {
'eq': {
'type': 'int'
},
'gt': {
'type': 'int'
},
'lt': {
'type': 'int'
},
'neq': {
'type': 'int'
},
'range': {
'options': {
'end': {
'type': 'int'
},
'start': {
'type': 'int'
}
},
'type': 'dict'
}
},
'type':
'dict'
},
'precedence': {
'type': 'str'
},
'protocol': {
'type': 'str'
},
'protocol_options': {
'mutually_exclusive': [['icmp', 'tcp', 'igmp', 'icmpv6']],
'options': {
'icmpv6': {
'type': 'dict',
'options': {
'address_unreachable': {
'type': 'bool'
},
'administratively_prohibited':
{
'type': 'bool'
},
'beyond_scope_of_source_address':
{
'type': 'bool'
},
'destination_unreachable': {
'type': 'bool'
},
'echo': {
'type': 'bool'
},
'echo_reply': {
'type': 'bool'
},
'erroneous_header_field': {
'type': 'bool'
},
'group_membership_query': {
'type': 'bool'
},
'group_membership_report': {
'type': 'bool'
},
'group_membership_termination':
{
'type': 'bool'
},
'host_unreachable': {
'type': 'bool'
},
'nd_na': {
'type': 'bool'
},
'nd_ns': {
'type': 'bool'
},
'neighbor_redirect': {
'type': 'bool'
},
'no_route_to_destination': {
'type': 'bool'
},
'node_information_request_is_refused':
{
'type': 'bool'
},
'node_information_successful_reply':
{
'type': 'bool'
},
'packet_too_big': {
'type': 'bool'
},
'parameter_problem': {
'type': 'bool'
},
'port_unreachable': {
'type': 'bool'
},
'query_subject_is_IPv4address':
{
'type': 'bool'
},
'query_subject_is_IPv6address':
{
'type': 'bool'
},
'query_subject_is_domainname': {
'type': 'bool'
},
'reassembly_timeout': {
'type': 'bool'
},
'redirect': {
'type': 'bool'
},
'router_advertisement': {
'type': 'bool'
},
'router_renumbering': {
'type': 'bool'
},
'router_solicitation': {
'type': 'bool'
},
'rr_command': {
'type': 'bool'
},
'rr_result': {
'type': 'bool'
},
'rr_seqnum_reset': {
'type': 'bool'
},
'time_exceeded': {
'type': 'bool'
},
'ttl_exceeded': {
'type': 'bool'
},
'unknown_query_type': {
'type': 'bool'
},
'unreachable': {
'type': 'bool'
},
'unrecognized_next_header': {
'type': 'bool'
},
'unrecognized_option': {
'type': 'bool'
},
'whoareyou_reply': {
'type': 'bool'
},
'whoareyou_request': {
'type': 'bool'
}
}
},
'icmp': {
'options': {
'administratively_prohibited':
{
'type': 'bool'
},
'alternate_address': {
'type': 'bool'
},
'conversion_error': {
'type': 'bool'
},
'dod_host_prohibited': {
'type': 'bool'
},
'dod_net_prohibited': {
'type': 'bool'
},
'echo': {
'type': 'bool'
},
'echo_reply': {
'type': 'bool'
},
'general_parameter_problem': {
'type': 'bool'
},
'host_isolated': {
'type': 'bool'
},
'host_precedence_unreachable':
{
'type': 'bool'
},
'host_redirect': {
'type': 'bool'
},
'host_tos_redirect': {
'type': 'bool'
},
'host_tos_unreachable': {
'type': 'bool'
},
'host_unknown': {
'type': 'bool'
},
'host_unreachable': {
'type': 'bool'
},
'information_reply': {
'type': 'bool'
},
'information_request': {
'type': 'bool'
},
'mask_reply': {
'type': 'bool'
},
'mask_request': {
'type': 'bool'
},
'mobile_redirect': {
'type': 'bool'
},
'net_redirect': {
'type': 'bool'
},
'net_tos_redirect': {
'type': 'bool'
},
'net_tos_unreachable': {
'type': 'bool'
},
'net_unreachable': {
'type': 'bool'
},
'network_unknown': {
'type': 'bool'
},
'no_room_for_option': {
'type': 'bool'
},
'option_missing': {
'type': 'bool'
},
'packet_too_big': {
'type': 'bool'
},
'parameter_problem': {
'type': 'bool'
},
'port_unreachable': {
'type': 'bool'
},
'precedence_unreachable': {
'type': 'bool'
},
'protocol_unreachable': {
'type': 'bool'
},
'reassembly_timeout': {
'type': 'bool'
},
'redirect': {
'type': 'bool'
},
'router_advertisement': {
'type': 'bool'
},
'router_solicitation': {
'type': 'bool'
},
'source_quench': {
'type': 'bool'
},
'source_route_failed': {
'type': 'bool'
},
'time_exceeded': {
'type': 'bool'
},
'timestamp_reply': {
'type': 'bool'
},
'timestamp_request': {
'type': 'bool'
},
'traceroute': {
'type': 'bool'
},
'ttl_exceeded': {
'type': 'bool'
},
'unreachable': {
'type': 'bool'
}
},
'type': 'dict'
},
'igmp': {
'options': {
'dvmrp': {
'type': 'bool'
},
'host_query': {
'type': 'bool'
},
'host_report': {
'type': 'bool'
},
'mtrace': {
'type': 'bool'
},
'mtrace_response': {
'type': 'bool'
},
'pim': {
'type': 'bool'
},
'trace': {
'type': 'bool'
}
},
'type': 'dict'
},
'tcp': {
'options': {
'ack': {
'type': 'bool'
},
'established': {
'type': 'bool'
},
'fin': {
'type': 'bool'
},
'psh': {
'type': 'bool'
},
'rst': {
'type': 'bool'
},
'syn': {
'type': 'bool'
},
'urg': {
'type': 'bool'
}
},
'type': 'dict'
}
},
'type': 'dict'
},
'remark': {
'type': 'str'
},
'sequence': {
'type': 'int'
},
'source': {
'mutually_exclusive': [['address', 'any', 'host', 'prefix'], ['wildcard_bits', 'any', 'host', 'prefix']],
'options': {
'host': {
'type': 'str'
},
'address': {
'type': 'str'
},
'any': {
'type': 'bool'
},
'prefix': {
'type': 'str'
},
'port_protocol': {
'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']],
'options': {
'eq': {
'type': 'str'
},
'gt': {
'type': 'str'
},
'lt': {
'type': 'str'
},
'neq': {
'type': 'str'
},
'range': {
'options': {
'end': {
'type': 'str'
},
'start': {
'type': 'str'
}
},
'required_together': [['start', 'end']],
'type': 'dict'
}
},
'type': 'dict'
},
'wildcard_bits': {
'type': 'str'
}
},
'required_together': [['address', 'wildcard_bits']],
'type': 'dict'
},
'ttl': {
'mutually_exclusive': [['eq', 'gt', 'lt', 'neq', 'range']],
'options': {
'eq': {
'type': 'int'
},
'gt': {
'type': 'int'
},
'lt': {
'type': 'int'
},
'neq': {
'type': 'int'
},
'range': {
'options': {
'end': {
'type': 'int'
},
'start': {
'type': 'int'
}
},
'type': 'dict'
}
},
'type': 'dict'
}
},
'type': 'list'
},
},
'type': 'list'
},
'afi': {
'choices': ['ipv4', 'ipv6'],
'required': True,
'type': 'str'
}
},
'type': 'list'
},
'state': {
'choices': [
'merged', 'replaced', 'overridden', 'deleted', 'gathered',
'rendered', 'parsed'
],
'default': 'merged',
'type': 'str'
}
} # pylint: disable=C0301

View file

@ -0,0 +1,442 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr_acls class
It is in this file where the current configuration (as dict)
is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.iosxr.utils.utils \
import (
flatten_dict,
prefix_to_address_wildcard,
is_ipv4_address
)
from ansible.module_utils.network.iosxr.argspec.acls.acls import AclsArgs
from ansible.module_utils.network.common.utils \
import (
to_list,
search_obj_in_list,
dict_diff,
remove_empties,
dict_merge,
)
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.iosxr.facts.facts import Facts
class Acls(ConfigBase):
"""
The iosxr_acls class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'acls',
]
def __init__(self, module):
super(Acls, self).__init__(module)
def get_acls_facts(self, data=None):
""" Get the 'facts' (the current configuration)
:rtype: A dictionary
:returns: The current configuration as a dictionary
"""
facts, _warnings = Facts(self._module).get_facts(
self.gather_subset, self.gather_network_resources, data=data)
acls_facts = facts["ansible_network_resources"].get("acls")
if not acls_facts:
return []
return acls_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
warnings = list()
commands = list()
if self.state in self.ACTION_STATES:
existing_acls_facts = self.get_acls_facts()
else:
existing_acls_facts = []
if self.state in self.ACTION_STATES or self.state == "rendered":
commands.extend(self.set_config(existing_acls_facts))
if commands and self.state in self.ACTION_STATES:
if not self._module.check_mode:
self._connection.edit_config(commands)
result["changed"] = True
if self.state in self.ACTION_STATES:
result["commands"] = commands
if self.state in self.ACTION_STATES or self.state == "gathered":
changed_acls_facts = self.get_acls_facts()
elif self.state == "rendered":
result["rendered"] = commands
elif self.state == "parsed":
running_config = self._module.params["running_config"]
if not running_config:
self._module.fail_json(
msg="value of running_config parameter must not be empty for state parsed"
)
result["parsed"] = self.get_acls_facts(data=running_config)
if self.state in self.ACTION_STATES:
result["before"] = existing_acls_facts
if result["changed"]:
result["after"] = changed_acls_facts
elif self.state == "gathered":
result["gathered"] = changed_acls_facts
result["warnings"] = warnings
return result
def set_config(self, existing_acls_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
want = self._module.params['config']
have = existing_acls_facts
resp = self.set_state(want, have)
return to_list(resp)
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
state = self._module.params['state']
commands = []
if state in ('overridden', 'merged', 'replaced',
'rendered') and not want:
self._module.fail_json(
msg='value of config parameter must not be empty for state {0}'
.format(state))
if state == 'overridden':
commands.extend(self._state_overridden(want, have))
elif state == 'deleted':
commands.extend(self._state_deleted(want, have))
else:
# Instead of passing entire want and have
# list of dictionaries to the respective
# _state_* methods we are passing the want
# and have dictionaries per AFI
for item in want:
afi = item['afi']
obj_in_have = search_obj_in_list(afi, have, key='afi')
if state == 'merged' or self.state == 'rendered':
commands.extend(
self._state_merged(remove_empties(item), obj_in_have))
elif state == 'replaced':
commands.extend(
self._state_replaced(remove_empties(item),
obj_in_have))
return commands
def _state_replaced(self, want, have):
""" The command generator when state is replaced
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
for want_acl in want['acls']:
have_acl = search_obj_in_list(want_acl['name'], have['acls']) or {}
acl_updates = []
for have_ace in have_acl.get('aces', []):
want_ace = search_obj_in_list(have_ace['sequence'], want_acl['aces'], key='sequence') or {}
if not want_ace:
acl_updates.append('no {0}'.format(have_ace['sequence']))
for want_ace in want_acl.get('aces', []):
have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {}
set_cmd = self._set_commands(want_ace, have_ace)
if set_cmd:
acl_updates.append(set_cmd)
if acl_updates:
acl_updates.insert(0, '{0} access-list {1}'.format(want['afi'], want_acl['name']))
commands.extend(acl_updates)
return commands
def _state_overridden(self, want, have):
""" The command generator when state is overridden
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
# Remove extraneous AFI that are present in config but not
# specified in `want`
for have_afi in have:
want_afi = search_obj_in_list(have_afi['afi'], want, key='afi') or {}
if not want_afi:
for acl in have_afi.get('acls', []):
commands.append('no {0} access-list {1}'.format(have_afi['afi'], acl['name']))
# First we remove the extraneous ACLs from the AFIs that
# are present both in `want` and in `have` and then
# we call `_state_replaced` to update the ACEs within those ACLs
for want_afi in want:
want_afi = remove_empties(want_afi)
have_afi = search_obj_in_list(want_afi['afi'], have, key='afi') or {}
if have_afi:
for have_acl in have_afi.get('acls', []):
want_acl = search_obj_in_list(have_acl['name'], want_afi.get('acls', [])) or {}
if not want_acl:
commands.append('no {0} access-list {1}'.format(have_afi['afi'], have_acl['name']))
commands.extend(self._state_replaced(want_afi, have_afi))
return commands
def _state_merged(self, want, have):
""" The command generator when state is merged
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
commands = []
if not have:
have = {}
for want_acl in want['acls']:
have_acl = search_obj_in_list(want_acl['name'], have.get('acls', {})) or {}
acl_updates = []
for want_ace in want_acl['aces']:
have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {}
set_cmd = self._set_commands(want_ace, have_ace)
if set_cmd:
acl_updates.append(set_cmd)
if acl_updates:
acl_updates.insert(0, '{0} access-list {1}'.format(want['afi'], want_acl['name']))
commands.extend(acl_updates)
return commands
def _state_deleted(self, want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
if not want:
want = [{'afi': 'ipv4'}, {'afi': 'ipv6'}]
for item in want:
item = remove_empties(item)
have_item = search_obj_in_list(item['afi'], have, key='afi') or {}
if 'acls' not in item:
if have_item:
for acl in have_item['acls']:
commands.append('no {0} access-list {1}'.format(have_item['afi'], acl['name']))
else:
for want_acl in item['acls']:
have_acl = search_obj_in_list(want_acl['name'], have_item.get('acls', [])) or {}
if have_acl:
if 'aces' not in want_acl:
commands.append('no {0} access-list {1}'.format(have_item['afi'], have_acl['name']))
else:
acl_updates = []
for want_ace in want_acl['aces']:
have_ace = search_obj_in_list(want_ace.get('sequence'), have_acl.get('aces', []), key='sequence') or {}
if have_ace:
acl_updates.append('no {0}'.format(have_ace['sequence']))
if acl_updates:
acl_updates.insert(0, '{0} access-list {1}'.format(have_item['afi'], have_acl['name']))
commands.extend(acl_updates)
return commands
def _compute_commands(self, want_ace):
"""This command creates an ACE line from an ACE dictionary
:rtype: A string
:returns: An ACE generated from a structured ACE dictionary
"""
def __compute_src_dest(dir_dict):
cmd = ""
if 'any' in dir_dict:
cmd += 'any '
elif 'host' in dir_dict:
cmd += 'host {0} '.format(dir_dict['host'])
elif 'prefix' in dir_dict:
cmd += '{0} '.format(dir_dict['prefix'])
else:
cmd += '{0} {1} '.format(dir_dict['address'],
dir_dict['wildcard_bits'])
if 'port_protocol' in dir_dict:
protocol_range = dir_dict['port_protocol'].get('range')
if protocol_range:
cmd += 'range {0} {1} '.format(protocol_range['start'],
protocol_range['end'])
else:
for key, value in iteritems(dir_dict['port_protocol']):
cmd += '{0} {1} '.format(key, value)
return cmd
def __compute_protocol_options(protocol_dict):
cmd = ""
for value in protocol_options.values():
for subkey, subvalue in iteritems(value):
if subvalue:
cmd += '{0} '.format(subkey.replace('_', '-'))
return cmd
def __compute_match_options(want_ace):
cmd = ""
if 'precedence' in want_ace:
cmd += 'precedence {0} '.format(want_ace['precedence'])
for x in ['dscp', 'packet_length', 'ttl']:
if x in want_ace:
opt_range = want_ace[x].get('range')
if opt_range:
cmd += '{0} range {1} {2} '.format(
x.replace('_', '-'), opt_range['start'],
opt_range['end'])
else:
for key, value in iteritems(want_ace[x]):
cmd += '{0} {1} {2} '.format(
x.replace('_', '-'), key, value)
for x in ('authen', 'capture', 'fragments', 'routing', 'log',
'log_input', 'icmp_off', 'destopts', 'hop_by_hop'):
if x in want_ace:
cmd += '{0} '.format(x.replace('_', '-'))
return cmd
cmd = ""
if 'sequence' in want_ace:
cmd += '{0} '.format(want_ace['sequence'])
if 'remark' in want_ace:
cmd += 'remark {0}'.format(want_ace['remark'])
elif 'line' in want_ace:
cmd += want_ace['line']
else:
cmd += '{0} '.format(want_ace['grant'])
if 'protocol' in want_ace:
cmd += '{0} '.format(want_ace['protocol'])
cmd += __compute_src_dest(want_ace['source'])
cmd += __compute_src_dest(want_ace['destination'])
protocol_options = want_ace.get('protocol_options', {})
if protocol_options:
cmd += __compute_protocol_options(protocol_options)
cmd += __compute_match_options(want_ace)
return cmd.strip()
def _set_commands(self, want_ace, have_ace):
"""A helped method that checks if there is
a delta between the `have_ace` and `want_ace`.
If there is a delta then it calls `_compute_commands`
to create the ACE line.
:rtype: A string
:returns: An ACE generated from a structured ACE dictionary
via a call to `_compute_commands`
"""
if 'line' in want_ace:
if want_ace['line'] != have_ace.get('line'):
return self._compute_commands(want_ace)
else:
if ('prefix' in want_ace.get('source', {})) or ('prefix' in want_ace.get('destination', {})):
self._prepare_for_diff(want_ace)
protocol_opt_delta = {}
delta = dict_diff(have_ace, want_ace)
# `dict_diff` doesn't work properly for `protocol_options` diff,
# so we need to handle it separately
if want_ace.get('protocol_options', {}):
protocol_opt_delta = set(flatten_dict(have_ace.get('protocol_options', {}))) ^ \
set(flatten_dict(want_ace.get('protocol_options', {})))
if delta or protocol_opt_delta:
want_ace = self._dict_merge(have_ace, want_ace)
return self._compute_commands(want_ace)
def _prepare_for_diff(self, ace):
"""This method prepares the want ace dict
for diff calculation against the have ace dict.
:param ace: The want ace to prepare for diff calculation
"""
# Convert prefixes to "address wildcard bits" format for IPv4 addresses
# Not valid for IPv6 addresses because those can only be specified as prefixes
# and are always rendered in running-config as prefixes too
for x in ['source', 'destination']:
prefix = ace.get(x, {}).get('prefix')
if prefix and is_ipv4_address(prefix):
del ace[x]['prefix']
ace[x]['address'], ace[x]['wildcard_bits'] = prefix_to_address_wildcard(prefix)
def _dict_merge(self, have_ace, want_ace):
for x in want_ace:
have_ace[x] = want_ace[x]
return have_ace

View file

@ -0,0 +1,381 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr acls fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
from copy import deepcopy
from collections import deque
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.iosxr.argspec.acls.acls import AclsArgs
from ansible.module_utils.network.iosxr.utils.utils import isipaddress
PROTOCOL_OPTIONS = {
'tcp': (
'ack',
'fin',
'psh',
'rst',
'syn',
'urg',
'established',
),
'igmp': ('dvmrp', 'host_query', 'host_report', 'mtrace', 'mtrace_response',
'pim', 'trace', 'v2_leave', 'v2_report', 'v3_report'),
'icmp':
('administratively_prohibited', 'alternate_address', 'conversion_error',
'dod_host_prohibited', 'dod_net_prohibited', 'echo', 'echo_reply',
'general_parameter_problem', 'host_isolated',
'host_precedence_unreachable', 'host_redirect', 'host_tos_redirect',
'host_tos_unreachable', 'host_unknown', 'host_unreachable',
'information_reply', 'information_request', 'mask_reply', 'mask_request',
'mobile_redirect', 'net_redirect', 'net_tos_redirect',
'net_tos_unreachable', 'net_unreachable', 'network_unknown',
'no_room_for_option', 'option_missing', 'packet_too_big',
'parameter_problem', 'port_unreachable', 'precedence_unreachable',
'protocol_unreachable', 'reassembly_timeout', 'redirect',
'router_advertisement', 'router_solicitation', 'source_quench',
'source_route_failed', 'time_exceeded', 'timestamp_reply',
'timestamp_request', 'traceroute', 'ttl_exceeded', 'unreachable'),
'icmpv6':
('address_unreachable', 'administratively_prohibited',
'beyond_scope_of_source_address', 'destination_unreachable', 'echo',
'echo_reply', 'erroneous_header_field', 'group_membership_query',
'group_membership_report', 'group_membership_termination',
'host_unreachable', 'nd_na', 'nd_ns', 'neighbor_redirect',
'no_route_to_destination', 'node_information_request_is_refused',
'node_information_successful_reply', 'packet_too_big',
'parameter_problem', 'port_unreachable', 'query_subject_is_IPv4address',
'query_subject_is_IPv6address', 'query_subject_is_domainname',
'reassembly_timeout', 'redirect', 'router_advertisement',
'router_renumbering', 'router_solicitation', 'rr_command', 'rr_result',
'rr_seqnum_reset', 'time_exceeded', 'ttl_exceeded', 'unknown_query_type',
'unreachable', 'unrecognized_next_header', 'unrecognized_option',
'whoareyou_reply', 'whoareyou_request')
}
class AclsFacts(object):
""" The iosxr acls fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = AclsArgs.argument_spec
spec = deepcopy(self.argument_spec)
if subspec:
if options:
facts_argument_spec = spec[subspec][options]
else:
facts_argument_spec = spec[subspec]
else:
facts_argument_spec = spec
self.generated_spec = utils.generate_dict(facts_argument_spec)
def get_device_data(self, connection):
return connection.get('show access-lists afi-all')
def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for acls
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
if not data:
data = self.get_device_data(connection)
objs = []
acl_lines = data.splitlines()
# We iterate through the data and create a list of ACLs
# where each ACL is a dictionary in the following format:
# {'afi': 'ipv4', 'name': 'acl_1', 'aces': ['10 permit 172.16.0.0 0.0.255.255', '20 deny 192.168.34.0 0.0.0.255']}
if acl_lines:
acl, acls = {}, []
for line in acl_lines:
if line.startswith('ip'):
if acl:
acls.append(acl)
acl = {'aces': []}
acl['afi'], acl['name'] = line.split()[0], line.split()[2]
else:
acl['aces'].append(line.strip())
acls.append(acl)
# Here we group the ACLs based on AFI
# {
# 'ipv6': [{'aces': ['10 permit ipv6 2000::/12 any'], 'name': 'acl_2'}],
# 'ipv4': [{'aces': ['10 permit 172.16.0.0 0.0.255.255', '20 deny 192.168.34.0 0.0.0.255'], 'name': 'acl_1'},
# {'aces': ['20 deny 10.0.0.0/8 log'], 'name': 'acl_3'}]
# }
grouped_acls = {'ipv4': [], 'ipv6': []}
for acl in acls:
acl_copy = deepcopy(acl)
del acl_copy['afi']
grouped_acls[acl['afi']].append(acl_copy)
# Now that we have the ACLs in a fairly structured format,
# we pass it on to render_config to convert it to model spec
for key, value in iteritems(grouped_acls):
obj = self.render_config(self.generated_spec, value)
if obj:
obj['afi'] = key
objs.append(obj)
ansible_facts['ansible_network_resources'].pop('acls', None)
facts = {}
facts['acls'] = []
params = utils.validate_config(self.argument_spec, {'config': objs})
for cfg in params['config']:
facts['acls'].append(utils.remove_empties(cfg))
ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts
def render_config(self, spec, conf):
"""
Render config as dictionary structure and delete keys
from spec for null values
:param spec: The facts tree, generated from the argspec
:param conf: The configuration
:rtype: dictionary
:returns: The generated config
"""
config = deepcopy(spec)
config['acls'] = []
for item in conf:
acl = {'name': item['name']}
aces = item.get('aces', [])
if aces:
acl['aces'] = []
for ace in aces:
acl['aces'].append(self._render_ace(ace))
config['acls'].append(acl)
return utils.remove_empties(config)
def _render_ace(self, ace):
"""
Parses an Access Control Entry (ACE) and converts it
into model spec
:param ace: An ACE in device specific format
:rtype: dictionary
:returns: The ACE in structured format
"""
def __parse_src_dest(rendered_ace, ace_queue, direction):
"""
Parses the ACE queue and populates address, wildcard_bits,
host or any keys in the source/destination dictionary of
ace dictionary, i.e., `rendered_ace`.
:param rendered_ace: The dictionary containing the ACE in structured format
:param ace_queue: The ACE queue
:param direction: Specifies whether to populate `source` or `destination`
dictionary
"""
element = ace_queue.popleft()
if element == 'host':
rendered_ace[direction] = {'host': ace_queue.popleft()}
elif element == 'any':
rendered_ace[direction] = {'any': True}
elif '/' in element:
rendered_ace[direction] = {
'prefix': element
}
elif isipaddress(element):
rendered_ace[direction] = {
'address': element,
'wildcard_bits': ace_queue.popleft()
}
def __parse_port_protocol(rendered_ace, ace_queue, direction):
"""
Parses the ACE queue and populates `port_protocol` dictionary in the
ACE dictionary, i.e., `rendered_ace`.
:param rendered_ace: The dictionary containing the ACE in structured format
:param ace_queue: The ACE queue
:param direction: Specifies whether to populate `source` or `destination`
dictionary
"""
if len(ace_queue) > 0 and ace_queue[0] in ('eq', 'gt', 'lt', 'neq',
'range'):
element = ace_queue.popleft()
port_protocol = {}
if element == 'range':
port_protocol['range'] = {
'start': ace_queue.popleft(),
'end': ace_queue.popleft()
}
else:
port_protocol[element] = ace_queue.popleft()
rendered_ace[direction]['port_protocol'] = port_protocol
def __parse_protocol_options(rendered_ace, ace_queue, protocol):
"""
Parses the ACE queue and populates protocol specific options
of the required dictionary and updates the ACE dictionary, i.e.,
`rendered_ace`.
:param rendered_ace: The dictionary containing the ACE in structured format
:param ace_queue: The ACE queue
:param protocol: Specifies the protocol that will be populated under
`protocol_options` dictionary
"""
if len(ace_queue) > 0:
protocol_options = {protocol: {}}
for match_bit in PROTOCOL_OPTIONS.get(protocol, ()):
if match_bit.replace('_', '-') in ace_queue:
protocol_options[protocol][match_bit] = True
ace_queue.remove(match_bit.replace('_', '-'))
rendered_ace['protocol_options'] = protocol_options
def __parse_match_options(rendered_ace, ace_queue):
"""
Parses the ACE queue and populates remaining options in the ACE dictionary,
i.e., `rendered_ace`
:param rendered_ace: The dictionary containing the ACE in structured format
:param ace_queue: The ACE queue
"""
if len(ace_queue) > 0:
# We deepcopy the actual queue and iterate through the
# copied queue. However, we pop off the elements from
# the actual queue. Then, in every pass we update the copied
# queue with the current state of the original queue.
# This is done because a queue cannot be mutated during iteration.
copy_ace_queue = deepcopy(ace_queue)
for element in copy_ace_queue:
if element == 'precedence':
ace_queue.popleft()
rendered_ace['precedence'] = ace_queue.popleft()
elif element == 'dscp':
ace_queue.popleft()
dscp = {}
operation = ace_queue.popleft()
if operation in ('eq', 'gt', 'neq', 'lt', 'range'):
if operation == 'range':
dscp['range'] = {
'start': ace_queue.popleft(),
'end': ace_queue.popleft()
}
else:
dscp[operation] = ace_queue.popleft()
else:
# `dscp` can be followed by either the dscp value itself or
# the same thing can be represented using "dscp eq <dscp_value>".
# In both cases, it would show up as {'dscp': {'eq': "dscp_value"}}.
dscp['eq'] = operation
rendered_ace['dscp'] = dscp
elif element in ('packet-length', 'ttl'):
ace_queue.popleft()
element_dict = {}
operation = ace_queue.popleft()
if operation == 'range':
element_dict['range'] = {
'start': ace_queue.popleft(),
'end': ace_queue.popleft()
}
else:
element_dict[operation] = ace_queue.popleft()
rendered_ace[element.replace('-', '_')] = element_dict
elif element in ('log', 'log-input', 'fragments',
'icmp-off', 'capture', 'destopts',
'authen', 'routing', 'hop-by-hop'):
rendered_ace[element.replace('-', '_')] = True
ace_queue.remove(element)
copy_ace_queue = deepcopy(ace_queue)
rendered_ace = {}
split_ace = ace.split()
# Create a queue with each word in the ace
# We parse each element and pop it off the queue
ace_queue = deque(split_ace)
# An ACE will always have a sequence number, even if
# it is not explicitly provided while configuring
sequence = int(ace_queue.popleft())
rendered_ace['sequence'] = sequence
operation = ace_queue.popleft()
if operation == 'remark':
rendered_ace['remark'] = ' '.join(split_ace[2:])
else:
rendered_ace['grant'] = operation
# If the entry is a non-remark entry, the third element
# will always be the protocol specified. By default, it's
# the AFI.
rendered_ace['protocol'] = ace_queue.popleft()
# Populate source dictionary
__parse_src_dest(rendered_ace, ace_queue, direction='source')
# Populate port_protocol key in source dictionary
__parse_port_protocol(rendered_ace, ace_queue, direction='source')
# Populate destination dictionary
__parse_src_dest(rendered_ace, ace_queue, direction='destination')
# Populate port_protocol key in destination dictionary
__parse_port_protocol(rendered_ace,
ace_queue,
direction='destination')
# Populate protocol_options dictionary
__parse_protocol_options(rendered_ace,
ace_queue,
protocol=rendered_ace['protocol'])
# Populate remaining match options' dictionaries
__parse_match_options(rendered_ace, ace_queue)
# At this stage the queue should be empty
# If the queue is not empty, it means that
# we haven't been able to parse the entire ACE
# In this case, we add the whole unprocessed ACE
# to a key called `line` and send it back
if len(ace_queue) > 0:
rendered_ace = {
'sequence': sequence,
'line': ' '.join(split_ace[1:])
}
return utils.remove_empties(rendered_ace)

View file

@ -24,6 +24,8 @@ from ansible.module_utils.network.iosxr.facts.lag_interfaces.lag_interfaces impo
from ansible.module_utils.network.iosxr.facts.l2_interfaces.l2_interfaces import L2_InterfacesFacts
from ansible.module_utils.network.iosxr.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts
from ansible.module_utils.network.iosxr.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts
from ansible.module_utils.network.iosxr.facts.acls.acls import AclsFacts
FACT_LEGACY_SUBSETS = dict(
default=Default,
@ -40,7 +42,8 @@ FACT_RESOURCE_SUBSETS = dict(
l2_interfaces=L2_InterfacesFacts,
lag_interfaces=Lag_interfacesFacts,
l3_interfaces=L3_InterfacesFacts,
acl_interfaces=Acl_interfacesFacts
acl_interfaces=Acl_interfacesFacts,
acls=AclsFacts
)

View file

@ -8,6 +8,8 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils._text import to_text
from ansible.module_utils.compat import ipaddress
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.utils import dict_diff, is_masklen, to_netmask, search_obj_in_list
@ -303,3 +305,51 @@ def get_interface_type(interface):
return 'preconfigure'
else:
return 'unknown'
def isipaddress(data):
"""
Checks if the passed string is
a valid IPv4 or IPv6 address
"""
isipaddress = True
try:
ipaddress.ip_address(data)
except ValueError:
isipaddress = False
return isipaddress
def is_ipv4_address(data):
"""
Checks if the passed string is
a valid IPv4 address
"""
if '/' in data:
data = data.split('/')[0]
if not isipaddress(to_text(data)):
raise ValueError('{0} is not a valid IP address'.format(data))
return (ipaddress.ip_address(to_text(data)).version == 4)
def prefix_to_address_wildcard(prefix):
""" Converts a IPv4 prefix into address and
wildcard mask
:returns: IPv4 address and wildcard mask
"""
wildcard = []
subnet = to_text(ipaddress.IPv4Network(to_text(prefix)).netmask)
for x in subnet.split('.'):
component = 255 - int(x)
wildcard.append(str(component))
wildcard = '.'.join(wildcard)
return prefix.split('/')[0], wildcard

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ options:
specific subset should not be collected.
Valid subsets are 'all', 'lacp', 'lacp_interfaces', 'lldp_global',
'lldp_interfaces', 'interfaces', 'l2_interfaces', 'l3_interfaces',
'lag_interfaces'.
'lag_interfaces', 'acls'.
required: false
version_added: "2.9"
"""

View file

@ -0,0 +1,3 @@
---
testcase: "[^_].*"
test_items: []

View file

@ -0,0 +1,20 @@
---
- name: Collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
use_regex: true
register: test_cases
delegate_to: localhost
- name: Set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
delegate_to: localhost
- name: Run test case (connection=network_cli)
include: "{{ test_case_to_run }}"
vars:
ansible_connection: network_cli
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,13 @@
---
- name: Setup
iosxr_config:
lines: |
ipv6 access-list acl6_1
10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log
20 permit icmpv6 any any router-advertisement precedence network destopts
ipv4 access-list acl_1
16 remark TEST_ACL_1_REMARK
21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst
23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12
ipv4 access-list acl_2
10 remark TEST_ACL_2_REMARK

View file

@ -0,0 +1,14 @@
---
- name: Remove ACLs
cli_config:
config: "{{ lines }}"
vars:
lines: |
no ipv4 access-list acl_1
no ipv4 access-list acl_2
no ipv4 access-list acl_3
no ipv4 access-list acl_3
no ipv6 access-list acl6_1
no ipv6 access-list acl6_2
no ipv6 access-list acl6_3
ignore_errors: yes

View file

@ -0,0 +1,116 @@
---
- debug:
msg: "Start iosxr_lag_interfaces deleted integration tests ansible_connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Delete a single ACE
iosxr_acls: &deleted_1
config:
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 23
state: deleted
register: result
- assert:
that:
- '"ipv4 access-list acl_1" in result.commands'
- '"no 23" in result.commands'
- "result.commands|length == 2"
- name: Delete a single ACE (IDEMPOTENT)
iosxr_acls: *deleted_1
register: result
- name: Assert that the previous task was idempotent
assert: &unchanged
that:
- "result.changed == false"
- "result.commands|length == 0"
- name: Delete a single ACL
iosxr_acls: &deleted_2
config:
- afi: ipv6
acls:
- name: acl6_1
state: deleted
register: result
- assert:
that:
- '"no ipv6 access-list acl6_1" in result.commands'
- "result.commands|length == 1"
- name: Delete a single ACL (IDEMPOTENT)
iosxr_acls: *deleted_2
register: result
- name: Assert that the previous task was idempotent
assert: *unchanged
- name: Delete all ACLs under one AFI
iosxr_acls: &deleted_3
config:
- afi: ipv4
state: deleted
register: result
- assert:
that:
- '"no ipv4 access-list acl_1" in result.commands'
- '"no ipv4 access-list acl_2" in result.commands'
- "result.commands|length == 2"
- name: Delete all ACLs under one AFI (IDEMPOTENT)
iosxr_acls: *deleted_3
register: result
- name: Assert that the previous task was idempotent
assert: *unchanged
- include_tasks: _populate_config.yaml
- name: Delete all ACLs from the device
iosxr_acls: &deleted_4
state: deleted
register: result
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that the correct set of commands were generated
assert:
that:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that the after dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Delete all ACLs from the device (IDEMPOTENT)
iosxr_lag_interfaces: *deleted_4
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,57 @@
- debug:
msg: "START iosxr_acls empty_config integration tests on connection={{ ansible_connection }}"
- name: Merged with empty config should give appropriate error message
iosxr_acls:
config:
state: merged
register: result
ignore_errors: True
- assert:
that:
- result.msg == 'value of config parameter must not be empty for state merged'
- name: Replaced with empty config should give appropriate error message
iosxr_acls:
config:
state: replaced
register: result
ignore_errors: True
- assert:
that:
- result.msg == 'value of config parameter must not be empty for state replaced'
- name: Overridden with empty config should give appropriate error message
iosxr_acls:
config:
state: overridden
register: result
ignore_errors: True
- assert:
that:
- result.msg == 'value of config parameter must not be empty for state overridden'
- name: Rendered with empty config should give appropriate error message
iosxr_acls:
config:
state: rendered
register: result
ignore_errors: True
- assert:
that:
- result.msg == 'value of config parameter must not be empty for state rendered'
- name: Parsed with empty config should give appropriate error message
iosxr_acls:
running_config:
state: parsed
register: result
ignore_errors: True
- assert:
that:
- result.msg == 'value of running_config parameter must not be empty for state parsed'

View file

@ -0,0 +1,8 @@
ipv4 access-list acl_1
10 remark TEST_ACL_2_REMARK
ipv4 access-list acl_2
11 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log
21 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts
ipv6 access-list acl6_1
10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 routing authen log
20 permit icmpv6 any any router-advertisement precedence network packet-length eq 576 destopts

View file

@ -0,0 +1,20 @@
---
- debug:
msg: "START iosxr_acls gathered integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Gather ACL interfaces facts using gathered state
iosxr_acls:
state: gathered
register: result
- name: Assert that facts were correctly generated
assert:
that: "{{ merged['after'] | symmetric_difference(result['gathered']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,168 @@
---
- debug:
msg: "START iosxr_acls merged integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge the provided configuration with the exisiting running configuration
iosxr_acls: &merged
config:
- afi: ipv6
acls:
- name: acl6_1
aces:
- sequence: 10
grant: deny
protocol: tcp
source:
prefix: 2001:db8:1234::/48
port_protocol:
range:
start: ftp
end: telnet
destination:
any: True
protocol_options:
tcp:
syn: True
ttl:
range:
start: 180
end: 250
routing: True
authen: True
log: True
- sequence: 20
grant: permit
protocol: icmpv6
source:
any: True
destination:
any: True
protocol_options:
icmpv6:
router_advertisement: True
precedence: network
destopts: True
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 16
remark: TEST_ACL_1_REMARK
- sequence: 21
grant: permit
protocol: tcp
source:
host: 192.0.2.10
port_protocol:
range:
start: pop3
end: 121
destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
protocol_options:
tcp:
rst: True
- sequence: 23
grant: deny
protocol: icmp
source:
any: True
destination:
prefix: 198.51.100.0/28
protocol_options:
icmp:
reassembly_timeout: True
dscp:
lt: af12
- name: acl_2
aces:
- sequence: 10
remark: TEST_ACL_2_REMARK
state: merged
register: result
- name: Assert that before dicts were correctly generated
assert:
that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that after dicts was correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
iosxr_acls: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Update existing ACEs
iosxr_acls: &update
config:
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 21
source:
prefix: 198.51.100.32/28
port_protocol:
range:
start: pop3
end: 121
protocol_options:
tcp:
syn: True
- sequence: 23
protocol_options:
icmp:
router_advertisement: True
dscp:
eq: af23
state: merged
register: result
- name: Assert that the correct set of commands were generated
assert:
that:
- '"ipv4 access-list acl_1" in result.commands'
- '"21 permit tcp 198.51.100.32 0.0.0.15 range pop3 121 198.51.100.0 0.0.0.15 syn" in result.commands'
- '"23 deny icmp any 198.51.100.0 0.0.0.15 router-advertisement dscp eq af23" in result.commands'
- "result.commands|length == 3"
- name: Update existing ACEs (IDEMPOTENT)
iosxr_acls: *update
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,68 @@
---
- debug:
msg: "START iosxr_acls overridden integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Overridde all ACLs configuration with provided configuration
iosxr_acls: &overridden
config:
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 10
grant: permit
source:
any: True
destination:
any: True
protocol: tcp
- name: acl_2
aces:
- sequence: 20
grant: permit
source:
any: True
destination:
any: True
protocol: igmp
state: overridden
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Overridde all interface LAG interface configuration with provided configuration (IDEMPOTENT)
iosxr_acls: *overridden
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,14 @@
---
- debug:
msg: "START iosxr_acls parsed integration tests on connection={{ ansible_connection }}"
- name: Parse externally provided ACL config to agnostic model
iosxr_acls:
running_config: "{{ lookup('file', './fixtures/parsed.cfg') }}"
state: parsed
register: result
- name: Assert that config was correctly parsed
assert:
that:
- "{{ parsed | symmetric_difference(result['parsed']) |length == 0 }}"

View file

@ -0,0 +1,93 @@
---
- debug:
msg: "START iosxr_acls rendered integration tests on connection={{ ansible_connection }}"
- name: Render platform specific commands from task input using rendered state
iosxr_acls:
config:
- afi: ipv6
acls:
- name: acl6_1
aces:
- sequence: 10
grant: deny
protocol: tcp
source:
prefix: 2001:db8:1234::/48
port_protocol:
range:
start: ftp
end: telnet
destination:
any: True
protocol_options:
tcp:
syn: True
ttl:
range:
start: 180
end: 250
routing: True
authen: True
log: True
- sequence: 20
grant: permit
protocol: icmpv6
source:
any: True
destination:
any: True
protocol_options:
icmpv6:
router_advertisement: True
precedence: network
destopts: True
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 16
remark: TEST_ACL_1_REMARK
- sequence: 21
grant: permit
protocol: tcp
source:
host: 192.0.2.10
port_protocol:
range:
start: pop3
end: 121
destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
protocol_options:
tcp:
rst: True
- sequence: 23
grant: deny
protocol: icmp
source:
any: True
destination:
prefix: 198.51.100.0/28
protocol_options:
icmp:
reassembly_timeout: True
dscp:
lt: af12
- name: acl_2
aces:
- sequence: 10
remark: TEST_ACL_2_REMARK
state: rendered
register: result
- name: Assert that correct set of commands were rendered
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['rendered']) |length == 0 }}"

View file

@ -0,0 +1,68 @@
---
- debug:
msg: "START iosxr_acl_interfaces replaced integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Replace device configurations of listed ACL with provided configurations
iosxr_acls: &replaced
config:
- afi: ipv4
acls:
- name: acl_2
aces:
- sequence: 11
grant: permit
protocol: igmp
source:
host: 198.51.100.130
destination:
any: True
ttl:
eq: 100
- sequence: 12
grant: deny
source:
any: True
destination:
any: True
protocol: icmp
state: replaced
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT)
iosxr_acls: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,85 @@
---
- debug:
msg: "START iosxr_acls round trip integration tests on connection={{ ansible_connection }}"
- block:
- include_tasks: _remove_config.yaml
- name: Apply the provided configuration (base config)
iosxr_acls:
config:
- afi: ipv4
acls:
- name: acl_2
aces:
- sequence: 11
grant: permit
protocol: igmp
source:
host: 198.51.100.130
destination:
any: True
ttl:
eq: 100
- sequence: 12
grant: deny
source:
any: True
destination:
any: True
protocol: icmp
state: merged
register: base_config
- name: Gather interfaces facts
iosxr_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources:
- acls
- name: Apply the provided configuration (config to be reverted)
iosxr_acls:
config:
- afi: ipv4
acls:
- name: acl_1
aces:
- sequence: 10
grant: permit
source:
any: True
destination:
any: True
protocol: tcp
- name: acl_2
aces:
- sequence: 20
grant: permit
source:
any: True
destination:
any: True
protocol: igmp
state: overridden
register: result
- name: Assert that changes were applied
assert:
that: "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Revert back to base config using facts round trip
iosxr_acls:
config: "{{ ansible_facts['network_resources']['acls'] }}"
state: overridden
register: revert
- name: Assert that config was reverted
assert:
that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,338 @@
---
merged:
before: []
commands:
- ipv6 access-list acl6_1
- 10 deny tcp 2001:db8:1234::/48 range ftp telnet any syn ttl range 180 250 authen routing log
- 20 permit icmpv6 any any router-advertisement precedence network destopts
- ipv4 access-list acl_1
- 16 remark TEST_ACL_1_REMARK
- 21 permit tcp host 192.0.2.10 range pop3 121 198.51.100.0 0.0.0.15 rst
- 23 deny icmp any 198.51.100.0 0.0.0.15 reassembly-timeout dscp lt af12
- ipv4 access-list acl_2
- 10 remark TEST_ACL_2_REMARK
after:
- acls:
- aces:
- remark: TEST_ACL_1_REMARK
sequence: 16
- destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
grant: permit
protocol: tcp
protocol_options:
tcp:
rst: true
sequence: 21
source:
host: 192.0.2.10
port_protocol:
range:
end: '121'
start: pop3
- destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
dscp:
lt: af12
grant: deny
protocol: icmp
protocol_options:
icmp:
reassembly_timeout: true
sequence: 23
source:
any: true
name: acl_1
- aces:
- remark: TEST_ACL_2_REMARK
sequence: 10
name: acl_2
afi: ipv4
- acls:
- aces:
- authen: true
destination:
any: true
grant: deny
log: true
protocol: tcp
protocol_options:
tcp:
syn: true
routing: true
sequence: 10
source:
port_protocol:
range:
end: telnet
start: ftp
prefix: 2001:db8:1234::/48
ttl:
range:
end: 250
start: 180
- destination:
any: true
destopts: true
grant: permit
precedence: network
protocol: icmpv6
protocol_options:
icmpv6:
router_advertisement: true
sequence: 20
source:
any: true
name: acl6_1
afi: ipv6
replaced:
commands:
- ipv4 access-list acl_2
- no 10
- 11 permit igmp host 198.51.100.130 any ttl eq 100
- 12 deny icmp any any
after:
- acls:
- aces:
- remark: TEST_ACL_1_REMARK
sequence: 16
- destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
grant: permit
protocol: tcp
protocol_options:
tcp:
rst: true
sequence: 21
source:
host: 192.0.2.10
port_protocol:
range:
end: '121'
start: pop3
- destination:
address: 198.51.100.0
wildcard_bits: 0.0.0.15
dscp:
lt: af12
grant: deny
protocol: icmp
protocol_options:
icmp:
reassembly_timeout: true
sequence: 23
source:
any: true
name: acl_1
- aces:
- destination:
any: true
grant: permit
protocol: igmp
sequence: 11
source:
host: 198.51.100.130
ttl:
eq: 100
- destination:
any: true
grant: deny
protocol: icmp
sequence: 12
source:
any: true
name: acl_2
afi: ipv4
- acls:
- aces:
- authen: true
destination:
any: true
grant: deny
log: true
protocol: tcp
protocol_options:
tcp:
syn: true
routing: true
sequence: 10
source:
port_protocol:
range:
end: telnet
start: ftp
prefix: 2001:db8:1234::/48
ttl:
range:
end: 250
start: 180
- destination:
any: true
destopts: true
grant: permit
precedence: network
protocol: icmpv6
protocol_options:
icmpv6:
router_advertisement: true
sequence: 20
source:
any: true
name: acl6_1
afi: ipv6
overridden:
commands:
- no ipv6 access-list acl6_1
- ipv4 access-list acl_1
- no 16
- no 21
- no 23
- 10 permit tcp any any
- ipv4 access-list acl_2
- no 10
- 20 permit igmp any any
after:
- acls:
- aces:
- destination:
any: true
grant: permit
protocol: tcp
sequence: 10
source:
any: true
name: acl_1
- aces:
- destination:
any: true
grant: permit
protocol: igmp
sequence: 20
source:
any: true
name: acl_2
afi: ipv4
deleted:
commands:
- no ipv4 access-list acl_1
- no ipv4 access-list acl_2
- no ipv6 access-list acl6_1
after: []
parsed:
- acls:
- aces:
- remark: TEST_ACL_2_REMARK
sequence: 10
name: acl_1
- aces:
- authen: true
destination:
any: true
grant: deny
log: true
protocol: tcp
protocol_options:
tcp:
syn: true
routing: true
sequence: 11
source:
port_protocol:
range:
end: telnet
start: ftp
prefix: 2001:db8:1234::/48
ttl:
range:
end: 250
start: 180
- destination:
any: true
destopts: true
grant: permit
packet_length:
eq: 576
precedence: network
protocol: icmpv6
protocol_options:
icmpv6:
router_advertisement: true
sequence: 21
source:
any: true
name: acl_2
afi: ipv4
- acls:
- aces:
- authen: true
destination:
any: true
grant: deny
log: true
protocol: tcp
protocol_options:
tcp:
syn: true
routing: true
sequence: 10
source:
port_protocol:
range:
end: telnet
start: ftp
prefix: 2001:db8:1234::/48
ttl:
range:
end: 250
start: 180
- destination:
any: true
destopts: true
grant: permit
packet_length:
eq: 576
precedence: network
protocol: icmpv6
protocol_options:
icmpv6:
router_advertisement: true
sequence: 20
source:
any: true
name: acl6_1
afi: ipv6
round_trip:
after:
- members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: active
name: Bundle-Ether10
- mode: active
name: Bundle-Ether11

View file

@ -0,0 +1,5 @@
ipv4 access-list acl_2
10 deny ipv4 any any
20 permit tcp host 192.168.1.100 any
ipv6 access-list acl6_1
10 deny icmpv6 any any

View file

@ -0,0 +1,247 @@
#
# (c) 2019, Ansible by Red Hat, inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from units.compat.mock import patch
from ansible.modules.network.iosxr import iosxr_acls
from units.modules.utils import set_module_args
from .iosxr_module import TestIosxrModule, load_fixture
import itertools
class TestIosxrAclsModule(TestIosxrModule):
module = iosxr_acls
def setUp(self):
super(TestIosxrAclsModule, self).setUp()
self.mock_get_config = patch(
'ansible.module_utils.network.common.network.Config.get_config')
self.get_config = self.mock_get_config.start()
self.mock_load_config = patch(
'ansible.module_utils.network.common.network.Config.load_config')
self.load_config = self.mock_load_config.start()
self.mock_get_resource_connection_config = patch(
'ansible.module_utils.network.common.cfg.base.get_resource_connection'
)
self.get_resource_connection_config = self.mock_get_resource_connection_config.start(
)
self.mock_get_resource_connection_facts = patch(
'ansible.module_utils.network.common.facts.facts.get_resource_connection'
)
self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start(
)
self.mock_execute_show_command = patch(
'ansible.module_utils.network.iosxr.facts.acls.acls.AclsFacts.get_device_data'
)
self.execute_show_command = self.mock_execute_show_command.start()
def tearDown(self):
super(TestIosxrAclsModule, self).tearDown()
self.mock_get_resource_connection_config.stop()
self.mock_get_resource_connection_facts.stop()
self.mock_get_config.stop()
self.mock_load_config.stop()
self.mock_execute_show_command.stop()
def load_fixtures(self, commands=None):
def load_from_file(*args, **kwargs):
return load_fixture('iosxr_acls_config.cfg')
self.execute_show_command.side_effect = load_from_file
def test_iosxr_acls_merged(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_1",
aces=[
dict(sequence="10",
grant="permit",
protocol="ospf",
source=dict(prefix="192.168.1.0/24"),
destination=dict(any="true"),
log="true")
])
])
],
state="merged"))
commands = [
'ipv4 access-list acl_1',
'10 permit ospf 192.168.1.0 0.0.0.255 any log'
]
result = self.execute_module(changed=True, commands=commands)
def test_iosxr_acls_merged_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(sequence="10",
grant="deny",
protocol='ipv4',
destination=dict(any='true'),
source=dict(any="true")),
])
])
],
state="merged"))
result = self.execute_module(changed=False, commands=[])
def test_iosxr_acls_replaced(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(sequence="30",
grant="permit",
protocol="ospf",
source=dict(prefix="10.0.0.0/8"),
destination=dict(any="true"),
log="true")
])
])
],
state="replaced"))
commands = [
'ipv4 access-list acl_2', 'no 10', 'no 20',
'30 permit ospf 10.0.0.0 0.255.255.255 any log'
]
result = self.execute_module(changed=True, commands=commands)
def test_iosxr_acls_replaced_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(sequence="10",
grant="deny",
protocol='ipv4',
destination=dict(any='true'),
source=dict(any="true")),
dict(sequence="20",
grant="permit",
protocol="tcp",
destination=dict(any='true'),
source=dict(host="192.168.1.100")),
])
])
],
state="replaced"))
result = self.execute_module(changed=False, commands=[])
def test_iosxr_acls_overridden(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(sequence="40",
grant="permit",
protocol="ospf",
source=dict(any="true"),
destination=dict(any="true"),
log="true")
])
])
],
state="overridden"))
commands = [
'no ipv6 access-list acl6_1', 'ipv4 access-list acl_2', 'no 10',
'no 20', '40 permit ospf any any log'
]
result = self.execute_module(changed=True, commands=commands)
def test_iosxr_acls_overridden_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(sequence="10",
grant="deny",
protocol='ipv4',
destination=dict(any='true'),
source=dict(any="true")),
dict(sequence="20",
grant="permit",
protocol="tcp",
destination=dict(any='true'),
source=dict(host="192.168.1.100")),
])
]),
dict(afi='ipv6',
acls=[
dict(name="acl6_1",
aces=[
dict(
sequence="10",
grant="deny",
protocol="icmpv6",
destination=dict(any='true'),
source=dict(any='true'),
)
])
])
],
state="overridden"))
result = self.execute_module(changed=False, commands=[])
def test_iosxr_acls_deletedaces(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[dict(name="acl_2", aces=[dict(sequence="20")])])
],
state="deleted"))
commands = ['ipv4 access-list acl_2', 'no 20']
result = self.execute_module(changed=True, commands=commands)
def test_iosxr_acls_deletedacls(self):
set_module_args(
dict(config=[dict(afi="ipv6", acls=[dict(name="acl6_1")])],
state="deleted"))
commands = ['no ipv6 access-list acl6_1']
result = self.execute_module(changed=True, commands=commands)
def test_iosxr_acls_deletedafis(self):
set_module_args(dict(config=[dict(afi="ipv4")], state="deleted"))
commands = ['no ipv4 access-list acl_2']
result = self.execute_module(changed=True, commands=commands)
def test_eos_acls_rendered(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="acl_2",
aces=[
dict(grant="permit",
source=dict(any="true"),
destination=dict(any="true"),
protocol='igmp')
])
])
],
state="rendered"))
commands = ['ipv4 access-list acl_2', 'permit igmp any any']
result = self.execute_module(changed=False)
self.assertEqual(sorted(result['rendered']), sorted(commands),
result['rendered'])