1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# (C) 2019 Red Hat Inc. 5# Copyright (C) 2019 Lenovo. 6# 7# GNU General Public License v3.0+ 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 15# Module to Collect facts from Lenovo Switches running Lenovo CNOS commands 16# Lenovo Networking 17# 18from __future__ import absolute_import, division, print_function 19__metaclass__ = type 20 21 22ANSIBLE_METADATA = {'metadata_version': '1.1', 23 'status': ['preview'], 24 'supported_by': 'community'} 25 26DOCUMENTATION = ''' 27--- 28module: cnos_facts 29version_added: "2.3" 30author: "Anil Kumar Muraleedharan (@amuraleedhar)" 31short_description: Collect facts from remote devices running Lenovo CNOS 32description: 33 - Collects a base set of device facts from a remote Lenovo device 34 running on CNOS. This module prepends all of the 35 base network fact keys with C(ansible_net_<fact>). The facts 36 module will always collect a base set of facts from the device 37 and can enable or disable collection of additional facts. 38notes: 39 - Tested against CNOS 10.8.1 40options: 41 authorize: 42 version_added: "2.6" 43 description: 44 - Instructs the module to enter privileged mode on the remote device 45 before sending any commands. If not specified, the device will 46 attempt to execute all commands in non-privileged mode. If the value 47 is not specified in the task, the value of environment variable 48 C(ANSIBLE_NET_AUTHORIZE) will be used instead. 49 type: bool 50 default: 'no' 51 auth_pass: 52 version_added: "2.6" 53 description: 54 - Specifies the password to use if required to enter privileged mode 55 on the remote device. If I(authorize) is false, then this argument 56 does nothing. If the value is not specified in the task, the value of 57 environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. 58 gather_subset: 59 version_added: "2.6" 60 description: 61 - When supplied, this argument will restrict the facts collected 62 to a given subset. Possible values for this argument include 63 all, hardware, config, and interfaces. Can specify a list of 64 values to include a larger subset. Values can also be used 65 with an initial C(M(!)) to specify that a specific subset should 66 not be collected. 67 required: false 68 default: '!config' 69''' 70EXAMPLES = ''' 71Tasks: The following are examples of using the module cnos_facts. 72--- 73- name: Test cnos Facts 74 cnos_facts: 75 76--- 77# Collect all facts from the device 78- cnos_facts: 79 gather_subset: all 80 81# Collect only the config and default facts 82- cnos_facts: 83 gather_subset: 84 - config 85 86# Do not collect hardware facts 87- cnos_facts: 88 gather_subset: 89 - "!hardware" 90''' 91RETURN = ''' 92 ansible_net_gather_subset: 93 description: The list of fact subsets collected from the device 94 returned: always 95 type: list 96# default 97 ansible_net_model: 98 description: The model name returned from the Lenovo CNOS device 99 returned: always 100 type: str 101 ansible_net_serialnum: 102 description: The serial number of the Lenovo CNOS device 103 returned: always 104 type: str 105 ansible_net_version: 106 description: The CNOS operating system version running on the remote device 107 returned: always 108 type: str 109 ansible_net_hostname: 110 description: The configured hostname of the device 111 returned: always 112 type: str 113 ansible_net_image: 114 description: Indicates the active image for the device 115 returned: always 116 type: str 117# hardware 118 ansible_net_memfree_mb: 119 description: The available free memory on the remote device in MB 120 returned: when hardware is configured 121 type: int 122# config 123 ansible_net_config: 124 description: The current active config from the device 125 returned: when config is configured 126 type: str 127# interfaces 128 ansible_net_all_ipv4_addresses: 129 description: All IPv4 addresses configured on the device 130 returned: when interfaces is configured 131 type: list 132 ansible_net_all_ipv6_addresses: 133 description: All IPv6 addresses configured on the device 134 returned: when interfaces is configured 135 type: list 136 ansible_net_interfaces: 137 description: A hash of all interfaces running on the system. 138 This gives information on description, mac address, mtu, speed, 139 duplex and operstatus 140 returned: when interfaces is configured 141 type: dict 142 ansible_net_neighbors: 143 description: The list of LLDP neighbors from the remote device 144 returned: when interfaces is configured 145 type: dict 146''' 147 148import re 149 150from ansible.module_utils.network.cnos.cnos import run_commands 151from ansible.module_utils.network.cnos.cnos import check_args 152from ansible.module_utils._text import to_text 153from ansible.module_utils.basic import AnsibleModule 154from ansible.module_utils.six import iteritems 155from ansible.module_utils.six.moves import zip 156 157 158class FactsBase(object): 159 160 COMMANDS = list() 161 162 def __init__(self, module): 163 self.module = module 164 self.facts = dict() 165 self.responses = None 166 self.PERSISTENT_COMMAND_TIMEOUT = 60 167 168 def populate(self): 169 self.responses = run_commands(self.module, self.COMMANDS, 170 check_rc=False) 171 172 def run(self, cmd): 173 return run_commands(self.module, cmd, check_rc=False) 174 175 176class Default(FactsBase): 177 178 COMMANDS = ['show sys-info', 'show running-config'] 179 180 def populate(self): 181 super(Default, self).populate() 182 data = self.responses[0] 183 data_run = self.responses[1] 184 if data: 185 self.facts['version'] = self.parse_version(data) 186 self.facts['serialnum'] = self.parse_serialnum(data) 187 self.facts['model'] = self.parse_model(data) 188 self.facts['image'] = self.parse_image(data) 189 if data_run: 190 self.facts['hostname'] = self.parse_hostname(data_run) 191 192 def parse_version(self, data): 193 for line in data.split('\n'): 194 line = line.strip() 195 match = re.match(r'System Software Revision (.*?)', 196 line, re.M | re.I) 197 if match: 198 vers = line.split(':') 199 ver = vers[1].strip() 200 return ver 201 return "NA" 202 203 def parse_hostname(self, data_run): 204 for line in data_run.split('\n'): 205 line = line.strip() 206 match = re.match(r'hostname (.*?)', line, re.M | re.I) 207 if match: 208 hosts = line.split() 209 hostname = hosts[1].strip('\"') 210 return hostname 211 return "NA" 212 213 def parse_model(self, data): 214 for line in data.split('\n'): 215 line = line.strip() 216 match = re.match(r'System Model (.*?)', line, re.M | re.I) 217 if match: 218 mdls = line.split(':') 219 mdl = mdls[1].strip() 220 return mdl 221 return "NA" 222 223 def parse_image(self, data): 224 match = re.search(r'(.*) image(.*)', data, re.M | re.I) 225 if match: 226 return "Image1" 227 else: 228 return "Image2" 229 230 def parse_serialnum(self, data): 231 for line in data.split('\n'): 232 line = line.strip() 233 match = re.match(r'System Serial Number (.*?)', line, re.M | re.I) 234 if match: 235 serNums = line.split(':') 236 ser = serNums[1].strip() 237 return ser 238 return "NA" 239 240 241class Hardware(FactsBase): 242 243 COMMANDS = [ 244 'show running-config' 245 ] 246 247 def populate(self): 248 super(Hardware, self).populate() 249 data = self.run(['show process memory']) 250 data = to_text(data, errors='surrogate_or_strict').strip() 251 data = data.replace(r"\n", "\n") 252 if data: 253 for line in data.split('\n'): 254 line = line.strip() 255 match = re.match(r'Mem: (.*?)', line, re.M | re.I) 256 if match: 257 memline = line.split(':') 258 mems = memline[1].strip().split() 259 self.facts['memtotal_mb'] = int(mems[0]) / 1024 260 self.facts['memused_mb'] = int(mems[1]) / 1024 261 self.facts['memfree_mb'] = int(mems[2]) / 1024 262 self.facts['memshared_mb'] = int(mems[3]) / 1024 263 self.facts['memavailable_mb'] = int(mems[5]) / 1024 264 265 def parse_memtotal(self, data): 266 match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I) 267 if match: 268 return int(match.group(1)) / 1024 269 270 def parse_memfree(self, data): 271 match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I) 272 if match: 273 return int(match.group(1)) / 1024 274 275 276class Config(FactsBase): 277 278 COMMANDS = ['show running-config'] 279 280 def populate(self): 281 super(Config, self).populate() 282 data = self.responses[0] 283 if data: 284 self.facts['config'] = data 285 286 287class Interfaces(FactsBase): 288 289 COMMANDS = ['show interface brief'] 290 291 def populate(self): 292 super(Interfaces, self).populate() 293 294 self.facts['all_ipv4_addresses'] = list() 295 self.facts['all_ipv6_addresses'] = list() 296 297 data1 = self.run(['show interface status']) 298 data1 = to_text(data1, errors='surrogate_or_strict').strip() 299 data1 = data1.replace(r"\n", "\n") 300 data2 = self.run(['show interface mac-address']) 301 data2 = to_text(data2, errors='surrogate_or_strict').strip() 302 data2 = data2.replace(r"\n", "\n") 303 lines1 = None 304 lines2 = None 305 if data1: 306 lines1 = self.parse_interfaces(data1) 307 if data2: 308 lines2 = self.parse_interfaces(data2) 309 if lines1 is not None and lines2 is not None: 310 self.facts['interfaces'] = self.populate_interfaces(lines1, lines2) 311 data3 = self.run(['show lldp neighbors']) 312 data3 = to_text(data3, errors='surrogate_or_strict').strip() 313 data3 = data3.replace(r"\n", "\n") 314 if data3: 315 lines3 = self.parse_neighbors(data3) 316 if lines3 is not None: 317 self.facts['neighbors'] = self.populate_neighbors(lines3) 318 319 data4 = self.run(['show ip interface brief vrf all']) 320 data5 = self.run(['show ipv6 interface brief vrf all']) 321 data4 = to_text(data4, errors='surrogate_or_strict').strip() 322 data4 = data4.replace(r"\n", "\n") 323 data5 = to_text(data5, errors='surrogate_or_strict').strip() 324 data5 = data5.replace(r"\n", "\n") 325 lines4 = None 326 lines5 = None 327 if data4: 328 lines4 = self.parse_ipaddresses(data4) 329 ipv4_interfaces = self.set_ip_interfaces(lines4) 330 self.facts['all_ipv4_addresses'] = ipv4_interfaces 331 if data5: 332 lines5 = self.parse_ipaddresses(data5) 333 ipv6_interfaces = self.set_ipv6_interfaces(lines5) 334 self.facts['all_ipv6_addresses'] = ipv6_interfaces 335 336 def parse_ipaddresses(self, data): 337 parsed = list() 338 for line in data.split('\n'): 339 if len(line) == 0: 340 continue 341 else: 342 line = line.strip() 343 match = re.match(r'^(Ethernet+)', line) 344 if match: 345 key = match.group(1) 346 parsed.append(line) 347 match = re.match(r'^(po+)', line) 348 if match: 349 key = match.group(1) 350 parsed.append(line) 351 match = re.match(r'^(mgmt+)', line) 352 if match: 353 key = match.group(1) 354 parsed.append(line) 355 match = re.match(r'^(loopback+)', line) 356 if match: 357 key = match.group(1) 358 parsed.append(line) 359 return parsed 360 361 def populate_interfaces(self, lines1, lines2): 362 interfaces = dict() 363 for line1, line2 in zip(lines1, lines2): 364 line = line1 + " " + line2 365 intfSplit = line.split() 366 innerData = dict() 367 innerData['description'] = intfSplit[1].strip() 368 innerData['macaddress'] = intfSplit[8].strip() 369 innerData['type'] = intfSplit[6].strip() 370 innerData['speed'] = intfSplit[5].strip() 371 innerData['duplex'] = intfSplit[4].strip() 372 innerData['operstatus'] = intfSplit[2].strip() 373 interfaces[intfSplit[0].strip()] = innerData 374 return interfaces 375 376 def parse_interfaces(self, data): 377 parsed = list() 378 for line in data.split('\n'): 379 if len(line) == 0: 380 continue 381 else: 382 line = line.strip() 383 match = re.match(r'^(Ethernet+)', line) 384 if match: 385 key = match.group(1) 386 parsed.append(line) 387 match = re.match(r'^(po+)', line) 388 if match: 389 key = match.group(1) 390 parsed.append(line) 391 match = re.match(r'^(mgmt+)', line) 392 if match: 393 key = match.group(1) 394 parsed.append(line) 395 return parsed 396 397 def set_ip_interfaces(self, line4): 398 ipv4_addresses = list() 399 for line in line4: 400 ipv4Split = line.split() 401 if 'Ethernet' in ipv4Split[0]: 402 ipv4_addresses.append(ipv4Split[1]) 403 if 'mgmt' in ipv4Split[0]: 404 ipv4_addresses.append(ipv4Split[1]) 405 if 'po' in ipv4Split[0]: 406 ipv4_addresses.append(ipv4Split[1]) 407 if 'loopback' in ipv4Split[0]: 408 ipv4_addresses.append(ipv4Split[1]) 409 return ipv4_addresses 410 411 def set_ipv6_interfaces(self, line4): 412 ipv6_addresses = list() 413 for line in line4: 414 ipv6Split = line.split() 415 if 'Ethernet' in ipv6Split[0]: 416 ipv6_addresses.append(ipv6Split[1]) 417 if 'mgmt' in ipv6Split[0]: 418 ipv6_addresses.append(ipv6Split[1]) 419 if 'po' in ipv6Split[0]: 420 ipv6_addresses.append(ipv6Split[1]) 421 if 'loopback' in ipv6Split[0]: 422 ipv6_addresses.append(ipv6Split[1]) 423 return ipv6_addresses 424 425 def populate_neighbors(self, lines3): 426 neighbors = dict() 427 device_name = '' 428 for line in lines3: 429 neighborSplit = line.split() 430 innerData = dict() 431 count = len(neighborSplit) 432 if count == 5: 433 local_interface = neighborSplit[1].strip() 434 innerData['Device Name'] = neighborSplit[0].strip() 435 innerData['Hold Time'] = neighborSplit[2].strip() 436 innerData['Capability'] = neighborSplit[3].strip() 437 innerData['Remote Port'] = neighborSplit[4].strip() 438 neighbors[local_interface] = innerData 439 elif count == 4: 440 local_interface = neighborSplit[0].strip() 441 innerData['Hold Time'] = neighborSplit[1].strip() 442 innerData['Capability'] = neighborSplit[2].strip() 443 innerData['Remote Port'] = neighborSplit[3].strip() 444 neighbors[local_interface] = innerData 445 return neighbors 446 447 def parse_neighbors(self, neighbors): 448 parsed = list() 449 for line in neighbors.split('\n'): 450 if len(line) == 0: 451 continue 452 else: 453 line = line.strip() 454 if 'Ethernet' in line: 455 parsed.append(line) 456 if 'mgmt' in line: 457 parsed.append(line) 458 if 'po' in line: 459 parsed.append(line) 460 if 'loopback' in line: 461 parsed.append(line) 462 return parsed 463 464 465FACT_SUBSETS = dict( 466 default=Default, 467 hardware=Hardware, 468 interfaces=Interfaces, 469 config=Config, 470) 471 472VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 473 474PERSISTENT_COMMAND_TIMEOUT = 60 475 476 477def main(): 478 """main entry point for module execution 479 """ 480 argument_spec = dict( 481 gather_subset=dict(default=['!config'], type='list') 482 ) 483 484 module = AnsibleModule(argument_spec=argument_spec, 485 supports_check_mode=True) 486 487 gather_subset = module.params['gather_subset'] 488 489 runable_subsets = set() 490 exclude_subsets = set() 491 492 for subset in gather_subset: 493 if subset == 'all': 494 runable_subsets.update(VALID_SUBSETS) 495 continue 496 497 if subset.startswith('!'): 498 subset = subset[1:] 499 if subset == 'all': 500 exclude_subsets.update(VALID_SUBSETS) 501 continue 502 exclude = True 503 else: 504 exclude = False 505 506 if subset not in VALID_SUBSETS: 507 module.fail_json(msg='Bad subset') 508 509 if exclude: 510 exclude_subsets.add(subset) 511 else: 512 runable_subsets.add(subset) 513 514 if not runable_subsets: 515 runable_subsets.update(VALID_SUBSETS) 516 517 runable_subsets.difference_update(exclude_subsets) 518 runable_subsets.add('default') 519 520 facts = dict() 521 facts['gather_subset'] = list(runable_subsets) 522 523 instances = list() 524 for key in runable_subsets: 525 instances.append(FACT_SUBSETS[key](module)) 526 527 for inst in instances: 528 inst.populate() 529 facts.update(inst.facts) 530 531 ansible_facts = dict() 532 for key, value in iteritems(facts): 533 key = 'ansible_net_%s' % key 534 ansible_facts[key] = value 535 536 warnings = list() 537 check_args(module, warnings) 538 539 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 540 541 542if __name__ == '__main__': 543 main() 544