# -*- coding: utf-8 -*-
# Copyright 2015 Spotify AB. All rights reserved.
#
# The contents of this file are licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# import stdlib
import re
import copy
import ipaddress
from collections import defaultdict
import logging

# import third party lib
from lxml import etree as ETREE

from napalm.pyIOSXR import IOSXR
from napalm.pyIOSXR.exceptions import ConnectError
from napalm.pyIOSXR.exceptions import TimeoutError
from napalm.pyIOSXR.exceptions import InvalidInputError
from napalm.pyIOSXR.exceptions import XMLCLIError

# import NAPALM base
import napalm.base.helpers
from napalm.base.netmiko_helpers import netmiko_args
from napalm.iosxr import constants as C
from napalm.base.base import NetworkDriver
from napalm.base.exceptions import ConnectionException
from napalm.base.exceptions import MergeConfigException
from napalm.base.exceptions import ReplaceConfigException
from napalm.base.exceptions import CommandTimeoutException

logger = logging.getLogger(__name__)
IP_RIBRoute = "IP_RIBRoute"


class IOSXRDriver(NetworkDriver):
    """IOS-XR driver class: inherits NetworkDriver from napalm.base."""

    def __init__(self, hostname, username, password, timeout=60, optional_args=None):
        self.hostname = hostname
        self.username = username
        self.password = password
        self.timeout = timeout
        self.pending_changes = False
        self.replace = False
        if optional_args is None:
            optional_args = {}
        self.lock_on_connect = optional_args.get("config_lock", False)

        self.netmiko_optional_args = netmiko_args(optional_args)
        try:
            self.port = self.netmiko_optional_args.pop("port")
        except KeyError:
            self.port = 22

        self.platform = "iosxr"
        self.device = IOSXR(
            hostname,
            username,
            password,
            timeout=timeout,
            port=self.port,
            lock=self.lock_on_connect,
            **self.netmiko_optional_args,
        )

    def open(self):
        try:
            self.device.open()
        except ConnectError as conn_err:
            logger.error(conn_err.args[0])
            raise ConnectionException(conn_err.args[0])

    def close(self):
        logger.debug("Closed connection with device %s" % (self.hostname))
        self.device.close()

    def is_alive(self):
        """Returns a flag with the state of the connection."""
        if self.device is None:
            return {"is_alive": False}
        # Simply returns the flag from pyIOSXR
        return {"is_alive": self.device.is_alive()}

    def load_replace_candidate(self, filename=None, config=None):
        self.pending_changes = True
        self.replace = True
        if not self.lock_on_connect:
            self.device.lock()

        try:
            self.device.load_candidate_config(filename=filename, config=config)
        except InvalidInputError as e:
            self.pending_changes = False
            self.replace = False
            logger.error(e.args[0])
            raise ReplaceConfigException(e.args[0])

    def load_merge_candidate(self, filename=None, config=None):
        self.pending_changes = True
        self.replace = False
        if not self.lock_on_connect:
            self.device.lock()

        try:
            self.device.load_candidate_config(filename=filename, config=config)
        except InvalidInputError as e:
            self.pending_changes = False
            self.replace = False
            logger.error(e.args[0])
            raise MergeConfigException(e.args[0])

    def compare_config(self):
        if not self.pending_changes:
            return ""
        elif self.replace:
            return self.device.compare_replace_config().strip()
        else:
            return self.device.compare_config().strip()

    def commit_config(self, message="", revert_in=None):
        if revert_in is not None:
            raise NotImplementedError(
                "Commit confirm has not been implemented on this platform."
            )
        commit_args = {"comment": message} if message else {}
        if self.replace:
            self.device.commit_replace_config(**commit_args)
        else:
            self.device.commit_config(**commit_args)
        self.pending_changes = False
        if not self.lock_on_connect:
            self.device.unlock()

    def discard_config(self):
        self.device.discard_config()
        self.pending_changes = False
        if not self.lock_on_connect:
            self.device.unlock()

    def rollback(self):
        self.device.rollback()

    def get_facts(self):
        facts = {
            "vendor": "Cisco",
            "os_version": "",
            "hostname": "",
            "uptime": -1.0,
            "serial_number": "",
            "fqdn": "",
            "model": "",
            "interface_list": [],
        }

        facts_rpc_request = """
<Get>
  <Operational>
    <SystemTime/>
    <PlatformInventory>
      <RackTable>
        <Rack>
          <Naming>
            <Name>0</Name>
          </Naming>
          <Attributes>
            <BasicInfo/>
          </Attributes>
        </Rack>
      </RackTable>
    </PlatformInventory>
  </Operational>
</Get>
        """

        # IOS-XR 7.3.3 and possibly other 7.X versions have this located in
        # different location in the XML tree
        facts_rpc_request_alt = """
<Get>
  <Operational>
    <SystemTime/>
    <Inventory>
      <Entities>
        <Entity>
          <Naming>
            <Name>Rack 0</Name>
          </Naming>
          <Attributes>
            <InvBasicBag></InvBasicBag>
          </Attributes>
        </Entity>
      </Entities>
    </Inventory>
  </Operational>
</Get>
"""

        system_time_xpath = ".//SystemTime/Uptime"
        try:
            facts_rpc_reply = ETREE.fromstring(
                self.device.make_rpc_call(facts_rpc_request)
            )
            platform_attr_xpath = ".//RackTable/Rack/Attributes/BasicInfo"
        except XMLCLIError:
            facts_rpc_reply = ETREE.fromstring(
                self.device.make_rpc_call(facts_rpc_request_alt)
            )
            platform_attr_xpath = ".//Entities/Entity/Attributes/InvBasicBag"

        system_time_tree = facts_rpc_reply.xpath(system_time_xpath)[0]
        try:
            platform_attr_tree = facts_rpc_reply.xpath(platform_attr_xpath)[0]
        except IndexError:
            platform_attr_tree = facts_rpc_reply.xpath(platform_attr_xpath)

        hostname = napalm.base.helpers.convert(
            str, napalm.base.helpers.find_txt(system_time_tree, "Hostname")
        )
        uptime = napalm.base.helpers.convert(
            float, napalm.base.helpers.find_txt(system_time_tree, "Uptime"), -1.0
        )
        serial = napalm.base.helpers.convert(
            str, napalm.base.helpers.find_txt(platform_attr_tree, "SerialNumber")
        )
        os_version = napalm.base.helpers.convert(
            str, napalm.base.helpers.find_txt(platform_attr_tree, "SoftwareRevision")
        )
        model = napalm.base.helpers.convert(
            str, napalm.base.helpers.find_txt(platform_attr_tree, "ModelName")
        )
        interface_list = sorted(list(self.get_interfaces().keys()))

        facts.update(
            {
                "os_version": os_version,
                "hostname": hostname,
                "model": model,
                "uptime": uptime,
                "serial_number": serial,
                "fqdn": hostname,
                "interface_list": interface_list,
            }
        )

        return facts

    def get_interfaces(self):
        interfaces = {}

        INTERFACE_DEFAULTS = {
            "is_enabled": False,
            "is_up": False,
            "mac_address": "",
            "description": "",
            "speed": -1.0,
            "last_flapped": -1.0,
        }

        interfaces_rpc_request = "<Get><Operational><Interfaces/></Operational></Get>"

        interfaces_rpc_reply = ETREE.fromstring(
            self.device.make_rpc_call(interfaces_rpc_request)
        )

        for interface_tree in interfaces_rpc_reply.xpath(
            ".//Interfaces/InterfaceTable/Interface"
        ):
            interface_name = napalm.base.helpers.find_txt(
                interface_tree, "Naming/InterfaceName"
            )
            if not interface_name:
                continue
            is_up = (
                napalm.base.helpers.find_txt(interface_tree, "LineState")
                == "IM_STATE_UP"
            )
            enabled = (
                napalm.base.helpers.find_txt(interface_tree, "State")
                != "IM_STATE_ADMINDOWN"
            )
            raw_mac = napalm.base.helpers.find_txt(interface_tree, "MACAddress/Address")
            mac_address = napalm.base.helpers.convert(
                napalm.base.helpers.mac, raw_mac, raw_mac
            )
            speed = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.convert(
                    float, napalm.base.helpers.find_txt(interface_tree, "Bandwidth"), 0
                )
                * 1e-3,
            )

            mtu = int(napalm.base.helpers.find_txt(interface_tree, "MTU"))
            description = napalm.base.helpers.find_txt(interface_tree, "Description")
            last_flapped = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    interface_tree, "LastStateTransitionTime", -1
                ),
                -1,
            )
            interfaces[interface_name] = copy.deepcopy(INTERFACE_DEFAULTS)
            interfaces[interface_name].update(
                {
                    "is_up": is_up,
                    "speed": speed,
                    "mtu": mtu,
                    "is_enabled": enabled,
                    "mac_address": mac_address,
                    "description": description,
                    "last_flapped": (
                        last_flapped / 1e9 if last_flapped != -1.0 else -1.0
                    ),
                }
            )

        return interfaces

    def get_interfaces_counters(self):
        rpc_command = "<Get><Operational><Interfaces><InterfaceTable></InterfaceTable>\
        </Interfaces></Operational></Get>"
        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        interface_counters = {}

        for interface in result_tree.xpath(".//Interface"):
            interface_name = napalm.base.helpers.find_txt(interface, "InterfaceHandle")
            interface_stats = {}
            if not interface.xpath("InterfaceStatistics"):
                continue
            else:
                interface_stats = {}
                interface_stats["tx_multicast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/MulticastPacketsSent",
                    ),
                )
                interface_stats["tx_discards"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/OutputDrops"
                    ),
                )
                interface_stats["tx_octets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/BytesSent"
                    ),
                )
                interface_stats["tx_errors"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/OutputErrors"
                    ),
                )
                interface_stats["rx_octets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/BytesReceived",
                    ),
                )
                interface_stats["tx_unicast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/PacketsSent"
                    ),
                )
                interface_stats["rx_errors"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/InputErrors"
                    ),
                )
                interface_stats["tx_broadcast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/BroadcastPacketsSent",
                    ),
                )
                interface_stats["rx_multicast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/MulticastPacketsReceived",
                    ),
                )
                interface_stats["rx_broadcast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/BroadcastPacketsReceived",
                    ),
                )
                interface_stats["rx_discards"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface, "InterfaceStatistics/FullInterfaceStats/InputDrops"
                    ),
                )
                interface_stats["rx_unicast_packets"] = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        interface,
                        "InterfaceStatistics/FullInterfaceStats/PacketsReceived",
                    ),
                )
            interface_counters[interface_name] = interface_stats

        return interface_counters

    def get_bgp_neighbors(self):
        def generate_vrf_query(vrf_name):
            """
            Helper to provide XML-query for the VRF-type we're interested in.
            """
            if vrf_name == "global":
                rpc_command = "<Get><Operational><BGP><InstanceTable><Instance><Naming>\
                <InstanceName>default</InstanceName></Naming><InstanceActive><DefaultVRF>\
                <GlobalProcessInfo></GlobalProcessInfo><NeighborTable></NeighborTable></DefaultVRF>\
                </InstanceActive></Instance></InstanceTable></BGP></Operational></Get>"

            else:
                rpc_command = "<Get><Operational><BGP><InstanceTable><Instance><Naming>\
                <InstanceName>default</InstanceName></Naming><InstanceActive><VRFTable><VRF>\
                <Naming>{vrf_name}</Naming><GlobalProcessInfo></GlobalProcessInfo><NeighborTable>\
                </NeighborTable></VRF></VRFTable></InstanceActive></Instance></InstanceTable>\
                </BGP></Operational></Get>".format(
                    vrf_name=vrf_name
                )
            return rpc_command

        """
        Initial run to figure out what VRF's are available
        Decided to get this one from Configured-section
        because bulk-getting all instance-data to do the same could get ridiculously heavy
        Assuming we're always interested in the DefaultVRF
        """

        active_vrfs = ["global"]

        rpc_command = "<Get><Operational><BGP><ConfigInstanceTable><ConfigInstance><Naming>\
        <InstanceName>default</InstanceName></Naming><ConfigInstanceVRFTable>\
        </ConfigInstanceVRFTable></ConfigInstance></ConfigInstanceTable></BGP></Operational></Get>"

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for node in result_tree.xpath(".//ConfigVRF"):
            active_vrfs.append(napalm.base.helpers.find_txt(node, "Naming/VRFName"))

        result = {}

        for vrf in active_vrfs:
            rpc_command = generate_vrf_query(vrf)
            result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

            this_vrf = {}
            this_vrf["peers"] = {}

            if vrf == "global":
                this_vrf["router_id"] = napalm.base.helpers.convert(
                    str,
                    napalm.base.helpers.find_txt(
                        result_tree,
                        "Get/Operational/BGP/InstanceTable/Instance/InstanceActive/DefaultVRF"
                        "/GlobalProcessInfo/VRF/RouterID",
                    ),
                )
            else:
                this_vrf["router_id"] = napalm.base.helpers.convert(
                    str,
                    napalm.base.helpers.find_txt(
                        result_tree,
                        "Get/Operational/BGP/InstanceTable/Instance/InstanceActive/VRFTable/VRF"
                        "/GlobalProcessInfo/VRF/RouterID",
                    ),
                )

            neighbors = {}

            for neighbor in result_tree.xpath(".//Neighbor"):
                this_neighbor = {}
                this_neighbor["local_as"] = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "LocalAS")
                )
                this_neighbor["remote_as"] = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "RemoteAS")
                )
                this_neighbor["remote_id"] = napalm.base.helpers.convert(
                    str, napalm.base.helpers.find_txt(neighbor, "RouterID")
                )

                if (
                    napalm.base.helpers.find_txt(neighbor, "ConnectionAdminStatus")
                    == "1"
                ):
                    this_neighbor["is_enabled"] = True

                try:
                    this_neighbor["description"] = napalm.base.helpers.convert(
                        str, napalm.base.helpers.find_txt(neighbor, "Description")
                    )
                except AttributeError:
                    logger.debug(
                        "No attribute 'description' for neighbor %s"
                        % (this_neighbor["remote_as"])
                    )
                    this_neighbor["description"] = ""

                this_neighbor["is_enabled"] = (
                    napalm.base.helpers.find_txt(neighbor, "ConnectionAdminStatus")
                    == "1"
                )

                if (
                    str(napalm.base.helpers.find_txt(neighbor, "ConnectionAdminStatus"))
                    == "1"
                ):
                    this_neighbor["is_enabled"] = True
                else:
                    this_neighbor["is_enabled"] = False

                if (
                    str(napalm.base.helpers.find_txt(neighbor, "ConnectionState"))
                    == "BGP_ST_ESTAB"
                ):
                    this_neighbor["is_up"] = True
                    this_neighbor["uptime"] = napalm.base.helpers.convert(
                        int,
                        napalm.base.helpers.find_txt(
                            neighbor, "ConnectionEstablishedTime"
                        ),
                    )
                else:
                    this_neighbor["is_up"] = False
                    this_neighbor["uptime"] = -1

                this_neighbor["address_family"] = {}

                if (
                    napalm.base.helpers.find_txt(
                        neighbor, "ConnectionRemoteAddress/AFI"
                    )
                    == "IPv4"
                ):
                    this_afi = "ipv4"
                elif (
                    napalm.base.helpers.find_txt(
                        neighbor, "ConnectionRemoteAddress/AFI"
                    )
                    == "IPv6"
                ):
                    this_afi = "ipv6"
                else:
                    this_afi = napalm.base.helpers.find_txt(
                        neighbor, "ConnectionRemoteAddress/AFI"
                    )

                this_neighbor["address_family"][this_afi] = {}

                try:
                    this_neighbor["address_family"][this_afi][
                        "received_prefixes"
                    ] = napalm.base.helpers.convert(
                        int,
                        napalm.base.helpers.find_txt(
                            neighbor, "AFData/Entry/PrefixesAccepted"
                        ),
                        0,
                    ) + napalm.base.helpers.convert(
                        int,
                        napalm.base.helpers.find_txt(
                            neighbor, "AFData/Entry/PrefixesDenied"
                        ),
                        0,
                    )
                    this_neighbor["address_family"][this_afi]["accepted_prefixes"] = (
                        napalm.base.helpers.convert(
                            int,
                            napalm.base.helpers.find_txt(
                                neighbor, "AFData/Entry/PrefixesAccepted"
                            ),
                            0,
                        )
                    )
                    this_neighbor["address_family"][this_afi]["sent_prefixes"] = (
                        napalm.base.helpers.convert(
                            int,
                            napalm.base.helpers.find_txt(
                                neighbor, "AFData/Entry/PrefixesAdvertised"
                            ),
                            0,
                        )
                    )
                except AttributeError:
                    this_neighbor["address_family"][this_afi]["received_prefixes"] = -1
                    this_neighbor["address_family"][this_afi]["accepted_prefixes"] = -1
                    this_neighbor["address_family"][this_afi]["sent_prefixes"] = -1

                neighbor_ip = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(
                        neighbor, "Naming/NeighborAddress/IPV4Address"
                    )
                    or napalm.base.helpers.find_txt(
                        neighbor, "Naming/NeighborAddress/IPV6Address"
                    )
                )

                neighbors[neighbor_ip] = this_neighbor

            this_vrf["peers"] = neighbors
            result[vrf] = this_vrf

        return result

    def get_environment(self):
        def get_module_xml_query(module, selection):
            return "<Get><AdminOperational><EnvironmentalMonitoring><RackTable><Rack><Naming>\
            <rack>0</rack></Naming><SlotTable><Slot><Naming><slot>{slot}</slot></Naming>{name}\
            </Slot></SlotTable></Rack></RackTable></EnvironmentalMonitoring></AdminOperational>\
            </Get>".format(
                slot=module, name=selection
            )

        environment_status = {}
        environment_status["fans"] = {}
        environment_status["temperature"] = {}
        environment_status["power"] = {}
        environment_status["cpu"] = {}
        environment_status["memory"] = 0.0

        # finding slots with equipment we're interested in
        rpc_command = "<Get><AdminOperational><PlatformInventory><RackTable><Rack><Naming>\
        <Name>0</Name></Naming><SlotTable></SlotTable></Rack></RackTable></PlatformInventory>\
        </AdminOperational></Get>"

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        active_modules = defaultdict(list)

        for slot in result_tree.xpath(".//Slot"):
            for card in slot.xpath(".//CardTable"):
                # find enabled slots, figoure out type and save for later
                if (
                    napalm.base.helpers.find_txt(
                        card, "Card/Attributes/FRUInfo/ModuleAdministrativeState"
                    )
                    == "ADMIN_UP"
                ):
                    slot_name = napalm.base.helpers.find_txt(slot, "Naming/Name")
                    module_type = re.sub(r"\d+", "", slot_name)
                    if len(module_type) > 0:
                        active_modules[module_type].append(slot_name)
                    else:
                        active_modules["LC"].append(slot_name)

        #
        # PSU's
        #

        for psu in active_modules["PM"]:
            if psu in ["PM6", "PM7"]:  # Cisco bug, chassis difference V01<->V02
                continue

            rpc_command = get_module_xml_query(psu, "")
            result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

            psu_status = {}
            psu_status["status"] = False
            psu_status["capacity"] = 0.0
            psu_status["output"] = 0.0

            for sensor in result_tree.xpath(".//SensorName"):
                if napalm.base.helpers.find_txt(sensor, "Naming/Name") == "host__VOLT":
                    this_psu_voltage = napalm.base.helpers.convert(
                        float, napalm.base.helpers.find_txt(sensor, "ValueBrief")
                    )
                elif (
                    napalm.base.helpers.find_txt(sensor, "Naming/Name") == "host__CURR"
                ):
                    this_psu_current = napalm.base.helpers.convert(
                        float, napalm.base.helpers.find_txt(sensor, "ValueBrief")
                    )
                elif napalm.base.helpers.find_txt(sensor, "Naming/Name") == "host__PM":
                    this_psu_capacity = napalm.base.helpers.convert(
                        float, napalm.base.helpers.find_txt(sensor, "ValueBrief")
                    )

            if this_psu_capacity > 0:
                psu_status["capacity"] = this_psu_capacity
                psu_status["status"] = True

            if this_psu_current and this_psu_voltage:
                psu_status["output"] = (
                    this_psu_voltage * this_psu_current
                ) / 1_000_000.0

            environment_status["power"][psu] = psu_status

        #
        # Memory
        #

        facts = self.get_facts()
        router_model = facts.get("model")
        xr_version = facts.get("os_version")
        major_version = (
            int(xr_version.split(".")[0])
            if xr_version
            and isinstance(xr_version, str)
            and xr_version.split(".")[0].isnumeric()
            else 0
        )
        is_xrv = router_model.lower().startswith("xrv")
        environment_status["memory"] = {"available_ram": 0.0, "used_ram": 0.0}

        if not is_xrv:
            if major_version >= 7:
                rpc_command = "<Get><Operational><MemorySummary>\
                </MemorySummary></Operational></Get>"
            else:
                rpc_command = "<Get><AdminOperational><MemorySummary>\
                </MemorySummary></AdminOperational></Get>"
            result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

            for node in result_tree.xpath(".//Node"):
                if (
                    napalm.base.helpers.find_txt(node, "Naming/NodeName/Slot")
                    == active_modules["RSP"][0]
                ):
                    available_ram = napalm.base.helpers.convert(
                        int,
                        napalm.base.helpers.find_txt(node, "Summary/SystemRAMMemory"),
                    )
                    free_ram = napalm.base.helpers.convert(
                        int,
                        napalm.base.helpers.find_txt(
                            node, "Summary/FreeApplicationMemory"
                        ),
                    )
                    break  # we're only looking at one of the RSP's

            if available_ram and free_ram:
                used_ram = available_ram - free_ram
                memory = {}
                memory["available_ram"] = available_ram
                memory["used_ram"] = used_ram
                environment_status["memory"] = memory

            #
            # Fans
            #

            for fan in active_modules["FT"]:
                rpc_command = get_module_xml_query(fan, "")
                result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))
                for module in result_tree.xpath(".//Module"):
                    for sensortype in module.xpath(".//SensorType"):
                        for sensorname in sensortype.xpath(".//SensorNameTable"):
                            if (
                                napalm.base.helpers.find_txt(
                                    sensorname, "SensorName/Naming/Name"
                                )
                                == "host__FanSpeed_0"
                            ):
                                environment_status["fans"][fan] = {
                                    "status": napalm.base.helpers.convert(
                                        int,
                                        napalm.base.helpers.find_txt(
                                            sensorname,
                                            "SensorName/ValueDetailed/Status",
                                        ),
                                    )
                                    == 1
                                }

        #
        # CPU
        #
        cpu = {}

        rpc_command = "<Get><Operational><SystemMonitoring></SystemMonitoring></Operational></Get>"
        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for module in result_tree.xpath(".//CPUUtilization"):
            this_cpu = {}
            this_cpu["%usage"] = napalm.base.helpers.convert(
                float, napalm.base.helpers.find_txt(module, "TotalCPUFiveMinute")
            )

            rack = napalm.base.helpers.find_txt(module, "Naming/NodeName/Rack")
            slot = napalm.base.helpers.find_txt(module, "Naming/NodeName/Slot")
            instance = napalm.base.helpers.find_txt(module, "Naming/NodeName/Instance")
            position = "%s/%s/%s" % (rack, slot, instance)

            cpu[position] = this_cpu

        environment_status["cpu"] = cpu

        #
        # Temperature
        #

        slot_list = set()
        for category, slot in active_modules.items():
            slot_list |= set(slot)

        if not is_xrv:
            for slot in slot_list:
                rpc_command = get_module_xml_query(slot, "")
                result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))
                for sensor in result_tree.xpath(".//SensorName"):
                    if (
                        not napalm.base.helpers.find_txt(sensor, "Naming/Name")
                        == "host__Inlet0"
                    ):
                        continue
                    this_reading = {}
                    this_reading["temperature"] = napalm.base.helpers.convert(
                        float, napalm.base.helpers.find_txt(sensor, "ValueBrief")
                    )
                    threshold_value = [
                        napalm.base.helpers.convert(float, x.text)
                        for x in sensor.xpath("ThresholdTable/Threshold/ValueBrief")
                    ]
                    this_reading["is_alert"] = (
                        threshold_value[2]
                        <= this_reading["temperature"]
                        <= threshold_value[3]
                    )
                    this_reading["is_critical"] = (
                        threshold_value[4]
                        <= this_reading["temperature"]
                        <= threshold_value[5]
                    )
                    this_reading["temperature"] = this_reading["temperature"] / 10
                    environment_status["temperature"][slot] = this_reading

        return environment_status

    def get_lldp_neighbors(self):
        # init result dict
        lldp = {}
        sh_lldp = self.device.show_lldp_neighbors().splitlines()[5:-3]

        for n in sh_lldp:
            local_interface = n.split()[1]
            if local_interface not in lldp.keys():
                lldp[local_interface] = []

            lldp[local_interface].append(
                {
                    "hostname": napalm.base.helpers.convert(str, n.split()[0]),
                    "port": napalm.base.helpers.convert(str, n.split()[4]),
                }
            )

        return lldp

    def get_lldp_neighbors_detail(self, interface=""):
        lldp_neighbors = {}

        rpc_command = (
            "<Get><Operational>"
            "<LLDP><NodeTable></NodeTable></LLDP>"
            "</Operational></Get>"
        )

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for neighbor in result_tree.xpath(".//Neighbors/DetailTable/Detail/Entry"):
            interface_name = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "ReceivingInterfaceName")
            )
            parent_interface = napalm.base.helpers.convert(
                str,
                napalm.base.helpers.find_txt(neighbor, "ReceivingParentInterfaceName"),
            )
            chassis_id_raw = napalm.base.helpers.find_txt(neighbor, "ChassisID")
            chassis_id = napalm.base.helpers.convert(
                napalm.base.helpers.mac, chassis_id_raw, chassis_id_raw
            )
            port_id = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "PortIDDetail")
            )
            port_descr = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "Detail/PortDescription")
            )
            system_name = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "Detail/SystemName")
            )
            system_descr = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "Detail/SystemDescription")
            )
            system_capabilities = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(neighbor, "Detail/SystemCapabilities")
            )
            enabled_capabilities = napalm.base.helpers.convert(
                str,
                napalm.base.helpers.find_txt(neighbor, "Detail/EnabledCapabilities"),
            )

            if interface_name not in lldp_neighbors.keys():
                lldp_neighbors[interface_name] = []
            lldp_neighbors[interface_name].append(
                {
                    "parent_interface": parent_interface,
                    "remote_chassis_id": chassis_id,
                    "remote_port": port_id,
                    "remote_port_description": port_descr,
                    "remote_system_name": system_name,
                    "remote_system_description": system_descr,
                    "remote_system_capab": napalm.base.helpers.transform_lldp_capab(
                        system_capabilities
                    ),
                    "remote_system_enable_capab": napalm.base.helpers.transform_lldp_capab(
                        enabled_capabilities
                    ),
                }
            )

        return lldp_neighbors

    def cli(self, commands, encoding="text"):
        if encoding not in ("text",):
            raise NotImplementedError("%s is not a supported encoding" % encoding)

        cli_output = {}

        if type(commands) is not list:
            raise TypeError("Please enter a valid list of commands!")

        for command in commands:
            try:
                cli_output[str(command)] = str(self.device._execute_show(command))
            except TimeoutError:
                cli_output[str(command)] = (
                    'Execution of command \
                    "{command}" took too long! Please adjust your params!'.format(
                        command=command
                    )
                )
                logger.error(str(cli_output))
                raise CommandTimeoutException(str(cli_output))

        return cli_output

    def get_bgp_config(self, group="", neighbor=""):
        bgp_config = {}

        # a helper
        def build_prefix_limit(af_table, limit, prefix_percent, prefix_timeout):
            prefix_limit = {}
            inet = False
            inet6 = False
            preifx_type = "inet"
            if "ipv4" in af_table.lower():
                inet = True
            if "ipv6" in af_table.lower():
                inet6 = True
                preifx_type = "inet6"
            if inet or inet6:
                prefix_limit = {
                    preifx_type: {
                        af_table[4:].lower(): {
                            "limit": limit,
                            "teardown": {
                                "threshold": prefix_percent,
                                "timeout": prefix_timeout,
                            },
                        }
                    }
                }
            return prefix_limit

        # here begins actual method...

        rpc_command = "<Get><Configuration><BGP><Instance><Naming>\
        <InstanceName>default</InstanceName></Naming></Instance></BGP></Configuration></Get>"
        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        # Check if BGP is not configured.
        get_tag = result_tree.find("./Get")
        if get_tag is not None:
            bgp_not_found = get_tag.attrib.get("ItemNotFound")
            if bgp_not_found:
                return {}

        bgp_asn = napalm.base.helpers.convert(
            int,
            napalm.base.helpers.find_txt(
                result_tree,
                "Get/Configuration/BGP/Instance[1]/InstanceAS/FourByteAS/Naming/AS",
            ),
            0,
        )

        if not group:
            neighbor = ""

        bgp_group_neighbors = {}
        for bgp_neighbor in result_tree.xpath(".//Neighbor"):
            group_name = napalm.base.helpers.find_txt(
                bgp_neighbor, "NeighborGroupAddMember"
            )
            peer = napalm.base.helpers.ip(
                napalm.base.helpers.find_txt(
                    bgp_neighbor, "Naming/NeighborAddress/IPV4Address"
                )
                or napalm.base.helpers.find_txt(
                    bgp_neighbor, "Naming/NeighborAddress/IPV6Address"
                )
            )
            if neighbor and peer != neighbor:
                continue
            description = napalm.base.helpers.find_txt(bgp_neighbor, "Description")
            peer_as = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(bgp_neighbor, "RemoteAS/AS_YY"), 0
            )
            local_as = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(bgp_neighbor, "LocalAS/AS_YY"),
                bgp_asn,
            )
            af_table = napalm.base.helpers.find_txt(
                bgp_neighbor, "NeighborAFTable/NeighborAF/Naming/AFName"
            )
            prefix_limit = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_neighbor,
                    "NeighborAFTable/NeighborAF/MaximumPrefixes/PrefixLimit",
                ),
                0,
            )
            prefix_percent = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_neighbor,
                    "NeighborAFTable/NeighborAF/MaximumPrefixes/WarningPercentage",
                ),
                0,
            )
            prefix_timeout = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_neighbor,
                    "NeighborAFTable/NeighborAF/MaximumPrefixes/RestartTime",
                ),
                0,
            )
            import_policy = napalm.base.helpers.find_txt(
                bgp_neighbor, "NeighborAFTable/NeighborAF/RoutePolicyIn"
            )
            export_policy = napalm.base.helpers.find_txt(
                bgp_neighbor, "NeighborAFTable/NeighborAF/RoutePolicyOut"
            )
            local_addr_raw = napalm.base.helpers.find_txt(
                bgp_neighbor, "LocalAddress/LocalIPAddress/IPV4Address"
            ) or napalm.base.helpers.find_txt(
                bgp_neighbor, "LocalAddress/LocalIPAddress/IPV6Address"
            )
            local_address = napalm.base.helpers.convert(
                napalm.base.helpers.ip, local_addr_raw, local_addr_raw
            )
            password = napalm.base.helpers.find_txt(
                bgp_neighbor, "Password/Password/Password"
            )
            nhs = False
            route_reflector = False
            if group_name not in bgp_group_neighbors.keys():
                bgp_group_neighbors[group_name] = {}
            bgp_group_neighbors[group_name][peer] = {
                "description": description,
                "remote_as": peer_as,
                "prefix_limit": build_prefix_limit(
                    af_table, prefix_limit, prefix_percent, prefix_timeout
                ),
                "export_policy": export_policy,
                "import_policy": import_policy,
                "local_address": local_address,
                "local_as": local_as,
                "authentication_key": password,
                "nhs": nhs,
                "route_reflector_client": route_reflector,
            }
            if neighbor and peer == neighbor:
                break

        for bgp_group in result_tree.xpath(".//NeighborGroup"):
            group_name = napalm.base.helpers.find_txt(
                bgp_group, "Naming/NeighborGroupName"
            )
            if group and group != group_name:
                continue
            bgp_type = "external"  # by default external
            # must check
            description = napalm.base.helpers.find_txt(bgp_group, "Description")
            import_policy = napalm.base.helpers.find_txt(
                bgp_group, "NeighborGroupAFTable/NeighborGroupAF/RoutePolicyIn"
            )
            export_policy = napalm.base.helpers.find_txt(
                bgp_group, "NeighborGroupAFTable/NeighborGroupAF/RoutePolicyOut"
            )
            multipath = (
                napalm.base.helpers.find_txt(
                    bgp_group, "NeighborGroupAFTable/NeighborGroupAF/Multipath"
                )
                == "true"
            )
            peer_as = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(bgp_group, "RemoteAS/AS_YY"), 0
            )
            local_as = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(bgp_group, "LocalAS/AS_YY"), bgp_asn
            )
            multihop_ttl = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(bgp_group, "EBGPMultihop/MaxHopCount"),
                0,
            )
            local_addr_raw = napalm.base.helpers.find_txt(
                bgp_group, "LocalAddress/LocalIPAddress/IPV4Address"
            ) or napalm.base.helpers.find_txt(
                bgp_group, "LocalAddress/LocalIPAddress/IPV6Address"
            )
            local_address = napalm.base.helpers.convert(
                napalm.base.helpers.ip, local_addr_raw, local_addr_raw
            )
            af_table = napalm.base.helpers.find_txt(
                bgp_group, "NeighborAFTable/NeighborAF/Naming/AFName"
            )
            prefix_limit = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_group,
                    "NeighborGroupAFTable/NeighborGroupAF/MaximumPrefixes/PrefixLimit",
                ),
                0,
            )
            prefix_percent = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_group,
                    "NeighborGroupAFTable/NeighborGroupAF/MaximumPrefixes/WarningPercentage",
                ),
                0,
            )
            prefix_timeout = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    bgp_group,
                    "NeighborGroupAFTable/NeighborGroupAF/MaximumPrefixes/RestartTime",
                ),
                0,
            )
            remove_private = True  # is it specified in the XML?
            bgp_config[group_name] = {
                "apply_groups": [],  # on IOS-XR will always be empty list!
                "description": description,
                "local_as": local_as,
                "type": str(bgp_type),
                "import_policy": import_policy,
                "export_policy": export_policy,
                "local_address": local_address,
                "multipath": multipath,
                "multihop_ttl": multihop_ttl,
                "remote_as": peer_as,
                "remove_private_as": remove_private,
                "prefix_limit": build_prefix_limit(
                    af_table, prefix_limit, prefix_percent, prefix_timeout
                ),
                "neighbors": bgp_group_neighbors.get(group_name, {}),
            }
            if group and group == group_name:
                break

        bgp_config["_"] = {
            "apply_groups": [],
            "description": "",
            "local_as": bgp_asn,
            "type": "",
            "import_policy": "",
            "export_policy": "",
            "local_address": "",
            "multipath": False,
            "multihop_ttl": 0,
            "remote_as": 0,
            "remove_private_as": False,
            "prefix_limit": {},
            "neighbors": bgp_group_neighbors.get("", {}),
        }

        return bgp_config

    def get_bgp_neighbors_detail(self, neighbor_address=""):
        bgp_neighbors_detail = {}

        active_vrfs = ["default"]

        active_vrfs_rpc_request = "<Get><Operational><BGP><ConfigInstanceTable><ConfigInstance>\
        <Naming><InstanceName>default</InstanceName></Naming><ConfigInstanceVRFTable/>\
        </ConfigInstance></ConfigInstanceTable></BGP></Operational></Get>"

        active_vrfs_rpc_reply = ETREE.fromstring(
            self.device.make_rpc_call(active_vrfs_rpc_request)
        )
        active_vrfs_tree = active_vrfs_rpc_reply.xpath(".//ConfigVRF")

        for active_vrf_tree in active_vrfs_tree:
            active_vrfs.append(
                napalm.base.helpers.find_txt(active_vrf_tree, "Naming/VRFName")
            )

        unique_active_vrfs = sorted(set(active_vrfs))

        bgp_neighbors_vrf_all_rpc = "<Get><Operational><BGP><InstanceTable><Instance><Naming>\
        <InstanceName>default</InstanceName></Naming>"

        for active_vrf in unique_active_vrfs:
            vrf_rpc = "<InstanceActive><VRFTable><VRF><Naming>{vrf_name}</Naming>\
            <GlobalProcessInfo/><NeighborTable/></VRF></VRFTable></InstanceActive>"
            bgp_neighbors_vrf_all_rpc += vrf_rpc.format(vrf_name=active_vrf)

        bgp_neighbors_vrf_all_rpc += (
            "</Instance></InstanceTable></BGP></Operational></Get>"
        )

        bgp_neighbors_vrf_all_tree = ETREE.fromstring(
            self.device.make_rpc_call(bgp_neighbors_vrf_all_rpc)
        )

        _BGP_STATE_ = {
            "0": "Unknown",
            "1": "Idle",
            "2": "Connect",
            "3": "OpenSent",
            "4": "OpenConfirm",
            "5": "Active",
            "6": "Established",
        }

        instance_active_list = bgp_neighbors_vrf_all_tree.xpath(
            ".//InstanceTable/Instance/InstanceActive/VRFTable/VRF"
        )

        for vrf_tree in instance_active_list:
            vrf_name = napalm.base.helpers.find_txt(vrf_tree, "Naming/VRFName")
            vrf_keepalive = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    vrf_tree, "GlobalProcessInfo/VRF/KeepAliveTime"
                ),
            )
            vrf_holdtime = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    vrf_tree, "GlobalProcessInfo/VRF/HoldTime"
                ),
            )
            if vrf_name not in bgp_neighbors_detail.keys():
                bgp_neighbors_detail[vrf_name] = {}
            for neighbor in vrf_tree.xpath("NeighborTable/Neighbor"):
                up = (
                    napalm.base.helpers.find_txt(neighbor, "ConnectionState")
                    == "BGP_ST_ESTAB"
                )
                local_as = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "LocalAS"), 0
                )
                remote_as = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "RemoteAS"), 0
                )
                router_id = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(neighbor, "RouterID")
                )
                remote_address = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(
                        neighbor, "Naming/NeighborAddress/IPV4Address"
                    )
                    or napalm.base.helpers.find_txt(
                        neighbor, "Naming/NeighborAddress/IPV6Address"
                    )
                )
                local_address_configured = (
                    napalm.base.helpers.find_txt(neighbor, "IsLocalAddressConfigured")
                    == "true"
                )
                local_address = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(
                        neighbor, "ConnectionLocalAddress/IPV4Address"
                    )
                    or napalm.base.helpers.find_txt(
                        neighbor, "ConnectionLocalAddress/IPV6Address"
                    )
                )
                local_port = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "ConnectionLocalPort")
                )
                remote_address = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(
                        neighbor, "ConnectionRemoteAddress/IPV4Address"
                    )
                    or napalm.base.helpers.find_txt(
                        neighbor, "ConnectionRemoteAddress/IPV6Address"
                    )
                )
                remote_port = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "ConnectionRemotePort")
                )
                multihop = (
                    napalm.base.helpers.find_txt(
                        neighbor, "IsExternalNeighborNotDirectlyConnected"
                    )
                    == "true"
                )
                remove_private_as = (
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/RemovePrivateASFromUpdates"
                    )
                    == "true"
                )
                multipath = (
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/SelectiveMultipathEligible"
                    )
                    == "true"
                )
                import_policy = napalm.base.helpers.find_txt(
                    neighbor, "AFData/Entry/RoutePolicyIn"
                )
                export_policy = napalm.base.helpers.find_txt(
                    neighbor, "AFData/Entry/RoutePolicyOut"
                )
                input_messages = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "MessgesReceived"), 0
                )
                output_messages = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "MessagesSent"), 0
                )
                flap_count = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(neighbor, "ConnectionDownCount"),
                    0,
                )
                messages_queued_out = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "MessagesQueuedOut"), 0
                )
                connection_state = (
                    napalm.base.helpers.find_txt(neighbor, "ConnectionState")
                    .replace("BGP_ST_", "")
                    .title()
                )
                if connection_state == "Estab":
                    connection_state = "Established"
                previous_connection_state = napalm.base.helpers.convert(
                    str,
                    _BGP_STATE_.get(
                        napalm.base.helpers.find_txt(
                            neighbor, "PreviousConnectionState", "0"
                        )
                    ),
                )
                active_prefix_count = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/NumberOfBestpaths"
                    ),
                    0,
                )
                accepted_prefix_count = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/PrefixesAccepted"
                    ),
                    0,
                )
                suppressed_prefix_count = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/PrefixesDenied"
                    ),
                    0,
                )
                received_prefix_count = accepted_prefix_count + suppressed_prefix_count
                advertised_prefix_count = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(
                        neighbor, "AFData/Entry/PrefixesAdvertised"
                    ),
                    0,
                )
                suppress_4byte_as = (
                    napalm.base.helpers.find_txt(neighbor, "Suppress4ByteAs") == "true"
                )
                local_as_prepend = (
                    napalm.base.helpers.find_txt(neighbor, "LocalASNoPrepend") != "true"
                )
                holdtime = (
                    napalm.base.helpers.convert(
                        int, napalm.base.helpers.find_txt(neighbor, "HoldTime"), 0
                    )
                    or vrf_holdtime
                )
                configured_holdtime = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(neighbor, "ConfiguredHoldTime"), 0
                )
                keepalive = (
                    napalm.base.helpers.convert(
                        int, napalm.base.helpers.find_txt(neighbor, "KeepAliveTime"), 0
                    )
                    or vrf_keepalive
                )
                configured_keepalive = napalm.base.helpers.convert(
                    int,
                    napalm.base.helpers.find_txt(neighbor, "ConfiguredKeepalive"),
                    0,
                )
                if remote_as not in bgp_neighbors_detail[vrf_name].keys():
                    bgp_neighbors_detail[vrf_name][remote_as] = []
                bgp_neighbors_detail[vrf_name][remote_as].append(
                    {
                        "up": up,
                        "local_as": local_as,
                        "remote_as": remote_as,
                        "router_id": router_id,
                        "local_address": local_address,
                        "routing_table": vrf_name,
                        "local_address_configured": local_address_configured,
                        "local_port": local_port,
                        "remote_address": remote_address,
                        "remote_port": remote_port,
                        "multihop": multihop,
                        "multipath": multipath,
                        "import_policy": import_policy,
                        "export_policy": export_policy,
                        "input_messages": input_messages,
                        "output_messages": output_messages,
                        "input_updates": 0,
                        "output_updates": 0,
                        "messages_queued_out": messages_queued_out,
                        "connection_state": connection_state,
                        "previous_connection_state": previous_connection_state,
                        "last_event": "",
                        "remove_private_as": remove_private_as,
                        "suppress_4byte_as": suppress_4byte_as,
                        "local_as_prepend": local_as_prepend,
                        "holdtime": holdtime,
                        "configured_holdtime": configured_holdtime,
                        "keepalive": keepalive,
                        "configured_keepalive": configured_keepalive,
                        "active_prefix_count": active_prefix_count,
                        "received_prefix_count": received_prefix_count,
                        "accepted_prefix_count": accepted_prefix_count,
                        "suppressed_prefix_count": suppressed_prefix_count,
                        "advertised_prefix_count": advertised_prefix_count,
                        "flap_count": flap_count,
                    }
                )
        bgp_neighbors_detail["global"] = bgp_neighbors_detail.pop("default")
        return bgp_neighbors_detail

    def get_arp_table(self, vrf=""):
        if vrf:
            msg = "VRF support has not been added for this getter on this platform."
            raise NotImplementedError(msg)

        arp_table = []

        rpc_command = "<Get><Operational><ARP></ARP></Operational></Get>"

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for arp_entry in result_tree.xpath(".//EntryTable/Entry"):
            interface = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(arp_entry, ".//InterfaceName")
            )
            ip = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(arp_entry, ".//Address")
            )
            age = napalm.base.helpers.convert(
                float, napalm.base.helpers.find_txt(arp_entry, ".//Age"), -1.0
            )
            mac_raw = napalm.base.helpers.find_txt(arp_entry, ".//HardwareAddress")

            arp_table.append(
                {
                    "interface": interface,
                    "mac": napalm.base.helpers.mac(mac_raw),
                    "ip": napalm.base.helpers.ip(ip),
                    "age": age,
                }
            )

        return arp_table

    def get_ntp_peers(self):
        ntp_peers = {}

        rpc_command = "<Get><Configuration><NTP></NTP></Configuration></Get>"

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for version in ["IPV4", "IPV6"]:
            xpath = ".//Peer{version}Table/Peer{version}".format(version=version)
            for peer in result_tree.xpath(xpath):
                peer_type = napalm.base.helpers.find_txt(
                    peer, "PeerType{version}/Naming/PeerType".format(version=version)
                )
                if peer_type != "Peer":
                    continue
                peer_address = napalm.base.helpers.find_txt(
                    peer, "Naming/Address{version}".format(version=version)
                )
                if not peer_address:
                    continue
                ntp_peers[peer_address] = {}

        return ntp_peers

    def get_ntp_servers(self):
        ntp_servers = {}

        rpc_command = "<Get><Configuration><NTP></NTP></Configuration></Get>"

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for version in ["IPV4", "IPV6"]:
            xpath = ".//Peer{version}Table/Peer{version}".format(version=version)
            for peer in result_tree.xpath(xpath):
                peer_type = napalm.base.helpers.find_txt(
                    peer, "PeerType{version}/Naming/PeerType".format(version=version)
                )
                if peer_type != "Server":
                    continue
                server_address = napalm.base.helpers.find_txt(
                    peer, "Naming/Address{version}".format(version=version)
                )
                if not server_address:
                    continue
                ntp_servers[server_address] = {}

        return ntp_servers

    def get_ntp_stats(self):
        ntp_stats = []

        rpc_command = (
            "<Get><Operational><NTP><NodeTable></NodeTable></NTP></Operational></Get>"
        )

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        xpath = ".//NodeTable/Node/Associations/PeerSummaryInfo/Entry/PeerInfoCommon"
        for node in result_tree.xpath(xpath):
            synchronized = napalm.base.helpers.find_txt(node, "IsSysPeer") == "true"
            address = napalm.base.helpers.find_txt(node, "Address")
            if address == "DLRSC node":
                continue
            referenceid = napalm.base.helpers.find_txt(node, "ReferenceID")
            hostpoll = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(node, "HostPoll", "0")
            )
            reachability = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(node, "Reachability", "0")
            )
            stratum = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(node, "Stratum", "0")
            )
            delay = napalm.base.helpers.convert(
                float, napalm.base.helpers.find_txt(node, "Delay", "0.0")
            )
            offset = napalm.base.helpers.convert(
                float, napalm.base.helpers.find_txt(node, "Offset", "0.0")
            )
            jitter = napalm.base.helpers.convert(
                float, napalm.base.helpers.find_txt(node, "Dispersion", "0.0")
            )
            ntp_stats.append(
                {
                    "remote": address,
                    "synchronized": synchronized,
                    "referenceid": referenceid,
                    "stratum": stratum,
                    "type": "",
                    "when": "",
                    "hostpoll": hostpoll,
                    "reachability": reachability,
                    "delay": delay,
                    "offset": offset,
                    "jitter": jitter,
                }
            )

        return ntp_stats

    def get_interfaces_ip(self):
        interfaces_ip = {}

        rpc_command_ipv4_ipv6 = "<Get><Operational><IPV4Network></IPV4Network>\
        <IPV6Network></IPV6Network></Operational></Get>"

        # only one request
        ipv4_ipv6_tree = ETREE.fromstring(
            self.device.make_rpc_call(rpc_command_ipv4_ipv6)
        )

        # parsing IPv4
        ipv4_xpath = ".//IPV4Network/InterfaceTable/Interface"
        for interface in ipv4_ipv6_tree.xpath(ipv4_xpath):
            interface_name = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(interface, "Naming/InterfaceName")
            )
            primary_ip = napalm.base.helpers.ip(
                napalm.base.helpers.find_txt(
                    interface, "VRFTable/VRF/Detail/PrimaryAddress"
                )
            )
            primary_prefix = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(
                    interface, "VRFTable/VRF/Detail/PrefixLength"
                ),
            )
            if interface_name not in interfaces_ip.keys():
                interfaces_ip[interface_name] = {}
            if "ipv4" not in interfaces_ip[interface_name].keys():
                interfaces_ip[interface_name]["ipv4"] = {}
            if primary_ip not in interfaces_ip[interface_name].get("ipv4", {}).keys():
                interfaces_ip[interface_name]["ipv4"][primary_ip] = {
                    "prefix_length": primary_prefix
                }
            for secondary_address in interface.xpath(
                "VRFTable/VRF/Detail/SecondaryAddress/Entry"
            ):
                secondary_ip = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(secondary_address, "Address")
                )
                secondary_prefix = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(secondary_address, "PrefixLength")
                )
                if secondary_ip not in interfaces_ip[interface_name]:
                    interfaces_ip[interface_name]["ipv4"][secondary_ip] = {
                        "prefix_length": secondary_prefix
                    }

        # parsing IPv6
        ipv6_xpath = (
            ".//IPV6Network/NodeTable/Node/InterfaceData"
            "/VRFTable/VRF/GlobalDetailTable/GlobalDetail"
        )
        for interface in ipv4_ipv6_tree.xpath(ipv6_xpath):
            interface_name = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(interface, "Naming/InterfaceName")
            )
            if interface_name not in interfaces_ip.keys():
                interfaces_ip[interface_name] = {}
            if "ipv6" not in interfaces_ip[interface_name].keys():
                interfaces_ip[interface_name]["ipv6"] = {}
            for address in interface.xpath("AddressList/Entry"):
                address_ip = napalm.base.helpers.ip(
                    napalm.base.helpers.find_txt(address, "Address")
                )
                address_prefix = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(address, "PrefixLength")
                )
                if (
                    address_ip
                    not in interfaces_ip[interface_name].get("ipv6", {}).keys()
                ):
                    interfaces_ip[interface_name]["ipv6"][address_ip] = {
                        "prefix_length": address_prefix
                    }

        return interfaces_ip

    def get_mac_address_table(self):
        mac_table = []

        rpc_command = (
            "<Get><Operational><L2VPNForwarding></L2VPNForwarding></Operational></Get>"
        )

        result_tree = ETREE.fromstring(self.device.make_rpc_call(rpc_command))

        for mac_entry in result_tree.xpath(".//L2FIBMACDetailTable/L2FIBMACDetail"):
            mac_raw = napalm.base.helpers.find_txt(mac_entry, "Naming/Address")
            vlan = napalm.base.helpers.convert(
                int,
                napalm.base.helpers.find_txt(mac_entry, "Naming/Name", "").replace(
                    "vlan", ""
                ),
                0,
            )
            interface = napalm.base.helpers.find_txt(
                mac_entry, "Segment/AC/InterfaceHandle", ""
            )

            mac_table.append(
                {
                    "mac": napalm.base.helpers.mac(mac_raw),
                    "interface": interface,
                    "vlan": vlan,
                    "active": True,
                    "static": False,
                    "moves": 0,
                    "last_move": 0.0,
                }
            )

        return mac_table

    def get_route_to(self, destination="", protocol="", longer=False):
        routes = {}
        global IP_RIBRoute

        if not isinstance(destination, str):
            raise TypeError("Please specify a valid destination!")

        if longer:
            raise NotImplementedError("Longer prefixes not yet supported for IOS-XR")

        protocol = protocol.lower()
        if protocol == "direct":
            protocol = "connected"

        dest_split = destination.split("/")
        network = dest_split[0]
        prefix_tag = ""
        if len(dest_split) == 2:
            prefix_tag = "<PrefixLength>{prefix_length}</PrefixLength>".format(
                prefix_length=dest_split[1]
            )

        ipv = 4
        try:
            ipv = ipaddress.ip_address(network).version
        except ValueError:
            logger.error("Wrong destination IP Address format supplied to get_route_to")
            raise TypeError("Wrong destination IP Address!")

        if ipv == 6:
            route_info_rpc_command = (
                "<Get><Operational><IPV6_RIB><VRFTable><VRF><Naming><VRFName>"
                "default</VRFName></Naming><AFTable><AF><Naming><AFName>IPv6</AFName></Naming>"
                "<SAFTable>"
                "<SAF><Naming><SAFName>Unicast</SAFName></Naming><IP_RIBRouteTable><{ipribroute}>"
                "<Naming>"
                "<RouteTableName>default</RouteTableName></Naming><RouteTable><Route><Naming>"
                "<Address>"
                "{network}</Address>{prefix}</Naming></Route></RouteTable></{ipribroute}>"
                "</IP_RIBRouteTable></SAF></SAFTable></AF></AFTable></VRF></VRFTable></IPV6_RIB>"
                "</Operational></Get>"
            ).format(network=network, prefix=prefix_tag, ipribroute=IP_RIBRoute)
        else:
            route_info_rpc_command = (
                "<Get><Operational><RIB><VRFTable><VRF><Naming><VRFName>"
                "default"
                "</VRFName></Naming><AFTable><AF><Naming><AFName>IPv4</AFName></Naming>"
                "<SAFTable><SAF>"
                "<Naming><SAFName>Unicast</SAFName></Naming><IP_RIBRouteTable><{ipribroute}>"
                "<Naming>"
                "<RouteTableName>default</RouteTableName></Naming><RouteTable><Route><Naming>"
                "<Address>"
                "{network}</Address>{prefix}</Naming></Route></RouteTable></{ipribroute}>"
                "</IP_RIBRouteTable>"
                "</SAF></SAFTable></AF></AFTable></VRF></VRFTable></RIB></Operational></Get>"
            ).format(network=network, prefix=prefix_tag, ipribroute=IP_RIBRoute)

        try:
            routes_tree = ETREE.fromstring(
                self.device.make_rpc_call(route_info_rpc_command)
            )
        except Exception:
            pass
            # Some versions of IOS-XR use IP_RIBRouteTableName instead of IP_RIBRoute.
            # If IP_RIBRoute throws an exception, try again with IP_RIBRouteTableName
            # and have subsequent get_route_to calls use that.
            IP_RIBRoute = "IP_RIBRouteTableName"
            route_info_rpc_command = route_info_rpc_command.replace(
                "IP_RIBRoute>", "{ipribroute}>".format(ipribroute=IP_RIBRoute)
            )
            routes_tree = ETREE.fromstring(
                self.device.make_rpc_call(route_info_rpc_command)
            )

        for route in routes_tree.xpath(".//Route"):
            route_protocol = napalm.base.helpers.convert(
                str, napalm.base.helpers.find_txt(route, "ProtocolName").lower()
            )
            if protocol and route_protocol != protocol:
                continue  # ignore routes learned via a different protocol
                # only in case the user requested a certain protocol
            route_details = {}
            address = napalm.base.helpers.find_txt(route, "Prefix")
            length = napalm.base.helpers.find_txt(route, "PrefixLength")

            priority = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(route, "Priority")
            )
            age = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(route, "RouteAge")
            )
            destination = napalm.base.helpers.convert(
                str, "{prefix}/{length}".format(prefix=address, length=length)
            )
            if destination not in routes.keys():
                routes[destination] = []

            route_details = {
                "current_active": False,
                "last_active": False,
                "age": age,
                "next_hop": "",
                "protocol": route_protocol,
                "outgoing_interface": "",
                "preference": priority,
                "selected_next_hop": False,
                "inactive_reason": "",
                "routing_table": "default",
                "protocol_attributes": {},
            }

            # from BGP will try to get some more information
            if route_protocol == "bgp" and C.SR_638170159_SOLVED:
                # looks like IOS-XR does not filter correctly
                # !IMPORTANT
                bgp_route_info_rpc_command = "<Get><Operational><BGP><Active><DefaultVRF><AFTable>\
                <AF><Naming><AFName>IPv4Unicast</AFName></Naming><PathTable><Path><Naming><Network>\
                <IPV4Address>{network}</IPV4Address><IPV4PrefixLength>{prefix_len}\
                </IPV4PrefixLength></Network></Naming></Path></PathTable></AF></AFTable>\
                </DefaultVRF></Active></BGP></Operational></Get>".format(
                    network=network, prefix_len=dest_split[-1]
                )
                bgp_route_tree = ETREE.fromstring(
                    self.device.make_rpc_call(bgp_route_info_rpc_command)
                )
                for bgp_path in bgp_route_tree.xpath(".//Path"):
                    single_route_details = route_details.copy()
                    if "NotFound" not in bgp_path.keys():
                        best_path = (
                            napalm.base.helpers.find_txt(
                                bgp_path, "PathInformation/IsBestPath"
                            )
                            == "true"
                        )
                        local_preference = napalm.base.helpers.convert(
                            int,
                            napalm.base.helpers.find_txt(
                                bgp_path,
                                "AttributesAfterPolicyIn/CommonAttributes/LocalPreference",
                            ),
                            0,
                        )
                        local_preference = napalm.base.helpers.convert(
                            int,
                            napalm.base.helpers.find_txt(
                                bgp_path,
                                "AttributesAfterPolicyIn/CommonAttributes/LocalPreference",
                            ),
                            0,
                        )
                        remote_as = napalm.base.helpers.convert(
                            int,
                            napalm.base.helpers.find_txt(
                                bgp_path,
                                "AttributesAfterPolicyIn/CommonAttributes/NeighborAS",
                            ),
                            0,
                        )
                        remote_address = napalm.base.helpers.ip(
                            napalm.base.helpers.find_txt(
                                bgp_path, "PathInformation/NeighborAddress/IPV4Address"
                            )
                            or napalm.base.helpers.find_txt(
                                bgp_path, "PathInformation/NeighborAddress/IPV6Address"
                            )
                        )
                        as_path = " ".join(
                            [
                                bgp_as.text
                                for bgp_as in bgp_path.xpath(
                                    "AttributesAfterPolicyIn/CommonAttributes/NeighborAS/Entry"
                                )
                            ]
                        )
                        next_hop = napalm.base.helpers.find_txt(
                            bgp_path, "PathInformation/NextHop/IPV4Address"
                        ) or napalm.base.helpers.find_txt(
                            bgp_path, "PathInformation/NextHop/IPV6Address"
                        )
                        single_route_details["current_active"] = best_path
                        single_route_details["next_hop"] = next_hop
                        single_route_details["protocol_attributes"] = {
                            "local_preference": local_preference,
                            "as_path": as_path,
                            "remote_as": remote_as,
                            "remote_address": remote_address,
                        }
                    routes[destination].append(single_route_details)
            else:
                first_route = True
                for route_entry in route.xpath("RoutePath/Entry"):
                    # get all possible entries
                    next_hop = napalm.base.helpers.find_txt(route_entry, "Address")
                    outgoing_interface = napalm.base.helpers.find_txt(
                        route_entry, "InterfaceName"
                    )
                    single_route_details = {}
                    single_route_details.update(route_details)
                    single_route_details.update(
                        {
                            "current_active": first_route,
                            "next_hop": next_hop,
                            "outgoing_interface": outgoing_interface,
                        }
                    )
                    routes[destination].append(single_route_details)
                    first_route = False

        return routes

    def get_snmp_information(self):
        snmp_information = {}

        snmp_rpc_command = "<Get><Configuration><SNMP></SNMP></Configuration></Get>"

        snmp_result_tree = ETREE.fromstring(self.device.make_rpc_call(snmp_rpc_command))

        _PRIVILEGE_MODE_MAP_ = {"ReadOnly": "ro", "ReadWrite": "rw"}

        snmp_information = {
            "chassis_id": napalm.base.helpers.find_txt(
                snmp_result_tree, ".//ChassisID"
            ),
            "contact": napalm.base.helpers.find_txt(snmp_result_tree, ".//Contact"),
            "location": napalm.base.helpers.find_txt(snmp_result_tree, ".//Location"),
            "community": {},
        }

        for community in snmp_result_tree.xpath(".//DefaultCommunity"):
            name = napalm.base.helpers.find_txt(community, "Naming/CommunityName")
            privilege = napalm.base.helpers.find_txt(community, "Priviledge")
            acl = napalm.base.helpers.find_txt(community, "AccessList")
            snmp_information["community"][name] = {
                "mode": _PRIVILEGE_MODE_MAP_.get(privilege, ""),
                "acl": acl,
            }

        return snmp_information

    def get_probes_config(self):
        sla_config = {}

        _PROBE_TYPE_XML_TAG_MAP_ = {
            "ICMPEcho": "icmp-ping",
            "UDPEcho": "udp-ping",
            "ICMPJitter": "icmp-ping-timestamp",
            "UDPJitter": "udp-ping-timestamp",
        }

        sla_config_rpc_command = (
            "<Get><Configuration><IPSLA></IPSLA></Configuration></Get>"
        )

        sla_config_result_tree = ETREE.fromstring(
            self.device.make_rpc_call(sla_config_rpc_command)
        )

        for probe in sla_config_result_tree.xpath(".//Definition"):
            probe_name = napalm.base.helpers.find_txt(probe, "Naming/OperationID")
            operation_type = probe.xpath("OperationType")[0].getchildren()[0].tag
            probe_type = _PROBE_TYPE_XML_TAG_MAP_.get(operation_type, "")
            operation_xpath = "OperationType/{op_type}".format(op_type=operation_type)
            operation = probe.xpath(operation_xpath)[0]
            test_name = napalm.base.helpers.find_txt(operation, "Tag")
            source = napalm.base.helpers.find_txt(operation, "SourceAddress")
            target = napalm.base.helpers.find_txt(operation, "DestAddress")
            test_interval = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(operation, "Frequency", "0")
            )
            probe_count = napalm.base.helpers.convert(
                int, napalm.base.helpers.find_txt(operation, "History/Buckets", "0")
            )
            if probe_name not in sla_config.keys():
                sla_config[probe_name] = {}
            if test_name not in sla_config[probe_name]:
                sla_config[probe_name][test_name] = {}
            sla_config[probe_name][test_name] = {
                "probe_type": probe_type,
                "source": source,
                "target": target,
                "probe_count": probe_count,
                "test_interval": test_interval,
            }

        return sla_config

    def get_probes_results(self):
        sla_results = {}

        _PROBE_TYPE_XML_TAG_MAP_ = {
            "ICMPEcho": "icmp-ping",
            "UDPEcho": "udp-ping",
            "ICMPJitter": "icmp-ping-timestamp",
            "UDPJitter": "udp-ping-timestamp",
        }

        sla_results_rpc_command = (
            "<Get><Operational><IPSLA></IPSLA></Operational></Get>"
        )

        sla_results_tree = ETREE.fromstring(
            self.device.make_rpc_call(sla_results_rpc_command)
        )

        probes_config = (
            self.get_probes_config()
        )  # need to retrieve also the configuration
        # source and tag/test_name not provided

        for probe in sla_results_tree.xpath(".//Operation"):
            probe_name = napalm.base.helpers.find_txt(probe, "Naming/OperationID")
            test_name = list(probes_config.get(probe_name).keys())[0]
            target = napalm.base.helpers.find_txt(
                probe,
                "History/Target/LifeTable/Life/BucketTable/Bucket[0]/TargetAddress\
                /IPv4AddressTarget",
            )
            source = probes_config.get(probe_name).get(test_name, {}).get("source", "")
            probe_type = _PROBE_TYPE_XML_TAG_MAP_.get(
                napalm.base.helpers.find_txt(
                    probe, "Statistics/Latest/Target/SpecificStats/op_type"
                )
            )
            probe_count = (
                probes_config.get(probe_name).get(test_name, {}).get("probe_count", 0)
            )
            response_times = probe.xpath(
                "History/Target/LifeTable/Life[last()]/BucketTable/Bucket/ResponseTime"
            )
            response_times = [
                napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(response_time, ".", "0")
                )
                for response_time in response_times
            ]
            rtt = 0.0

            if len(response_times):
                rtt = sum(response_times, 0.0) / len(response_times)
            return_codes = probe.xpath(
                "History/Target/LifeTable/Life[last()]/BucketTable/Bucket/ReturnCode"
            )
            return_codes = [
                napalm.base.helpers.find_txt(return_code, ".")
                for return_code in return_codes
            ]

            last_test_loss = 0.0
            if len(return_codes):
                last_test_loss = napalm.base.helpers.convert(
                    int,
                    100
                    * (
                        1
                        - return_codes.count("ipslaRetCodeOK")
                        / napalm.base.helpers.convert(float, len(return_codes))
                    ),
                )
            rms = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe,
                    "Statistics/Aggregated/HourTable/Hour\
                    /Distributed/Target/DistributionIntervalTable/DistributionInterval/CommonStats\
                    /Sum2ResponseTime",
                ),
            )
            global_test_updates = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe,
                    "Statistics/Aggregated/HourTable/Hour\
                    /Distributed/Target/DistributionIntervalTable/DistributionInterval/CommonStats\
                    /UpdateCount",
                ),
            )

            jitter = 0.0
            if global_test_updates:
                jitter = rtt - (rms / global_test_updates) ** 0.5
            # jitter = max(rtt - max(response_times), rtt - min(response_times))
            current_test_min_delay = 0.0  # no stats for undergoing test :(
            current_test_max_delay = 0.0
            current_test_avg_delay = 0.0
            last_test_min_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe, "Statistics/Latest/Target/CommonStats/MinResponseTime"
                ),
            )
            last_test_max_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe, "Statistics/Latest/Target/CommonStats/MaxResponseTime"
                ),
            )
            last_test_sum_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe, "Statistics/Latest/Target/CommonStats/SumResponseTime"
                ),
            )
            last_test_updates = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe, "Statistics/Latest/Target/CommonStats/UpdateCount"
                ),
            )
            last_test_avg_delay = 0.0
            if last_test_updates:
                last_test_avg_delay = last_test_sum_delay / last_test_updates
            global_test_min_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe,
                    "Statistics/Aggregated/HourTable/Hour/Distributed/Target\
                    /DistributionIntervalTable/DistributionInterval/CommonStats/MinResponseTime",
                ),
            )
            global_test_max_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe,
                    "Statistics/Aggregated/HourTable/Hour/Distributed/Target\
                    /DistributionIntervalTable/DistributionInterval/CommonStats/MaxResponseTime",
                ),
            )
            global_test_sum_delay = napalm.base.helpers.convert(
                float,
                napalm.base.helpers.find_txt(
                    probe,
                    "Statistics/Aggregated/HourTable/Hour\
                    /Distributed/Target/DistributionIntervalTable/DistributionInterval\
                    /CommonStats/SumResponseTime",
                ),
            )
            global_test_avg_delay = 0.0
            if global_test_updates:
                global_test_avg_delay = global_test_sum_delay / global_test_updates
            if probe_name not in sla_results.keys():
                sla_results[probe_name] = {}
            sla_results[probe_name][test_name] = {
                "target": target,
                "source": source,
                "probe_type": probe_type,
                "probe_count": probe_count,
                "rtt": rtt,
                "round_trip_jitter": jitter,
                "last_test_loss": last_test_loss,
                "current_test_min_delay": current_test_min_delay,
                "current_test_max_delay": current_test_max_delay,
                "current_test_avg_delay": current_test_avg_delay,
                "last_test_min_delay": last_test_min_delay,
                "last_test_max_delay": last_test_max_delay,
                "last_test_avg_delay": last_test_avg_delay,
                "global_test_min_delay": global_test_min_delay,
                "global_test_max_delay": global_test_max_delay,
                "global_test_avg_delay": global_test_avg_delay,
            }

        return sla_results

    def traceroute(
        self,
        destination,
        source=C.TRACEROUTE_SOURCE,
        ttl=C.TRACEROUTE_TTL,
        timeout=C.TRACEROUTE_TIMEOUT,
        vrf=C.TRACEROUTE_VRF,
    ):
        traceroute_result = {}

        ipv = 4
        try:
            ipv = ipaddress.ip_address(destination).version
        except ValueError:
            logger.error(
                "Incorrect format of IP Address in traceroute \
             with value provided:%s"
                % (str(destination))
            )
            return {"error": "Wrong destination IP Address!"}

        source_tag = ""
        ttl_tag = ""
        timeout_tag = ""
        vrf_tag = ""
        if source:
            source_tag = "<Source>{source}</Source>".format(source=source)
        if ttl:
            ttl_tag = "<MaxTTL>{maxttl}</MaxTTL>".format(maxttl=ttl)
        if timeout:
            timeout_tag = "<Timeout>{timeout}</Timeout>".format(timeout=timeout)
        if vrf:
            vrf_tag = "<VRFName>{vrf}</VRFName>".format(vrf=vrf)

        traceroute_rpc_command = "<Set><Action><TraceRoute><IPV{version}><Destination>{destination}\
        </Destination>{vrf_tag}{source_tag}{ttl_tag}{timeout_tag}</IPV{version}></TraceRoute></Action>\
        </Set>".format(
            version=ipv,
            destination=destination,
            vrf_tag=vrf_tag,
            source_tag=source_tag,
            ttl_tag=ttl_tag,
            timeout_tag=timeout_tag,
        )

        xml_tree_txt = self.device.make_rpc_call(traceroute_rpc_command)
        traceroute_tree = ETREE.fromstring(xml_tree_txt)

        results_tree = traceroute_tree.xpath(".//Results")
        if results_tree is None or not len(results_tree):
            return {"error": "Device returned empty results."}

        results_error = napalm.base.helpers.find_txt(results_tree[0], "Error")

        if results_error:
            return {"error": results_error}

        traceroute_result["success"] = {}

        last_hop_index = 1
        last_probe_index = 1
        last_probe_ip_address = "*"
        last_probe_host_name = ""
        last_hop_dict = {"probes": {}}

        for thanks_cisco in results_tree[0].getchildren():
            tag_name = thanks_cisco.tag
            tag_value = thanks_cisco.text
            if tag_name == "HopIndex":
                new_hop_index = napalm.base.helpers.convert(
                    int, napalm.base.helpers.find_txt(thanks_cisco, ".", "-1")
                )
                if last_hop_index and last_hop_index != new_hop_index:
                    traceroute_result["success"][last_hop_index] = copy.deepcopy(
                        last_hop_dict
                    )
                    last_hop_dict = {"probes": {}}
                    last_probe_ip_address = "*"
                    last_probe_host_name = ""
                last_hop_index = new_hop_index
                continue
            tag_value = napalm.base.helpers.find_txt(thanks_cisco, ".", "")
            if tag_name == "ProbeIndex":
                last_probe_index = napalm.base.helpers.convert(int, tag_value, 0) + 1
                if last_probe_index not in last_hop_dict.get("probes").keys():
                    last_hop_dict["probes"][last_probe_index] = {}
                if not last_probe_host_name:
                    last_probe_host_name = last_probe_ip_address
                last_hop_dict["probes"][last_probe_index] = {
                    "ip_address": napalm.base.helpers.convert(
                        str, last_probe_ip_address
                    ),
                    "host_name": napalm.base.helpers.convert(str, last_probe_host_name),
                    "rtt": timeout * 1000.0,
                }
                continue
            if tag_name == "HopAddress":
                last_probe_ip_address = tag_value
                continue
            if tag_name == "HopHostName":
                last_probe_host_name = tag_value
                continue
            if tag_name == "DeltaTime":
                last_hop_dict["probes"][last_probe_index]["rtt"] = (
                    napalm.base.helpers.convert(float, tag_value, 0.0)
                )
                continue

        if last_hop_index:
            traceroute_result["success"][last_hop_index] = last_hop_dict

        return traceroute_result

    def get_users(self):
        users = {}

        _CISCO_GROUP_TO_CISCO_PRIVILEGE_MAP = {
            "root-system": 15,
            "operator": 5,
            "sysadmin": 1,
            "serviceadmin": 1,
            "root-lr": 15,
        }

        _DEFAULT_USER_DETAILS = {"level": 0, "password": "", "sshkeys": []}

        users_xml_req = "<Get><Configuration><AAA></AAA></Configuration></Get>"

        users_xml_reply = ETREE.fromstring(self.device.make_rpc_call(users_xml_req))

        for user_entry in users_xml_reply.xpath(".//Username"):
            username = napalm.base.helpers.find_txt(user_entry, "Naming/Name")
            group = napalm.base.helpers.find_txt(
                user_entry, "UsergroupsUnderUsername/UsergroupUnderUsername/Naming/Name"
            )
            level = _CISCO_GROUP_TO_CISCO_PRIVILEGE_MAP.get(group, 0)
            password = napalm.base.helpers.find_txt(user_entry, "Password/Password")
            user_details = _DEFAULT_USER_DETAILS.copy()
            user_details.update({"level": level, "password": str(password)})
            users[username] = user_details

        return users

    def get_config(self, retrieve="all", full=False, sanitized=False, format="text"):
        config = {"startup": "", "running": "", "candidate": ""}  # default values

        # IOS-XR only supports "all" on "show run"
        run_full = " all" if full else ""

        filter_strings = [r"^Building configuration.*$", r"^!! IOS XR Configuration.*$"]
        filter_pattern = napalm.base.helpers.generate_regex_or(filter_strings)

        if retrieve.lower() in ["running", "all"]:
            running = str(
                self.device._execute_config_show(f"show running-config{run_full}")
            )
            running = re.sub(filter_pattern, "", running, flags=re.M)
            config["running"] = running
        if retrieve.lower() in ["candidate", "all"]:
            candidate = str(
                self.device._execute_config_show("show configuration merge")
            )
            candidate = re.sub(filter_pattern, "", candidate, flags=re.M)
            config["candidate"] = candidate

        if sanitized:
            return napalm.base.helpers.sanitize_configs(
                config, C.CISCO_SANITIZE_FILTERS
            )

        return config
