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