-
Pelle Koster authoredPelle Koster authored
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")