1#!/usr/bin/python 2# Copyright: Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import absolute_import, division, print_function 6__metaclass__ = type 7 8ANSIBLE_METADATA = {'metadata_version': '1.1', 9 'status': ['preview'], 10 'supported_by': 'community'} 11 12DOCUMENTATION = """ 13--- 14module: icx_facts 15version_added: "2.9" 16author: "Ruckus Wireless (@Commscope)" 17short_description: Collect facts from remote Ruckus ICX 7000 series switches 18description: 19 - Collects a base set of device facts from a remote device that 20 is running ICX. This module prepends all of the 21 base network fact keys with C(ansible_net_<fact>). The facts 22 module will always collect a base set of facts from the device 23 and can enable or disable collection of additional facts. 24notes: 25 - Tested against ICX 10.1. 26 - For information on using ICX platform, see L(the ICX OS Platform Options guide,../network/user_guide/platform_icx.html). 27options: 28 gather_subset: 29 description: 30 - When supplied, this argument will restrict the facts collected 31 to a given subset. Possible values for this argument include 32 all, hardware, config, and interfaces. Can specify a list of 33 values to include a larger subset. Values can also be used 34 with an initial C(M(!)) to specify that a specific subset should 35 not be collected. 36 required: false 37 type: list 38 default: '!config' 39""" 40 41EXAMPLES = """ 42# Collect all facts from the device 43- icx_facts: 44 gather_subset: all 45 46# Collect only the config and default facts 47- icx_facts: 48 gather_subset: 49 - config 50 51# Do not collect hardware facts 52- icx_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_model: 65 description: The model name returned from the device 66 returned: always 67 type: str 68ansible_net_serialnum: 69 description: The serial number of the remote device 70 returned: always 71 type: str 72ansible_net_version: 73 description: The operating system version running on the remote device 74 returned: always 75 type: str 76ansible_net_hostname: 77 description: The configured hostname of the device 78 returned: always 79 type: str 80ansible_net_image: 81 description: The image file the device is running 82 returned: always 83 type: str 84ansible_net_stacked_models: 85 description: The model names of each device in the stack 86 returned: when multiple devices are configured in a stack 87 type: list 88ansible_net_stacked_serialnums: 89 description: The serial numbers of each device in the stack 90 returned: when multiple devices are configured in a stack 91 type: list 92 93# hardware 94ansible_net_filesystems: 95 description: All file system names available on the device 96 returned: when hardware is configured 97 type: list 98ansible_net_filesystems_info: 99 description: A hash of all file systems containing info about each file system (e.g. free and total space) 100 returned: when hardware is configured 101 type: dict 102ansible_net_memfree_mb: 103 description: The available free memory on the remote device in Mb 104 returned: when hardware is configured 105 type: int 106ansible_net_memtotal_mb: 107 description: The total memory on the remote device in Mb 108 returned: when hardware is configured 109 type: int 110 111# config 112ansible_net_config: 113 description: The current active config from the device 114 returned: when config is configured 115 type: str 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""" 135 136 137import re 138from ansible.module_utils.network.icx.icx import run_commands 139from ansible.module_utils.basic import AnsibleModule 140from ansible.module_utils.six import iteritems 141from ansible.module_utils.six.moves import zip 142 143 144class FactsBase(object): 145 146 COMMANDS = list() 147 148 def __init__(self, module): 149 self.module = module 150 self.facts = dict() 151 self.responses = None 152 153 def populate(self): 154 self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False) 155 156 def run(self, cmd): 157 return run_commands(self.module, commands=cmd, check_rc=False) 158 159 160class Default(FactsBase): 161 162 COMMANDS = ['show version', 'show running-config | include hostname'] 163 164 def populate(self): 165 super(Default, self).run(['skip']) 166 super(Default, self).populate() 167 data = self.responses[0] 168 if data: 169 self.facts['version'] = self.parse_version(data) 170 self.facts['serialnum'] = self.parse_serialnum(data) 171 self.facts['model'] = self.parse_model(data) 172 self.facts['image'] = self.parse_image(data) 173 self.facts['hostname'] = self.parse_hostname(self.responses[1]) 174 self.parse_stacks(data) 175 176 def parse_version(self, data): 177 match = re.search(r'SW: Version ([0-9]+.[0-9]+.[0-9a-zA-Z]+)', data) 178 if match: 179 return match.group(1) 180 181 def parse_hostname(self, data): 182 match = re.search(r'^hostname (\S+)', data, re.M) 183 if match: 184 return match.group(1) 185 186 def parse_model(self, data): 187 match = re.search(r'HW: (\S+ \S+)', data, re.M) 188 if match: 189 return match.group(1) 190 191 def parse_image(self, data): 192 match = re.search(r'\([0-9]+ bytes\) from \S+ (\S+)', data) 193 if match: 194 return match.group(1) 195 196 def parse_serialnum(self, data): 197 match = re.search(r'Serial #:(\S+)', data) 198 if match: 199 return match.group(1) 200 201 def parse_stacks(self, data): 202 match = re.findall(r'UNIT [1-9]+: SL [1-9]+: (\S+)', data, re.M) 203 if match: 204 self.facts['stacked_models'] = match 205 206 match = re.findall(r'^System [Ss]erial [Nn]umber\s+: (\S+)', data, re.M) 207 if match: 208 self.facts['stacked_serialnums'] = match 209 210 211class Hardware(FactsBase): 212 213 COMMANDS = [ 214 'show memory', 215 'show flash' 216 ] 217 218 def populate(self): 219 super(Hardware, self).populate() 220 data = self.responses[0] 221 if data: 222 self.facts['filesystems'] = self.parse_filesystems(data) 223 self.facts['filesystems_info'] = self.parse_filesystems_info(self.responses[1]) 224 225 if data: 226 if 'Invalid input detected' in data: 227 warnings.append('Unable to gather memory statistics') 228 else: 229 match = re.search(r'Dynamic memory: ([0-9]+) bytes total, ([0-9]+) bytes free, ([0-9]+%) used', data) 230 if match: 231 self.facts['memtotal_mb'] = int(match.group(1)) / 1024 232 self.facts['memfree_mb'] = int(match.group(2)) / 1024 233 234 def parse_filesystems(self, data): 235 return "flash" 236 237 def parse_filesystems_info(self, data): 238 facts = dict() 239 fs = '' 240 for line in data.split('\n'): 241 match = re.match(r'^(Stack unit \S+):', line) 242 if match: 243 fs = match.group(1) 244 facts[fs] = dict() 245 continue 246 match = re.match(r'\W+NAND Type: Micron NAND (\S+)', line) 247 if match: 248 facts[fs]['spacetotal'] = match.group(1) 249 match = re.match(r'\W+Code Flash Free Space = (\S+)', line) 250 if match: 251 facts[fs]['spacefree'] = int(int(match.group(1)) / 1024) 252 facts[fs]['spacefree'] = str(facts[fs]['spacefree']) + "Kb" 253 return {"flash": facts} 254 255 256class Config(FactsBase): 257 258 COMMANDS = ['skip', 'show running-config'] 259 260 def populate(self): 261 super(Config, self).populate() 262 data = self.responses[1] 263 if data: 264 self.facts['config'] = data 265 266 267class Interfaces(FactsBase): 268 269 COMMANDS = [ 270 'skip', 271 'show interfaces', 272 'show running-config', 273 'show lldp', 274 'show media' 275 ] 276 277 def populate(self): 278 super(Interfaces, self).populate() 279 280 self.facts['all_ipv4_addresses'] = list() 281 self.facts['all_ipv6_addresses'] = list() 282 data = self.responses[1] 283 if data: 284 interfaces = self.parse_interfaces(data) 285 self.facts['interfaces'] = self.populate_interfaces(interfaces) 286 287 data = self.responses[1] 288 if data: 289 data = self.parse_interfaces(data) 290 self.populate_ipv4_interfaces(data) 291 292 data = self.responses[2] 293 if data: 294 self.populate_ipv6_interfaces(data) 295 296 data = self.responses[3] 297 lldp_errs = ['Invalid input', 'LLDP is not enabled'] 298 299 if data and not any(err in data for err in lldp_errs): 300 neighbors = self.run(['show lldp neighbors detail']) 301 if neighbors: 302 self.facts['neighbors'] = self.parse_neighbors(neighbors[0]) 303 304 data = self.responses[4] 305 self.populate_mediatype(data) 306 307 interfaceList = {} 308 for iface in self.facts['interfaces']: 309 if 'type' in self.facts['interfaces'][iface]: 310 newName = self.facts['interfaces'][iface]['type'] + iface 311 else: 312 newName = iface 313 interfaceList[newName] = self.facts['interfaces'][iface] 314 self.facts['interfaces'] = interfaceList 315 316 def populate_mediatype(self, data): 317 lines = data.split("\n") 318 for line in lines: 319 match = re.match(r'Port (\S+):\W+Type\W+:\W+(.*)', line) 320 if match: 321 self.facts['interfaces'][match.group(1)]["mediatype"] = match.group(2) 322 323 def populate_interfaces(self, interfaces): 324 facts = dict() 325 for key, value in iteritems(interfaces): 326 intf = dict() 327 intf['description'] = self.parse_description(value) 328 intf['macaddress'] = self.parse_macaddress(value) 329 intf['mtu'] = self.parse_mtu(value) 330 intf['bandwidth'] = self.parse_bandwidth(value) 331 intf['duplex'] = self.parse_duplex(value) 332 intf['lineprotocol'] = self.parse_lineprotocol(value) 333 intf['operstatus'] = self.parse_operstatus(value) 334 intf['type'] = self.parse_type(value) 335 facts[key] = intf 336 return facts 337 338 def populate_ipv4_interfaces(self, data): 339 for key, value in data.items(): 340 self.facts['interfaces'][key]['ipv4'] = dict() 341 primary_address = addresses = [] 342 primary_address = re.findall(r'Internet address is (\S+/\S+), .*$', value, re.M) 343 addresses = re.findall(r'Secondary address (.+)$', value, re.M) 344 if len(primary_address) == 0: 345 continue 346 addresses.append(primary_address[0]) 347 for address in addresses: 348 addr, subnet = address.split("/") 349 ipv4 = dict(address=addr.strip(), subnet=subnet.strip()) 350 self.add_ip_address(addr.strip(), 'ipv4') 351 self.facts['interfaces'][key]['ipv4'] = ipv4 352 353 def populate_ipv6_interfaces(self, data): 354 parts = data.split("\n") 355 for line in parts: 356 match = re.match(r'\W*interface \S+ (\S+)', line) 357 if match: 358 key = match.group(1) 359 try: 360 self.facts['interfaces'][key]['ipv6'] = list() 361 except KeyError: 362 self.facts['interfaces'][key] = dict() 363 self.facts['interfaces'][key]['ipv6'] = list() 364 self.facts['interfaces'][key]['ipv6'] = {} 365 continue 366 match = re.match(r'\W+ipv6 address (\S+)/(\S+)', line) 367 if match: 368 self.add_ip_address(match.group(1), "ipv6") 369 self.facts['interfaces'][key]['ipv6']["address"] = match.group(1) 370 self.facts['interfaces'][key]['ipv6']["subnet"] = match.group(2) 371 372 def add_ip_address(self, address, family): 373 if family == 'ipv4': 374 self.facts['all_ipv4_addresses'].append(address) 375 else: 376 self.facts['all_ipv6_addresses'].append(address) 377 378 def parse_neighbors(self, neighbors): 379 facts = dict() 380 for entry in neighbors.split('------------------------------------------------'): 381 if entry == '': 382 continue 383 intf = self.parse_lldp_intf(entry) 384 if intf not in facts: 385 facts[intf] = list() 386 fact = dict() 387 fact['host'] = self.parse_lldp_host(entry) 388 fact['port'] = self.parse_lldp_port(entry) 389 facts[intf].append(fact) 390 return facts 391 392 def parse_interfaces(self, data): 393 parsed = dict() 394 key = '' 395 for line in data.split('\n'): 396 if len(line) == 0: 397 continue 398 elif line[0] == ' ': 399 parsed[key] += '\n%s' % line 400 else: 401 match = re.match(r'\S+Ethernet(\S+)', line) 402 if match: 403 key = match.group(1) 404 parsed[key] = line 405 return parsed 406 407 def parse_description(self, data): 408 match = re.search(r'Port name is ([ \S]+)', data, re.M) 409 if match: 410 return match.group(1) 411 412 def parse_macaddress(self, data): 413 match = re.search(r'Hardware is \S+, address is (\S+)', data) 414 if match: 415 return match.group(1) 416 417 def parse_ipv4(self, data): 418 match = re.search(r'Internet address is (\S+)', data) 419 if match: 420 addr, masklen = match.group(1).split('/') 421 return dict(address=addr, masklen=int(masklen)) 422 423 def parse_mtu(self, data): 424 match = re.search(r'MTU (\d+)', data) 425 if match: 426 return int(match.group(1)) 427 428 def parse_bandwidth(self, data): 429 match = re.search(r'Configured speed (\S+), actual (\S+)', data) 430 if match: 431 return match.group(1) 432 433 def parse_duplex(self, data): 434 match = re.search(r'configured duplex (\S+), actual (\S+)', data, re.M) 435 if match: 436 return match.group(2) 437 438 def parse_mediatype(self, data): 439 match = re.search(r'media type is (.+)$', data, re.M) 440 if match: 441 return match.group(1) 442 443 def parse_type(self, data): 444 match = re.search(r'Hardware is (.+),', data, re.M) 445 if match: 446 return match.group(1) 447 448 def parse_lineprotocol(self, data): 449 match = re.search(r'line protocol is (.+)$', data, re.M) 450 if match: 451 return match.group(1) 452 453 def parse_operstatus(self, data): 454 match = re.search(r'^(?:.+) is (.+),', data, re.M) 455 if match: 456 return match.group(1) 457 458 def parse_lldp_intf(self, data): 459 match = re.search(r'^Local Intf: (.+)$', data, re.M) 460 if match: 461 return match.group(1) 462 463 def parse_lldp_host(self, data): 464 match = re.search(r'System Name: (.+)$', data, re.M) 465 if match: 466 return match.group(1) 467 468 def parse_lldp_port(self, data): 469 match = re.search(r'Port id: (.+)$', data, re.M) 470 if match: 471 return match.group(1) 472 473 474FACT_SUBSETS = dict( 475 default=Default, 476 hardware=Hardware, 477 interfaces=Interfaces, 478 config=Config, 479) 480 481VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 482 483warnings = list() 484 485 486def main(): 487 """main entry point for module execution 488 """ 489 argument_spec = dict( 490 gather_subset=dict(default=['!config'], type='list') 491 ) 492 493 module = AnsibleModule(argument_spec=argument_spec, 494 supports_check_mode=True) 495 496 gather_subset = module.params['gather_subset'] 497 498 runable_subsets = set() 499 exclude_subsets = set() 500 501 for subset in gather_subset: 502 if subset == 'all': 503 runable_subsets.update(VALID_SUBSETS) 504 continue 505 506 if subset.startswith('!'): 507 subset = subset[1:] 508 if subset == 'all': 509 exclude_subsets.update(VALID_SUBSETS) 510 continue 511 exclude = True 512 else: 513 exclude = False 514 515 if subset not in VALID_SUBSETS: 516 module.fail_json(msg='Bad subset') 517 518 if exclude: 519 exclude_subsets.add(subset) 520 else: 521 runable_subsets.add(subset) 522 523 if not runable_subsets: 524 runable_subsets.update(VALID_SUBSETS) 525 526 runable_subsets.difference_update(exclude_subsets) 527 runable_subsets.add('default') 528 529 facts = dict() 530 facts['gather_subset'] = list(runable_subsets) 531 532 instances = list() 533 for key in runable_subsets: 534 instances.append(FACT_SUBSETS[key](module)) 535 536 for inst in instances: 537 inst.populate() 538 facts.update(inst.facts) 539 540 ansible_facts = dict() 541 for key, value in iteritems(facts): 542 key = 'ansible_net_%s' % key 543 ansible_facts[key] = value 544 545 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 546 547 548if __name__ == '__main__': 549 main() 550