VOL-671: ADTRAN OLT support for new OLT drivers

Change-Id: I0c1d33f71e0dd3ebff6b467af1ee97db0943e37c
diff --git a/voltha/adapters/adtran_olt/README.md b/voltha/adapters/adtran_olt/README.md
index 1ba04f7..5ec8732 100644
--- a/voltha/adapters/adtran_olt/README.md
+++ b/voltha/adapters/adtran_olt/README.md
@@ -14,7 +14,6 @@
 |  -T   | --rc_port        | 8081    | REST TCP Port |
 |  -z   | --zmq_port       | 5656    | ZeroMQ OMCI Proxy Port |
 |  -M   | --multicast_vlan | 4000    | Multicast VLANs (comma-delimeted) |
-|  -V   | --packet_in_vlan | 4000    | OpenFlow Packet-In/Out VLAN, Zero to disable |
 |  -v   | --untagged_vlan  | 4092    | VLAN wrapper for untagged ONU frames |
 
 For example, if your Adtran OLT is address 10.17.174.193 with the default TCP ports and
diff --git a/voltha/adapters/adtran_olt/adtran_device_handler.py b/voltha/adapters/adtran_olt/adtran_device_handler.py
index f015856..6f28504 100644
--- a/voltha/adapters/adtran_olt/adtran_device_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_device_handler.py
@@ -60,7 +60,7 @@
 _DEFAULT_NETCONF_PASSWORD = ""
 _DEFAULT_NETCONF_PORT = 830
 
-FIXED_ONU = True  # Enhanced ONU support
+FIXED_ONU = False  # TODO: Deprecate this.  Enhanced ONU support
 
 
 class AdtranDeviceHandler(object):
@@ -267,7 +267,7 @@
         parser.add_argument('--utility_vlan', '-B', action='store',
                             default='{}'.format(DEFAULT_UTILITY_VLAN),
                             help='VLAN for Untagged Frames from ONUs'),
