diff options
author | Nilashish Chakraborty <nilashishchakraborty8@gmail.com> | 2019-02-18 19:35:40 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-18 19:35:40 +0530 |
commit | bc403dbcda1edb8ebeee275fa3344ab88bc54ab1 (patch) | |
tree | 5064e5552bc51c9f33a8bef8d1cb63c431ea3576 /lib/ansible | |
parent | 2b9341f13924274cd8434fbd2164858d4fd54e1d (diff) | |
download | ansible-bc403dbcda1edb8ebeee275fa3344ab88bc54ab1.tar.gz |
Added new module - frr_facts (#51804)
* Add new module frr_facts
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix return value
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix review comments
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix review comments
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Handle empty row for mpls ldp neighbors
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix review comments
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix CI
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Remove timestamp from cliconf pluging
* Updated examples
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
* Fix sanity tests
Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
Diffstat (limited to 'lib/ansible')
-rw-r--r-- | lib/ansible/module_utils/network/frr/__init__.py | 0 | ||||
-rw-r--r-- | lib/ansible/module_utils/network/frr/frr.py | 40 | ||||
-rw-r--r-- | lib/ansible/modules/network/frr/__init__.py | 0 | ||||
-rw-r--r-- | lib/ansible/modules/network/frr/frr_facts.py | 400 | ||||
-rw-r--r-- | lib/ansible/plugins/cliconf/frr.py | 10 |
5 files changed, 442 insertions, 8 deletions
diff --git a/lib/ansible/module_utils/network/frr/__init__.py b/lib/ansible/module_utils/network/frr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/frr/__init__.py diff --git a/lib/ansible/module_utils/network/frr/frr.py b/lib/ansible/module_utils/network/frr/frr.py new file mode 100644 index 0000000000..30b3b268d8 --- /dev/null +++ b/lib/ansible/module_utils/network/frr/frr.py @@ -0,0 +1,40 @@ +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import json + +from ansible.module_utils.connection import Connection, ConnectionError +from ansible.module_utils._text import to_text + + +def get_capabilities(module): + if hasattr(module, '_frr_capabilities'): + return module._frr_capabilities + try: + capabilities = Connection(module._socket_path).get_capabilities() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + module._frr_capabilities = json.loads(capabilities) + return module._frr_capabilities + + +def run_commands(module, commands, check_rc=True): + connection = get_connection(module) + try: + return connection.run_commands(commands=commands, check_rc=check_rc) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + + +def get_connection(module): + if hasattr(module, '_frr_connection'): + return module._frr_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get('network_api') + if network_api == 'cliconf': + module._frr_connection = Connection(module._socket_path) + else: + module.fail_json(msg='Invalid connection type %s' % network_api) + + return module._frr_connection diff --git a/lib/ansible/modules/network/frr/__init__.py b/lib/ansible/modules/network/frr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/modules/network/frr/__init__.py diff --git a/lib/ansible/modules/network/frr/frr_facts.py b/lib/ansible/modules/network/frr/frr_facts.py new file mode 100644 index 0000000000..be91ff5bcb --- /dev/null +++ b/lib/ansible/modules/network/frr/frr_facts.py @@ -0,0 +1,400 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: frr_facts +version_added: "2.8" +author: "Nilashish Chakraborty (@nilashishc)" +short_description: Collect facts from remote devices running Free Range Routing (FRR). +description: + - Collects a base set of device facts from a remote device that + is running FRR. 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. +notes: + - Tested against FRR 6.0. +options: + gather_subset: + description: + - When supplied, this argument restricts the facts collected + to a given subset. + - Possible values for this argument include + C(all), C(hardware), C(config), and C(interfaces). + - Specify a list of values to include a larger subset. + - Use a value with an initial C(!) to collect all facts except that subset. + required: false + default: '!config' +""" + +EXAMPLES = """ +- name: Collect all facts from the device + frr_facts: + gather_subset: all + +- name: Collect only the config and default facts + frr_facts: + gather_subset: + - config + +- name: Collect the config and hardware facts + frr_facts: + gather_subset: + - config + - hardware + +- name: Do not collect hardware facts + frr_facts: + gather_subset: + - "!hardware" +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str + +# hardware +ansible_net_mem_stats: + description: The memory statistics fetched from the device + returned: when hardware is configured + type: dict + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + +# interfaces +ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_mpls_ldp_neighbors: + description: The list of MPLS LDP neighbors from the remote device + returned: when interfaces is configured and LDP daemon is running on the device + type: dict +""" + +import re + +from ansible.module_utils.network.frr.frr import run_commands, get_capabilities +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems + + +class FactsBase(object): + + COMMANDS = list() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.responses = None + self._capabilities = get_capabilities(self.module) + + def populate(self): + self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False) + + def run(self, cmd): + return run_commands(commands=cmd, check_rc=False) + + def parse_facts(self, pattern, data): + value = None + match = re.search(pattern, data, re.M) + if match: + value = match.group(1) + return value + + +class Default(FactsBase): + + COMMANDS = ['show version'] + + def populate(self): + super(Default, self).populate() + self.facts.update(self.platform_facts()) + + def platform_facts(self): + platform_facts = {} + + resp = self._capabilities + device_info = resp['device_info'] + + platform_facts['system'] = device_info['network_os'] + + for item in ('version', 'hostname'): + val = device_info.get('network_os_%s' % item) + if val: + platform_facts[item] = val + + return platform_facts + + +class Hardware(FactsBase): + + COMMANDS = ['show memory'] + + def _parse_daemons(self, data): + match = re.search(r'Memory statistics for (\w+)', data, re.M) + if match: + return match.group(1) + + def gather_memory_facts(self, data): + mem_details = data.split('\n\n') + mem_stats = {} + mem_counters = { + 'total_heap_allocated': r'Total heap allocated:(?:\s*)(.*)', + 'holding_block_headers': r'Holding block headers:(?:\s*)(.*)', + 'used_small_blocks': r'Used small blocks:(?:\s*)(.*)', + 'used_ordinary_blocks': r'Used ordinary blocks:(?:\s*)(.*)', + 'free_small_blocks': r'Free small blocks:(?:\s*)(.*)', + 'free_ordinary_blocks': r'Free ordinary blocks:(?:\s*)(.*)', + 'ordinary_blocks': r'Ordinary blocks:(?:\s*)(.*)', + 'small_blocks': r'Small blocks:(?:\s*)(.*)', + 'holding_blocks': r'Holding blocks:(?:\s*)(.*)' + } + + for item in mem_details: + daemon = self._parse_daemons(item) + mem_stats[daemon] = {} + for fact, pattern in iteritems(mem_counters): + mem_stats[daemon][fact] = self.parse_facts(pattern, item) + + return mem_stats + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.facts['mem_stats'] = self.gather_memory_facts(data) + + +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:', '', data, flags=re.MULTILINE) + self.facts['config'] = data + + +class Interfaces(FactsBase): + + COMMANDS = ['show interface'] + + def populate(self): + ldp_supported = self._capabilities['supported_protocols']['ldp'] + + if ldp_supported: + self.COMMANDS.append('show mpls ldp discovery') + + super(Interfaces, self).populate() + data = self.responses[0] + + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + if data: + interfaces = self.parse_interfaces(data) + self.facts['interfaces'] = self.populate_interfaces(interfaces) + self.populate_ipv4_interfaces(interfaces) + self.populate_ipv6_interfaces(interfaces) + + if ldp_supported: + data = self.responses[1] + if data: + self.facts['mpls_ldp_neighbors'] = self.populate_mpls_ldp_neighbors(data) + + 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'^Interface (\S+)', line) + if match: + key = match.group(1) + parsed[key] = line + return parsed + + def populate_interfaces(self, interfaces): + facts = dict() + counters = { + 'description': r'Description: (.+)', + 'macaddress': r'HWaddr: (\S+)', + 'type': r'Type: (\S+)', + 'vrf': r'vrf: (\S+)', + 'mtu': r'mtu (\d+)', + 'bandwidth': r'bandwidth (\d+)', + 'lineprotocol': r'line protocol is (\S+)', + 'operstatus': r'^(?:.+) is (.+),' + } + + for key, value in iteritems(interfaces): + intf = dict() + for fact, pattern in iteritems(counters): + intf[fact] = self.parse_facts(pattern, 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'inet (\S+) broadcast (?:\S+)(?:\s{2,})', value, re.M) + addresses = re.findall(r'inet (\S+) broadcast (?:\S+)(?:\s+)secondary', 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 data.items(): + self.facts['interfaces'][key]['ipv6'] = list() + addresses = re.findall(r'inet6 (\S+)', value, re.M) + for address in addresses: + addr, subnet = address.split("/") + 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 populate_mpls_ldp_neighbors(self, data): + facts = {} + entries = data.splitlines() + for x in entries: + if x.startswith('AF'): + continue + x = x.split() + if len(x) > 0: + ldp = {} + ldp['neighbor'] = x[1] + ldp['source'] = x[3] + facts[ldp['source']] = [] + facts[ldp['source']].append(ldp) + + return facts + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + config=Config, + interfaces=Interfaces +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + gather_subset = module.params['gather_subset'] + + 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='Subset must be one of [%s], got %s' % (', '.join(VALID_SUBSETS), 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) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/cliconf/frr.py b/lib/ansible/plugins/cliconf/frr.py index be80b04aec..37ba5ba4e0 100644 --- a/lib/ansible/plugins/cliconf/frr.py +++ b/lib/ansible/plugins/cliconf/frr.py @@ -187,12 +187,11 @@ class Cliconf(CliconfBase): return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) - def run_commands(self, commands=None, check_rc=True, return_timestamps=False): + def run_commands(self, commands=None, check_rc=True): if commands is None: raise ValueError("'commands' value is required") responses = list() - timestamps = list() for cmd in to_list(commands): if not isinstance(cmd, Mapping): cmd = {'command': cmd} @@ -203,16 +202,11 @@ class Cliconf(CliconfBase): try: out = self.send_command(**cmd) - timestamp = get_timestamp() except AnsibleConnectionFailure as e: if check_rc: raise out = getattr(e, 'err', to_text(e)) responses.append(out) - timestamps.append(timestamp) - if return_timestamps: - return responses, timestamps - else: - return responses + return responses |