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))