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"""Driver for JunOS devices.""" 17 18# import stdlib 19import re 20import json 21import logging 22import collections 23from copy import deepcopy 24from collections import OrderedDict, defaultdict 25 26# import third party lib 27from lxml.builder import E 28from lxml import etree 29 30from jnpr.junos import Device 31from jnpr.junos.utils.config import Config 32from jnpr.junos.exception import RpcError 33from jnpr.junos.exception import ConfigLoadError 34from jnpr.junos.exception import RpcTimeoutError 35from jnpr.junos.exception import ConnectTimeoutError 36from jnpr.junos.exception import ProbeError 37from jnpr.junos.exception import LockError as JnprLockError 38from jnpr.junos.exception import UnlockError as JnrpUnlockError 39 40# import NAPALM Base 41import napalm.base.helpers 42from napalm.base.base import NetworkDriver 43from napalm.junos import constants as C 44from napalm.base.exceptions import ConnectionException 45from napalm.base.exceptions import MergeConfigException 46from napalm.base.exceptions import CommandErrorException 47from napalm.base.exceptions import ReplaceConfigException 48from napalm.base.exceptions import CommandTimeoutException 49from napalm.base.exceptions import LockError 50from napalm.base.exceptions import UnlockError 51from napalm.base.exceptions import CommitConfirmException 52 53# import local modules 54from napalm.junos.utils import junos_views 55 56log = logging.getLogger(__file__) 57 58 59class JunOSDriver(NetworkDriver): 60 """JunOSDriver class - inherits NetworkDriver from napalm.base.""" 61 62 def __init__(self, hostname, username, password, timeout=60, optional_args=None): 63 """ 64 Initialise JunOS driver. 65 66 Optional args: 67 * config_lock (True/False): lock configuration DB after the connection is established. 68 * lock_disable (True/False): force configuration lock to be disabled (for external lock 69 management). 70 * config_private (True/False): juniper configure private command, no DB locking 71 * port (int): custom port 72 * key_file (string): SSH key file path 73 * keepalive (int): Keepalive interval 74 * ignore_warning (boolean): not generate warning exceptions 75 """ 76 self.hostname = hostname 77 self.username = username 78 self.password = password 79 self.timeout = timeout 80 self.config_replace = False 81 self.locked = False 82 83 # Get optional arguments 84 if optional_args is None: 85 optional_args = {} 86 87 self.port = optional_args.get("port", 22) 88 self.key_file = optional_args.get("key_file", None) 89 self.keepalive = optional_args.get("keepalive", 30) 90 self.ssh_config_file = optional_args.get("ssh_config_file", None) 91 self.ignore_warning = optional_args.get("ignore_warning", False) 92 self.auto_probe = optional_args.get("auto_probe", 0) 93 94 # Define locking method 95 self.lock_disable = optional_args.get("lock_disable", False) 96 self.session_config_lock = optional_args.get("config_lock", False) 97 self.config_private = optional_args.get("config_private", False) 98 99 # Junos driver specific options 100 self.junos_config_database = optional_args.get( 101 "junos_config_database", "committed" 102 ) 103 self.junos_config_inheritance = optional_args.get( 104 "junos_config_inherit", "inherit" 105 ) 106 self.junos_config_groups = optional_args.get("junos_config_groups", "groups") 107 self.junos_config_options = { 108 "database": self.junos_config_database, 109 "inherit": self.junos_config_inheritance, 110 "groups": self.junos_config_groups, 111 } 112 self.junos_config_options = optional_args.get( 113 "junos_config_options", self.junos_config_options 114 ) 115 116 if self.key_file: 117 self.device = Device( 118 hostname, 119 user=username, 120 password=password, 121 ssh_private_key_file=self.key_file, 122 ssh_config=self.ssh_config_file, 123 port=self.port, 124 ) 125 else: 126 self.device = Device( 127 hostname, 128 user=username, 129 password=password, 130 port=self.port, 131 ssh_config=self.ssh_config_file, 132 ) 133 134 self.platform = "junos" 135 self.profile = [self.platform] 136 137 def open(self): 138 """Open the connection with the device.""" 139 try: 140 self.device.open(auto_probe=self.auto_probe) 141 except (ConnectTimeoutError, ProbeError) as cte: 142 raise ConnectionException(cte.msg) from cte 143 self.device.timeout = self.timeout 144 self.device._conn._session.transport.set_keepalive(self.keepalive) 145 if hasattr(self.device, "cu"): 146 # make sure to remove the cu attr from previous session 147 # ValueError: requested attribute name cu already exists 148 del self.device.cu 149 self.device.bind(cu=Config) 150 if not self.lock_disable and self.session_config_lock: 151 self._lock() 152 153 def close(self): 154 """Close the connection.""" 155 if not self.lock_disable and self.session_config_lock: 156 self._unlock() 157 self.device.close() 158 159 def _lock(self): 160 """Lock the config DB.""" 161 if not self.locked: 162 try: 163 self.device.cu.lock() 164 self.locked = True 165 except JnprLockError as jle: 166 raise LockError(str(jle)) 167 168 def _unlock(self): 169 """Unlock the config DB.""" 170 if self.locked: 171 try: 172 self.device.cu.unlock() 173 self.locked = False 174 except JnrpUnlockError as jue: 175 raise UnlockError(jue) 176 177 def _rpc(self, get, child=None, **kwargs): 178 """ 179 This allows you to construct an arbitrary RPC call to retreive common stuff. For example: 180 Configuration: get: "<get-configuration/>" 181 Interface information: get: "<get-interface-information/>" 182 A particular interfacece information: 183 get: "<get-interface-information/>" 184 child: "<interface-name>ge-0/0/0</interface-name>" 185 """ 186 rpc = etree.fromstring(get) 187 188 if child: 189 rpc.append(etree.fromstring(child)) 190 191 response = self.device.execute(rpc) 192 return etree.tostring(response) 193 194 def is_alive(self): 195 # evaluate the state of the underlying SSH connection 196 # and also the NETCONF status from PyEZ 197 return { 198 "is_alive": self.device._conn._session.transport.is_active() 199 and self.device.connected 200 } 201 202 @staticmethod 203 def _is_json_format(config): 204 try: 205 _ = json.loads(config) # noqa 206 except (TypeError, ValueError): 207 return False 208 return True 209 210 def _detect_config_format(self, config): 211 fmt = "text" 212 set_action_matches = [ 213 "set", 214 "activate", 215 "deactivate", 216 "annotate", 217 "copy", 218 "delete", 219 "insert", 220 "protect", 221 "rename", 222 "unprotect", 223 "edit", 224 "top", 225 "wildcard", 226 ] 227 if config.strip().startswith("<"): 228 return "xml" 229 elif config.strip().split(" ")[0] in set_action_matches: 230 return "set" 231 elif self._is_json_format(config): 232 return "json" 233 return fmt 234 235 def _load_candidate(self, filename, config, overwrite): 236 if filename is None: 237 configuration = config 238 else: 239 with open(filename) as f: 240 configuration = f.read() 241 242 if ( 243 not self.lock_disable 244 and not self.session_config_lock 245 and not self.config_private 246 ): 247 # if not locked during connection time, will try to lock 248 self._lock() 249 250 try: 251 fmt = self._detect_config_format(configuration) 252 253 if fmt == "xml": 254 configuration = etree.XML(configuration) 255 256 if self.config_private: 257 try: 258 self.device.rpc.open_configuration(private=True, normalize=True) 259 except RpcError as err: 260 if str(err) == "uncommitted changes will be discarded on exit": 261 pass 262 263 self.device.cu.load( 264 configuration, 265 format=fmt, 266 overwrite=overwrite, 267 ignore_warning=self.ignore_warning, 268 ) 269 except ConfigLoadError as e: 270 if self.config_replace: 271 raise ReplaceConfigException(e.errs) 272 else: 273 raise MergeConfigException(e.errs) 274 275 def load_replace_candidate(self, filename=None, config=None): 276 """Open the candidate config and merge.""" 277 self.config_replace = True 278 self._load_candidate(filename, config, True) 279 280 def load_merge_candidate(self, filename=None, config=None): 281 """Open the candidate config and replace.""" 282 self.config_replace = False 283 self._load_candidate(filename, config, False) 284 285 def compare_config(self): 286 """Compare candidate config with running.""" 287 diff = self.device.cu.diff() 288 289 if diff is None: 290 return "" 291 else: 292 return diff.strip() 293 294 def commit_config(self, message="", revert_in=None): 295 """Commit configuration.""" 296 commit_args = {} 297 if revert_in is not None: 298 if revert_in % 60 != 0: 299 if not self.lock_disable and not self.session_config_lock: 300 self._unlock() 301 raise CommitConfirmException( 302 "For Junos devices revert_in must be a multiple of 60 (60, 120, 180...)" 303 ) 304 else: 305 juniper_confirm_time = int(revert_in / 60) 306 commit_args["confirm"] = juniper_confirm_time 307 308 if message: 309 commit_args["comment"] = message 310 self.device.cu.commit(ignore_warning=self.ignore_warning, **commit_args) 311 312 if not self.lock_disable and not self.session_config_lock: 313 self._unlock() 314 315 if self.config_private: 316 self.device.rpc.close_configuration() 317 318 def has_pending_commit(self): 319 """Boolean indicating if there is a commit-confirm in process.""" 320 pending_commit = self._get_pending_commits() 321 if pending_commit: 322 return True 323 else: 324 return False 325 326 def _get_pending_commits(self): 327 """ 328 Return a dictionary of commit sequences with pending commit confirms and 329 corresponding time when confirm needs to happen by. This is converted to seconds 330 since Juniper reports this in minutes. 331 332 Example: 333 {'re0-1616554286-559': 522} 334 335 Will only report on a single commit (the most recent one). 336 337 Will return an empty dictionary if there is no pending commit-confirms. 338 """ 339 # show system commit revision detail 340 # Command introduced in Junos OS Release 14.1 341 try: 342 pending_commit = self.device.rpc.get_commit_revision_information( 343 detail=True 344 ) 345 except RpcError: 346 msg = "Using commit-confirm with NAPALM requires Junos OS >= 14.1" 347 raise CommitConfirmException(msg) 348 349 commit_time_element = pending_commit.find("./date-time") 350 commit_time = int(commit_time_element.attrib["seconds"]) 351 352 commit_revision_element = pending_commit.find("./revision") 353 commit_revision = commit_revision_element.text 354 commit_comment_element = pending_commit.find("./comment") 355 if commit_comment_element is None: 356 # No commit comment means no commit-confirm 357 return {} 358 else: 359 commit_comment = commit_comment_element.text 360 361 sys_uptime_info = self.device.rpc.get_system_uptime_information() 362 current_time_element = sys_uptime_info.find("./current-time/date-time") 363 current_time = int(current_time_element.attrib["seconds"]) 364 365 # Msg from Jnpr: 'commit confirmed, rollback in 5mins' 366 if "commit confirmed" in commit_comment and "rollback in" in commit_comment: 367 match = re.search(r"rollback in (\d+)mins", commit_comment) 368 if match: 369 confirm_time = match.group(1) 370 confirm_time_seconds = int(confirm_time) * 60 371 elapsed_time = current_time - commit_time 372 confirm_time_remaining = confirm_time_seconds - elapsed_time 373 if confirm_time_remaining <= 0: 374 confirm_time_remaining = 0 375 376 return {commit_revision: confirm_time_remaining} 377 378 return {} 379 380 def confirm_commit(self): 381 """Send final commit to confirm an in-proces commit that requires confirmation.""" 382 self.device.cu.commit(ignore_warning=self.ignore_warning) 383 384 def discard_config(self): 385 """Discard changes (rollback 0).""" 386 self.device.cu.rollback(rb_id=0) 387 if not self.lock_disable and not self.session_config_lock: 388 self._unlock() 389 if self.config_private: 390 self.device.rpc.close_configuration() 391 392 def rollback(self): 393 """Rollback to previous commit.""" 394 self.device.cu.rollback(rb_id=1) 395 self.commit_config() 396 397 def get_facts(self): 398 """Return facts of the device.""" 399 output = self.device.facts 400 401 uptime = self.device.uptime or -1 402 403 interfaces = junos_views.junos_iface_table(self.device) 404 interfaces.get() 405 interface_list = interfaces.keys() 406 407 return { 408 "vendor": "Juniper", 409 "model": str(output["model"]), 410 "serial_number": str(output["serialnumber"]), 411 "os_version": str(output["version"]), 412 "hostname": str(output["hostname"]), 413 "fqdn": str(output["fqdn"]), 414 "uptime": uptime, 415 "interface_list": interface_list, 416 } 417 418 def get_interfaces(self): 419 """Return interfaces details.""" 420 result = {} 421 422 interfaces = junos_views.junos_iface_table(self.device) 423 interfaces.get() 424 interfaces_logical = junos_views.junos_logical_iface_table(self.device) 425 interfaces_logical.get() 426 427 # convert all the tuples to our pre-defined dict structure 428 def _convert_to_dict(interfaces): 429 # calling .items() here wont work. 430 # The dictionary values will end up being tuples instead of dictionaries 431 interfaces = dict(interfaces) 432 for iface, iface_data in interfaces.items(): 433 result[iface] = { 434 "is_up": iface_data["is_up"], 435 # For physical interfaces <admin-status> will always be there, so just 436 # return the value interfaces[iface]['is_enabled'] 437 # For logical interfaces if <iff-down> is present interface is disabled, 438 # otherwise interface is enabled 439 "is_enabled": ( 440 True 441 if iface_data["is_enabled"] is None 442 else iface_data["is_enabled"] 443 ), 444 "description": (iface_data["description"] or ""), 445 "last_flapped": float((iface_data["last_flapped"] or -1)), 446 "mac_address": napalm.base.helpers.convert( 447 napalm.base.helpers.mac, 448 iface_data["mac_address"], 449 str(iface_data["mac_address"]), 450 ), 451 "speed": -1, 452 "mtu": 0, 453 } 454 # result[iface]['last_flapped'] = float(result[iface]['last_flapped']) 455 456 match_mtu = re.search(r"(\w+)", str(iface_data["mtu"]) or "") 457 mtu = napalm.base.helpers.convert(int, match_mtu.group(0), 0) 458 result[iface]["mtu"] = mtu 459 match = re.search(r"(\d+|[Aa]uto)(\w*)", iface_data["speed"] or "") 460 if match and match.group(1).lower() == "auto": 461 match = re.search( 462 r"(\d+)(\w*)", iface_data["negotiated_speed"] or "" 463 ) 464 if match is None: 465 continue 466 speed_value = napalm.base.helpers.convert(int, match.group(1), -1) 467 if speed_value == -1: 468 continue 469 speed_unit = match.group(2) 470 if speed_unit.lower() == "gbps": 471 speed_value *= 1000 472 result[iface]["speed"] = speed_value 473 474 return result 475 476 result = _convert_to_dict(interfaces) 477 result.update(_convert_to_dict(interfaces_logical)) 478 return result 479 480 def get_interfaces_counters(self): 481 """Return interfaces counters.""" 482 query = junos_views.junos_iface_counter_table(self.device) 483 query.get() 484 interface_counters = {} 485 for interface, counters in query.items(): 486 _interface_counters = {} 487 for k, v in counters: 488 if k == "logical_interfaces": 489 for _interface, _counters in v.items(): 490 interface_counters[_interface] = { 491 k: v if v is not None else -1 for k, v in _counters 492 } 493 else: 494 _interface_counters[k] = v if v is not None else -1 495 496 interface_counters[interface] = _interface_counters 497 return interface_counters 498 499 def get_environment(self): 500 """Return environment details.""" 501 if self.device.facts.get("srx_cluster", False): 502 environment = junos_views.junos_environment_table_srx_cluster(self.device) 503 routing_engine = junos_views.junos_routing_engine_table_srx_cluster( 504 self.device 505 ) 506 temperature_thresholds = ( 507 junos_views.junos_temperature_thresholds_srx_cluster(self.device) 508 ) 509 else: 510 environment = junos_views.junos_environment_table(self.device) 511 routing_engine = junos_views.junos_routing_engine_table(self.device) 512 temperature_thresholds = junos_views.junos_temperature_thresholds( 513 self.device 514 ) 515 power_supplies = junos_views.junos_pem_table(self.device) 516 environment.get() 517 routing_engine.get() 518 temperature_thresholds.get() 519 environment_data = {} 520 current_class = None 521 522 for sensor_object, object_data in environment.items(): 523 structured_object_data = {k: v for k, v in object_data} 524 525 if structured_object_data["class"]: 526 # If current object has a 'class' defined, store it for use 527 # on subsequent unlabeled lines. 528 current_class = structured_object_data["class"] 529 else: 530 # Juniper doesn't label the 2nd+ lines of a given class with a 531 # class name. In that case, we use the most recent class seen. 532 structured_object_data["class"] = current_class 533 534 if structured_object_data["class"] == "Power": 535 # Make sure naming is consistent 536 sensor_object = sensor_object.replace("PEM", "Power Supply") 537 538 # Create a dict for the 'power' key 539 try: 540 environment_data["power"][sensor_object] = {} 541 except KeyError: 542 environment_data["power"] = {} 543 environment_data["power"][sensor_object] = {} 544 545 environment_data["power"][sensor_object]["capacity"] = -1.0 546 environment_data["power"][sensor_object]["output"] = -1.0 547 548 if structured_object_data["class"] == "Fans": 549 # Create a dict for the 'fans' key 550 try: 551 environment_data["fans"][sensor_object] = {} 552 except KeyError: 553 environment_data["fans"] = {} 554 environment_data["fans"][sensor_object] = {} 555 556 status = structured_object_data["status"] 557 env_class = structured_object_data["class"] 558 if status == "OK" and env_class == "Power": 559 # If status is Failed, Absent or Testing, set status to False. 560 environment_data["power"][sensor_object]["status"] = True 561 562 elif status != "OK" and env_class == "Power": 563 environment_data["power"][sensor_object]["status"] = False 564 565 elif status == "OK" and env_class == "Fans": 566 # If status is Failed, Absent or Testing, set status to False. 567 environment_data["fans"][sensor_object]["status"] = True 568 569 elif status != "OK" and env_class == "Fans": 570 environment_data["fans"][sensor_object]["status"] = False 571 572 for temperature_object, temperature_data in temperature_thresholds.items(): 573 structured_temperature_data = {k: v for k, v in temperature_data} 574 if structured_object_data["class"] == "Temp": 575 # Create a dict for the 'temperature' key 576 try: 577 environment_data["temperature"][sensor_object] = {} 578 except KeyError: 579 environment_data["temperature"] = {} 580 environment_data["temperature"][sensor_object] = {} 581 # Check we have a temperature field in this class (See #66) 582 if structured_object_data["temperature"]: 583 environment_data["temperature"][sensor_object][ 584 "temperature" 585 ] = float(structured_object_data["temperature"]) 586 # Set a default value (False) to the key is_critical and is_alert 587 environment_data["temperature"][sensor_object]["is_alert"] = False 588 environment_data["temperature"][sensor_object][ 589 "is_critical" 590 ] = False 591 # Check if the working temperature is equal to or higher than alerting threshold 592 temp = structured_object_data["temperature"] 593 if temp is not None: 594 if structured_temperature_data["red-alarm"] <= temp: 595 environment_data["temperature"][sensor_object][ 596 "is_critical" 597 ] = True 598 environment_data["temperature"][sensor_object][ 599 "is_alert" 600 ] = True 601 elif structured_temperature_data["yellow-alarm"] <= temp: 602 environment_data["temperature"][sensor_object][ 603 "is_alert" 604 ] = True 605 else: 606 environment_data["temperature"][sensor_object][ 607 "temperature" 608 ] = 0.0 609 610 # Try to correct Power Supply information 611 pem_table = dict() 612 try: 613 power_supplies.get() 614 except RpcError: 615 # Not all platforms have support for this 616 pass 617 else: 618 # Format PEM information and correct capacity and output values 619 if "power" not in environment_data.keys(): 620 # Power supplies were not included from the environment table above 621 # Need to initialize data 622 environment_data["power"] = {} 623 for pem in power_supplies.items(): 624 pem_name = pem[0].replace("PEM", "Power Supply") 625 environment_data["power"][pem_name] = {} 626 environment_data["power"][pem_name]["output"] = -1.0 627 environment_data["power"][pem_name]["capacity"] = -1.0 628 environment_data["power"][pem_name]["status"] = False 629 for pem in power_supplies.items(): 630 pem_name = pem[0].replace("PEM", "Power Supply") 631 pem_table[pem_name] = dict(pem[1]) 632 if pem_table[pem_name]["capacity"] is not None: 633 environment_data["power"][pem_name]["capacity"] = pem_table[ 634 pem_name 635 ]["capacity"] 636 if pem_table[pem_name]["output"] is not None: 637 environment_data["power"][pem_name]["output"] = pem_table[pem_name][ 638 "output" 639 ] 640 environment_data["power"][pem_name]["status"] = pem_table[pem_name][ 641 "status" 642 ] 643 644 for routing_engine_object, routing_engine_data in routing_engine.items(): 645 structured_routing_engine_data = {k: v for k, v in routing_engine_data} 646 # Create dicts for 'cpu' and 'memory'. 647 try: 648 environment_data["cpu"][routing_engine_object] = {} 649 environment_data["memory"] = {} 650 except KeyError: 651 environment_data["cpu"] = {} 652 environment_data["cpu"][routing_engine_object] = {} 653 environment_data["memory"] = {} 654 # Calculate the CPU usage by using the CPU idle value. 655 environment_data["cpu"][routing_engine_object]["%usage"] = ( 656 100.0 - structured_routing_engine_data["cpu-idle"] 657 ) 658 try: 659 environment_data["memory"]["available_ram"] = int( 660 structured_routing_engine_data["memory-dram-size"] 661 ) 662 except ValueError: 663 environment_data["memory"]["available_ram"] = int( 664 "".join( 665 i 666 for i in structured_routing_engine_data["memory-dram-size"] 667 if i.isdigit() 668 ) 669 ) 670 if not structured_routing_engine_data["memory-system-total-used"]: 671 # Junos gives us RAM in %, so calculation has to be made. 672 # Sadly, bacause of this, results are not 100% accurate to the truth. 673 environment_data["memory"]["used_ram"] = int( 674 round( 675 environment_data["memory"]["available_ram"] 676 / 100.0 677 * structured_routing_engine_data["memory-buffer-utilization"] 678 ) 679 ) 680 else: 681 environment_data["memory"]["used_ram"] = structured_routing_engine_data[ 682 "memory-system-total-used" 683 ] 684 685 return environment_data 686 687 @staticmethod 688 def _get_address_family(table, instance): 689 """ 690 Function to derive address family from a junos table name. 691 692 :params table: The name of the routing table 693 :returns: address family 694 """ 695 address_family_mapping = {"inet": "ipv4", "inet6": "ipv6", "inetflow": "flow"} 696 if instance == "master": 697 family = table.rsplit(".", 1)[-2] 698 else: 699 family = table.split(".")[-2] 700 try: 701 address_family = address_family_mapping[family] 702 except KeyError: 703 address_family = None 704 return address_family 705 706 def _parse_route_stats(self, neighbor, instance): 707 data = { 708 "ipv4": { 709 "received_prefixes": -1, 710 "accepted_prefixes": -1, 711 "sent_prefixes": -1, 712 }, 713 "ipv6": { 714 "received_prefixes": -1, 715 "accepted_prefixes": -1, 716 "sent_prefixes": -1, 717 }, 718 } 719 if not neighbor["is_up"]: 720 return data 721 elif isinstance(neighbor["tables"], list): 722 if isinstance(neighbor["sent_prefixes"], int): 723 # We expect sent_prefixes to be a list, but sometimes it 724 # is of type int. Therefore convert attribute to list 725 neighbor["sent_prefixes"] = [neighbor["sent_prefixes"]] 726 for idx, table in enumerate(neighbor["tables"]): 727 family = self._get_address_family(table, instance) 728 if family is None: 729 # Need to remove counter from sent_prefixes list anyway 730 if "in sync" in neighbor["send-state"][idx]: 731 neighbor["sent_prefixes"].pop(0) 732 continue 733 data[family] = {} 734 data[family]["received_prefixes"] = neighbor["received_prefixes"][idx] 735 data[family]["accepted_prefixes"] = neighbor["accepted_prefixes"][idx] 736 if "in sync" in neighbor["send-state"][idx]: 737 data[family]["sent_prefixes"] = neighbor["sent_prefixes"].pop(0) 738 else: 739 data[family]["sent_prefixes"] = 0 740 else: 741 family = self._get_address_family(neighbor["tables"], instance) 742 if family is not None: 743 data[family] = {} 744 data[family]["received_prefixes"] = neighbor["received_prefixes"] 745 data[family]["accepted_prefixes"] = neighbor["accepted_prefixes"] 746 data[family]["sent_prefixes"] = neighbor["sent_prefixes"] 747 return data 748 749 @staticmethod 750 def _parse_value(value): 751 if isinstance(value, str): 752 return str(value) 753 elif value is None: 754 return "" 755 else: 756 return value 757 758 def get_bgp_neighbors(self): 759 """Return BGP neighbors details.""" 760 bgp_neighbor_data = {} 761 default_neighbor_details = { 762 "local_as": 0, 763 "remote_as": 0, 764 "remote_id": "", 765 "is_up": False, 766 "is_enabled": False, 767 "description": "", 768 "uptime": 0, 769 "address_family": {}, 770 } 771 keys = default_neighbor_details.keys() 772 773 uptime_table = junos_views.junos_bgp_uptime_table(self.device) 774 bgp_neighbors_table = junos_views.junos_bgp_table(self.device) 775 776 uptime_table_lookup = {} 777 778 def _get_uptime_table(instance): 779 if instance not in uptime_table_lookup: 780 uptime_table_lookup[instance] = uptime_table.get( 781 instance=instance 782 ).items() 783 return uptime_table_lookup[instance] 784 785 def _get_bgp_neighbors_core( 786 neighbor_data, instance=None, uptime_table_items=None 787 ): 788 """ 789 Make sure to execute a simple request whenever using 790 junos > 13. This is a helper used to avoid code redundancy 791 and reuse the function also when iterating through the list 792 BGP neighbors under a specific routing instance, 793 also when the device is capable to return the routing 794 instance name at the BGP neighbor level. 795 """ 796 for bgp_neighbor in neighbor_data: 797 peer_ip = napalm.base.helpers.ip(bgp_neighbor[0].split("+")[0]) 798 neighbor_details = deepcopy(default_neighbor_details) 799 neighbor_details.update( 800 { 801 elem[0]: elem[1] 802 for elem in bgp_neighbor[1] 803 if elem[1] is not None 804 } 805 ) 806 if not instance: 807 # not instance, means newer Junos version, 808 # as we request everything in a single request 809 peer_fwd_rti = neighbor_details.pop("peer_fwd_rti") 810 instance = peer_fwd_rti 811 else: 812 # instance is explicitly requests, 813 # thus it's an old Junos, so we retrieve the BGP neighbors 814 # under a certain routing instance 815 peer_fwd_rti = neighbor_details.pop("peer_fwd_rti", "") 816 instance_name = "global" if instance == "master" else instance 817 if instance_name not in bgp_neighbor_data: 818 bgp_neighbor_data[instance_name] = {} 819 if "router_id" not in bgp_neighbor_data[instance_name]: 820 # we only need to set this once 821 bgp_neighbor_data[instance_name]["router_id"] = str( 822 neighbor_details.get("local_id", "") 823 ) 824 peer = { 825 key: self._parse_value(value) 826 for key, value in neighbor_details.items() 827 if key in keys 828 } 829 peer["local_as"] = napalm.base.helpers.as_number(peer["local_as"]) 830 peer["remote_as"] = napalm.base.helpers.as_number(peer["remote_as"]) 831 peer["address_family"] = self._parse_route_stats( 832 neighbor_details, instance 833 ) 834 if "peers" not in bgp_neighbor_data[instance_name]: 835 bgp_neighbor_data[instance_name]["peers"] = {} 836 bgp_neighbor_data[instance_name]["peers"][peer_ip] = peer 837 if not uptime_table_items: 838 uptime_table_items = _get_uptime_table(instance) 839 for neighbor, uptime in uptime_table_items: 840 normalized_neighbor = napalm.base.helpers.ip(neighbor) 841 if ( 842 normalized_neighbor 843 not in bgp_neighbor_data[instance_name]["peers"] 844 ): 845 bgp_neighbor_data[instance_name]["peers"][ 846 normalized_neighbor 847 ] = {} 848 bgp_neighbor_data[instance_name]["peers"][normalized_neighbor][ 849 "uptime" 850 ] = uptime[0][1] 851 852 # Commenting out the following sections, till Junos 853 # will provide a way to identify the routing instance name 854 # from the details of the BGP neighbor 855 # currently, there are Junos 15 version having a field called `peer_fwd_rti` 856 # but unfortunately, this is not consistent. 857 # Junos 17 might have this fixed, but this needs to be revisited later. 858 # In the definition below, `old_junos` means a version that does not provide 859 # the forwarding RTI information. 860 # 861 # old_junos = napalm.base.helpers.convert( 862 # int, self.device.facts.get('version', '0.0').split('.')[0], 0) < 15 863 864 # if old_junos: 865 instances = junos_views.junos_route_instance_table(self.device).get() 866 for instance, instance_data in instances.items(): 867 if instance.startswith("__"): 868 # junos internal instances 869 continue 870 bgp_neighbor_data[instance] = {"peers": {}} 871 instance_neighbors = bgp_neighbors_table.get(instance=instance).items() 872 uptime_table_items = uptime_table.get(instance=instance).items() 873 _get_bgp_neighbors_core( 874 instance_neighbors, 875 instance=instance, 876 uptime_table_items=uptime_table_items, 877 ) 878 # If the OS provides the `peer_fwd_rti` or any way to identify the 879 # routing instance name (see above), the performances of this getter 880 # can be significantly improved, as we won't execute one request 881 # for each an every RT. 882 # However, this improvement would only be beneficial for multi-VRF envs. 883 # 884 # else: 885 # instance_neighbors = bgp_neighbors_table.get().items() 886 # _get_bgp_neighbors_core(instance_neighbors) 887 bgp_tmp_dict = {} 888 for k, v in bgp_neighbor_data.items(): 889 if bgp_neighbor_data[k]["peers"]: 890 bgp_tmp_dict[k] = v 891 return bgp_tmp_dict 892 893 def get_lldp_neighbors(self): 894 """Return LLDP neighbors details.""" 895 lldp = junos_views.junos_lldp_table(self.device) 896 try: 897 lldp.get() 898 except RpcError as rpcerr: 899 # this assumes the library runs in an environment 900 # able to handle logs 901 # otherwise, the user just won't see this happening 902 log.error("Unable to retrieve the LLDP neighbors information:") 903 log.error(str(rpcerr)) 904 return {} 905 result = lldp.items() 906 907 neighbors = {} 908 for neigh in result: 909 if neigh[0] not in neighbors.keys(): 910 neighbors[neigh[0]] = [] 911 neighbors[neigh[0]].append({x[0]: str(x[1]) for x in neigh[1]}) 912 913 return neighbors 914 915 def _transform_lldp_capab(self, capabilities): 916 if capabilities and isinstance(capabilities, str): 917 capabilities = capabilities.lower() 918 return sorted( 919 [ 920 translation 921 for entry, translation in C.LLDP_CAPAB_TRANFORM_TABLE.items() 922 if entry in capabilities 923 ] 924 ) 925 else: 926 return [] 927 928 def get_lldp_neighbors_detail(self, interface=""): 929 """Detailed view of the LLDP neighbors.""" 930 lldp_neighbors = defaultdict(list) 931 lldp_table = junos_views.junos_lldp_neighbors_detail_table(self.device) 932 if not interface: 933 try: 934 lldp_table.get() 935 except RpcError as rpcerr: 936 # this assumes the library runs in an environment 937 # able to handle logs 938 # otherwise, the user just won't see this happening 939 log.error("Unable to retrieve the LLDP neighbors information:") 940 log.error(str(rpcerr)) 941 return {} 942 interfaces = lldp_table.get().keys() 943 else: 944 interfaces = [interface] 945 946 if self.device.facts.get("switch_style") == "VLAN": 947 lldp_table.GET_RPC = "get-lldp-interface-neighbors-information" 948 interface_variable = "interface_name" 949 alt_rpc = "get-lldp-interface-neighbors" 950 alt_interface_variable = "interface_device" 951 else: 952 lldp_table.GET_RPC = "get-lldp-interface-neighbors" 953 interface_variable = "interface_device" 954 alt_rpc = "get-lldp-interface-neighbors-information" 955 alt_interface_variable = "interface_name" 956 957 for interface in interfaces: 958 try: 959 interface_args = {interface_variable: interface} 960 lldp_table.get(**interface_args) 961 except RpcError as e: 962 if "syntax error" in str(e): 963 # Looks like we need to call a different RPC on this device 964 # Switch to the alternate style 965 lldp_table.GET_RPC = alt_rpc 966 interface_variable = alt_interface_variable 967 # Retry 968 interface_args = {interface_variable: interface} 969 lldp_table.get(**interface_args) 970 971 for item in lldp_table: 972 lldp_neighbors[interface].append( 973 { 974 "parent_interface": item.parent_interface, 975 "remote_port": item.remote_port or "", 976 "remote_chassis_id": napalm.base.helpers.convert( 977 napalm.base.helpers.mac, 978 item.remote_chassis_id, 979 item.remote_chassis_id, 980 ), 981 "remote_port_description": napalm.base.helpers.convert( 982 str, item.remote_port_description 983 ), 984 "remote_system_name": item.remote_system_name, 985 "remote_system_description": item.remote_system_description, 986 "remote_system_capab": self._transform_lldp_capab( 987 item.remote_system_capab 988 ), 989 "remote_system_enable_capab": self._transform_lldp_capab( 990 item.remote_system_enable_capab 991 ), 992 } 993 ) 994 995 return lldp_neighbors 996 997 def cli(self, commands): 998 """Execute raw CLI commands and returns their output.""" 999 cli_output = {} 1000 1001 def _count(txt, none): # Second arg for consistency only. noqa 1002 """ 1003 Return the exact output, as Junos displays 1004 e.g.: 1005 > show system processes extensive | match root | count 1006 Count: 113 lines 1007 """ 1008 count = len(txt.splitlines()) 1009 return "Count: {count} lines".format(count=count) 1010 1011 def _trim(txt, length): 1012 """ 1013 Trim specified number of columns from start of line. 1014 """ 1015 try: 1016 newlines = [] 1017 for line in txt.splitlines(): 1018 newlines.append(line[int(length) :]) 1019 return "\n".join(newlines) 1020 except ValueError: 1021 return txt 1022 1023 def _except(txt, pattern): 1024 """ 1025 Show only text that does not match a pattern. 1026 """ 1027 rgx = "^.*({pattern}).*$".format(pattern=pattern) 1028 unmatched = [ 1029 line for line in txt.splitlines() if not re.search(rgx, line, re.I) 1030 ] 1031 return "\n".join(unmatched) 1032 1033 def _last(txt, length): 1034 """ 1035 Display end of output only. 1036 """ 1037 try: 1038 return "\n".join(txt.splitlines()[(-1) * int(length) :]) 1039 except ValueError: 1040 return txt 1041 1042 def _match(txt, pattern): 1043 """ 1044 Show only text that matches a pattern. 1045 """ 1046 rgx = "^.*({pattern}).*$".format(pattern=pattern) 1047 matched = [line for line in txt.splitlines() if re.search(rgx, line, re.I)] 1048 return "\n".join(matched) 1049 1050 def _find(txt, pattern): 1051 """ 1052 Search for first occurrence of pattern. 1053 """ 1054 rgx = "^.*({pattern})(.*)$".format(pattern=pattern) 1055 match = re.search(rgx, txt, re.I | re.M | re.DOTALL) 1056 if match: 1057 return "{pattern}{rest}".format(pattern=pattern, rest=match.group(2)) 1058 else: 1059 return "\nPattern not found" 1060 1061 def _process_pipe(cmd, txt): 1062 """ 1063 Process CLI output from Juniper device that 1064 doesn't allow piping the output. 1065 """ 1066 if txt is None: 1067 return txt 1068 _OF_MAP = OrderedDict() 1069 _OF_MAP["except"] = _except 1070 _OF_MAP["match"] = _match 1071 _OF_MAP["last"] = _last 1072 _OF_MAP["trim"] = _trim 1073 _OF_MAP["count"] = _count 1074 _OF_MAP["find"] = _find 1075 # the operations order matter in this case! 1076 exploded_cmd = cmd.split("|") 1077 pipe_oper_args = {} 1078 for pipe in exploded_cmd[1:]: 1079 exploded_pipe = pipe.split() 1080 pipe_oper = exploded_pipe[0] # always there 1081 pipe_args = "".join(exploded_pipe[1:2]) 1082 # will not throw error when there's no arg 1083 pipe_oper_args[pipe_oper] = pipe_args 1084 for oper in _OF_MAP.keys(): 1085 # to make sure the operation sequence is correct 1086 if oper not in pipe_oper_args.keys(): 1087 continue 1088 txt = _OF_MAP[oper](txt, pipe_oper_args[oper]) 1089 return txt 1090 1091 if not isinstance(commands, list): 1092 raise TypeError("Please enter a valid list of commands!") 1093 _PIPE_BLACKLIST = ["save"] 1094 # Preprocessing to avoid forbidden commands 1095 for command in commands: 1096 exploded_cmd = command.split("|") 1097 command_safe_parts = [] 1098 for pipe in exploded_cmd[1:]: 1099 exploded_pipe = pipe.split() 1100 pipe_oper = exploded_pipe[0] # always there 1101 if pipe_oper in _PIPE_BLACKLIST: 1102 continue 1103 pipe_args = "".join(exploded_pipe[1:2]) 1104 safe_pipe = ( 1105 pipe_oper 1106 if not pipe_args 1107 else "{fun} {args}".format(fun=pipe_oper, args=pipe_args) 1108 ) 1109 command_safe_parts.append(safe_pipe) 1110 safe_command = ( 1111 exploded_cmd[0] 1112 if not command_safe_parts 1113 else "{base} | {pipes}".format( 1114 base=exploded_cmd[0], pipes=" | ".join(command_safe_parts) 1115 ) 1116 ) 1117 raw_txt = self.device.cli(safe_command, warning=False) 1118 if isinstance(raw_txt, etree._Element): 1119 raw_txt = etree.tostring(raw_txt.get_parent()).decode() 1120 cli_output[str(command)] = raw_txt 1121 else: 1122 cli_output[str(command)] = str(_process_pipe(command, raw_txt)) 1123 return cli_output 1124 1125 def get_bgp_config(self, group="", neighbor=""): 1126 """Return BGP configuration.""" 1127 1128 def _check_nhs(policies, nhs_policies): 1129 if not isinstance(policies, list): 1130 # Make it a list if it is a single policy 1131 policies = [policies] 1132 # Return True if "next-hop self" was found in any of the policies p 1133 for p in policies: 1134 if nhs_policies[p] is True or isinstance(nhs_policies[p], list): 1135 return True 1136 return False 1137 1138 def update_dict(d, u): # for deep dictionary update 1139 for k, v in u.items(): 1140 if isinstance(d, collections.abc.Mapping): 1141 if isinstance(v, collections.abc.Mapping): 1142 r = update_dict(d.get(k, {}), v) 1143 d[k] = r 1144 else: 1145 d[k] = u[k] 1146 else: 1147 d = {k: u[k]} 1148 return d 1149 1150 def build_prefix_limit(**args): 1151 """ 1152 Transform the lements of a dictionary into nested dictionaries. 1153 1154 Example: 1155 { 1156 'inet_unicast_limit': 500, 1157 'inet_unicast_teardown_threshold': 95, 1158 'inet_unicast_teardown_timeout': 5 1159 } 1160 1161 becomes: 1162 1163 { 1164 'inet': { 1165 'unicast': { 1166 'limit': 500, 1167 'teardown': { 1168 'threshold': 95, 1169 'timeout': 5 1170 } 1171 } 1172 } 1173 } 1174 """ 1175 prefix_limit = {} 1176 1177 for key, value in args.items(): 1178 key_levels = key.split("_") 1179 length = len(key_levels) - 1 1180 temp_dict = {key_levels[length]: value} 1181 for index in reversed(range(length)): 1182 level = key_levels[index] 1183 temp_dict = {level: temp_dict} 1184 update_dict(prefix_limit, temp_dict) 1185 1186 return prefix_limit 1187 1188 _COMMON_FIELDS_DATATYPE_ = { 1189 "description": str, 1190 "local_address": str, 1191 "local_as": int, 1192 "remote_as": int, 1193 "import_policy": str, 1194 "export_policy": str, 1195 "inet_unicast_limit_prefix_limit": int, 1196 "inet_unicast_teardown_threshold_prefix_limit": int, 1197 "inet_unicast_teardown_timeout_prefix_limit": int, 1198 "inet_unicast_novalidate_prefix_limit": int, 1199 "inet_flow_limit_prefix_limit": int, 1200 "inet_flow_teardown_threshold_prefix_limit": int, 1201 "inet_flow_teardown_timeout_prefix_limit": int, 1202 "inet_flow_novalidate_prefix_limit": str, 1203 "inet6_unicast_limit_prefix_limit": int, 1204 "inet6_unicast_teardown_threshold_prefix_limit": int, 1205 "inet6_unicast_teardown_timeout_prefix_limit": int, 1206 "inet6_unicast_novalidate_prefix_limit": int, 1207 "inet6_flow_limit_prefix_limit": int, 1208 "inet6_flow_teardown_threshold_prefix_limit": int, 1209 "inet6_flow_teardown_timeout_prefix_limit": int, 1210 "inet6_flow_novalidate_prefix_limit": str, 1211 } 1212 1213 _PEER_FIELDS_DATATYPE_MAP_ = { 1214 "authentication_key": str, 1215 "route_reflector_client": bool, 1216 "nhs": bool, 1217 } 1218 _PEER_FIELDS_DATATYPE_MAP_.update(_COMMON_FIELDS_DATATYPE_) 1219 1220 _GROUP_FIELDS_DATATYPE_MAP_ = { 1221 "type": str, 1222 "apply_groups": list, 1223 "remove_private_as": bool, 1224 "multipath": bool, 1225 "multihop_ttl": int, 1226 } 1227 _GROUP_FIELDS_DATATYPE_MAP_.update(_COMMON_FIELDS_DATATYPE_) 1228 1229 _DATATYPE_DEFAULT_ = {str: "", int: 0, bool: False, list: []} 1230 1231 bgp_config = {} 1232 1233 if group: 1234 bgp = junos_views.junos_bgp_config_group_table(self.device) 1235 bgp.get(group=group, options=self.junos_config_options) 1236 else: 1237 bgp = junos_views.junos_bgp_config_table(self.device) 1238 bgp.get(options=self.junos_config_options) 1239 neighbor = "" # if no group is set, no neighbor should be set either 1240 bgp_items = bgp.items() 1241 1242 if neighbor: 1243 neighbor_ip = napalm.base.helpers.ip(neighbor) 1244 1245 # Get all policies configured in one go and check if "next-hop self" is found in each policy 1246 # Save the result in a dict indexed by policy name (junos policy-statement) 1247 # The value is a boolean. True if "next-hop self" was found 1248 # The resulting dict (nhs_policies) will be used by _check_nhs to determine if "nhs" 1249 # is configured or not in the policies applied to a BGP neighbor 1250 policy = junos_views.junos_policy_nhs_config_table(self.device) 1251 policy.get(options=self.junos_config_options) 1252 nhs_policies = dict() 1253 for policy_name, is_nhs_list in policy.items(): 1254 # is_nhs_list is a list with one element. Ex: [('is_nhs', True)] 1255 is_nhs, boolean = is_nhs_list[0] 1256 nhs_policies[policy_name] = boolean if boolean is not None else False 1257 1258 for bgp_group in bgp_items: 1259 bgp_group_name = bgp_group[0] 1260 bgp_group_details = bgp_group[1] 1261 bgp_config[bgp_group_name] = { 1262 field: _DATATYPE_DEFAULT_.get(datatype) 1263 for field, datatype in _GROUP_FIELDS_DATATYPE_MAP_.items() 1264 if "_prefix_limit" not in field 1265 } 1266 for elem in bgp_group_details: 1267 if not ("_prefix_limit" not in elem[0] and elem[1] is not None): 1268 continue 1269 datatype = _GROUP_FIELDS_DATATYPE_MAP_.get(elem[0]) 1270 default = _DATATYPE_DEFAULT_.get(datatype) 1271 key = elem[0] 1272 value = elem[1] 1273 if key in ["export_policy", "import_policy"]: 1274 if isinstance(value, list): 1275 value = " ".join(value) 1276 if key == "local_address": 1277 value = napalm.base.helpers.convert( 1278 napalm.base.helpers.ip, value, value 1279 ) 1280 if key == "neighbors": 1281 bgp_group_peers = value 1282 continue 1283 bgp_config[bgp_group_name].update( 1284 {key: napalm.base.helpers.convert(datatype, value, default)} 1285 ) 1286 prefix_limit_fields = {} 1287 for elem in bgp_group_details: 1288 if "_prefix_limit" in elem[0] and elem[1] is not None: 1289 datatype = _GROUP_FIELDS_DATATYPE_MAP_.get(elem[0]) 1290 default = _DATATYPE_DEFAULT_.get(datatype) 1291 prefix_limit_fields.update( 1292 { 1293 elem[0].replace( 1294 "_prefix_limit", "" 1295 ): napalm.base.helpers.convert(datatype, elem[1], default) 1296 } 1297 ) 1298 bgp_config[bgp_group_name]["prefix_limit"] = build_prefix_limit( 1299 **prefix_limit_fields 1300 ) 1301 if "multihop" in bgp_config[bgp_group_name].keys(): 1302 # Delete 'multihop' key from the output 1303 del bgp_config[bgp_group_name]["multihop"] 1304 if bgp_config[bgp_group_name]["multihop_ttl"] == 0: 1305 # Set ttl to default value 64 1306 bgp_config[bgp_group_name]["multihop_ttl"] = 64 1307 1308 bgp_config[bgp_group_name]["neighbors"] = {} 1309 for bgp_group_neighbor in bgp_group_peers.items(): 1310 bgp_peer_address = napalm.base.helpers.ip(bgp_group_neighbor[0]) 1311 if neighbor and bgp_peer_address != neighbor: 1312 continue # if filters applied, jump over all other neighbors 1313 bgp_group_details = bgp_group_neighbor[1] 1314 bgp_peer_details = { 1315 field: _DATATYPE_DEFAULT_.get(datatype) 1316 for field, datatype in _PEER_FIELDS_DATATYPE_MAP_.items() 1317 if "_prefix_limit" not in field 1318 } 1319 for elem in bgp_group_details: 1320 if not ("_prefix_limit" not in elem[0] and elem[1] is not None): 1321 continue 1322 datatype = _PEER_FIELDS_DATATYPE_MAP_.get(elem[0]) 1323 default = _DATATYPE_DEFAULT_.get(datatype) 1324 key = elem[0] 1325 value = elem[1] 1326 if key in ["export_policy"]: 1327 # next-hop self is applied on export IBGP sessions 1328 bgp_peer_details["nhs"] = _check_nhs(value, nhs_policies) 1329 if key in ["export_policy", "import_policy"]: 1330 if isinstance(value, list): 1331 value = " ".join(value) 1332 if key == "local_address": 1333 value = napalm.base.helpers.convert( 1334 napalm.base.helpers.ip, value, value 1335 ) 1336 bgp_peer_details.update( 1337 {key: napalm.base.helpers.convert(datatype, value, default)} 1338 ) 1339 bgp_peer_details["local_as"] = napalm.base.helpers.as_number( 1340 bgp_peer_details["local_as"] 1341 ) 1342 bgp_peer_details["remote_as"] = napalm.base.helpers.as_number( 1343 bgp_peer_details["remote_as"] 1344 ) 1345 if key == "cluster": 1346 bgp_peer_details["route_reflector_client"] = True 1347 # we do not want cluster in the output 1348 del bgp_peer_details["cluster"] 1349 1350 if "cluster" in bgp_config[bgp_group_name].keys(): 1351 bgp_peer_details["route_reflector_client"] = True 1352 prefix_limit_fields = {} 1353 for elem in bgp_group_details: 1354 if "_prefix_limit" in elem[0] and elem[1] is not None: 1355 datatype = _PEER_FIELDS_DATATYPE_MAP_.get(elem[0]) 1356 default = _DATATYPE_DEFAULT_.get(datatype) 1357 prefix_limit_fields.update( 1358 { 1359 elem[0].replace( 1360 "_prefix_limit", "" 1361 ): napalm.base.helpers.convert( 1362 datatype, elem[1], default 1363 ) 1364 } 1365 ) 1366 bgp_peer_details["prefix_limit"] = build_prefix_limit( 1367 **prefix_limit_fields 1368 ) 1369 bgp_config[bgp_group_name]["neighbors"][ 1370 bgp_peer_address 1371 ] = bgp_peer_details 1372 if neighbor and bgp_peer_address == neighbor_ip: 1373 break # found the desired neighbor 1374 1375 if "cluster" in bgp_config[bgp_group_name].keys(): 1376 # we do not want cluster in the output 1377 del bgp_config[bgp_group_name]["cluster"] 1378 1379 return bgp_config 1380 1381 def get_bgp_neighbors_detail(self, neighbor_address=""): 1382 """Detailed view of the BGP neighbors operational data.""" 1383 bgp_neighbors = {} 1384 default_neighbor_details = { 1385 "up": False, 1386 "local_as": 0, 1387 "remote_as": 0, 1388 "router_id": "", 1389 "local_address": "", 1390 "routing_table": "", 1391 "local_address_configured": False, 1392 "local_port": 0, 1393 "remote_address": "", 1394 "remote_port": 0, 1395 "multihop": False, 1396 "multipath": False, 1397 "remove_private_as": False, 1398 "import_policy": "", 1399 "export_policy": "", 1400 "input_messages": -1, 1401 "output_messages": -1, 1402 "input_updates": -1, 1403 "output_updates": -1, 1404 "messages_queued_out": -1, 1405 "connection_state": "", 1406 "previous_connection_state": "", 1407 "last_event": "", 1408 "suppress_4byte_as": False, 1409 "local_as_prepend": False, 1410 "holdtime": 0, 1411 "configured_holdtime": 0, 1412 "keepalive": 0, 1413 "configured_keepalive": 0, 1414 "active_prefix_count": -1, 1415 "received_prefix_count": -1, 1416 "accepted_prefix_count": -1, 1417 "suppressed_prefix_count": -1, 1418 "advertised_prefix_count": -1, 1419 "flap_count": 0, 1420 } 1421 OPTION_KEY_MAP = { 1422 "RemovePrivateAS": "remove_private_as", 1423 "Multipath": "multipath", 1424 "Multihop": "multihop", 1425 "AddressFamily": "local_address_configured" 1426 # 'AuthKey' : 'authentication_key_set' 1427 # but other vendors do not specify if auth key is set 1428 # other options: 1429 # Preference, HoldTime, Ttl, LogUpDown, Refresh 1430 } 1431 1432 def _bgp_iter_core(neighbor_data, instance=None): 1433 """ 1434 Iterate over a list of neighbors. 1435 For older junos, the routing instance is not specified inside the 1436 BGP neighbors XML, therefore we need to use a super sub-optimal structure 1437 as in get_bgp_neighbors: iterate through the list of network instances 1438 then execute one request for each and every routing instance. 1439 For newer junos, this is not necessary as the routing instance is available 1440 and we can get everything solve in a single request. 1441 """ 1442 for bgp_neighbor in neighbor_data: 1443 remote_as = int(bgp_neighbor[0]) 1444 neighbor_details = deepcopy(default_neighbor_details) 1445 neighbor_details.update( 1446 { 1447 elem[0]: elem[1] 1448 for elem in bgp_neighbor[1] 1449 if elem[1] is not None 1450 } 1451 ) 1452 if not instance: 1453 peer_fwd_rti = neighbor_details.pop("peer_fwd_rti") 1454 instance = peer_fwd_rti 1455 else: 1456 peer_fwd_rti = neighbor_details.pop("peer_fwd_rti", "") 1457 instance_name = "global" if instance == "master" else instance 1458 options = neighbor_details.pop("options", "") 1459 if isinstance(options, str): 1460 options_list = options.split() 1461 for option in options_list: 1462 key = OPTION_KEY_MAP.get(option) 1463 if key is not None: 1464 neighbor_details[key] = True 1465 four_byte_as = neighbor_details.pop("4byte_as", 0) 1466 local_address = neighbor_details.pop("local_address", "") 1467 local_details = local_address.split("+") 1468 neighbor_details["local_address"] = napalm.base.helpers.convert( 1469 napalm.base.helpers.ip, local_details[0], local_details[0] 1470 ) 1471 if len(local_details) == 2: 1472 neighbor_details["local_port"] = int(local_details[1]) 1473 else: 1474 neighbor_details["local_port"] = 179 1475 neighbor_details["suppress_4byte_as"] = remote_as != four_byte_as 1476 peer_address = neighbor_details.pop("peer_address", "") 1477 remote_details = peer_address.split("+") 1478 neighbor_details["remote_address"] = napalm.base.helpers.convert( 1479 napalm.base.helpers.ip, remote_details[0], remote_details[0] 1480 ) 1481 if len(remote_details) == 2: 1482 neighbor_details["remote_port"] = int(remote_details[1]) 1483 else: 1484 neighbor_details["remote_port"] = 179 1485 neighbor_details["routing_table"] = instance_name 1486 neighbor_details["local_as"] = napalm.base.helpers.as_number( 1487 neighbor_details["local_as"] 1488 ) 1489 neighbor_details["remote_as"] = napalm.base.helpers.as_number( 1490 neighbor_details["remote_as"] 1491 ) 1492 neighbors_rib = neighbor_details.pop("rib") 1493 neighbors_queue = neighbor_details.pop("queue") 1494 messages_queued_out = 0 1495 for queue_entry in neighbors_queue.items(): 1496 messages_queued_out += queue_entry[1][0][1] 1497 neighbor_details["messages_queued_out"] = messages_queued_out 1498 if instance_name not in bgp_neighbors.keys(): 1499 bgp_neighbors[instance_name] = {} 1500 if remote_as not in bgp_neighbors[instance_name].keys(): 1501 bgp_neighbors[instance_name][remote_as] = [] 1502 neighbor_rib_stats = neighbors_rib.items() 1503 if not neighbor_rib_stats: 1504 bgp_neighbors[instance_name][remote_as].append(neighbor_details) 1505 continue # no RIBs available, pass default details 1506 neighbor_rib_details = { 1507 "active_prefix_count": 0, 1508 "received_prefix_count": 0, 1509 "accepted_prefix_count": 0, 1510 "suppressed_prefix_count": 0, 1511 "advertised_prefix_count": 0, 1512 } 1513 for rib_entry in neighbor_rib_stats: 1514 for elem in rib_entry[1]: 1515 if elem[1] is None: 1516 neighbor_rib_details[elem[0]] += 0 1517 else: 1518 neighbor_rib_details[elem[0]] += elem[1] 1519 neighbor_details.update(neighbor_rib_details) 1520 bgp_neighbors[instance_name][remote_as].append(neighbor_details) 1521 1522 # old_junos = napalm.base.helpers.convert( 1523 # int, self.device.facts.get('version', '0.0').split('.')[0], 0) < 15 1524 bgp_neighbors_table = junos_views.junos_bgp_neighbors_table(self.device) 1525 1526 # if old_junos: 1527 instances = junos_views.junos_route_instance_table(self.device) 1528 for instance, instance_data in instances.get().items(): 1529 if instance.startswith("__"): 1530 # junos internal instances 1531 continue 1532 neighbor_data = bgp_neighbors_table.get( 1533 instance=instance, neighbor_address=str(neighbor_address) 1534 ).items() 1535 _bgp_iter_core(neighbor_data, instance=instance) 1536 # else: 1537 # bgp_neighbors_table = junos_views.junos_bgp_neighbors_table(self.device) 1538 # neighbor_data = bgp_neighbors_table.get(neighbor_address=neighbor_address).items() 1539 # _bgp_iter_core(neighbor_data) 1540 return bgp_neighbors 1541 1542 def get_arp_table(self, vrf=""): 1543 """Return the ARP table.""" 1544 # could use ArpTable 1545 # from jnpr.junos.op.phyport import ArpTable 1546 # and simply use it 1547 # but 1548 # we need: 1549 # - filters 1550 # - group by VLAN ID 1551 # - hostname & TTE fields as well 1552 if vrf: 1553 msg = "VRF support has not been added for this getter on this platform." 1554 raise NotImplementedError(msg) 1555 1556 arp_table = [] 1557 1558 arp_table_raw = junos_views.junos_arp_table(self.device) 1559 arp_table_raw.get() 1560 arp_table_items = arp_table_raw.items() 1561 1562 for arp_table_entry in arp_table_items: 1563 arp_entry = {elem[0]: elem[1] for elem in arp_table_entry[1]} 1564 arp_entry["mac"] = napalm.base.helpers.mac(arp_entry.get("mac")) 1565 arp_entry["ip"] = napalm.base.helpers.ip(arp_entry.get("ip")) 1566 arp_table.append(arp_entry) 1567 1568 return arp_table 1569 1570 def get_ipv6_neighbors_table(self): 1571 """Return the IPv6 neighbors table.""" 1572 ipv6_neighbors_table = [] 1573 1574 ipv6_neighbors_table_raw = junos_views.junos_ipv6_neighbors_table(self.device) 1575 ipv6_neighbors_table_raw.get() 1576 ipv6_neighbors_table_items = ipv6_neighbors_table_raw.items() 1577 1578 for ipv6_table_entry in ipv6_neighbors_table_items: 1579 ipv6_entry = {elem[0]: elem[1] for elem in ipv6_table_entry[1]} 1580 ipv6_entry["mac"] = napalm.base.helpers.mac(ipv6_entry.get("mac")) 1581 ipv6_entry["ip"] = napalm.base.helpers.ip(ipv6_entry.get("ip")) 1582 ipv6_neighbors_table.append(ipv6_entry) 1583 1584 return ipv6_neighbors_table 1585 1586 def get_ntp_peers(self): 1587 """Return the NTP peers configured on the device.""" 1588 ntp_table = junos_views.junos_ntp_peers_config_table(self.device) 1589 ntp_table.get(options=self.junos_config_options) 1590 1591 ntp_peers = ntp_table.items() 1592 1593 if not ntp_peers: 1594 return {} 1595 1596 return {napalm.base.helpers.ip(peer[0]): {} for peer in ntp_peers} 1597 1598 def get_ntp_servers(self): 1599 """Return the NTP servers configured on the device.""" 1600 ntp_table = junos_views.junos_ntp_servers_config_table(self.device) 1601 ntp_table.get(options=self.junos_config_options) 1602 1603 ntp_servers = ntp_table.items() 1604 1605 if not ntp_servers: 1606 return {} 1607 1608 return {napalm.base.helpers.ip(server[0]): {} for server in ntp_servers} 1609 1610 def get_ntp_stats(self): 1611 """Return NTP stats (associations).""" 1612 # NTP Peers does not have XML RPC defined 1613 # thus we need to retrieve raw text and parse... 1614 # :( 1615 1616 ntp_stats = [] 1617 1618 REGEX = ( 1619 r"^\s?(\+|\*|x|-)?([a-zA-Z0-9\.+-:]+)" 1620 r"\s+([a-zA-Z0-9\.]+)\s+([0-9]{1,2})" 1621 r"\s+(-|u)\s+([0-9h-]+)\s+([0-9]+)" 1622 r"\s+([0-9]+)\s+([0-9\.]+)\s+([0-9\.-]+)" 1623 r"\s+([0-9\.]+)\s?$" 1624 ) 1625 1626 ntp_assoc_output = self.device.cli("show ntp associations no-resolve") 1627 ntp_assoc_output_lines = ntp_assoc_output.splitlines() 1628 1629 for ntp_assoc_output_line in ntp_assoc_output_lines[3:]: # except last line 1630 line_search = re.search(REGEX, ntp_assoc_output_line, re.I) 1631 if not line_search: 1632 continue # pattern not found 1633 line_groups = line_search.groups() 1634 try: 1635 ntp_stats.append( 1636 { 1637 "remote": napalm.base.helpers.ip(line_groups[1]), 1638 "synchronized": (line_groups[0] == "*"), 1639 "referenceid": str(line_groups[2]), 1640 "stratum": int(line_groups[3]), 1641 "type": str(line_groups[4]), 1642 "when": str(line_groups[5]), 1643 "hostpoll": int(line_groups[6]), 1644 "reachability": int(line_groups[7]), 1645 "delay": float(line_groups[8]), 1646 "offset": float(line_groups[9]), 1647 "jitter": float(line_groups[10]), 1648 } 1649 ) 1650 except Exception: 1651 continue # jump to next line 1652 1653 return ntp_stats 1654 1655 def get_interfaces_ip(self): 1656 """Return the configured IP addresses.""" 1657 interfaces_ip = {} 1658 1659 interface_table = junos_views.junos_ip_interfaces_table(self.device) 1660 interface_table.get() 1661 interface_table_items = interface_table.items() 1662 1663 _FAMILY_VMAP_ = { 1664 "inet": "ipv4", 1665 "inet6": "ipv6" 1666 # can add more mappings 1667 } 1668 _FAMILY_MAX_PREFIXLEN = {"inet": 32, "inet6": 128} 1669 1670 for interface_details in interface_table_items: 1671 ip_network = interface_details[0] 1672 ip_address = ip_network.split("/")[0] 1673 address = napalm.base.helpers.convert( 1674 napalm.base.helpers.ip, ip_address, ip_address 1675 ) 1676 try: 1677 interface_details_dict = dict(interface_details[1]) 1678 family_raw = interface_details_dict.get("family") 1679 interface = str(interface_details_dict.get("interface")) 1680 except ValueError: 1681 continue 1682 prefix = napalm.base.helpers.convert( 1683 int, ip_network.split("/")[-1], _FAMILY_MAX_PREFIXLEN.get(family_raw) 1684 ) 1685 family = _FAMILY_VMAP_.get(family_raw) 1686 if not family or not interface: 1687 continue 1688 if interface not in interfaces_ip.keys(): 1689 interfaces_ip[interface] = {} 1690 if family not in interfaces_ip[interface].keys(): 1691 interfaces_ip[interface][family] = {} 1692 if address not in interfaces_ip[interface][family].keys(): 1693 interfaces_ip[interface][family][address] = {} 1694 interfaces_ip[interface][family][address]["prefix_length"] = prefix 1695 1696 return interfaces_ip 1697 1698 def get_mac_address_table(self): 1699 """Return the MAC address table.""" 1700 mac_address_table = [] 1701 1702 switch_style = self.device.facts.get("switch_style", "") 1703 if switch_style == "VLAN_L2NG": 1704 mac_table = junos_views.junos_mac_address_table_switch_l2ng(self.device) 1705 elif switch_style == "BRIDGE_DOMAIN": 1706 mac_table = junos_views.junos_mac_address_table(self.device) 1707 else: # switch_style == "VLAN" 1708 mac_table = junos_views.junos_mac_address_table_switch(self.device) 1709 1710 try: 1711 mac_table.get() 1712 except RpcError as e: 1713 # Device hasn't got it's l2 subsystem running 1714 # Don't error but just return an empty result 1715 if "l2-learning subsystem" in str(e): 1716 return [] 1717 else: 1718 raise 1719 1720 mac_table_items = mac_table.items() 1721 1722 default_values = { 1723 "mac": "", 1724 "interface": "", 1725 "vlan": 0, 1726 "static": False, 1727 "active": True, 1728 "moves": 0, 1729 "last_move": 0.0, 1730 } 1731 1732 for mac_table_entry in mac_table_items: 1733 mac_entry = default_values.copy() 1734 mac_entry.update({elem[0]: elem[1] for elem in mac_table_entry[1]}) 1735 mac = mac_entry.get("mac") 1736 1737 # JUNOS returns '*' for Type = Flood 1738 if mac == "*": 1739 continue 1740 1741 mac_entry["mac"] = napalm.base.helpers.mac(mac) 1742 mac_address_table.append(mac_entry) 1743 1744 return mac_address_table 1745 1746 def get_route_to(self, destination="", protocol="", longer=False): 1747 """Return route details to a specific destination, learned from a certain protocol.""" 1748 routes = {} 1749 1750 if not isinstance(destination, str): 1751 raise TypeError("Please specify a valid destination!") 1752 1753 if longer: 1754 raise NotImplementedError("Longer prefixes not yet supported on JunOS") 1755 1756 if protocol and isinstance(destination, str): 1757 protocol = protocol.lower() 1758 1759 if protocol == "connected": 1760 protocol = "direct" # this is how is called on JunOS 1761 1762 _COMMON_PROTOCOL_FIELDS_ = [ 1763 "destination", 1764 "prefix_length", 1765 "protocol", 1766 "current_active", 1767 "last_active", 1768 "age", 1769 "next_hop", 1770 "outgoing_interface", 1771 "selected_next_hop", 1772 "preference", 1773 "inactive_reason", 1774 "routing_table", 1775 ] # identifies the list of fileds common for all protocols 1776 1777 _BOOLEAN_FIELDS_ = [ 1778 "current_active", 1779 "selected_next_hop", 1780 "last_active", 1781 ] # fields expected to have boolean values 1782 1783 _PROTOCOL_SPECIFIC_FIELDS_ = { 1784 "bgp": [ 1785 "local_as", 1786 "remote_as", 1787 "as_path", 1788 "communities", 1789 "local_preference", 1790 "preference2", 1791 "remote_address", 1792 "metric", 1793 "metric2", 1794 ], 1795 "isis": ["level", "metric", "local_as"], 1796 } 1797 1798 routes_table = junos_views.junos_protocol_route_table(self.device) 1799 1800 rt_kargs = {"destination": destination} 1801 if protocol and isinstance(destination, str): 1802 rt_kargs["protocol"] = protocol 1803 1804 try: 1805 routes_table.get(**rt_kargs) 1806 except RpcTimeoutError: 1807 # on devices with milions of routes 1808 # in case the destination is too generic (e.g.: 10/8) 1809 # will take very very long to determine all routes and 1810 # moreover will return a huge list 1811 raise CommandTimeoutException( 1812 "Too many routes returned! Please try with a longer prefix or a specific protocol!" 1813 ) 1814 except RpcError as rpce: 1815 if len(rpce.errs) > 0 and "bad_element" in rpce.errs[0]: 1816 raise CommandErrorException( 1817 "Unknown protocol: {proto}".format( 1818 proto=rpce.errs[0]["bad_element"] 1819 ) 1820 ) 1821 raise CommandErrorException(rpce) 1822 except Exception as err: 1823 raise CommandErrorException( 1824 "Cannot retrieve routes! Reason: {err}".format(err=err) 1825 ) 1826 1827 routes_items = routes_table.items() 1828 1829 for route in routes_items: 1830 d = {} 1831 # next_hop = route[0] 1832 d = {elem[0]: elem[1] for elem in route[1]} 1833 destination = d.pop("destination", "") 1834 prefix_length = d.pop("prefix_length", 32) 1835 destination = "{d}/{p}".format(d=destination, p=prefix_length) 1836 d.update({key: False for key in _BOOLEAN_FIELDS_ if d.get(key) is None}) 1837 as_path = d.get("as_path") 1838 if as_path is not None: 1839 d["as_path"] = ( 1840 as_path.split(" I ")[0] 1841 .replace("AS path:", "") 1842 .replace("I", "") 1843 .strip() 1844 ) 1845 # to be sure that contains only AS Numbers 1846 if d.get("inactive_reason") is None: 1847 d["inactive_reason"] = "" 1848 route_protocol = d.get("protocol").lower() 1849 if protocol and protocol != route_protocol: 1850 continue 1851 communities = d.get("communities") 1852 if communities is not None and type(communities) is not list: 1853 d["communities"] = [communities] 1854 d_keys = list(d.keys()) 1855 # fields that are not in _COMMON_PROTOCOL_FIELDS_ are supposed to be protocol specific 1856 all_protocol_attributes = { 1857 key: d.pop(key) for key in d_keys if key not in _COMMON_PROTOCOL_FIELDS_ 1858 } 1859 protocol_attributes = { 1860 key: value 1861 for key, value in all_protocol_attributes.items() 1862 if key in _PROTOCOL_SPECIFIC_FIELDS_.get(route_protocol, []) 1863 } 1864 d["protocol_attributes"] = protocol_attributes 1865 if destination not in routes.keys(): 1866 routes[destination] = [] 1867 routes[destination].append(d) 1868 1869 return routes 1870 1871 def get_snmp_information(self): 1872 """Return the SNMP configuration.""" 1873 snmp_information = {} 1874 1875 snmp_config = junos_views.junos_snmp_config_table(self.device) 1876 snmp_config.get(options=self.junos_config_options) 1877 snmp_items = snmp_config.items() 1878 1879 if not snmp_items: 1880 return snmp_information 1881 1882 snmp_information = { 1883 str(ele[0]): ele[1] if ele[1] else "" for ele in snmp_items[0][1] 1884 } 1885 1886 snmp_information["community"] = {} 1887 communities_table = snmp_information.pop("communities_table") 1888 if not communities_table: 1889 return snmp_information 1890 1891 for community in communities_table.items(): 1892 community_name = str(community[0]) 1893 community_details = {"acl": ""} 1894 community_details.update( 1895 { 1896 str(ele[0]): str( 1897 ele[1] 1898 if ele[0] != "mode" 1899 else C.SNMP_AUTHORIZATION_MODE_MAP.get(ele[1]) 1900 ) 1901 for ele in community[1] 1902 } 1903 ) 1904 snmp_information["community"][community_name] = community_details 1905 1906 return snmp_information 1907 1908 def get_probes_config(self): 1909 """Return the configuration of the RPM probes.""" 1910 probes = {} 1911 1912 probes_table = junos_views.junos_rpm_probes_config_table(self.device) 1913 probes_table.get(options=self.junos_config_options) 1914 probes_table_items = probes_table.items() 1915 1916 for probe_test in probes_table_items: 1917 test_name = str(probe_test[0]) 1918 test_details = {p[0]: p[1] for p in probe_test[1]} 1919 probe_name = napalm.base.helpers.convert( 1920 str, test_details.pop("probe_name") 1921 ) 1922 target = napalm.base.helpers.convert(str, test_details.pop("target", "")) 1923 test_interval = napalm.base.helpers.convert( 1924 int, test_details.pop("test_interval", "0") 1925 ) 1926 probe_count = napalm.base.helpers.convert( 1927 int, test_details.pop("probe_count", "0") 1928 ) 1929 probe_type = napalm.base.helpers.convert( 1930 str, test_details.pop("probe_type", "") 1931 ) 1932 source = napalm.base.helpers.convert( 1933 str, test_details.pop("source_address", "") 1934 ) 1935 if probe_name not in probes.keys(): 1936 probes[probe_name] = {} 1937 probes[probe_name][test_name] = { 1938 "probe_type": probe_type, 1939 "target": target, 1940 "source": source, 1941 "probe_count": probe_count, 1942 "test_interval": test_interval, 1943 } 1944 1945 return probes 1946 1947 def get_probes_results(self): 1948 """Return the results of the RPM probes.""" 1949 probes_results = {} 1950 1951 probes_results_table = junos_views.junos_rpm_probes_results_table(self.device) 1952 probes_results_table.get() 1953 probes_results_items = probes_results_table.items() 1954 1955 for probe_result in probes_results_items: 1956 probe_name = str(probe_result[0]) 1957 test_results = {p[0]: p[1] for p in probe_result[1]} 1958 test_results["last_test_loss"] = napalm.base.helpers.convert( 1959 int, test_results.pop("last_test_loss"), 0 1960 ) 1961 for test_param_name, test_param_value in test_results.items(): 1962 if isinstance(test_param_value, float): 1963 test_results[test_param_name] = test_param_value * 1e-3 1964 # convert from useconds to mseconds 1965 test_name = test_results.pop("test_name", "") 1966 source = test_results.get("source", "") 1967 if source is None: 1968 test_results["source"] = "" 1969 if probe_name not in probes_results.keys(): 1970 probes_results[probe_name] = {} 1971 probes_results[probe_name][test_name] = test_results 1972 1973 return probes_results 1974 1975 def traceroute( 1976 self, 1977 destination, 1978 source=C.TRACEROUTE_SOURCE, 1979 ttl=C.TRACEROUTE_TTL, 1980 timeout=C.TRACEROUTE_TIMEOUT, 1981 vrf=C.TRACEROUTE_VRF, 1982 ): 1983 """Execute traceroute and return results.""" 1984 traceroute_result = {} 1985 1986 # calling form RPC does not work properly :( 1987 # but defined junos_route_instance_table just in case 1988 1989 source_str = "" 1990 maxttl_str = "" 1991 wait_str = "" 1992 vrf_str = "" 1993 1994 if source: 1995 source_str = " source {source}".format(source=source) 1996 if ttl: 1997 maxttl_str = " ttl {ttl}".format(ttl=ttl) 1998 if timeout: 1999 wait_str = " wait {timeout}".format(timeout=timeout) 2000 if vrf: 2001 vrf_str = " routing-instance {vrf}".format(vrf=vrf) 2002 2003 traceroute_command = ( 2004 "traceroute {destination}{source}{maxttl}{wait}{vrf}".format( 2005 destination=destination, 2006 source=source_str, 2007 maxttl=maxttl_str, 2008 wait=wait_str, 2009 vrf=vrf_str, 2010 ) 2011 ) 2012 2013 traceroute_rpc = E("command", traceroute_command) 2014 rpc_reply = self.device._conn.rpc(traceroute_rpc)._NCElement__doc 2015 # make direct RPC call via NETCONF 2016 traceroute_results = rpc_reply.find(".//traceroute-results") 2017 2018 traceroute_failure = napalm.base.helpers.find_txt( 2019 traceroute_results, "traceroute-failure", "" 2020 ) 2021 error_message = napalm.base.helpers.find_txt( 2022 traceroute_results, "rpc-error/error-message", "" 2023 ) 2024 2025 if traceroute_failure and error_message: 2026 return {"error": "{}: {}".format(traceroute_failure, error_message)} 2027 2028 traceroute_result["success"] = {} 2029 for hop in traceroute_results.findall("hop"): 2030 ttl_value = napalm.base.helpers.convert( 2031 int, napalm.base.helpers.find_txt(hop, "ttl-value"), 1 2032 ) 2033 if ttl_value not in traceroute_result["success"]: 2034 traceroute_result["success"][ttl_value] = {"probes": {}} 2035 for probe in hop.findall("probe-result"): 2036 probe_index = napalm.base.helpers.convert( 2037 int, napalm.base.helpers.find_txt(probe, "probe-index"), 0 2038 ) 2039 ip_address = napalm.base.helpers.convert( 2040 napalm.base.helpers.ip, 2041 napalm.base.helpers.find_txt(probe, "ip-address"), 2042 "*", 2043 ) 2044 host_name = str(napalm.base.helpers.find_txt(probe, "host-name", "*")) 2045 rtt = ( 2046 napalm.base.helpers.convert( 2047 float, napalm.base.helpers.find_txt(probe, "rtt"), 0 2048 ) 2049 * 1e-3 2050 ) # ms 2051 traceroute_result["success"][ttl_value]["probes"][probe_index] = { 2052 "ip_address": ip_address, 2053 "host_name": host_name, 2054 "rtt": rtt, 2055 } 2056 2057 return traceroute_result 2058 2059 def ping( 2060 self, 2061 destination, 2062 source=C.PING_SOURCE, 2063 ttl=C.PING_TTL, 2064 timeout=C.PING_TIMEOUT, 2065 size=C.PING_SIZE, 2066 count=C.PING_COUNT, 2067 vrf=C.PING_VRF, 2068 source_interface=C.PING_SOURCE_INTERFACE, 2069 ): 2070 2071 ping_dict = {} 2072 2073 source_str = "" 2074 maxttl_str = "" 2075 timeout_str = "" 2076 size_str = "" 2077 count_str = "" 2078 vrf_str = "" 2079 source_interface_str = "" 2080 2081 if source: 2082 source_str = " source {source}".format(source=source) 2083 if ttl: 2084 maxttl_str = " ttl {ttl}".format(ttl=ttl) 2085 if timeout: 2086 timeout_str = " wait {timeout}".format(timeout=timeout) 2087 if size: 2088 size_str = " size {size}".format(size=size) 2089 if count: 2090 count_str = " count {count}".format(count=count) 2091 if vrf: 2092 vrf_str = " routing-instance {vrf}".format(vrf=vrf) 2093 if source_interface: 2094 source_interface_str = " interface {source_interface}".format( 2095 source_interface=source_interface 2096 ) 2097 2098 ping_command = ( 2099 "ping {destination}{source}{ttl}{timeout}{size}{count}{vrf}{source_interface}" 2100 ).format( 2101 destination=destination, 2102 source=source_str, 2103 ttl=maxttl_str, 2104 timeout=timeout_str, 2105 size=size_str, 2106 count=count_str, 2107 vrf=vrf_str, 2108 source_interface=source_interface_str, 2109 ) 2110 2111 ping_rpc = E("command", ping_command) 2112 rpc_reply = self.device._conn.rpc(ping_rpc)._NCElement__doc 2113 # make direct RPC call via NETCONF 2114 probe_summary = rpc_reply.find(".//probe-results-summary") 2115 2116 if probe_summary is None: 2117 rpc_error = rpc_reply.find(".//rpc-error") 2118 return { 2119 "error": "{}".format( 2120 napalm.base.helpers.find_txt(rpc_error, "error-message") 2121 ) 2122 } 2123 2124 packet_loss = napalm.base.helpers.convert( 2125 int, napalm.base.helpers.find_txt(probe_summary, "packet-loss"), 100 2126 ) 2127 2128 # rtt values are valid only if a we get an ICMP reply 2129 if packet_loss != 100: 2130 ping_dict["success"] = {} 2131 ping_dict["success"]["probes_sent"] = int( 2132 probe_summary.findtext("probes-sent") 2133 ) 2134 ping_dict["success"]["packet_loss"] = packet_loss 2135 ping_dict["success"].update( 2136 { 2137 "rtt_min": round( 2138 ( 2139 napalm.base.helpers.convert( 2140 float, 2141 napalm.base.helpers.find_txt( 2142 probe_summary, "rtt-minimum" 2143 ), 2144 -1, 2145 ) 2146 * 1e-3 2147 ), 2148 3, 2149 ), 2150 "rtt_max": round( 2151 ( 2152 napalm.base.helpers.convert( 2153 float, 2154 napalm.base.helpers.find_txt( 2155 probe_summary, "rtt-maximum" 2156 ), 2157 -1, 2158 ) 2159 * 1e-3 2160 ), 2161 3, 2162 ), 2163 "rtt_avg": round( 2164 ( 2165 napalm.base.helpers.convert( 2166 float, 2167 napalm.base.helpers.find_txt( 2168 probe_summary, "rtt-average" 2169 ), 2170 -1, 2171 ) 2172 * 1e-3 2173 ), 2174 3, 2175 ), 2176 "rtt_stddev": round( 2177 ( 2178 napalm.base.helpers.convert( 2179 float, 2180 napalm.base.helpers.find_txt( 2181 probe_summary, "rtt-stddev" 2182 ), 2183 -1, 2184 ) 2185 * 1e-3 2186 ), 2187 3, 2188 ), 2189 } 2190 ) 2191 2192 tmp = rpc_reply.find(".//ping-results") 2193 2194 results_array = [] 2195 for probe_result in tmp.findall("probe-result"): 2196 ip_address = napalm.base.helpers.convert( 2197 napalm.base.helpers.ip, 2198 napalm.base.helpers.find_txt(probe_result, "ip-address"), 2199 "*", 2200 ) 2201 2202 rtt = round( 2203 ( 2204 napalm.base.helpers.convert( 2205 float, napalm.base.helpers.find_txt(probe_result, "rtt"), -1 2206 ) 2207 * 1e-3 2208 ), 2209 3, 2210 ) 2211 2212 results_array.append({"ip_address": ip_address, "rtt": rtt}) 2213 2214 ping_dict["success"].update({"results": results_array}) 2215 else: 2216 return {"error": "Packet loss {}".format(packet_loss)} 2217 2218 return ping_dict 2219 2220 def _get_root(self): 2221 """get root user password.""" 2222 _DEFAULT_USER_DETAILS = {"level": 20, "password": "", "sshkeys": []} 2223 root = {} 2224 root_table = junos_views.junos_root_table(self.device) 2225 root_table.get(options=self.junos_config_options) 2226 root_items = root_table.items() 2227 for user_entry in root_items: 2228 username = "root" 2229 user_details = _DEFAULT_USER_DETAILS.copy() 2230 user_details.update({d[0]: d[1] for d in user_entry[1] if d[1]}) 2231 user_details = {key: str(user_details[key]) for key in user_details.keys()} 2232 user_details["level"] = int(user_details["level"]) 2233 user_details["sshkeys"] = [ 2234 user_details.pop(key) 2235 for key in ["ssh_rsa", "ssh_dsa", "ssh_ecdsa"] 2236 if user_details.get(key, "") 2237 ] 2238 root[username] = user_details 2239 return root 2240 2241 def get_users(self): 2242 """Return the configuration of the users.""" 2243 users = {} 2244 2245 _JUNOS_CLASS_CISCO_PRIVILEGE_LEVEL_MAP = { 2246 "super-user": 15, 2247 "superuser": 15, 2248 "operator": 5, 2249 "read-only": 1, 2250 "unauthorized": 0, 2251 } 2252 2253 _DEFAULT_USER_DETAILS = {"level": 0, "password": "", "sshkeys": []} 2254 2255 users_table = junos_views.junos_users_table(self.device) 2256 users_table.get(options=self.junos_config_options) 2257 users_items = users_table.items() 2258 root_user = self._get_root() 2259 2260 for user_entry in users_items: 2261 username = user_entry[0] 2262 user_details = _DEFAULT_USER_DETAILS.copy() 2263 user_details.update({d[0]: d[1] for d in user_entry[1] if d[1]}) 2264 user_class = user_details.pop("class", "") 2265 user_details = {key: str(user_details[key]) for key in user_details.keys()} 2266 level = _JUNOS_CLASS_CISCO_PRIVILEGE_LEVEL_MAP.get(user_class, 0) 2267 user_details.update({"level": level}) 2268 user_details["sshkeys"] = [ 2269 user_details.pop(key) 2270 for key in ["ssh_rsa", "ssh_dsa", "ssh_ecdsa"] 2271 if user_details.get(key, "") 2272 ] 2273 users[username] = user_details 2274 users.update(root_user) 2275 return users 2276 2277 def get_optics(self): 2278 """Return optics information.""" 2279 optics_table = junos_views.junos_intf_optics_table(self.device) 2280 optics_table.get() 2281 optics_items = optics_table.items() 2282 2283 # optics_items has no lane information, so we need to re-format data 2284 # inserting lane 0 for all optics. Note it contains all optics 10G/40G/100G 2285 # but the information for 40G/100G is incorrect at this point 2286 # Example: intf_optic item is now: ('xe-0/0/0', [ optical_values ]) 2287 optics_items_with_lane = [] 2288 for intf_optic_item in optics_items: 2289 temp_list = list(intf_optic_item) 2290 temp_list.insert(1, "0") 2291 new_intf_optic_item = tuple(temp_list) 2292 optics_items_with_lane.append(new_intf_optic_item) 2293 2294 # Now optics_items_with_lane has all optics with lane 0 included 2295 # Example: ('xe-0/0/0', u'0', [ optical_values ]) 2296 2297 # Get optical information for 40G/100G optics 2298 optics_table40G = junos_views.junos_intf_40Goptics_table(self.device) 2299 optics_table40G.get() 2300 optics_40Gitems = optics_table40G.items() 2301 2302 # Re-format data as before inserting lane value 2303 new_optics_40Gitems = [] 2304 for item in optics_40Gitems: 2305 lane = item[0] 2306 iface = item[1].pop(0) 2307 new_optics_40Gitems.append((iface[1], str(lane), item[1])) 2308 2309 # New_optics_40Gitems contains 40G/100G optics only: 2310 # ('et-0/0/49', u'0', [ optical_values ]), 2311 # ('et-0/0/49', u'1', [ optical_values ]), 2312 # ('et-0/0/49', u'2', [ optical_values ]) 2313 2314 # Remove 40G/100G optics entries with wrong information returned 2315 # from junos_intf_optics_table() 2316 iface_40G = [item[0] for item in new_optics_40Gitems] 2317 for intf_optic_item in optics_items_with_lane: 2318 iface_name = intf_optic_item[0] 2319 if iface_name not in iface_40G: 2320 new_optics_40Gitems.append(intf_optic_item) 2321 2322 # New_optics_40Gitems contains all optics 10G/40G/100G with the lane 2323 optics_detail = {} 2324 for intf_optic_item in new_optics_40Gitems: 2325 lane = intf_optic_item[1] 2326 interface_name = str(intf_optic_item[0]) 2327 optics = dict(intf_optic_item[2]) 2328 if interface_name not in optics_detail: 2329 optics_detail[interface_name] = {} 2330 optics_detail[interface_name]["physical_channels"] = {} 2331 optics_detail[interface_name]["physical_channels"]["channel"] = [] 2332 2333 INVALID_LIGHT_LEVEL = [None, C.OPTICS_NULL_LEVEL, C.OPTICS_NULL_LEVEL_SPC] 2334 2335 # Defaulting avg, min, max values to 0.0 since device does not 2336 # return these values 2337 intf_optics = { 2338 "index": int(lane), 2339 "state": { 2340 "input_power": { 2341 "instant": ( 2342 float(optics["input_power"]) 2343 if optics["input_power"] not in INVALID_LIGHT_LEVEL 2344 else 0.0 2345 ), 2346 "avg": 0.0, 2347 "max": 0.0, 2348 "min": 0.0, 2349 }, 2350 "output_power": { 2351 "instant": ( 2352 float(optics["output_power"]) 2353 if optics["output_power"] not in INVALID_LIGHT_LEVEL 2354 else 0.0 2355 ), 2356 "avg": 0.0, 2357 "max": 0.0, 2358 "min": 0.0, 2359 }, 2360 "laser_bias_current": { 2361 "instant": ( 2362 float(optics["laser_bias_current"]) 2363 if optics["laser_bias_current"] not in INVALID_LIGHT_LEVEL 2364 else 0.0 2365 ), 2366 "avg": 0.0, 2367 "max": 0.0, 2368 "min": 0.0, 2369 }, 2370 }, 2371 } 2372 optics_detail[interface_name]["physical_channels"]["channel"].append( 2373 intf_optics 2374 ) 2375 2376 return optics_detail 2377 2378 def get_config(self, retrieve="all", full=False, sanitized=False): 2379 rv = {"startup": "", "running": "", "candidate": ""} 2380 2381 options = {"format": "text", "database": "candidate"} 2382 sanitize_strings = { 2383 r"^(\s+community\s+)\w+(;.*|\s+{.*)$": r"\1<removed>\2", 2384 r'^(.*)"\$\d\$\S+"(;.*)$': r"\1<removed>\2", 2385 } 2386 if retrieve in ("candidate", "all"): 2387 config = self.device.rpc.get_config(filter_xml=None, options=options) 2388 rv["candidate"] = str(config.text) 2389 if retrieve in ("running", "all"): 2390 options["database"] = "committed" 2391 config = self.device.rpc.get_config(filter_xml=None, options=options) 2392 rv["running"] = str(config.text) 2393 2394 if sanitized: 2395 return napalm.base.helpers.sanitize_configs(rv, sanitize_strings) 2396 2397 return rv 2398 2399 def get_network_instances(self, name=""): 2400 2401 network_instances = {} 2402 2403 ri_table = junos_views.junos_nw_instances_table(self.device) 2404 ri_table.get(options=self.junos_config_options) 2405 ri_entries = ri_table.items() 2406 2407 vrf_interfaces = [] 2408 2409 for ri_entry in ri_entries: 2410 ri_name = str(ri_entry[0]) 2411 ri_details = {d[0]: d[1] for d in ri_entry[1]} 2412 ri_type = ri_details["instance_type"] 2413 if ri_type is None: 2414 ri_type = "default" 2415 ri_rd = ri_details["route_distinguisher"] 2416 ri_interfaces = ri_details["interfaces"] 2417 if not isinstance(ri_interfaces, list): 2418 ri_interfaces = [ri_interfaces] 2419 network_instances[ri_name] = { 2420 "name": ri_name, 2421 "type": C.OC_NETWORK_INSTANCE_TYPE_MAP.get( 2422 ri_type, ri_type 2423 ), # default: return raw 2424 "state": {"route_distinguisher": ri_rd if ri_rd else ""}, 2425 "interfaces": { 2426 "interface": { 2427 intrf_name: {} for intrf_name in ri_interfaces if intrf_name 2428 } 2429 }, 2430 } 2431 vrf_interfaces.extend( 2432 network_instances[ri_name]["interfaces"]["interface"].keys() 2433 ) 2434 2435 all_interfaces = self.get_interfaces().keys() 2436 default_interfaces = list(set(all_interfaces) - set(vrf_interfaces)) 2437 if "default" not in network_instances: 2438 network_instances["default"] = { 2439 "name": "default", 2440 "type": C.OC_NETWORK_INSTANCE_TYPE_MAP.get("default"), 2441 "state": {"route_distinguisher": ""}, 2442 "interfaces": { 2443 "interface": { 2444 str(intrf_name): {} for intrf_name in default_interfaces 2445 } 2446 }, 2447 } 2448 2449 if not name: 2450 return network_instances 2451 if name not in network_instances: 2452 return {} 2453 return {name: network_instances[name]} 2454 2455 def get_vlans(self): 2456 result = {} 2457 switch_style = self.device.facts.get("switch_style", "") 2458 if switch_style == "VLAN_L2NG": 2459 vlan = junos_views.junos_vlans_table_switch_l2ng(self.device) 2460 elif switch_style == "BRIDGE_DOMAIN": 2461 vlan = junos_views.junos_vlans_table(self.device) 2462 elif switch_style == "VLAN": 2463 vlan = junos_views.junos_vlans_table_switch(self.device) 2464 else: # switch_style == "NONE" 2465 return result 2466 2467 vlan.get() 2468 unmatch_pattern = "l2rtb-interface-name|None|l2ng-l2rtb-vlan-member-interface" 2469 for vlan_id, vlan_data in vlan.items(): 2470 _vlan_data = {} 2471 for k, v in vlan_data: 2472 if k == "vlan_name": 2473 _vlan_data["name"] = v 2474 if k == "interfaces": 2475 if v is None: 2476 _vlan_data["interfaces"] = [] 2477 elif isinstance(v, str): 2478 if bool(re.match(unmatch_pattern, v)): 2479 _vlan_data["interfaces"] = [] 2480 else: 2481 _vlan_data["interfaces"] = [v.replace("*", "")] 2482 else: 2483 _vlan_data["interfaces"] = [_v.replace("*", "") for _v in v] 2484 2485 result[vlan_id] = _vlan_data 2486 return result 2487