Skip to content
Snippets Groups Projects
nokia.py 6.77 KiB
import logging
import pathlib
from typing import Dict, Optional, Sequence
from brian_polling_manager.interface_stats.vendors.common import (
    extract_all_counters,
    extract_selected_counters,
    netconf_connect,
    remove_xml_namespaces,
)
from lxml import etree

logger = logging.getLogger(__name__)


NCCLIENT_PARAMS = {
    "device_params": {"name": "sros"},
    "nc_params": {"capabilities": ["urn:nokia.com:nc:pysros:pc"]},
    "timeout": 60,
}

INTERFACE_COUNTERS = {
    # Counters must be floats to be compatible with sensu writing to the same
    # measurement. cf https://github.com/sensu/sensu-go/issues/2213
    "__defaults__": {"transform": float, "required": False},
    "name": {
        "path": ["./lag-name", "./port-id"],
        "transform": lambda v: str(v).strip(),
    },
    "brian": {
        "ingressOctets": {"path": "./statistics/in-octets", "required": True},
        "ingressPackets": {
            "path": "./statistics/in-packets",
            "required": True,
        },
        "egressOctets": {"path": "./statistics/out-octets", "required": True},
        "egressPackets": {"path": "./statistics/out-packets", "required": True},
        "ingressErrors": {"path": "./statistics/in-errors"},
        "egressErrors": {"path": "./statistics/out-errors"},
        "ingressDiscards": {"path": "./statistics/in-discards"},
    },
    "errors": {
        "__defaults__": {"transform": int},
        "output_total_errors": {"path": "./statistics/out-errors"},
        "input_total_errors": {"path": "./statistics/in-errors"},
        "input_discards": {"path": "./statistics/in-discards"},
        "output_discards": {"path": "./statistics/out-discards"},
        "crc_align_errors": {"path": "./ethernet/statistics/crc-align-errors"},
        "fcs_errors": {"path": "./ethernet/statistics/ethernet-like-medium/error/fcs"},
        "oper_state_change_count": {"path": "./ethernet/oper-state-change-count"},
    },
}

INTERFACE_COUNTERS_ALT = {
    # Counters must be floats to be compatible with sensu writing to the same
    # measurement. cf https://github.com/sensu/sensu-go/issues/2213
    "__defaults__": {"transform": float, "required": False},
    "name": {
        "path": "./interface-name",
        "transform": lambda v: str(v).strip(),
    },
    "brian": {
        "ingressOctets": {"path": "./statistics/ip/in-octets", "required": True},
        "ingressPackets": {
            "path": "./statistics/ip/in-packets",
            "required": True,
        },
        "egressOctets": {"path": "./statistics/ip/out-octets", "required": True},
        "egressPackets": {"path": "./statistics/ip/out-packets", "required": True},
        "ingressOctetsv6": {"path": "./ipv6/statistics/in-octets"},
        "ingressPacketsv6": {"path": "./ipv6/statistics/in-packets"},
        "egressOctetsv6": {"path": "./ipv6/statistics/out-octets"},
        "egressPacketsv6": {"path": "./ipv6/statistics/out-packets"},
    },
    "errors": {
        "__defaults__": {"transform": int},
        "output_discards": {"path": "./statistics/ip/out-discard-packets"},
    },
}


def get_netconf_interface_info(
    router_name: str, ssh_params: dict
) -> Dict[str, etree.ElementBase]:
    """
    :param router_name: the router to poll
    :param ssh_params: ssh params for connecting to the router

    :returns: a dictionary  with doctype (``port``, ``lag`` & ``router-interface`` ) as
        keys and their respective netconf document as values
    """

    query_path = {
        "port": ["port", ["statistics", "ethernet"]],
        "lag": ["lag", "statistics"],
        "router-interface": ["router", "interface"],
        "vprn": ["service", "vprn", "interface"],
        "ies": ["service", "ies", "interface"],
    }
    with netconf_connect(
        hostname=router_name, ssh_params=ssh_params, **NCCLIENT_PARAMS
    ) as conn:
        return {
            doctype: remove_xml_namespaces(query(conn, *qpath).tostring.decode("utf-8"))
            for doctype, qpath in query_path.items()
        }


def query(connection, *path):
    STATE_NS = "urn:nokia.com:sros:ns:yang:sr:state"

    root = etree.Element("filter")
    sub_elem = etree.SubElement(
        root, f"{{{STATE_NS}}}state", nsmap={"nokia-state": STATE_NS}
    )
    is_leaf = False
    for p in path:
        if is_leaf:
            raise ValueError("Can only have multiple nodes as leaf elements in path")
        if isinstance(p, str):
            sub_elem = etree.SubElement(sub_elem, f"{{{STATE_NS}}}{p}")
        else:
            is_leaf = True
            for item in p:
                etree.SubElement(sub_elem, f"{{{STATE_NS}}}{item}")

    return connection.get(filter=root)


def get_netconf_interface_info_from_source_dir(router_name: str, source_dir: str):
    def read_doc_or_raise(name: str):
        file = pathlib.Path(source_dir) / name
        if not file.is_file():
            raise ValueError(f"file {file} is not a valid file")

        return etree.fromstring(file.read_text())

    return {
        key: read_doc_or_raise(f"{router_name}-{key}s.xml")
        for key in ["port", "lag", "router-interface", "ies", "vprn"]
    }


def _port_xml(state_doc: etree.Element):
    return state_doc.xpath("//data/state/port")


def _lag_xml(state_doc: etree.Element):
    return state_doc.xpath("//data/state/lag")


def _router_interface_xml(state_doc: etree.Element):
    return state_doc.xpath("//data/state/router/interface")


def _ies_interface_xml(state_doc: etree.Element):
    return state_doc.xpath("//data/state/service/ies/interface")


def _vprn_interface_xml(state_doc: etree.Element):
    return state_doc.xpath("//data/state/service/vprn/interface")


def interface_counters(
    raw_counter_docs: dict, interfaces: Optional[Sequence[str]] = None
):
    """
    :param raw_counter_docs: output of a call to ``get_netconf_interface_info``
    :pararm interfaces: a sequence of interfaces for which to retrieve counters.
        ``None`` means return counters for all interfaces
    :return: iterable of interface counters
    """
    doc_info = {
        "port": (INTERFACE_COUNTERS, _port_xml),
        "lag": (INTERFACE_COUNTERS, _lag_xml),
        "router-interface": (INTERFACE_COUNTERS_ALT, _router_interface_xml),
        "ies": (INTERFACE_COUNTERS_ALT, _ies_interface_xml),
        "vprn": (INTERFACE_COUNTERS_ALT, _vprn_interface_xml),
    }

    if interfaces is None:
        for doctype, doc in raw_counter_docs.items():
            struct, xpath = doc_info[doctype]
            yield from extract_all_counters(xpath(doc), struct)
        return

    remaining = set(interfaces)
    for doctype, doc in raw_counter_docs.items():
        struct, xpath = doc_info[doctype]
        yield from extract_selected_counters(xpath(doc), struct, interfaces=remaining)
        if not remaining:
            return

    for ifc in remaining:
        logger.error(f"Interface {ifc} was not found on router")