Skip to content
Snippets Groups Projects
nokia.py 6.42 KiB
"""

"""
import contextlib
import logging
import pathlib
import time
import typing

from lxml import etree
import ncclient.manager

from brian_polling_manager.interface_stats.vendors.common import parse_interface_xml

if typing.TYPE_CHECKING:
    from ncclient.lxml_ import NCElement

logger = logging.getLogger(__name__)

STATE_NS = 'urn:nokia.com:sros:ns:yang:sr:state'

# TODO: decide if these are really global
DEFAULT_NETCONF_PORT = 830
# cf. POL1-799
DEFAULT_NETCONF_CONNECT_TIMEOUT = 60  # seconds
# DEFAULT_NETCONF_CONNECT_TIMEOUT = 5  # seconds
ENDPOINT_TYPES = {'port', 'lag'}


@contextlib.contextmanager
def _connection(hostname: str, ssh_params: dict, port: int = DEFAULT_NETCONF_PORT):
    params = {
        'host': hostname,
        'port': port,
        'device_params': {'name': 'sros'},
        'nc_params': {'capabilities': ['urn:nokia.com:nc:pysros:pc']},
        'timeout': DEFAULT_NETCONF_CONNECT_TIMEOUT
    }
    params.update(ssh_params)

    conn = ncclient.manager.connect(**params)
    conn.async_mode = False
    yield conn


def _remove_ns(nc_doc: 'NCElement'):
    # convert to etree
    etree_doc = etree.fromstring(nc_doc.tostring.decode('utf-8'))
    for elem in etree_doc.getiterator():
        elem.tag = etree.QName(elem).localname
    etree.cleanup_namespaces(etree_doc)
    return etree_doc


def get_netconf_interface_info(router_name: str, ssh_params: dict):

    def _filter(endpoint_type: str):
        # https://github.com/nokia/pysros/blob/main/examples/show_port_counters.py
        # https://netdevops.me/2020/nokia-yang-tree-and-path-browser/
        # https://github.com/hellt/nokia-yangtree
        _f = etree.Element('filter')
        state = etree.SubElement(
            _f,
            f'{{{STATE_NS}}}state',
            nsmap={'nokia-state': STATE_NS})
        _ = etree.SubElement(state, f'{{{STATE_NS}}}{endpoint_type}')
        # cf. POL1-799
        # _ = etree.SubElement(_state, f'{{{STATE_NS}}}statistics')
        return _f

    start = time.time()
    with _connection(hostname=router_name, ssh_params=ssh_params) as router:
        # return {_ept: _remove_ns(router.get(filter=_filter(_ept))) for _ept in ENDPOINT_TYPES}
        now = time.time()
        logger.debug(f"seconds to connect ({router_name}: {now - start}")

        rsp = {}
        for _ept in ENDPOINT_TYPES:
            start = now
            logger.debug(f"Getting {router_name} {_ept} info")
            rsp[_ept] = _remove_ns(router.get(filter=_filter(_ept)))
            now = time.time()
            logger.debug(f"   seconds to get {_ept} info: {now - start}")

        return rsp


def get_netconf_interface_info_from_source_dir(
    router_name: str, source_dir: str
):
    file = pathlib.Path(source_dir) / f"{router_name}-ports.xml"
    if not file.is_file():
        raise ValueError(f"file {file} is not a valid file")
    ports_doc = etree.fromstring(file.read_text())

    file = pathlib.Path(source_dir) / f"{router_name}-lags.xml"
    if not file.is_file():
        raise ValueError(f"file {file} is not a valid file")
    lags_doc = etree.fromstring(file.read_text())

    return {
        'port': ports_doc,
        'lag': lags_doc
    }


def _counter_parse_config(name_tag: str):
    """
    the stats info for both port and lag are the same, only the
    name we use for reference is different, so the parsing is nearly
    identical

    :param name_tag: either 'port-id' or 'lag-name'
    :return:
    """
    # sanity: special-purpose method for only these 2 cases
    assert name_tag in ('port-id', 'lag-name')

    return {
        "__defaults__": {"transform": int, "required": False},
        "name": {"path": f"./{name_tag}", "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"},
            "egressDiscards": {"path": "./statistics/out-discards"},
        },
        "errors": {
            "output_total_errors": {"path": "./statistics/out-errors"},
            "input_discards": {"path": "./statistics/in-discards"},
            "output_discards": {"path": "./statistics/out-discards"},
        },
    }


def _port_counters(state_doc: etree.Element):
    """
    :param state_doc: /nokia-state:state/port yang node
    :return: counters for all monitored ports
    """

    parse_config = _counter_parse_config('port-id')

    for port in state_doc.xpath(
        "//data/state/port["
        ' normalize-space(oper-state)="up"'
        "]"
    ):
        result = parse_interface_xml(port, parse_config)
        assert result
        yield result


def _lag_counters(state_doc: etree.Element):
    """
    :param state_doc: /nokia-state:state/lag yang node
    :return: counters for all monitored lags
    """

    parse_config = _counter_parse_config('lag-name')

    for port in state_doc.xpath(
        "//data/state/lag["
        ' normalize-space(oper-state)="up"'
        "]"
    ):
        result = parse_interface_xml(port, parse_config)
        assert result
        yield result


def interface_counters(raw_counter_docs: dict):
    """

    :param raw_counter_docs: output of a call to get_netconf_interface_info
    :return: iterable of interface counters
    """
    yield from _port_counters(raw_counter_docs['port'])
    yield from _lag_counters(raw_counter_docs['lag'])


if __name__ == '__main__':
    from brian_polling_manager.interface_stats import config
    import os
    config_filename = os.path.join(
        os.path.dirname(__file__),
        '..', 'config-test.json')
    with open(config_filename) as f:
        params = config.load(f)

    ROUTERS = [
        'rt0.lon.uk.lab.office.geant.net',
        'rt0.ams.nl.lab.office.geant.net'
    ]

    # rsp = get_interface_info_ncrpc(
    raw_counter_docs = get_netconf_interface_info(
        router_name=ROUTERS[0],
        ssh_params=params['nokia'])

    print(interface_counters(raw_counter_docs))