VOL-1023 - Supporting multiple UNI per ONU

Added support for multiple UNIs per ONU by enabling an opt-in parameter in Resource Manager Profile
attribute key "uni_id_start" and "uni_id_end". This represents the 0 based local-device uni
index range. So, for an ONU to support a single UNI, the default (including omitted) value is 0.
To support multiple UNIs per ONU, set "uni_id_end" to the maximum (0-based) index on the ONU.

Plumbed in support throughout for multiple UNIs. Each UNI receives a dedicated TCONT/GEM from
TechProfile and is applied a dedicated MAC Bridge Instance in the ONU. Each UNI is effectively
treated the same whether on the same or different ONUs.

uni_id is used throughout to be the ONU-relative device port number (0-based)
port_no is the logical port number

Change-Id: I443d2322a2d414a358f1e0c629779c4929ce13c8
diff --git a/voltha/adapters/brcm_openomci_onu/brcm_openomci_onu_handler.py b/voltha/adapters/brcm_openomci_onu/brcm_openomci_onu_handler.py
index f3fdf9f..fde2591 100644
--- a/voltha/adapters/brcm_openomci_onu/brcm_openomci_onu_handler.py
+++ b/voltha/adapters/brcm_openomci_onu/brcm_openomci_onu_handler.py
@@ -21,6 +21,9 @@
 import json
 import ast
 import structlog
+
+from collections import OrderedDict
+
 from twisted.internet import reactor, task
 from twisted.internet.defer import DeferredQueue, inlineCallbacks, returnValue, TimeoutError
 
@@ -60,7 +63,6 @@
 log = structlog.get_logger()
 
 _STARTUP_RETRY_WAIT = 20
-_MAXIMUM_PORT = 128  # UNI ports
 
 
 class BrcmOpenomciOnuHandler(object):
@@ -86,7 +88,6 @@
 
         self._onu_indication = None
         self._unis = dict()  # Port # -> UniPort
-        self._port_number_pool = IndexPool(_MAXIMUM_PORT, 0)
 
         self._pon = None
         # TODO: probably shouldnt be hardcoded, determine from olt maybe?
@@ -158,19 +159,13 @@
                          if uni.name == port_no_or_name), None)
 
         assert isinstance(port_no_or_name, int), 'Invalid parameter type'
-        return self._unis.get(port_no_or_name)
+        return next((uni for uni in self.uni_ports
+                    if uni.logical_port_number == port_no_or_name), None)
 
     @property
     def pon_port(self):
         return self._pon
 
-    @property
-    def _next_port_number(self):
-        return self._port_number_pool.get_next()
-
-    def _release_port_number(self, number):
-        self._port_number_pool.release(number)
-
     def receive_message(self, msg):
         if self.omci_cc is not None:
             self.omci_cc.receive_message(msg)
@@ -275,7 +270,8 @@
         try:
             if event_msg['event'] == 'download_tech_profile':
                 tp_path = event_msg['event_data']
-                self.load_and_configure_tech_profile(tp_path)
+                uni_id = event_msg['uni_id']
+                self.load_and_configure_tech_profile(uni_id, tp_path)
 
         except Exception as e:
             self.log.error("exception-handling-onu-event", e=e)
@@ -328,7 +324,7 @@
         else:
             self.log.debug("parent-adapter-not-available")
 
-    def _create_tconts(self, us_scheduler):
+    def _create_tconts(self, uni_id, us_scheduler):
         alloc_id = us_scheduler['alloc_id']
         q_sched_policy = us_scheduler['q_sched_policy']
         self.log.debug('create-tcont', us_scheduler=us_scheduler)
@@ -336,6 +332,7 @@
         tcontdict = dict()
         tcontdict['alloc-id'] = alloc_id
         tcontdict['q_sched_policy'] = q_sched_policy
+        tcontdict['uni_id'] = uni_id
 
         # TODO: Not sure what to do with any of this...
         tddata = dict()
@@ -353,7 +350,7 @@
         self.log.debug('pon-add-tcont', tcont=tcont)
 
     # Called when there is an olt up indication, providing the gem port id chosen by the olt handler
-    def _create_gemports(self, gem_ports, alloc_id_ref, direction):
+    def _create_gemports(self, uni_id, gem_ports, alloc_id_ref, direction):
         self.log.debug('create-gemport',
                        gem_ports=gem_ports, direction=direction)
 
@@ -376,6 +373,7 @@
             gemdict['priority_q'] = gem_port['priority_q']
             gemdict['scheduling_policy'] = gem_port['scheduling_policy']
             gemdict['weight'] = gem_port['weight']
+            gemdict['uni_id'] = uni_id
 
             gem_port = OnuGemPort.create(self, gem_port=gemdict, entity_id=self._pon.next_gem_entity_id)
 
