Resource module for ios_interfaces and Facts Update (#59716)

* ios interfaces resource
This commit is contained in:
Sumit Jaiswal 2019-08-02 14:34:05 +05:30 committed by GitHub
parent 18aae0a02b
commit b847327645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1981 additions and 444 deletions

View file

@ -0,0 +1,31 @@
#
# -*- 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 arg spec for the ios facts module.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class FactsArgs(object):
""" The arg spec for the ios facts module
"""
def __init__(self, **kwargs):
pass
choices = [
'all',
'!all',
'interfaces',
'!interfaces'
]
argument_spec = {
'gather_subset': dict(default=['!config'], type='list'),
'gather_network_resources': dict(choices=choices, type='list'),
}

View file

@ -0,0 +1,47 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The arg spec for the ios_interfaces module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class InterfacesArgs(object):
def __init__(self, **kwargs):
pass
argument_spec = {'config': {'elements': 'dict',
'options': {'name': {'type': 'str', 'required': True},
'description': {'type': 'str'},
'enabled': {'default': True, 'type': 'bool'},
'speed': {'type': 'str'},
'mtu': {'type': 'int'},
'duplex': {'type': 'str', 'choices': ['full', 'half', 'auto']}},
'type': 'list'},
'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'],
'default': 'merged',
'type': 'str'}}

View file

@ -0,0 +1,279 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat Inc.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The ios_interfaces class
It is in this file where the current configuration (as dict)
is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.network.ios.facts.facts import Facts
from ansible.module_utils.network.ios.utils.utils import get_interface_type, dict_diff
from ansible.module_utils.network.ios.utils.utils import remove_command_from_config_list, add_command_to_config_list
from ansible.module_utils.network.ios.utils.utils import filter_dict_having_none_value, remove_duplicate_interface
class Interfaces(ConfigBase):
"""
The ios_interfaces class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'interfaces',
]
params = ('description', 'mtu', 'speed', 'duplex')
def __init__(self, module):
super(Interfaces, self).__init__(module)
def get_interfaces_facts(self):
""" 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)
interfaces_facts = facts['ansible_network_resources'].get('interfaces')
if not interfaces_facts:
return []
return interfaces_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from moduel execution
"""
result = {'changed': False}
commands = list()
warnings = list()
existing_interfaces_facts = self.get_interfaces_facts()
commands.extend(self.set_config(existing_interfaces_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_interfaces_facts = self.get_interfaces_facts()
result['before'] = existing_interfaces_facts
if result['changed']:
result['after'] = changed_interfaces_facts
result['warnings'] = warnings
return result
def set_config(self, existing_interfaces_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the deisred configuration
"""
want = self._module.params['config']
have = existing_interfaces_facts
resp = self.set_state(want, have)
return to_list(resp)
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the deisred configuration
"""
commands = []
state = self._module.params['state']
if state == 'overridden':
commands = self._state_overridden(want, have)
elif state == 'deleted':
commands = self._state_deleted(want, have)
elif state == 'merged':
commands = self._state_merged(want, have)
elif state == 'replaced':
commands = self._state_replaced(want, have)
return commands
def _state_replaced(self, want, have):
""" The command generator when state is replaced
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:param interface_type: interface type
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the deisred configuration
"""
commands = []
for interface in want:
for each in have:
if each['name'] == interface['name']:
break
elif interface['name'] in each['name']:
break
else:
continue
have_dict = filter_dict_having_none_value(interface, each)
want = dict()
commands.extend(self._clear_config(want, have_dict))
commands.extend(self._set_config(interface, each))
# Remove the duplicate interface call
commands = remove_duplicate_interface(commands)
return commands
def _state_overridden(self, want, have):
""" The command generator when state is overridden
:param want: the desired configuration as a dictionary
:param obj_in_have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
for each in have:
for interface in want:
if each['name'] == interface['name']:
break
elif interface['name'] in each['name']:
break
else:
# We didn't find a matching desired state, which means we can
# pretend we recieved an empty desired state.
interface = dict(name=each['name'])
commands.extend(self._clear_config(interface, each))
continue
have_dict = filter_dict_having_none_value(interface, each)
want = dict()
commands.extend(self._clear_config(want, have_dict))
commands.extend(self._set_config(interface, each))
# Remove the duplicate interface call
commands = remove_duplicate_interface(commands)
return commands
def _state_merged(self, want, have):
""" The command generator when state is merged
:param want: the additive configuration as a dictionary
:param obj_in_have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
commands = []
for interface in want:
for each in have:
if each['name'] == interface['name']:
break
else:
continue
commands.extend(self._set_config(interface, each))
return commands
def _state_deleted(self, want, have):
""" The command generator when state is deleted
:param want: the objects from which the configuration should be removed
:param obj_in_have: the current configuration as a dictionary
:param interface_type: interface type
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
if want:
for interface in want:
for each in have:
if each['name'] == interface['name']:
break
else:
continue
interface = dict(name=interface['name'])
commands.extend(self._clear_config(interface, each))
else:
for each in have:
want = dict()
commands.extend(self._clear_config(want, each))
return commands
def _set_config(self, want, have):
# Set the interface config based on the want and have config
commands = []
interface = 'interface ' + want['name']
# Get the diff b/w want and have
want_dict = dict_diff(want)
have_dict = dict_diff(have)
diff = want_dict - have_dict
if diff:
diff = dict(diff)
for item in self.params:
if diff.get(item):
cmd = item + ' ' + str(want.get(item))
add_command_to_config_list(interface, cmd, commands)
if diff.get('enabled'):
add_command_to_config_list(interface, 'no shutdown', commands)
elif diff.get('enabled') is False:
add_command_to_config_list(interface, 'shutdown', commands)
return commands
def _clear_config(self, want, have):
# Delete the interface config based on the want and have config
commands = []
if want.get('name'):
interface_type = get_interface_type(want['name'])
interface = 'interface ' + want['name']
else:
interface_type = get_interface_type(have['name'])
interface = 'interface ' + have['name']
if have.get('description') and want.get('description') != have.get('description'):
remove_command_from_config_list(interface, 'description', commands)
if not have.get('enabled') and want.get('enabled') != have.get('enabled'):
# if enable is False set enable as True which is the default behavior
remove_command_from_config_list(interface, 'shutdown', commands)
if interface_type.lower() == 'gigabitethernet':
if have.get('speed') and have.get('speed') != 'auto' and want.get('speed') != have.get('speed'):
remove_command_from_config_list(interface, 'speed', commands)
if have.get('duplex') and have.get('duplex') != 'auto' and want.get('duplex') != have.get('duplex'):
remove_command_from_config_list(interface, 'duplex', commands)
if have.get('mtu') and want.get('mtu') != have.get('mtu'):
remove_command_from_config_list(interface, 'mtu', commands)
return commands

