mirror of https://github.com/ansible/ansible.git
Add iosxr_acls RM (#66207)
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>pull/67709/head
parent
3acd8f6f7f
commit
b818436c5f
@ -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
|
@ -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
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
||||
---
|
||||
testcase: "[^_].*"
|
||||
test_items: []
|
@ -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
|
@ -0,0 +1,2 @@
|
||||
---
|
||||
- { include: cli.yaml, tags: ['cli'] }
|
@ -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
|
@ -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
|
@ -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
|
@ -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'
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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 }}"
|
@ -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 }}"
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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'])
|
Loading…
Reference in New Issue