@@ -383,24 +381,31 @@
 
             self.log.debug('pon-add-gemport', gem_port=gem_port)
 
-    def _do_tech_profile_configuration(self, tp):
+    def _do_tech_profile_configuration(self, uni_id, tp):
         num_of_tconts = tp['num_of_tconts']
         us_scheduler = tp['us_scheduler']
         alloc_id = us_scheduler['alloc_id']
-        self._create_tconts(us_scheduler)
+        self._create_tconts(uni_id, us_scheduler)
         upstream_gem_port_attribute_list = tp['upstream_gem_port_attribute_list']
-        self._create_gemports(upstream_gem_port_attribute_list, alloc_id, "UPSTREAM")
+        self._create_gemports(uni_id, upstream_gem_port_attribute_list, alloc_id, "UPSTREAM")
         downstream_gem_port_attribute_list = tp['downstream_gem_port_attribute_list']
-        self._create_gemports(downstream_gem_port_attribute_list, alloc_id, "DOWNSTREAM")
+        self._create_gemports(uni_id, downstream_gem_port_attribute_list, alloc_id, "DOWNSTREAM")
 
-    def load_and_configure_tech_profile(self, tp_path):
+    def load_and_configure_tech_profile(self, uni_id, tp_path):
         self.log.debug("loading-tech-profile-configuration")
-        if tp_path not in self._tech_profile_download_done:
-            self._tech_profile_download_done[tp_path] = False
 
-        if not self._tech_profile_download_done[tp_path]:
+        if uni_id not in self._tp_service_specific_task:
+            self._tp_service_specific_task[uni_id] = dict()
+
+        if uni_id not in self._tech_profile_download_done:
+            self._tech_profile_download_done[uni_id] = dict()
+
+        if tp_path not in self._tech_profile_download_done[uni_id]:
+            self._tech_profile_download_done[uni_id][tp_path] = False
+
+        if not self._tech_profile_download_done[uni_id][tp_path]:
             try:
-                if tp_path in self._tp_service_specific_task:
+                if tp_path in self._tp_service_specific_task[uni_id]:
                     self.log.info("tech-profile-config-already-in-progress",
                                    tp_path=tp_path)
                     return
@@ -408,16 +413,16 @@
                 tp = self.kv_client[tp_path]
                 tp = ast.literal_eval(tp)
                 self.log.debug("tp-instance", tp=tp)
-                self._do_tech_profile_configuration(tp)
+                self._do_tech_profile_configuration(uni_id, tp)
 
                 def success(_results):
                     self.log.info("tech-profile-config-done-successfully")
                     device = self.adapter_agent.get_device(self.device_id)
                     device.reason = 'tech-profile-config-download-success'
                     self.adapter_agent.update_device(device)
-                    if tp_path in self._tp_service_specific_task:
-                        del self._tp_service_specific_task[tp_path]
-                    self._tech_profile_download_done[tp_path] = True
+                    if tp_path in self._tp_service_specific_task[uni_id]:
+                        del self._tp_service_specific_task[uni_id][tp_path]
+                    self._tech_profile_download_done[uni_id][tp_path] = True
 
                 def failure(_reason):
                     self.log.warn('tech-profile-config-failure-retrying',
@@ -425,16 +430,16 @@
                     device = self.adapter_agent.get_device(self.device_id)
                     device.reason = 'tech-profile-config-download-failure-retrying'
                     self.adapter_agent.update_device(device)
-                    if tp_path in self._tp_service_specific_task:
-                        del self._tp_service_specific_task[tp_path]
+                    if tp_path in self._tp_service_specific_task[uni_id]:
+                        del self._tp_service_specific_task[uni_id][tp_path]
                     self._deferred = reactor.callLater(_STARTUP_RETRY_WAIT, self.load_and_configure_tech_profile,
-                                                       tp_path)
+                                                       uni_id, tp_path)
 
                 self.log.info('downloading-tech-profile-configuration')
-                self._tp_service_specific_task[tp_path] = \
-                       BrcmTpServiceSpecificTask(self.omci_agent, self)
+                self._tp_service_specific_task[uni_id][tp_path] = \
+                       BrcmTpServiceSpecificTask(self.omci_agent, self, uni_id)
                 self._deferred = \
-                       self._onu_omci_device.task_runner.queue_task(self._tp_service_specific_task[tp_path])
+                       self._onu_omci_device.task_runner.queue_task(self._tp_service_specific_task[uni_id][tp_path])
                 self._deferred.addCallbacks(success, failure)
 
             except Exception as e:
@@ -489,15 +494,18 @@
                 _in_port = fd.get_in_port(flow)
                 assert _in_port is not None
 