-        parser.add_argument('--no_exception_gems', '-X', action='store_true', default=not FIXED_ONU,
+        parser.add_argument('--no_exception_gems', '-X', action='store_true', default=True,
                             help='Native OpenFlow Packet-In/Out support')
         try:
             args = parser.parse_args(shlex.split(device.extra_args))
@@ -331,32 +331,37 @@
                 self.parse_provisioning_options(device)
 
                 ############################################################################
-                # Start initial discovery of RESTCONF support (if any)
-
-                try:
-                    self.startup = self.make_restconf_connection()
-                    results = yield self.startup
-                    self._rest_support = results
-                    self.log.debug('HELLO_Contents: {}'.format(pprint.PrettyPrinter().pformat(results)))
-
-                    # See if this is a virtualized OLT. If so, no NETCONF support available
-                    self.is_virtual_olt = 'module-info' in results and\
-                                          any(mod.get('module-name', None) == 'adtran-ont-mock'
-                                              for mod in results['module-info'])
-
-                except Exception as e:
-                    self.log.exception('Initial_RESTCONF_hello_failed', e=e)
-                    self.activate_failed(device, e.message, reachable=False)
+                # Currently, only virtual OLT (pizzabox) is supported
+                # self.is_virtual_olt = Add test for MOCK Device if we want to support it
 
                 ############################################################################
                 # Start initial discovery of NETCONF support (if any)
-
                 try:
                     self.startup = self.make_netconf_connection()
                     yield self.startup
 
                 except Exception as e:
-                    self.log.exception('NETCONF_connection_failed', e=e)
+                    self.log.exception('netconf-connection', e=e)
+                    self.activate_failed(device, e.message, reachable=False)
+
+                ############################################################################
+                # Update access information on network device for full protocol support
+                try:
+                    self.startup = self.ready_network_access()
+                    yield self.startup
+
+                except Exception as e:
+                    self.log.exception('network-setup', e=e)
+                    self.activate_failed(device, e.message, reachable=False)
+
+                ############################################################################
+                # Restconf setup
+                try:
+                    self.startup = self.make_restconf_connection()
+                    yield self.startup
+
+                except Exception as e:
+                    self.log.exception('restconf-setup', e=e)
                     self.activate_failed(device, e.message, reachable=False)
 
                 ############################################################################
@@ -382,7 +387,7 @@
                         self.adapter_agent.update_device(device)
 
                     except Exception as e:
-                        self.log.exception('Device_info_failed', e=e)
+                        self.log.exception('device-info', e=e)
                         self.activate_failed(device, e.message, reachable=False)
 
                 try:
@@ -404,7 +409,7 @@
                             self.adapter_agent.add_port(device.id, port.get_port())
 
                 except Exception as e:
-                    self.log.exception('NNI_enumeration', e=e)
+                    self.log.exception('NNI-enumeration', e=e)
                     self.activate_failed(device, e.message)
 
                 try:
@@ -517,7 +522,6 @@
                 reactor.callLater(10, self.start_kpi_collection, device.id)
 
                 # Signal completion
-
                 self.log.info('activated')
 
             except Exception as e:
@@ -525,11 +529,17 @@
                 if done_deferred is not None:
                     done_deferred.errback(e)
                 raise
+
         if done_deferred is not None:
             done_deferred.callback('activated')
 
         returnValue(done_deferred)
 
+    @inlineCallbacks
+    def ready_network_access(self):
+        # Override in device specific class if needed
+        returnValue('nop')
+
     def activate_failed(self, device, reason, reachable=True):
         """
         Activation process (adopt_device) has failed.
diff --git a/voltha/adapters/adtran_olt/adtran_olt_handler.py b/voltha/adapters/adtran_olt/adtran_olt_handler.py
index bd81b47..96acf0f 100644
--- a/voltha/adapters/adtran_olt/adtran_olt_handler.py
+++ b/voltha/adapters/adtran_olt/adtran_olt_handler.py
@@ -74,12 +74,12 @@
         self.status_poll_skew = self.status_poll_interval / 10
         self._pon_agent = None
         self._pio_agent = None
-        self._is_async_control = False
         self._ssh_deferred = None
         self._system_id = None
         self._download_protocols = None
         self._download_deferred = None
         self._downloads = {}        # name -> Download obj
+        self._pio_exception_map = []
 
     def __del__(self):
         # OLT Specific things here.
@@ -141,7 +141,8 @@
                 the device type specification returned by device_types().
         """
         from codec.physical_entities_state import PhysicalEntitiesState
-        # TODO: After a CLI 'reboot' command, the device info may get messed up (prints labels and not values)  Enter device and type 'show'
+        # TODO: After a CLI 'reboot' command, the device info may get messed up (prints labels and not values)
+        # #     Enter device and type 'show'
         device = {
             'model': 'n/a',
             'hardware_version': 'unknown',
@@ -388,9 +389,6 @@
 
         :param reconciling: (boolean) True if taking over for another VOLTHA
         """
-        # Make sure configured for ZMQ remote access
-        self._ready_network_access()
-
         # ZeroMQ clients
         self._zmq_startup()
 
@@ -406,7 +404,7 @@
 
     def on_heatbeat_alarm(self, active):
         if not active:
-            self._ready_network_access()
+            self.ready_network_access()
 
     @inlineCallbacks
     def _get_download_protocols(self):
@@ -433,10 +431,8 @@
                 self._download_deferred = reactor.callLater(10, self._get_download_protocols)
 
     @inlineCallbacks
-    def _ready_network_access(self):
+    def ready_network_access(self):
         from net.rcmd import RCmd
-        # Software version
-        self._is_async_control = self._olt_version() >= 2
 
         # Check for port status
         command = 'netstat -pan | grep -i 0.0.0.0:{} |  wc -l'.format(self.pon_agent_port)
@@ -464,25 +460,26 @@
 
             def v2_v3_method():
                 # Old V2 method
-                command = "sed --in-place=voltha-sav 's/^#export ZMQ_LISTEN/export ZMQ_LISTEN/' " \
-                          "/etc/ngpon2_agent/ngpon2_agent_feature_flags; "
+                # For V2 images, want -> export ZMQ_LISTEN_ON_ANY_ADDRESS=1
+                # For V3+ images, want -> export AGENT_LISTEN_ON_ANY_ADDRESS=1
 
                 # V3 unifies listening port, compatible with v2
-                # command = "sed --in-place '/add feature flags/aexport ZMQ_LISTEN_ON_ANY_ADDRESS=1' " \
-                #            "/etc/ngpon2_agent/ngpon2_agent_feature_flags; "
-                # command += "sed --in-place '/^export ZMQ_LISTEN/aAGENT_LISTEN_ON_ANY_ADDRESS=1' " \
-                #            "/etc/ngpon2_agent/ngpon2_agent_feature_flags; "
+                cmd = "sed --in-place '/add feature/aexport ZMQ_LISTEN_ON_ANY_ADDRESS=1' " \
+                      "/etc/ngpon2_agent/ngpon2_agent_feature_flags; "
+                cmd += "sed --in-place '/add feature/aexport AGENT_LISTEN_ON_ANY_ADDRESS=1' " \
+                      "/etc/ngpon2_agent/ngpon2_agent_feature_flags; "
 
-                command += 'ps -ae | grep -i ngpon2_agent; '
-                command += 'service_supervisor stop ngpon2_agent; service_supervisor start ngpon2_agent; '
-                command += 'ps -ae | grep -i ngpon2_agent'
+                # Note: 'ps' commands are to help decorate the logfile with useful info
+                cmd += 'ps -ae | grep -i ngpon2_agent; '
+                cmd += 'service_supervisor stop ngpon2_agent; service_supervisor start ngpon2_agent; '
+                cmd += 'ps -ae | grep -i ngpon2_agent'
 
-                self.log.debug('create-request', command=command)
-                return RCmd(self.ip_address, self.netconf_username, self.netconf_password, command)
+                self.log.debug('create-request', command=cmd)
+                return RCmd(self.ip_address, self.netconf_username, self.netconf_password, cmd)
 
             # Look for version
             next_run = 15
-            version = v2_v3_method if self._olt_version() > 1 else v1_method
+            version = v2_v3_method    # NOTE: Only v2 or later supported.
 
             if version is not None:
                 try:
@@ -496,7 +493,9 @@
             next_run = 0
 
         if next_run > 0:
-            self._ssh_deferred = reactor.callLater(next_run, self._ready_network_access)
+            self._ssh_deferred = reactor.callLater(next_run, self.ready_network_access)
+
+        returnValue('retrying' if next_run > 0 else 'ready')
 
     def _zmq_startup(self):
         # ZeroMQ clients
@@ -535,7 +534,7 @@
     def reenable(self, done_deferred=None):
         super(AdtranOltHandler, self).reenable(done_deferred=done_deferred)
 
-        self._ready_network_access()
+        self.ready_network_access()
         self._zmq_startup()
 
         # Register for adapter messages
@@ -558,7 +557,7 @@
     def _finish_reboot(self, timeout, previous_oper_status, previous_conn_status):
         super(AdtranOltHandler, self)._finish_reboot(timeout, previous_oper_status, previous_conn_status)
 
-        self._ready_network_access()
+        self.ready_network_access()
 
         # Download support
         self._download_deferred = reactor.callLater(0, self._get_download_protocols)
@@ -584,8 +583,8 @@
         if self._pon_agent is not None:
             for packet in packets:
                 try:
-                    pon_id, onu_id, msg_bytes, is_omci = \
-                        self._pon_agent.decode_packet(packet, self._is_async_control)
+                    pon_id, onu_id, msg_bytes, is_omci = self._pon_agent.decode_packet(packet)
+
                     if is_omci:
                         proxy_address = self._pon_onu_id_to_proxy_address(pon_id, onu_id)
 
@@ -632,14 +631,23 @@
                 if url_type == PioClient.UrlType.EVCMAPS_RESPONSE:
                     exception_map = self._pio_agent.decode_query_response_packet(packet)
                     self.log.debug('rx-pio-packet', exception_map=exception_map)
+                    # update latest pio exception map
+                    self._pio_exception_map = exception_map
 
                 elif url_type == PioClient.UrlType.PACKET_IN:
                     try:
                         from scapy.layers.l2 import Ether, Dot1Q
                         ifindex, evc_map, packet = self._pio_agent.decode_packet(packet)
 
-                        # convert ifindex to physical port number (HACK)
-                        port_no = (ifindex - 60000) + 4
+                        # convert ifindex to physical port number
+                        # pon port numbers start at 60001 and end at 600016 (16 pons)
+                        if ifindex > 60000 and ifindex < 60017:
+                            port_no = (ifindex - 60000) + 4
+                        # nni port numbers start at 1401 and end at 1404 (16 nnis)
+                        elif ifindex > 1400 and ifindex < 1405:
+                            port_no = ifindex - 1400
+                        else:
+                            raise ValueError('Unknown physical port. ifindex: {}'.format(ifindex))
 
                         logical_port_no = self._compute_logical_port_no(port_no, evc_map, packet)
 
@@ -674,7 +682,7 @@
 
         if self.pio_port is not None or self.io_port is not None:
             from scapy.layers.l2 import Ether, Dot1Q
-            from scapy.layers.inet import IP, UDP
+            from scapy.layers.inet import UDP
             from common.frameio.frameio import hexify
 
             self.log.debug('sending-packet-out', egress_port=egress_port,
@@ -710,33 +718,43 @@
                 elif pkt.type == 2:
                     exceptiontype = 'igmp'
                 elif pkt.type == FlowEntry.EtherType.IPv4:
-                    ippkt = IP(pkt.payload)
-                    if ippkt.proto == FlowEntry.IpProtocol.UDP:
-                        udppkt = UDP(ippkt.payload)
-                        # packet out from DHCP server is reversed ports
-                        if udppkt.sport == 67 and udppkt.dport == 68:
-                            exceptiontype = 'dhcp'
+                    if UDP in pkt and pkt[UDP].sport == 67 and pkt[UDP].dport == 68:
+                        exceptiontype = 'dhcp'
 
                 if exceptiontype is None:
                     self.log.warn('packet-out-exceptiontype-unknown', eEtherType=pkt.type)
 
-                elif port is not None and ctag is not None and vlan_id is not None and evcmapname is not None:
-                    self.log.debug('sending-pio-packet-out', port=port, ctag=ctag, vlan_id=vlan_id, evcmapname=evcmapname, exceptiontype=exceptiontype)
+                elif port is not None and ctag is not None and vlan_id is not None and \
+                     evcmapname is not None and self.pio_exception_exists(evcmapname, exceptiontype):
+
+                    self.log.debug('sending-pio-packet-out', port=port, ctag=ctag, vlan_id=vlan_id,
+                                   evcmapname=evcmapname, exceptiontype=exceptiontype)
                     out_pkt = (
                         Ether(src=pkt.src, dst=pkt.dst) /
-                        Dot1Q(vlan=port) /
                         Dot1Q(vlan=vlan_id) /
                         Dot1Q(vlan=ctag, type=pkt.type) /
                         pkt.payload
                     )
                     data = self._pio_agent.encode_packet(port, str(out_pkt), evcmapname, exceptiontype)
+                    self.log.debug('pio-packet-out', message=data)
                     try:
                         self._pio_agent.send(data)
 
                     except Exception as e:
                         self.log.exception('pio-send', egress_port=egress_port, e=e)
                 else:
-                    self.log.debug('packet-out-flow-not-found', egress_port=egress_port)
+                    self.log.warn('packet-out-flow-not-found', egress_port=egress_port)
+
+    def pio_exception_exists(self, name, exp):
+        # verify exception is in the OLT's reported exception map for this evcmap name
+        if exp is None:
+            return False
+        entry = next((entry for entry in self._pio_exception_map if entry['evc-map-name'] == name), None)
+        if entry is None:
+            return False
+        if exp not in entry['exception-types']:
+            return False
+        return True
 
     def send_packet_exceptions_request(self):
         if self._pio_agent is not None:
@@ -905,8 +923,7 @@
                 onu = pon.onu(onu_id)
 
                 if onu is not None and onu.enabled:
-                    data = self._pon_agent.encode_omci_packet(msg, pon_id, onu_id,
-                                                              self._is_async_control)
+                    data = self._pon_agent.encode_omci_packet(msg, pon_id, onu_id)
                     try:
                         self._pon_agent.send(data)
 
diff --git a/voltha/adapters/adtran_olt/flow/acl.py b/voltha/adapters/adtran_olt/flow/acl.py
index 6d012f2..170d997 100644
--- a/voltha/adapters/adtran_olt/flow/acl.py
+++ b/voltha/adapters/adtran_olt/flow/acl.py
@@ -20,7 +20,7 @@
 
 log = structlog.get_logger()
 
-_acl_list = {}      # Key -> Name: List of encoded EVCs
+_acl_list = {}      # Key -> device-id -> Name: List of encoded EVCs
 
 ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}'  # format(flow_entry.handler.device_id, flow_entry.flow.id)
 ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*'
@@ -217,8 +217,12 @@
         log.debug('installing-acl', installed=self._installed)
 
         if not self._installed and self._enabled:
-            if self._name in _acl_list:
-                self._status_message = "ACL '{}' already is installed".format(self._name)
+            if self._handler.device_id not in _acl_list:
+                _acl_list[self._handler.device_id] = {}
+
+            acls_installed = _acl_list[self._handler.device_id]
+            if self._name in acls_installed:
+                self._status_message = "ACL '{}' id already installed".format(self._name)
                 raise Exception(self._status_message)
 
             try:
@@ -230,7 +234,7 @@
                 self._status_message = '' if results.ok else results.error
 
                 if self._installed:
-                    _acl_list[self._name] = self
+                    acls_installed[self._name] = self
 
             except Exception as e:
                 log.exception('install-failure', name=self._name, e=e)
@@ -251,7 +255,9 @@
             self._status_message = '' if results.ok else results.error
 
             if not self._installed:
-                _acl_list.pop(self._name)
+                acls_installed = _acl_list.get(self._handler.device_id)
+                if acls_installed is not None and self._name in acls_installed:
+                    del acls_installed[self._name]
 
         returnValue(not self._installed)
 
diff --git a/voltha/adapters/adtran_olt/flow/evc.py b/voltha/adapters/adtran_olt/flow/evc.py
index f6c1fc6..ebd1f7f 100644
--- a/voltha/adapters/adtran_olt/flow/evc.py
+++ b/voltha/adapters/adtran_olt/flow/evc.py
@@ -344,6 +344,7 @@
         """
         log.info('deleting', evc=self, delete_maps=delete_maps)
 
+        assert self._flow, 'Delete EVC must have flow reference'
         try:
             dl = [self.remove()]
             self._valid = False
diff --git a/voltha/adapters/adtran_olt/flow/evc_map.py b/voltha/adapters/adtran_olt/flow/evc_map.py
index 6dfee88..17228d1 100644
--- a/voltha/adapters/adtran_olt/flow/evc_map.py
+++ b/voltha/adapters/adtran_olt/flow/evc_map.py
@@ -231,10 +231,10 @@
         # self._udp_src = None
         return xml
 
-    def _ingress_install_xml(self, onu_s_gem_ids_and_vid):
+    def _ingress_install_xml(self, onu_s_gem_ids_and_vid, acl_list):
         from ..onu import Onu
 
-        if len(self._new_acls):
+        if len(acl_list):
             xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' +\
                    '         xmlns:adtn-evc-map-acl="http://www.adtran.com/ns/yang/adtran-evc-map-access-control-list">'
         else:
@@ -258,12 +258,12 @@
                     xml += '<men-ctag>{}</men-ctag>'.format(vid)  # Added in August 2017 model
                     xml += '</network-ingress-filter>'
 
-                if len(self._new_acls):
+                if len(acl_list):
                     xml += '<adtn-evc-map-acl:access-lists>'
-                    xml += ' <adtn-evc-map-acl:ingress-acl>'
-                    for acl in self._new_acls.itervalues():
+                    for acl in acl_list:
+                        xml += ' <adtn-evc-map-acl:ingress-acl>'
                         xml += acl.evc_map_ingress_xml()
-                    xml += ' </adtn-evc-map-acl:ingress-acl>'
+                        xml += ' </adtn-evc-map-acl:ingress-acl>'
                     xml += '</adtn-evc-map-acl:access-lists>'
                 xml += self._common_install_xml()
                 xml += '</evc-map>'
@@ -277,6 +277,27 @@
         xml += EVCMap._xml_trailer()
         return xml
 
+    def _ingress_remove_acl_xml(self, onu_s_gem_ids_and_vid, acl):
+        xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' +\
+               ' xmlns:adtn-evc-map-acl="http://www.adtran.com/ns/yang/adtran-evc-map-access-control-list">'
+        for onu_or_vlan_id, gem_ids_and_vid in onu_s_gem_ids_and_vid.iteritems():
+            first_gem_id = True
+            vid = gem_ids_and_vid[1]
+            ident = '{}.{}'.format(self._pon_id, onu_or_vlan_id) if vid is None \
+                else onu_or_vlan_id
+
+            for gem_id in gem_ids_and_vid[0]:
+                xml += '<evc-map>'
+                xml += '<name>{}.{}.{}</name>'.format(self.name, ident, gem_id)
+                xml += '<adtn-evc-map-acl:access-lists>'
+                xml += ' <adtn-evc-map-acl:ingress-acl xc:operation="delete">'
+                xml += acl.evc_map_ingress_xml()
+                xml += ' </adtn-evc-map-acl:ingress-acl>'
+                xml += '</adtn-evc-map-acl:access-lists>'
+                xml += '</evc-map>'
+        xml += '</evc-maps>'
+        return xml
+
     @inlineCallbacks
     def install(self):
         def gem_ports():
@@ -287,10 +308,10 @@
 
         if self._valid and len(gem_ports()) > 0:
             # Install ACLs first (if not yet installed)
-            acl_list = self._new_acls.values()
+            work_acls = self._new_acls.copy()
             self._new_acls = dict()
 
-            for acl in acl_list:
+            for acl in work_acls.itervalues():
                 try:
                     yield acl.install()
                     # if not results.ok:
@@ -298,14 +319,14 @@
 
                 except Exception as e:
                     log.exception('acl-install', name=self.name, e=e)
-                    self._new_acls.update(acl_list)
+                    self._new_acls.update(work_acls)
                     raise
 
             # Now EVC-MAP
             if not self._installed or self._needs_update:
                 try:
                     self._cancel_deferred()
-                    map_xml = self._ingress_install_xml(self._gem_ids_and_vid) \
+                    map_xml = self._ingress_install_xml(self._gem_ids_and_vid, work_acls.values()) \
                         if self._is_ingress_map else self._egress_install_xml()
 
                     log.debug('install', xml=map_xml, name=self.name)
@@ -316,13 +337,14 @@
                     self.status = '' if results.ok else results.error
 
                     if results.ok:
-                        self._existing_acls.update(acl_list)
+                        self._existing_acls.update(work_acls)
+
                     else:
-                        self._new_acls.update(acl_list)
+                        self._new_acls.update(work_acls)
 
                 except Exception as e:
                     log.exception('map-install', name=self.name, e=e)
-                    self._new_acls.update(acl_list)
+                    self._new_acls.update(work_acls)
                     raise
 
         returnValue(self._installed and self._valid)
@@ -389,6 +411,7 @@
         flows = [flow] if flow is not None else list(self._flows.values())
         removing_all = len(flows) == len(self._flows)
 
+        log.debug('delete', removing_all=removing_all)
         if not removing_all:
             for f in flows:
                 self._remove_flow(f)
@@ -477,6 +500,7 @@
         if tmp_map is None or not tmp_map.valid:
             return None
 
+        self._flows[flow.flow_id] = flow
         self._needs_update = True
 
         if len(tmp_map._new_acls) > 0:
@@ -500,26 +524,45 @@
         EVC-MAP over to another EVC.
 
         :param flow: (FlowEntry) Flow to remove
-        :param removing_all: (bool) If True, all flows are being removed from EVC-MAP
         """
         try:
-            self._flows.pop(flow.flow_id)
+            del self._flows[flow.flow_id]
 
             if not flow.handler.exception_gems:  # ! FIXED_ONU
                 # Remove any ACLs
 
-                acl = ACL.create(flow)
+                acl_name = ACL.flow_to_name(flow)
+                acl = None
+
+                # if not yet installed just remove it from list
+                if acl_name in self._new_acls:
+                    del self._new_acls[acl_name]
+                else:
+                    acl = self._existing_acls[acl_name]
                 if acl is not None:
                     # Remove ACL from EVC-MAP entry
 
                     try:
-                        # TODO: Create EVC-MAP with proper 'delete-acl-list' request
-                        # TODO: and send it
-                        pass
+                        map_xml = self._ingress_remove_acl_xml(self._gem_ids_and_vid, acl)
+                        log.debug('remove', xml=map_xml, name=acl.name)
+                        results = yield self._handler.netconf_client.edit_config(map_xml)
+                        if results.ok:
+                            del self._existing_acls[acl.name]
 
-                        # TODO: Scan EVC to see if it needs to move back to the Utility
-                        #       or Untagged EVC from a user data EVC
-                        pass
+                        # Scan EVC to see if it needs to move back to the Utility
+                        # or Untagged EVC from a user data EVC
+                        if not self._evc.service_evc and\
+                            len(self._flows) > 0 and\
+                            all(f.is_acl_flow for f in self._flows.itervalues()):
+
+                            self._evc.remove_evc_map(self)
+                            first_flow = self._flows.itervalues().next()
+                            self._evc = first_flow.get_utility_evc(None, True)
+                            self._evc.add_evc_map(self)
+                            log.debug('moved-acl-flows-to-utility-evc', newevcname=self._evc.name)
+
+                            self._needs_update = True
+                            self._evc.schedule_install()
 
                     except Exception as e:
                         log.exception('acl-remove-from-evc', e=e)
diff --git a/voltha/adapters/adtran_olt/flow/flow_entry.py b/voltha/adapters/adtran_olt/flow/flow_entry.py
index 7af92c8..993eb38 100644
--- a/voltha/adapters/adtran_olt/flow/flow_entry.py
+++ b/voltha/adapters/adtran_olt/flow/flow_entry.py
@@ -286,11 +286,7 @@
                 downstream_flow.evc = MCastEVC.create(downstream_flow)
 
             elif downstream_flow.is_acl_flow:
-                if any(flow.eth_type == FlowEntry.EtherType.EAPOL for flow in upstream_flows) and\
-                       downstream_flow.handler.utility_vlan != downstream_flow.handler.untagged_vlan:
-                    downstream_flow.evc = UntaggedEVC.create(downstream_flow)
-                else:
-                    downstream_flow.evc = UtilityEVC.create(downstream_flow)
+                downstream_flow.evc = downstream_flow.get_utility_evc(upstream_flows)
             else:
                 downstream_flow.evc = EVC(downstream_flow)
 
@@ -332,6 +328,15 @@
 
         return downstream_flow.evc if all_maps_valid else None
 
+    def get_utility_evc(self, upstream_flows=None, use_default_vlan_id=False):
+        assert self.is_acl_flow, 'Utility evcs are for acl flows only'
+        if upstream_flows is not None and\
+            any(flow.eth_type == FlowEntry.EtherType.EAPOL for flow in upstream_flows) and\
+            self.handler.utility_vlan != self.handler.untagged_vlan:
+            return UntaggedEVC.create(self, use_default_vlan_id)
+
+        return UtilityEVC.create(self, use_default_vlan_id)
+
     @property
     def _needs_acl_support(self):    # FIXED_ONU- maybe
         if self.ipv4_dst is not None:  # In case MCAST downstream has ACL on it
@@ -695,20 +700,26 @@
         """
         from ..onu import Onu
 
-        log.debug('get-packetout-info', device_id=device_id, logical_port=logical_port)
         all_flow_entries = _existing_upstream_flow_entries.get(device_id) or {}
         for flow_entry in all_flow_entries.itervalues():
             log.debug('get-packetout-info', flow_entry=flow_entry)
-            if flow_entry.evc_map is not None and flow_entry.evc_map.valid and flow_entry.logical_port == logical_port:
+
+            # match logical port
+            if flow_entry.evc_map is not None and flow_entry.evc_map.valid and \
+               flow_entry.logical_port == logical_port:
                 evc_map = flow_entry.evc_map
                 gem_ids_and_vid = evc_map.gem_ids_and_vid
+
+                # must have valid gem id
                 if len(gem_ids_and_vid) > 0:
                     for onu_id, gem_ids_with_vid in gem_ids_and_vid.iteritems():
-                        log.debug('get-packetout-info', onu_id=onu_id, gem_ids_with_vid=gem_ids_with_vid)
+                        log.debug('get-packetout-info', onu_id=onu_id, 
+                                   gem_ids_with_vid=gem_ids_with_vid)
                         if len(gem_ids_with_vid) > 0:
                             gem_ids = gem_ids_with_vid[0]
                             ctag = gem_ids_with_vid[1]
                             gem_id = gem_ids[0]     # TODO: always grab fist in list
-                            return flow_entry.in_port, ctag, Onu.gem_id_to_gvid(gem_id), evc_map.get_evcmap_name(onu_id, gem_id)
+                            return flow_entry.in_port, ctag, Onu.gem_id_to_gvid(gem_id), \
+                                   evc_map.get_evcmap_name(onu_id, gem_id)
         return  None, None, None, None
 
diff --git a/voltha/adapters/adtran_olt/flow/untagged_evc.py b/voltha/adapters/adtran_olt/flow/untagged_evc.py
index 84a92c7..3a4a3dc 100644
--- a/voltha/adapters/adtran_olt/flow/untagged_evc.py
+++ b/voltha/adapters/adtran_olt/flow/untagged_evc.py
@@ -43,15 +43,16 @@
     def __str__(self):
         return "VOLTHA-UNTAGGED-{}: MEN: {}, VLAN: {}".format(self._name, self._men_ports, self._s_tag)
 
-    def _create_name(self):
+    def _create_name(self, vlan_id=None):
         #
         # TODO: Take into account selection criteria and output to make the name
         #
-        return EVC_NAME_FORMAT.format(self._flow.vlan_id)
+        return EVC_NAME_FORMAT.format(self._flow.vlan_id if vlan_id is None else vlan_id)
 
     @staticmethod
-    def create(flow_entry):
+    def create(flow_entry, use_default_vlan_id=False):
         device_id = flow_entry.device_id
+        vlan_id = flow_entry.vlan_id if use_default_vlan_id else flow_entry.handler.untagged_vlan
         evc_table = _untagged_evcs.get(device_id)
 
         if evc_table is None:
@@ -64,6 +65,12 @@
             if evc is None:
                 # Create EVC and initial EVC Map
                 evc = UntaggedEVC(flow_entry)
+
+                # reapply the stag and name if forced vlan id
+                if use_default_vlan_id:
+                    evc._s_tag = vlan_id
+                    evc._create_name(vlan_id)
+
                 evc_table[flow_entry.vlan_id] = evc
             else:
                 if flow_entry.flow_id in evc.downstream_flows:    # TODO: Debug only to see if flow_ids are unique
@@ -100,7 +107,7 @@
         """
         log.info('removing', evc=self, remove_maps=remove_maps)
 
-        device_id = self._handler.device_id
+        device_id = self._flow.handler.device_id
         flow_id = self._flow.id
         evc_table = _untagged_evcs.get(device_id)
 
@@ -124,6 +131,7 @@
         """
         log.info('deleting', evc=self, delete_maps=delete_maps)
 
+        assert self._flow, 'Delete EVC must have flow reference'
         try:
             dl = [self.remove()]
             if delete_maps:
diff --git a/voltha/adapters/adtran_olt/flow/utility_evc.py b/voltha/adapters/adtran_olt/flow/utility_evc.py
index cb8a4ae..60a463a 100644
--- a/voltha/adapters/adtran_olt/flow/utility_evc.py
+++ b/voltha/adapters/adtran_olt/flow/utility_evc.py
@@ -25,7 +25,7 @@
 
 _utility_evcs = {}  # device-id -> flow dictionary
                     #                  |
-                    #                  +-> untagged-vlan-id -> evcs
+                    #                  +-> utility-vlan-id -> evcs
 
 
 class UtilityEVC(EVC):
@@ -40,15 +40,16 @@
     def __str__(self):
         return "VOLTHA-UTILITY-{}: MEN: {}, VLAN: {}".format(self._name, self._men_ports, self._s_tag)
 
-    def _create_name(self):
+    def _create_name(self, vlan_id=None):
         #
         # TODO: Take into account selection criteria and output to make the name
         #
-        return EVC_NAME_FORMAT.format(self._flow.vlan_id)
+        return EVC_NAME_FORMAT.format(self._flow.vlan_id if vlan_id is None else vlan_id)
 
     @staticmethod
-    def create(flow_entry):
+    def create(flow_entry, use_default_vlan_id=False):
         device_id = flow_entry.device_id
+        vlan_id = flow_entry.vlan_id if not use_default_vlan_id else flow_entry.handler.utility_vlan
         evc_table = _utility_evcs.get(device_id)
 
         if evc_table is None:
@@ -56,12 +57,18 @@
             evc_table = _utility_evcs[device_id]
 
         try:
-            evc = evc_table.get(flow_entry.vlan_id)
+            evc = evc_table.get(vlan_id)
 
             if evc is None:
                 # Create EVC and initial EVC Map
                 evc = UtilityEVC(flow_entry)
-                evc_table[flow_entry.vlan_id] = evc
+
+                # reapply the stag and name if forced vlan id
+                if use_default_vlan_id:
+                    evc._s_tag = vlan_id
+                    evc._name = evc._create_name(vlan_id)
+
+                evc_table[vlan_id] = evc
             else:
                 if flow_entry.flow_id in evc.downstream_flows:    # TODO: Debug only to see if flow_ids are unique
                     pass
@@ -71,7 +78,7 @@
             return evc
 
         except Exception as e:
-            log.exception('untagged-create', e=e)
+            log.exception('utility-create', e=e)
             return None
 
     @property
@@ -97,7 +104,7 @@
         """
         log.info('removing', evc=self, remove_maps=remove_maps)
 
-        device_id = self._handler.device_id
+        device_id = self._flow.handler.device_id
         flow_id = self._flow.id
         evc_table = _utility_evcs.get(device_id)
 
@@ -121,6 +128,7 @@
         """
         log.info('deleting', evc=self, delete_maps=delete_maps)
 
+        assert self._flow, 'Delete EVC must have flow reference'
         try:
             dl = [self.remove()]
             if delete_maps:
diff --git a/voltha/adapters/adtran_olt/net/pon_zmq.py b/voltha/adapters/adtran_olt/net/pon_zmq.py
index 3cea8e2..9afbe8c 100644
--- a/voltha/adapters/adtran_olt/net/pon_zmq.py
+++ b/voltha/adapters/adtran_olt/net/pon_zmq.py
@@ -27,25 +27,7 @@
     def __init__(self, ip_address, rx_callback, port):
         super(PonClient, self).__init__(ip_address, rx_callback, port)
 
-    def encode_omci_packet(self, msg, pon_index, onu_id, is_async_control):
-        """
-        Create an OMCI Tx Packet for the specified ONU
-
-        :param msg: (str) OMCI message to send
-        :param pon_index: (unsigned int) PON Port index
-        :param onu_id: (unsigned int) ONU ID
-        :param is_async_control: (bool) Newer async/JSON support
-
-        :return: (bytes) octet string to send
-        """
-        assert msg, 'No message provided'
-
-        return PonClient._encode_omci_message_json(msg, pon_index, onu_id) \
-            if is_async_control else \
-            PonClient._encode_omci_message_legacy(msg, pon_index, onu_id)
-
-    @staticmethod
-    def _encode_omci_message_legacy(msg, pon_index, onu_id):
+    def encode_omci_packet(self, msg, pon_index, onu_id):
         """
         Create an OMCI Tx Packet for the specified ONU
 
@@ -55,29 +37,6 @@
 
         :return: (bytes) octet string to send
         """
-        s = struct.Struct('!II')
-
-        # Check if length is prepended (32-bits = 4 bytes ASCII)
-        msglen = len(msg)
-        assert msglen == 40*2 or msglen == 44*2, 'Invalid OMCI message length'
-
-        if len(msg) > 40*2:
-            msg = msg[:40*2]
-
-        return s.pack(pon_index, onu_id) + binascii.unhexlify(msg)
-
-    @staticmethod
-    def _encode_omci_message_json(msg, pon_index, onu_id):
-        """
-        Create an OMCI Tx Packet for the specified ONU
-
-        :param msg: (str) OMCI message to send
-        :param pon_index: (unsigned int) PON Port index
-        :param onu_id: (unsigned int) ONU ID
-
-        :return: (bytes) octet string to send
-        """
-
         return json.dumps({"operation": "NOTIFY",
                            "url": "adtran-olt-pon-control/omci-message",
                            "pon-id": pon_index,
@@ -85,39 +44,14 @@
                            "message-contents": msg.decode("hex").encode("base64")
                            })
 
-    def decode_packet(self, packet, is_async_control):
+    def decode_packet(self, packet):
         """
         Decode the PON-Agent packet provided by the ZMQ client
 
         :param packet: (bytes) Packet
-        :param is_async_control: (bool) Newer async/JSON support
         :return: (long, long, bytes, boolean) PON Index, ONU ID, Frame Contents (OMCI or Ethernet),\
                                               and a flag indicating if it is OMCI
         """
-        return PonClient._decode_omci_message_json(packet) if is_async_control \
-            else PonClient._decode_omci_message_legacy(packet)
-
-    @staticmethod
-    def _decode_omci_message_legacy(packet):
-        """
-        Decode the packet provided by the ZMQ client (binary legacy format)
-
-        :param packet: (bytes) Packet
-        :return: (long, long, bytes) PON Index, ONU ID, OMCI Frame Contents
-        """
-        (pon_index, onu_id) = struct.unpack_from('!II', packet)
-        omci_msg = packet[8:]
-
-        return pon_index, onu_id, omci_msg, True
-
-    @staticmethod
-    def _decode_omci_message_json(packet):
-        """
-        Decode the packet provided by the ZMQ client (JSON format)
-
-        :param packet: (string) Packet
-        :return: (long, long, bytes) PON Index, ONU ID, OMCI Frame Contents
-        """
         msg = json.loads(packet)
         pon_id = msg['pon-id']
         onu_id = msg['onu-id']