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