1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# (C) 2017 Red Hat Inc. 5# Copyright (C) 2017 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# 16# Module to Collect facts from Lenovo Switches running Lenovo ENOS commands 17# Lenovo Networking 18# 19from __future__ import absolute_import, division, print_function 20__metaclass__ = type 21 22 23ANSIBLE_METADATA = {'metadata_version': '1.1', 24 'status': ['preview'], 25 'supported_by': 'community'} 26 27DOCUMENTATION = ''' 28--- 29module: enos_facts 30version_added: "2.5" 31author: "Anil Kumar Muraleedharan (@amuraleedhar)" 32short_description: Collect facts from remote devices running Lenovo ENOS 33description: 34 - Collects a base set of device facts from a remote Lenovo device 35 running on ENOS. This module prepends all of the 36 base network fact keys with C(ansible_net_<fact>). The facts 37 module will always collect a base set of facts from the device 38 and can enable or disable collection of additional facts. 39extends_documentation_fragment: enos 40notes: 41 - Tested against ENOS 8.4.1 42options: 43 gather_subset: 44 description: 45 - When supplied, this argument will restrict the facts collected 46 to a given subset. Possible values for this argument include 47 all, hardware, config, and interfaces. Can specify a list of 48 values to include a larger subset. Values can also be used 49 with an initial C(M(!)) to specify that a specific subset should 50 not be collected. 51 required: false 52 default: '!config' 53''' 54EXAMPLES = ''' 55Tasks: The following are examples of using the module enos_facts. 56--- 57- name: Test Enos Facts 58 enos_facts: 59 provider={{ cli }} 60 61 vars: 62 cli: 63 host: "{{ inventory_hostname }}" 64 port: 22 65 username: admin 66 password: admin 67 transport: cli 68 timeout: 30 69 authorize: True 70 auth_pass: 71 72--- 73# Collect all facts from the device 74- enos_facts: 75 gather_subset: all 76 provider: "{{ cli }}" 77 78# Collect only the config and default facts 79- enos_facts: 80 gather_subset: 81 - config 82 provider: "{{ cli }}" 83 84# Do not collect hardware facts 85- enos_facts: 86 gather_subset: 87 - "!hardware" 88 provider: "{{ cli }}" 89 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 ENOS device 99 returned: always 100 type: str 101 ansible_net_serialnum: 102 description: The serial number of the Lenovo ENOS device 103 returned: always 104 type: str 105 ansible_net_version: 106 description: The ENOS 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.enos.enos import run_commands, enos_argument_spec, check_args 151from ansible.module_utils._text import to_text 152from ansible.module_utils.basic import AnsibleModule 153from ansible.module_utils.six import iteritems 154from ansible.module_utils.six.moves import zip 155 156 157class FactsBase(object): 158 159 COMMANDS = list() 160 161 def __init__(self, module): 162 self.module = module 163 self.facts = dict() 164 self.responses = None 165 self.PERSISTENT_COMMAND_TIMEOUT = 60 166 167 def populate(self): 168 self.responses = run_commands(self.module, self.COMMANDS, 169 check_rc=False) 170 171 def run(self, cmd): 172 return run_commands(self.module, cmd, check_rc=False) 173 174 175class Default(FactsBase): 176 177 COMMANDS = ['show version', 'show run'] 178 179 def populate(self): 180 super(Default, self).populate() 181 data = self.responses[0] 182 data_run = self.responses[1] 183 if data: 184 self.facts['version'] = self.parse_version(data) 185 self.facts['serialnum'] = self.parse_serialnum(data) 186 self.facts['model'] = self.parse_model(data) 187 self.facts['image'] = self.parse_image(data) 188 if data_run: 189 self.facts['hostname'] = self.parse_hostname(data_run) 190 191 def parse_version(self, data): 192 match = re.search(r'^Software Version (.*?) ', data, re.M | re.I) 193 if match: 194 return match.group(1) 195 196 def parse_hostname(self, data_run): 197 for line in data_run.split('\n'): 198 line = line.strip() 199 match = re.match(r'hostname (.*?)', line, re.M | re.I) 200 if match: 201 hosts = line.split() 202 hostname = hosts[1].strip('\"') 203 return hostname 204 return "NA" 205 206 def parse_model(self, data): 207 match = re.search(r'^Lenovo RackSwitch (\S+)', data, re.M | re.I) 208 if match: 209 return match.group(1) 210 211 def parse_image(self, data): 212 match = re.search(r'(.*) image1(.*)', data, re.M | re.I) 213 if match: 214 return "Image1" 215 else: 216 return "Image2" 217 218 def parse_serialnum(self, data): 219 match = re.search(r'^Switch Serial No: (\S+)', data, re.M | re.I) 220 if match: 221 return match.group(1) 222 223 224class Hardware(FactsBase): 225 226 COMMANDS = [ 227 'show system memory' 228 ] 229 230 def populate(self): 231 super(Hardware, self).populate() 232 data = self.run(['show system memory']) 233 data = to_text(data, errors='surrogate_or_strict').strip() 234 data = data.replace(r"\n", "\n") 235 if data: 236 self.facts['memtotal_mb'] = self.parse_memtotal(data) 237 self.facts['memfree_mb'] = self.parse_memfree(data) 238 239 def parse_memtotal(self, data): 240 match = re.search(r'^MemTotal:\s*(.*) kB', data, re.M | re.I) 241 if match: 242 return int(match.group(1)) / 1024 243 244 def parse_memfree(self, data): 245 match = re.search(r'^MemFree:\s*(.*) kB', data, re.M | re.I) 246 if match: 247 return int(match.group(1)) / 1024 248 249 250class Config(FactsBase): 251 252 COMMANDS = ['show running-config'] 253 254 def populate(self): 255 super(Config, self).populate() 256 data = self.responses[0] 257 if data: 258 self.facts['config'] = data 259 260 261class Interfaces(FactsBase): 262 263 COMMANDS = ['show interface status'] 264 265 def populate(self): 266 super(Interfaces, self).populate() 267 268 self.facts['all_ipv4_addresses'] = list() 269 self.facts['all_ipv6_addresses'] = list() 270 271 data1 = self.run(['show interface status']) 272 data1 = to_text(data1, errors='surrogate_or_strict').strip() 273 data1 = data1.replace(r"\n", "\n") 274 data2 = self.run(['show lldp port']) 275 data2 = to_text(data2, errors='surrogate_or_strict').strip() 276 data2 = data2.replace(r"\n", "\n") 277 lines1 = None 278 lines2 = None 279 if data1: 280 lines1 = self.parse_interfaces(data1) 281 if data2: 282 lines2 = self.parse_interfaces(data2) 283 if lines1 is not None and lines2 is not None: 284 self.facts['interfaces'] = self.populate_interfaces(lines1, lines2) 285 data3 = self.run(['show lldp remote-device port']) 286 data3 = to_text(data3, errors='surrogate_or_strict').strip() 287 data3 = data3.replace(r"\n", "\n") 288 289 lines3 = None 290 if data3: 291 lines3 = self.parse_neighbors(data3) 292 if lines3 is not None: 293 self.facts['neighbors'] = self.populate_neighbors(lines3) 294 295 data4 = self.run(['show interface ip']) 296 data4 = data4[0].split('\n') 297 lines4 = None 298 if data4: 299 lines4 = self.parse_ipaddresses(data4) 300 ipv4_interfaces = self.set_ipv4_interfaces(lines4) 301 self.facts['all_ipv4_addresses'] = ipv4_interfaces 302 ipv6_interfaces = self.set_ipv6_interfaces(lines4) 303 self.facts['all_ipv6_addresses'] = ipv6_interfaces 304 305 def parse_ipaddresses(self, data4): 306 parsed = list() 307 for line in data4: 308 if len(line) == 0: 309 continue 310 else: 311 line = line.strip() 312 if len(line) == 0: 313 continue 314 match = re.search(r'IP4', line, re.M | re.I) 315 if match: 316 key = match.group() 317 parsed.append(line) 318 match = re.search(r'IP6', line, re.M | re.I) 319 if match: 320 key = match.group() 321 parsed.append(line) 322 return parsed 323 324 def set_ipv4_interfaces(self, line4): 325 ipv4_addresses = list() 326 for line in line4: 327 ipv4Split = line.split() 328 if ipv4Split[1] == "IP4": 329 ipv4_addresses.append(ipv4Split[2]) 330 return ipv4_addresses 331 332 def set_ipv6_interfaces(self, line4): 333 ipv6_addresses = list() 334 for line in line4: 335 ipv6Split = line.split() 336 if ipv6Split[1] == "IP6": 337 ipv6_addresses.append(ipv6Split[2]) 338 return ipv6_addresses 339 340 def populate_neighbors(self, lines3): 341 neighbors = dict() 342 for line in lines3: 343 neighborSplit = line.split("|") 344 innerData = dict() 345 innerData['Remote Chassis ID'] = neighborSplit[2].strip() 346 innerData['Remote Port'] = neighborSplit[3].strip() 347 sysName = neighborSplit[4].strip() 348 if sysName is not None: 349 innerData['Remote System Name'] = neighborSplit[4].strip() 350 else: 351 innerData['Remote System Name'] = "NA" 352 neighbors[neighborSplit[0].strip()] = innerData 353 return neighbors 354 355 def populate_interfaces(self, lines1, lines2): 356 interfaces = dict() 357 for line1, line2 in zip(lines1, lines2): 358 line = line1 + " " + line2 359 intfSplit = line.split() 360 innerData = dict() 361 innerData['description'] = intfSplit[6].strip() 362 innerData['macaddress'] = intfSplit[8].strip() 363 innerData['mtu'] = intfSplit[9].strip() 364 innerData['speed'] = intfSplit[1].strip() 365 innerData['duplex'] = intfSplit[2].strip() 366 innerData['operstatus'] = intfSplit[5].strip() 367 if("up" not in intfSplit[5].strip()) and ("down" not in intfSplit[5].strip()): 368 innerData['description'] = intfSplit[7].strip() 369 innerData['macaddress'] = intfSplit[9].strip() 370 innerData['mtu'] = intfSplit[10].strip() 371 innerData['operstatus'] = intfSplit[6].strip() 372 interfaces[intfSplit[0].strip()] = innerData 373 return interfaces 374 375 def parse_neighbors(self, neighbors): 376 parsed = list() 377 for line in neighbors.split('\n'): 378 if len(line) == 0: 379 continue 380 else: 381 line = line.strip() 382 match = re.match(r'^([0-9]+)', line) 383 if match: 384 key = match.group(1) 385 parsed.append(line) 386 match = re.match(r'^(INT+)', line) 387 if match: 388 key = match.group(1) 389 parsed.append(line) 390 match = re.match(r'^(EXT+)', line) 391 if match: 392 key = match.group(1) 393 parsed.append(line) 394 match = re.match(r'^(MGT+)', line) 395 if match: 396 key = match.group(1) 397 parsed.append(line) 398 return parsed 399 400 def parse_interfaces(self, data): 401 parsed = list() 402 for line in data.split('\n'): 403 if len(line) == 0: 404 continue 405 else: 406 line = line.strip() 407 match = re.match(r'^([0-9]+)', line) 408 if match: 409 key = match.group(1) 410 parsed.append(line) 411 match = re.match(r'^(INT+)', line) 412 if match: 413 key = match.group(1) 414 parsed.append(line) 415 match = re.match(r'^(EXT+)', line) 416 if match: 417 key = match.group(1) 418 parsed.append(line) 419 match = re.match(r'^(MGT+)', line) 420 if match: 421 key = match.group(1) 422 parsed.append(line) 423 return parsed 424 425 426FACT_SUBSETS = dict( 427 default=Default, 428 hardware=Hardware, 429 interfaces=Interfaces, 430 config=Config, 431) 432 433VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) 434 435PERSISTENT_COMMAND_TIMEOUT = 60 436 437 438def main(): 439 """main entry point for module execution 440 """ 441 argument_spec = dict( 442 gather_subset=dict(default=['!config'], type='list') 443 ) 444 445 argument_spec.update(enos_argument_spec) 446 447 module = AnsibleModule(argument_spec=argument_spec, 448 supports_check_mode=True) 449 450 gather_subset = module.params['gather_subset'] 451 452 runable_subsets = set() 453 exclude_subsets = set() 454 455 for subset in gather_subset: 456 if subset == 'all': 457 runable_subsets.update(VALID_SUBSETS) 458 continue 459 460 if subset.startswith('!'): 461 subset = subset[1:] 462 if subset == 'all': 463 exclude_subsets.update(VALID_SUBSETS) 464 continue 465 exclude = True 466 else: 467 exclude = False 468 469 if subset not in VALID_SUBSETS: 470 module.fail_json(msg='Bad subset') 471 472 if exclude: 473 exclude_subsets.add(subset) 474 else: 475 runable_subsets.add(subset) 476 477 if not runable_subsets: 478 runable_subsets.update(VALID_SUBSETS) 479 480 runable_subsets.difference_update(exclude_subsets) 481 runable_subsets.add('default') 482 483 facts = dict() 484 facts['gather_subset'] = list(runable_subsets) 485 486 instances = list() 487 for key in runable_subsets: 488 instances.append(FACT_SUBSETS[key](module)) 489 490 for inst in instances: 491 inst.populate() 492 facts.update(inst.facts) 493 494 ansible_facts = dict() 495 for key, value in iteritems(facts): 496 key = 'ansible_net_%s' % key 497 ansible_facts[key] = value 498 499 warnings = list() 500 check_args(module, warnings) 501 502 module.exit_json(ansible_facts=ansible_facts, warnings=warnings) 503 504 505if __name__ == '__main__': 506 main() 507