blob: 47cfa37bbebe945686181405f757acb3d29b3d65 [file] [log] [blame]
#
# Copyright 2018 the original author or authors.
#
# 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.
#
from twisted.internet import reactor, defer
from pyvoltha.adapters.extensions.kpi.olt.olt_pm_metrics import OltPmMetrics
from voltha_protos.device_pb2 import PmConfig, PmConfigs, PmGroupConfig, Port
class OpenOltStatisticsMgr(object):
def __init__(self, openolt_device, log, platform, **kargs):
"""
kargs are used to pass debugging flags at this time.
:param openolt_device:
:param log:
:param kargs:
"""
self.device = openolt_device
self.log = log
self.platform = platform
# Northbound and Southbound ports
# added to initialize the pm_metrics
self.northbound_ports = self.init_ports(type="nni")
self.southbound_ports = self.init_ports(type='pon')
self.pm_metrics = None
# The following can be used to allow a standalone test routine to start
# the metrics independently
self.metrics_init = kargs.pop("metrics_init", True)
if self.metrics_init == True:
self.init_pm_metrics()
def init_pm_metrics(self):
# Setup PM configuration for this device
if self.pm_metrics is None:
try:
self.device.reason = 'setting up Performance Monitoring configuration'
kwargs = {
'nni-ports': self.northbound_ports.values(),
'pon-ports': self.southbound_ports.values()
}
self.pm_metrics = OltPmMetrics(self.device.adapter_agent, self.device.device_id,
self.device.logical_device_id,
grouped=True, freq_override=False,
**kwargs)
"""
override the default naming structures in the OltPmMetrics class.
This is being done until the protos can be modified in the BAL driver
"""
self.pm_metrics.nni_pm_names = (self.get_openolt_port_pm_names())['nni_pm_names']
self.pm_metrics.nni_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
for (m, t) in self.pm_metrics.nni_pm_names}
self.pm_metrics.pon_pm_names = (self.get_openolt_port_pm_names())['pon_pm_names']
self.pm_metrics.pon_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
for (m, t) in self.pm_metrics.pon_pm_names}
pm_config = self.pm_metrics.make_proto()
self.log.info("initial-pm-config", pm_config=pm_config)
self.device.adapter_agent.update_device_pm_config(pm_config, init=True)
# Start collecting stats from the device after a brief pause
reactor.callLater(10, self.pm_metrics.start_collector)
except Exception as e:
self.log.exception('pm-setup', e=e)
def port_statistics_indication(self, port_stats):
# self.log.info('port-stats-collected', stats=port_stats)
self.ports_statistics_kpis(port_stats)
#FIXME: etcd problem, do not update objects for now
#
#
# #FIXME : only the first uplink is a logical port
# if platform.intf_id_to_port_type_name(port_stats.intf_id) ==
# Port.ETHERNET_NNI:
# # ONOS update
# self.update_logical_port_stats(port_stats)
# # update port object stats
# port = self.device.adapter_agent.get_port(self.device.device_id,
# port_no=port_stats.intf_id)
#
# if port is None:
# self.log.warn('port associated with this stats does not exist')
# return
#
# port.rx_packets = port_stats.rx_packets
# port.rx_bytes = port_stats.rx_bytes
# port.rx_errors = port_stats.rx_error_packets
# port.tx_packets = port_stats.tx_packets
# port.tx_bytes = port_stats.tx_bytes
# port.tx_errors = port_stats.tx_error_packets
#
# # Add port does an update if port exists
# self.device.adapter_agent.add_port(self.device.device_id, port)
def flow_statistics_indication(self, flow_stats):
self.log.info('flow-stats-collected', stats=flow_stats)
# TODO: send to kafka ?
# FIXME: etcd problem, do not update objects for now
# # UNTESTED : the openolt driver does not yet provide flow stats
# self.device.adapter_agent.update_flow_stats(
# self.device.logical_device_id,
# flow_id=flow_stats.flow_id, packet_count=flow_stats.tx_packets,
# byte_count=flow_stats.tx_bytes)
def ports_statistics_kpis(self, port_stats):
"""
map the port stats values into a dictionary
Create a kpoEvent and publish to Kafka
:param port_stats:
:return:
"""
try:
intf_id = port_stats.intf_id
if self.platform.intf_id_to_port_no(0, Port.ETHERNET_NNI) < intf_id < \
self.platform.intf_id_to_port_no(4, Port.ETHERNET_NNI) :
"""
for this release we are only interested in the first NNI for
Northbound.
we are not using the other 3
"""
return
else:
pm_data = {}
pm_data["rx_bytes"] = port_stats.rx_bytes
pm_data["rx_packets"] = port_stats.rx_packets
pm_data["rx_ucast_packets"] = port_stats.rx_ucast_packets
pm_data["rx_mcast_packets"] = port_stats.rx_mcast_packets
pm_data["rx_bcast_packets"] = port_stats.rx_bcast_packets
pm_data["rx_error_packets"] = port_stats.rx_error_packets
pm_data["tx_bytes"] = port_stats.tx_bytes
pm_data["tx_packets"] = port_stats.tx_packets
pm_data["tx_ucast_packets"] = port_stats.tx_ucast_packets
pm_data["tx_mcast_packets"] = port_stats.tx_mcast_packets
pm_data["tx_bcast_packets"] = port_stats.tx_bcast_packets
pm_data["tx_error_packets"] = port_stats.tx_error_packets
pm_data["rx_crc_errors"] = port_stats.rx_crc_errors
pm_data["bip_errors"] = port_stats.bip_errors
pm_data["intf_id"] = intf_id
"""
Based upon the intf_id map to an nni port or a pon port
the intf_id is the key to the north or south bound collections
Based upon the intf_id the port object (nni_port or pon_port) will
have its data attr. updated by the current dataset collected.
For prefixing the rule is currently to use the port number and not the intf_id
"""
#FIXME : Just use first NNI for now
if intf_id == self.platform.intf_id_to_port_no(0,
Port.ETHERNET_NNI):
#NNI port (just the first one)
self.update_port_object_kpi_data(
port_object=self.northbound_ports[port_stats.intf_id], datadict=pm_data)
else:
#PON ports
self.update_port_object_kpi_data(
port_object=self.southbound_ports[port_stats.intf_id],datadict=pm_data)
except Exception as err:
self.log.exception("Error publishing kpi statistics. ", errmessage=err)
def update_logical_port_stats(self, port_stats):
try:
label = 'nni-{}'.format(port_stats.intf_id)
logical_port = self.device.adapter_agent.get_logical_port(
self.device.logical_device_id, label)
except KeyError as e:
self.log.warn('logical port was not found, it may not have been '
'created yet', exception=e)
return
if logical_port is None:
self.log.error('logical-port-is-None',
logical_device_id=self.device.logical_device_id, label=label,
port_stats=port_stats)
return
logical_port.ofp_port_stats.rx_packets = port_stats.rx_packets
logical_port.ofp_port_stats.rx_bytes = port_stats.rx_bytes
logical_port.ofp_port_stats.tx_packets = port_stats.tx_packets
logical_port.ofp_port_stats.tx_bytes = port_stats.tx_bytes
logical_port.ofp_port_stats.rx_errors = port_stats.rx_error_packets
logical_port.ofp_port_stats.tx_errors = port_stats.tx_error_packets
logical_port.ofp_port_stats.rx_crc_err = port_stats.rx_crc_errors
self.log.debug('after-stats-update', port=logical_port)
self.device.adapter_agent.update_logical_port(
self.device.logical_device_id, logical_port)
"""
The following 4 methods customer naming, the generation of the port objects, building of those
objects and populating new data. The pm metrics operate on the value that are contained in the Port objects.
This class updates those port objects with the current data from the grpc indication and
post the data on a fixed interval.
"""
def get_openolt_port_pm_names(self):
"""
This collects a dictionary of the custom port names
used by the openolt.
Some of these are the same as the pm names used by the olt_pm_metrics class
if the set is the same then there is no need to call this method. However, when
custom names are used in the protos then the specific names should be pushed into
the olt_pm_metrics class.
:return:
"""
nni_pm_names = {
('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
('admin_state', PmConfig.STATE),
('oper_status', PmConfig.STATE),
('port_no', PmConfig.GAUGE),
('rx_bytes', PmConfig.COUNTER),
('rx_packets', PmConfig.COUNTER),
('rx_ucast_packets', PmConfig.COUNTER),
('rx_mcast_packets', PmConfig.COUNTER),
('rx_bcast_packets', PmConfig.COUNTER),
('rx_error_packets', PmConfig.COUNTER),
('tx_bytes', PmConfig.COUNTER),
('tx_packets', PmConfig.COUNTER),
('tx_ucast_packets', PmConfig.COUNTER),
('tx_mcast_packets', PmConfig.COUNTER),
('tx_bcast_packets', PmConfig.COUNTER),
('tx_error_packets', PmConfig.COUNTER)
}
nni_pm_names_from_kpi_extension = {
('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
('admin_state', PmConfig.STATE),
('oper_status', PmConfig.STATE),
('rx_bytes', PmConfig.COUNTER),
('rx_packets', PmConfig.COUNTER),
('rx_ucast_packets', PmConfig.COUNTER),
('rx_mcast_packets', PmConfig.COUNTER),
('rx_bcast_packets', PmConfig.COUNTER),
('rx_error_packets', PmConfig.COUNTER),
('tx_bytes', PmConfig.COUNTER),
('tx_packets', PmConfig.COUNTER),
('tx_ucast_packets', PmConfig.COUNTER),
('tx_mcast_packets', PmConfig.COUNTER),
('tx_bcast_packets', PmConfig.COUNTER),
('tx_error_packets', PmConfig.COUNTER),
('rx_crc_errors', PmConfig.COUNTER),
('bip_errors', PmConfig.COUNTER),
}
# pon_names uses same structure as nmi_names with the addition of pon_id to context
pon_pm_names = {
('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
('port_no', PmConfig.CONTEXT),
('admin_state', PmConfig.STATE),
('oper_status', PmConfig.STATE),
('rx_bytes', PmConfig.COUNTER),
('rx_packets', PmConfig.COUNTER),
('rx_ucast_packets', PmConfig.COUNTER),
('rx_mcast_packets', PmConfig.COUNTER),
('rx_bcast_packets', PmConfig.COUNTER),
('rx_error_packets', PmConfig.COUNTER),
('tx_bytes', PmConfig.COUNTER),
('tx_packets', PmConfig.COUNTER),
('tx_ucast_packets', PmConfig.COUNTER),
('tx_mcast_packets', PmConfig.COUNTER),
('tx_bcast_packets', PmConfig.COUNTER),
('tx_error_packets', PmConfig.COUNTER)
}
pon_pm_names_from_kpi_extension = {
('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
('admin_state', PmConfig.STATE),
('oper_status', PmConfig.STATE),
('rx_packets', PmConfig.COUNTER),
('rx_bytes', PmConfig.COUNTER),
('tx_packets', PmConfig.COUNTER),
('tx_bytes', PmConfig.COUNTER),
('tx_bip_errors', PmConfig.COUNTER),
('in_service_onus', PmConfig.GAUGE),
('closest_onu_distance', PmConfig.GAUGE)
}
onu_pm_names = {
('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
('pon_id', PmConfig.CONTEXT),
('onu_id', PmConfig.CONTEXT),
('fiber_length', PmConfig.GAUGE),
('equalization_delay', PmConfig.GAUGE),
('rssi', PmConfig.GAUGE),
}
gem_pm_names = {
('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
('pon_id', PmConfig.CONTEXT),
('onu_id', PmConfig.CONTEXT),
('gem_id', PmConfig.CONTEXT),
('alloc_id', PmConfig.GAUGE),
('rx_packets', PmConfig.COUNTER),
('rx_bytes', PmConfig.COUNTER),
('tx_packets', PmConfig.COUNTER),
('tx_bytes', PmConfig.COUNTER),
}
# Build a dict for the names. The caller will index to the correct values
names_dict = {"nni_pm_names": nni_pm_names,
"pon_pm_names": pon_pm_names,
"pon_pm_names_orig": pon_pm_names_from_kpi_extension,
"onu_pm_names": onu_pm_names,
"gem_pm_names": gem_pm_names,
}
return names_dict
def init_ports(self, device_id=12345, type="nni", log=None):
"""
This method collects the port objects: nni and pon that are updated with the
current data from the OLT
Both the northbound (nni) and southbound ports are indexed by the interface id (intf_id)
and NOT the port number. When the port object is instantiated it will contain the intf_id and
port_no values
:param type:
:param device_id:
:param log:
:return:
"""
try:
if type == "nni":
nni_ports = {}
for i in range(0, 1):
nni_port = self.build_port_object(i, type='nni')
nni_ports[nni_port.intf_id] = nni_port
return nni_ports
elif type == "pon":
pon_ports = {}
for i in range(0, 16):
pon_port = self.build_port_object(i, type="pon")
pon_ports[pon_port.intf_id] = pon_port
return pon_ports
else:
self.log.exception("Unmapped port type requested = " , type=type)
raise Exception("Unmapped port type requested = " + type)
except Exception as err:
raise Exception(err)
def build_port_object(self, port_num, type="nni"):
"""
Seperate method to allow for updating north and southbound ports
newly discovered ports and devices
:param port_num:
:param type:
:return:
"""
try:
"""
This builds a port object which is added to the
appropriate northbound or southbound values
"""
if type == "nni":
kwargs = {
'port_no': port_num,
'intf_id': self.platform.intf_id_to_port_no(port_num,
Port.ETHERNET_NNI),
"device_id": self.device.device_id
}
nni_port = NniPort
port = nni_port( **kwargs)
return port
elif type == "pon":
# PON ports require a different configuration
# intf_id and pon_id are currently equal.
kwargs = {
'port_no': port_num,
'intf_id': self.platform.intf_id_to_port_no(port_num,
Port.PON_OLT),
'pon-id': self.platform.intf_id_to_port_no(port_num,
Port.PON_OLT),
"device_id": self.device.device_id
}
pon_port = PonPort
port = pon_port(**kwargs)
return port
else:
self.log.exception("Unknown port type")
raise Exception("Unknown port type")
except Exception as err:
self.log.exception("Unknown port type", error=err)
raise Exception(err)
def update_port_object_kpi_data(self, port_object, datadict={}):
"""
This method takes the formatted data the is marshalled from
the initicator collector and updates the corresponding property by
attr get and set.
:param port: The port class to be updated
:param datadict:
:return:
"""
try:
cur_attr = ""
if isinstance(port_object, NniPort):
for k, v in datadict.items():
cur_attr = k
if hasattr(port_object, k):
setattr(port_object, k, v)
elif isinstance(port_object, PonPort):
for k, v in datadict.items():
cur_attr = k
if hasattr(port_object, k):
setattr(port_object, k, v)
else:
raise Exception("Must be either PON or NNI port.")
return
except Exception as err:
self.log.exception("Caught error updating port data: ", cur_attr=cur_attr, errormsg=err.message)
raise Exception(err)
class PonPort(object):
"""
This is a highly reduced version taken from the adtran pon_port.
TODO: Extend for use in the openolt adapter set.
"""
MAX_ONUS_SUPPORTED = 256
DEFAULT_ENABLED = False
MAX_DEPLOYMENT_RANGE = 25000 # Meters (OLT-PB maximum)
_MCAST_ONU_ID = 253
_MCAST_ALLOC_BASE = 0x500
_SUPPORTED_ACTIVATION_METHODS = ['autodiscovery'] # , 'autoactivate']
_SUPPORTED_AUTHENTICATION_METHODS = ['serial-number']
def __init__(self, **kwargs):
assert 'pon-id' in kwargs, 'PON ID not found'
self._pon_id = kwargs['pon-id']
self._device_id = kwargs['device_id']
self._intf_id = kwargs['intf_id']
self._port_no = kwargs['port_no']
self._port_id = 0
# self._name = 'xpon 0/{}'.format(self._pon_id+1)
self._label = 'pon-{}'.format(self._pon_id)
self._onus = {} # serial_number-base64 -> ONU (allowed list)
self._onu_by_id = {} # onu-id -> ONU
"""
Statistics taken from nni_port
self.intf_id = 0 #handled by getter
self.port_no = 0 #handled by getter
self.port_id = 0 #handled by getter
Note: In the current implementation of the kpis coming from the BAL the stats are the
samne model for NNI and PON.
TODO: Integrate additional kpis for the PON and other southbound port objecgts.
"""
self.rx_bytes = 0
self.rx_packets = 0
self.rx_mcast_packets = 0
self.rx_bcast_packets = 0
self.rx_error_packets = 0
self.tx_bytes = 0
self.tx_packets = 0
self.tx_ucast_packets = 0
self.tx_mcast_packets = 0
self.tx_bcast_packets = 0
self.tx_error_packets = 0
return
def __str__(self):
return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
self._admin_state,
self._oper_status,
self.olt)
@property
def intf_id(self):
return self._intf_id
@intf_id.setter
def intf_id(self, value):
self._intf_id = value
@property
def pon_id(self):
return self._pon_id
@pon_id.setter
def pon_id(self, value):
self._pon_id = value
@property
def port_no(self):
return self._port_no
@port_no.setter
def port_no(self, value):
self._port_no = value
@property
def port_id(self):
return self._port_id
@intf_id.setter
def port_id(self, value):
self._port_id = value
@property
def onus(self):
"""
Get a set of all ONUs. While the set is immutable, do not use this method
to get a collection that you will iterate through that my yield the CPU
such as inline callback. ONUs may be deleted at any time and they will
set some references to other objects to NULL during the 'delete' call.
Instead, get a list of ONU-IDs and iterate on these and call the 'onu'
method below (which will return 'None' if the ONU has been deleted.
:return: (frozenset) collection of ONU objects on this PON
"""
return frozenset(self._onus.values())
@property
def onu_ids(self):
return frozenset(self._onu_by_id.keys())
def onu(self, onu_id):
return self._onu_by_id.get(onu_id)
class NniPort(object):
"""
Northbound network port, often Ethernet-based
This is a highly reduced version taken from the adtran nni_port code set
TODO: add functions to allow for port specific values and operations
"""
def __init__(self, **kwargs):
# TODO: Extend for use in the openolt adapter set.
self.port_no = kwargs.get('port_no')
self._port_no = self.port_no
self._name = kwargs.get('name', 'nni-{}'.format(self._port_no))
self._logical_port = None
# Statistics
self.intf_id = kwargs.pop('intf_id', None)
self.port_no = 0
self.rx_bytes = 0
self.rx_packets = 0
self.rx_mcast_packets = 0
self.rx_bcast_packets = 0
self.rx_error_packets = 0
self.tx_bytes = 0
self.tx_packets = 0
self.tx_ucast_packets = 0
self.tx_mcast_packets = 0
self.tx_bcast_packets = 0
self.tx_error_packets = 0
return
def __str__(self):
return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
self._admin_state,
self._oper_status,
self._parent)