1#!/usr/bin/python 2# 3# (c) 2015 Peter Sprygada, <psprygada@ansible.com> 4# Copyright (c) 2017 Dell Inc. 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'community'} 14 15 16DOCUMENTATION = """ 17--- 18module: dellos10_facts 19version_added: "2.2" 20author: "Senthil Kumar Ganesan (@skg-net)" 21short_description: Collect facts from remote devices running Dell EMC Networking OS10 22description: 23 - Collects a base set of device facts from a remote device that 24 is running OS10. This module prepends all of the 25 base network fact keys with C(ansible_net_<fact>). The facts 26 module will always collect a base set of facts from the device 27 and can enable or disable collection of additional facts. 28extends_documentation_fragment: dellos10 29options: 30 gather_subset: 31 description: 32 - When supplied, this argument will restrict the facts collected 33 to a given subset. Possible values for this argument include 34 all, hardware, config, and interfaces. Can specify a list of 35 values to include a larger subset. Values can also be used 36 with an initial C(M(!)) to specify that a specific subset should 37 not be collected. 38 default: [ '!config' ] 39""" 40 41EXAMPLES = """ 42# Collect all facts from the device 43- dellos10_facts: 44 gather_subset: all 45 46# Collect only the config and default facts 47- dellos10_facts: 48 gather_subset: 49 - config 50 51# Do not collect hardware facts 52- dellos10_facts: 53 gather_subset: 54 - "!hardware" 55""" 56 57RETURN = """ 58ansible_net_gather_subset: 59 description: The list of fact subsets collected from the device 60 returned: always 61 type: list 62 63# default 64ansible_net_name: 65 description: The name of the OS that is running. 66 returned: Always. 67 type: str 68ansible_net_version: 69 description: The operating system version running on the remote device 70 returned: always 71 type: str 72ansible_net_servicetag: 73 description: The service tag number of the remote device. 74 returned: always 75 type: str 76ansible_net_model: 77 description: The model name returned from the device. 78 returned: always 79 type: str 80ansible_net_hostname: 81 description: The configured hostname of the device 82 returned: always 83 type: str 84 85# hardware 86ansible_net_cpu_arch: 87 description: CPU Architecture of the remote device. 88 returned: when hardware is configured 89 type: str 90ansible_net_memfree_mb: 91 description: The available free memory on the remote device in Mb 92 returned: when hardware is configured 93 type: int 94ansible_net_memtotal_mb: 95 description: The total memory on the remote device in Mb 96 returned: when hardware is configured 97 type: int 98 99# config 100ansible_net_config: 101 description: The current active config from the device 102 returned: when config is configured 103 type: str 104 105# interfaces 106ansible_net_all_ipv4_addresses: 107 description: All IPv4 addresses configured on the device 108 returned: when interfaces is configured 109 type: list 110ansible_net_all_ipv6_addresses: 111 description: All IPv6 addresses configured on the device 112 returned: when interfaces is configured 113 type: list 114ansible_net_interfaces: 115 description: A hash of all interfaces running on the system 116 returned: when interfaces is configured 117 type: dict 118ansible_net_neighbors: 119 description: The list of LLDP neighbors from the remote device 120 returned: when interfaces is configured 121 type: dict 122""" 123 124import re 125 126try: 127 from lxml import etree as ET 128except ImportError: 129 import xml.etree.ElementTree as ET 130 131from ansible.module_utils.network.dellos10.dellos10 import run_commands 132from ansible.module_utils.network.dellos10.dellos10 import dellos10_argument_spec, check_args 133from ansible.module_utils.basic import AnsibleModule 134from ansible.module_utils.six import iteritems 135 136 137class FactsBase(object): 138 139 COMMANDS = [] 140 141 def __init__(self, module): 142 self.module = module 143 self.facts = dict() 144 self.responses = None 145 146 def populate(self): 147 self.responses = run_commands(self.module, self.COMMANDS, check_rc=False) 148 149 def run(self, cmd): 150 return run_commands(self.module, cmd, check_rc=False) 151 152 153class Default(FactsBase): 154 155 COMMANDS = [ 156 'show version | display-xml', 157 'show system | display-xml', 158 ] 159 160 def populate(self): 161 super(Default, self).populate() 162 data = self.responses[0] 163 xml_data = ET.fromstring(data.encode('utf8')) 164 165 self.facts['name'] = self.parse_name(xml_data) 166 self.facts['version'] = self.parse_version(xml_data) 167 self.facts['model'] = self.parse_model(xml_data) 168 self.facts['hostname'] = self.parse_hostname(xml_data) 169 170 data = self.responses[1] 171 xml_data = ET.fromstring(data.encode('utf8')) 172 173 self.facts['servicetag'] = self.parse_servicetag(xml_data) 174 175 def parse_name(self, data): 176 sw_name = data.find('./data/system-sw-state/sw-version/sw-name') 177 if sw_name is not None: 178 return sw_name.text 179 else: 180 return "" 181 182 def parse_version(self, data): 183 sw_ver = data.find('./data/system-sw-state/sw-version/sw-version') 184 if sw_ver is not None: 185 return sw_ver.text 186 else: 187 return "" 188 189 def parse_hostname(self, data): 190 hostname = data.find('./data/system-state/system-status/hostname') 191 if hostname is not None: 192 return hostname.text 193 else: 194 return "" 195 196 def parse_model(self, data): 197 prod_name = data.find('./data/system-sw-state/sw-version/sw-platform') 198 if prod_name is not None: 199 return prod_name.text 200 else: 201 return "" 202 203 def parse_servicetag(self, data): 204 svc_tag = data.find('./data/system/node/unit/mfg-info/service-tag') 205 if svc_tag is not None: 206 return svc_tag.text 207 else: 208 return "" 209 210 211class Hardware(FactsBase): 212 213 COMMANDS = [ 214 'show version | display-xml', 215 'show processes node-id 1 | grep Mem:' 216 ] 217 218 def populate(self): 219 220 super(Hardware, self).populate() 221 data = self.responses[0] 222 223 xml_data = ET.fromstring(data.encode('utf8')) 224 225 self.facts['cpu_arch'] = self.parse_cpu_arch(xml_data) 226 227 data = self.responses[1] 228 match = self.parse_memory(data) 229 if match: 230 self.facts['memtotal_mb'] = int(match[0]) // 1024 231 self.facts['memfree_mb'] = int(match[2]) // 1024 232 233 def parse_cpu_arch(self, data): 234 cpu_arch = data.find('./data/system-sw-state/sw-version/cpu-arch') 235 if cpu_arch is not None: 236 return cpu_arch.text 237 else: 238 return "" 239 240 def parse_memory(self, data): 241 return re.findall(r'(\d+)', data, re.M) 242 243 244class Config(FactsBase): 245 246 COMMANDS = ['show running-config'] 247 248 def populate(self): 249 super(Config, self).populate() 250 self.facts['config'] = self.responses[0] 251 252 253class Interfaces(FactsBase): 254 255 COMMANDS = [ 256 'show interface | display-xml', 257 'show lldp neighbors | display-xml' 258 ] 259 260 def __init__(self, module): 261 self.intf_facts = dict() 262 self.lldp_facts = dict() 263 super(Interfaces, self).__init__(module) 264 265 def populate(self): 266 super(Interfaces, self).populate() 267 self.facts['all_ipv4_addresses'] = list() 268 self.facts['all_ipv6_addresses'] = list() 269 270 int_show_data = (self.responses[0]).splitlines() 271 pattern = '?xml version' 272 data = '' 273 skip = True 274 275 # The output returns multiple xml trees 276 # parse them before handling. 277 for line in int_show_data: 278 if pattern in line: 279 if skip is False: 280 xml_data = ET.fromstring(data.encode('utf8')) 281 self.populate_interfaces(xml_data) 282 data = '' 283 else: 284 skip = False 285 286 data += line 287 288 if skip is False: 289 xml_data = ET.fromstring(data.encode('utf8')) 290 self.populate_interfaces(xml_data) 291 292 self.facts['interfaces'] = self.intf_facts 293 294 lldp_data = (self.responses[1]).splitlines() 295 data = '' 296 skip = True 297 # The output returns multiple xml trees 298 # parse them before handling. 299 for line in lldp_data: 300 if pattern in line: 301 if skip is False: 302 xml_data = ET.fromstring(data.encode('utf8')) 303 self.populate_neighbors(xml_data) 304 data = '' 305 else: 306 skip = False 307 308 data += line 309 310 if skip is False: 311 xml_data = ET.fromstring(data.encode('utf8')) 312 self.populate_neighbors(xml_data) 313 314 self.facts['neighbors'] = self.lldp_facts 315 316 def populate_interfaces(self, interfaces): 317 318 for interface in interfaces.findall('./data/interfaces/interface'): 319 intf = dict() 320 name = self.parse_item(interface, 'name') 321 322 intf['description'] = self.parse_item(interface, 'description') 323 intf['duplex'] = self.parse_item(interface, 'duplex') 324 intf['primary_ipv4'] = self.parse_primary_ipv4(interface) 325 intf['secondary_ipv4'] = self.parse_secondary_ipv4(interface) 326 intf['ipv6'] = self.parse_ipv6_address(interface) 327 intf['mtu'] = self.parse_item(interface, 'mtu') 328 intf['type'] = self.parse_item(interface, 'type') 329 330 self.intf_facts[name] = intf 331 332 for interface in interfaces.findall('./bulk/data/interface'): 333 name = self.parse_item(interface, 'name') 334 try: 335 intf = self.intf_facts[name] 336 intf['bandwidth'] = self.parse_item(interface, 'speed') 337 intf['adminstatus'] = self.parse_item(interface, 'admin-status') 338 intf['operstatus'] = self.parse_item(interface, 'oper-status') 339 intf['macaddress'] = self.parse_item(interface, 'phys-address') 340 except KeyError: 341 # skip the reserved interfaces 342 pass 343 344 for interface in interfaces.findall('./data/ports/ports-state/port'): 345 name = self.parse_item(interface, 'name') 346 # media-type name interface name format phy-eth 1/1/1 347 mediatype = self.parse_item(interface, 'media-type') 348 349 typ, sname = name.split('-eth') 350 name = "ethernet" + sname 351 try: 352 intf = self.intf_facts[name] 353 intf['mediatype'] = mediatype 354 except Exception: 355 # fanout 356 for subport in range(1, 5): 357 name = "ethernet" + sname + ":" + str(subport) 358 try: 359 intf = self.intf_facts[name] 360 intf['mediatype'] = mediatype 361 except Exception: 362 # valid case to handle 2x50G 363 pass 364 365 def add_ip_address(self, address, family): 366 if family == 'ipv4': 367 self.facts['all_ipv4_addresses'].append(address) 368 else: 369 self.facts['all_ipv6_addresses'].append(address) 370 371 def parse_item(self, interface, item): 372 elem = interface.find(item) 373 if elem is not None: 374 return elem.text 375 else: 376 return "" 377 378 def parse_primary_ipv4(self, interface): 379 ipv4 = interface.find('ipv4') 380 ip_address = "" 381 if ipv4 is not None: 382 prim_ipaddr = ipv4.find('./address/primary-addr') 383 if prim_ipaddr is not None: 384 ip_address = prim_ipaddr.text 385 self.add_ip_address(ip_address, 'ipv4') 386 387 return ip_address 388 389 def parse_secondary_ipv4(self, interface): 390 ipv4 = interface.find('ipv4') 391 ip_address = "" 392 if ipv4 is not None: 393 sec_ipaddr = ipv4.find('./address/secondary-addr') 394 if sec_ipaddr is not None: 395 ip_address = sec_ipaddr.text 396 self.add_ip_address(ip_address, 'ipv4') 397 398 return ip_address 399 400 def parse_ipv6_address(self, interface): 401 402 ip_address = list() 403 404 for addr in interface.findall('./ipv6/ipv6-addresses/address'): 405 406 ipv6_addr = addr.find('./ipv6-address') 407 408 if ipv6_addr is not None: 409 ip_address.append(ipv6_addr.text) 410 self.add_ip_address(ipv6_addr.text, 'ipv6') 411 412 return ip_address 413 414 def populate_neighbors(self, interfaces): 415 for interface in interfaces.findall('./bulk/data/interface'): 416 name = interface.find('name').text 417 rem_sys_name = interface.find('./lldp-rem-neighbor-info/info/rem-system-name') 418 if rem_sys_name is not None: 419 self.lldp_facts[name] = list() 420 fact = dict() 421 fact['host'] = rem_sys_name.text 422 rem_sys_port = interface.find('./lldp-rem-neighbor-info/info/rem-lldp-port-id') 423 fact['port'] = rem_sys_port.text 424 self.lldp_facts[name].append(fact) 425 426 427FACT_SUBSETS = dict( 428 default=Default, 429 hardware=Hardware, 430 interfaces=Interfaces, 431 config=Config, 432) 433 434VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 435 436 437def main(): 438 """main entry point for module execution 439 """ 440 argument_spec = dict( 441 gather_subset=dict(default=['!config'], type='list') 442 ) 443 444 argument_spec.update(dellos10_argument_spec) 445 446 module = AnsibleModule(argument_spec=argument_spec, 447 supports_check_mode=True) 448 449 gather_subset = module.params['gather_subset'] 450 451 runable_subsets = set() 452 exclude_subsets = set() 453 454 for subset in gather_subset: 455 if subset == 'all': 456 runable_subsets.update(VALID_SUBSETS) 457 continue 458 459 if subset.startswith('!'): 460 subset = subset[1:] 461 if subset == 'all': 462 exclude_subsets.update(VALID_SUBSETS) 463 continue 464 exclude = True 465 else: 466 exclude = False 467 468 if subset not in VALID_SUBSETS: 469 module.fail_json(msg='Bad subset') 470 471 if exclude: 472 exclude_subsets.add(subset) 473 else: 474 runable_subsets.add(subset) 475 476 if not runable_subsets: 477 runable_subsets.update(VALID_SUBSETS) 478 479 runable_subsets.difference_update(exclude_subsets) 480 runable_subsets.add('default') 481 482 facts = dict() 483 facts['gather_subset'] = list(runable_subsets) 484 485 instances = list() 486 for key in runable_subsets: 487 instances.append(FACT_SUBSETS[key](module)) 488 489 for inst in instances: 490 inst.populate() 491 facts.update(inst.facts) 492 493 ansible_facts = dict() 494 for key, value in iteritems(facts): 495 key = 'ansible_net_%s' % key 496 ansible_facts[key] = value 497 498 warnings = list() 499 check_args(module, warnings) 500 501 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 502 503 504if __name__ == '__main__': 505 main() 506