blob: ccab90a654c2379fa95655805ad783f4c2bf7658 [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
20from pyvoltha.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 }
56 self.pm_metrics = OltPmMetrics(self.device.adapter_agent, self.device.device_id,
57 self.device.logical_device_id,
58 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)
74 self.device.adapter_agent.update_device_pm_config(pm_config, init=True)
75 # 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
93 # port = self.device.adapter_agent.get_port(self.device.device_id,
94 # 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
108 # self.device.adapter_agent.add_port(self.device.device_id, port)
109
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
115 # self.device.adapter_agent.update_flow_stats(
116 # 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
183 def update_logical_port_stats(self, port_stats):
184 try:
185 label = 'nni-{}'.format(port_stats.intf_id)
186 logical_port = self.device.adapter_agent.get_logical_port(
187 self.device.logical_device_id, label)
188 except KeyError as e:
189 self.log.warn('logical port was not found, it may not have been '
190 'created yet', exception=e)
191 return
192
193 if logical_port is None:
194 self.log.error('logical-port-is-None',
195 logical_device_id=self.device.logical_device_id, label=label,
196 port_stats=port_stats)
197 return
198
199 logical_port.ofp_port_stats.rx_packets = port_stats.rx_packets
200 logical_port.ofp_port_stats.rx_bytes = port_stats.rx_bytes
201 logical_port.ofp_port_stats.tx_packets = port_stats.tx_packets
202 logical_port.ofp_port_stats.tx_bytes = port_stats.tx_bytes
203 logical_port.ofp_port_stats.rx_errors = port_stats.rx_error_packets
204 logical_port.ofp_port_stats.tx_errors = port_stats.tx_error_packets
205 logical_port.ofp_port_stats.rx_crc_err = port_stats.rx_crc_errors
206
207 self.log.debug('after-stats-update', port=logical_port)
208
209 self.device.adapter_agent.update_logical_port(
210 self.device.logical_device_id, logical_port)
211
212 """
213 The following 4 methods customer naming, the generation of the port objects, building of those
214 objects and populating new data. The pm metrics operate on the value that are contained in the Port objects.
215 This class updates those port objects with the current data from the grpc indication and
216 post the data on a fixed interval.
217
218 """
219 def get_openolt_port_pm_names(self):
220 """
221 This collects a dictionary of the custom port names
222 used by the openolt.
223
224 Some of these are the same as the pm names used by the olt_pm_metrics class
225 if the set is the same then there is no need to call this method. However, when
226 custom names are used in the protos then the specific names should be pushed into
227 the olt_pm_metrics class.
228
229 :return:
230 """
231 nni_pm_names = {
232 ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
233
234 ('admin_state', PmConfig.STATE),
235 ('oper_status', PmConfig.STATE),
236 ('port_no', PmConfig.GAUGE),
237 ('rx_bytes', PmConfig.COUNTER),
238 ('rx_packets', PmConfig.COUNTER),
239 ('rx_ucast_packets', PmConfig.COUNTER),
240 ('rx_mcast_packets', PmConfig.COUNTER),
241 ('rx_bcast_packets', PmConfig.COUNTER),
242 ('rx_error_packets', PmConfig.COUNTER),
243 ('tx_bytes', PmConfig.COUNTER),
244 ('tx_packets', PmConfig.COUNTER),
245 ('tx_ucast_packets', PmConfig.COUNTER),
246 ('tx_mcast_packets', PmConfig.COUNTER),
247 ('tx_bcast_packets', PmConfig.COUNTER),
248 ('tx_error_packets', PmConfig.COUNTER)
249 }
250 nni_pm_names_from_kpi_extension = {
251 ('intf_id', PmConfig.CONTEXT), # Physical device interface ID/Port number
252
253 ('admin_state', PmConfig.STATE),
254 ('oper_status', PmConfig.STATE),
255
256 ('rx_bytes', PmConfig.COUNTER),
257 ('rx_packets', PmConfig.COUNTER),
258 ('rx_ucast_packets', PmConfig.COUNTER),
259 ('rx_mcast_packets', PmConfig.COUNTER),
260 ('rx_bcast_packets', PmConfig.COUNTER),
261 ('rx_error_packets', PmConfig.COUNTER),
262
263 ('tx_bytes', PmConfig.COUNTER),
264 ('tx_packets', PmConfig.COUNTER),
265 ('tx_ucast_packets', PmConfig.COUNTER),
266 ('tx_mcast_packets', PmConfig.COUNTER),
267 ('tx_bcast_packets', PmConfig.COUNTER),
268 ('tx_error_packets', PmConfig.COUNTER),
269 ('rx_crc_errors', PmConfig.COUNTER),
270 ('bip_errors', PmConfig.COUNTER),
271 }
272
273 # pon_names uses same structure as nmi_names with the addition of pon_id to context
274 pon_pm_names = {
275 ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
276 ('port_no', PmConfig.CONTEXT),
277
278 ('admin_state', PmConfig.STATE),
279 ('oper_status', PmConfig.STATE),
280 ('rx_bytes', PmConfig.COUNTER),
281 ('rx_packets', PmConfig.COUNTER),
282 ('rx_ucast_packets', PmConfig.COUNTER),
283 ('rx_mcast_packets', PmConfig.COUNTER),
284 ('rx_bcast_packets', PmConfig.COUNTER),
285 ('rx_error_packets', PmConfig.COUNTER),
286 ('tx_bytes', PmConfig.COUNTER),
287 ('tx_packets', PmConfig.COUNTER),
288 ('tx_ucast_packets', PmConfig.COUNTER),
289 ('tx_mcast_packets', PmConfig.COUNTER),
290 ('tx_bcast_packets', PmConfig.COUNTER),
291 ('tx_error_packets', PmConfig.COUNTER)
292 }
293 pon_pm_names_from_kpi_extension = {
294 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
295 ('pon_id', PmConfig.CONTEXT), # PON ID (0..n)
296
297 ('admin_state', PmConfig.STATE),
298 ('oper_status', PmConfig.STATE),
299 ('rx_packets', PmConfig.COUNTER),
300 ('rx_bytes', PmConfig.COUNTER),
301 ('tx_packets', PmConfig.COUNTER),
302 ('tx_bytes', PmConfig.COUNTER),
303 ('tx_bip_errors', PmConfig.COUNTER),
304 ('in_service_onus', PmConfig.GAUGE),
305 ('closest_onu_distance', PmConfig.GAUGE)
306 }
307 onu_pm_names = {
308 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
309 ('pon_id', PmConfig.CONTEXT),
310 ('onu_id', PmConfig.CONTEXT),
311
312 ('fiber_length', PmConfig.GAUGE),
313 ('equalization_delay', PmConfig.GAUGE),
314 ('rssi', PmConfig.GAUGE),
315 }
316 gem_pm_names = {
317 ('intf_id', PmConfig.CONTEXT), # Physical device port number (PON)
318 ('pon_id', PmConfig.CONTEXT),
319 ('onu_id', PmConfig.CONTEXT),
320 ('gem_id', PmConfig.CONTEXT),
321
322 ('alloc_id', PmConfig.GAUGE),
323 ('rx_packets', PmConfig.COUNTER),
324 ('rx_bytes', PmConfig.COUNTER),
325 ('tx_packets', PmConfig.COUNTER),
326 ('tx_bytes', PmConfig.COUNTER),
327 }
328 # Build a dict for the names. The caller will index to the correct values
329 names_dict = {"nni_pm_names": nni_pm_names,
330 "pon_pm_names": pon_pm_names,
331 "pon_pm_names_orig": pon_pm_names_from_kpi_extension,
332 "onu_pm_names": onu_pm_names,
333 "gem_pm_names": gem_pm_names,
334
335 }
336
337 return names_dict
338
339 def init_ports(self, device_id=12345, type="nni", log=None):
340 """
341 This method collects the port objects: nni and pon that are updated with the
342 current data from the OLT
343
344 Both the northbound (nni) and southbound ports are indexed by the interface id (intf_id)
345 and NOT the port number. When the port object is instantiated it will contain the intf_id and
346 port_no values
347
348 :param type:
349 :param device_id:
350 :param log:
351 :return:
352 """
353 try:
354 if type == "nni":
355 nni_ports = {}
356 for i in range(0, 1):
357 nni_port = self.build_port_object(i, type='nni')
358 nni_ports[nni_port.intf_id] = nni_port
359 return nni_ports
360 elif type == "pon":
361 pon_ports = {}
362 for i in range(0, 16):
363 pon_port = self.build_port_object(i, type="pon")
364 pon_ports[pon_port.intf_id] = pon_port
365 return pon_ports
366 else:
367 self.log.exception("Unmapped port type requested = " , type=type)
368 raise Exception("Unmapped port type requested = " + type)
369
370 except Exception as err:
371 raise Exception(err)
372
373 def build_port_object(self, port_num, type="nni"):
374 """
375 Seperate method to allow for updating north and southbound ports
376 newly discovered ports and devices
377
378 :param port_num:
379 :param type:
380 :return:
381 """
382 try:
383 """
384 This builds a port object which is added to the
385 appropriate northbound or southbound values
386 """
387 if type == "nni":
388 kwargs = {
389 'port_no': port_num,
390 'intf_id': self.platform.intf_id_to_port_no(port_num,
391 Port.ETHERNET_NNI),
392 "device_id": self.device.device_id
393 }
394 nni_port = NniPort
395 port = nni_port( **kwargs)
396 return port
397 elif type == "pon":
398 # PON ports require a different configuration
399 # intf_id and pon_id are currently equal.
400 kwargs = {
401 'port_no': port_num,
402 'intf_id': self.platform.intf_id_to_port_no(port_num,
403 Port.PON_OLT),
404 'pon-id': self.platform.intf_id_to_port_no(port_num,
405 Port.PON_OLT),
406 "device_id": self.device.device_id
407 }
408 pon_port = PonPort
409 port = pon_port(**kwargs)
410 return port
411
412 else:
413 self.log.exception("Unknown port type")
414 raise Exception("Unknown port type")
415
416 except Exception as err:
417 self.log.exception("Unknown port type", error=err)
418 raise Exception(err)
419
420 def update_port_object_kpi_data(self, port_object, datadict={}):
421 """
422 This method takes the formatted data the is marshalled from
423 the initicator collector and updates the corresponding property by
424 attr get and set.
425
426 :param port: The port class to be updated
427 :param datadict:
428 :return:
429 """
430
431 try:
432 cur_attr = ""
433 if isinstance(port_object, NniPort):
434 for k, v in datadict.items():
435 cur_attr = k
436 if hasattr(port_object, k):
437 setattr(port_object, k, v)
438 elif isinstance(port_object, PonPort):
439 for k, v in datadict.items():
440 cur_attr = k
441 if hasattr(port_object, k):
442 setattr(port_object, k, v)
443 else:
444 raise Exception("Must be either PON or NNI port.")
445 return
446 except Exception as err:
447 self.log.exception("Caught error updating port data: ", cur_attr=cur_attr, errormsg=err.message)
448 raise Exception(err)
449
450
451class PonPort(object):
452 """
453 This is a highly reduced version taken from the adtran pon_port.
454 TODO: Extend for use in the openolt adapter set.
455 """
456 MAX_ONUS_SUPPORTED = 256
457 DEFAULT_ENABLED = False
458 MAX_DEPLOYMENT_RANGE = 25000 # Meters (OLT-PB maximum)
459
460 _MCAST_ONU_ID = 253
461 _MCAST_ALLOC_BASE = 0x500
462
463 _SUPPORTED_ACTIVATION_METHODS = ['autodiscovery'] # , 'autoactivate']
464 _SUPPORTED_AUTHENTICATION_METHODS = ['serial-number']
465
466 def __init__(self, **kwargs):
467 assert 'pon-id' in kwargs, 'PON ID not found'
468
469 self._pon_id = kwargs['pon-id']
470 self._device_id = kwargs['device_id']
471 self._intf_id = kwargs['intf_id']
472 self._port_no = kwargs['port_no']
473 self._port_id = 0
474 # self._name = 'xpon 0/{}'.format(self._pon_id+1)
475 self._label = 'pon-{}'.format(self._pon_id)
476
477 self._onus = {} # serial_number-base64 -> ONU (allowed list)
478 self._onu_by_id = {} # onu-id -> ONU
479
480 """
481 Statistics taken from nni_port
482 self.intf_id = 0 #handled by getter
483 self.port_no = 0 #handled by getter
484 self.port_id = 0 #handled by getter
485
486 Note: In the current implementation of the kpis coming from the BAL the stats are the
487 samne model for NNI and PON.
488
489 TODO: Integrate additional kpis for the PON and other southbound port objecgts.
490
491 """
492
493 self.rx_bytes = 0
494 self.rx_packets = 0
495 self.rx_mcast_packets = 0
496 self.rx_bcast_packets = 0
497 self.rx_error_packets = 0
498 self.tx_bytes = 0
499 self.tx_packets = 0
500 self.tx_ucast_packets = 0
501 self.tx_mcast_packets = 0
502 self.tx_bcast_packets = 0
503 self.tx_error_packets = 0
504 return
505
506 def __str__(self):
507 return "PonPort-{}: Admin: {}, Oper: {}, OLT: {}".format(self._label,
508 self._admin_state,
509 self._oper_status,
510 self.olt)
511
512 @property
513 def intf_id(self):
514 return self._intf_id
515
516 @intf_id.setter
517 def intf_id(self, value):
518 self._intf_id = value
519
520 @property
521 def pon_id(self):
522 return self._pon_id
523
524 @pon_id.setter
525 def pon_id(self, value):
526 self._pon_id = value
527
528 @property
529 def port_no(self):
530 return self._port_no
531
532 @port_no.setter
533 def port_no(self, value):
534 self._port_no = value
535
536 @property
537 def port_id(self):
538 return self._port_id
539
540 @intf_id.setter
541 def port_id(self, value):
542 self._port_id = value
543
544 @property
545 def onus(self):
546 """
547 Get a set of all ONUs. While the set is immutable, do not use this method
548 to get a collection that you will iterate through that my yield the CPU
549 such as inline callback. ONUs may be deleted at any time and they will
550 set some references to other objects to NULL during the 'delete' call.
551 Instead, get a list of ONU-IDs and iterate on these and call the 'onu'
552 method below (which will return 'None' if the ONU has been deleted.
553
554 :return: (frozenset) collection of ONU objects on this PON
555 """
556 return frozenset(self._onus.values())
557
558 @property
559 def onu_ids(self):
560 return frozenset(self._onu_by_id.keys())
561
562 def onu(self, onu_id):
563 return self._onu_by_id.get(onu_id)
564
565
566class NniPort(object):
567 """
568 Northbound network port, often Ethernet-based
569
570 This is a highly reduced version taken from the adtran nni_port code set
571 TODO: add functions to allow for port specific values and operations
572
573 """
574 def __init__(self, **kwargs):
575 # TODO: Extend for use in the openolt adapter set.
576 self.port_no = kwargs.get('port_no')
577 self._port_no = self.port_no
578 self._name = kwargs.get('name', 'nni-{}'.format(self._port_no))
579 self._logical_port = None
580
581 # Statistics
582 self.intf_id = kwargs.pop('intf_id', None)
583 self.port_no = 0
584 self.rx_bytes = 0
585 self.rx_packets = 0
586 self.rx_mcast_packets = 0
587 self.rx_bcast_packets = 0
588 self.rx_error_packets = 0
589 self.tx_bytes = 0
590 self.tx_packets = 0
591 self.tx_ucast_packets = 0
592 self.tx_mcast_packets = 0
593 self.tx_bcast_packets = 0
594 self.tx_error_packets = 0
595 return
596
597 def __str__(self):
598 return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
599 self._admin_state,
600 self._oper_status,
601 self._parent)