NX-OS ACLs module (#67558)

* Added nxos_acls module

* Adding tests

* Added integration tests

* Integration tests update

* Updated documentation

* Replaced state changes

* Added warning detection

* Added port-protocol mapping

* Added change

* Merge update changes

* Completed integration tests, rtt

* Added unit tests

* Linting

Added metaclass info

* Changed port protocol to str

* Fixed shippable errors, added examples

* Fixed type error, updated examples
pull/67926/head
Adharsh Srivats R 5 years ago committed by GitHub
parent 4ef7bd4c79
commit 7307339a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -0,0 +1,690 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The nxos_acls class
It is in this file where the current configuration (as dict)
is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import socket
import re
from copy import deepcopy
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.common.utils import to_list, remove_empties, dict_diff
from ansible.module_utils.network.nxos.facts.facts import Facts
from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.nxos.utils.utils import flatten_dict, search_obj_in_list, get_interface_type, normalize_interface
class Acls(ConfigBase):
"""
The nxos_acls class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'acls',
]
def __init__(self, module):
super(Acls, self).__init__(module)
def get_acls_facts(self, data=None):
""" Get the 'facts' (the current configuration)
:rtype: A dictionary
:returns: The current configuration as a dictionary
"""
facts, _warnings = Facts(self._module).get_facts(
self.gather_subset, self.gather_network_resources, data=data)
acls_facts = facts['ansible_network_resources'].get('acls')
if not acls_facts:
return []
return acls_facts
def edit_config(self, commands):
"""Wrapper method for `_connection.edit_config()`
This exists solely to allow the unit test framework to mock device connection calls.
"""
return self._connection.edit_config(commands)
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
warnings = list()
commands = list()
state = self._module.params['state']
action_states = ['merged', 'replaced', 'deleted', 'overridden']
if state == 'gathered':
result['gathered'] = self.get_acls_facts()
elif state == 'rendered':
result['rendered'] = self.set_config({})
elif state == 'parsed':
result['parsed'] = self.set_config({})
else:
existing_acls_facts = self.get_acls_facts()
commands.extend(self.set_config(existing_acls_facts))
if commands and state in action_states:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['before'] = existing_acls_facts
result['commands'] = commands
changed_acls_facts = self.get_acls_facts()
if result['changed']:
result['after'] = changed_acls_facts
result['warnings'] = warnings
return result
def set_config(self, existing_acls_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
config = self._module.params['config']
want = []
if config:
for w in config:
want.append(remove_empties(w))
have = existing_acls_facts
if want:
want = self.convert_values(want)
resp = self.set_state(want, have)
return to_list(resp)
def convert_values(self, want):
'''
This method is used to map and convert the user given values with what will actually be present in the device configuation
'''
port_protocol = {
515: 'lpd',
517: 'talk',
7: 'echo',
9: 'discard',
12: 'exec',
13: 'login',
14: 'cmd',
109: 'pop2',
19: 'chargen',
20: 'ftp-data',
21: 'ftp',
23: 'telnet',
25: 'smtp',
540: 'uucp',
543: 'klogin',
544: 'kshell',
37: 'time',
43: 'whois',
49: 'tacacs',
179: 'bgp',
53: 'domain',
194: 'irc',
70: 'gopher',
79: 'finger',
80: 'www',
101: 'hostname',
3949: 'drip',
110: 'pop3',
111: 'sunrpc',
496: 'pim-auto-rp',
113: 'ident',
119: 'nntp'
}
protocol = {
1: 'icmp',
2: 'igmp',
4: 'ip',
6: 'tcp',
103: 'pim',
108: 'pcp',
47: 'gre',
17: 'udp',
50: 'esp',
51: 'ahp',
88: 'eigrp',
89: 'ospf',
94: 'nos'
}
precedence = {
0: 'routine',
1: 'priority',
2: 'immediate',
3: 'flash',
4: 'flash-override',
5: 'critical',
6: 'internet',
7: 'network'
}
dscp = {
10: 'AF11',
12: 'AF12',
14: 'AF13',
18: 'AF21',
20: 'AF22',
22: 'AF23',
26: 'AF31',
28: 'AF32',
30: 'AF33',
34: 'AF41',
36: 'AF42',
38: 'AF43',
8: 'CS1',
16: 'CS2',
24: 'CS3',
32: 'CS4',
40: 'CS5',
48: 'CS6',
56: 'CS7',
0: 'Default',
46: 'EF'
}
# port_pro_num = list(protocol.keys())
for afi in want:
if 'acls' in afi.keys():
for acl in afi['acls']:
if 'aces' in acl.keys():
for ace in acl['aces']:
if 'dscp' in ace.keys():
if ace['dscp'].isdigit():
ace['dscp'] = dscp[int(ace['dscp'])]
ace['dscp'] = ace['dscp'].lower()
if 'precedence' in ace.keys():
if ace['precedence'].isdigit():
ace['precedence'] = precedence[int(
ace['precedence'])]
if 'protocol' in ace.keys(
) and ace['protocol'].isdigit() and int(
ace['protocol']) in protocol.keys():
ace['protocol'] = protocol[int(
ace['protocol'])]
# convert number to name
if 'protocol' in ace.keys(
) and ace['protocol'] in ['tcp', 'udp']:
for end in ['source', 'destination']:
if 'port_protocol' in ace[end].keys():
key = list(ace[end]
['port_protocol'].keys())[0]
# key could be eq,gt,lt,neq or range
if key != 'range':
val = ace[end]['port_protocol'][
key]
if val.isdigit() and int(val) in port_protocol.keys(
):
ace[end]['port_protocol'][
key] = port_protocol[int(
val)]
else:
st = int(ace[end]['port_protocol']
['range']['start'])
end = int(ace[end]['port_protocol']
['range']['end'])
if st in port_protocol.keys():
ace[end]['port_protocol'][
'range'][
'start'] = port_protocol[
st]
if end in port_protocol.keys():
ace[end]['port_protocol'][
'range'][
'end'] = port_protocol[
end]
return want
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
state = self._module.params['state']
commands = []
if state == 'overridden':
commands = (self._state_overridden(want, have))
elif state == 'deleted':
commands = (self._state_deleted(want, have))
elif state == 'rendered':
commands = self._state_rendered(want)
elif state == 'parsed':
want = self._module.params['running_config']
commands = self._state_parsed(want)
else:
for w in want:
if state == 'merged':
commands.extend(self._state_merged(w, have))
elif state == 'replaced':
commands.extend(self._state_replaced(w, have))
if state != 'parsed':
commands = [c.strip() for c in commands]
return commands
def _state_parsed(self, want):
return self.get_acls_facts(want)
def _state_rendered(self, want):
commands = []
for w in want:
commands.extend(self.set_commands(w, {}))
return commands
def _state_replaced(self, want, have):
""" The command generator when state is replaced
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
have_afi = search_obj_in_list(want['afi'], have, 'afi')
del_dict = {'acls': []}
want_names = []
if have_afi != want:
if have_afi:
del_dict.update({'afi': have_afi['afi'], 'acls': []})
if want.get('acls'):
want_names = [w['name'] for w in want['acls']]
have_names = [h['name'] for h in have_afi['acls']]
want_acls = want.get('acls')
for w in want_acls:
acl_commands = []
if w['name'] not in have_names:
# creates new ACL in replaced state
merge_dict = {'afi': want['afi'], 'acls': [w]}
commands.extend(
self._state_merged(merge_dict, have))
else:
# acl in want exists in have
have_name = search_obj_in_list(
w['name'], have_afi['acls'], 'name')
have_aces = have_name.get('aces') if have_name.get(
'aces') else []
merge_aces = []
del_aces = []
w_aces = w.get('aces') if w.get('aces') else []
for ace in have_aces:
if ace not in w_aces:
del_aces.append(ace)
for ace in w_aces:
if ace not in have_aces:
merge_aces.append(ace)
merge_dict = {
'afi': want['afi'],
'acls': [{
'name': w['name'],
'aces': merge_aces
}]
}
del_dict = {
'afi': want['afi'],
'acls': [{
'name': w['name'],
'aces': del_aces
}]
}
if del_dict['acls']:
acl_commands.extend(
self._state_deleted([del_dict], have))
acl_commands.extend(
self._state_merged(merge_dict, have))
for i in range(1, len(acl_commands)):
if acl_commands[i] == acl_commands[0]:
acl_commands[i] = ''
commands.extend(acl_commands)
else:
acls = []
# no acls given in want, so delete all have acls
for acl in have_afi['acls']:
acls.append({'name': acl['name']})
del_dict['acls'] = acls
if del_dict['acls']:
commands.extend(self._state_deleted([del_dict], have))
else:
# want_afi is not present in have
commands.extend(self._state_merged(want, have))
commands = list(filter(None, commands))
return commands
def _state_overridden(self, want, have):
""" The command generator when state is overridden
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
want_afi = [w['afi'] for w in want]
for h in have:
if h['afi'] in want_afi:
w = search_obj_in_list(h['afi'], want, 'afi')
for h_acl in h['acls']:
w_acl = search_obj_in_list(h_acl['name'], w['acls'],
'name')
if not w_acl:
del_dict = {
'afi': h['afi'],
'acls': [{
'name': h_acl['name']
}]
}
commands.extend(self._state_deleted([del_dict], have))
else:
# if afi is not in want
commands.extend(self._state_deleted([{'afi': h['afi']}], have))
for w in want:
commands.extend(self._state_replaced(w, have))
return commands
def _state_merged(self, want, have):
""" The command generator when state is merged
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
return self.set_commands(want, have)
def _state_deleted(self, want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
if want: # and have != want:
for w in want:
ip = 'ipv6' if w['afi'] == 'ipv6' else 'ip'
acl_names = []
have_afi = search_obj_in_list(w['afi'], have, 'afi')
# if want['afi] not in have, ignore
if have_afi:
if w.get('acls'):
for acl in w['acls']:
if 'aces' in acl.keys():
have_name = search_obj_in_list(
acl['name'], have_afi['acls'], 'name')
if have_name:
ace_commands = []
flag = 0
for ace in acl['aces']:
if list(ace.keys()) == ['sequence']:
# only sequence number is specified to be deleted
if 'aces' in have_name.keys():
for h_ace in have_name['aces']:
if h_ace[
'sequence'] == ace[
'sequence']:
ace_commands.append(
'no ' +
str(ace['sequence']
))
flag = 1
else:
if 'aces' in have_name.keys():
for h_ace in have_name['aces']:
# when want['ace'] does not have seq number
if 'sequence' not in ace.keys(
):
del h_ace['sequence']
if ace == h_ace:
ace_commands.append(
'no ' +
self.process_ace(
ace))
flag = 1
if flag:
ace_commands.insert(
0,
ip + ' access-list ' + acl['name'])
commands.extend(ace_commands)
else:
# only name given
for h in have_afi['acls']:
if h['name'] == acl['name']:
acl_names.append(acl['name'])
for name in acl_names:
commands.append('no ' + ip + ' access-list ' +
name)
else:
# 'only afi is given'
if have_afi.get('acls'):
for h in have_afi['acls']:
acl_names.append(h['name'])
for name in acl_names:
commands.append('no ' + ip + ' access-list ' +
name)
else:
v6 = []
v4 = []
v6_local = v4_local = None
for h in have:
if h['afi'] == 'ipv6':
v6 = (acl['name'] for acl in h['acls'])
if 'match_local_traffic' in h.keys():
v6_local = True
else:
v4 = (acl['name'] for acl in h['acls'])
if 'match_local_traffic' in h.keys():
v4_local = True
self.no_commands(v4, commands, v4_local, 'ip')
self.no_commands(v6, commands, v6_local, 'ipv6')
for name in v6:
commands.append('no ipv6 access-list ' + name)
if v4_local:
commands.append('no ipv6 access-list match-local-traffic')
return commands
def no_commands(self, v_list, commands, match_local, ip):
for name in v_list:
commands.append('no ' + ip + ' access-list ' + name)
if match_local:
commands.append('no ' + ip + ' access-list match-local-traffic')
def set_commands(self, want, have):
commands = []
have_afi = search_obj_in_list(want['afi'], have, 'afi')
ip = ''
if 'v6' in want['afi']:
ip = 'ipv6 '
else:
ip = 'ip '
if have_afi:
if want.get('acls'):
for w_acl in want['acls']:
have_acl = search_obj_in_list(w_acl['name'],
have_afi['acls'], 'name')
name = w_acl['name']
flag = 0
ace_commands = []
if have_acl != w_acl:
if have_acl:
ace_list = []
if w_acl.get('aces') and have_acl.get('aces'):
# case 1 --> sequence number not given in want --> new ace
# case 2 --> new sequence number in want --> new ace
# case 3 --> existing sequence number given --> update rule (only for merged state.
# For replaced and overridden, rule is deleted in the state's config)
ace_list = [
item for item in w_acl['aces']
if 'sequence' not in item.keys()
] # case 1
want_seq = [
item['sequence'] for item in w_acl['aces']
if 'sequence' in item.keys()
]
have_seq = [
item['sequence']
for item in have_acl['aces']
]
new_seq = list(set(want_seq) - set(have_seq))
common_seq = list(
set(want_seq).intersection(set(have_seq)))
temp_list = [
item for item in w_acl['aces']
if 'sequence' in item.keys()
and item['sequence'] in new_seq
] # case 2
ace_list.extend(temp_list)
for w in w_acl['aces']:
self.argument_spec = AclsArgs.argument_spec
params = utils.validate_config(
self.argument_spec, {
'config': [{
'afi':
want['afi'],
'acls': [{
'name': name,
'aces': ace_list
}]
}]
})
if 'sequence' in w.keys(
) and w['sequence'] in common_seq:
temp_obj = search_obj_in_list(
w['sequence'], have_acl['aces'],
'sequence') # case 3
if temp_obj != w:
for key, val in w.items():
temp_obj[key] = val
ace_list.append(temp_obj)
if self._module.params[
'state'] == 'merged':
ace_commands.append(
'no ' + str(w['sequence']))
# remove existing rule to update it
elif w_acl.get('aces'):
# 'have' has ACL defined without any ACE
ace_list = [item for item in w_acl['aces']]
for w_ace in ace_list:
ace_commands.append(
self.process_ace(w_ace).strip())
flag = 1
if flag:
ace_commands.insert(0,
ip + 'access-list ' + name)
else:
commands.append(ip + 'access-list ' + name)
if 'aces' in w_acl.keys():
for w_ace in w_acl['aces']:
commands.append(
self.process_ace(w_ace).strip())
commands.extend(ace_commands)
else:
if want.get('acls'):
for w_acl in want['acls']:
name = w_acl['name']
commands.append(ip + 'access-list ' + name)
if 'aces' in w_acl.keys():
for w_ace in w_acl['aces']:
commands.append(self.process_ace(w_ace).strip())
return commands
def process_ace(self, w_ace):
command = ''
ace_keys = w_ace.keys()
if 'remark' in ace_keys:
command += 'remark ' + w_ace['remark'] + ' '
else:
command += w_ace['grant'] + ' '
if 'protocol' in ace_keys:
command += w_ace['protocol'] + ' '
src = self.get_address(w_ace['source'], w_ace['protocol'])
dest = self.get_address(w_ace['destination'],
w_ace['protocol'])
command += src + dest
if 'protocol_options' in ace_keys:
pro = list(w_ace['protocol_options'].keys())[0]
if pro != w_ace['protocol']:
self._module.fail_json(
msg='protocol and protocol_options mismatch')
flags = ''
for k in w_ace['protocol_options'][pro].keys():
k = re.sub('_', '-', k)
flags += k + ' '
command += flags
if 'dscp' in ace_keys:
command += 'dscp ' + w_ace['dscp'] + ' '
if 'fragments' in ace_keys:
command += 'fragments '
if 'precedence' in ace_keys:
command += 'precedence ' + w_ace['precedence'] + ' '
if 'log' in ace_keys:
command += 'log '
if 'sequence' in ace_keys:
command = str(w_ace['sequence']) + ' ' + command
return command
def get_address(self, endpoint, pro=''):
ret_addr = ''
keys = list(endpoint.keys())
if 'address' in keys:
if 'wildcard_bits' not in keys:
self._module.fail_json(
msg='wildcard bits not specified for address')
else:
ret_addr = endpoint['address'] + \
' ' + endpoint['wildcard_bits'] + ' '
elif 'any' in keys:
ret_addr = 'any '
elif 'host' in keys:
ret_addr = 'host ' + endpoint['host'] + ' '
elif 'prefix' in keys:
ret_addr = endpoint['prefix'] + ' '
if pro in ['tcp', 'udp']:
if 'port_protocol' in keys:
options = self.get_options(endpoint['port_protocol'])
ret_addr += options
return ret_addr
def get_options(self, item):
com = ''
subkey = list(item.keys())
if 'range' in subkey:
com = 'range ' + item['range']['start'] + \
' ' + item['range']['end'] + ' '
else:
com = subkey[0] + ' ' + item[subkey[0]] + ' '
return com

@ -0,0 +1,236 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The nxos acls fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
from copy import deepcopy
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs
class AclsFacts(object):
""" The nxos acls fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = AclsArgs.argument_spec
spec = deepcopy(self.argument_spec)
if subspec:
if options:
facts_argument_spec = spec[subspec][options]
else:
facts_argument_spec = spec[subspec]
else:
facts_argument_spec = spec
self.generated_spec = utils.generate_dict(facts_argument_spec)
def get_device_data(self, connection):
return connection.get(
"show running-config | section 'ip(v6)* access-list'")
def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for acls
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
if not data:
data = self.get_device_data(connection)
data = re.split('\nip', data)
v6 = []
v4 = []
for i in range(len(data)):
if str(data[i]):
if 'v6' in str(data[i]).split()[0]:
v6.append(data[i])
else:
v4.append(data[i])
resources = []
resources.append(v6)
resources.append(v4)
objs = []
for resource in resources:
if resource:
obj = self.render_config(self.generated_spec, resource)
if obj:
objs.append(obj)
ansible_facts['ansible_network_resources'].pop('acls', None)
facts = {}
if objs:
params = utils.validate_config(self.argument_spec,
{'config': objs})
params = utils.remove_empties(params)
facts['acls'] = params['config']
ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts
def get_endpoint(self, ace, pro):
ret_dict = {}
option = ace.split()[0]
if option == 'any':
ret_dict.update({'any': True})
else:
# it could be a.b.c.d or a.b.c.d/x or a.b.c.d/32
if '/' in option: # or 'host' in option:
ip = re.search(r'(.*)/(\d+)', option)
if int(ip.group(2)) < 32 or 32 < int(ip.group(2)) < 128:
ret_dict.update({'prefix': option})
else:
ret_dict.update({'host': ip.group(1)})
else:
ret_dict.update({'address': option})
wb = ace.split()[1]
ret_dict.update({'wildcard_bits': wb})
ace = re.sub('{0}'.format(wb), '', ace, 1)
ace = re.sub(option, '', ace, 1)
if pro in ['tcp', 'udp']:
keywords = ['eq', 'lt', 'gt', 'neq', 'range']
if len(ace.split()) and ace.split()[0] in keywords:
port_protocol = {}
port_pro = re.search(r'(eq|lt|gt|neq) (\w*)', ace)
if port_pro:
port_protocol.update(
{port_pro.group(1): port_pro.group(2)})
ace = re.sub(port_pro.group(1), '', ace, 1)
ace = re.sub(port_pro.group(2), '', ace, 1)
else:
limit = re.search(r'(range) (\w*) (\w*)', ace)
if limit:
port_protocol.update({
'range': {
'start': limit.group(2),
'end': limit.group(3)
}
})
ace = re.sub(limit.group(2), '', ace, 1)
ace = re.sub(limit.group(3), '', ace, 1)
if port_protocol:
ret_dict.update({'port_protocol': port_protocol})
return ace, ret_dict
def render_config(self, spec, conf):
"""
Render config as dictionary structure and delete keys
from spec for null values
:param spec: The facts tree, generated from the argspec
:param conf: The configuration
:rtype: dictionary
:returns: The generated config
"""
config = deepcopy(spec)
protocol_options = {
'tcp': ['fin', 'established', 'psh', 'rst', 'syn', 'urg', 'ack'],
'icmp': [
'administratively_prohibited', 'alternate_address',
'conversion_error', 'dod_host_prohibited',
'dod_net_prohibited', 'echo', 'echo_reply',
'general_parameter_problem', 'host_isolated',
'host_precedence_unreachable', 'host_redirect',
'host_tos_redirect', 'host_tos_unreachable', 'host_unknown',
'host_unreachable', 'information_reply', 'information_request',
'mask_reply', 'mask_request', 'mobile_redirect',
'net_redirect', 'net_tos_redirect', 'net_tos_unreachable',
'net_unreachable', 'network_unknown', 'no_room_for_option',
'option_missing', 'packet_too_big', 'parameter_problem',
'port_unreachable', 'precedence_unreachable',
'protocol_unreachable', 'reassembly_timeout', 'redirect',
'router_advertisement', 'router_solicitation', 'source_quench',
'source_route_failed', 'time_exceeded', 'timestamp_reply',
'timestamp_request', 'traceroute', 'ttl_exceeded',
'unreachable'
],
'igmp': ['dvmrp', 'host_query', 'host_report'],
}
if conf:
if 'v6' in conf[0].split()[0]:
config['afi'] = 'ipv6'
else:
config['afi'] = 'ipv4'
config['acls'] = []
for acl in conf:
acls = {}
if 'match-local-traffic' in acl:
config['match_local_traffic'] = True
continue
acl = acl.split('\n')
acl = [a.strip() for a in acl]
acl = list(filter(None, acl))
acls['name'] = re.match(r'(ip)?(v6)?\s?access-list (.*)',
acl[0]).group(3)
acls['aces'] = []
for ace in list(filter(None, acl[1:])):
if re.search(r'ip(.*)access-list.*', ace):
break
entry = {}
ace = ace.strip()
seq = re.match(r'(\d*)', ace).group(0)
entry.update({'sequence': seq})
ace = re.sub(seq, '', ace, 1)
grant = ace.split()[0]
rem = ''
if grant != 'remark':
entry.update({'grant': grant})
else:
rem = re.match('.*remark (.*)', ace).group(1)
entry.update({'remark': rem})
if not rem:
ace = re.sub(grant, '', ace, 1)
pro = ace.split()[0]
entry.update({'protocol': pro})
ace = re.sub(pro, '', ace, 1)
ace, source = self.get_endpoint(ace, pro)
entry.update({'source': source})
ace, dest = self.get_endpoint(ace, pro)
entry.update({'destination': dest})
dscp = re.search(r'dscp (\w*)', ace)
if dscp:
entry.update({'dscp': dscp.group(1)})
frag = re.search(r'fragments', ace)
if frag:
entry.update({'fragments': True})
prec = re.search(r'precedence (\w*)', ace)
if prec:
entry.update({'precedence': prec.group(1)})
log = re.search('log', ace)
if log:
entry.update({'log': True})
if pro == 'tcp' or pro == 'icmp' or pro == 'igmp':
pro_options = {}
options = {}
for option in protocol_options[pro]:
option = re.sub('_', '-', option)
if option in ace:
option = re.sub('-', '_', option)
options.update({option: True})
if options:
pro_options.update({pro: options})
if pro_options:
entry.update({'protocol_options': pro_options})
acls['aces'].append(entry)
config['acls'].append(acls)
return utils.remove_empties(config)

@ -24,6 +24,7 @@ from ansible.module_utils.network.nxos.facts.lacp_interfaces.lacp_interfaces imp
from ansible.module_utils.network.nxos.facts.lldp_global.lldp_global import Lldp_globalFacts from ansible.module_utils.network.nxos.facts.lldp_global.lldp_global import Lldp_globalFacts
from ansible.module_utils.network.nxos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts from ansible.module_utils.network.nxos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts
from ansible.module_utils.network.nxos.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts from ansible.module_utils.network.nxos.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts
from ansible.module_utils.network.nxos.facts.acls.acls import AclsFacts
FACT_LEGACY_SUBSETS = dict( FACT_LEGACY_SUBSETS = dict(
@ -48,6 +49,7 @@ FACT_RESOURCE_SUBSETS = dict(
l2_interfaces=L2_interfacesFacts, l2_interfaces=L2_interfacesFacts,
lldp_interfaces=Lldp_interfacesFacts, lldp_interfaces=Lldp_interfacesFacts,
acl_interfaces=Acl_interfacesFacts, acl_interfaces=Acl_interfacesFacts,
acls=AclsFacts,
) )

@ -0,0 +1,825 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The module file for nxos_acls
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: nxos_acls
version_added: '2.10'
short_description: Manage named IP ACLs on the Cisco NX-OS platform
description: Manage named IP ACLs on the Cisco NX-OS platform
author: Adharsh Srivats Rangarajan (@adharshsrivatsr)
notes:
- Tested against NX-OS 7.3.(0)D1(1) on VIRL
- As NX-OS allows configuring a rule again with different sequence numbers,
the user is expected to provide sequence numbers for the access control entries to preserve idempotency.
If no sequence number is given, the rule will be added as a new rule by the device.
- To parse configuration text, provide the output of show running-config | section access-list or a mocked up config
options:
running_config:
description:
- Parse given commands into structured format. Required if I(state=parsed).
type: str
config:
description: A dictionary of ACL options.
type: list
elements: dict
suboptions:
afi:
description: The Address Family Indicator (AFI) for the ACL.
type: str
required: true
choices: ['ipv4', 'ipv6']
acls:
description: A list of the ACLs.
type: list
elements: dict
suboptions:
name:
description: Name of the ACL.
type: str
required: true
aces:
description: The entries within the ACL.
type: list
elements: dict
suboptions:
grant:
description: Action to be applied on the rule.
type: str
choices: ['permit', 'deny']
destination:
description: Specify the packet destination.
type: dict
suboptions:
address:
description: Destination network address.
type: str
any:
description: Any destination address.
type: bool
host:
description: Host IP address.
type: str
port_protocol:
description: Specify the destination port or protocol (only for TCP and UDP).
type: dict
suboptions:
eq:
description: Match only packets on a given port number.
type: str
gt:
description: Match only packets with a greater port number.
type: str
lt:
description: Match only packets with a lower port number.
type: str
neq:
description: Match only packets not on a given port number.
type: str
range:
description: Match only packets in the range of port numbers.
type: dict
suboptions:
start:
description: Specify the start of the port range.
type: str
end:
description: Specify the end of the port range.
type: str
prefix:
description: Destination network prefix. Only for prefixes of value less than 31 for ipv4 and 127 for ipv6.
Prefixes of 32 (ipv4) and 128 (ipv6) should be given in the 'host' key.
type: str
wildcard_bits:
description: Destination wildcard bits.
type: str
dscp:
description: Match packets with given DSCP value.
type: str
fragments:
description: Check non-initial fragments.
type: bool
remark:
description: Access list entry comment.
type: str
sequence:
description: Sequence number.
type: int
source:
description: Specify the packet source.
type: dict
suboptions:
address:
description: Source network address.
type: str
any:
description: Any source address.
type: bool
host:
description: Host IP address.
type: str
port_protocol:
description: Specify the destination port or protocol (only for TCP and UDP).
type: dict
suboptions:
eq:
description: Match only packets on a given port number.
type: str
gt:
description: Match only packets with a greater port number.
type: str
lt:
description: Match only packets with a lower port number.
type: str
neq:
description: Match only packets not on a given port number.
type: str
range:
description: Match only packets in the range of port numbers.
type: dict
suboptions:
start:
description: Specify the start of the port range.
type: str
end:
description: Specify the end of the port range.
type: str
prefix:
description: Source network prefix. Only for prefixes of mask value less than 31 for ipv4 and 127 for ipv6.
Prefixes of mask 32 (ipv4) and 128 (ipv6) should be given in the 'host' key.
type: str
wildcard_bits:
description: Source wildcard bits.
type: str
log:
description: Log matches against this entry.
type: bool
precedence:
description: Match packets with given precedence value.
type: str
protocol:
description: Specify the protocol.
type: str
protocol_options:
description: All possible suboptions for the protocol chosen.
type: dict
suboptions:
icmp:
description: ICMP protocol options.
type: dict
suboptions:
administratively_prohibited:
description: Administratively prohibited
type: bool
alternate_address:
description: Alternate address
type: bool
conversion_error:
description: Datagram conversion
type: bool
dod_host_prohibited:
description: Host prohibited
type: bool
dod_net_prohibited:
description: Net prohibited
type: bool
echo:
description: Echo (ping)
type: bool
echo_reply:
description: Echo reply
type: bool
general_parameter_problem:
description: Parameter problem
type: bool
host_isolated:
description: Host isolated
type: bool
host_precedence_unreachable:
description: Host unreachable for precedence
type: bool
host_redirect:
description: Host redirect
type: bool
host_tos_redirect:
description: Host redirect for TOS
type: bool
host_tos_unreachable:
description: Host unreachable for TOS
type: bool
host_unknown:
description: Host unknown
type: bool
host_unreachable:
description: Host unreachable
type: bool
information_reply:
description: Information replies
type: bool
information_request:
description: Information requests
type: bool
mask_reply:
description: Mask replies
type: bool
mask_request:
description: Mask requests
type: bool
message_code:
description: ICMP message code
type: int
message_type:
description: ICMP message type
type: int
mobile_redirect:
description: Mobile host redirect
type: bool
net_redirect:
description: Network redirect
type: bool
net_tos_redirect:
description: Net redirect for TOS
type: bool
net_tos_unreachable:
description: Network unreachable for TOS
type: bool
net_unreachable:
description: Net unreachable
type: bool
network_unknown:
description: Network unknown
type: bool
no_room_for_option:
description: Parameter required but no room
type: bool
option_missing:
description: Parameter required but not present
type: bool
packet_too_big:
description: Fragmentation needed and DF set
type: bool
parameter_problem:
description: All parameter problems
type: bool
port_unreachable:
description: Port unreachable
type: bool
precedence_unreachable:
description: Precedence cutoff
type: bool
protocol_unreachable:
description: Protocol unreachable
type: bool
reassembly_timeout:
description: Reassembly timeout
type: bool
redirect:
description: All redirects
type: bool
router_advertisement:
description: Router discovery advertisements
type: bool
router_solicitation:
description: Router discovery solicitations
type: bool
source_quench:
description: Source quenches
type: bool
source_route_failed:
description: Source route failed
type: bool
time_exceeded:
description: All time exceeded.
type: bool
timestamp_reply:
description: Timestamp replies
type: bool
timestamp_request:
description: Timestamp requests
type: bool
traceroute:
description: Traceroute
type: bool
ttl_exceeded:
description: TTL exceeded
type: bool
unreachable:
description: All unreachables
type: bool
tcp:
description: TCP flags.
type: dict
suboptions:
ack:
description: Match on the ACK bit
type: bool
established:
description: Match established connections
type: bool
fin:
description: Match on the FIN bit
type: bool
psh:
description: Match on the PSH bit
type: bool
rst:
description: Match on the RST bit
type: bool
syn:
description: Match on the SYN bit
type: bool
urg:
description: Match on the URG bit
type: bool
igmp:
description: IGMP protocol options.
type: dict
suboptions:
dvmrp:
description: Distance Vector Multicast Routing Protocol
type: bool
host_query:
description: Host Query
type: bool
host_report:
description: Host Report
type: bool
state:
description:
- The state the configuration should be left in
type: str
choices:
- deleted
- gathered
- merged
- overridden
- rendered
- replaced
- parsed
default: merged
"""
EXAMPLES = """
# Using merged
# Before state:
# -------------
#
- name: Merge new ACLs configuration
nxos_acls:
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: deny
destination:
address: 192.0.2.64
wildcard_bits: 0.0.0.255
source:
any: true
port_protocol:
lt: 55
protocol: tcp
protocol_options:
tcp:
ack: true
fin: true
sequence: 50
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
prefix: 2001:db8:12::/32
protocol: sctp
state: merged
# After state:
# ------------
#
# ip access-list ACL1v4
# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin
# ipv6 access-list ACL1v6
# 10 permit sctp any any
# Using replaced
# Before state:
# ----------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
- name: Replace existing ACL configuration with provided configuration
nxos_acls:
config:
- afi: ipv4
- afi: ipv6
acls:
- name: ACL1v6
aces:
- sequence: 20
grant: permit
source:
any: true
destination:
any: true
protocol: pip
- remark: Replaced ACE
- name: ACL2v6
state: replaced
# After state:
# ---------------
#
# ipv6 access-list ACL1v6
# 20 permit pip any any
# 30 remark Replaced ACE
# ipv6 access-list ACL2v6
# Using overridden
# Before state:
# ----------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
- name: Override existing configuration with provided configuration
nxos_acls:
config:
- afi: ipv4
acls:
- name: NewACL
aces:
- grant: deny
source:
address: 192.0.2.0
wildcard_bits: 0.0.255.255
destination:
any: true
protocol: eigrp
- remark: Example for overridden state
state: overridden
# After state:
# ------------
#
# ip access-list NewACL
# 10 deny eigrp 192.0.2.0 0.0.255.255 any
# 20 remark Example for overridden state
# Using deleted:
#
# Before state:
# -------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
- name: Delete all ACLs
nxos_acls:
config:
state: deleted
# After state:
# -----------
#
# Before state:
# -------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
- name: Delete all ACLs in given AFI
nxos_acls:
config:
- afi: ipv4
state: deleted
# After state:
# ------------
#
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
# Before state:
# -------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACL1v6
# 10 permit sctp any any
# 20 remark IPv6 ACL
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
- name: Delete specific ACLs
nxos_acls:
config:
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
any: true
protocol: sctp
- sequence: 20
state: deleted
# After state:
# ------------
#
# ip access-list ACL1v4
# 10 permit ip any any
# 20 deny udp any any
# ip access-list ACL2v4
# 10 permit ahp 192.0.2.0 0.0.0.255 any
# ip access-list ACl1v6
# ip access-list ACL2v6
# 10 deny ipv6 any 2001:db8:3000::/36
# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
# Using parsed
- name: Parse given config to structured data
nxos_acls:
running_config: |
ip access-list ACL1v4
50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin
ipv6 access-list ACL1v6
10 permit sctp any any
state: parsed
# returns:
# parsed:
# - afi: ipv4
# acls:
# - name: ACL1v4
# aces:
# - grant: deny
# destination:
# address: 192.0.2.64
# wildcard_bits: 0.0.0.255
# source:
# any: true
# port_protocol:
# lt: 55
# protocol: tcp
# protocol_options:
# tcp:
# ack: true
# fin: true
# sequence: 50
#
# - afi: ipv6
# acls:
# - name: ACL1v6
# aces:
# - grant: permit
# sequence: 10
# source:
# any: true
# destination:
# prefix: 2001:db8:12::/32
# protocol: sctp
# Using gathered:
# Before state:
# ------------
#
# ip access-list ACL1v4
# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin
# ipv6 access-list ACL1v6
# 10 permit sctp any any
- name: Gather existing configuration
nxos_acls:
state: gathered
# returns:
# gathered:
# - afi: ipv4
# acls:
# - name: ACL1v4
# aces:
# - grant: deny
# destination:
# address: 192.0.2.64
# wildcard_bits: 0.0.0.255
# source:
# any: true
# port_protocol:
# lt: 55
# protocol: tcp
# protocol_options:
# tcp:
# ack: true
# fin: true
# sequence: 50
# - afi: ipv6
# acls:
# - name: ACL1v6
# aces:
# - grant: permit
# sequence: 10
# source:
# any: true
# destination:
# prefix: 2001:db8:12::/32
# protocol: sctp
# Using rendered
- name: Render required configuration to be pushed to the device
nxos_acls:
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: deny
destination:
address: 192.0.2.64
wildcard_bits: 0.0.0.255
source:
any: true
port_protocol:
lt: 55
protocol: tcp
protocol_options:
tcp:
ack: true
fin: true
sequence: 50
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
prefix: 2001:db8:12::/32
protocol: sctp
state: rendered
# returns:
# rendered:
# ip access-list ACL1v4
# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin
# ipv6 access-list ACL1v6
# 10 permit sctp any any
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: dict
sample: >
The configuration returned will always be in the same format
of the parameters above.
after:
description: The resulting configuration model invocation.
returned: when changed
type: dict
sample: >
The configuration returned will always be in the same format
of the parameters above.
commands:
description: The set of commands pushed to the remote device.
returned: always
type: list
sample: ['ip access-list ACL1v4', '10 permit ip any any precedence critical log', '20 deny tcp any lt smtp host 192.0.2.64 ack fin']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.nxos.argspec.acls.acls import AclsArgs
from ansible.module_utils.network.nxos.config.acls.acls import Acls
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=AclsArgs.argument_spec,
supports_check_mode=True)
result = Acls(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

@ -0,0 +1,2 @@
dependencies:
- prepare_nxos_tests

@ -0,0 +1,20 @@
---
- name: collect cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yml"
connection: local
register: test_cases
- set_fact:
test_cases:
files: "{{ test_cases.files }}"
- name: set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: run test cases (connection=network_cli)
include: "{{ test_case_to_run }} ansible_connection=network_cli connection={{ cli }}"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

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

@ -0,0 +1,69 @@
---
- debug:
msg: Start nxos_acls deleted integration tests connection={{ansible_connection}}"
- include_tasks: populate_config.yaml
- block:
- name: Deleted (All ACLs)
nxos_acls:
config:
state: deleted
- name: Gather acls facts
nxos_facts: &facts
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- assert:
that:
- "ansible_facts.network_resources == {}"
- include_tasks: populate_config.yaml
- name: Deleted
nxos_acls: &deleted
config:
- afi: ipv4
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
any: true
protocol: sctp
- sequence: 20
state: deleted
register: result
- assert:
that:
- "result.changed==True"
- "'no ip access-list ACL1v4' in result.commands"
- "'no ip access-list ACL2v4' in result.commands"
- "'ipv6 access-list ACL1v6' in result.commands"
- "'no 10 permit sctp any any' in result.commands"
- "'no 20' in result.commands"
- "result.commands | length == 5"
- name: Gather acls facts
nxos_facts: *facts
- name: Idempotence - deleted
nxos_acls: *deleted
register: result
- assert:
that:
- "result.changed == false"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,34 @@
---
- debug:
msg: Start nxos_acls gathered integration tests connection={{ansible_connection}}"
- include_tasks: populate_config.yaml
- block:
- name: Gather acls facts
nxos_facts: &facts
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- name: Gathered
nxos_acls: &gathered
state: gathered
register: result
- assert:
that:
- "result.changed == false"
- "ansible_facts.network_resources.acls == result.gathered"
- name: Idempotence - Gathered
nxos_acls: *gathered
register: result
- assert:
that:
- "result.changed == false"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,108 @@
---
- debug:
msg: Start nxos_acls merged integration tests connection={{ansible_connection}}"
- include_tasks: remove_config.yaml
- block:
- name: Merged
nxos_acls: &merged
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: deny
destination:
address: 192.0.2.64
wildcard_bits: 0.0.0.255
source:
any: true
port_protocol:
lt: 25
protocol: tcp
protocol_options:
tcp:
ack: true
fin: true
sequence: 50
- grant: permit
protocol: ip
source:
any: true
destination:
any: true
fragments: true
log: true
sequence: 20
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
host: 2001:db8:12::128
protocol: sctp
state: merged
register: result
- assert:
that:
- "result.changed == True"
- "'ip access-list ACL1v4' in result.commands"
- "'20 permit ip any any fragments log' in result.commands"
- "'50 deny tcp any lt smtp 192.0.2.64 0.0.0.255 ack fin' in result.commands"
- "'ipv6 access-list ACL1v6' in result.commands"
- "'10 permit sctp any host 2001:db8:12::128' in result.commands"
- "result.commands | length == 5 "
- name: Gather acls facts
nxos_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- assert:
that:
- "ansible_facts.network_resources.acls == result.after"
- name: Idempotence - Merged
nxos_acls: *merged
register: result
- assert:
that:
- "result.changed == false"
- name: Update one parameter of an ACE
nxos_acls:
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: permit
protocol: tcp
source:
any: true
destination:
any: true
sequence: 20
precedence: 5
state: merged
register: result
- assert:
that:
- "result.changed == True"
- "'ip access-list ACL1v4' in result.commands"
- "'no 20' in result.commands"
- "'20 permit tcp any any fragments precedence critical log' in result.commands"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,99 @@
---
- debug:
msg: Start nxos_acls overridden integration tests connection={{ansible_connection}}"
- include_tasks: populate_config.yaml
- block:
- name: Overridden (first test)
nxos_acls:
config:
- afi: ipv4
acls:
- name: NewACL
aces:
- grant: deny
source:
address: 192.0.2.0
wildcard_bits: 0.0.255.255
destination:
any: true
protocol: eigrp
- remark: Example for overridden state
state: overridden
register: result
- assert:
that:
- "result.changed==True"
- "'no ip access-list ACL1v4' in result.commands"
- "'no ip access-list ACL2v4' in result.commands"
- "'no ipv6 access-list ACL1v6' in result.commands"
- "'no ipv6 access-list ACL2v6' in result.commands"
- "'ip access-list NewACL' in result.commands"
- "'deny eigrp 192.0.2.0 0.0.255.255 any' in result.commands"
- "'remark Example for overridden state' in result.commands"
- "result.commands|length==7"
- name: Gather acls post facts
nxos_facts: &facts
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- assert:
that:
- "ansible_facts.network_resources.acls == result.after"
- include_tasks: populate_config.yaml
- name: Overridden (second test)
nxos_acls: &overridden
config:
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: deny
protocol: udp
destination:
any: true
source:
host: 2001:db8:3431::12
port_protocol:
lt: 35
sequence: 10
state: overridden
register: result
- assert:
that:
- "result.changed==True"
- "'no ip access-list ACL1v4' in result.commands"
- "'no ip access-list ACL2v4' in result.commands"
- "'no ipv6 access-list ACL2v6' in result.commands"
- "'no ip access-list NewACL' in result.commands"
- "'ipv6 access-list ACL1v6' in result.commands"
- "'no 10 permit sctp any any' in result.commands"
- "'no 20 remark IPv6 ACL' in result.commands"
- "'10 deny udp host 2001:db8:3431::12 lt 35 any' in result.commands"
- "result.commands|length==8"
- name: Gather acls post facts
nxos_facts: *facts
- assert:
that:
- "ansible_facts.network_resources.acls == result.after"
- name: Idempotence - overridden
nxos_acls: *overridden
register: result
- assert:
that:
- "result.changed == false"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,45 @@
---
- debug:
msg: Start nxos_acls gathered integration tests connection={{ansible_connection}}"
- include_tasks: populate_config.yaml
- block:
- name: Gather acls facts
nxos_facts: &facts
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- name: Parsed
nxos_acls: &parsed
running_config: |
ip access-list ACL1v4
10 permit ip any any
20 deny udp any any
ip access-list ACL2v4
10 permit ahp 192.0.2.0 0.0.0.255 any
ipv6 access-list ACL1v6
10 permit sctp any any
20 remark IPv6 ACL
ipv6 access-list ACL2v6
10 deny ipv6 any 2001:db8:3000::36/128
20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128
state: parsed
register: result
- assert:
that:
- "result.changed == false"
- "ansible_facts.network_resources.acls == result.parsed"
- name: Idempotence - Parsed
nxos_acls: *parsed
register: result
- assert:
that: "result.changed == false"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,15 @@
---
- name: Add configuration
cli_config:
config: |
ip access-list ACL1v4
10 permit ip any any
20 deny udp any any
ip access-list ACL2v4
10 permit ahp 192.0.2.0 0.0.0.255 any
ipv6 access-list ACL1v6
10 permit sctp any any
20 remark IPv6 ACL
ipv6 access-list ACL2v6
10 deny ipv6 any host 2001:db8:3000::36
20 permit tcp host 2001:db8:2000:2::2 host 2001:db8:2000:ab::2

@ -0,0 +1,9 @@
---
- name: Remove config
cli_config:
config: |
no ip access-list ACL1v4
no ip access-list ACL2v4
no ipv6 access-list ACL1v6
no ipv6 access-list ACL2v6
no ip access-list NewACL

@ -0,0 +1,56 @@
---
- debug:
msg: "Start nxos_acls rendered tests connection={{ ansible_connection }}"
- name: Rendered
nxos_acls: &rendered
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: deny
destination:
address: 192.0.2.64
wildcard_bits: 0.0.0.255
source:
any: true
port_protocol:
eq: 43
protocol: tcp
protocol_options:
tcp:
ack: true
fin: true
sequence: 50
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
prefix: 2001:db8:12::/32
protocol: sctp
state: rendered
register: result
- assert:
that:
- "result.changed == false"
- "'ip access-list ACL1v4' in result.rendered"
- "'50 deny tcp any eq whois 192.0.2.64 0.0.0.255 ack fin' in result.rendered"
- "'ipv6 access-list ACL1v6' in result.rendered"
- "'10 permit sctp any 2001:db8:12::/32' in result.rendered"
- "result.rendered | length == 4"
- name: Idempotence - Rendered
nxos_acls: *rendered
register: result
- assert:
that:
- "result.changed == false"

@ -0,0 +1,65 @@
---
- debug:
msg: Start nxos_acls replaced integration tests connection={{ansible_connection}}"
- include_tasks: populate_config.yaml
- block:
- name: Replaced
nxos_acls: &replaced
config:
- afi: ipv4
- afi: ipv6
acls:
- name: ACL1v6
aces:
- sequence: 30
grant: permit
source:
any: true
destination:
any: true
protocol: pim
- sequence: 40
remark: Replaced ACE
- name: ACL2v6
state: replaced
register: result
- assert:
that:
- "'no ip access-list ACL1v4' in result.commands"
- "'no ip access-list ACL2v4' in result.commands"
- "'ipv6 access-list ACL1v6' in result.commands"
- "'no 10 permit sctp any any' in result.commands"
- "'no 20 remark IPv6 ACL' in result.commands"
- "'30 permit pim any any' in result.commands"
- "'40 remark Replaced ACE' in result.commands"
- "'ipv6 access-list ACL2v6' in result.commands"
- "'no 10 deny ipv6 any host 2001:db8:3000::36' in result.commands"
- "'no 20 permit tcp host 2001:db8:2000:2::2 host 2001:db8:2000:ab::2' in result.commands"
- "result.commands|length == 10"
- name: Gather static_routes post facts
nxos_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources: acls
- assert:
that:
- "ansible_facts.network_resources.acls == result.after"
- name: Idempotence - Replaced
nxos_acls: *replaced
register: result
- assert:
that:
- "result.changed == false"
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,87 @@
---
- debug:
msg: "Start nxos_acls round trip integration tests connection = {{ansible_connection}}"
- block:
- name: RTT - Apply provided configuration
nxos_acls:
config:
- afi: ipv4
acls:
- name: ACL1v4
aces:
- grant: deny
destination:
address: 192.0.2.64
wildcard_bits: 0.0.0.255
source:
any: true
port_protocol:
lt: 25
protocol: tcp
protocol_options:
tcp:
ack: true
fin: true
sequence: 50
- grant: permit
protocol: ip
source:
any: true
destination:
any: true
fragments: true
log: true
sequence: 20
state: merged
- name: Gather interfaces facts
nxos_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources:
- acls
- name: Apply configuration to be reverted
nxos_acls:
config:
- afi: ipv6
acls:
- name: ACL1v6
aces:
- grant: permit
sequence: 10
source:
any: true
destination:
host: 2001:db8:12::128
protocol: sctp
state: overridden
register: result
- assert:
that:
- "result.changed == True"
- "'no ip access-list ACL1v4' in result.commands"
- "'ipv6 access-list ACL1v6' in result.commands"
- "'10 permit sctp any host 2001:db8:12::128' in result.commands"
- "result.commands | length == 3 "
- name: Revert back to base configuration using facts round trip
nxos_acls:
config: "{{ ansible_facts['network_resources']['acls'] }}"
state: overridden
register: result
- assert:
that:
- "result.changed == True"
- "'ip access-list ACL1v4' in result.commands"
- "'20 permit ip any any fragments log' in result.commands"
- "'50 deny tcp any lt smtp 192.0.2.64 0.0.0.255 fin ack' in result.commands"
- "'no ipv6 access-list ACL1v6' in result.commands"
- "result.commands | length == 4 "
always:
- include_tasks: remove_config.yaml

@ -0,0 +1,370 @@
#
# (c) 2019, Ansible by Red Hat, inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.modules.network.nxos import nxos_acls
from units.compat.mock import patch, MagicMock
from units.modules.utils import set_module_args
from .nxos_module import TestNxosModule, load_fixture
class TestNxosAclsModule(TestNxosModule):
module = nxos_acls
def setUp(self):
super(TestNxosAclsModule, self).setUp()
self.mock_get_config = patch(
'ansible.module_utils.network.common.network.Config.get_config')
self.get_config = self.mock_get_config.start()
self.mock_load_config = patch(
'ansible.module_utils.network.common.network.Config.load_config')
self.load_config = self.mock_load_config.start()
self.mock_get_resource_connection_config = patch(
'ansible.module_utils.network.common.cfg.base.get_resource_connection'
)
self.get_resource_connection_config = self.mock_get_resource_connection_config.start(
)
self.mock_get_resource_connection_facts = patch(
'ansible.module_utils.network.common.facts.facts.get_resource_connection'
)
self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
self.mock_edit_config = patch(
'ansible.module_utils.network.nxos.config.acls.acls.Acls.edit_config'
)
self.edit_config = self.mock_edit_config.start()
self.mock_execute_show_command = patch(
'ansible.module_utils.network.nxos.facts.acls.acls.AclsFacts.get_device_data'
)
self.execute_show_command = self.mock_execute_show_command.start()
def tearDown(self):
super(TestNxosAclsModule, self).tearDown()
self.mock_get_resource_connection_config.stop()
self.mock_get_resource_connection_facts.stop()
self.mock_edit_config.stop()
self.mock_get_config.stop()
self.mock_load_config.stop()
self.mock_execute_show_command.stop()
def load_fixtures(self, commands=None, device=''):
def load_from_file(*args, **kwargs):
v4 = '''\nip access-list ACL1v4\n 10 permit ip any any\n 20 deny udp any any'''
v6 = '''\nipv6 access-list ACL1v6\n 10 permit sctp any any'''
return v4 + v6
self.execute_show_command.side_effect = load_from_file
def test_nxos_acls_merged(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL2v4",
aces=[
dict(
grant="deny",
destination=dict(any=True),
source=dict(any=True),
fragments=True,
sequence=20,
protocol="tcp",
protocol_options=dict(
tcp=dict(ack=True))
)
]
)
]
),
dict(afi="ipv6",
acls=[
dict(name="ACL2v6")
])
], state="merged"))
commands = ['ip access-list ACL2v4',
'20 deny tcp any any ack fragments',
'ipv6 access-list ACL2v6']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_merged_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL1v4",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="ip"
),
dict(
grant="deny",
destination=dict(any=True),
source=dict(any=True),
sequence=20,
protocol="udp")
]
),
]
),
dict(afi="ipv6",
acls=[
dict(name="ACL1v6",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="sctp",
)
])
])
], state="merged"))
self.execute_module(changed=False, commands=[])
def test_nxos_acls_replaced(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL1v4",
aces=[
dict(
grant="permit",
destination=dict(host="192.0.2.28"),
source=dict(any=True),
log=True,
sequence=50,
protocol="icmp",
protocol_options=dict(
icmp=dict(administratively_prohibited=True))
)
]
)
]
)
], state="replaced"))
commands = ['ip access-list ACL1v4', 'no 20 deny udp any any',
'no 10 permit ip any any',
'50 permit icmp any host 192.0.2.28 administratively-prohibited log']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_replaced_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL1v4",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="ip",
),
dict(
grant="deny",
destination=dict(any=True),
source=dict(any=True),
sequence=20,
protocol="udp")
]
),
]
),
dict(afi="ipv6",
acls=[
dict(name="ACL1v6",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="sctp",
)
])
])
], state="replaced"))
self.execute_module(changed=False, commands=[])
def test_nxos_acls_overridden(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL2v4",
aces=[
dict(
grant="permit",
destination=dict(host="192.0.2.28"),
source=dict(any=True),
log=True,
sequence=50,
protocol="icmp",
protocol_options=dict(
icmp=dict(administratively_prohibited=True))
),
dict(
remark="Overridden ACL"
)
]
)
]
)
], state="overridden"))
commands = ['no ip access-list ACL1v4', 'no ipv6 access-list ACL1v6', 'ip access-list ACL2v4',
'50 permit icmp any host 192.0.2.28 administratively-prohibited log', 'remark Overridden ACL']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_overridden_idempotent(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL1v4",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="ip",
),
dict(
grant="deny",
destination=dict(any=True),
source=dict(any=True),
sequence=20,
protocol="udp")
]
),
]
),
dict(afi="ipv6",
acls=[
dict(name="ACL1v6",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="sctp",
)
])
])
], state="overridden"))
self.execute_module(changed=False, commands=[])
def test_nxos_acls_deletedafi(self):
set_module_args(
dict(config=[dict(afi="ipv4")], state="deleted"))
commands = ['no ip access-list ACL1v4']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_deletedace(self):
set_module_args(
dict(config=[dict(afi="ipv6",
acls=[
dict(name="ACL1v6",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="sctp",
)
])
])], state="deleted"))
commands = ['ipv6 access-list ACL1v6', 'no 10 permit sctp any any']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_deletedall(self):
set_module_args(dict(config=[], state='deleted'))
commands = ['no ipv6 access-list ACL1v6', 'no ip access-list ACL1v4']
self.execute_module(changed=True, commands=commands)
def test_nxos_acls_rendered(self):
set_module_args(
dict(config=[
dict(afi="ipv4",
acls=[
dict(name="ACL1v4",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="ip",
),
dict(
grant="deny",
destination=dict(any=True),
source=dict(any=True),
sequence=20,
protocol="udp")
]
),
]
),
dict(afi="ipv6",
acls=[
dict(name="ACL1v6",
aces=[
dict(
grant="permit",
destination=dict(any=True),
source=dict(any=True),
sequence=10,
protocol="sctp",
)
])
])
], state="rendered"))
commands = ['ip access-list ACL1v4', '10 permit ip any any', '20 deny udp any any',
'ipv6 access-list ACL1v6', '10 permit sctp any any']
result = self.execute_module(changed=False)
self.assertEqual(sorted(result['rendered']), sorted(
commands), result['rendered'])
def test_nxos_acls_parsed(self):
set_module_args(dict(running_config='''\nip access-list ACL1v4\n 10 permit ip any any\n 20 deny udp any any dscp AF23 precedence critical''',
state="parsed"))
result = self.execute_module(changed=False)
compare_list = [{'afi': 'ipv4', 'acls': [{'name': 'ACL1v4',
'aces': [{'grant': 'permit', 'sequence': 10, 'protocol': 'ip', 'source': {'any': True},
'destination': {'any': True}}, {'grant': 'deny', 'sequence': 20,
'protocol': 'udp', 'source': {'any': True},
'destination': {'any': True},
'dscp': 'AF23', 'precedence': 'critical'}]}]}]
self.assertEqual(result['parsed'], compare_list, result['parsed'])
def test_nxos_acls_gathered(self):
set_module_args(dict(config=[], state="gathered"))
result = self.execute_module(changed=False)
compare_list = [{'acls': [{'aces': [{'destination': {'any': True}, 'sequence': 10, 'protocol': 'sctp', 'source': {'any': True}, 'grant': 'permit'}],
'name': 'ACL1v6'}], 'afi': 'ipv6'}, {'acls': [{'aces': [{'destination': {'any': True}, 'sequence': 10, 'protocol': 'ip',
'source': {'any': True}, 'grant': 'permit'},
{'destination': {'any': True}, 'sequence': 20, 'protocol': 'udp',
'source': {'any': True}, 'grant': 'deny'}], 'name': 'ACL1v4'}],
'afi': 'ipv4'}]
self.assertEqual(result['gathered'],
compare_list, result['gathered'])
Loading…
Cancel
Save