1#!/usr/bin/python 2# 3# Copyright: Ansible Project 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14 15DOCUMENTATION = """ 16--- 17module: ironware_facts 18version_added: "2.5" 19author: "Paul Baker (@paulquack)" 20short_description: Collect facts from devices running Extreme Ironware 21description: 22 - Collects a base set of device facts from a remote device that 23 is running Ironware. This module prepends all of the 24 base network fact keys with C(ansible_net_<fact>). The facts 25 module will always collect a base set of facts from the device 26 and can enable or disable collection of additional facts. 27extends_documentation_fragment: ironware 28notes: 29 - Tested against Ironware 5.8e 30options: 31 gather_subset: 32 description: 33 - When supplied, this argument will restrict the facts collected 34 to a given subset. Possible values for this argument include 35 all, hardware, config, mpls and interfaces. Can specify a list of 36 values to include a larger subset. Values can also be used 37 with an initial C(M(!)) to specify that a specific subset should 38 not be collected. 39 required: false 40 default: ['!config','!mpls'] 41""" 42 43EXAMPLES = """ 44# Collect all facts from the device 45- ironware_facts: 46 gather_subset: all 47 48# Collect only the config and default facts 49- ironware_facts: 50 gather_subset: 51 - config 52 53# Do not collect hardware facts 54- ironware_facts: 55 gather_subset: 56 - "!hardware" 57""" 58 59RETURN = """ 60ansible_net_gather_subset: 61 description: The list of fact subsets collected from the device 62 returned: always 63 type: list 64 65# default 66ansible_net_model: 67 description: The model name returned from the device 68 returned: always 69 type: str 70ansible_net_serialnum: 71 description: The serial number of the remote device 72 returned: always 73 type: str 74ansible_net_version: 75 description: The operating system version running on the remote device 76 returned: always 77 type: str 78 79# hardware 80ansible_net_filesystems: 81 description: All file system names available on the device 82 returned: when hardware is configured 83 type: list 84ansible_net_memfree_mb: 85 description: The available free memory on the remote device in Mb 86 returned: when hardware is configured 87 type: int 88ansible_net_memtotal_mb: 89 description: The total memory on the remote device in Mb 90 returned: when hardware is configured 91 type: int 92 93# config 94ansible_net_config: 95 description: The current active config from the device 96 returned: when config is configured 97 type: str 98 99# mpls 100ansible_net_mpls_lsps: 101 description: All MPLS LSPs configured on the device 102 returned: When LSP is configured 103 type: dict 104ansible_net_mpls_vll: 105 description: All VLL instances configured on the device 106 returned: When MPLS VLL is configured 107 type: dict 108ansible_net_mpls_vll_local: 109 description: All VLL-LOCAL instances configured on the device 110 returned: When MPLS VLL-LOCAL is configured 111 type: dict 112ansible_net_mpls_vpls: 113 description: All VPLS instances configured on the device 114 returned: When MPLS VPLS is configured 115 type: dict 116 117# interfaces 118ansible_net_all_ipv4_addresses: 119 description: All IPv4 addresses configured on the device 120 returned: when interfaces is configured 121 type: list 122ansible_net_all_ipv6_addresses: 123 description: All IPv6 addresses configured on the device 124 returned: when interfaces is configured 125 type: list 126ansible_net_interfaces: 127 description: A hash of all interfaces running on the system 128 returned: when interfaces is configured 129 type: dict 130ansible_net_neighbors: 131 description: The list of LLDP neighbors from the remote device 132 returned: when interfaces is configured 133 type: dict 134""" 135import re 136 137from ansible.module_utils.network.ironware.ironware import run_commands 138from ansible.module_utils.network.ironware.ironware import ironware_argument_spec, check_args 139from ansible.module_utils.basic import AnsibleModule 140from ansible.module_utils.six import iteritems 141 142 143class FactsBase(object): 144 145 COMMANDS = list() 146 147 def __init__(self, module): 148 self.module = module 149 self.facts = dict() 150 self.responses = None 151 152 def populate(self): 153 self.responses = run_commands(self.module, self.COMMANDS, check_rc=False) 154 155 def run(self, cmd): 156 return run_commands(self.module, cmd, check_rc=False) 157 158 159class Default(FactsBase): 160 161 COMMANDS = [ 162 'show version', 163 'show chassis' 164 ] 165 166 def populate(self): 167 super(Default, self).populate() 168 data = self.responses[0] 169 if data: 170 self.facts['version'] = self.parse_version(data) 171 self.facts['serialnum'] = self.parse_serialnum(data) 172 173 data = self.responses[1] 174 if data: 175 self.facts['model'] = self.parse_model(data) 176 177 def parse_version(self, data): 178 match = re.search(r'IronWare : Version (\S+)', data) 179 if match: 180 return match.group(1) 181 182 def parse_model(self, data): 183 match = re.search(r'^\*\*\* (.+) \*\*\*$', data, re.M) 184 if match: 185 return match.group(1) 186 187 def parse_serialnum(self, data): 188 match = re.search(r'Serial #: (\S+),', data) 189 if match: 190 return match.group(1) 191 192 193class Hardware(FactsBase): 194 195 COMMANDS = [ 196 'dir | include Directory', 197 'show memory' 198 ] 199 200 def populate(self): 201 super(Hardware, self).populate() 202 data = self.responses[0] 203 if data: 204 self.facts['filesystems'] = self.parse_filesystems(data) 205 206 data = self.responses[1] 207 if data: 208 self.facts['memtotal_mb'] = int(round(int(self.parse_memtotal(data)) / 1024 / 1024, 0)) 209 self.facts['memfree_mb'] = int(round(int(self.parse_memfree(data)) / 1024 / 1024, 0)) 210 211 def parse_filesystems(self, data): 212 return re.findall(r'^Directory of (\S+)', data, re.M) 213 214 def parse_memtotal(self, data): 215 match = re.search(r'Total SDRAM\D*(\d+)\s', data, re.M) 216 if match: 217 return match.group(1) 218 219 def parse_memfree(self, data): 220 match = re.search(r'(Total Free Memory|Available Memory)\D*(\d+)\s', data, re.M) 221 if match: 222 return match.group(2) 223 224 225class Config(FactsBase): 226 227 COMMANDS = ['show running-config'] 228 229 def populate(self): 230 super(Config, self).populate() 231 data = self.responses[0] 232 if data: 233 self.facts['config'] = data 234 235 236class MPLS(FactsBase): 237 238 COMMANDS = [ 239 'show mpls lsp detail', 240 'show mpls vll-local detail', 241 'show mpls vll detail', 242 'show mpls vpls detail' 243 ] 244 245 def populate(self): 246 super(MPLS, self).populate() 247 data = self.responses[0] 248 if data: 249 data = self.parse_mpls(data) 250 self.facts['mpls_lsps'] = self.populate_lsps(data) 251 252 data = self.responses[1] 253 if data: 254 data = self.parse_mpls(data) 255 self.facts['mpls_vll_local'] = self.populate_vll_local(data) 256 257 data = self.responses[2] 258 if data: 259 data = self.parse_mpls(data) 260 self.facts['mpls_vll'] = self.populate_vll(data) 261 262 data = self.responses[3] 263 if data: 264 data = self.parse_mpls(data) 265 self.facts['mpls_vpls'] = self.populate_vpls(data) 266 267 def parse_mpls(self, data): 268 parsed = dict() 269 for line in data.split('\n'): 270 if not line: 271 continue 272 elif line[0] == ' ': 273 parsed[key] += '\n%s' % line 274 else: 275 match = re.match(r'^(LSP|VLL|VPLS) ([^\s,]+)', line) 276 if match: 277 key = match.group(2) 278 parsed[key] = line 279 return parsed 280 281 def populate_vpls(self, vpls): 282 facts = dict() 283 for key, value in iteritems(vpls): 284 vpls = dict() 285 vpls['endpoints'] = self.parse_vpls_endpoints(value) 286 vpls['vc-id'] = self.parse_vpls_vcid(value) 287 facts[key] = vpls 288 return facts 289 290 def populate_vll_local(self, vll_locals): 291 facts = dict() 292 for key, value in iteritems(vll_locals): 293 vll = dict() 294 vll['endpoints'] = self.parse_vll_endpoints(value) 295 facts[key] = vll 296 return facts 297 298 def populate_vll(self, vlls): 299 facts = dict() 300 for key, value in iteritems(vlls): 301 vll = dict() 302 vll['endpoints'] = self.parse_vll_endpoints(value) 303 vll['vc-id'] = self.parse_vll_vcid(value) 304 vll['cos'] = self.parse_vll_cos(value) 305 facts[key] = vll 306 return facts 307 308 def parse_vll_vcid(self, data): 309 match = re.search(r'VC-ID (\d+),', data, re.M) 310 if match: 311 return match.group(1) 312 313 def parse_vll_cos(self, data): 314 match = re.search(r'COS +: +(\d+)', data, re.M) 315 if match: 316 return match.group(1) 317 318 def parse_vll_endpoints(self, data): 319 facts = list() 320 regex = r'End-point[0-9 ]*: +(?P<tagged>tagged|untagged) +(vlan +(?P<vlan>[0-9]+) +)?(inner- vlan +(?P<innervlan>[0-9]+) +)?(?P<port>e [0-9/]+|--)' 321 matches = re.finditer(regex, data, re.IGNORECASE | re.DOTALL) 322 for match in matches: 323 f = match.groupdict() 324 f['type'] = 'local' 325 facts.append(f) 326 327 regex = r'Vll-Peer +: +(?P<vllpeer>[0-9\.]+).*Tunnel LSP +: +(?P<lsp>\S+)' 328 matches = re.finditer(regex, data, re.IGNORECASE | re.DOTALL) 329 for match in matches: 330 f = match.groupdict() 331 f['type'] = 'remote' 332 facts.append(f) 333 334 return facts 335 336 def parse_vpls_vcid(self, data): 337 match = re.search(r'Id (\d+),', data, re.M) 338 if match: 339 return match.group(1) 340 341 def parse_vpls_endpoints(self, data): 342 facts = list() 343 regex = r'Vlan (?P<vlanid>[0-9]+)\s(?: +(?:L2.*)\s| +Tagged: (?P<tagged>.+)+\s| +Untagged: (?P<untagged>.+)\s)*' 344 matches = re.finditer(regex, data, re.IGNORECASE) 345 for match in matches: 346 f = match.groupdict() 347 f['type'] = 'local' 348 facts.append(f) 349 350 regex = r'Peer address: (?P<vllpeer>[0-9\.]+)' 351 matches = re.finditer(regex, data, re.IGNORECASE) 352 for match in matches: 353 f = match.groupdict() 354 f['type'] = 'remote' 355 facts.append(f) 356 357 return facts 358 359 def populate_lsps(self, lsps): 360 facts = dict() 361 for key, value in iteritems(lsps): 362 lsp = dict() 363 lsp['to'] = self.parse_lsp_to(value) 364 lsp['from'] = self.parse_lsp_from(value) 365 lsp['adminstatus'] = self.parse_lsp_adminstatus(value) 366 lsp['operstatus'] = self.parse_lsp_operstatus(value) 367 lsp['pri_path'] = self.parse_lsp_pripath(value) 368 lsp['sec_path'] = self.parse_lsp_secpath(value) 369 lsp['frr'] = self.parse_lsp_frr(value) 370 371 facts[key] = lsp 372 373 return facts 374 375 def parse_lsp_to(self, data): 376 match = re.search(r'^LSP .* to (\S+)', data, re.M) 377 if match: 378 return match.group(1) 379 380 def parse_lsp_from(self, data): 381 match = re.search(r'From: ([^\s,]+),', data, re.M) 382 if match: 383 return match.group(1) 384 385 def parse_lsp_adminstatus(self, data): 386 match = re.search(r'admin: (\w+),', data, re.M) 387 if match: 388 return match.group(1) 389 390 def parse_lsp_operstatus(self, data): 391 match = re.search(r'From: .* status: (\w+)', data, re.M) 392 if match: 393 return match.group(1) 394 395 def parse_lsp_pripath(self, data): 396 match = re.search(r'Pri\. path: ([^\s,]+), up: (\w+), active: (\w+)', data, re.M) 397 if match: 398 path = dict() 399 path['name'] = match.group(1) if match.group(1) != 'NONE' else None 400 path['up'] = True if match.group(2) == 'yes' else False 401 path['active'] = True if match.group(3) == 'yes' else False 402 return path 403 404 def parse_lsp_secpath(self, data): 405 match = re.search(r'Sec\. path: ([^\s,]+), active: (\w+).*\n.* status: (\w+)', data, re.M) 406 if match: 407 path = dict() 408 path['name'] = match.group(1) if match.group(1) != 'NONE' else None 409 path['up'] = True if match.group(3) == 'up' else False 410 path['active'] = True if match.group(2) == 'yes' else False 411 return path 412 413 def parse_lsp_frr(self, data): 414 match = re.search(r'Backup LSP: (\w+)', data, re.M) 415 if match: 416 path = dict() 417 path['up'] = True if match.group(1) == 'UP' else False 418 path['name'] = None 419 if path['up']: 420 match = re.search(r'bypass_lsp: (\S)', data, re.M) 421 path['name'] = match.group(1) if match else None 422 return path 423 424 425class Interfaces(FactsBase): 426 427 COMMANDS = [ 428 'show interfaces', 429 'show ipv6 interface', 430 'show lldp neighbors' 431 ] 432 433 def populate(self): 434 super(Interfaces, self).populate() 435 436 self.facts['all_ipv4_addresses'] = list() 437 self.facts['all_ipv6_addresses'] = list() 438 439 data = self.responses[0] 440 if data: 441 interfaces = self.parse_interfaces(data) 442 self.facts['interfaces'] = self.populate_interfaces(interfaces) 443 444 data = self.responses[1] 445 if data: 446 data = self.parse_interfaces(data) 447 self.populate_ipv6_interfaces(data) 448 449 data = self.responses[2] 450 if data and 'LLDP is not running' not in data: 451 self.facts['neighbors'] = self.parse_neighbors(data) 452 453 def populate_interfaces(self, interfaces): 454 facts = dict() 455 for key, value in iteritems(interfaces): 456 intf = dict() 457 intf['description'] = self.parse_description(value) 458 intf['macaddress'] = self.parse_macaddress(value) 459 460 ipv4 = self.parse_ipv4(value) 461 intf['ipv4'] = self.parse_ipv4(value) 462 if ipv4: 463 self.add_ip_address(ipv4['address'], 'ipv4') 464 465 intf['mtu'] = self.parse_mtu(value) 466 intf['bandwidth'] = self.parse_bandwidth(value) 467 intf['duplex'] = self.parse_duplex(value) 468 intf['lineprotocol'] = self.parse_lineprotocol(value) 469 intf['operstatus'] = self.parse_operstatus(value) 470 intf['type'] = self.parse_type(value) 471 472 facts[key] = intf 473 return facts 474 475 def populate_ipv6_interfaces(self, data): 476 for key, value in iteritems(data): 477 self.facts['interfaces'][key]['ipv6'] = list() 478 addresses = re.findall(r'\s([0-9a-f]+:+[0-9a-f:]+\/\d+)\s', value, re.M) 479 for addr in addresses: 480 address, masklen = addr.split('/') 481 ipv6 = dict(address=address, masklen=int(masklen)) 482 self.add_ip_address(ipv6['address'], 'ipv6') 483 self.facts['interfaces'][key]['ipv6'].append(ipv6) 484 485 def add_ip_address(self, address, family): 486 if family == 'ipv4': 487 self.facts['all_ipv4_addresses'].append(address) 488 else: 489 self.facts['all_ipv6_addresses'].append(address) 490 491 def parse_neighbors(self, neighbors): 492 facts = dict() 493 for line in neighbors.split('\n'): 494 if line == '': 495 continue 496 match = re.search(r'([\d\/]+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', line, re.M) 497 if match: 498 intf = match.group(1) 499 if intf not in facts: 500 facts[intf] = list() 501 fact = dict() 502 fact['host'] = match.group(5) 503 fact['port'] = match.group(3) 504 facts[intf].append(fact) 505 return facts 506 507 def parse_interfaces(self, data): 508 parsed = dict() 509 for line in data.split('\n'): 510 if not line: 511 continue 512 elif line[0] == ' ': 513 parsed[key] += '\n%s' % line 514 else: 515 match = re.match(r'^(\S+Ethernet|eth )(\S+)', line) 516 if match: 517 key = match.group(2) 518 parsed[key] = line 519 return parsed 520 521 def parse_description(self, data): 522 match = re.search(r'Port name is (.+)$', data, re.M) 523 if match: 524 return match.group(1) 525 526 def parse_macaddress(self, data): 527 match = re.search(r'address is (\S+)', data) 528 if match: 529 return match.group(1) 530 531 def parse_ipv4(self, data): 532 match = re.search(r'Internet address is ([^\s,]+)', data) 533 if match: 534 addr, masklen = match.group(1).split('/') 535 return dict(address=addr, masklen=int(masklen)) 536 537 def parse_mtu(self, data): 538 match = re.search(r'MTU (\d+)', data) 539 if match: 540 return int(match.group(1)) 541 542 def parse_bandwidth(self, data): 543 match = re.search(r'BW is (\d+)', data) 544 if match: 545 return int(match.group(1)) 546 547 def parse_duplex(self, data): 548 match = re.search(r'configured duplex \S+ actual (\S+)', data, re.M) 549 if match: 550 return match.group(1) 551 552 def parse_mediatype(self, data): 553 match = re.search(r'Type\s*:\s*(.+)$', data, re.M) 554 if match: 555 return match.group(1) 556 557 def parse_type(self, data): 558 match = re.search(r'Hardware is (.+),', data, re.M) 559 if match: 560 return match.group(1) 561 562 def parse_lineprotocol(self, data): 563 match = re.search(r'line protocol is (\S+)', data, re.M) 564 if match: 565 return match.group(1) 566 567 def parse_operstatus(self, data): 568 match = re.search(r'^(?:.+) is (.+),', data, re.M) 569 if match: 570 return match.group(1) 571 572 573FACT_SUBSETS = dict( 574 default=Default, 575 hardware=Hardware, 576 interfaces=Interfaces, 577 config=Config, 578 mpls=MPLS, 579) 580 581VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 582 583 584def main(): 585 """main entry point for module execution 586 """ 587 argument_spec = dict( 588 gather_subset=dict(default=["!config", "!mpls"], type='list') 589 ) 590 591 argument_spec.update(ironware_argument_spec) 592 593 module = AnsibleModule(argument_spec=argument_spec, 594 supports_check_mode=True) 595 596 gather_subset = module.params['gather_subset'] 597 598 runable_subsets = set() 599 exclude_subsets = set() 600 601 for subset in gather_subset: 602 if subset == 'all': 603 runable_subsets.update(VALID_SUBSETS) 604 continue 605 606 if subset.startswith('!'): 607 subset = subset[1:] 608 if subset == 'all': 609 exclude_subsets.update(VALID_SUBSETS) 610 continue 611 exclude = True 612 else: 613 exclude = False 614 615 if subset not in VALID_SUBSETS: 616 module.fail_json(msg='Bad subset') 617 618 if exclude: 619 exclude_subsets.add(subset) 620 else: 621 runable_subsets.add(subset) 622 623 if not runable_subsets: 624 runable_subsets.update(VALID_SUBSETS) 625 626 runable_subsets.difference_update(exclude_subsets) 627 runable_subsets.add('default') 628 629 facts = dict() 630 facts['gather_subset'] = list(runable_subsets) 631 632 instances = list() 633 for key in runable_subsets: 634 instances.append(FACT_SUBSETS[key](module)) 635 636 for inst in instances: 637 inst.populate() 638 facts.update(inst.facts) 639 640 ansible_facts = dict() 641 for key, value in iteritems(facts): 642 key = 'ansible_net_%s' % key 643 ansible_facts[key] = value 644 645 check_args(module) 646 647 module.exit_json(ansible_facts=ansible_facts) 648 649 650if __name__ == '__main__': 651 main() 652