View file

@ -0,0 +1,59 @@
#
# -*- 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 facts class for ios
this file validates each subset of facts and selectively
calls the appropriate facts gathering function
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.network.ios.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.ios.facts.interfaces.interfaces import InterfacesFacts
from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config
FACT_LEGACY_SUBSETS = dict(
default=Default,
hardware=Hardware,
interfaces=Interfaces,
config=Config
)
FACT_RESOURCE_SUBSETS = dict(
interfaces=InterfacesFacts,
)
class Facts(FactsBase):
""" The fact class for ios
"""
VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys())
VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys())
def __init__(self, module):
super(Facts, self).__init__(module)
def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None):
""" Collect the facts for ios
:param legacy_facts_type: List of legacy facts types
:param resource_facts_type: List of resource fact types
:param data: previously collected conf
:rtype: dict
:return: the facts gathered
"""
netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', [])
if self.VALID_RESOURCE_SUBSETS:
self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, resource_facts_type, data)
if self.VALID_LEGACY_GATHER_SUBSETS:
self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type)
return self.ansible_facts, self._warnings

View file

@ -0,0 +1,97 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The ios interfaces fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from copy import deepcopy
import re
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.ios.utils.utils import get_interface_type, normalize_interface
from ansible.module_utils.network.ios.argspec.interfaces.interfaces import InterfacesArgs
class InterfacesFacts(object):
""" The ios interfaces fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = InterfacesArgs.argument_spec
spec = deepcopy(self.argument_spec)
if subspec:
if options:
facts_argument_spec = spec[subspec][options]
else:
facts_argument_spec = spec[subspec]
else:
facts_argument_spec = spec
self.generated_spec = utils.generate_dict(facts_argument_spec)
def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for interfaces
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
objs = []
if not data:
data = connection.get('show running-config | section ^interface')
# operate on a collection of resource x
config = data.split('interface ')
for conf in config:
if conf:
obj = self.render_config(self.generated_spec, conf)
if obj:
objs.append(obj)
facts = {}
if objs:
facts['interfaces'] = []
params = utils.validate_config(self.argument_spec, {'config': objs})
for cfg in params['config']:
facts['interfaces'].append(utils.remove_empties(cfg))
ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts
def render_config(self, spec, conf):
"""
Render config as dictionary structure and delete keys from spec for null values
:param spec: The facts tree, generated from the argspec
:param conf: The configuration
:rtype: dictionary
:returns: The generated config
"""
config = deepcopy(spec)
match = re.search(r'^(\S+)', conf)
intf = match.group(1)
if get_interface_type(intf) == 'unknown':
return {}
# populate the facts from the configuration
config['name'] = normalize_interface(intf)
config['description'] = utils.parse_conf_arg(conf, 'description')
config['speed'] = utils.parse_conf_arg(conf, 'speed')
if utils.parse_conf_arg(conf, 'mtu'):
config['mtu'] = int(utils.parse_conf_arg(conf, 'mtu'))
config['duplex'] = utils.parse_conf_arg(conf, 'duplex')
enabled = utils.parse_conf_cmd_arg(conf, 'shutdown', False)
config['enabled'] = enabled if enabled is not None else True
return utils.remove_empties(config)

View file

@ -0,0 +1,380 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The ios legacy 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 platform
import re
from ansible.module_utils.network.ios.ios import run_commands, get_capabilities
from ansible.module_utils.network.ios.ios import normalize_interface
from ansible.module_utils.six import iteritems
from ansible.module_utils.six.moves import zip
class FactsBase(object):
COMMANDS = list()
def __init__(self, module):
self.module = module
self.facts = dict()
self.warnings = list()
self.responses = None
def populate(self):
self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False)
def run(self, cmd):
return run_commands(self.module, commands=cmd, check_rc=False)
class Default(FactsBase):
COMMANDS = ['show version']
def populate(self):
super(Default, self).populate()
self.facts.update(self.platform_facts())
data = self.responses[0]
if data:
self.facts['iostype'] = self.parse_iostype(data)
self.facts['serialnum'] = self.parse_serialnum(data)
self.parse_stacks(data)
def parse_iostype(self, data):
match = re.search(r'\S+(X86_64_LINUX_IOSD-UNIVERSALK9-M)(\S+)', data)
if match:
return "IOS-XE"
else:
return "IOS"
def parse_serialnum(self, data):
match = re.search(r'board ID (\S+)', data)
if match:
return match.group(1)
def parse_stacks(self, data):
match = re.findall(r'^Model [Nn]umber\s+: (\S+)', data, re.M)
if match:
self.facts['stacked_models'] = match
match = re.findall(r'^System [Ss]erial [Nn]umber\s+: (\S+)', data, re.M)
if match:
self.facts['stacked_serialnums'] = match
def platform_facts(self):
platform_facts = {}
resp = get_capabilities(self.module)
device_info = resp['device_info']
platform_facts['system'] = device_info['network_os']
for item in ('model', 'image', 'version', 'platform', 'hostname'):
val = device_info.get('network_os_%s' % item)
if val:
platform_facts[item] = val
platform_facts['api'] = resp['network_api']
platform_facts['python_version'] = platform.python_version()
return platform_facts
class Hardware(FactsBase):
COMMANDS = [
'dir',
'show memory statistics'
]
def populate(self):
warnings = list()
super(Hardware, self).populate()
data = self.responses[0]
if data:
self.facts['filesystems'] = self.parse_filesystems(data)
self.facts['filesystems_info'] = self.parse_filesystems_info(data)
data = self.responses[1]
if data:
if 'Invalid input detected' in data:
warnings.append('Unable to gather memory statistics')
else:
processor_line = [l for l in data.splitlines()
if 'Processor' in l].pop()
match = re.findall(r'\s(\d+)\s', processor_line)
if match:
self.facts['memtotal_mb'] = int(match[0]) / 1024
self.facts['memfree_mb'] = int(match[3]) / 1024
def parse_filesystems(self, data):
return re.findall(r'^Directory of (\S+)/', data, re.M)
def parse_filesystems_info(self, data):
facts = dict()
fs = ''
for line in data.split('\n'):
match = re.match(r'^Directory of (\S+)/', line)
if match:
fs = match.group(1)
facts[fs] = dict()
continue
match = re.match(r'^(\d+) bytes total \((\d+) bytes free\)', line)
if match:
facts[fs]['spacetotal_kb'] = int(match.group(1)) / 1024
facts[fs]['spacefree_kb'] = int(match.group(2)) / 1024
return facts
class Config(FactsBase):
COMMANDS = ['show running-config']
def populate(self):
super(Config, self).populate()
data = self.responses[0]
if data:
data = re.sub(
r'^Building configuration...\s+Current configuration : \d+ bytes\n',
'', data, flags=re.MULTILINE)
self.facts['config'] = data
class Interfaces(FactsBase):
COMMANDS = [
'show interfaces',
'show ip interface',
'show ipv6 interface',
'show lldp',
'show cdp'
]
def populate(self):
super(Interfaces, self).populate()
self.facts['all_ipv4_addresses'] = list()
self.facts['all_ipv6_addresses'] = list()
self.facts['neighbors'] = {}
data = self.responses[0]
if data:
interfaces = self.parse_interfaces(data)
self.facts['interfaces'] = self.populate_interfaces(interfaces)
data = self.responses[1]
if data:
data = self.parse_interfaces(data)
self.populate_ipv4_interfaces(data)
data = self.responses[2]
if data:
data = self.parse_interfaces(data)
self.populate_ipv6_interfaces(data)
data = self.responses[3]
lldp_errs = ['Invalid input', 'LLDP is not enabled']
if data and not any(err in data for err in lldp_errs):
neighbors = self.run(['show lldp neighbors detail'])
if neighbors:
self.facts['neighbors'].update(self.parse_neighbors(neighbors[0]))
data = self.responses[4]
cdp_errs = ['CDP is not enabled']
if data and not any(err in data for err in cdp_errs):
cdp_neighbors = self.run(['show cdp neighbors detail'])
if cdp_neighbors:
self.facts['neighbors'].update(self.parse_cdp_neighbors(cdp_neighbors[0]))
def populate_interfaces(self, interfaces):
facts = dict()
for key, value in iteritems(interfaces):
intf = dict()
intf['description'] = self.parse_description(value)
intf['macaddress'] = self.parse_macaddress(value)
intf['mtu'] = self.parse_mtu(value)
intf['bandwidth'] = self.parse_bandwidth(value)
intf['mediatype'] = self.parse_mediatype(value)
intf['duplex'] = self.parse_duplex(value)
intf['lineprotocol'] = self.parse_lineprotocol(value)
intf['operstatus'] = self.parse_operstatus(value)
intf['type'] = self.parse_type(value)
facts[key] = intf
return facts
def populate_ipv4_interfaces(self, data):
for key, value in data.items():
self.facts['interfaces'][key]['ipv4'] = list()
primary_address = addresses = []
primary_address = re.findall(r'Internet address is (.+)$', value, re.M)
addresses = re.findall(r'Secondary address (.+)$', value, re.M)
if len(primary_address) == 0:
continue
addresses.append(primary_address[0])
for address in addresses:
addr, subnet = address.split("/")
ipv4 = dict(address=addr.strip(), subnet=subnet.strip())
self.add_ip_address(addr.strip(), 'ipv4')
self.facts['interfaces'][key]['ipv4'].append(ipv4)
def populate_ipv6_interfaces(self, data):
for key, value in iteritems(data):
try:
self.facts['interfaces'][key]['ipv6'] = list()
except KeyError:
self.facts['interfaces'][key] = dict()
self.facts['interfaces'][key]['ipv6'] = list()
addresses = re.findall(r'\s+(.+), subnet', value, re.M)
subnets = re.findall(r', subnet is (.+)$', value, re.M)
for addr, subnet in zip(addresses, subnets):
ipv6 = dict(address=addr.strip(), subnet=subnet.strip())
self.add_ip_address(addr.strip(), 'ipv6')
self.facts['interfaces'][key]['ipv6'].append(ipv6)
def add_ip_address(self, address, family):
if family == 'ipv4':
self.facts['all_ipv4_addresses'].append(address)
else:
self.facts['all_ipv6_addresses'].append(address)
def parse_neighbors(self, neighbors):
facts = dict()
for entry in neighbors.split('------------------------------------------------'):
if entry == '':
continue
intf = self.parse_lldp_intf(entry)
if intf is None:
return facts
intf = normalize_interface(intf)
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_lldp_host(entry)
fact['port'] = self.parse_lldp_port(entry)
facts[intf].append(fact)
return facts
def parse_cdp_neighbors(self, neighbors):
facts = dict()
for entry in neighbors.split('-------------------------'):
if entry == '':
continue
intf_port = self.parse_cdp_intf_port(entry)
if intf_port is None:
return facts
intf, port = intf_port
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_cdp_host(entry)
fact['port'] = port
facts[intf].append(fact)
return facts
def parse_interfaces(self, data):
parsed = dict()
key = ''
for line in data.split('\n'):
if len(line) == 0:
continue
elif line[0] == ' ':
parsed[key] += '\n%s' % line
else:
match = re.match(r'^(\S+)', line)
if match:
key = match.group(1)
parsed[key] = line
return parsed
def parse_description(self, data):
match = re.search(r'Description: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_macaddress(self, data):
match = re.search(r'Hardware is (?:.*), address is (\S+)', data)
if match:
return match.group(1)
def parse_ipv4(self, data):
match = re.search(r'Internet address is (\S+)', data)
if match:
addr, masklen = match.group(1).split('/')
return dict(address=addr, masklen=int(masklen))
def parse_mtu(self, data):
match = re.search(r'MTU (\d+)', data)
if match:
return int(match.group(1))
def parse_bandwidth(self, data):
match = re.search(r'BW (\d+)', data)
if match:
return int(match.group(1))
def parse_duplex(self, data):
match = re.search(r'(\w+) Duplex', data, re.M)
if match:
return match.group(1)
def parse_mediatype(self, data):
match = re.search(r'media type is (.+)$', data, re.M)
if match:
return match.group(1)
def parse_type(self, data):
match = re.search(r'Hardware is (.+),', data, re.M)
if match:
return match.group(1)
def parse_lineprotocol(self, data):
match = re.search(r'line protocol is (\S+)\s*$', data, re.M)
if match:
return match.group(1)
def parse_operstatus(self, data):
match = re.search(r'^(?:.+) is (.+),', data, re.M)
if match:
return match.group(1)
def parse_lldp_intf(self, data):
match = re.search(r'^Local Intf: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_host(self, data):
match = re.search(r'System Name: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_port(self, data):
match = re.search(r'Port id: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_cdp_intf_port(self, data):
match = re.search(r'^Interface: (.+), Port ID \(outgoing port\): (.+)$', data, re.M)
if match:
return match.group(1), match.group(2)
def parse_cdp_host(self, data):
match = re.search(r'^Device ID: (.+)$', data, re.M)
if match:
return match.group(1)

View file

@ -0,0 +1,154 @@
#
# -*- 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)
# utils
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.six import iteritems
def remove_command_from_config_list(interface, cmd, commands):
# To delete the passed config
if interface not in commands:
commands.insert(0, interface)
commands.append('no %s' % cmd)
return commands
def add_command_to_config_list(interface, cmd, commands):
# To set the passed config
if interface not in commands:
commands.insert(0, interface)
commands.append(cmd)
def dict_diff(sample_dict):
# Generate a set with passed dictionary for comparison
test_dict = {}
for k, v in iteritems(sample_dict):
if v is not None:
test_dict.update({k: v})
return_set = set(tuple(test_dict.items()))
return return_set
def filter_dict_having_none_value(want, have):
# Generate dict with have dict value which is None in want dict
test_dict = dict()
test_dict['name'] = want.get('name')
for k, v in iteritems(want):
if v is None:
val = have.get(k)
test_dict.update({k: val})
return test_dict
def remove_duplicate_interface(commands):
# Remove duplicate interface from commands
set_cmd = []
for each in commands:
if 'interface' in each:
if each not in set_cmd:
set_cmd.append(each)
else:
set_cmd.append(each)
return set_cmd
def search_obj_in_list(name, lst):
for o in lst:
if o['name'] == name:
return o
return None
def normalize_interface(name):
"""Return the normalized interface name
"""
if not name:
return
def _get_number(name):
digits = ''
for char in name:
if char.isdigit() or char in '/.':
digits += char
return digits
if name.lower().startswith('gi'):
if_type = 'GigabitEthernet'
elif name.lower().startswith('te'):
if_type = 'TenGigabitEthernet'
elif name.lower().startswith('fa'):
if_type = 'FastEthernet'
elif name.lower().startswith('fo'):
if_type = 'FortyGigabitEthernet'
elif name.lower().startswith('long'):
if_type = 'LongReachEthernet'
elif name.lower().startswith('et'):
if_type = 'Ethernet'
elif name.lower().startswith('vl'):
if_type = 'Vlan'
elif name.lower().startswith('lo'):
if_type = 'loopback'
elif name.lower().startswith('po'):
if_type = 'port-channel'
elif name.lower().startswith('nv'):
if_type = 'nve'
elif name.lower().startswith('twe'):
if_type = 'TwentyFiveGigE'
elif name.lower().startswith('hu'):
if_type = 'HundredGigE'
else:
if_type = None
number_list = name.split(' ')
if len(number_list) == 2:
number = number_list[-1].strip()
else:
number = _get_number(name)
if if_type:
proper_interface = if_type + number
else:
proper_interface = name
return proper_interface
def get_interface_type(interface):
"""Gets the type of interface
"""
if interface.upper().startswith('GI'):
return 'GigabitEthernet'
elif interface.upper().startswith('TE'):
return 'TenGigabitEthernet'
elif interface.upper().startswith('FA'):
return 'FastEthernet'
elif interface.upper().startswith('FO'):
return 'FortyGigabitEthernet'
elif interface.upper().startswith('LON'):
return 'LongReachEthernet'
elif interface.upper().startswith('ET'):
return 'Ethernet'
elif interface.upper().startswith('VL'):
return 'Vlan'
elif interface.upper().startswith('LO'):
return 'loopback'
elif interface.upper().startswith('PO'):
return 'port-channel'
elif interface.upper().startswith('NV'):
return 'nve'
elif interface.upper().startswith('TWE'):
return 'TwentyFiveGigE'
elif interface.upper().startswith('HU'):
return 'HundredGigE'
else:
return 'unknown'

View file

@ -9,7 +9,7 @@ __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': ['deprecated'],
'supported_by': 'network'} 'supported_by': 'network'}
@ -22,6 +22,10 @@ short_description: Manage Interface on Cisco IOS network devices
description: description:
- This module provides declarative management of Interfaces - This module provides declarative management of Interfaces
on Cisco IOS network devices. on Cisco IOS network devices.
deprecated:
removed_in: '2.13'
alternative: ios_interfaces
why: Newer and updated modules released with more functionality in Ansible 2.9
notes: notes:
- Tested against IOS 15.6 - Tested against IOS 15.6
options: options:

View file

@ -24,7 +24,9 @@ DOCUMENTATION = """
--- ---
module: ios_facts module: ios_facts
version_added: "2.2" version_added: "2.2"
author: "Peter Sprygada (@privateip)" author:
- "Peter Sprygada (@privateip)"
- "Sumit Jaiswal (@justjais)"
short_description: Collect facts from remote devices running Cisco IOS short_description: Collect facts from remote devices running Cisco IOS
description: description:
- Collects a base set of device facts from a remote device that - Collects a base set of device facts from a remote device that
@ -46,22 +48,48 @@ options:
- Use a value with an initial C(!) to collect all facts except that subset. - Use a value with an initial C(!) to collect all facts except that subset.
required: false required: false
default: '!config' default: '!config'
gather_network_resources:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
all and the resources like interfaces, vlans etc.
Can specify a list of values to include a larger subset.
choices: ['all', '!all', 'interfaces', '!interfaces']
version_added: "2.9"
""" """
EXAMPLES = """ EXAMPLES = """
# Collect all facts from the device - name: Gather all legacy facts
- ios_facts: ios_facts:
gather_subset: all gather_subset: all
# Collect only the config and default facts - name: Gather only the config and default facts
- ios_facts: ios_facts:
gather_subset: gather_subset:
- config - config
# Do not collect hardware facts - name: Do not gather hardware facts
- ios_facts: ios_facts:
gather_subset: gather_subset:
- "!hardware" - "!hardware"
- name: Gather legacy and resource facts
ios_facts:
gather_subset: all
gather_network_resources: all
- name: Gather only the interfaces resource facts and no legacy facts
ios_facts:
gather_subset:
- '!all'
- '!min'
gather_network_resources:
- interfaces
- name: Gather interfaces resource and minimal legacy facts
ios_facts:
gather_subset: min
gather_network_resources: interfaces
""" """
RETURN = """ RETURN = """
@ -70,6 +98,11 @@ ansible_net_gather_subset:
returned: always returned: always
type: list type: list
ansible_net_gather_network_resources:
description: The list of fact for network resource subsets collected from the device
returned: when the resource is configured
type: list
# default # default
ansible_net_model: ansible_net_model:
description: The model name returned from the device description: The model name returned from the device
@ -156,446 +189,29 @@ ansible_net_neighbors:
returned: when interfaces is configured returned: when interfaces is configured
type: dict type: dict
""" """
import platform
import re
from ansible.module_utils.network.ios.ios import run_commands, get_capabilities
from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args
from ansible.module_utils.network.ios.ios import normalize_interface
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems from ansible.module_utils.network.ios.argspec.facts.facts import FactsArgs
from ansible.module_utils.six.moves import zip from ansible.module_utils.network.ios.facts.facts import Facts
from ansible.module_utils.network.ios.ios import ios_argument_spec
class FactsBase(object):
COMMANDS = list()
def __init__(self, module):
self.module = module
self.facts = dict()
self.responses = None
def populate(self):
self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False)
def run(self, cmd):
return run_commands(self.module, commands=cmd, check_rc=False)
class Default(FactsBase):
COMMANDS = ['show version']
def populate(self):
super(Default, self).populate()
self.facts.update(self.platform_facts())
data = self.responses[0]
if data:
self.facts['iostype'] = self.parse_iostype(data)
self.facts['serialnum'] = self.parse_serialnum(data)
self.parse_stacks(data)
def parse_iostype(self, data):
match = re.search(r'\S+(X86_64_LINUX_IOSD-UNIVERSALK9-M)(\S+)', data)
if match:
return "IOS-XE"
else:
return "IOS"
def parse_serialnum(self, data):
match = re.search(r'board ID (\S+)', data)
if match:
return match.group(1)
def parse_stacks(self, data):
match = re.findall(r'^Model [Nn]umber\s+: (\S+)', data, re.M)
if match:
self.facts['stacked_models'] = match
match = re.findall(r'^System [Ss]erial [Nn]umber\s+: (\S+)', data, re.M)
if match:
self.facts['stacked_serialnums'] = match
def platform_facts(self):
platform_facts = {}
resp = get_capabilities(self.module)
device_info = resp['device_info']
platform_facts['system'] = device_info['network_os']
for item in ('model', 'image', 'version', 'platform', 'hostname'):
val = device_info.get('network_os_%s' % item)
if val:
platform_facts[item] = val
platform_facts['api'] = resp['network_api']
platform_facts['python_version'] = platform.python_version()
return platform_facts
class Hardware(FactsBase):
COMMANDS = [
'dir',
'show memory statistics'
]
def populate(self):
super(Hardware, self).populate()
data = self.responses[0]
if data:
self.facts['filesystems'] = self.parse_filesystems(data)
self.facts['filesystems_info'] = self.parse_filesystems_info(data)
data = self.responses[1]
if data:
if 'Invalid input detected' in data:
warnings.append('Unable to gather memory statistics')
else:
processor_line = [l for l in data.splitlines()
if 'Processor' in l].pop()
match = re.findall(r'\s(\d+)\s', processor_line)
if match:
self.facts['memtotal_mb'] = int(match[0]) / 1024
self.facts['memfree_mb'] = int(match[3]) / 1024
def parse_filesystems(self, data):
return re.findall(r'^Directory of (\S+)/', data, re.M)
def parse_filesystems_info(self, data):
facts = dict()
fs = ''
for line in data.split('\n'):
match = re.match(r'^Directory of (\S+)/', line)
if match:
fs = match.group(1)
facts[fs] = dict()
continue
match = re.match(r'^(\d+) bytes total \((\d+) bytes free\)', line)
if match:
facts[fs]['spacetotal_kb'] = int(match.group(1)) / 1024
facts[fs]['spacefree_kb'] = int(match.group(2)) / 1024
return facts
class Config(FactsBase):
COMMANDS = ['show running-config']
def populate(self):
super(Config, self).populate()
data = self.responses[0]
if data:
data = re.sub(
r'^Building configuration...\s+Current configuration : \d+ bytes\n',
'', data, flags=re.MULTILINE)
self.facts['config'] = data
class Interfaces(FactsBase):
COMMANDS = [
'show interfaces',
'show ip interface',
'show ipv6 interface',
'show lldp',
'show cdp'
]
def populate(self):
super(Interfaces, self).populate()
self.facts['all_ipv4_addresses'] = list()
self.facts['all_ipv6_addresses'] = list()
self.facts['neighbors'] = {}
data = self.responses[0]
if data:
interfaces = self.parse_interfaces(data)
self.facts['interfaces'] = self.populate_interfaces(interfaces)
data = self.responses[1]
if data:
data = self.parse_interfaces(data)
self.populate_ipv4_interfaces(data)
data = self.responses[2]
if data:
data = self.parse_interfaces(data)
self.populate_ipv6_interfaces(data)
data = self.responses[3]
lldp_errs = ['Invalid input', 'LLDP is not enabled']
if data and not any(err in data for err in lldp_errs):
neighbors = self.run(['show lldp neighbors detail'])
if neighbors:
self.facts['neighbors'].update(self.parse_neighbors(neighbors[0]))
data = self.responses[4]
cdp_errs = ['CDP is not enabled']
if data and not any(err in data for err in cdp_errs):
cdp_neighbors = self.run(['show cdp neighbors detail'])
if cdp_neighbors:
self.facts['neighbors'].update(self.parse_cdp_neighbors(cdp_neighbors[0]))
def populate_interfaces(self, interfaces):
facts = dict()
for key, value in iteritems(interfaces):
intf = dict()
intf['description'] = self.parse_description(value)
intf['macaddress'] = self.parse_macaddress(value)
intf['mtu'] = self.parse_mtu(value)
intf['bandwidth'] = self.parse_bandwidth(value)
intf['mediatype'] = self.parse_mediatype(value)
intf['duplex'] = self.parse_duplex(value)
intf['lineprotocol'] = self.parse_lineprotocol(value)
intf['operstatus'] = self.parse_operstatus(value)
intf['type'] = self.parse_type(value)
facts[key] = intf
return facts
def populate_ipv4_interfaces(self, data):
for key, value in data.items():
self.facts['interfaces'][key]['ipv4'] = list()
primary_address = addresses = []
primary_address = re.findall(r'Internet address is (.+)$', value, re.M)
addresses = re.findall(r'Secondary address (.+)$', value, re.M)
if len(primary_address) == 0:
continue
addresses.append(primary_address[0])
for address in addresses:
addr, subnet = address.split("/")
ipv4 = dict(address=addr.strip(), subnet=subnet.strip())
self.add_ip_address(addr.strip(), 'ipv4')
self.facts['interfaces'][key]['ipv4'].append(ipv4)
def populate_ipv6_interfaces(self, data):
for key, value in iteritems(data):
try:
self.facts['interfaces'][key]['ipv6'] = list()
except KeyError:
self.facts['interfaces'][key] = dict()
self.facts['interfaces'][key]['ipv6'] = list()
addresses = re.findall(r'\s+(.+), subnet', value, re.M)
subnets = re.findall(r', subnet is (.+)$', value, re.M)
for addr, subnet in zip(addresses, subnets):
ipv6 = dict(address=addr.strip(), subnet=subnet.strip())
self.add_ip_address(addr.strip(), 'ipv6')
self.facts['interfaces'][key]['ipv6'].append(ipv6)
def add_ip_address(self, address, family):
if family == 'ipv4':
self.facts['all_ipv4_addresses'].append(address)
else:
self.facts['all_ipv6_addresses'].append(address)
def parse_neighbors(self, neighbors):
facts = dict()
for entry in neighbors.split('------------------------------------------------'):
if entry == '':
continue
intf = self.parse_lldp_intf(entry)
if intf is None:
return facts
intf = normalize_interface(intf)
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_lldp_host(entry)
fact['port'] = self.parse_lldp_port(entry)
facts[intf].append(fact)
return facts
def parse_cdp_neighbors(self, neighbors):
facts = dict()
for entry in neighbors.split('-------------------------'):
if entry == '':
continue
intf_port = self.parse_cdp_intf_port(entry)
if intf_port is None:
return facts
intf, port = intf_port
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_cdp_host(entry)
fact['port'] = port
facts[intf].append(fact)
return facts
def parse_interfaces(self, data):
parsed = dict()
key = ''
for line in data.split('\n'):
if len(line) == 0:
continue
elif line[0] == ' ':
parsed[key] += '\n%s' % line
else:
match = re.match(r'^(\S+)', line)
if match:
key = match.group(1)
parsed[key] = line
return parsed
def parse_description(self, data):
match = re.search(r'Description: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_macaddress(self, data):
match = re.search(r'Hardware is (?:.*), address is (\S+)', data)
if match:
return match.group(1)
def parse_ipv4(self, data):
match = re.search(r'Internet address is (\S+)', data)
if match:
addr, masklen = match.group(1).split('/')
return dict(address=addr, masklen=int(masklen))
def parse_mtu(self, data):
match = re.search(r'MTU (\d+)', data)
if match:
return int(match.group(1))
def parse_bandwidth(self, data):
match = re.search(r'BW (\d+)', data)
if match:
return int(match.group(1))
def parse_duplex(self, data):
match = re.search(r'(\w+) Duplex', data, re.M)
if match:
return match.group(1)
def parse_mediatype(self, data):
match = re.search(r'media type is (.+)$', data, re.M)
if match:
return match.group(1)
def parse_type(self, data):
match = re.search(r'Hardware is (.+),', data, re.M)
if match:
return match.group(1)
def parse_lineprotocol(self, data):
match = re.search(r'line protocol is (\S+)\s*$', data, re.M)
if match:
return match.group(1)
def parse_operstatus(self, data):
match = re.search(r'^(?:.+) is (.+),', data, re.M)
if match:
return match.group(1)
def parse_lldp_intf(self, data):
match = re.search(r'^Local Intf: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_host(self, data):
match = re.search(r'System Name: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_port(self, data):
match = re.search(r'Port id: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_cdp_intf_port(self, data):
match = re.search(r'^Interface: (.+), Port ID \(outgoing port\): (.+)$', data, re.M)
if match:
return match.group(1), match.group(2)
def parse_cdp_host(self, data):
match = re.search(r'^Device ID: (.+)$', data, re.M)
if match:
return match.group(1)
FACT_SUBSETS = dict(
default=Default,
hardware=Hardware,
interfaces=Interfaces,
config=Config,
)
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
warnings = list()
def main(): def main():
"""main entry point for module execution """ Main entry point for AnsibleModule
""" """
argument_spec = dict( argument_spec = FactsArgs.argument_spec
gather_subset=dict(default=['!config'], type='list')
)
argument_spec.update(ios_argument_spec) argument_spec.update(ios_argument_spec)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
gather_subset = module.params['gather_subset'] warnings = ['default value for `gather_subset` '
'will be changed to `min` from `!config` v2.11 onwards']
runable_subsets = set() result = Facts(module).get_facts()
exclude_subsets = set()
for subset in gather_subset: ansible_facts, additional_warnings = result
if subset == 'all': warnings.extend(additional_warnings)
runable_subsets.update(VALID_SUBSETS)
continue
if subset.startswith('!'):
subset = subset[1:]
if subset == 'all':
exclude_subsets.update(VALID_SUBSETS)
continue
exclude = True
else:
exclude = False
if subset not in VALID_SUBSETS:
module.fail_json(msg='Bad subset')
if exclude:
exclude_subsets.add(subset)
else:
runable_subsets.add(subset)
if not runable_subsets:
runable_subsets.update(VALID_SUBSETS)
runable_subsets.difference_update(exclude_subsets)
runable_subsets.add('default')
facts = dict()
facts['gather_subset'] = list(runable_subsets)
instances = list()
for key in runable_subsets:
instances.append(FACT_SUBSETS[key](module))
for inst in instances:
inst.populate()
facts.update(inst.facts)
ansible_facts = dict()
for key, value in iteritems(facts):
key = 'ansible_net_%s' % key
ansible_facts[key] = value
check_args(module, warnings)
module.exit_json(ansible_facts=ansible_facts, warnings=warnings) module.exit_json(ansible_facts=ansible_facts, warnings=warnings)

View file

@ -0,0 +1,396 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The module file for ios_interfaces
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'}
DOCUMENTATION = """
---
module: ios_interfaces
version_added: 2.9
short_description: Manages interface attributes of Cisco IOS network devices
description: This module manages the interface attributes of Cisco IOS network devices.
author: Sumit Jaiswal (@justjais)
notes:
- Tested against Cisco IOSv Version 15.2 on VIRL
options:
config:
description: A dictionary of interface options
type: list
suboptions:
name:
description:
- Full name of interface, e.g. GigabitEthernet0/2, loopback999.
type: str
required: True
description:
description:
- Interface description.
type: str
enabled:
description:
- Administrative state of the interface.
- Set the value to C(true) to administratively enable the interface or C(false) to disable it.
type: bool
default: True
speed:
description:
- Interface link speed. Applicable for Ethernet interfaces only.
type: str
mtu:
description:
- MTU for a specific interface. Applicable for Ethernet interfaces only.
- Refer to vendor documentation for valid values.
type: int
duplex:
description:
- Interface link status. Applicable for Ethernet interfaces only, either in half duplex,
full duplex or in automatic state which negotiates the duplex automatically.
type: str
choices: ['full', 'half', 'auto']
state:
choices:
- merged
- replaced
- overridden
- deleted
default: merged
description:
- The state the configuration should be left in
type: str
"""
EXAMPLES = """
---
# Using merged
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# no ip address
# duplex auto
# speed auto
- name: Merge provided configuration with device configuration
ios_interfaces:
config:
- name: GigabitEthernet0/2
description: 'Configured and Merged by Ansible Network'
enabled: True
- name: GigabitEthernet0/3
description: 'Configured and Merged by Ansible Network'
mtu: 2800
enabled: False
speed: 100
duplex: full
state: merged
# After state:
# ------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured and Merged by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured and Merged by Ansible Network
# mtu 2800
# no ip address
# shutdown
# duplex full
# speed 100
# Using replaced
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# mtu 2000
# no ip address
# shutdown
# duplex full
# speed 100
- name: Replaces device configuration of listed interfaces with provided configuration
ios_interfaces:
config:
- name: GigabitEthernet0/3
description: 'Configured and Replaced by Ansible Network'
enabled: False
duplex: auto
mtu: 2500
speed: 1000
state: replaced
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured and Replaced by Ansible Network
# mtu 2500
# no ip address
# shutdown
# duplex full
# speed 1000
# Using overridden
# Before state:
# -------------
#
# vios#show running-config | section ^interface#
# interface GigabitEthernet0/1
# description Configured by Ansible
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible
# mtu 2800
# no ip address
# shutdown
# duplex full
# speed 100
- name: Override device configuration of all interfaces with provided configuration
ios_interfaces:
config:
- name: GigabitEthernet0/2
description: 'Configured and Overridden by Ansible Network'
speed: 1000
- name: GigabitEthernet0/3
description: 'Configured and Overridden by Ansible Network'
enabled: False
duplex: full
mtu: 2000
state: overridden
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured and Overridden by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured and Overridden by Ansible Network
# mtu 2000
# no ip address
# shutdown
# duplex full
# speed 100
# Using Deleted
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured and Overridden by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured and Replaced by Ansible Network
# mtu 2500
# no ip address
# shutdown
# duplex full
# speed 1000
- name: "Delete module attributes of given interfaces (Note: This won't delete the interface itself)"
ios_interfaces:
config:
- name: GigabitEthernet0/2
- name: GigabitEthernet0/3
state: deleted
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/3
# no ip address
# duplex auto
# speed auto
# Using Deleted without any config passed
#"(NOTE: This will delete all of configured resource module attributes from each configured interface)"
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured and Overridden by Ansible Network
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured and Replaced by Ansible Network
# mtu 2500
# no ip address
# shutdown
# duplex full
# speed 1000
- name: "Delete module attributes of all interfaces (Note: This won't delete the interface itself)"
ios_interfaces:
state: deleted
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/3
# no ip address
# duplex auto
# speed auto
"""
RETURN = """
before:
description: The configuration prior to the model invocation
returned: always
type: list
sample: The configuration returned will alwys be in the same format of the paramters above.
after:
description: The resulting configuration model invocation
returned: when changed
type: list
sample: The configuration returned will alwys be in the same format of the paramters above.
commands:
description: The set of commands pushed to the remote device
returned: always
type: list
sample: ['interface GigabitEthernet 0/1', 'description This is test', 'speed 100']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.ios.argspec.interfaces.interfaces import InterfacesArgs
from ansible.module_utils.network.ios.config.interfaces.interfaces import Interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=InterfacesArgs.argument_spec,
supports_check_mode=True)
result = Interfaces(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -18,7 +18,7 @@
# It's a failure # It's a failure
- "result.failed == true" - "result.failed == true"
# Sensible Failure message # Sensible Failure message
- "result.msg == 'Bad subset'" - "result.msg == 'Subset must be one of [config, default, hardware, interfaces], got foobar'"
############### ###############
# FIXME Future # FIXME Future

View file

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

View file

@ -0,0 +1 @@
dependencies: []

View file

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

View file

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

View file

@ -0,0 +1,17 @@
---
- name: Populate Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
interface GigabitEthernet 0/1
description this is interface1
mtu 65
speed 10
no shutdown
interface GigabitEthernet 0/2
description this is interface2
mtu 110
duplex auto
speed 100
shutdown

View file

@ -0,0 +1,24 @@
---
- name: Remove Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
interface loopback888
no description
no shutdown
interface loopback999
no description
no shutdown
interface GigabitEthernet 0/1
no description
no mtu
no duplex
no speed
no shutdown
interface GigabitEthernet 0/2
no description
no mtu
no duplex
no speed
no shutdown

View file

@ -0,0 +1,47 @@
---
- debug:
msg: "Start Deleted integration state for ios_interfaces ansible_connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Delete attributes of all configured interfaces
ios_interfaces: &deleted
state: deleted
register: result
- debug:
msg:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) }}"
- "{{ deleted['before'] | symmetric_difference(result['before']) }}"
- "{{ deleted['after'] | symmetric_difference(result['after']) }}"
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) | length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ deleted['before'] | symmetric_difference(result['before']) | length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['after']) | length == 0 }}"
- name: Delete attributes of all configured interfaces (IDEMPOTENT)
ios_interfaces: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,50 @@
---
- debug:
msg: "START Merged ios_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge provided configuration with device configuration
ios_interfaces: &merged
config:
- name: GigabitEthernet0/1
description: 'Configured and Merged by Ansible-Network'
mtu: 110
enabled: True
duplex: half
- name: GigabitEthernet0/2
description: 'Configured and Merged by Ansible-Network'
mtu: 2800
enabled: False
speed: 100
duplex: full
state: merged
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ merged['before'] | symmetric_difference(result['before']) | length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['after']) | length == 0 }}"
- name: Merge provided configuration with device configuration (IDEMPOTENT)
ios_interfaces: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,46 @@
---
- debug:
msg: "START Overridden ios_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Override device configuration of all interfaces with provided configuration
ios_interfaces: &overridden
config:
- name: GigabitEthernet0/2
description: 'Configured and Overridden by Ansible-Network'
enabled: False
duplex: full
mtu: 2000
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:
- "{{ overridden['before'] | 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: Override device configuration of all interfaces with provided configuration (IDEMPOTENT)
ios_interfaces: *overridden
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,47 @@
---
- debug:
msg: "START Replaced ios_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Replaces device configuration of listed interfaces with provided configuration
ios_interfaces: &replaced
config:
- name: GigabitEthernet0/1
description: 'Configured and Replaced by Ansible-Network'
mtu: 110
- name: GigabitEthernet0/2
description: 'Configured and Replaced by Ansible-Network'
speed: 10
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:
- "{{ replaced['before'] | 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: Replaces device configuration of listed interfaces with provided configuration (IDEMPOTENT)
ios_interfaces: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,214 @@
---
merged:
before:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- duplex: auto
enabled: true
name: GigabitEthernet0/1
speed: auto
- duplex: auto
enabled: true
name: GigabitEthernet0/2
speed: auto
commands:
- "interface GigabitEthernet0/1"
- "description Configured and Merged by Ansible-Network"
- "mtu 110"
- "duplex half"
- "interface GigabitEthernet0/2"
- "description Configured and Merged by Ansible-Network"
- "mtu 2800"
- "speed 100"
- "duplex full"
- "shutdown"
after:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- description: Configured and Merged by Ansible-Network
duplex: half
enabled: true
mtu: 110
name: GigabitEthernet0/1
speed: auto
- description: Configured and Merged by Ansible-Network
duplex: full
enabled: false
mtu: 2800
name: GigabitEthernet0/2
speed: '100'
replaced:
before:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- description: this is interface1
duplex: auto
enabled: true
mtu: 65
name: GigabitEthernet0/1
speed: '10'
- description: this is interface2
duplex: auto
enabled: false
mtu: 110
name: GigabitEthernet0/2
speed: '100'
commands:
- "interface GigabitEthernet0/1"
- "no speed"
- "description Configured and Replaced by Ansible-Network"
- "mtu 110"
- "interface GigabitEthernet0/2"
- "no shutdown"
- "no mtu"
- "description Configured and Replaced by Ansible-Network"
- "speed 10"
after:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- description: Configured and Replaced by Ansible-Network
duplex: auto
enabled: true
mtu: 110
name: GigabitEthernet0/1
speed: auto
- description: Configured and Replaced by Ansible-Network
duplex: auto
enabled: true
name: GigabitEthernet0/2
speed: '10'
overridden:
before:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- description: this is interface1
duplex: auto
enabled: true
mtu: 65
name: GigabitEthernet0/1
speed: '10'
- description: this is interface2
duplex: auto
enabled: false
mtu: 110
name: GigabitEthernet0/2
speed: '100'
commands:
- "interface GigabitEthernet0/1"
- "no description"
- "no speed"
- "no mtu"
- "interface GigabitEthernet0/2"
- "no speed"
- "description Configured and Overridden by Ansible-Network"
- "mtu 2000"
- "duplex full"
after:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- duplex: auto
enabled: true
name: GigabitEthernet0/1
speed: auto
- description: Configured and Overridden by Ansible-Network
duplex: full
enabled: false
mtu: 2000
name: GigabitEthernet0/2
speed: auto
deleted:
before:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- description: this is interface1
duplex: auto
enabled: true
mtu: 65
name: GigabitEthernet0/1
speed: '10'
- description: this is interface2
duplex: auto
enabled: false
mtu: 110
name: GigabitEthernet0/2
speed: '100'
commands:
- "interface GigabitEthernet0/1"
- "no description"
- "no speed"
- "no mtu"
- "interface GigabitEthernet0/2"
- "no description"
- "no shutdown"
- "no speed"
- "no mtu"
after:
- enabled: true
name: loopback888
- enabled: true
name: loopback999
- duplex: auto
enabled: true
name: GigabitEthernet0/0
speed: auto
- duplex: auto
enabled: true
name: GigabitEthernet0/1
speed: auto
- duplex: auto
enabled: true
name: GigabitEthernet0/2
speed: auto

View file

@ -4724,12 +4724,12 @@ lib/ansible/modules/network/ios/ios_facts.py future-import-boilerplate
lib/ansible/modules/network/ios/ios_facts.py metaclass-boilerplate lib/ansible/modules/network/ios/ios_facts.py metaclass-boilerplate
lib/ansible/modules/network/ios/ios_facts.py validate-modules:E324 lib/ansible/modules/network/ios/ios_facts.py validate-modules:E324
lib/ansible/modules/network/ios/ios_facts.py validate-modules:E337 lib/ansible/modules/network/ios/ios_facts.py validate-modules:E337
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E322 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E322
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E324 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E324
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E326 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E326
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E337 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E337
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E338 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E338
lib/ansible/modules/network/ios/ios_interface.py validate-modules:E340 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E340
lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E322 lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E322
lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E324 lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E324
lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E326 lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E326

View file

@ -30,10 +30,13 @@ class TestIosFactsModule(TestIosModule):
def setUp(self): def setUp(self):
super(TestIosFactsModule, self).setUp() super(TestIosFactsModule, self).setUp()
self.mock_run_commands = patch('ansible.modules.network.ios.ios_facts.run_commands') self.mock_run_commands = patch('ansible.module_utils.network.ios.facts.legacy.base.run_commands')
self.run_commands = self.mock_run_commands.start() self.run_commands = self.mock_run_commands.start()
self.mock_get_capabilities = patch('ansible.modules.network.ios.ios_facts.get_capabilities') self.mock_get_resource_connection = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection')
self.get_resource_connection = self.mock_get_resource_connection.start()
self.mock_get_capabilities = patch('ansible.module_utils.network.ios.facts.legacy.base.get_capabilities')
self.get_capabilities = self.mock_get_capabilities.start() self.get_capabilities = self.mock_get_capabilities.start()
self.get_capabilities.return_value = { self.get_capabilities.return_value = {
'device_info': { 'device_info': {