+                _out_port = fd.get_out_port(flow)  # may be None
+
                 if is_downstream(_in_port):
-                    self.log.debug('downstream-flow')
+                    self.log.debug('downstream-flow', in_port=_in_port, out_port=_out_port)
+                    uni_port = self.uni_port(_out_port)
                 elif is_upstream(_in_port):
-                    self.log.debug('upstream-flow')
+                    self.log.debug('upstream-flow', in_port=_in_port, out_port=_out_port)
+                    uni_port = self.uni_port(_in_port)
                 else:
                     raise Exception('port should be 1 or 2 by our convention')
 
-                _out_port = fd.get_out_port(flow)  # may be None
-                self.log.debug('out-port', out_port=_out_port)
+                self.log.debug('flow-ports', in_port=_in_port, out_port=_out_port, uni_port=str(uni_port))
 
                 for field in fd.get_ofb_fields(flow):
                     if field.type == fd.ETH_TYPE:
@@ -596,13 +604,16 @@
                 elif _set_vlan_vid is None or _set_vlan_vid == 0:
                     self.log.warn('ignorning-flow-that-does-not-set-vlanid')
                 else:
-                    self._add_vlan_filter_task(device, _set_vlan_vid)
+                    self.log.warn('set-vlanid', uni_id=uni_port.port_number, set_vlan_vid=_set_vlan_vid)
+                    self._add_vlan_filter_task(device, uni_port, _set_vlan_vid)
 
             except Exception as e:
                 self.log.exception('failed-to-install-flow', e=e, flow=flow)
 
 
-    def _add_vlan_filter_task(self, device, _set_vlan_vid):
+    def _add_vlan_filter_task(self, device, uni_port, _set_vlan_vid):
+        assert uni_port is not None
+
         def success(_results):
             self.log.info('vlan-tagging-success', _results=_results)
             device.reason = 'omci-flows-pushed'
@@ -612,10 +623,10 @@
             self.log.warn('vlan-tagging-failure', _reason=_reason)
             device.reason = 'omci-flows-failed-retrying'
             self._vlan_filter_task = reactor.callLater(_STARTUP_RETRY_WAIT,
-                                                       self._add_vlan_filter_task, device, _set_vlan_vid)
+                                                       self._add_vlan_filter_task, device, uni_port, _set_vlan_vid)
 
         self.log.info('setting-vlan-tag')
-        self._vlan_filter_task = BrcmVlanFilterTask(self.omci_agent, self.device_id, _set_vlan_vid)
+        self._vlan_filter_task = BrcmVlanFilterTask(self.omci_agent, self.device_id, uni_port, _set_vlan_vid)
         self._deferred = self._onu_omci_device.task_runner.queue_task(self._vlan_filter_task)
         self._deferred.addCallbacks(success, failure)
 
@@ -653,8 +664,8 @@
             reactor.callLater(0, self._onu_omci_device.stop)
 
             # Let TP download happen again
-            self._tp_service_specific_task.clear()
-            self._tech_profile_download_done.clear()
+            for i in self._tp_service_specific_task: i.clear()
+            for i in self._tech_profile_download_done: i.clear()
 
             self.disable_ports(onu_device)
             onu_device.reason = "stopping-openomci"
@@ -674,8 +685,8 @@
         reactor.callLater(0, self._onu_omci_device.stop)
 
         # Let TP download happen again
-        self._tp_service_specific_task.clear()
-        self._tech_profile_download_done.clear()
+        for i in self._tp_service_specific_task: i.clear()
+        for i in self._tech_profile_download_done: i.clear()
 
         self.disable_ports(onu_device)
         onu_device.reason = "stopping-openomci"
@@ -720,8 +731,8 @@
                 reactor.callLater(0, self._onu_omci_device.stop)
 
                 # Let TP download happen again
-                self._tp_service_specific_task.clear()
-                self._tech_profile_download_done.clear()
+                for i in self._tp_service_specific_task: i.clear()
+                for i in self._tech_profile_download_done: i.clear()
 
                 self.disable_ports(device)
                 device.oper_status = OperStatus.UNKNOWN
@@ -895,18 +906,30 @@
                     uni_value = config.uni_g_entities[entity_id]
                     self.log.debug("discovered-uni", entity_id=entity_id, value=uni_value)
 
-                # TODO: can only support one UNI per ONU at this time. break out as soon as we have a good UNI
+                uni_entities = OrderedDict()
                 for entity_id in pptp_list:
                     pptp_value = config.pptp_entities[entity_id]
                     self.log.debug("discovered-pptp", entity_id=entity_id, value=pptp_value)
