blob: 5bade181b2d937b06c63b9dced32526a4a9389a1 [file] [log] [blame]
William Kurkian6f436d02019-02-06 16:25:01 -05001#
2# Copyright 2018 the original author or authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
William Kurkian6f436d02019-02-06 16:25:01 -050016
William Kurkian6f436d02019-02-06 16:25:01 -050017
18from twisted.internet import reactor, defer
William Kurkian44cd7bb2019-02-11 16:39:12 -050019from pyvoltha.adapters.extensions.kpi.olt.olt_pm_metrics import OltPmMetrics
William Kurkian8b1690c2019-03-04 16:53:22 -050020from voltha_protos.device_pb2 import PmConfig, PmConfigs, PmGroupConfig, Port
William Kurkian6f436d02019-02-06 16:25:01 -050021
22
23class OpenOltStatisticsMgr(object):
24 def __init__(self, openolt_device, log, platform, **kargs):
25
26 """
27 kargs are used to pass debugging flags at this time.
28 :param openolt_device:
29 :param log:
30 :param kargs:
31 """
32 self.device = openolt_device
33 self.log = log
34 self.platform = platform
35 # Northbound and Southbound ports
36 # added to initialize the pm_metrics
37 self.northbound_ports = self.init_ports(type="nni")
38 self.southbound_ports = self.init_ports(type='pon')
39
40 self.pm_metrics = None
41 # The following can be used to allow a standalone test routine to start
42 # the metrics independently
43 self.metrics_init = kargs.pop("metrics_init", True)
44 if self.metrics_init == True:
45 self.init_pm_metrics()
46
47 def init_pm_metrics(self):
48 # Setup PM configuration for this device
49 if self.pm_metrics is None:
50 try:
51 self.device.reason = 'setting up Performance Monitoring configuration'
52 kwargs = {
53 'nni-ports': self.northbound_ports.values(),
54 'pon-ports': self.southbound_ports.values()
55 }
Arun Arora0972b602019-03-14 09:50:52 +000056 self.pm_metrics = OltPmMetrics(self.device.core_proxy, self.device.device_id,
William Kurkian4a14a942019-04-02 18:35:25 -040057 self.device.logical_device_id, self.device.serial_number,
William Kurkian6f436d02019-02-06 16:25:01 -050058 grouped=True, freq_override=False,
59 **kwargs)
60 """
61 override the default naming structures in the OltPmMetrics class.
62 This is being done until the protos can be modified in the BAL driver
63
64 """
65 self.pm_metrics.nni_pm_names = (self.get_openolt_port_pm_names())['nni_pm_names']
66 self.pm_metrics.nni_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
67 for (m, t) in self.pm_metrics.nni_pm_names}
68
69 self.pm_metrics.pon_pm_names = (self.get_openolt_port_pm_names())['pon_pm_names']
70 self.pm_metrics.pon_metrics_config = {m: PmConfig(name=m, type=t, enabled=True)
71 for (m, t) in self.pm_metrics.pon_pm_names}
72 pm_config = self.pm_metrics.make_proto()
73 self.log.info("initial-pm-config", pm_config=pm_config)
Arun Arora0972b602019-03-14 09:50:52 +000074 self.device.core_proxy.device_pm_config_update(pm_config, init=True)
William Kurkian6f436d02019-02-06 16:25:01 -050075 # Start collecting stats from the device after a brief pause
76 reactor.callLater(10, self.pm_metrics.start_collector)
77 except Exception as e:
78 self.log.exception('pm-setup', e=e)
79
80 def port_statistics_indication(self, port_stats):
81 # self.log.info('port-stats-collected', stats=port_stats)
82 self.ports_statistics_kpis(port_stats)
83 #FIXME: etcd problem, do not update objects for now
84
85 #
86 #
87 # #FIXME : only the first uplink is a logical port
88 # if platform.intf_id_to_port_type_name(port_stats.intf_id) ==
89 # Port.ETHERNET_NNI:
90 # # ONOS update
91 # self.update_logical_port_stats(port_stats)
92 # # update port object stats
William Kurkian23047b92019-05-01 11:02:35 -040093 # port = self.device.core_proxy.get_port(self.device.device_id,
William Kurkian6f436d02019-02-06 16:25:01 -050094 # port_no=port_stats.intf_id)
95 #
96 # if port is None:
97 # self.log.warn('port associated with this stats does not exist')
98 # return
99 #
100 # port.rx_packets = port_stats.rx_packets
101 # port.rx_bytes = port_stats.rx_bytes
102 # port.rx_errors = port_stats.rx_error_packets
103 # port.tx_packets = port_stats.tx_packets
104 # port.tx_bytes = port_stats.tx_bytes
105 # port.tx_errors = port_stats.tx_error_packets
106 #
107 # # Add port does an update if port exists
William Kurkian23047b92019-05-01 11:02:35 -0400108 # self.device.core_proxy.add_port(self.device.device_id, port)
William Kurkian6f436d02019-02-06 16:25:01 -0500109
110 def flow_statistics_indication(self, flow_stats):
111 self.log.info('flow-stats-collected', stats=flow_stats)
112 # TODO: send to kafka ?
113 # FIXME: etcd problem, do not update objects for now
114 # # UNTESTED : the openolt driver does not yet provide flow stats
William Kurkian23047b92019-05-01 11:02:35 -0400115 # self.device.core_proxy.update_flow_stats(
William Kurkian6f436d02019-02-06 16:25:01 -0500116 # self.device.logical_device_id,
117 # flow_id=flow_stats.flow_id, packet_count=flow_stats.tx_packets,
118 # byte_count=flow_stats.tx_bytes)
119
120 def ports_statistics_kpis(self, port_stats):
121 """
122 map the port stats values into a dictionary
123 Create a kpoEvent and publish to Kafka
124
125 :param port_stats:
126 :return:
127 """
128
129 try:
130 intf_id = port_stats.intf_id
131
132 if self.platform.intf_id_to_port_no(0, Port.ETHERNET_NNI) < intf_id < \
133 self.platform.intf_id_to_port_no(4, Port.ETHERNET_NNI) :
134 """
135 for this release we are only interested in the first NNI for
136 Northbound.
137 we are not using the other 3
138 """
139 return
140 else:
141
142 pm_data = {}
143 pm_data["rx_bytes"] = port_stats.rx_bytes
144 pm_data["rx_packets"] = port_stats.rx_packets
145 pm_data["rx_ucast_packets"] = port_stats.rx_ucast_packets
146 pm_data["rx_mcast_packets"] = port_stats.rx_mcast_packets
147 pm_data["rx_bcast_packets"] = port_stats.rx_bcast_packets
148 pm_data["rx_error_packets"] = port_stats.rx_error_packets
149 pm_data["tx_bytes"] = port_stats.tx_bytes
150 pm_data["tx_packets"] = port_stats.tx_packets
151 pm_data["tx_ucast_packets"] = port_stats.tx_ucast_packets
152 pm_data["tx_mcast_packets"] = port_stats.tx_mcast_packets
153 pm_data["tx_bcast_packets"] = port_stats.tx_bcast_packets
154 pm_data["tx_error_packets"] = port_stats.tx_error_packets
155 pm_data["rx_crc_errors"] = port_stats.rx_crc_errors
156 pm_data["bip_errors"] = port_stats.bip_errors
157
158 pm_data["intf_id"] = intf_id
159
160 """
161 Based upon the intf_id map to an nni port or a pon port
162 the intf_id is the key to the north or south bound collections
163
164 Based upon the intf_id the port object (nni_port or pon_port) will
165 have its data attr. updated by the current dataset collected.
166
167 For prefixing the rule is currently to use the port number and not the intf_id
168
169 """
170 #FIXME : Just use first NNI for now
171 if intf_id == self.platform.intf_id_to_port_no(0,
172 Port.ETHERNET_NNI):
173 #NNI port (just the first one)
174 self.update_port_object_kpi_data(
175 port_object=self.northbound_ports[port_stats.intf_id], datadict=pm_data)
176 else:
177 #PON ports
178 self.update_port_object_kpi_data(
179 port_object=self.southbound_ports[port_stats.intf_id],datadict=pm_data)
180 except Exception as err:
181 self.log.exception("Error publishing kpi statistics. ", errmessage=err)
182
William Kurkian6f436d02019-02-06 16:25:01 -0500183 """
184 The following 4 methods customer naming, the generation of the port objects, building of those
185 objects and populating new data. The pm metrics operate on the value that are contained in the Port objects.
186 This class updates those port objects with the current data from the grpc indication and
187 post the data on a fixed interval.
188
189 """
190 def get_openolt_port_pm_names(self):
191 """
192 This collects a dictionary of the custom port names
193 used by the openolt.
194
195 Some of these are the same as the pm names used by the olt_pm_metrics class
196 if the set is the same then there is no need to call this method. However, when
197 custom names are used in the protos then the specific names should be pushed into
198 the olt_pm_metrics class.
199
200 :return:
201 """
202 nni_pm_names = {
203 ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
204
205 ('admin_state', PmConfig.STATE),
206 ('oper_status', PmConfig.STATE),
207 ('port_no', PmConfig.GAUGE),
208 ('rx_bytes', PmConfig.COUNTER),
209 ('rx_packets', PmConfig.COUNTER),
210 ('rx_ucast_packets', PmConfig.COUNTER),
211 ('rx_mcast_packets', PmConfig.COUNTER),
212 ('rx_bcast_packets', PmConfig.COUNTER),
213 ('rx_error_packets', PmConfig.COUNTER),
214 ('tx_bytes', PmConfig.COUNTER),
215 ('tx_packets', PmConfig.COUNTER),
216 ('tx_ucast_packets', PmConfig.COUNTER),
217 ('tx_mcast_packets', PmConfig.COUNTER),
218 ('tx_bcast_packets', PmConfig.COUNTER),
219 ('tx_error_packets', PmConfig.COUNTER)
220 }
221 nni_pm_names_from_kpi_extension = {
222 ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
223
224 ('admin_state', PmConfig.STATE),
225 ('oper_status', PmConfig.STATE),
226
227 ('rx_bytes', PmConfig.COUNTER),
228 ('rx_packets', PmConfig.COUNTER),
229 ('rx_ucast_packets', PmConfig.COUNTER),
230 ('rx_mcast_packets', PmConfig.COUNTER),
231 ('rx_bcast_packets', PmConfig.COUNTER),
232 ('rx_error_packets', PmConfig.COUNTER),
233
234 ('tx_bytes', PmConfig.COUNTER),
235 ('tx_packets', PmConfig.COUNTER),
236 ('tx_ucast_packets', PmConfig.COUNTER),
237 ('tx_mcast_packets', PmConfig.COUNTER),
238 ('tx_bcast_packets', PmConfig.COUNTER),
239 ('tx_error_packets', PmConfig.COUNTER),
240 ('rx_crc_errors', PmConfig.COUNTER),
241 ('bip_errors', PmConfig.COUNTER),
242 }
243
244 # pon_names uses same structure as nmi_names with the addition of pon_id to context
245 pon_pm_names = {
246 ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
247 ('port_no', PmConfig.CONTEXT),
248
249 ('admin_state', PmConfig.STATE),
250 ('oper_status', PmConfig.STATE),
251 ('rx_bytes', PmConfig.COUNTER),
252 ('rx_packets', PmConfig.COUNTER),
253 ('rx_ucast_packets', PmConfig.COUNTER),
254 ('rx_mcast_packets', PmConfig.COUNTER),
255 ('rx_bcast_packets', PmConfig.COUNTER),
256 ('rx_error_packets', PmConfig.COUNTER),
257 ('tx_bytes', PmConfig.COUNTER),
258 ('tx_packets', PmConfig.COUNTER),
259 ('tx_ucast_packets', PmConfig.COUNTER),
260 ('tx_mcast_packets', PmConfig.COUNTER),
261 ('tx_bcast_packets', PmConfig.COUNTER),
262 ('tx_error_packets', PmConfig.COUNTER)
263 }
264 pon_pm_names_from_kpi_extension = {
265 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
266 ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
267
268 ('admin_state', PmConfig.STATE),
269 ('oper_status', PmConfig.STATE),
270 ('rx_packets', PmConfig.COUNTER),
271 ('rx_bytes', PmConfig.COUNTER),
272 ('tx_packets', PmConfig.COUNTER),
273 ('tx_bytes', PmConfig.COUNTER),
274 ('tx_bip_errors', PmConfig.COUNTER),
275 ('in_service_onus', PmConfig.GAUGE),
276 ('closest_onu_distance', PmConfig.GAUGE)
277 }
278 onu_pm_names = {
279 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
280 ('pon_id', PmConfig.CONTEXT),
281 ('onu_id', PmConfig.CONTEXT),
282
283 ('fiber_length', PmConfig.GAUGE),
284 ('equalization_delay', PmConfig.GAUGE),
285 ('rssi', PmConfig.GAUGE),
286 }
287 gem_pm_names = {
288 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
289 ('pon_id', PmConfig.CONTEXT),
290 ('onu_id', PmConfig.CONTEXT),
291 ('gem_id', PmConfig.CONTEXT),
292
293 ('alloc_id', PmConfig.GAUGE),
294 ('rx_packets', PmConfig.COUNTER),
295 ('rx_bytes', PmConfig.COUNTER),
296 ('tx_packets', PmConfig.COUNTER),
297 ('tx_bytes', PmConfig.COUNTER),
298 }
299 # Build a dict for the names. The caller will index to the correct values
300 names_dict = {"nni_pm_names": nni_pm_names,
301 "pon_pm_names": pon_pm_names,
302 "pon_pm_names_orig": pon_pm_names_from_kpi_extension,
303 "onu_pm_names": onu_pm_names,
304 "gem_pm_names": gem_pm_names,
305
306 }
307
308 return names_dict
309
310 def init_ports(self, device_id=12345, type="nni", log=None):
311 """
312 This method collects the port objects: nni and pon that are updated with the
313 current data from the OLT
314
315 Both the northbound (nni) and southbound ports are indexed by the interface id (intf_id)
316 and NOT the port number. When the port object is instantiated it will contain the intf_id and
317 port_no values
318
319 :param type:
320 :param device_id:
321 :param log:
322 :return:
323 """
324 try:
325 if type == "nni":
326 nni_ports = {}
327 for i in range(0, 1):
328 nni_port = self.build_port_object(i, type='nni')
329 nni_ports[nni_port.intf_id] = nni_port
330 return nni_ports
331 elif type == "pon":
332 pon_ports = {}
333 for i in range(0, 16):
334 pon_port = self.build_port_object(i, type="pon")
335 pon_ports[pon_port.intf_id] = pon_port
336 return pon_ports
337 else:
338 self.log.exception("Unmapped port type requested = " , type=type)
339 raise Exception("Unmapped port type requested = " + type)
340
341 except Exception as err:
342 raise Exception(err)
343
344 def build_port_object(self, port_num, type="nni"):
345 """
346 Seperate method to allow for updating north and southbound ports
347 newly discovered ports and devices
348
349 :param port_num:
350 :param type:
351 :return:
352 """
353 try:
354 """
355 This builds a port object which is added to the
356 appropriate northbound or southbound values
357 """
358 if type == "nni":
359 kwargs = {
360 'port_no': port_num,
361 'intf_id': self.platform.intf_id_to_port_no(port_num,
362 Port.ETHERNET_NNI),
363 "device_id": self.device.device_id
364 }
365 nni_port = NniPort
366 port = nni_port( **kwargs)
367 return port
368 elif type == "pon":
369 # PON ports require a different configuration
370 # intf_id and pon_id are currently equal.
371 kwargs = {
372 'port_no': port_num,
373 'intf_id': self.platform.intf_id_to_port_no(port_num,
374 Port.PON_OLT),
375 'pon-id': self.platform.intf_id_to_port_no(port_num,
376 Port.PON_OLT),
377 "device_id": self.device.device_id
378 }
379 pon_port = PonPort
380 port = pon_port(**kwargs)
381 return port
382
383 else:
384 self.log.exception("Unknown port type")
385 raise Exception("Unknown port type")
386
387 except Exception as err:
388 self.log.exception("Unknown port type", error=err)
389 raise Exception(err)
390
391 def update_port_object_kpi_data(self, port_object, datadict={}):
392 """
393 This method takes the formatted data the is marshalled from
394 the initicator collector and updates the corresponding property by
395 attr get and set.
396
397 :param port: The port class to be updated
398 :param datadict:
399 :return:
400 """
401
402 try:
403 cur_attr = ""
404 if isinstance(port_object, NniPort):
405 for k, v in datadict.items():
406 cur_attr = k
407 if hasattr(port_object, k):
408 setattr(port_object, k, v)
409 elif isinstance(port_object, PonPort):
410 for k, v in datadict.items():
411 cur_attr = k
412 if hasattr(port_object, k):
413 setattr(port_object, k, v)
414 else:
415 raise Exception("Must be either PON or NNI port.")
416 return
417 except Exception as err:
418 self.log.exception("Caught error updating port data: ", cur_attr=cur_attr, errormsg=err.message)
419 raise Exception(err)
420
421
422class PonPort(object):
423 """
424 This is a highly reduced version taken from the adtran pon_port.
425 TODO: Extend for use in the openolt adapter set.
426 """
427 MAX_ONUS_SUPPORTED = 256
428 DEFAULT_ENABLED = False
429 MAX_DEPLOYMENT_RANGE = 25000 # Meters (OLT-PB maximum)
430
431 _MCAST_ONU_ID = 253
432 _MCAST_ALLOC_BASE = 0x500
433
434 _SUPPORTED_ACTIVATION_METHODS = ['autodiscovery'] # , 'autoactivate']
435 _SUPPORTED_AUTHENTICATION_METHODS = ['serial-number']
436
437 def __init__(self, **kwargs):
438 assert 'pon-id' in kwargs, 'PON ID not found'
439
440 self._pon_id = kwargs['pon-id']
441 self._device_id = kwargs['device_id']
442 self._intf_id = kwargs['intf_id']
443 self._port_no = kwargs['port_no']
444 self._port_id = 0
445 # self._name = 'xpon 0/{}'.format(self._pon_id+1)
446 self._label = 'pon-{}'.format(self._pon_id)
447
448 self._onus = {} # serial_number-base64 -> ONU (allowed list)
449 self._onu_by_id = {} # onu-id -> ONU
450
451 """
452 Statistics taken from nni_port
453 self.intf_id = 0 #handled by getter
454 self.port_no = 0 #handled by getter
455 self.port_id = 0 #handled by getter
456
457 Note: In the current implementation of the kpis coming from the BAL the stats are the
458 samne model for NNI and PON.
459
460 TODO: Integrate additional kpis for the PON and other southbound port objecgts.
461
462 """
463
464 self.rx_bytes = 0
465 self.rx_packets = 0
466 self.rx_mcast_packets = 0
467 self.rx_bcast_packets = 0
468 self.rx_error_packets = 0
469 self.tx_bytes = 0
470 self.tx_packets = 0
471 self.tx_ucast_packets = 0
472 self.tx_mcast_packets = 0
473 self.tx_bcast_packets = 0
474 self.tx_error_packets = 0
475 return
476
477 def __str__(self):
478 return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
479 self._admin_state,
480 self._oper_status,
481 self.olt)
482
483 @property
484 def intf_id(self):
485 return self._intf_id
486
487 @intf_id.setter
488 def intf_id(self, value):
489 self._intf_id = value
490
491 @property
492 def pon_id(self):
493 return self._pon_id
494
495 @pon_id.setter
496 def pon_id(self, value):
497 self._pon_id = value
498
499 @property
500 def port_no(self):
501 return self._port_no
502
503 @port_no.setter
504 def port_no(self, value):
505 self._port_no = value
506
507 @property
508 def port_id(self):
509 return self._port_id
510
511 @intf_id.setter
512 def port_id(self, value):
513 self._port_id = value
514
515 @property
516 def onus(self):
517 """
518 Get a set of all ONUs. While the set is immutable, do not use this method
519 to get a collection that you will iterate through that my yield the CPU
520 such as inline callback. ONUs may be deleted at any time and they will
521 set some references to other objects to NULL during the 'delete' call.
522 Instead, get a list of ONU-IDs and iterate on these and call the 'onu'
523 method below (which will return 'None' if the ONU has been deleted.
524
525 :return: (frozenset) collection of ONU objects on this PON
526 """
527 return frozenset(self._onus.values())
528
529 @property
530 def onu_ids(self):
531 return frozenset(self._onu_by_id.keys())
532
533 def onu(self, onu_id):
534 return self._onu_by_id.get(onu_id)
535
536
537class NniPort(object):
538 """
539 Northbound network port, often Ethernet-based
540
541 This is a highly reduced version taken from the adtran nni_port code set
542 TODO: add functions to allow for port specific values and operations
543
544 """
545 def __init__(self, **kwargs):
546 # TODO: Extend for use in the openolt adapter set.
547 self.port_no = kwargs.get('port_no')
548 self._port_no = self.port_no
549 self._name = kwargs.get('name', 'nni-{}'.format(self._port_no))
550 self._logical_port = None
551
552 # Statistics
553 self.intf_id = kwargs.pop('intf_id', None)
554 self.port_no = 0
555 self.rx_bytes = 0
556 self.rx_packets = 0
557 self.rx_mcast_packets = 0
558 self.rx_bcast_packets = 0
559 self.rx_error_packets = 0
560 self.tx_bytes = 0
561 self.tx_packets = 0
562 self.tx_ucast_packets = 0
563 self.tx_mcast_packets = 0
564 self.tx_bcast_packets = 0
565 self.tx_error_packets = 0
566 return
567
568 def __str__(self):
569 return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
570 self._admin_state,
571 self._oper_status,
572 self._parent)