1#!/usr/bin/python 2# 3# (c) 2015 Peter Sprygada, <psprygada@ansible.com> 4# Copyright (c) 2016 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: dellos9_facts 19version_added: "2.2" 20author: "Dhivya P (@dhivyap)" 21short_description: Collect facts from remote devices running Dell EMC Networking OS9 22description: 23 - Collects a base set of device facts from a remote device that 24 is running OS9. 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: dellos9 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' ] 39notes: 40 - This module requires OS9 version 9.10.0.1P13 or above. 41 42 - This module requires an increase of the SSH connection rate limit. 43 Use the following command I(ip ssh connection-rate-limit 60) 44 to configure the same. This can be also be done with the M(dellos9_config) module. 45""" 46 47EXAMPLES = """ 48# Collect all facts from the device 49- dellos9_facts: 50 gather_subset: all 51 52# Collect only the config and default facts 53- dellos9_facts: 54 gather_subset: 55 - config 56 57# Do not collect hardware facts 58- dellos9_facts: 59 gather_subset: 60 - "!hardware" 61""" 62 63RETURN = """ 64ansible_net_gather_subset: 65 description: The list of fact subsets collected from the device 66 returned: always 67 type: list 68 69# default 70ansible_net_model: 71 description: The model name returned from the device 72 returned: always 73 type: str 74ansible_net_serialnum: 75 description: The serial number of the remote device 76 returned: always 77 type: str 78ansible_net_version: 79 description: The operating system version running on the remote device 80 returned: always 81 type: str 82ansible_net_hostname: 83 description: The configured hostname of the device 84 returned: always 85 type: str 86ansible_net_image: 87 description: The image file the device is running 88 returned: always 89 type: str 90 91# hardware 92ansible_net_filesystems: 93 description: All file system names available on the device 94 returned: when hardware is configured 95 type: list 96ansible_net_memfree_mb: 97 description: The available free memory on the remote device in Mb 98 returned: when hardware is configured 99 type: int 100ansible_net_memtotal_mb: 101 description: The total memory on the remote device in Mb 102 returned: when hardware is configured 103 type: int 104 105# config 106ansible_net_config: 107 description: The current active config from the device 108 returned: when config is configured 109 type: str 110 111# interfaces 112ansible_net_all_ipv4_addresses: 113 description: All IPv4 addresses configured on the device 114 returned: when interfaces is configured 115 type: list 116ansible_net_all_ipv6_addresses: 117 description: All IPv6 addresses configured on the device 118 returned: when interfaces is configured 119 type: list 120ansible_net_interfaces: 121 description: A hash of all interfaces running on the system 122 returned: when interfaces is configured 123 type: dict 124ansible_net_neighbors: 125 description: The list of LLDP neighbors from the remote device 126 returned: when interfaces is configured 127 type: dict 128""" 129import re 130try: 131 from itertools import izip 132except ImportError: 133 izip = zip 134 135from ansible.module_utils.basic import AnsibleModule 136from ansible.module_utils.network.dellos9.dellos9 import run_commands 137from ansible.module_utils.network.dellos9.dellos9 import dellos9_argument_spec, check_args 138from ansible.module_utils.six import iteritems 139 140 141class FactsBase(object): 142 143 COMMANDS = list() 144 145 def __init__(self, module): 146 self.module = module 147 self.facts = dict() 148 self.responses = None 149 150 def populate(self): 151 self.responses = run_commands(self.module, self.COMMANDS, check_rc=False) 152 153 def run(self, cmd): 154 return run_commands(self.module, cmd, check_rc=False) 155 156 157class Default(FactsBase): 158 159 COMMANDS = [ 160 'show version', 161 'show inventory', 162 'show running-config | grep hostname' 163 ] 164 165 def populate(self): 166 super(Default, self).populate() 167 data = self.responses[0] 168 self.facts['version'] = self.parse_version(data) 169 self.facts['model'] = self.parse_model(data) 170 self.facts['image'] = self.parse_image(data) 171 172 data = self.responses[1] 173 self.facts['serialnum'] = self.parse_serialnum(data) 174 175 data = self.responses[2] 176 self.facts['hostname'] = self.parse_hostname(data) 177 178 def parse_version(self, data): 179 match = re.search(r'Software Version:\s*(.+)', data) 180 if match: 181 return match.group(1) 182 183 def parse_hostname(self, data): 184 match = re.search(r'^hostname (.+)', data, re.M) 185 if match: 186 return match.group(1) 187 188 def parse_model(self, data): 189 match = re.search(r'^System Type:\s*(.+)', data, re.M) 190 if match: 191 return match.group(1) 192 193 def parse_image(self, data): 194 match = re.search(r'image file is "(.+)"', data) 195 if match: 196 return match.group(1) 197 198 def parse_serialnum(self, data): 199 for line in data.split('\n'): 200 if line.startswith('*'): 201 match = re.search( 202 r'\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', line, re.M) 203 if match: 204 return match.group(3) 205 206 207class Hardware(FactsBase): 208 209 COMMANDS = [ 210 'show file-systems', 211 'show memory | except Processor' 212 ] 213 214 def populate(self): 215 super(Hardware, self).populate() 216 data = self.responses[0] 217 self.facts['filesystems'] = self.parse_filesystems(data) 218 219 data = self.responses[1] 220 match = re.findall(r'\s(\d+)\s', data) 221 if match: 222 self.facts['memtotal_mb'] = int(match[0]) // 1024 223 self.facts['memfree_mb'] = int(match[2]) // 1024 224 225 def parse_filesystems(self, data): 226 return re.findall(r'\s(\S+):$', data, re.M) 227 228 229class Config(FactsBase): 230 231 COMMANDS = ['show running-config'] 232 233 def populate(self): 234 super(Config, self).populate() 235 self.facts['config'] = self.responses[0] 236 237 238class Interfaces(FactsBase): 239 240 COMMANDS = [ 241 'show interfaces', 242 'show ipv6 interface', 243 'show lldp neighbors detail', 244 'show inventory' 245 ] 246 247 def populate(self): 248 super(Interfaces, self).populate() 249 self.facts['all_ipv4_addresses'] = list() 250 self.facts['all_ipv6_addresses'] = list() 251 252 data = self.responses[0] 253 interfaces = self.parse_interfaces(data) 254 255 for key in list(interfaces.keys()): 256 if "ManagementEthernet" in key: 257 temp_parsed = interfaces[key] 258 del interfaces[key] 259 interfaces.update(self.parse_mgmt_interfaces(temp_parsed)) 260 261 for key in list(interfaces.keys()): 262 if "Vlan" in key: 263 temp_parsed = interfaces[key] 264 del interfaces[key] 265 interfaces.update(self.parse_vlan_interfaces(temp_parsed)) 266 267 self.facts['interfaces'] = self.populate_interfaces(interfaces) 268 269 data = self.responses[1] 270 if len(data) > 0: 271 data = self.parse_ipv6_interfaces(data) 272 self.populate_ipv6_interfaces(data) 273 274 data = self.responses[3] 275 if 'LLDP' in self.get_protocol_list(data): 276 neighbors = self.responses[2] 277 self.facts['neighbors'] = self.parse_neighbors(neighbors) 278 279 def get_protocol_list(self, data): 280 start = False 281 protocol_list = list() 282 for line in data.split('\n'): 283 match = re.search(r'Software Protocol Configured\s*', line) 284 if match: 285 start = True 286 continue 287 if start: 288 line = line.strip() 289 if line.isalnum(): 290 protocol_list.append(line) 291 return protocol_list 292 293 def populate_interfaces(self, interfaces): 294 facts = dict() 295 for key, value in interfaces.items(): 296 intf = dict() 297 intf['description'] = self.parse_description(value) 298 intf['macaddress'] = self.parse_macaddress(value) 299 ipv4 = self.parse_ipv4(value) 300 intf['ipv4'] = self.parse_ipv4(value) 301 if ipv4: 302 self.add_ip_address(ipv4['address'], 'ipv4') 303 304 intf['mtu'] = self.parse_mtu(value) 305 intf['bandwidth'] = self.parse_bandwidth(value) 306 intf['mediatype'] = self.parse_mediatype(value) 307 intf['duplex'] = self.parse_duplex(value) 308 intf['lineprotocol'] = self.parse_lineprotocol(value) 309 intf['operstatus'] = self.parse_operstatus(value) 310 intf['type'] = self.parse_type(value) 311 312 facts[key] = intf 313 return facts 314 315 def populate_ipv6_interfaces(self, data): 316 for key, value in data.items(): 317 if key in self.facts['interfaces']: 318 self.facts['interfaces'][key]['ipv6'] = list() 319 addresses = re.findall(r'\s+(.+), subnet', value, re.M) 320 subnets = re.findall(r', subnet is (\S+)', value, re.M) 321 for addr, subnet in izip(addresses, subnets): 322 ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) 323 self.add_ip_address(addr.strip(), 'ipv6') 324 self.facts['interfaces'][key]['ipv6'].append(ipv6) 325 326 def add_ip_address(self, address, family): 327 if family == 'ipv4': 328 self.facts['all_ipv4_addresses'].append(address) 329 else: 330 self.facts['all_ipv6_addresses'].append(address) 331 332 def parse_neighbors(self, neighbors): 333 facts = dict() 334 335 for entry in neighbors.split( 336 '========================================================================'): 337 if entry == '': 338 continue 339 340 intf = self.parse_lldp_intf(entry) 341 if intf not in facts: 342 facts[intf] = list() 343 fact = dict() 344 fact['host'] = self.parse_lldp_host(entry) 345 fact['port'] = self.parse_lldp_port(entry) 346 facts[intf].append(fact) 347 return facts 348 349 def parse_interfaces(self, data): 350 parsed = dict() 351 newline_count = 0 352 interface_start = True 353 354 for line in data.split('\n'): 355 if interface_start: 356 newline_count = 0 357 if len(line) == 0: 358 newline_count += 1 359 if newline_count == 2: 360 interface_start = True 361 continue 362 else: 363 match = re.match(r'^(\S+) (\S+)', line) 364 if match and interface_start: 365 interface_start = False 366 key = match.group(0) 367 parsed[key] = line 368 else: 369 parsed[key] += '\n%s' % line 370 return parsed 371 372 def parse_mgmt_interfaces(self, data): 373 parsed = dict() 374 interface_start = True 375 for line in data.split('\n'): 376 match = re.match(r'^(\S+) (\S+)', line) 377 if "Time since" in line: 378 interface_start = True 379 parsed[key] += '\n%s' % line 380 continue 381 elif match and interface_start: 382 interface_start = False 383 key = match.group(0) 384 parsed[key] = line 385 else: 386 parsed[key] += '\n%s' % line 387 return parsed 388 389 def parse_vlan_interfaces(self, data): 390 parsed = dict() 391 interface_start = True 392 line_before_end = False 393 for line in data.split('\n'): 394 match = re.match(r'^(\S+) (\S+)', line) 395 match_endline = re.match(r'^\s*\d+ packets, \d+ bytes$', line) 396 397 if "Output Statistics" in line: 398 line_before_end = True 399 parsed[key] += '\n%s' % line 400 elif match_endline and line_before_end: 401 line_before_end = False 402 interface_start = True 403 parsed[key] += '\n%s' % line 404 elif match and interface_start: 405 interface_start = False 406 key = match.group(0) 407 parsed[key] = line 408 else: 409 parsed[key] += '\n%s' % line 410 return parsed 411 412 def parse_ipv6_interfaces(self, data): 413 parsed = dict() 414 for line in data.split('\n'): 415 if len(line) == 0: 416 continue 417 elif line[0] == ' ': 418 parsed[key] += '\n%s' % line 419 else: 420 match = re.match(r'^(\S+) (\S+)', line) 421 if match: 422 key = match.group(0) 423 parsed[key] = line 424 return parsed 425 426 def parse_description(self, data): 427 match = re.search(r'Description: (.+)$', data, re.M) 428 if match: 429 return match.group(1) 430 431 def parse_macaddress(self, data): 432 match = re.search(r'address is (\S+)', data) 433 if match: 434 if match.group(1) != "not": 435 return match.group(1) 436 437 def parse_ipv4(self, data): 438 match = re.search(r'Internet address is (\S+)', data) 439 if match: 440 if match.group(1) != "not": 441 addr, masklen = match.group(1).split('/') 442 return dict(address=addr, masklen=int(masklen)) 443 444 def parse_mtu(self, data): 445 match = re.search(r'MTU (\d+)', data) 446 if match: 447 return int(match.group(1)) 448 449 def parse_bandwidth(self, data): 450 match = re.search(r'LineSpeed (\d+)', data) 451 if match: 452 return int(match.group(1)) 453 454 def parse_duplex(self, data): 455 match = re.search(r'(\w+) duplex', data, re.M) 456 if match: 457 return match.group(1) 458 459 def parse_mediatype(self, data): 460 media = re.search(r'(.+) media present, (.+)', data, re.M) 461 if media: 462 match = re.search(r'type is (.+)$', media.group(0), re.M) 463 return match.group(1) 464 465 def parse_type(self, data): 466 match = re.search(r'Hardware is (.+),', data, re.M) 467 if match: 468 return match.group(1) 469 470 def parse_lineprotocol(self, data): 471 match = re.search(r'line protocol is (\w+[ ]?\w*)\(?.*\)?$', data, re.M) 472 if match: 473 return match.group(1) 474 475 def parse_operstatus(self, data): 476 match = re.search(r'^(?:.+) is (.+),', data, re.M) 477 if match: 478 return match.group(1) 479 480 def parse_lldp_intf(self, data): 481 match = re.search(r'^\sLocal Interface (\S+\s\S+)', data, re.M) 482 if match: 483 return match.group(1) 484 485 def parse_lldp_host(self, data): 486 match = re.search(r'Remote System Name: (.+)$', data, re.M) 487 if match: 488 return match.group(1) 489 490 def parse_lldp_port(self, data): 491 match = re.search(r'Remote Port ID: (.+)$', data, re.M) 492 if match: 493 return match.group(1) 494 495 496FACT_SUBSETS = dict( 497 default=Default, 498 hardware=Hardware, 499 interfaces=Interfaces, 500 config=Config, 501) 502 503VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 504 505 506def main(): 507 """main entry point for module execution 508 """ 509 argument_spec = dict( 510 gather_subset=dict(default=['!config'], type='list') 511 ) 512 513 argument_spec.update(dellos9_argument_spec) 514 515 module = AnsibleModule(argument_spec=argument_spec, 516 supports_check_mode=True) 517 518 gather_subset = module.params['gather_subset'] 519 520 runable_subsets = set() 521 exclude_subsets = set() 522 523 for subset in gather_subset: 524 if subset == 'all': 525 runable_subsets.update(VALID_SUBSETS) 526 continue 527 528 if subset.startswith('!'): 529 subset = subset[1:] 530 if subset == 'all': 531 exclude_subsets.update(VALID_SUBSETS) 532 continue 533 exclude = True 534 else: 535 exclude = False 536 537 if subset not in VALID_SUBSETS: 538 module.fail_json(msg='Bad subset') 539 540 if exclude: 541 exclude_subsets.add(subset) 542 else: 543 runable_subsets.add(subset) 544 545 if not runable_subsets: 546 runable_subsets.update(VALID_SUBSETS) 547 548 runable_subsets.difference_update(exclude_subsets) 549 runable_subsets.add('default') 550 551 facts = dict() 552 facts['gather_subset'] = list(runable_subsets) 553 554 instances = list() 555 for key in runable_subsets: 556 instances.append(FACT_SUBSETS[key](module)) 557 558 for inst in instances: 559 inst.populate() 560 facts.update(inst.facts) 561 562 ansible_facts = dict() 563 for key, value in iteritems(facts): 564 key = 'ansible_net_%s' % key 565 ansible_facts[key] = value 566 567 warnings = list() 568 check_args(module, warnings) 569 570 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 571 572 573if __name__ == '__main__': 574 main() 575