Initial commit moving openolt adapter from voltha-go to the new repo.
This version works with ponsim rather than openolt, this is temporary.
It is currently being fixed to work with openolt.

Change-Id: I34a800c98f050140b367e2d474b7aa8b79f34b9a
Signed-off-by: William Kurkian <wkurkian@cisco.com>
diff --git a/python/adapters/openolt/openolt_statistics.py b/python/adapters/openolt/openolt_statistics.py
new file mode 100644
index 0000000..bfdb61c
--- /dev/null
+++ b/python/adapters/openolt/openolt_statistics.py
@@ -0,0 +1,606 @@
+#
+# 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 voltha.protos.events_pb2 import KpiEvent, MetricValuePairs
+# from voltha.protos.events_pb2 import KpiEventType
+
+# from voltha.adapters.openolt.nni_port import NniPort
+# from voltha.adapters.openolt.pon_port import PonPort
+# from voltha.protos.device_pb2 import Port
+
+from twisted.internet import reactor, defer
+from voltha.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)