From a7573218722d1bc9993ed4a905f11890572c74a7 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Fri, 16 Sep 2016 11:06:06 -0700 Subject: [PATCH] Added support for facts module for Dell Networking OS10 device. (#4879) * Added support for dnos10_facts module * Added the missing quotes * Addressed @privateip comments --- .../modules/network/dnos10/dnos10_facts.py | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 lib/ansible/modules/network/dnos10/dnos10_facts.py diff --git a/lib/ansible/modules/network/dnos10/dnos10_facts.py b/lib/ansible/modules/network/dnos10/dnos10_facts.py new file mode 100644 index 00000000000..2ab0dc9d400 --- /dev/null +++ b/lib/ansible/modules/network/dnos10/dnos10_facts.py @@ -0,0 +1,447 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: dnos10_facts +version_added: "2.2" +author: "Senthil Kumar Ganesan (@skg_net)" +short_description: Collect facts from remote devices running Dell OS10 +description: + - Collects a base set of device facts from a remote device that + is running Dell OS10. This module prepends all of the + base network fact keys with C(ansible_net_). 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: dnos10 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument inlcude + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial M(!) to specify that a specific subset should + not be collected. + required: false + default: '!config' +""" + +EXAMPLES = """ +# Collect all facts from the device +- dnos10_facts: + gather_subset: all + +# Collect only the config and default facts +- dnos10_facts: + gather_subset: + - config + +# Do not collect hardware facts +- dnos10_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_name: + description: The name of the OS which is running + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_servicetag: + description: The service tag number of the remote device + returned: always + type: str +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str + +# hardware +ansible_net_cpu_arch: + description: Cpu Architecture of the remote device + returned: when hardware is configured + type: str +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int + +# 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_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +""" + +import re +import itertools + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dnos10 + +try: + from lxml import etree as ET +except ImportError: + import xml.etree.ElementTree as ET + +class FactsBase(object): + + def __init__(self, runner): + self.runner = runner + self.facts = dict() + + self.commands() + + +class Default(FactsBase): + + def commands(self): + self.runner.add_command('show version | display-xml') + self.runner.add_command('show system | display-xml') + self.runner.add_command('show running-configuration | grep hostname') + + def populate(self): + + data = self.runner.get_command('show version | display-xml') + xml_data = ET.fromstring(data) + + self.facts['name'] = self.parse_name(xml_data) + self.facts['version'] = self.parse_version(xml_data) + + data = self.runner.get_command('show system | display-xml') + xml_data = ET.fromstring(data) + + self.facts['servicetag'] = self.parse_serialnum(xml_data) + self.facts['model'] = self.parse_model(xml_data) + + data = self.runner.get_command('show running-configuration | grep hostname') + self.facts['hostname'] = self.parse_hostname(data) + + def parse_name(self, data): + sw_name = data.find('./data/system-sw-state/sw-version/sw-name') + if sw_name is not None: + return sw_name.text + else: + return "" + + def parse_version(self, data): + sw_ver = data.find('./data/system-sw-state/sw-version/sw-version') + if sw_ver is not None: + return sw_ver.text + else: + return "" + + def parse_hostname(self, data): + match = re.search(r'hostname\s+(\S+)', data, re.M) + if match: + return match.group(1) + + def parse_model(self, data): + prod_name = data.find('./data/system/node/mfg-info/product-name') + if prod_name is not None: + return prod_name.text + else: + return "" + + def parse_serialnum(self, data): + svc_tag = data.find('./data/system/node/unit/mfg-info/service-tag') + if svc_tag is not None: + return svc_tag.text + else: + return "" + + +class Hardware(FactsBase): + + def commands(self): + self.runner.add_command('show processes memory | grep Total') + + def populate(self): + + data = self.runner.get_command('show version | display-xml') + xml_data = ET.fromstring(data) + + self.facts['cpu_arch'] = self.parse_cpu_arch(xml_data) + + data = self.runner.get_command('show processes memory | grep Total') + + match = self.parse_memory(data) + if match: + self.facts['memtotal_mb'] = int(match[0]) / 1024 + self.facts['memfree_mb'] = int(match[2]) / 1024 + + def parse_cpu_arch(self, data): + cpu_arch = data.find('./data/system-sw-state/sw-version/cpu-arch') + if cpu_arch is not None: + return cpu_arch.text + else: + return "" + + def parse_memory(self, data): + return re.findall(r'\:\s*(\d+)', data, re.M) + + +class Config(FactsBase): + + def commands(self): + self.runner.add_command('show running-config') + + def populate(self): + config = self.runner.get_command('show running-config') + self.facts['config'] = config + + +class Interfaces(FactsBase): + + def commands(self): + self.runner.add_command('show interface | display-xml') + + def populate(self): + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data = self.runner.get_command('show interface | display-xml') + + xml_data = ET.fromstring(data) + + self.facts['interfaces'] = self.populate_interfaces(xml_data) + self.facts['neighbors'] = self.populate_neighbors(xml_data) + + def populate_interfaces(self, interfaces): + int_facts = dict() + + for interface in interfaces.findall('./data/interfaces/interface'): + intf = dict() + name = self.parse_item(interface, 'name') + + intf['description'] = self.parse_item(interface, 'description') + intf['duplex'] = self.parse_item(interface, 'duplex') + intf['primary_ipv4'] = self.parse_primary_ipv4(interface) + intf['secondary_ipv4'] = self.parse_secondary_ipv4(interface) + intf['ipv6'] = self.parse_ipv6_address(interface) + intf['mtu'] = self.parse_item(interface, 'mtu') + intf['type'] = self.parse_item(interface, 'type') + + int_facts[name] = intf + + for interface in interfaces.findall('./data/interfaces-state/interface'): + name = self.parse_item(interface, 'name') + intf = int_facts[name] + intf['bandwidth'] = self.parse_item(interface, 'speed') + intf['adminstatus'] = self.parse_item(interface, 'admin-status') + intf['operstatus'] = self.parse_item(interface, 'oper-status') + intf['macaddress'] = self.parse_item(interface, 'phys-address') + + for interface in interfaces.findall('./data/ports/ports-state/port'): + name = self.parse_item(interface, 'name') + fanout = self.parse_item(interface, 'fanout-state') + mediatype = self.parse_item(interface, 'media-type') + + typ, sname = name.split('-eth') + + if fanout == "BREAKOUT_1x1": + name = "ethernet" + sname + intf = int_facts[name] + intf['mediatype'] = mediatype + else: + #TODO: Loop for the exact subport + for subport in xrange(1, 5): + name = "ethernet" + sname + ":" + str(subport) + intf = int_facts[name] + intf['mediatype'] = mediatype + + return int_facts + + 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_item(self, interface, item): + elem = interface.find(item) + if elem is not None: + return elem.text + else: + return "" + + def parse_primary_ipv4(self, interface): + ipv4 = interface.find('ipv4') + ip_address = "" + if ipv4 is not None: + prim_ipaddr = ipv4.find('./address/primary-addr') + if prim_ipaddr is not None: + ip_address = prim_ipaddr.text + self.add_ip_address(ip_address, 'ipv4') + + return ip_address + + def parse_secondary_ipv4(self, interface): + ipv4 = interface.find('ipv4') + ip_address = "" + if ipv4 is not None: + sec_ipaddr = ipv4.find('./address/secondary-addr') + if sec_ipaddr is not None: + ip_address = sec_ipaddr.text + self.add_ip_address(ip_address, 'ipv4') + + return ip_address + + def parse_ipv6_address(self, interface): + ipv6 = interface.find('ipv6') + ip_address = "" + if ipv6 is not None: + ipv6_addr = ipv6.find('./address/ipv6-address') + if ipv6_addr is not None: + ip_address = ipv6_addr.text + self.add_ip_address(ip_address, 'ipv6') + + return ip_address + + def populate_neighbors(self, interfaces): + lldp_facts = dict() + for interface in interfaces.findall('./data/interfaces-state/interface'): + name = interface.find('name').text + rem_sys_name = interface.find('./lldp-rem-neighbor-info/info/rem-system-name') + if rem_sys_name is not None: + lldp_facts[name] = list() + fact = dict() + fact['host'] = rem_sys_name.text + rem_sys_port = interface.find('./lldp-rem-neighbor-info/info/rem-lldp-port-id') + fact['port'] = rem_sys_port.text + lldp_facts[name].append(fact) + + return lldp_facts + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + module = NetworkModule(argument_spec=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='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) + + runner = CommandRunner(module) + + instances = list() + for key in runable_subsets: + runs = FACT_SUBSETS[key](runner) + instances.append(runs) + + runner.run() + + try: + for inst in instances: + inst.populate() + facts.update(inst.facts) + except Exception: + module.exit_json(out=module.from_json(runner.items)) + + ansible_facts = dict() + for key, value in facts.iteritems(): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts) + + +if __name__ == '__main__': + main()