1# -*- coding: utf-8 -*- 2# Copyright 2015 Spotify AB. All rights reserved. 3# 4# The contents of this file are licensed under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with the 6# License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15 16# import stdlib 17from builtins import super 18import re 19import socket 20 21# import third party lib 22from netaddr import IPAddress, IPNetwork 23from netaddr.core import AddrFormatError 24 25# import NAPALM Base 26from napalm.base import helpers 27from napalm.base.exceptions import CommandErrorException, ReplaceConfigException 28from napalm.nxos import NXOSDriverBase 29 30# Easier to store these as constants 31HOUR_SECONDS = 3600 32DAY_SECONDS = 24 * HOUR_SECONDS 33WEEK_SECONDS = 7 * DAY_SECONDS 34YEAR_SECONDS = 365 * DAY_SECONDS 35 36# STD REGEX PATTERNS 37IP_ADDR_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" 38IPV4_ADDR_REGEX = IP_ADDR_REGEX 39IPV6_ADDR_REGEX_1 = r"::" 40IPV6_ADDR_REGEX_2 = r"[0-9a-fA-F:]{1,39}::[0-9a-fA-F:]{1,39}" 41IPV6_ADDR_REGEX_3 = ( 42 r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:" 43 r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}" 44) 45# Should validate IPv6 address using an IP address library after matching with this regex 46IPV6_ADDR_REGEX = r"(?:{}|{}|{})".format( 47 IPV6_ADDR_REGEX_1, IPV6_ADDR_REGEX_2, IPV6_ADDR_REGEX_3 48) 49IPV4_OR_IPV6_REGEX = r"(?:{}|{})".format(IPV4_ADDR_REGEX, IPV6_ADDR_REGEX) 50 51MAC_REGEX = r"[a-fA-F0-9]{4}\.[a-fA-F0-9]{4}\.[a-fA-F0-9]{4}" 52VLAN_REGEX = r"\d{1,4}" 53 54RE_IPADDR = re.compile(r"{}".format(IP_ADDR_REGEX)) 55RE_MAC = re.compile(r"{}".format(MAC_REGEX)) 56 57# Period needed for 32-bit AS Numbers 58ASN_REGEX = r"[\d\.]+" 59 60RE_IP_ROUTE_VIA_REGEX = re.compile( 61 r" (?P<used>[\*| ])via ((?P<ip>" + IPV4_ADDR_REGEX + r")" 62 r"(%(?P<vrf>\S+))?, )?" 63 r"((?P<int>[\w./:]+), )?\[(\d+)/(?P<metric>\d+)\]" 64 r", (?P<age>[\d\w:]+), (?P<source>[\w]+)(-(?P<procnr>\d+))?" 65 r"(?P<rest>.*)" 66) 67RE_RT_VRF_NAME = re.compile(r"VRF \"(\S+)\"") 68RE_RT_IPV4_ROUTE_PREF = re.compile(r"(" + IPV4_ADDR_REGEX + r"/\d{1,2}), ubest.*") 69 70RE_BGP_PROTO_TAG = re.compile(r"BGP Protocol Tag\s+: (\d+)") 71RE_BGP_REMOTE_AS = re.compile(r"remote AS (" + ASN_REGEX + r")") 72RE_BGP_COMMUN = re.compile(r"[ ]{10}([\S ]+)") 73 74 75def parse_intf_section(interface): 76 """Parse a single entry from show interfaces output. 77 78 Different cases: 79 mgmt0 is up 80 admin state is up 81 82 Ethernet2/1 is up 83 admin state is up, Dedicated Interface 84 85 Vlan1 is down (Administratively down), line protocol is down, autostate enabled 86 87 Ethernet154/1/48 is up (with no 'admin state') 88 """ 89 interface = interface.strip() 90 re_protocol = ( 91 r"^(?P<intf_name>\S+?)\s+is\s+(?P<status>.+?)" 92 r",\s+line\s+protocol\s+is\s+(?P<protocol>\S+).*$" 93 ) 94 re_intf_name_state = r"^(?P<intf_name>\S+) is (?P<intf_state>\S+).*" 95 re_is_enabled_1 = r"^admin state is (?P<is_enabled>\S+)$" 96 re_is_enabled_2 = r"^admin state is (?P<is_enabled>\S+), " 97 re_is_enabled_3 = r"^.* is down.*Administratively down.*$" 98 re_mac = r"^\s+Hardware:\s+(?P<hardware>.*),\s+address:\s+(?P<mac_address>\S+) " 99 re_speed = ( 100 r"\s+MTU (?P<mtu>\S+)\s+bytes,\s+BW\s+(?P<speed>\S+)\s+(?P<speed_unit>\S+).*$" 101 ) 102 re_mtu_nve = r"\s+MTU (?P<mtu_nve>\S+)\s+bytes.*$" 103 re_description_1 = r"^\s+Description:\s+(?P<description>.*) (?:MTU|Internet)" 104 re_description_2 = r"^\s+Description:\s+(?P<description>.*)$" 105 re_hardware = r"^.* Hardware: (?P<hardware>\S+)$" 106 107 # Check for 'protocol is ' lines 108 match = re.search(re_protocol, interface, flags=re.M) 109 if match: 110 intf_name = match.group("intf_name") 111 status = match.group("status") 112 protocol = match.group("protocol") 113 114 if "admin" in status.lower(): 115 is_enabled = False 116 else: 117 is_enabled = True 118 is_up = bool("up" in protocol) 119 120 else: 121 # More standard is up, next line admin state is lines 122 match = re.search(re_intf_name_state, interface) 123 intf_name = helpers.canonical_interface_name(match.group("intf_name")) 124 intf_state = match.group("intf_state").strip() 125 is_up = True if intf_state == "up" else False 126 127 admin_state_present = re.search("admin state is", interface) 128 if admin_state_present: 129 # Parse cases where 'admin state' string exists 130 for x_pattern in [re_is_enabled_1, re_is_enabled_2]: 131 match = re.search(x_pattern, interface, flags=re.M) 132 if match: 133 is_enabled = match.group("is_enabled").strip() 134 is_enabled = True if re.search("up", is_enabled) else False 135 break 136 else: 137 msg = "Error parsing intf, 'admin state' never detected:\n\n{}".format( 138 interface 139 ) 140 raise ValueError(msg) 141 else: 142 # No 'admin state' should be 'is up' or 'is down' strings 143 # If interface is up; it is enabled 144 is_enabled = True 145 if not is_up: 146 match = re.search(re_is_enabled_3, interface, flags=re.M) 147 if match: 148 is_enabled = False 149 150 match = re.search(re_mac, interface, flags=re.M) 151 if match: 152 mac_address = match.group("mac_address") 153 mac_address = helpers.mac(mac_address) 154 else: 155 mac_address = "" 156 157 match = re.search(re_hardware, interface, flags=re.M) 158 speed_exist = True 159 if match: 160 if match.group("hardware") == "NVE": 161 match = re.search(re_mtu_nve, interface, flags=re.M) 162 mtu = int(match.group("mtu_nve")) 163 speed_exist = False 164 165 if speed_exist: 166 match = re.search(re_speed, interface, flags=re.M) 167 speed = int(match.group("speed")) 168 mtu = int(match.group("mtu")) 169 speed_unit = match.group("speed_unit") 170 speed_unit = speed_unit.rstrip(",") 171 # This was alway in Kbit (in the data I saw) 172 if speed_unit != "Kbit": 173 msg = "Unexpected speed unit in show interfaces parsing:\n\n{}".format( 174 interface 175 ) 176 raise ValueError(msg) 177 speed = int(round(speed / 1000.0)) 178 else: 179 speed = -1 180 181 description = "" 182 for x_pattern in [re_description_1, re_description_2]: 183 match = re.search(x_pattern, interface, flags=re.M) 184 if match: 185 description = match.group("description") 186 break 187 188 return { 189 intf_name: { 190 "description": description, 191 "is_enabled": is_enabled, 192 "is_up": is_up, 193 "last_flapped": -1.0, 194 "mac_address": mac_address, 195 "mtu": mtu, 196 "speed": speed, 197 } 198 } 199 200 201def convert_hhmmss(hhmmss): 202 """Convert hh:mm:ss to seconds.""" 203 fields = hhmmss.split(":") 204 if len(fields) != 3: 205 raise ValueError("Received invalid HH:MM:SS data: {}".format(hhmmss)) 206 fields = [int(x) for x in fields] 207 hours, minutes, seconds = fields 208 return (hours * 3600) + (minutes * 60) + seconds 209 210 211def bgp_time_conversion(bgp_uptime): 212 """Convert string time to seconds. 213 214 Examples 215 00:14:23 216 00:13:40 217 00:00:21 218 00:00:13 219 00:00:49 220 1d11h 221 1d17h 222 1w0d 223 8w5d 224 1y28w 225 never 226 """ 227 bgp_uptime = bgp_uptime.strip() 228 uptime_letters = set(["w", "h", "d"]) 229 230 if "never" in bgp_uptime: 231 return -1 232 elif ":" in bgp_uptime: 233 times = bgp_uptime.split(":") 234 times = [int(x) for x in times] 235 hours, minutes, seconds = times 236 return (hours * 3600) + (minutes * 60) + seconds 237 # Check if any letters 'w', 'h', 'd' are in the time string 238 elif uptime_letters & set(bgp_uptime): 239 form1 = r"(\d+)d(\d+)h" # 1d17h 240 form2 = r"(\d+)w(\d+)d" # 8w5d 241 form3 = r"(\d+)y(\d+)w" # 1y28w 242 match = re.search(form1, bgp_uptime) 243 if match: 244 days = int(match.group(1)) 245 hours = int(match.group(2)) 246 return (days * DAY_SECONDS) + (hours * 3600) 247 match = re.search(form2, bgp_uptime) 248 if match: 249 weeks = int(match.group(1)) 250 days = int(match.group(2)) 251 return (weeks * WEEK_SECONDS) + (days * DAY_SECONDS) 252 match = re.search(form3, bgp_uptime) 253 if match: 254 years = int(match.group(1)) 255 weeks = int(match.group(2)) 256 return (years * YEAR_SECONDS) + (weeks * WEEK_SECONDS) 257 raise ValueError("Unexpected value for BGP uptime string: {}".format(bgp_uptime)) 258 259 260def bgp_normalize_table_data(bgp_table): 261 """The 'show bgp all summary vrf all' table can have entries that wrap multiple lines. 262 263 2001:db8:4:701::2 264 4 65535 163664 163693 145 0 0 3w2d 3 265 2001:db8:e0:dd::1 266 4 10 327491 327278 145 0 0 3w1d 4 267 268 Normalize this so the line wrap doesn't exit. 269 """ 270 bgp_table = bgp_table.strip() 271 bgp_multiline_pattern = r"({})\s*\n".format(IPV4_OR_IPV6_REGEX) 272 # Strip out the newline 273 return re.sub(bgp_multiline_pattern, r"\1", bgp_table) 274 275 276def bgp_table_parser(bgp_table): 277 """Generator that parses a line of bgp summary table and returns a dict compatible with NAPALM 278 279 Example line: 280 10.2.1.14 4 10 472516 472238 361 0 0 3w1d 9 281 """ 282 bgp_table = bgp_table.strip() 283 for bgp_entry in bgp_table.splitlines(): 284 bgp_table_fields = bgp_entry.split() 285 286 try: 287 if re.search(r"Shut.*Admin", bgp_entry): 288 ( 289 peer_ip, 290 bgp_version, 291 remote_as, 292 msg_rcvd, 293 msg_sent, 294 _, 295 _, 296 _, 297 uptime, 298 state_1, 299 state_2, 300 ) = bgp_table_fields 301 state_pfxrcd = "{} {}".format(state_1, state_2) 302 else: 303 ( 304 peer_ip, 305 bgp_version, 306 remote_as, 307 msg_rcvd, 308 msg_sent, 309 _, 310 _, 311 _, 312 uptime, 313 state_pfxrcd, 314 ) = bgp_table_fields 315 except ValueError: 316 raise ValueError( 317 "Unexpected entry ({}) in BGP summary table".format(bgp_table_fields) 318 ) 319 320 is_enabled = True 321 try: 322 received_prefixes = int(state_pfxrcd) 323 is_up = True 324 except ValueError: 325 received_prefixes = -1 326 is_up = False 327 if re.search(r"Shut.*Admin", state_pfxrcd): 328 is_enabled = False 329 330 if not is_up: 331 uptime = -1 332 if uptime != -1: 333 uptime = bgp_time_conversion(uptime) 334 335 yield { 336 peer_ip: { 337 "is_enabled": is_enabled, 338 "uptime": uptime, 339 "remote_as": helpers.as_number(remote_as), 340 "is_up": is_up, 341 "description": "", 342 "received_prefixes": received_prefixes, 343 } 344 } 345 346 347def bgp_summary_parser(bgp_summary): 348 """Parse 'show bgp all summary vrf' output information from NX-OS devices.""" 349 350 bgp_summary_dict = {} 351 # Check for BGP summary information lines that have no data 352 if len(bgp_summary.strip().splitlines()) <= 1: 353 return {} 354 355 allowed_afi = ["ipv4", "ipv6", "l2vpn"] 356 vrf_regex = r"^BGP summary information for VRF\s+(?P<vrf>\S+)," 357 afi_regex = ( 358 r"^BGP summary information.*address family (?P<afi>\S+ (?:Unicast|EVPN))" 359 ) 360 local_router_regex = ( 361 r"^BGP router identifier\s+(?P<router_id>\S+)" 362 r",\s+local AS number\s+(?P<local_as>\S+)" 363 ) 364 365 for pattern in [vrf_regex, afi_regex, local_router_regex]: 366 match = re.search(pattern, bgp_summary, flags=re.M) 367 if match: 368 bgp_summary_dict.update(match.groupdict(1)) 369 370 # Some post regex cleanup and validation 371 vrf = bgp_summary_dict["vrf"] 372 if vrf.lower() == "default": 373 bgp_summary_dict["vrf"] = "global" 374 375 afi = bgp_summary_dict["afi"] 376 afi = afi.split()[0].lower() 377 if afi not in allowed_afi: 378 raise ValueError("AFI ({}) is invalid and not supported.".format(afi)) 379 bgp_summary_dict["afi"] = afi 380 381 local_as = bgp_summary_dict["local_as"] 382 local_as = helpers.as_number(local_as) 383 384 match = re.search(IPV4_ADDR_REGEX, bgp_summary_dict["router_id"]) 385 if not match: 386 raise ValueError( 387 "BGP router_id ({}) is not valid".format(bgp_summary_dict["router_id"]) 388 ) 389 390 vrf = bgp_summary_dict["vrf"] 391 bgp_return_dict = {vrf: {"router_id": bgp_summary_dict["router_id"], "peers": {}}} 392 393 # Extract and process the tabular data 394 tabular_divider = r"^Neighbor\s+.*PfxRcd$" 395 tabular_data = re.split(tabular_divider, bgp_summary, flags=re.M) 396 if len(tabular_data) != 2: 397 msg = "Unexpected data processing BGP summary information:\n\n{}".format( 398 bgp_summary 399 ) 400 raise ValueError(msg) 401 tabular_data = tabular_data[1] 402 bgp_table = bgp_normalize_table_data(tabular_data) 403 for bgp_entry in bgp_table_parser(bgp_table): 404 bgp_return_dict[vrf]["peers"].update(bgp_entry) 405 406 bgp_new_dict = {} 407 for neighbor, bgp_data in bgp_return_dict[vrf]["peers"].items(): 408 received_prefixes = bgp_data.pop("received_prefixes") 409 bgp_data["address_family"] = {} 410 prefixes_dict = { 411 "sent_prefixes": -1, 412 "accepted_prefixes": -1, 413 "received_prefixes": received_prefixes, 414 } 415 bgp_data["address_family"][afi] = prefixes_dict 416 bgp_data["local_as"] = local_as 417 # FIX, hard-coding 418 bgp_data["remote_id"] = "0.0.0.0" 419 bgp_new_dict[neighbor] = bgp_data 420 421 bgp_return_dict[vrf]["peers"] = bgp_new_dict 422 423 return bgp_return_dict 424 425 426class NXOSSSHDriver(NXOSDriverBase): 427 def __init__(self, hostname, username, password, timeout=60, optional_args=None): 428 super().__init__( 429 hostname, username, password, timeout=timeout, optional_args=optional_args 430 ) 431 self.platform = "nxos_ssh" 432 self.connector_type_map = { 433 "1000base-LH": "LC_CONNECTOR", 434 "1000base-SX": "LC_CONNECTOR", 435 "1000base-T": "Unknown", 436 "10Gbase-LR": "LC_CONNECTOR", 437 "10Gbase-SR": "LC_CONNECTOR", 438 "SFP-H10GB-CU1M": "DAC_CONNECTOR", 439 "SFP-H10GB-CU1.45M": "DAC_CONNECTOR", 440 "SFP-H10GB-CU3M": "DAC_CONNECTOR", 441 "SFP-H10GB-CU3.45M": "DAC_CONNECTOR", 442 } 443 444 def open(self): 445 self.device = self._netmiko_open( 446 device_type="cisco_nxos", netmiko_optional_args=self.netmiko_optional_args 447 ) 448 449 def close(self): 450 self._netmiko_close() 451 452 def _send_command(self, command, raw_text=False, cmd_verify=True): 453 """ 454 Wrapper for Netmiko's send_command method. 455 456 raw_text argument is not used and is for code sharing with NX-API. 457 """ 458 return self.device.send_command(command, cmd_verify=cmd_verify) 459 460 def _send_command_list(self, commands, expect_string=None): 461 """Wrapper for Netmiko's send_command method (for list of commands.""" 462 output = "" 463 for command in commands: 464 output += self.device.send_command( 465 command, 466 strip_prompt=False, 467 strip_command=False, 468 expect_string=expect_string, 469 ) 470 return output 471 472 def _send_config(self, commands): 473 if isinstance(commands, str): 474 commands = (command for command in commands.splitlines() if command) 475 return self.device.send_config_set(commands) 476 477 @staticmethod 478 def parse_uptime(uptime_str): 479 """ 480 Extract the uptime string from the given Cisco IOS Device. 481 Return the uptime in seconds as an integer 482 """ 483 # Initialize to zero 484 (years, weeks, days, hours, minutes) = (0, 0, 0, 0, 0) 485 486 uptime_str = uptime_str.strip() 487 time_list = uptime_str.split(",") 488 for element in time_list: 489 if re.search("year", element): 490 years = int(element.split()[0]) 491 elif re.search("week", element): 492 weeks = int(element.split()[0]) 493 elif re.search("day", element): 494 days = int(element.split()[0]) 495 elif re.search("hour", element): 496 hours = int(element.split()[0]) 497 elif re.search("minute", element): 498 minutes = int(element.split()[0]) 499 elif re.search("second", element): 500 seconds = int(element.split()[0]) 501 502 uptime_sec = ( 503 (years * YEAR_SECONDS) 504 + (weeks * WEEK_SECONDS) 505 + (days * DAY_SECONDS) 506 + (hours * 3600) 507 + (minutes * 60) 508 + seconds 509 ) 510 return uptime_sec 511 512 def is_alive(self): 513 """Returns a flag with the state of the SSH connection.""" 514 null = chr(0) 515 try: 516 if self.device is None: 517 return {"is_alive": False} 518 else: 519 # Try sending ASCII null byte to maintain the connection alive 520 self._send_command(null, cmd_verify=False) 521 except (socket.error, EOFError): 522 # If unable to send, we can tell for sure that the connection is unusable, 523 # hence return False. 524 return {"is_alive": False} 525 return {"is_alive": self.device.remote_conn.transport.is_active()} 526 527 def _copy_run_start(self): 528 529 output = self.device.save_config() 530 if "complete" in output.lower(): 531 return True 532 else: 533 msg = "Unable to save running-config to startup-config!" 534 raise CommandErrorException(msg) 535 536 def _load_cfg_from_checkpoint(self): 537 538 commands = [ 539 "terminal dont-ask", 540 "rollback running-config file {}".format(self.candidate_cfg), 541 "no terminal dont-ask", 542 ] 543 544 try: 545 rollback_result = self._send_command_list(commands, expect_string=r"[#>]") 546 finally: 547 self.changed = True 548 msg = rollback_result 549 if "Rollback failed." in msg: 550 raise ReplaceConfigException(msg) 551 552 def rollback(self): 553 if self.changed: 554 commands = [ 555 "terminal dont-ask", 556 "rollback running-config file {}".format(self.rollback_cfg), 557 "no terminal dont-ask", 558 ] 559 result = self._send_command_list(commands, expect_string=r"[#>]") 560 if "completed" not in result.lower(): 561 raise ReplaceConfigException(result) 562 # If hostname changes ensure Netmiko state is updated properly 563 self._netmiko_device.set_base_prompt() 564 self._copy_run_start() 565 self.changed = False 566 567 def _apply_key_map(self, key_map, table): 568 new_dict = {} 569 for key, value in table.items(): 570 new_key = key_map.get(key) 571 if new_key: 572 new_dict[new_key] = str(value) 573 return new_dict 574 575 def _convert_uptime_to_seconds(self, uptime_facts): 576 seconds = int(uptime_facts["up_days"]) * 24 * 60 * 60 577 seconds += int(uptime_facts["up_hours"]) * 60 * 60 578 seconds += int(uptime_facts["up_mins"]) * 60 579 seconds += int(uptime_facts["up_secs"]) 580 return seconds 581 582 def get_facts(self): 583 """Return a set of facts from the devices.""" 584 # default values. 585 vendor = "Cisco" 586 uptime = -1 587 serial_number, fqdn, os_version, hostname, domain_name, model = ("",) * 6 588 589 # obtain output from device 590 show_ver = self._send_command("show version") 591 show_hosts = self._send_command("show hosts") 592 show_int_status = self._send_command("show interface status") 593 show_hostname = self._send_command("show hostname") 594 595 try: 596 show_inventory_table = self._get_command_table( 597 "show inventory | json", "TABLE_inv", "ROW_inv" 598 ) 599 if isinstance(show_inventory_table, dict): 600 show_inventory_table = [show_inventory_table] 601 602 for row in show_inventory_table: 603 if row["name"] == '"Chassis"' or row["name"] == "Chassis": 604 serial_number = row.get("serialnum", "") 605 break 606 except ValueError: 607 show_inventory = self._send_command("show inventory") 608 find_regexp = r"^NAME:\s+\"(.*)\",.*\n^PID:.*SN:\s+(\w*)" 609 find = re.findall(find_regexp, show_inventory, re.MULTILINE) 610 for row in find: 611 if row[0] == "Chassis": 612 serial_number = row[1] 613 break 614 615 # uptime/serial_number/IOS version 616 for line in show_ver.splitlines(): 617 if " uptime is " in line: 618 _, uptime_str = line.split(" uptime is ") 619 uptime = self.parse_uptime(uptime_str) 620 621 if "system: " in line or "NXOS: " in line: 622 line = line.strip() 623 os_version = line.split()[2] 624 os_version = os_version.strip() 625 626 if "cisco" in line and "hassis" in line: 627 match = re.search(r".cisco (.*) \(", line) 628 if match: 629 model = match.group(1).strip() 630 match = re.search(r".cisco (.* [cC]hassis)", line) 631 if match: 632 model = match.group(1).strip() 633 634 hostname = show_hostname.strip() 635 636 # Determine domain_name and fqdn 637 for line in show_hosts.splitlines(): 638 if "Default domain" in line: 639 _, domain_name = re.split(r".*Default domain.*is ", line) 640 domain_name = domain_name.strip() 641 break 642 if hostname.count(".") >= 2: 643 fqdn = hostname 644 # Remove domain name from hostname 645 if domain_name: 646 hostname = re.sub(re.escape(domain_name) + "$", "", hostname) 647 hostname = hostname.strip(".") 648 elif domain_name: 649 fqdn = "{}.{}".format(hostname, domain_name) 650 651 # interface_list filter 652 interface_list = [] 653 show_int_status = show_int_status.strip() 654 # Remove the header information 655 show_int_status = re.sub( 656 r"(?:^---------+$|^Port .*$|^ .*$)", "", show_int_status, flags=re.M 657 ) 658 for line in show_int_status.splitlines(): 659 if not line: 660 continue 661 interface = line.split()[0] 662 # Return canonical interface name 663 interface_list.append(helpers.canonical_interface_name(interface)) 664 665 return { 666 "uptime": int(uptime), 667 "vendor": vendor, 668 "os_version": str(os_version), 669 "serial_number": str(serial_number), 670 "model": str(model), 671 "hostname": str(hostname), 672 "fqdn": fqdn, 673 "interface_list": interface_list, 674 } 675 676 def get_interfaces(self): 677 """ 678 Get interface details. 679 680 last_flapped is not implemented 681 682 Example Output: 683 684 { u'Vlan1': { 'description': u'', 685 'is_enabled': True, 686 'is_up': True, 687 'last_flapped': -1.0, 688 'mac_address': u'a493.4cc1.67a7', 689 'speed': 100}, 690 u'Vlan100': { 'description': u'Data Network', 691 'is_enabled': True, 692 'is_up': True, 693 'last_flapped': -1.0, 694 'mac_address': u'a493.4cc1.67a7', 695 'speed': 100}, 696 u'Vlan200': { 'description': u'Voice Network', 697 'is_enabled': True, 698 'is_up': True, 699 'last_flapped': -1.0, 700 'mac_address': u'a493.4cc1.67a7', 701 'speed': 100}} 702 """ 703 interfaces = {} 704 command = "show interface" 705 output = self._send_command(command) 706 if not output: 707 return {} 708 709 # Break output into per-interface sections (note, separator text is retained) 710 separator1 = r"^\S+\s+is \S+.*\nadmin state is.*$" 711 separator2 = r"^.* is .*, line protocol is .*$" 712 separator3 = r"^.* is (?:down|up).*$" 713 separators = r"({}|{}|{})".format(separator1, separator2, separator3) 714 interface_lines = re.split(separators, output, flags=re.M) 715 716 if len(interface_lines) == 1: 717 msg = "Unexpected output data in '{}':\n\n{}".format( 718 command, interface_lines 719 ) 720 raise ValueError(msg) 721 722 # Get rid of the blank data at the beginning 723 interface_lines.pop(0) 724 725 # Must be pairs of data (the separator and section corresponding to it) 726 if len(interface_lines) % 2 != 0: 727 msg = "Unexpected output data in '{}':\n\n{}".format( 728 command, interface_lines 729 ) 730 raise ValueError(msg) 731 732 # Combine the separator and section into one string 733 intf_iter = iter(interface_lines) 734 try: 735 new_interfaces = [line + next(intf_iter, "") for line in intf_iter] 736 except TypeError: 737 raise ValueError() 738 739 for entry in new_interfaces: 740 interfaces.update(parse_intf_section(entry)) 741 742 return interfaces 743 744 def get_bgp_neighbors(self): 745 """BGP neighbor information. 746 747 Supports VRFs and IPv4 and IPv6 AFIs 748 749 { 750 "global": { 751 "router_id": "1.1.1.103", 752 "peers": { 753 "10.99.99.2": { 754 "is_enabled": true, 755 "uptime": -1, 756 "remote_as": 22, 757 "address_family": { 758 "ipv4": { 759 "sent_prefixes": -1, 760 "accepted_prefixes": -1, 761 "received_prefixes": -1 762 } 763 }, 764 "remote_id": "0.0.0.0", 765 "local_as": 22, 766 "is_up": false, 767 "description": "" 768 } 769 } 770 } 771 """ 772 bgp_dict = {} 773 774 # get summary output from device 775 cmd_bgp_all_sum = "show bgp all summary vrf all" 776 bgp_summary_output = self._send_command(cmd_bgp_all_sum).strip() 777 778 section_separator = r"BGP summary information for " 779 bgp_summary_sections = re.split(section_separator, bgp_summary_output) 780 if len(bgp_summary_sections): 781 bgp_summary_sections.pop(0) 782 783 for bgp_section in bgp_summary_sections: 784 bgp_section = section_separator + bgp_section 785 bgp_dict.update(bgp_summary_parser(bgp_section)) 786 787 # FIX -- look up logical or behavior we did in Cisco IOS bgp parser (make consistent here) 788 # FIX -- need to merge IPv6 and IPv4 AFI for same neighbor 789 return bgp_dict 790 791 def cli(self, commands): 792 cli_output = {} 793 if type(commands) is not list: 794 raise TypeError("Please enter a valid list of commands!") 795 796 for command in commands: 797 output = self._send_command(command) 798 cli_output[str(command)] = output 799 return cli_output 800 801 def get_environment(self): 802 """ 803 Get environment facts. 804 805 power and fan are currently not implemented 806 cpu is using 1-minute average 807 """ 808 809 environment = {} 810 # sys_resources contains cpu and mem output 811 sys_resources = self._send_command("show system resources") 812 temp_cmd = "show environment temperature" 813 814 # cpu 815 environment.setdefault("cpu", {}) 816 environment["cpu"]["0"] = {} 817 environment["cpu"]["0"]["%usage"] = -1.0 818 system_resources_cpu = helpers.textfsm_extractor( 819 self, "system_resources", sys_resources 820 ) 821 for cpu in system_resources_cpu: 822 cpu_dict = { 823 cpu.get("cpu_id"): { 824 "%usage": round(100 - float(cpu.get("cpu_idle")), 2) 825 } 826 } 827 environment["cpu"].update(cpu_dict) 828 829 # memory 830 environment.setdefault("memory", {}) 831 for line in sys_resources.splitlines(): 832 # Memory usage: 16401224K total, 4798280K used, 11602944K free 833 if "Memory usage:" in line: 834 proc_total_mem, proc_used_mem, _ = line.split(",") 835 proc_used_mem = re.search(r"\d+", proc_used_mem).group(0) 836 proc_total_mem = re.search(r"\d+", proc_total_mem).group(0) 837 break 838 else: 839 raise ValueError("Unexpected output from: {}".format(line)) 840 environment["memory"]["used_ram"] = int(proc_used_mem) 841 environment["memory"]["available_ram"] = int(proc_total_mem) 842 843 # temperature 844 output = self._send_command(temp_cmd) 845 environment.setdefault("temperature", {}) 846 for line in output.splitlines(): 847 # Module Sensor MajorThresh MinorThres CurTemp Status 848 # 1 Intake 70 42 28 Ok 849 if re.match(r"^[0-9]", line): 850 module, sensor, is_critical, is_alert, temp, _ = line.split() 851 is_critical = float(is_critical) 852 is_alert = float(is_alert) 853 temp = float(temp) 854 env_value = { 855 "is_alert": temp >= is_alert, 856 "is_critical": temp >= is_critical, 857 "temperature": temp, 858 } 859 location = "{0}-{1}".format(sensor, module) 860 environment["temperature"][location] = env_value 861 862 # Initialize 'power' and 'fan' to default values (not implemented) 863 environment.setdefault("power", {}) 864 environment["power"]["invalid"] = { 865 "status": True, 866 "output": -1.0, 867 "capacity": -1.0, 868 } 869 environment.setdefault("fans", {}) 870 environment["fans"]["invalid"] = {"status": True} 871 872 return environment 873 874 def get_arp_table(self, vrf=""): 875 """ 876 Get arp table information. 877 878 Return a list of dictionaries having the following set of keys: 879 * interface (string) 880 * mac (string) 881 * ip (string) 882 * age (float) 883 884 For example:: 885 [ 886 { 887 'interface' : 'MgmtEth0/RSP0/CPU0/0', 888 'mac' : '5c:5e:ab:da:3c:f0', 889 'ip' : '172.17.17.1', 890 'age' : 12.0 891 }, 892 { 893 'interface': 'MgmtEth0/RSP0/CPU0/0', 894 'mac' : '66:0e:94:96:e0:ff', 895 'ip' : '172.17.17.2', 896 'age' : 14.0 897 } 898 ] 899 """ 900 arp_table = [] 901 902 command = "show ip arp vrf {} | exc INCOMPLETE".format(vrf or "all") 903 output = self._send_command(command) 904 905 separator = r"^Address\s+Age.*Interface.*$" 906 arp_list = re.split(separator, output, flags=re.M) 907 if len(arp_list) != 2: 908 raise ValueError("Error processing arp table output:\n\n{}".format(output)) 909 910 arp_entries = arp_list[1].strip() 911 for line in arp_entries.splitlines(): 912 if len(line.split()) >= 4: 913 # Search for extra characters to strip, currently strip '*', '+', '#', 'D' 914 line = re.sub(r"\s+[\*\+\#D]{1,4}\s*$", "", line, flags=re.M) 915 address, age, mac, interface = line.split() 916 else: 917 raise ValueError("Unexpected output from: {}".format(line.split())) 918 919 if age == "-": 920 age = -1.0 921 elif ":" not in age: 922 # Cisco sometimes returns a sub second arp time 0.411797 923 try: 924 age = float(age) 925 except ValueError: 926 age = -1.0 927 else: 928 age = convert_hhmmss(age) 929 age = float(age) 930 age = round(age, 1) 931 932 # Validate we matched correctly 933 if not re.search(RE_IPADDR, address): 934 raise ValueError("Invalid IP Address detected: {}".format(address)) 935 if not re.search(RE_MAC, mac): 936 raise ValueError("Invalid MAC Address detected: {}".format(mac)) 937 entry = { 938 "interface": interface, 939 "mac": helpers.mac(mac), 940 "ip": address, 941 "age": age, 942 } 943 arp_table.append(entry) 944 return arp_table 945 946 def _get_ntp_entity(self, peer_type): 947 ntp_entities = {} 948 command = "show ntp peers" 949 output = self._send_command(command) 950 951 for line in output.splitlines(): 952 # Skip first two lines and last line of command output 953 if line == "" or "-----" in line or "Peer IP Address" in line: 954 continue 955 elif IPAddress(len(line.split()[0])).is_unicast: 956 peer_addr = line.split()[0] 957 ntp_entities[peer_addr] = {} 958 else: 959 raise ValueError("Did not correctly find a Peer IP Address") 960 961 return ntp_entities 962 963 def get_ntp_peers(self): 964 return self._get_ntp_entity("Peer") 965 966 def get_ntp_servers(self): 967 return self._get_ntp_entity("Server") 968 969 def get_interfaces_ip(self): 970 """ 971 Get interface IP details. Returns a dictionary of dictionaries. 972 973 Sample output: 974 { 975 "Ethernet2/3": { 976 "ipv4": { 977 "4.4.4.4": { 978 "prefix_length": 16 979 } 980 }, 981 "ipv6": { 982 "2001:db8::1": { 983 "prefix_length": 10 984 }, 985 "fe80::2ec2:60ff:fe4f:feb2": { 986 "prefix_length": "128" 987 } 988 } 989 }, 990 "Ethernet2/2": { 991 "ipv4": { 992 "2.2.2.2": { 993 "prefix_length": 27 994 } 995 } 996 } 997 } 998 """ 999 interfaces_ip = {} 1000 ipv4_command = "show ip interface vrf all" 1001 ipv6_command = "show ipv6 interface vrf all" 1002 output_v4 = self._send_command(ipv4_command) 1003 output_v6 = self._send_command(ipv6_command) 1004 1005 v4_interfaces = {} 1006 for line in output_v4.splitlines(): 1007 # Ethernet2/2, Interface status: protocol-up/link-up/admin-up, iod: 38, 1008 # IP address: 2.2.2.2, IP subnet: 2.2.2.0/27 route-preference: 0, tag: 0 1009 # IP address: 3.3.3.3, IP subnet: 3.3.3.0/25 secondary route-preference: 0, tag: 0 1010 if "Interface status" in line: 1011 interface = line.split(",")[0] 1012 continue 1013 if "IP address" in line: 1014 ip_address = line.split(",")[0].split()[2] 1015 try: 1016 prefix_len = int(line.split()[5].split("/")[1]) 1017 except (ValueError, IndexError): 1018 prefix_len = "N/A" 1019 1020 if ip_address == "none": 1021 v4_interfaces.setdefault(interface, {}) 1022 else: 1023 val = {"prefix_length": prefix_len} 1024 v4_interfaces.setdefault(interface, {})[ip_address] = val 1025 1026 v6_interfaces = {} 1027 for line in output_v6.splitlines(): 1028 # Ethernet2/4, Interface status: protocol-up/link-up/admin-up, iod: 40 1029 # IPv6 address: 1030 # 2001:11:2233::a1/24 [VALID] 1031 # 2001:cc11:22bb:0:2ec2:60ff:fe4f:feb2/64 [VALID] 1032 # IPv6 subnet: 2001::/24 1033 # IPv6 link-local address: fe80::2ec2:60ff:fe4f:feb2 (default) [VALID] 1034 # IPv6 address: fe80::a293:51ff:fe5f:5ce9 [VALID] 1035 if "Interface status" in line: 1036 interface = line.split(",")[0] 1037 continue 1038 if "VALID" in line: 1039 line = line.strip() 1040 if "link-local address" in line: 1041 # match the following format: 1042 # IPv6 link-local address: fe80::2ec2:60ff:fe4f:feb2 (default) [VALID] 1043 ip_address = line.split()[3] 1044 prefix_len = "64" 1045 elif "IPv6 address" in line: 1046 # match the following format: 1047 # IPv6 address: fe80::a293:51ff:fe5f:5ce9 [VALID] 1048 ip_address = line.split()[2] 1049 prefix_len = "64" 1050 else: 1051 ip_address, prefix_len = line.split()[0].split("/") 1052 prefix_len = int(prefix_len) 1053 val = {"prefix_length": prefix_len} 1054 v6_interfaces.setdefault(interface, {})[ip_address] = val 1055 else: 1056 # match the following format: 1057 # IPv6 address: none 1058 v6_interfaces.setdefault(interface, {}) 1059 1060 # Join data from intermediate dictionaries. 1061 for interface, data in v4_interfaces.items(): 1062 interfaces_ip.setdefault(interface, {"ipv4": {}})["ipv4"] = data 1063 1064 for interface, data in v6_interfaces.items(): 1065 interfaces_ip.setdefault(interface, {"ipv6": {}})["ipv6"] = data 1066 1067 return interfaces_ip 1068 1069 def get_mac_address_table(self): 1070 """ 1071 Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address 1072 Table, having the following keys 1073 * mac (string) 1074 * interface (string) 1075 * vlan (int) 1076 * active (boolean) 1077 * static (boolean) 1078 * moves (int) 1079 * last_move (float) 1080 Format1: 1081 1082 Legend: 1083 * - primary entry, G - Gateway MAC, (R) - Routed MAC, O - Overlay MAC 1084 age - seconds since last seen,+ - primary entry using vPC Peer-Link, 1085 (T) - True, (F) - False 1086 VLAN MAC Address Type age Secure NTFY Ports/SWID.SSID.LID 1087 ---------+-----------------+--------+---------+------+----+------------------ 1088 * 27 0026.f064.0000 dynamic - F F po1 1089 * 27 001b.54c2.2644 dynamic - F F po1 1090 * 27 0000.0c9f.f2bc dynamic - F F po1 1091 * 27 0026.980a.df44 dynamic - F F po1 1092 * 16 0050.56bb.0164 dynamic - F F po2 1093 * 13 90e2.ba5a.9f30 dynamic - F F eth1/2 1094 * 13 90e2.ba4b.fc78 dynamic - F F eth1/1 1095 39 0100.5e00.4b4b igmp 0 F F Po1 Po2 Po22 1096 110 0100.5e00.0118 igmp 0 F F Po1 Po2 1097 Eth142/1/3 Eth112/1/5 1098 Eth112/1/6 Eth122/1/5 1099 1100 """ 1101 1102 # The '*' is stripped out later 1103 RE_MACTABLE_FORMAT1 = r"^\s+{}\s+{}\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+".format( 1104 VLAN_REGEX, MAC_REGEX 1105 ) 1106 RE_MACTABLE_FORMAT2 = r"^\s+{}\s+{}\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+".format( 1107 "-", MAC_REGEX 1108 ) 1109 # REGEX dedicated for lines with only interfaces (suite of the previous MAC address) 1110 RE_MACTABLE_FORMAT3 = r"^\s+\S+" 1111 1112 mac_address_table = [] 1113 command = "show mac address-table" 1114 output = self._send_command(command) 1115 1116 def remove_prefix(s, prefix): 1117 return s[len(prefix) :] if s.startswith(prefix) else s 1118 1119 def process_mac_fields(vlan, mac, mac_type, interface): 1120 """Return proper data for mac address fields.""" 1121 if mac_type.lower() in ["self", "static", "system"]: 1122 static = True 1123 if vlan.lower() == "all": 1124 vlan = 0 1125 elif vlan == "-": 1126 vlan = 0 1127 if ( 1128 interface.lower() == "cpu" 1129 or re.search(r"router", interface.lower()) 1130 or re.search(r"switch", interface.lower()) 1131 ): 1132 interface = "" 1133 else: 1134 static = False 1135 if mac_type.lower() in ["dynamic"]: 1136 active = True 1137 else: 1138 active = False 1139 return { 1140 "mac": helpers.mac(mac), 1141 "interface": helpers.canonical_interface_name(interface), 1142 "vlan": int(vlan), 1143 "static": static, 1144 "active": active, 1145 "moves": -1, 1146 "last_move": -1.0, 1147 } 1148 1149 # Skip the header lines 1150 output = re.split(r"^----.*", output, flags=re.M)[1:] 1151 output = "\n".join(output).strip() 1152 # Strip any leading characters 1153 output = re.sub(r"^[\*\+GOCE]", "", output, flags=re.M) 1154 output = re.sub(r"^\(R\)", "", output, flags=re.M) 1155 output = re.sub(r"^\(T\)", "", output, flags=re.M) 1156 output = re.sub(r"^\(F\)", "", output, flags=re.M) 1157 output = re.sub(r"vPC Peer-Link", "vPC-Peer-Link", output, flags=re.M) 1158 1159 for line in output.splitlines(): 1160 1161 # Every 500 Mac's Legend is reprinted, regardless of terminal length 1162 if re.search(r"^Legend", line): 1163 continue 1164 elif re.search(r"^\s+\* \- primary entry", line): 1165 continue 1166 elif re.search(r"^\s+age \-", line): 1167 continue 1168 elif re.search(r"^\s+VLAN", line): 1169 continue 1170 elif re.search(r"^------", line): 1171 continue 1172 elif re.search(r"^\s*$", line): 1173 continue 1174 1175 for pattern in [ 1176 RE_MACTABLE_FORMAT1, 1177 RE_MACTABLE_FORMAT2, 1178 RE_MACTABLE_FORMAT3, 1179 ]: 1180 if re.search(pattern, line): 1181 fields = line.split() 1182 if len(fields) >= 7: 1183 vlan, mac, mac_type, _, _, _, interface = fields[:7] 1184 mac_address_table.append( 1185 process_mac_fields(vlan, mac, mac_type, interface) 1186 ) 1187 1188 # there can be multiples interfaces for the same MAC on the same line 1189 for interface in fields[7:]: 1190 mac_address_table.append( 1191 process_mac_fields(vlan, mac, mac_type, interface) 1192 ) 1193 break 1194 1195 # interfaces can overhang to the next line (line only contains interfaces) 1196 elif len(fields) < 7: 1197 for interface in fields: 1198 mac_address_table.append( 1199 process_mac_fields(vlan, mac, mac_type, interface) 1200 ) 1201 break 1202 else: 1203 raise ValueError("Unexpected output from: {}".format(repr(line))) 1204 1205 return mac_address_table 1206 1207 def _get_bgp_route_attr(self, destination, vrf, next_hop, ip_version=4): 1208 """ 1209 BGP protocol attributes for get_route_tp 1210 Only IPv4 supported 1211 """ 1212 1213 CMD_SHIBNV = 'show ip bgp neighbors vrf {vrf} | include "is {neigh}"' 1214 1215 search_re_dict = { 1216 "aspath": { 1217 "re": r"AS-Path: ([\d\(\)]([\d\(\) ])*)", 1218 "group": 1, 1219 "default": "", 1220 }, 1221 "bgpnh": { 1222 "re": r"[^|\\n][ ]{4}(" + IP_ADDR_REGEX + r")", 1223 "group": 1, 1224 "default": "", 1225 }, 1226 "bgpfrom": { 1227 "re": r"from (" + IP_ADDR_REGEX + r")", 1228 "group": 1, 1229 "default": "", 1230 }, 1231 "bgpcomm": { 1232 "re": r" Community: ([\w\d\-\: ]+)", 1233 "group": 1, 1234 "default": "", 1235 }, 1236 "bgplp": {"re": r"localpref (\d+)", "group": 1, "default": ""}, 1237 # external, internal, redist 1238 "bgpie": {"re": r"^: (\w+),", "group": 1, "default": ""}, 1239 "vrfimp": { 1240 "re": r"Imported from [\S]+ \(VRF (\S+)\)", 1241 "group": 1, 1242 "default": "", 1243 }, 1244 } 1245 1246 bgp_attr = {} 1247 # get BGP AS number 1248 outbgp = self._send_command('show bgp process | include "BGP Protocol Tag"') 1249 matchbgpattr = RE_BGP_PROTO_TAG.match(outbgp) 1250 if not matchbgpattr: 1251 return bgp_attr 1252 bgpas = matchbgpattr.group(1) 1253 if ip_version == 4: 1254 bgpcmd = "show ip bgp vrf {vrf} {destination}".format( 1255 vrf=vrf, destination=destination 1256 ) 1257 outbgp = self._send_command(bgpcmd) 1258 outbgpsec = outbgp.split("Path type") 1259 1260 # this should not happen (zero BGP paths)... 1261 if len(outbgpsec) == 1: 1262 return bgp_attr 1263 1264 # process all bgp paths 1265 for bgppath in outbgpsec[1:]: 1266 if "is best path" not in bgppath: 1267 # only best path is added to protocol attributes 1268 continue 1269 # find BGP attributes 1270 for key in search_re_dict: 1271 matchre = re.search(search_re_dict[key]["re"], bgppath) 1272 if matchre: 1273 groupnr = int(search_re_dict[key]["group"]) 1274 search_re_dict[key]["result"] = matchre.group(groupnr) 1275 else: 1276 search_re_dict[key]["result"] = search_re_dict[key]["default"] 1277 bgpnh = search_re_dict["bgpnh"]["result"] 1278 1279 # if route is not leaked next hops have to match 1280 if ( 1281 not (search_re_dict["bgpie"]["result"] in ["redist", "local"]) 1282 ) and (bgpnh != next_hop): 1283 # this is not the right route 1284 continue 1285 # find remote AS nr. of this neighbor 1286 bgpcmd = CMD_SHIBNV.format(vrf=vrf, neigh=bgpnh) 1287 outbgpnei = self._send_command(bgpcmd) 1288 matchbgpras = RE_BGP_REMOTE_AS.search(outbgpnei) 1289 if matchbgpras: 1290 bgpras = matchbgpras.group(1) 1291 else: 1292 # next-hop is not known in this vrf, route leaked from 1293 # other vrf or from vpnv4 table? 1294 # get remote AS nr. from as-path if it is ebgp neighbor 1295 # if locally sourced remote AS if undefined 1296 bgpie = search_re_dict["bgpie"]["result"] 1297 if bgpie == "external": 1298 bgpras = bgpie.split(" ")[0].replace("(", "") 1299 elif bgpie == "internal": 1300 bgpras = bgpas 1301 else: # redist, local 1302 bgpras = "" 1303 # community 1304 bothcomm = [] 1305 extcomm = [] 1306 stdcomm = search_re_dict["bgpcomm"]["result"].split() 1307 commsplit = bgppath.split("Extcommunity:") 1308 if len(commsplit) == 2: 1309 for line in commsplit[1].split("\n")[1:]: 1310 # RT:65004:22 1311 matchcommun = RE_BGP_COMMUN.match(line) 1312 if matchcommun: 1313 extcomm.append(matchcommun.group(1)) 1314 else: 1315 # we've reached end of the extended community section 1316 break 1317 bothcomm = stdcomm + extcomm 1318 bgp_attr = { 1319 "as_path": search_re_dict["aspath"]["result"].strip(), 1320 "remote_address": search_re_dict["bgpfrom"]["result"], 1321 "local_preference": int(search_re_dict["bgplp"]["result"]), 1322 "communities": bothcomm, 1323 "local_as": helpers.as_number(bgpas), 1324 } 1325 if bgpras: 1326 bgp_attr["remote_as"] = helpers.as_number(bgpras) 1327 else: 1328 bgp_attr["remote_as"] = 0 # 0? , locally sourced 1329 return bgp_attr 1330 1331 def get_route_to(self, destination="", protocol="", longer=False): 1332 """ 1333 Only IPv4 supported, vrf aware, longer_prefixes parameter ready 1334 """ 1335 if longer: 1336 raise NotImplementedError("Longer prefixes not yet supported for NXOS") 1337 longer_pref = "" # longer_prefixes support, for future use 1338 vrf = "" 1339 1340 ip_version = None 1341 try: 1342 ip_version = IPNetwork(destination).version 1343 except AddrFormatError: 1344 return "Please specify a valid destination!" 1345 if ip_version == 4: # process IPv4 routing table 1346 routes = {} 1347 if vrf: 1348 send_cmd = "show ip route vrf {vrf} {destination} {longer}".format( 1349 vrf=vrf, destination=destination, longer=longer_pref 1350 ).rstrip() 1351 else: 1352 send_cmd = "show ip route vrf all {destination} {longer}".format( 1353 destination=destination, longer=longer_pref 1354 ).rstrip() 1355 out_sh_ip_rou = self._send_command(send_cmd) 1356 # IP Route Table for VRF "TEST" 1357 for vrfsec in out_sh_ip_rou.split("IP Route Table for ")[1:]: 1358 if "Route not found" in vrfsec: 1359 continue 1360 vrffound = False 1361 preffound = False 1362 nh_list = [] 1363 cur_prefix = "" 1364 for line in vrfsec.split("\n"): 1365 if not vrffound: 1366 vrfstr = RE_RT_VRF_NAME.match(line) 1367 if vrfstr: 1368 curvrf = vrfstr.group(1) 1369 vrffound = True 1370 else: 1371 # 10.10.56.0/24, ubest/mbest: 2/0 1372 prefstr = RE_RT_IPV4_ROUTE_PREF.match(line) 1373 if prefstr: 1374 if preffound: # precess previous prefix 1375 if cur_prefix not in routes: 1376 routes[cur_prefix] = [] 1377 for nh in nh_list: 1378 routes[cur_prefix].append(nh) 1379 nh_list = [] 1380 else: 1381 preffound = True 1382 cur_prefix = prefstr.group(1) 1383 continue 1384 # *via 10.2.49.60, Vlan3013, [0/0], 1y18w, direct 1385 # via 10.17.205.132, Po77.3602, [110/20], 1y18w, ospf-1000, 1386 # type-2, tag 2112 1387 # *via 10.17.207.42, Eth3/7.212, [110/20], 02:19:36, ospf-1000, type-2, 1388 # tag 2121 1389 # *via 10.17.207.73, [1/0], 1y18w, static 1390 # *via 10.17.209.132%vrf2, Po87.3606, [20/20], 1y25w, bgp-65000, 1391 # external, tag 65000 1392 # *via Vlan596, [1/0], 1y18w, static 1393 viastr = RE_IP_ROUTE_VIA_REGEX.match(line) 1394 if viastr: 1395 nh_used = viastr.group("used") == "*" 1396 nh_ip = viastr.group("ip") or "" 1397 # when next hop is leaked from other vrf, for future use 1398 # nh_vrf = viastr.group('vrf') 1399 nh_int = viastr.group("int") 1400 nh_metric = viastr.group("metric") 1401 nh_age = bgp_time_conversion(viastr.group("age")) 1402 nh_source = viastr.group("source") 1403 # for future use 1404 # rest_of_line = viastr.group('rest') 1405 # use only routes from specified protocol 1406 if protocol and protocol != nh_source: 1407 continue 1408 # routing protocol process number, for future use 1409 # nh_source_proc_nr = viastr.group('procnr) 1410 if nh_int: 1411 nh_int_canon = helpers.canonical_interface_name(nh_int) 1412 else: 1413 nh_int_canon = "" 1414 route_entry = { 1415 "protocol": nh_source, 1416 "outgoing_interface": nh_int_canon, 1417 "age": nh_age, 1418 "current_active": nh_used, 1419 "routing_table": curvrf, 1420 "last_active": nh_used, 1421 "next_hop": nh_ip, 1422 "selected_next_hop": nh_used, 1423 "inactive_reason": "", 1424 "preference": int(nh_metric), 1425 } 1426 if nh_source == "bgp": 1427 route_entry[ 1428 "protocol_attributes" 1429 ] = self._get_bgp_route_attr(cur_prefix, curvrf, nh_ip) 1430 else: 1431 route_entry["protocol_attributes"] = {} 1432 nh_list.append(route_entry) 1433 # process last next hop entries 1434 if preffound: 1435 if cur_prefix not in routes: 1436 routes[cur_prefix] = [] 1437 for nh in nh_list: 1438 routes[cur_prefix].append(nh) 1439 return routes 1440 1441 def get_snmp_information(self): 1442 snmp_information = {} 1443 command = "show running-config" 1444 output = self._send_command(command) 1445 snmp_config = helpers.textfsm_extractor(self, "snmp_config", output) 1446 1447 if not snmp_config: 1448 return snmp_information 1449 1450 snmp_information = { 1451 "contact": str(""), 1452 "location": str(""), 1453 "community": {}, 1454 "chassis_id": str(""), 1455 } 1456 1457 for snmp_entry in snmp_config: 1458 contact = str(snmp_entry.get("contact", "")) 1459 if contact: 1460 snmp_information["contact"] = contact 1461 location = str(snmp_entry.get("location", "")) 1462 if location: 1463 snmp_information["location"] = location 1464 1465 community_name = str(snmp_entry.get("community", "")) 1466 if not community_name: 1467 continue 1468 1469 if community_name not in snmp_information["community"].keys(): 1470 snmp_information["community"][community_name] = { 1471 "acl": str(snmp_entry.get("acl", "")), 1472 "mode": str(snmp_entry.get("mode", "").lower()), 1473 } 1474 else: 1475 acl = str(snmp_entry.get("acl", "")) 1476 if acl: 1477 snmp_information["community"][community_name]["acl"] = acl 1478 mode = str(snmp_entry.get("mode", "").lower()) 1479 if mode: 1480 snmp_information["community"][community_name]["mode"] = mode 1481 return snmp_information 1482 1483 def get_users(self): 1484 _CISCO_TO_CISCO_MAP = {"network-admin": 15, "network-operator": 5} 1485 1486 _DEFAULT_USER_DICT = {"password": "", "level": 0, "sshkeys": []} 1487 1488 users = {} 1489 command = "show running-config" 1490 output = self._send_command(command) 1491 section_username_tabled_output = helpers.textfsm_extractor( 1492 self, "users", output 1493 ) 1494 1495 for user in section_username_tabled_output: 1496 username = user.get("username", "") 1497 if not username: 1498 continue 1499 if username not in users: 1500 users[username] = _DEFAULT_USER_DICT.copy() 1501 1502 password = user.get("password", "") 1503 if password: 1504 users[username]["password"] = str(password.strip()) 1505 1506 level = 0 1507 role = user.get("role", "") 1508 if role.startswith("priv"): 1509 level = int(role.split("-")[-1]) 1510 else: 1511 level = _CISCO_TO_CISCO_MAP.get(role, 0) 1512 if level > users.get(username).get("level"): 1513 # unfortunately on Cisco you can set different priv levels for the same user 1514 # Good news though: the device will consider the highest level 1515 users[username]["level"] = level 1516 1517 sshkeytype = user.get("sshkeytype", "") 1518 sshkeyvalue = user.get("sshkeyvalue", "") 1519 if sshkeytype and sshkeyvalue: 1520 if sshkeytype not in ["ssh-rsa", "ssh-dsa"]: 1521 continue 1522 users[username]["sshkeys"].append(str(sshkeyvalue)) 1523 return users 1524 1525 def get_vlans(self): 1526 vlans = {} 1527 command = "show vlan brief | json" 1528 vlan_table_raw = self._get_command_table( 1529 command, "TABLE_vlanbriefxbrief", "ROW_vlanbriefxbrief" 1530 ) 1531 if isinstance(vlan_table_raw, dict): 1532 vlan_table_raw = [vlan_table_raw] 1533 1534 for vlan in vlan_table_raw: 1535 if "vlanshowplist-ifidx" not in vlan.keys(): 1536 vlan["vlanshowplist-ifidx"] = [] 1537 vlans[vlan["vlanshowbr-vlanid"]] = { 1538 "name": vlan["vlanshowbr-vlanname"], 1539 "interfaces": self._parse_vlan_ports(vlan["vlanshowplist-ifidx"]), 1540 } 1541 return vlans 1542 1543 def get_optics(self): 1544 command = "show interface transceiver details" 1545 output = self._send_command(command) 1546 1547 # Formatting data into return data structure 1548 optics_detail = {} 1549 1550 # Extraction Regexps 1551 port_ts_re = re.compile(r"^Ether.*?(?=\nEther|\Z)", re.M | re.DOTALL) 1552 port_re = re.compile(r"^(Ether.*)[ ]*?$", re.M) 1553 vendor_re = re.compile("name is (.*)$", re.M) 1554 vendor_part_re = re.compile("part number is (.*)$", re.M) 1555 vendor_rev_re = re.compile("revision is (.*)$", re.M) 1556 serial_no_re = re.compile("serial number is (.*)$", re.M) 1557 type_no_re = re.compile("type is (.*)$", re.M) 1558 rx_instant_re = re.compile(r"Rx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M) 1559 tx_instant_re = re.compile(r"Tx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M) 1560 current_instant_re = re.compile(r"Current[ ]+(?:(\S+?)[ ]+mA|(N.A))", re.M) 1561 1562 port_ts_l = port_ts_re.findall(output) 1563 1564 for port_ts in port_ts_l: 1565 port = port_re.search(port_ts).group(1) 1566 # No transceiver is present in those case 1567 if "transceiver is not present" in port_ts: 1568 continue 1569 if "transceiver is not applicable" in port_ts: 1570 continue 1571 port_detail = {"physical_channels": {"channel": []}} 1572 # No metric present 1573 vendor = vendor_re.search(port_ts).group(1) 1574 vendor_part = vendor_part_re.search(port_ts).group(1) 1575 vendor_rev = vendor_rev_re.search(port_ts).group(1) 1576 serial_no = serial_no_re.search(port_ts).group(1) 1577 type_s = type_no_re.search(port_ts).group(1) 1578 state = { 1579 "vendor": vendor.strip(), 1580 "vendor_part": vendor_part.strip(), 1581 "vendor_rev": vendor_rev.strip(), 1582 "serial_no": serial_no.strip(), 1583 "connector_type": self.connector_type_map.get(type_s, "Unknown"), 1584 } 1585 if "DOM is not supported" not in port_ts: 1586 res = rx_instant_re.search(port_ts) 1587 input_power = res.group(1) or res.group(2) 1588 res = tx_instant_re.search(port_ts) 1589 output_power = res.group(1) or res.group(2) 1590 res = current_instant_re.search(port_ts) 1591 current = res.group(1) or res.group(2) 1592 1593 # If interface is shutdown it returns "N/A" as output power 1594 # or "N/A" as input power 1595 # Converting that to -100.0 float 1596 try: 1597 float(output_power) 1598 except ValueError: 1599 output_power = -100.0 1600 try: 1601 float(input_power) 1602 except ValueError: 1603 input_power = -100.0 1604 try: 1605 float(current) 1606 except ValueError: 1607 current = -100.0 1608 1609 # Defaulting avg, min, max values to -100.0 since device does not 1610 # return these values 1611 optic_states = { 1612 "index": 0, 1613 "state": { 1614 "input_power": { 1615 "instant": ( 1616 float(input_power) if "input_power" else -100.0 1617 ), 1618 "avg": -100.0, 1619 "min": -100.0, 1620 "max": -100.0, 1621 }, 1622 "output_power": { 1623 "instant": ( 1624 float(output_power) if "output_power" else -100.0 1625 ), 1626 "avg": -100.0, 1627 "min": -100.0, 1628 "max": -100.0, 1629 }, 1630 "laser_bias_current": { 1631 "instant": (float(current) if "current" else -100.0), 1632 "avg": 0.0, 1633 "min": 0.0, 1634 "max": 0.0, 1635 }, 1636 }, 1637 } 1638 port_detail["physical_channels"]["channel"].append(optic_states) 1639 1640 port_detail["state"] = state 1641 optics_detail[port] = port_detail 1642 1643 return optics_detail 1644 1645 def get_interfaces_counters(self): 1646 """ 1647 Return interface counters and errors. 1648 1649 'tx_errors': int, 1650 'rx_errors': int, 1651 'tx_discards': int, 1652 'rx_discards': int, 1653 'tx_octets': int, 1654 'rx_octets': int, 1655 'tx_unicast_packets': int, 1656 'rx_unicast_packets': int, 1657 'tx_multicast_packets': int, 1658 'rx_multicast_packets': int, 1659 'tx_broadcast_packets': int, 1660 'rx_broadcast_packets': int, 1661 """ 1662 if_mapping = { 1663 "eth": { 1664 "regexp": re.compile("^(Ether|port-channel).*"), 1665 "mapping": { 1666 "tx_errors": "eth_outerr", 1667 "rx_errors": "eth_inerr", 1668 "tx_discards": "eth_outdiscard", 1669 "rx_discards": "eth_indiscard", 1670 "tx_octets": "eth_outbytes", 1671 "rx_octets": "eth_inbytes", 1672 "tx_unicast_packets": "eth_outucast", 1673 "rx_unicast_packets": "eth_inucast", 1674 "tx_multicast_packets": "eth_outmcast", 1675 "rx_multicast_packets": "eth_inmcast", 1676 "tx_broadcast_packets": "eth_outbcast", 1677 "rx_broadcast_packets": "eth_inbcast", 1678 }, 1679 }, 1680 "mgmt": { 1681 "regexp": re.compile("mgm.*"), 1682 "mapping": { 1683 "tx_errors": None, 1684 "rx_errors": None, 1685 "tx_discards": None, 1686 "rx_discards": None, 1687 "tx_octets": "mgmt_out_bytes", 1688 "rx_octets": "mgmt_in_bytes", 1689 "tx_unicast_packets": None, 1690 "rx_unicast_packets": None, 1691 "tx_multicast_packets": "mgmt_out_mcast", 1692 "rx_multicast_packets": "mgmt_in_mcast", 1693 "tx_broadcast_packets": None, 1694 "rx_broadcast_packets": None, 1695 }, 1696 }, 1697 } 1698 command = "show interface counters detailed | json" 1699 # To retrieve discards 1700 command_interface = "show interface | json" 1701 counters_table_raw = self._get_command_table( 1702 command, "TABLE_interface", "ROW_interface" 1703 ) 1704 counters_interface_table_raw = self._get_command_table( 1705 command_interface, "TABLE_interface", "ROW_interface" 1706 ) 1707 all_stats_d = {} 1708 # Start with show interface as all interfaces 1709 # Are surely listed 1710 for row in counters_interface_table_raw: 1711 if_counter = {} 1712 # loop through regexp to find mapping 1713 for if_v in if_mapping: 1714 my_re = if_mapping[if_v]["regexp"] 1715 re_match = my_re.match(row["interface"]) 1716 if re_match: 1717 interface = re_match.group() 1718 map_d = if_mapping[if_v]["mapping"] 1719 for k, v in map_d.items(): 1720 if_counter[k] = int(row[v]) if v in row else 0 1721 all_stats_d[interface] = if_counter 1722 break 1723 1724 for row in counters_table_raw: 1725 if_counter = {} 1726 # loop through regexp to find mapping 1727 for if_v in if_mapping: 1728 my_re = if_mapping[if_v]["regexp"] 1729 re_match = my_re.match(row["interface"]) 1730 if re_match: 1731 interface = re_match.group() 1732 map_d = if_mapping[if_v]["mapping"] 1733 for k, v in map_d.items(): 1734 if v in row: 1735 if_counter[k] = int(row[v]) 1736 all_stats_d[interface].update(if_counter) 1737 break 1738 1739 return all_stats_d 1740