Add iosxr_lacp resource module (#59281)

Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
This commit is contained in:
Nilashish Chakraborty 2019-07-24 22:55:42 +05:30 committed by GitHub
parent 97edfccc70
commit f2b0bfd4aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1289 additions and 323 deletions

View file

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

View file

@ -0,0 +1,62 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The arg spec for the iosxr_lacp module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class LacpArgs(object): # pylint: disable=R0903
"""The arg spec for the iosxr_lacp module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'options': {
'system': {
'options': {
'mac': {
'type': 'str'
},
'priority': {
'type': 'int'
}
},
'type': 'dict'
}
},
'type': 'dict'
},
'state': {
'choices': ['merged', 'replaced', 'deleted'],
'default': 'merged',
'type': 'str'
}
} # pylint: disable=C0301

View file

@ -0,0 +1,169 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr_lacp 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.iosxr.facts.facts import Facts
from ansible.module_utils.network.common.utils import dict_diff
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common import utils
class Lacp(ConfigBase):
"""
The iosxr_lacp class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'lacp',
]
def __init__(self, module):
super(Lacp, self).__init__(module)
def get_lacp_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)
lacp_facts = facts['ansible_network_resources'].get('lacp')
if not lacp_facts:
return {}
return lacp_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
commands = list()
warnings = list()
existing_lacp_facts = self.get_lacp_facts()
commands.extend(self.set_config(existing_lacp_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_lacp_facts = self.get_lacp_facts()
result['before'] = existing_lacp_facts
if result['changed']:
result['after'] = changed_lacp_facts
result['warnings'] = warnings
return result
def set_config(self, existing_lacp_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
want = self._module.params.get('config')
if not want:
want = {}
have = existing_lacp_facts
resp = self.set_state(want, have)
return to_list(resp)
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
state = self._module.params['state']
if 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
@staticmethod
def _state_replaced(want, have):
""" The command generator when state is replaced
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
commands.extend(
Lacp._state_deleted(want, have)
)
commands.extend(
Lacp._state_merged(want, have)
)
return commands
@staticmethod
def _state_merged(want, have):
""" The command generator when state is merged
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
commands = []
updates = dict_diff(have, want)
if updates:
for key, value in iteritems(updates['system']):
if value:
commands.append('lacp system {0} {1}'.format(key, value))
return commands
@staticmethod
def _state_deleted(want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
for x in [k for k in have.get('system', {}) if k not in utils.remove_empties(want.get('system', {}))]:
commands.append('no lacp system {0}'.format(x))
return commands

View file

@ -0,0 +1,60 @@
#
# -*- 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 iosxr
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.iosxr.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.iosxr.facts.lacp.lacp import LacpFacts
from ansible.module_utils.network.iosxr.facts.legacy.\
base import Default, Hardware, Interfaces, Config
FACT_LEGACY_SUBSETS = dict(
default=Default,
hardware=Hardware,
interfaces=Interfaces,
config=Config,
)
FACT_RESOURCE_SUBSETS = dict(
lacp=LacpFacts,
)
class Facts(FactsBase):
""" The fact class for iosxr
"""
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 iosxr
: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,82 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr lacp 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
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.iosxr.argspec.lacp.lacp import LacpArgs
class LacpFacts(object):
""" The iosxr lacp fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = LacpArgs.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 lacp
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
if not data:
data = connection.get_config(flags='lacp')
obj = {}
if data:
lacp_obj = self.render_config(self.generated_spec, data)
if lacp_obj:
obj = lacp_obj
ansible_facts['ansible_network_resources'].pop('lacp', None)
facts = {}
if obj:
params = utils.validate_config(self.argument_spec, {'config': obj})
facts['lacp'] = utils.remove_empties(params['config'])
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)
system_priority = utils.parse_conf_arg(conf, 'priority')
config['system']['priority'] = int(system_priority) if system_priority else system_priority
config['system']['mac'] = utils.parse_conf_arg(conf, 'mac')
return config

View file

@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr 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.iosxr.iosxr import run_commands, get_capabilities
from ansible.module_utils.six import iteritems
from ansible.module_utils.six.moves import zip
class FactsBase(object):
COMMANDS = frozenset()
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, list(self.COMMANDS), check_rc=False)
class Default(FactsBase):
def populate(self):
self.facts.update(self.platform_facts())
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 /all',
'show memory summary'
]
def populate(self):
super(Hardware, self).populate()
data = self.responses[0]
self.facts['filesystems'] = self.parse_filesystems(data)
data = self.responses[1]
match = re.search(r'Physical Memory: (\d+)M total \((\d+)', data)
if match:
self.facts['memtotal_mb'] = match.group(1)
self.facts['memfree_mb'] = match.group(2)
def parse_filesystems(self, data):
return re.findall(r'^Directory of (\S+)', data, re.M)
class Config(FactsBase):
COMMANDS = [
'show running-config'
]
def populate(self):
super(Config, self).populate()
self.facts['config'] = self.responses[0]
class Interfaces(FactsBase):
COMMANDS = [
'show interfaces',
'show ipv6 interface',
'show lldp',
'show lldp neighbors detail'
]
def populate(self):
super(Interfaces, self).populate()
self.facts['all_ipv4_addresses'] = list()
self.facts['all_ipv6_addresses'] = list()
interfaces = self.parse_interfaces(self.responses[0])
self.facts['interfaces'] = self.populate_interfaces(interfaces)
data = self.responses[1]
if len(data) > 0:
data = self.parse_interfaces(data)
self.populate_ipv6_interfaces(data)
if 'LLDP is not enabled' not in self.responses[2]:
neighbors = self.responses[3]
self.facts['neighbors'] = self.parse_neighbors(neighbors)
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)
ipv4 = self.parse_ipv4(value)
intf['ipv4'] = self.parse_ipv4(value)
if ipv4:
self.add_ip_address(ipv4['address'], 'ipv4')
intf['mtu'] = self.parse_mtu(value)
intf['bandwidth'] = self.parse_bandwidth(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_ipv6_interfaces(self, data):
for key, value in iteritems(data):
if key in ['No', 'RPF'] or key.startswith('IP'):
continue
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()
nbors = neighbors.split('------------------------------------------------')
for entry in nbors[1:]:
if entry == '':
continue
intf = self.parse_lldp_intf(entry)
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_lldp_host(entry)
fact['remote_description'] = self.parse_lldp_remote_desc(entry)
fact['port'] = self.parse_lldp_port(entry)
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'address is (\S+)', data)
if match:
return match.group(1)
def parse_ipv4(self, data):
match = re.search(r'Internet address is (\S+)/(\d+)', data)
if match:
addr = match.group(1)
masklen = int(match.group(2))
return dict(address=addr, masklen=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+)(?: D|-d)uplex', 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+?$', 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 Interface: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_remote_desc(self, data):
match = re.search(r'Port Description: (.+)$', 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)

View file

@ -1,33 +1,36 @@
#!/usr/bin/python #!/usr/bin/python
# # -*- coding: utf-8 -*-
# Copyright: Ansible Project # Copyright 2019 Red Hat
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The module file for iosxr_facts
"""
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': [u'preview'],
'supported_by': 'network'} 'supported_by': 'network'}
DOCUMENTATION = """ DOCUMENTATION = """
--- ---
module: iosxr_facts module: iosxr_facts
version_added: "2.2" version_added: 2.2
author: "Ricardo Carrillo Cruz (@rcarrillocruz)" short_description: Get facts about iosxr devices.
short_description: Collect facts from remote devices running IOS XR
description:
- Collects a base set of device facts from a remote device that
is running IOS XR. This module prepends all of the
base network fact keys with C(ansible_net_<fact>). The facts
module will always collect a base set of facts from the device
and can enable or disable collection of additional facts.
extends_documentation_fragment: iosxr extends_documentation_fragment: iosxr
notes: description:
- Tested against IOS XRv 6.1.2 - Collects facts from network devices running the iosxr operating
- This module does not support netconf connection system. This module places the facts gathered in the fact tree keyed by the
respective resource name. The facts module will always collect a
base set of facts from the device and can enable or disable
collection of additional facts.
author:
- Ricardo Carrillo Cruz (@rcarrillocruz)
- Nilashish Chakraborty (@Nilashishc)
options: options:
gather_subset: gather_subset:
description: description:
@ -39,12 +42,24 @@ options:
not be collected. not be collected.
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, lacp etc.
Can specify a list of values to include a larger subset. Values
can also be used with an initial C(M(!)) to specify that a
specific subset should not be collected.
required: false
choices: ['all', 'lacp']
version_added: "2.9"
""" """
EXAMPLES = """ EXAMPLES = """
# Collect all facts from the device # Gather all facts
- iosxr_facts: - iosxr_facts:
gather_subset: all gather_subset: all
gather_network_resources: all
# Collect only the config and default facts # Collect only the config and default facts
- iosxr_facts: - iosxr_facts:
@ -55,6 +70,24 @@ EXAMPLES = """
- iosxr_facts: - iosxr_facts:
gather_subset: gather_subset:
- "!hardware" - "!hardware"
# Collect only the lag_interfaces facts
- iosxr_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources:
- lacp
# Do not collect lag_interfaces facts
- iosxr_facts:
gather_network_resources:
- "!lacp"
# Collect lag_interfaces and minimal default facts
- iosxr_facts:
gather_subset: min
gather_network_resources: lacp
""" """
RETURN = """ RETURN = """
@ -126,322 +159,38 @@ ansible_net_neighbors:
description: The list of LLDP neighbors from the remote device description: The list of LLDP neighbors from the remote device
returned: when interfaces is configured returned: when interfaces is configured
type: dict type: dict
# network resources
ansible_net_gather_network_resources:
description: The list of fact resource subsets collected from the device
returned: always
type: list
""" """
import platform
import re
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, run_commands, get_capabilities from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec
from ansible.module_utils.six import iteritems from ansible.module_utils.network.iosxr.argspec.facts.facts import FactsArgs
from ansible.module_utils.six.moves import zip from ansible.module_utils.network.iosxr.facts.facts import Facts
class FactsBase(object):
COMMANDS = frozenset()
def __init__(self, module):
self.module = module
self.facts = dict()
self.responses = None
def populate(self):
self.responses = run_commands(self.module, list(self.COMMANDS), check_rc=False)
class Default(FactsBase):
def populate(self):
self.facts.update(self.platform_facts())
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 /all',
'show memory summary'
]
def populate(self):
super(Hardware, self).populate()
data = self.responses[0]
self.facts['filesystems'] = self.parse_filesystems(data)
data = self.responses[1]
match = re.search(r'Physical Memory: (\d+)M total \((\d+)', data)
if match:
self.facts['memtotal_mb'] = match.group(1)
self.facts['memfree_mb'] = match.group(2)
def parse_filesystems(self, data):
return re.findall(r'^Directory of (\S+)', data, re.M)
class Config(FactsBase):
COMMANDS = [
'show running-config'
]
def populate(self):
super(Config, self).populate()
self.facts['config'] = self.responses[0]
class Interfaces(FactsBase):
COMMANDS = [
'show interfaces',
'show ipv6 interface',
'show lldp',
'show lldp neighbors detail'
]
def populate(self):
super(Interfaces, self).populate()
self.facts['all_ipv4_addresses'] = list()
self.facts['all_ipv6_addresses'] = list()
interfaces = self.parse_interfaces(self.responses[0])
self.facts['interfaces'] = self.populate_interfaces(interfaces)
data = self.responses[1]
if len(data) > 0:
data = self.parse_interfaces(data)
self.populate_ipv6_interfaces(data)
if 'LLDP is not enabled' not in self.responses[2]:
neighbors = self.responses[3]
self.facts['neighbors'] = self.parse_neighbors(neighbors)
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)
ipv4 = self.parse_ipv4(value)
intf['ipv4'] = self.parse_ipv4(value)
if ipv4:
self.add_ip_address(ipv4['address'], 'ipv4')
intf['mtu'] = self.parse_mtu(value)
intf['bandwidth'] = self.parse_bandwidth(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_ipv6_interfaces(self, data):
for key, value in iteritems(data):
if key in ['No', 'RPF'] or key.startswith('IP'):
continue
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()
nbors = neighbors.split('------------------------------------------------')
for entry in nbors[1:]:
if entry == '':
continue
intf = self.parse_lldp_intf(entry)
if intf not in facts:
facts[intf] = list()
fact = dict()
fact['host'] = self.parse_lldp_host(entry)
fact['remote_description'] = self.parse_lldp_remote_desc(entry)
fact['port'] = self.parse_lldp_port(entry)
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'address is (\S+)', data)
if match:
return match.group(1)
def parse_ipv4(self, data):
match = re.search(r'Internet address is (\S+)/(\d+)', data)
if match:
addr = match.group(1)
masklen = int(match.group(2))
return dict(address=addr, masklen=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+)(?: D|-d)uplex', 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+?$', 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 Interface: (.+)$', data, re.M)
if match:
return match.group(1)
def parse_lldp_remote_desc(self, data):
match = re.search(r'Port Description: (.+)$', 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)
FACT_SUBSETS = dict(
default=Default,
hardware=Hardware,
interfaces=Interfaces,
config=Config,
)
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
def main(): def main():
spec = dict( """
gather_subset=dict(default=['!config'], type='list') Main entry point for module execution
)
:returns: ansible_facts
"""
spec = FactsArgs.argument_spec
spec.update(iosxr_argument_spec) spec.update(iosxr_argument_spec)
module = AnsibleModule(argument_spec=spec, module = AnsibleModule(argument_spec=spec,
supports_check_mode=True) supports_check_mode=True)
warnings = ['default value for `gather_subset` '
'will be changed to `min` from `!config` v2.11 onwards']
warnings = list() result = Facts(module).get_facts()
gather_subset = module.params['gather_subset'] ansible_facts, additional_warnings = result
warnings.extend(additional_warnings)
runable_subsets = set()
exclude_subsets = set()
for subset in gather_subset:
if subset == 'all':
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
module.exit_json(ansible_facts=ansible_facts, warnings=warnings) module.exit_json(ansible_facts=ansible_facts, warnings=warnings)

View file

@ -0,0 +1,283 @@
#!/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 iosxr_lacp
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: iosxr_lacp
version_added: 2.9
short_description: Manage Global Link Aggregation Control Protocol (LACP) on IOS-XR devices.
description:
- This module manages Global Link Aggregation Control Protocol (LACP) on IOS-XR devices.
author: Nilashish Chakraborty (@nilashishc)
options:
config:
description: The provided configurations.
type: dict
suboptions:
system:
description: This option sets the default system parameters for LACP bundles.
type: dict
suboptions:
priority:
description:
- The system priority to use in LACP negotiations.
- Lower value is higher priority.
- Refer to vendor documentation for valid values.
type: int
mac:
description:
- The system ID to use in LACP negotiations.
type: str
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- deleted
default: merged
"""
EXAMPLES = """
# Using merged
#
#
# ------------
# Before state
# ------------
#
#
# RP/0/0/CPU0:iosxr01#show running-config lacp
# Tue Jul 16 17:46:08.147 UTC
# % No such configuration item(s)
#
#
- name: Merge provided configuration with device configuration
iosxr_lacp:
config:
system:
priority: 10
mac: 00c1.4c00.bd15
state: merged
#
#
# -----------------------
# Module Execution Result
# -----------------------
#
# "before": {}
#
#
# "commands": [
# "lacp system priority 10",
# "lacp system mac 00c1.4c00.bd15"
# ]
#
#
# "after": {
# "system": {
# "mac": "00c1.4c00.bd15",
# "priority": 10
# }
# }
#
# -----------
# After state
# -----------
#
#
# RP/0/0/CPU0:iosxr01#sh run lacp
# Tue Jul 16 17:51:29.365 UTC
# lacp system mac 00c1.4c00.bd15
# lacp system priority 10
#
#
# Using replaced
#
#
# -------------
# Before state
# -------------
#
#
# RP/0/0/CPU0:iosxr01#sh run lacp
# Tue Jul 16 17:53:59.904 UTC
# lacp system mac 00c1.4c00.bd15
# lacp system priority 10
#
- name: Replace device global lacp configuration with the given configuration
iosxr_lacp:
config:
system:
priority: 11
state: replaced
#
#
# -----------------------
# Module Execution Result
# -----------------------
# "before": {
# "system": {
# "mac": "00c1.4c00.bd15",
# "priority": 10
# }
# }
#
#
# "commands": [
# "no lacp system mac",
# "lacp system priority 11"
# ]
#
#
# "after": {
# "system": {
# "priority": 11
# }
# }
#
# -----------
# After state
# -----------
#
#
# RP/0/0/CPU0:iosxr01#sh run lacp
# Tue Jul 16 18:02:40.379 UTC
# lacp system priority 11
#
#
# Using deleted
#
#
# ------------
# Before state
# ------------
#
#
# RP/0/0/CPU0:iosxr01#sh run lacp
# Tue Jul 16 18:37:09.727 UTC
# lacp system mac 00c1.4c00.bd15
# lacp system priority 11
#
#
- name: Delete global LACP configurations from the device
iosxr_lacp:
state: deleted
#
#
# -----------------------
# Module Execution Result
# -----------------------
# "before": {
# "system": {
# "mac": "00c1.4c00.bd15",
# "priority": 11
# }
# }
#
#
# "commands": [
# "no lacp system mac",
# "no lacp system priority"
# ]
#
#
# "after": {}
#
# ------------
# After state
# ------------
#
#
# RP/0/0/CPU0:iosxr01#sh run lacp
# Tue Jul 16 18:39:44.116 UTC
# % No such configuration item(s)
#
#
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: list
sample: >
The configuration returned will always be in the same format
of the parameters above.
after:
description: The resulting configuration model invocation.
returned: when changed
type: list
sample: >
The configuration returned will always be in the same format
of the parameters above.
commands:
description: The set of commands pushed to the remote device.
returned: always
type: list
sample: ['lacp system priority 10', 'lacp system mac 00c1.4c00.bd15']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.iosxr.argspec.lacp.lacp import LacpArgs
from ansible.module_utils.network.iosxr.config.lacp.lacp import Lacp
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=LacpArgs.argument_spec,
supports_check_mode=True)
result = Lacp(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,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,9 @@
---
- name: Setup
cli_config:
config: "{{ lines }}"
vars:
lines: |
lacp system priority 12
lacp system mac 00c1.4c00.bd16

View file

@ -0,0 +1,8 @@
---
- name: Remove Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
no lacp system priority
no lacp system mac

View file

@ -0,0 +1,45 @@
---
- debug:
msg: "Start iosxr_lacp deleted integration tests ansible_connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate.yaml
- block:
- name: Delete attributes of given interfaces
iosxr_lacp: &deleted
state: deleted
register: result
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ populate == result['before'] }}"
- name: Assert that the correct set of commands were generated
assert:
that:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that the after dicts were correctly generated
assert:
that:
- "{{ deleted['after'] == result['after'] }}"
- name: Delete attributes of given interfaces (IDEMPOTENT)
iosxr_lacp: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ deleted['after'] == result['before'] }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,45 @@
---
- debug:
msg: "START iosxr_lacp merged integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge the provided configuration with the exisiting running configuration
iosxr_lacp: &merged
config:
system:
priority: 11
mac: 00c1.4c00.bd15
state: merged
register: result
- name: Assert that before dicts were correctly generated
assert:
that: "{{ merged['before'] == result['before'] }}"
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that after dicts was correctly generated
assert:
that:
- "{{ merged['after'] == result['after'] }}"
- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
iosxr_lacp: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ merged['after'] == result['before']}}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,48 @@
---
- debug:
msg: "START iosxr_lacp replaced integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate.yaml
- block:
- name: Replace device configurations of listed interfaces with provided configurations
iosxr_lacp: &replaced
config:
system:
priority: 11
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:
- "{{ populate == result['before'] }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ replaced['after'] == result['after'] }}"
- name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT)
iosxr_lacp: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ replaced['after'] == result['before'] }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,49 @@
---
- debug:
msg: "START isoxr_lacp round trip integration tests on connection={{ ansible_connection }}"
- block:
- include_tasks: _remove_config.yaml
- name: Apply the provided configuration (base config)
iosxr_lacp:
config:
system:
priority: 15
mac: 00c1.4c00.bd16
state: merged
register: base_config
- name: Gather interfaces facts
iosxr_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources:
- lacp
- name: Apply the provided configuration (config to be reverted)
iosxr_lacp:
config:
system:
priority: 10
mac: 00c1.4c00.bd10
state: merged
register: result
- name: Assert that changes were applied
assert:
that: "{{ round_trip['after'] == result['after'] }}"
- name: Revert back to base config using facts round trip
iosxr_lacp:
config: "{{ ansible_facts['network_resources']['lacp'] }}"
state: replaced
register: revert
- name: Assert that config was reverted
assert:
that: "{{ base_config['after'] == revert['after'] }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,40 @@
---
merged:
before: {}
commands:
- "lacp system priority 11"
- "lacp system mac 00c1.4c00.bd15"
after:
system:
priority: 11
mac: "00c1.4c00.bd15"
populate:
system:
priority: 12
mac: "00c1.4c00.bd16"
replaced:
commands:
- "no lacp system mac"
- "lacp system priority 11"
after:
system:
priority: 11
deleted:
commands:
- "no lacp system priority"
- "no lacp system mac"
after: {}
round_trip:
after:
system:
priority: 10
mac: "00c1.4c00.bd10"

View file

@ -35,10 +35,13 @@ class TestIosxrFacts(TestIosxrModule):
super(TestIosxrFacts, self).setUp() super(TestIosxrFacts, self).setUp()
self.mock_run_commands = patch( self.mock_run_commands = patch(
'ansible.modules.network.iosxr.iosxr_facts.run_commands') 'ansible.module_utils.network.iosxr.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.iosxr.iosxr_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.iosxr.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': {
@ -55,6 +58,7 @@ class TestIosxrFacts(TestIosxrModule):
self.mock_run_commands.stop() self.mock_run_commands.stop()
self.mock_get_capabilities.stop() self.mock_get_capabilities.stop()
self.mock_get_resource_connection.stop()
def load_fixtures(self, commands=None): def load_fixtures(self, commands=None):