-                    self._add_uni_port(entity_id, uni_type=UniType.PPTP)
-                    break
+                    uni_entities[entity_id] = UniType.PPTP
 
                 for entity_id in veip_list:
                     veip_value = config.veip_entities[entity_id]
                     self.log.debug("discovered-veip", entity_id=entity_id, value=veip_value)
-                    self._add_uni_port(entity_id, uni_type=UniType.VEIP)
-                    break
+                    uni_entities[entity_id] = UniType.VEIP
+
+                uni_id = 0
+                for entity_id, uni_type in uni_entities.iteritems():
+                    try:
+                        self._add_uni_port(entity_id, uni_id, uni_type)
+                        uni_id += 1
+                    except AssertionError as e:
+                        self.log.warn("could not add UNI", entity_id=entity_id, uni_type=uni_type, e=e)
+
+                multi_uni = len(self._unis) > 1
+                for uni_port in self._unis.itervalues():
+                    uni_port.add_logical_port(uni_port.port_number, multi_uni)
+
+                self.adapter_agent.update_device(device)
 
                 self._qos_flexibility = config.qos_configuration_flexibility or 0
                 self._omcc_version = config.omcc_version or OMCCVersion.Unknown
@@ -957,7 +980,7 @@
             self.log.info('device-info-not-loaded-skipping-mib-download')
 
 
-    def _add_uni_port(self, entity_id, uni_type=UniType.PPTP):
+    def _add_uni_port(self, entity_id, uni_id, uni_type=UniType.PPTP):
         self.log.debug('function-entry')
 
         device = self.adapter_agent.get_device(self.device_id)
@@ -970,24 +993,21 @@
         # TODO: This knowledge is locked away in openolt.  and it assumes one onu equals one uni...
         parent_device = self.adapter_agent.get_device(device.parent_id)
         parent_adapter = parent_adapter_agent.adapter.devices[parent_device.id]
-        uni_no_start = parent_adapter.platform.mk_uni_port_num(
-            self._onu_indication.intf_id, self._onu_indication.onu_id)
+        uni_no = parent_adapter.platform.mk_uni_port_num(
+            self._onu_indication.intf_id, self._onu_indication.onu_id, uni_id)
 
         # TODO: Some or parts of this likely need to move to UniPort. especially the format stuff
-        working_port = self._next_port_number
-        uni_no = uni_no_start + working_port
         uni_name = "uni-{}".format(uni_no)
 
-        mac_bridge_port_num = working_port + 1
+        mac_bridge_port_num = uni_id + 1 # TODO +1 is only to test non-zero index
 
-        self.log.debug('uni-port-inputs', uni_no=uni_no, uni_name=uni_name, uni_type=uni_type,
+        self.log.debug('uni-port-inputs', uni_no=uni_no, uni_id=uni_id, uni_name=uni_name, uni_type=uni_type,
                        entity_id=entity_id, mac_bridge_port_num=mac_bridge_port_num)
 
-        uni_port = UniPort.create(self, uni_name, uni_no, uni_name, uni_type)
+        uni_port = UniPort.create(self, uni_name, uni_id, uni_no, uni_name, uni_type)
         uni_port.entity_id = entity_id
         uni_port.enabled = True
         uni_port.mac_bridge_port_num = mac_bridge_port_num
-        uni_port.add_logical_port(uni_port.port_number)
 
         self.log.debug("created-uni-port", uni=uni_port)
 
@@ -1000,14 +1020,19 @@
                                                                   uni_ports=self._unis.values())
         # TODO: this should be in the PonPortclass
         pon_port = self._pon.get_port()
-        self.adapter_agent.delete_port_reference_from_parent(self.device_id,
-                                                             pon_port)
 
-        pon_port.peers.extend([Port.PeerPort(device_id=device.parent_id,
-                                             port_no=uni_port.port_number)])
+        # Delete reference to my own UNI as peer from parent.
+        # TODO why is this here, add_port_reference_to_parent already prunes duplicates
+        me_as_peer = Port.PeerPort(device_id=device.parent_id, port_no=uni_port.port_number)
+        partial_pon_port = Port(port_no=pon_port.port_no, label=pon_port.label,
+                                type=pon_port.type, admin_state=pon_port.admin_state,
+                                oper_status=pon_port.oper_status,
+                                peers=[me_as_peer]) # only list myself as a peer to avoid deleting all other UNIs from parent
+        self.adapter_agent.delete_port_reference_from_parent(self.device_id, partial_pon_port)
+
+        pon_port.peers.extend([me_as_peer])
 
         self._pon._port = pon_port
 
         self.adapter_agent.add_port_reference_to_parent(self.device_id,
-                                                        pon_port)
-        self.adapter_agent.update_device(device)
+                                                        pon_port)
\ No newline at end of file