VOL-1397: Adtran-OLT - Initial containerization commit
 - Need to move VERSION to base directory

Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
diff --git a/adapters/adtran_common/flow/__init__.py b/adapters/adtran_common/flow/__init__.py
new file mode 100644
index 0000000..b0fb0b2
--- /dev/null
+++ b/adapters/adtran_common/flow/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# 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.
diff --git a/adapters/adtran_common/flow/acl.py b/adapters/adtran_common/flow/acl.py
new file mode 100644
index 0000000..67f8c08
--- /dev/null
+++ b/adapters/adtran_common/flow/acl.py
@@ -0,0 +1,385 @@
+#
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+#
+import xmltodict
+import re
+import structlog
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+log = structlog.get_logger()
+
+_acl_list = {}      # Key -> device-id -> Name: List of encoded EVCs
+
+ACL_NAME_FORMAT = 'VOLTHA-ACL-{}-{}'  # format(flow_entry.flow_id, flow-entry-hash)
+ACL_NAME_REGEX_ALL = 'VOLTHA-ACL-*'
+ACE_NAME_FORMAT = 'VOLTHA-ACE-{}'  # format(flow_entry.flow_id)
+
+
+class ACL(object):
+    """
+    Class to wrap Trap-to-Controller functionality
+    """
+    def __init__(self, flow_entry):
+        self._installed = False
+        self._status_message = None
+        self._parent = flow_entry           # FlowEntry parent
+        self._flow = flow_entry.flow
+        self._handler = flow_entry.handler
+        self._name = ACL.flow_to_name(flow_entry)
+        self._rule_name = ACL.flow_to_ace_name(flow_entry)
+        self._eth_type = flow_entry.eth_type
+        self._ip_protocol = flow_entry.ip_protocol
+        self._ipv4_dst = flow_entry.ipv4_dst
+        self._src_port = flow_entry.udp_src
+        self._dst_port = flow_entry.udp_dst
+        self._exception = False
+        self._enabled = True
+        self._valid = self._decode()
+
+    def __str__(self):
+        return 'ACL: {}, Installed: {}, L2: {}, L3/4: {}'.\
+            format(self.name, self._installed, self.is_l2_exception,
+                   self.is_l3_l4_exception)
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def installed(self):
+        return self._installed
+
+    @property
+    def is_l2_exception(self):
+        from flow_entry import FlowEntry
+        return self._eth_type not in (None,
+                                      FlowEntry.EtherType.IPv4,
+                                      FlowEntry.EtherType.IPv6)
+
+    @property
+    def is_l3_l4_exception(self):
+        return not self.is_l2_exception and self._ip_protocol is not None
+
+    @staticmethod
+    def _xml_header(operation=None):
+        return '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list"\
+                xmlns:adtn-ietf-ns-acl="http://www.adtran.com/ns/yang/adtran-ietf-ns-access-control-list"><acl{}>'.\
+            format('' if operation is None else ' xc:operation="{}"'.format(operation))
+
+    @staticmethod
+    def _xml_trailer():
+        return '</acl></access-lists>'
+
+    def _xml_action(self):
+        xml = '<actions>'
+        if self._exception:
+            xml += '<adtn-ietf-ns-acl:exception-to-cpu/>'
+        else:
+            xml += '<permit/>'
+        xml += '</actions>'
+        return xml
+
+    def _ace_l2(self):
+        xml = '<ace>'
+        xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
+        xml += '<matches><l2-acl><ether-type>{:04x}</ether-type></l2-acl></matches>'.format(self._eth_type)
+        xml += self._xml_action()
+        xml += '</ace>'
+        return xml
+
+    def _ace_l2_l3_ipv4(self):
+        xml = '<ace>'
+        xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
+        xml += '<matches><l2-l3-ipv4-acl>'
+        xml += '<ether-type>{:04X}</ether-type>'.format(self._eth_type)
+
+        if self._ip_protocol is not None:
+            xml += '<protocol>{}</protocol>'.format(self._ip_protocol)
+        if self._ipv4_dst is not None:
+            xml += '<destination-ipv4-network>{}/32</destination-ipv4-network>'.format(self._ipv4_dst)
+        if self._src_port is not None:
+            xml += '<source-port-range><lower-port>{}</lower-port><operation>eq</operation></source-port-range>'.\
+                format(self._src_port)
+        if self._dst_port is not None:
+            xml += '<destination-port-range><lower-port>' + \
+                   '{}</lower-port><operations>eq</operations></destination-port-range>'.format(self._dst_port)
+
+        xml += '</l2-l3-ipv4-acl></matches>'
+        xml += self._xml_action()
+        xml += '</ace>'
+        return xml
+
+    def _ace_any(self):
+        xml = '<ace>'
+        xml += '<rule-name>{}</rule-name>'.format(self._rule_name)
+        xml += '<matches><any-acl/></matches>'
+        xml += self._xml_action()
+        xml += '</ace>'
+        return xml
+
+    def _acl_eth(self):
+        xml = '<acl-type>eth-acl</acl-type>'
+        xml += '<acl-name>{}</acl-name>'.format(self._name)
+        return xml
+
+    def _acl_l4(self):
+        xml = '<acl-type>mixed-l2-l3-ipv4-acl</acl-type>'
+        xml += '<acl-name>{}</acl-name>'.format(self._name)
+        return xml
+
+    def _acl_any(self):
+        xml = '<acl-type>any-acl</acl-type>'
+        xml += '<acl-name>{}</acl-name>'.format(self._name)
+        return xml
+
+    def _install_xml(self):
+        xml = ACL._xml_header('create')
+        if self.is_l2_exception:
+            xml += self._acl_eth()
+            xml += '<aces>{}</aces>'.format(self._ace_l2())
+        elif self.is_l3_l4_exception:
+            xml += self._acl_l4()
+            xml += '<aces>{}</aces>'.format(self._ace_l2_l3_ipv4())
+        else:
+            xml += self._acl_any()
+            xml += '<aces>{}</aces>'.format(self._ace_any())
+
+        xml += ACL._xml_trailer()
+        return xml
+
+    def _remove_xml(self):
+        xml = ACL._xml_header('delete')
+        if self.is_l2_exception:
+            xml += self._acl_eth()
+        elif self.is_l3_l4_exception:
+            xml += self._acl_l4()
+        else:
+            xml += self._acl_any()
+        xml += ACL._xml_trailer()
+        return xml
+
+    def evc_map_ingress_xml(self):
+        """ Individual ACL specific XML for the EVC MAP """
+
+        xml = '<adtn-evc-map-acl:acl-type '
+        fmt = 'xmlns:adtn-ietf-acl="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">adtn-ietf-acl:{}'\
+              '</adtn-evc-map-acl:acl-type>'
+
+        if self.is_l2_exception:
+            xml += fmt.format('eth-acl')
+
+        elif self.is_l3_l4_exception:
+            xml += fmt.format('mixed-l2-l3-ipv4-acl')
+
+        else:
+            xml += fmt.format('any-acl')
+
+        xml += '<adtn-evc-map-acl:acl-name>{}</adtn-evc-map-acl:acl-name>'.format(self.name)
+        return xml
+
+    @staticmethod
+    def create(flow_entry):
+        acl = ACL(flow_entry)
+
+        # Already created and installed, return that one
+        acls_installed = _acl_list.get(flow_entry.handler.device_id)
+        if acls_installed is not None:
+            entry = acls_installed.get(acl._name)
+            if entry is not None:
+                return entry
+
+        return acl
+
+    @staticmethod
+    def flow_to_name(flow_entry):
+        return ACL_NAME_FORMAT.format(flow_entry.flow_id, ACL.acl_hash(flow_entry))
+
+    @staticmethod
+    def flow_to_ace_name(flow_entry):
+        return ACE_NAME_FORMAT.format(flow_entry.flow_id)
+
+    @staticmethod
+    def acl_hash(flow_entry):
+        from hashlib import md5
+        in_port = flow_entry.in_port or 0
+        eth_type = flow_entry.eth_type or 0
+        ip_protocol = flow_entry.ip_protocol or 0
+        ipv4_dst = flow_entry.ipv4_dst or 0
+        src_port = flow_entry.udp_src or 0
+        dst_port = flow_entry.udp_dst or 0
+        hex_string = md5('{},{},{},{},{},{}'.format(in_port, eth_type, ip_protocol,
+                                                    ipv4_dst, src_port, dst_port)).hexdigest()
+        return hex_string
+
+    @property
+    def valid(self):
+        return self._valid
+
+    @property
+    def installed(self):
+        return self._installed
+
+    @property
+    def status(self):
+        return self._status_message
+
+    @inlineCallbacks
+    def install(self):
+        log.debug('installing-acl', installed=self._installed)
+
+        if not self._installed and self._enabled:
+            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:
+                # Return OK
+                returnValue(self._enabled)
+
+            try:
+                acl_xml = self._install_xml()
+                log.debug('install-xml', xml=acl_xml, name=self._name)
+
+                results = yield self._handler.netconf_client.edit_config(acl_xml)
+                self._installed = results.ok
+                self._status_message = '' if results.ok else results.error
+
+                if self._installed:
+                    acls_installed[self._name] = self
+
+            except Exception as e:
+                log.exception('install-failure', name=self._name, e=e)
+                raise
+
+        returnValue(self._installed and self._enabled)
+
+    @inlineCallbacks
+    def remove(self):
+        log.debug('removing-acl', installed=self._installed)
+
+        if self._installed:
+            acl_xml = self._remove_xml()
+            log.info('remove-xml', xml=acl_xml, name=self._name)
+
+            results = yield self._handler.netconf_client.edit_config(acl_xml)
+            self._installed = not results.ok
+            self._status_message = '' if results.ok else results.error
+
+            if not self._installed:
+                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)
+
+    def enable(self):
+        if not self._enabled:
+            self._enabled = False
+            raise NotImplemented("TODO: Implement this")
+
+    def disable(self):
+        if self._enabled:
+            self._enabled = True
+            raise NotImplemented("TODO: Implement this")
+
+    def _decode(self):
+        """
+        Examine the field settings and set ACL up for requested fields
+        """
+        # If EtherType is not None and not IP, this is an L2 exception
+        self._exception = self.is_l2_exception or self.is_l3_l4_exception
+        return True
+
+    # BULK operations
+
+    @staticmethod
+    def enable_all():
+        raise NotImplemented("TODO: Implement this")
+
+    @staticmethod
+    def disable_all():
+        raise NotImplemented("TODO: Implement this")
+
+    @staticmethod
+    def clear_all(device_id):
+        """
+        Clear all acls for this device id from the list
+        :param device_id: id of the device
+        """
+        if device_id in _acl_list:
+            del _acl_list[device_id]
+
+    @staticmethod
+    def remove_all(client, regex_=ACL_NAME_REGEX_ALL):
+        """
+        Remove all matching ACLs from hardware
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        # Do a 'get' on the evc config an you should get the names
+        get_xml = """
+        <filter>
+          <access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">
+            <acl><acl-type/><acl-name/></acl>
+          </access-lists>
+        </filter>
+        """
+        log.debug('query', xml=get_xml, regex=regex_)
+
+        def request_failed(results, operation):
+            log.error('{}-failed'.format(operation), results=results)
+
+        def delete_complete(results):
+            log.debug('delete-complete', results=results)
+
+        def do_delete(rpc_reply, regexpr):
+            log.debug('query-complete', rpc_reply=rpc_reply)
+
+            if rpc_reply.ok:
+                result_dict = xmltodict.parse(rpc_reply.data_xml)
+                entries = result_dict['data']['access-lists'] if 'access-lists' in result_dict['data'] else {}
+
+                if 'acl' in entries:
+                    p = re.compile(regexpr)
+
+                    pairs = []
+                    if isinstance(entries['acl'], list):
+                        pairs = {(entry['acl-type'], entry['acl-name']) for entry in entries['acl']
+                                 if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name'])}
+                    else:
+                        if 'acl' in entries:
+                            entry = entries['acl']
+                            if 'acl-name' in entry and 'acl-type' in entry and p.match(entry['acl-name']):
+                                pairs = [(entry['acl-type'], entry['acl-name'])]
+
+                    if len(pairs) > 0:
+                        del_xml = '<access-lists xmlns="http://www.adtran.com/ns/yang/adtran-ietf-access-control-list">'
+                        for pair in pairs:
+                            del_xml += '<acl xc:operation = "delete">'
+                            del_xml += '<acl-type>{}</acl-type>'.format(pair[0])
+                            del_xml += '<acl-name>{}</acl-name>'.format(pair[1])
+                            del_xml += '</acl>'
+                        del_xml += '</access-lists>'
+                        log.debug('removing', xml=del_xml)
+
+                        return client.edit_config(del_xml)
+
+            return succeed('no entries')
+
+        d = client.get(get_xml)
+        d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
+        d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
+        return d
diff --git a/adapters/adtran_common/flow/evc.py b/adapters/adtran_common/flow/evc.py
new file mode 100644
index 0000000..5e00bca
--- /dev/null
+++ b/adapters/adtran_common/flow/evc.py
@@ -0,0 +1,479 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+import xmltodict
+import re
+import structlog
+from enum import IntEnum
+from twisted.internet import reactor, defer
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+log = structlog.get_logger()
+
+EVC_NAME_FORMAT = 'VOLTHA-{}'                       # format(flow.id)
+EVC_NAME_REGEX_ALL = EVC_NAME_FORMAT.format('*')
+DEFAULT_STPID = 0x8100
+
+
+class EVC(object):
+    """
+    Class to wrap EVC functionality
+    """
+    class SwitchingMethod(IntEnum):
+        SINGLE_TAGGED = 1
+        DOUBLE_TAGGED = 2
+        MAC_SWITCHED = 3
+        DOUBLE_TAGGED_MAC_SWITCHED = 4
+        DEFAULT = SINGLE_TAGGED
+
+        @staticmethod
+        def xml(value):
+            if value is None:
+                value = EVC.SwitchingMethod.DEFAULT
+            if value == EVC.SwitchingMethod.SINGLE_TAGGED:
+                return '<single-tag-switched/>'
+            elif value == EVC.SwitchingMethod.DOUBLE_TAGGED:
+                return '<double-tag-switched/>'
+            elif value == EVC.SwitchingMethod.MAC_SWITCHED:
+                return '<mac-switched/>'
+            elif value == EVC.SwitchingMethod.DOUBLE_TAGGED_MAC_SWITCHED:
+                return '<double-tag-mac-switched/>'
+            raise ValueError('Invalid SwitchingMethod enumeration')
+
+    class Men2UniManipulation(IntEnum):
+        SYMMETRIC = 1
+        POP_OUT_TAG_ONLY = 2
+        DEFAULT = SYMMETRIC
+
+        @staticmethod
+        def xml(value):
+            if value is None:
+                value = EVC.Men2UniManipulation.DEFAULT
+            fmt = '<men-to-uni-tag-manipulation>{}</men-to-uni-tag-manipulation>'
+            if value == EVC.Men2UniManipulation.SYMMETRIC:
+                return fmt.format('<symmetric/>')
+            elif value == EVC.Men2UniManipulation.POP_OUT_TAG_ONLY:
+                return fmt.format('<pop-outer-tag-only/>')
+            raise ValueError('Invalid Men2UniManipulation enumeration')
+
+    class ElineFlowType(IntEnum):
+        NNI_TO_UNI = 1
+        UNI_TO_NNI = 2
+        NNI_TO_NNI = 3
+        UNI_TO_UNI = 4
+        ACL_FILTER = 5
+        UNKNOWN = 6
+        UNSUPPORTED = 7    # Or Invalid
+
+    def __init__(self, flow_entry):
+        self._installed = False
+        self._status_message = None
+        self._flow = flow_entry
+        self._name = self._create_name()
+        self._deferred = None
+        self._evc_maps = {}               # Map Name -> evc-map
+
+        self._flow_type = EVC.ElineFlowType.UNKNOWN
+
+        # EVC related properties
+        self._enabled = True
+        self._men_ports = []
+        self._s_tag = None
+        self._stpid = None
+        self._switching_method = None
+        self.service_evc = False
+
+        self._ce_vlan_preservation = None
+        self._men_to_uni_tag_manipulation = None
+
+        try:
+            self._valid = self._decode()
+
+        except Exception as e:
+            log.exception('Failure during EVC decode', e=e)
+            self._valid = False
+
+    def __str__(self):
+        return "EVC-{}: MEN: {}, S-Tag: {}".format(self._name, self._men_ports, self._s_tag)
+
+    def _create_name(self):
+        #
+        # TODO: Take into account selection criteria and output to make the name
+        #
+        return EVC_NAME_FORMAT.format(self._flow.flow_id)
+
+    def _cancel_deferred(self):
+        d, self._deferred = self._deferred, None
+
+        try:
+            if d is not None and not d.called:
+                d.cancel()
+
+        except Exception as e:
+            pass
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def valid(self):
+        return self._valid
+
+    @property
+    def installed(self):
+        return self._installed
+
+    @installed.setter
+    def installed(self, value):
+        assert not value, 'EVC Install can only be reset'
+        self._installed = False
+
+    @property
+    def status(self):
+        return self._status_message
+
+    @status.setter
+    def status(self, value):
+        self._status_message = value
+
+    @property
+    def s_tag(self):
+        return self._s_tag
+
+    @property
+    def stpid(self):
+        return self._stpid
+
+    @stpid.setter
+    def stpid(self, value):
+        assert self._stpid is None or self._stpid == value, 'STPID can only be set once'
+        self._stpid = value
+
+    @property
+    def switching_method(self):
+        return self._switching_method
+
+    @switching_method.setter
+    def switching_method(self, value):
+        assert self._switching_method is None or self._switching_method == value,\
+            'Switching Method can only be set once. EVC: {}'.format(self.name)
+        self._switching_method = value
+
+    @property
+    def ce_vlan_preservation(self):
+        return self._ce_vlan_preservation
+
+    @ce_vlan_preservation.setter
+    def ce_vlan_preservation(self, value):
+        assert self._ce_vlan_preservation is None or self._ce_vlan_preservation == value,\
+            'CE VLAN Preservation can only be set once'
+        self._ce_vlan_preservation = value
+
+    @property
+    def men_to_uni_tag_manipulation(self):
+        return self._men_to_uni_tag_manipulation
+
+    @men_to_uni_tag_manipulation.setter
+    def men_to_uni_tag_manipulation(self, value):
+        assert self._men_to_uni_tag_manipulation is None or self._men_to_uni_tag_manipulation == value, \
+            'MEN-to-UNI tag manipulation can only be set once'
+        self._men_to_uni_tag_manipulation = value
+
+    @property
+    def flow_entry(self):
+        # Note that the first flow used to create the EVC is saved and it may
+        # eventually get deleted while others still use the EVC.  This should
+        # be okay as the downstream flow/signature table is used to maintain
+        # the lifetime on this EVC object.
+        return self._flow
+
+    @flow_entry.setter
+    def flow_entry(self, value):
+        self._flow = value
+
+    @property
+    def evc_maps(self):
+        """
+        Get all EVC Maps that reference this EVC
+        :return: list of EVCMap
+        """
+        return list(self._evc_maps.values()) if self._evc_maps is not None else []
+
+    @property
+    def evc_map_names(self):
+        """
+        Get all EVC Map names that reference this EVC
+        :return: list of EVCMap names
+        """
+        return list(self._evc_maps.keys()) if self._evc_maps is not None else []
+
+    def add_evc_map(self, evc_map):
+        if self._evc_maps is None:
+            self._evc_maps = dict()
+
+        if evc_map.name not in self._evc_maps:
+            self._evc_maps[evc_map.name] = evc_map
+
+    def remove_evc_map(self, evc_map):
+        if self._evc_maps is not None and evc_map.name in self._evc_maps:
+            del self._evc_maps[evc_map.name]
+
+    def schedule_install(self, delay=0):
+        """
+        Try to install EVC and all MAPs in a single operational sequence.
+        The delay parameter is used during recovery to allow multiple associated
+        EVC maps to be updated/modified independently before the parent EVC
+        is installed.
+
+        :param delay: (int) Seconds to delay before install
+        """
+        self._cancel_deferred()
+
+        self._deferred = reactor.callLater(delay, self._do_install) \
+            if self._valid else succeed('Not VALID')
+
+        return self._deferred
+
+    @staticmethod
+    def _xml_header(operation=None):
+        return '<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs"{}><evc>'.\
+            format('' if operation is None else ' xc:operation="{}"'.format(operation))
+
+    @staticmethod
+    def _xml_trailer():
+        return '</evc></evcs>'
+
+    @inlineCallbacks
+    def _do_install(self):
+        # Install the EVC if needed
+        log.debug('do-install', valid=self._valid, installed=self._installed)
+
+        if self._valid and not self._installed:
+            # TODO: Currently install EVC and then MAPs. Can do it all in a single edit-config operation
+
+            xml = EVC._xml_header()
+            xml += '<name>{}</name>'.format(self.name)
+            xml += '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
+
+            if self._ce_vlan_preservation is not None:
+                xml += '<ce-vlan-preservation>{}</ce-vlan-preservation>'.format('false')
+
+            if self._s_tag is not None:
+                xml += '<stag>{}</stag>'.format(self._s_tag)
+                xml += '<stag-tpid>{}</stag-tpid>'.format(self._stpid or DEFAULT_STPID)
+            else:
+                xml += 'no-stag/'
+
+            for port in self._men_ports:
+                xml += '<men-ports>{}</men-ports>'.format(port)
+
+            # xml += EVC.Men2UniManipulation.xml(self._men_to_uni_tag_manipulation)
+            # xml += EVC.SwitchingMethod.xml(self._switching_method)
+            xml += EVC._xml_trailer()
+
+            log.debug('create-evc', name=self.name, xml=xml)
+            try:
+                # Set installed to true while request is in progress
+                self._installed = True
+                results = yield self._flow.handler.netconf_client.edit_config(xml)
+                self._installed = results.ok
+                self.status = '' if results.ok else results.error
+
+            except Exception as e:
+                log.exception('install-failed', name=self.name, e=e)
+                raise
+
+        # Install any associated EVC Maps
+
+        if self._installed:
+            for evc_map in self.evc_maps:
+                try:
+                    yield evc_map.install()
+
+                except Exception as e:
+                    evc_map.status = 'Exception during EVC-MAP Install: {}'.format(e.message)
+                    log.exception('evc-map-install-failed', e=e)
+
+        returnValue(self._installed and self._valid)
+
+    def remove(self, remove_maps=True):
+        """
+        Remove EVC (and optional associated EVC-MAPs) from hardware
+        :param remove_maps: (boolean)
+        :return: (deferred)
+        """
+        if not self.installed:
+            return succeed('Not installed')
+
+        log.info('removing', evc=self, remove_maps=remove_maps)
+        dl = []
+
+        def _success(rpc_reply):
+            log.debug('remove-success', rpc_reply=rpc_reply)
+            self._installed = False
+
+        def _failure(results):
+            log.error('remove-failed', results=results)
+            self._installed = False
+
+        xml = EVC._xml_header('delete') + '<name>{}</name>'.format(self.name) + EVC._xml_trailer()
+        d = self._flow.handler.netconf_client.edit_config(xml)
+        d.addCallbacks(_success, _failure)
+        dl.append(d)
+
+        if remove_maps:
+            for evc_map in self.evc_maps:
+                dl.append(evc_map.remove())
+
+        return defer.gatherResults(dl, consumeErrors=True)
+
+    @inlineCallbacks
+    def delete(self, delete_maps=True):
+        """
+        Remove from hardware and delete/clean-up EVC Object
+        """
+        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
+
+            if delete_maps:
+                for evc_map in self.evc_maps:
+                    dl.append(evc_map.delete(None))   # TODO: implement bulk-flow procedures
+
+            yield defer.gatherResults(dl, consumeErrors=True)
+
+        except Exception as e:
+            log.exception('removal', e=e)
+
+        self._evc_maps = None
+        f, self._flow = self._flow, None
+        if f is not None and f.handler is not None:
+            f.handler.remove_evc(self)
+
+        returnValue('Done')
+
+    def reflow(self, reflow_maps=True):
+        """
+        Attempt to install/re-install a flow
+        :param reflow_maps: (boolean) Flag indication if EVC-MAPs should be reflowed as well
+        :return: (deferred)
+        """
+        self._installed = False
+
+        if reflow_maps:
+            for evc_map in self.evc_maps:
+                evc_map.installed = False
+
+        return self.schedule_install()
+
+    def _decode(self):
+        """
+        Examine flow rules and extract appropriate settings for this EVC
+        """
+        if self._flow.handler.is_nni_port(self._flow.in_port):
+            self._men_ports.append(self._flow.handler.get_port_name(self._flow.in_port))
+        else:
+            self._status_message = 'EVCs with UNI ports are not supported'
+            return False    # UNI Ports handled in the EVC Maps
+
+        self._s_tag = self._flow.vlan_id
+
+        if self._flow.inner_vid is not None:
+           self._switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED
+
+        # For the Utility VLAN, multiple ingress ACLs (different GEMs) will need to
+        # be trapped on this EVC. Since these are usually untagged, we have to force
+        # the EVC to preserve CE VLAN tags.
+
+        if self._s_tag == self._flow.handler.utility_vlan:
+            self._ce_vlan_preservation = True
+
+        # Note: The following fields may get set when the first EVC-MAP
+        #       is associated with this object. Once set, they cannot be changed to
+        #       another value.
+        #  self._stpid
+        #  self._switching_method
+        #  self._ce_vlan_preservation
+        #  self._men_to_uni_tag_manipulation
+        return True
+
+    # BULK operations
+
+    @staticmethod
+    def remove_all(client, regex_=EVC_NAME_REGEX_ALL):
+        """
+        Remove all matching EVCs from hardware
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        # Do a 'get' on the evc config an you should get the names
+        get_xml = """
+        <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+          <evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs">
+            <evc><name/></evc>
+          </evcs>
+        </filter>
+        """
+        log.debug('query', xml=get_xml, regex=regex_)
+
+        def request_failed(results, operation):
+            log.error('{}-failed'.format(operation), results=results)
+            # No further actions. Periodic poll later on will scrub any old EVCs if needed
+
+        def delete_complete(results):
+            log.debug('delete-complete', results=results)
+
+        def do_delete(rpc_reply, regexpr):
+            log.debug('query-complete', rpc_reply=rpc_reply)
+
+            if rpc_reply.ok:
+                result_dict = xmltodict.parse(rpc_reply.data_xml)
+                entries = result_dict['data']['evcs'] if 'evcs' in result_dict['data'] else {}
+
+                if 'evc' in entries:
+                    p = re.compile(regexpr)
+
+                    if isinstance(entries['evc'], list):
+                        names = {entry['name'] for entry in entries['evc'] if 'name' in entry
+                                 and p.match(entry['name'])}
+                    else:
+                        names = set()
+                        for item in entries['evc'].items():
+                            if isinstance(item, tuple) and item[0] == 'name':
+                                names.add(item[1])
+                                break
+
+                    if len(names) > 0:
+                        del_xml = '<evcs xmlns="http://www.adtran.com/ns/yang/adtran-evcs"' + \
+                                 ' xc:operation = "delete">'
+                        for name in names:
+                            del_xml += '<evc>'
+                            del_xml += '<name>{}</name>'.format(name)
+                            del_xml += '</evc>'
+                        del_xml += '</evcs>'
+                        log.debug('removing', xml=del_xml)
+
+                        return client.edit_config(del_xml)
+
+            return succeed('no entries')
+
+        d = client.get(get_xml)
+        d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
+        d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
+        return d
diff --git a/adapters/adtran_common/flow/evc_map.py b/adapters/adtran_common/flow/evc_map.py
new file mode 100644
index 0000000..688124a
--- /dev/null
+++ b/adapters/adtran_common/flow/evc_map.py
@@ -0,0 +1,1015 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+import xmltodict
+import re
+import structlog
+from enum import Enum
+from acl import ACL
+from twisted.internet import defer, reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from ncclient.operations.rpc import RPCError
+
+
+log = structlog.get_logger()
+
+# NOTE: For the EVC Map name, the ingress-port number is the VOLTHA port number (not pon-id since
+#       it covers NNI ports as well in order to handle the NNI-NNI case.  For flows that
+#       cover an entire pon, the name will have the ONU ID and GEM ID appended to it upon
+#       installation with a period as a separator.
+
+EVC_MAP_NAME_FORMAT = 'VOLTHA-{}-{}'   # format(logical-ingress-port-number, flow-id)
+EVC_MAP_NAME_REGEX_ALL = 'VOLTHA-*'
+
+
+class EVCMap(object):
+    """
+    Class to wrap EVC functionality
+    """
+    class EvcConnection(Enum):
+        NO_EVC_CONNECTION = 0
+        EVC = 1
+        DISCARD = 2
+        DEFAULT = NO_EVC_CONNECTION
+
+        @staticmethod
+        def xml(value):
+            # Note we do not have XML for 'EVC' enumeration.
+            if value is None:
+                value = EVCMap.EvcConnection.DEFAULT
+            if value == EVCMap.EvcConnection.DISCARD:
+                return '<no-evc-connection/>'
+            elif value == EVCMap.EvcConnection.DISCARD:
+                return 'discard/'
+            raise ValueError('Invalid EvcConnection enumeration')
+
+    class PriorityOption(Enum):
+        INHERIT_PRIORITY = 0
+        EXPLICIT_PRIORITY = 1
+        DEFAULT = INHERIT_PRIORITY
+
+        @staticmethod
+        def xml(value):
+            if value is None:
+                value = EVCMap.PriorityOption.DEFAULT
+            if value == EVCMap.PriorityOption.INHERIT_PRIORITY:
+                return '<inherit-pri/>'
+            elif value == EVCMap.PriorityOption.EXPLICIT_PRIORITY:
+                return '<explicit-pri/>'
+            raise ValueError('Invalid PriorityOption enumeration')
+
+    def __init__(self, flow, evc, is_ingress_map):
+        self._handler = flow.handler      # Same for all Flows attached to this EVC MAP
+        self._flows = {flow.flow_id: flow}
+        self._evc = None
+        self._new_acls = dict()           # ACL Name -> ACL Object (To be installed into h/w)
+        self._existing_acls = dict()      # ACL Name -> ACL Object (Already in H/w)
+        self._is_ingress_map = is_ingress_map
+        self._pon_id = None
+        self._onu_id = None               # Remains None if associated with a multicast flow
+        self._installed = False
+        self._needs_update = False
+        self._status_message = None
+        self._deferred = None
+        self._name = None
+        self._enabled = True
+        self._uni_port = None
+        self._evc_connection = EVCMap.EvcConnection.DEFAULT
+        self._men_priority = EVCMap.PriorityOption.DEFAULT
+        self._men_pri = 0  # If Explicit Priority
+
+        self._c_tag = None
+        self._men_ctag_priority = EVCMap.PriorityOption.DEFAULT
+        self._men_ctag_pri = 0  # If Explicit Priority
+        self._match_ce_vlan_id = None
+        self._match_untagged = False
+        self._match_destination_mac_address = None
+        self._match_l2cp = False
+        self._match_broadcast = False
+        self._match_multicast = False
+        self._match_unicast = False
+        self._match_igmp = False
+
+        from common.tech_profile.tech_profile import DEFAULT_TECH_PROFILE_TABLE_ID
+        self._tech_profile_id = DEFAULT_TECH_PROFILE_TABLE_ID
+        self._gem_ids_and_vid = None      # { key -> onu-id, value -> tuple(sorted GEM Port IDs, onu_vid) }
+        self._upstream_bandwidth = None
+        self._shaper_name = None
+
+        # ACL logic
+        self._eth_type = None
+        self._ip_protocol = None
+        self._ipv4_dst = None
+        self._udp_dst = None
+        self._udp_src = None
+
+        try:
+            self._valid = self._decode(evc)
+
+        except Exception as e:
+            log.exception('decode', e=e)
+            self._valid = False
+
+    def __str__(self):
+        return "EVCMap-{}: UNI: {}, hasACL: {}".format(self._name, self._uni_port,
+                                                       self._needs_acl_support)
+
+    @staticmethod
+    def create_ingress_map(flow, evc, dry_run=False):
+        evc_map = EVCMap(flow, evc, True)
+
+        if evc_map._valid and not dry_run:
+            evc.add_evc_map(evc_map)
+            evc_map._evc = evc
+
+        return evc_map
+
+    @staticmethod
+    def create_egress_map(flow, evc, dry_run=False):
+        evc_map = EVCMap(flow, evc, False)
+
+        if evc_map._valid and not dry_run:
+            evc.add_evc_map(evc_map)
+            evc_map._evc = evc
+
+        return evc_map
+
+    @property
+    def valid(self):
+        return self._valid
+
+    @property
+    def installed(self):
+        return self._installed
+
+    @property
+    def needs_update(self):
+        """ True if an parameter/ACL/... needs update or map needs to be reflowed after a failure"""
+        return self._needs_update
+
+    @needs_update.setter
+    def needs_update(self, value):
+        assert not value, 'needs update can only be reset'                # Can only reset
+        self._needs_update = False
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def status(self):
+        return self._status_message
+
+    @status.setter
+    def status(self, value):
+        self._status_message = value
+
+    @property
+    def evc(self):
+        return self._evc
+
+    @property
+    def _needs_acl_support(self):
+        if self._ipv4_dst is not None:  # In case MCAST downstream has ACL on it
+            return False
+
+        return self._eth_type is not None or self._ip_protocol is not None or\
+            self._udp_dst is not None or self._udp_src is not None
+
+    @property
+    def pon_id(self):
+        return self._pon_id     # May be None
+
+    @property
+    def onu_id(self):
+        return self._onu_id     # May be None if associated with a multicast flow
+
+    # @property
+    # def onu_ids(self):
+    #     return self._gem_ids_and_vid.keys()
+
+    @property
+    def gem_ids_and_vid(self):
+        return self._gem_ids_and_vid.copy()
+
+    @staticmethod
+    def _xml_header(operation=None):
+        return '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"{}><evc-map>'.\
+            format('' if operation is None else ' xc:operation="{}"'.format(operation))
+
+    @staticmethod
+    def _xml_trailer():
+        return '</evc-map></evc-maps>'
+
+    def get_evcmap_name(self, onu_id, gem_id):
+        return'{}.{}.{}.{}'.format(self.name, self.pon_id, onu_id, gem_id)
+
+    def _common_install_xml(self):
+        xml = '<enabled>{}</enabled>'.format('true' if self._enabled else 'false')
+        xml += '<uni>{}</uni>'.format(self._uni_port)
+
+        evc_name = self._evc.name if self._evc is not None else None
+        if evc_name is not None:
+            xml += '<evc>{}</evc>'.format(evc_name)
+        else:
+            xml += EVCMap.EvcConnection.xml(self._evc_connection)
+
+        xml += '<match-untagged>{}</match-untagged>'.format('true'
+                                                            if self._match_untagged
+                                                            else 'false')
+
+        # TODO: The following is not yet supported (and in some cases, not decoded)
+        # self._men_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
+        # self._men_pri = 0  # If Explicit Priority
+        #
+        # self._men_ctag_priority = EVCMap.PriorityOption.INHERIT_PRIORITY
+        # self._men_ctag_pri = 0  # If Explicit Priority
+        #
+        # self._match_ce_vlan_id = None
+        # self._match_untagged = True
+        # self._match_destination_mac_address = None
+        return xml
+
+    def _ingress_install_xml(self, onu_s_gem_ids_and_vid, acl_list, create):
+        from ..onu import Onu
+
+        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:
+            xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">'
+
+        for onu_or_vlan_id, gem_ids_and_vid in onu_s_gem_ids_and_vid.iteritems():
+            first_gem_id = True
+            gem_ids = gem_ids_and_vid[0]
+            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:
+                xml += '<evc-map{}>'.format('' if not create else ' xc:operation="create"')
+                xml += '<name>{}.{}.{}</name>'.format(self.name, ident, gem_id)
+                xml += '<ce-vlan-id>{}</ce-vlan-id>'.format(Onu.gem_id_to_gvid(gem_id))
+
+                # GEM-IDs are a sorted list (ascending). First gemport handles downstream traffic
+                if first_gem_id and (self._c_tag is not None or vid is not None):
+                    first_gem_id = False
+                    vlan = vid or self._c_tag
+                    xml += '<network-ingress-filter>'
+                    xml += '<men-ctag>{}</men-ctag>'.format(vlan)  # Added in August 2017 model
+                    xml += '</network-ingress-filter>'
+
+                if len(acl_list):
+                    xml += '<adtn-evc-map-acl:access-lists>'
+                    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:access-lists>'
+                xml += self._common_install_xml()
+                xml += '</evc-map>'
+        xml += '</evc-maps>'
+        return xml
+
+    def _egress_install_xml(self):
+        xml = EVCMap._xml_header()
+        xml += '<name>{}</name>'.format(self.name)
+        xml += self._common_install_xml()
+        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():
+            ports = []
+            for gems_and_vids in self._gem_ids_and_vid.itervalues():
+                ports.extend(gems_and_vids[0])
+            return ports
+
+        log.debug('install-evc-map', valid=self._valid, gem_ports=gem_ports())
+
+        if self._valid and len(gem_ports()) > 0:
+            # Install ACLs first (if not yet installed)
+            work_acls = self._new_acls.copy()
+            self._new_acls = dict()
+
+            log.debug('install-evc-map-acls', install_acls=len(work_acls))
+            for acl in work_acls.itervalues():
+                try:
+                    yield acl.install()
+
+                except Exception as e:
+                    log.exception('acl-install-failed', name=self.name, e=e)
+                    self._new_acls.update(work_acls)
+                    raise
+
+            # Any user-data flows attached to this map ?
+            c_tag = None
+            for flow_id, flow in self._flows.items():
+                c_tag = flow.inner_vid or flow.vlan_id or c_tag
+
+            self._c_tag = c_tag
+
+            # Now EVC-MAP
+            if not self._installed or self._needs_update:
+                log.debug('needs-install-or-update', installed=self._installed, update=self._needs_update)
+                is_installed = self._installed
+                self._installed = True
+                try:
+                    self._cancel_deferred()
+
+                    log.info('upstream-bandwidth')
+                    try:
+                        yield self.update_upstream_flow_bandwidth()
+
+                    except Exception as e:
+                        log.exception('upstream-bandwidth-failed', name=self.name, e=e)
+                        raise
+
+                    map_xml = self._ingress_install_xml(self._gem_ids_and_vid, work_acls.values(),
+                                                        not is_installed) \
+                        if self._is_ingress_map else self._egress_install_xml()
+
+                    log.debug('install', xml=map_xml, name=self.name)
+                    results = yield self._handler.netconf_client.edit_config(map_xml)
+                    self._installed = results.ok
+                    self._needs_update = results.ok
+                    self._status_message = '' if results.ok else results.error
+
+                    if results.ok:
+                        self._existing_acls.update(work_acls)
+                    else:
+                        self._new_acls.update(work_acls)
+
+                except RPCError as rpc_err:
+                    if rpc_err.tag == 'data-exists':    # Known race due to bulk-flow operation
+                        pass
+
+                except Exception as e:
+                    log.exception('evc-map-install-failed', name=self.name, e=e)
+                    self._installed = is_installed
+                    self._new_acls.update(work_acls)
+                    raise
+
+                # Install any needed shapers
+                if self._installed:
+                    try:
+                        yield self.update_downstream_flow_bandwidth()
+
+                    except Exception as e:
+                        log.exception('shaper-install-failed', name=self.name, e=e)
+                        raise
+
+        returnValue(self._installed and self._valid)
+
+    def _ingress_remove_xml(self, onus_gem_ids_and_vid):
+        xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \
+              ' xc:operation="delete">'
+
+        for onu_id, gem_ids_and_vid in onus_gem_ids_and_vid.iteritems():
+            for gem_id in gem_ids_and_vid[0]:
+                xml += '<evc-map>'
+                xml += '<name>{}.{}.{}</name>'.format(self.name, onu_id, gem_id)
+                xml += '</evc-map>'
+        xml += '</evc-maps>'
+        return xml
+
+    def _egress_remove_xml(self):
+        return EVCMap._xml_header('delete') + \
+               '<name>{}</name>'.format(self.name) + EVCMap._xml_trailer()
+
+    def _remove(self):
+        if not self.installed:
+            returnValue('Not installed')
+
+        log.info('removing', evc_map=self)
+
+        def _success(rpc_reply):
+            log.debug('remove-success', rpc_reply=rpc_reply)
+            self._installed = False
+
+        def _failure(failure):
+            log.error('remove-failed', failure=failure)
+            self._installed = False
+
+        def _remove_acls(_):
+            acls, self._new_acls = self._new_acls, dict()
+            existing, self._existing_acls = self._existing_acls, dict()
+            acls.update(existing)
+
+            dl = []
+            for acl in acls.itervalues():
+                dl.append(acl.remove())
+
+            if len(dl) > 0:
+                defer.gatherResults(dl, consumeErrors=True)
+
+        def _remove_shaper(_):
+            if self._shaper_name is not None:
+                self.update_downstream_flow_bandwidth(remove=True)
+
+        map_xml = self._ingress_remove_xml(self._gem_ids_and_vid) if self._is_ingress_map \
+            else self._egress_remove_xml()
+
+        d = self._handler.netconf_client.edit_config(map_xml)
+        d.addCallbacks(_success, _failure)
+        d.addBoth(_remove_acls)
+        d.addBoth(_remove_shaper)
+        return d
+
+    @inlineCallbacks
+    def delete(self, flow):
+        """
+        Remove from hardware and delete/clean-up EVC-MAP Object
+
+        :param flow: (FlowEntry) Specific flow to remove from the MAP or None if all
+                                 flows should be removed
+        :return:
+        """
+        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)
+
+        else:
+            if self._evc is not None:
+                self._evc.remove_evc_map(self)
+                self._evc = None
+
+            self._valid = False
+            self._cancel_deferred()
+            try:
+                yield self._remove()
+
+            except Exception as e:
+                log.exception('removal', e=e)
+
+        returnValue('Done')
+
+    def reflow_needed(self):
+        log.debug('reflow-needed', installed=self.installed, needs_update=self.needs_update)
+        reflow = not self.installed or self.needs_update
+
+        if not reflow:
+            pass  # TODO: implement retrieve & compare of EVC Map parameters
+
+        return reflow
+
+    @staticmethod
+    def find_matching_ingress_flow(flow, upstream_flow_table):
+        """
+        Look for an existing EVC-MAP that may match this flow.  Called when upstream signature
+        for a flow does not make match. This can happen if an ACL flow is added and only an User
+        Data flow exists, or if only an ACL flow exists.
+
+        :param flow: (FlowEntry) flow to add
+        :param upstream_flow_table: (dict of FlowEntry) Existing upstream flows for this device,
+                                     including the flow we are looking to add
+        :return: (EVCMap) if appropriate one is found, else None
+        """
+        # A User Data flow will have:
+        #      signature: <dev>.1.5.2.242
+        #       down-sig: <dev>.1.*.2.*
+        #   logical-port: 66
+        #    is-acl-flow: False
+        #
+        # An ACL flow will have:
+        #      signature: <dev>.1.5.[4092 or 4094].None    (untagged VLAN == utility VLAN case)
+        #       down-sig: <dev>.1.*.[4092 or 4094].*
+        #   logical-port: 66
+        #    is-acl-flow: True
+        #
+        # Reduce the upstream flow table to only those that match the ingress,
+        # and logical-ports match (and is not this flow) and have a map
+
+        log.debug('find-matching-ingress-flow', logical_port=flow.logical_port, flow=flow.output)
+        candidate_flows = [f for f in upstream_flow_table.itervalues() if
+                           f.in_port == flow.in_port and
+                           f.logical_port == flow.logical_port and
+                           f.output == flow.output and
+                           f.evc_map is not None]        # This weeds out this flow
+
+        log.debug('find-matching-ingress-flow', candidate_flows=candidate_flows)
+        return candidate_flows[0].evc_map if len(candidate_flows) > 0 else None
+
+    def add_flow(self, flow, evc):
+        """
+        Add a new flow to an existing EVC-MAP. This can be called to add:
+          o an ACL flow to an existing utility EVC, or
+          o an ACL flow to an existing User Data Flow, or
+          o a User Data Flow to an existing ACL flow (and this needs the EVC updated
+            as well.
+
+        Note that the Downstream EVC provided is the one that matches this flow. If
+        this is adding an ACL to and existing User data flow, we DO NOT want to
+        change the EVC Map's EVC
+
+        :param flow: (FlowEntry) New flow
+        :param evc: (EVC) Matching EVC for downstream flow
+        """
+        from flow_entry import FlowEntry
+        # Create temporary EVC-MAP
+        assert flow.flow_direction in FlowEntry.upstream_flow_types, \
+            'Only Upstream flows additions are supported at this time'
+
+        log.debug('add-flow-to-evc', flow=flow, evc=evc)
+
+        tmp_map = EVCMap.create_ingress_map(flow, evc, dry_run=True) \
+            if flow.flow_direction in FlowEntry.upstream_flow_types \
+            else EVCMap.create_egress_map(flow, evc, dry_run=True)
+
+        if tmp_map is None or not tmp_map.valid:
+            return None
+
+        self._flows[flow.flow_id] = flow
+        self._needs_update = True
+
+        # Are there ACLs to add to any existing (or empty) ACLs
+        if len(tmp_map._new_acls) > 0:
+            self._new_acls.update(tmp_map._new_acls)        # New ACL flow
+            log.debug('add-acl-flows', map=str(self), new=tmp_map._new_acls)
+
+        # Look up existing EVC for this flow. If it is a service EVC for
+        # Packet In/Out, and this is a regular flow, migrate to the newer EVC
+        if self._evc.service_evc and not evc.service_evc:
+            log.info('new-evc-for-map', old=self._evc.name, new=evc.name)
+            self._evc.remove_evc_map(self)
+            evc.add_evc_map(self)
+            self._evc = evc
+
+        return self
+
+    @inlineCallbacks
+    def _remove_flow(self, flow):
+        """
+        Remove a specific flow from an EVC_MAP. This includes removing any
+        ACL entries associated with the flow and could result in moving the
+        EVC-MAP over to another EVC.
+
+        :param flow: (FlowEntry) Flow to remove
+        """
+        try:
+            del self._flows[flow.flow_id]
+
+            log('remove-flow-to-evc', flow=flow)
+            # Remove any ACLs
+            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:
+                    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]
+
+                    # Scan EVC to see if it needs to move back to the Utility
+                    # or Untagged EVC from a user data EVC
+                    if self._evc and 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(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)
+
+                # Remove ACL itself
+                try:
+                    yield acl.remove()
+
+                except Exception as e:
+                    log.exception('acl-remove', e=e)
+
+        except Exception as e:
+            log.exception('remove-failed', e=e)
+
+    @staticmethod
+    def create_evc_map_name(flow):
+        # Note: When actually installed into the OLT, the .onu_id.gem_port is
+        #       appended to the name
+        return EVC_MAP_NAME_FORMAT.format(flow.logical_port, flow.flow_id)
+
+    @staticmethod
+    def decode_evc_map_name(name):
+        """
+        Reverse engineer EVC-MAP name parameters. Helpful in quick packet-in
+        processing
+
+        :param name: (str) EVC Map Name
+        :return: (dict) Logical Ingress Port, OpenFlow Flow-ID
+        """
+        items = name.split('-') if name is not None else dict()
+
+        # Note: When actually installed into the OLT, the .onu_id.gem_port is
+        #       appended to the name
+        return {'ingress-port': items[1],
+                'flow-id': items[2].split('.')[0]} if len(items) > 2 else dict()
+
+    @inlineCallbacks
+    def update_upstream_flow_bandwidth(self):
+        """
+        Upstream flow bandwidth comes from the flow_entry related to this EVC-MAP
+        and if no bandwidth property is found, allow full bandwidth
+        """
+        # all flows should should be on the same PON
+        flow = self._flows.itervalues().next()
+        is_pon = flow.handler.is_pon_port(flow.in_port)
+
+        if self._is_ingress_map and is_pon:
+            pon_port = flow.handler.get_southbound_port(flow.in_port)
+            if pon_port is None:
+                returnValue('no PON')
+
+            session = self._handler.rest_client
+            # TODO: Refactor with tech profiles
+            tconts = None               # pon_port.tconts
+            traffic_descriptors = None  # pon_port.traffic_descriptors
+
+            if traffic_descriptors is None or tconts is None:
+                returnValue('no TDs on PON')
+
+            bandwidth = self._upstream_bandwidth or 10000000000
+
+            if self.pon_id is not None and self.onu_id is not None:
+                name = 'tcont-{}-{}-data'.format(self.pon_id, self.onu_id)
+                td = traffic_descriptors.get(name)
+                tcont = tconts.get(name)
+
+                if td is not None and tcont is not None:
+                    alloc_id = tcont.alloc_id
+                    td.maximum_bandwidth = bandwidth
+                    try:
+                        results = yield td.add_to_hardware(session)
+                        log.debug('td-modify-results', results=results)
+
+                    except Exception as _e:
+                        pass
+
+    @inlineCallbacks
+    def update_downstream_flow_bandwidth(self, remove=False):
+        """
+        Downstream flow bandwidth is extracted from the related EVC flow_entry
+        bandwidth property. It is written to this EVC-MAP only if it is found
+        """
+        xml = None
+        results = None
+
+        if remove:
+            name, self._shaper_name = self._shaper_name, None
+            if name is not None:
+                xml = self._shaper_remove_xml(name)
+        else:
+            if self._evc is not None and self._evc.flow_entry is not None \
+                    and self._evc.flow_entry.bandwidth is not None:
+                self._shaper_name = self._name
+                xml = self._shaper_install_xml(self._shaper_name,
+                                               self._evc.flow_entry.bandwidth * 1000)  # kbps
+        if xml is not None:
+            try:
+                log.info('downstream-bandwidth', xml=xml, name=self.name, remove=remove)
+                results = yield self._handler.netconf_client.edit_config(xml)
+
+            except RPCError as rpc_err:
+                if rpc_err.tag == 'data-exists':
+                    pass
+
+            except Exception as e:
+                log.exception('downstream-bandwidth', name=self.name, remove=remove, e=e)
+                raise
+
+        returnValue(results)
+
+    def _shaper_install_xml(self, name, bandwidth):
+        xml = '<adtn-shaper:shapers xmlns:adtn-shaper="http://www.adtran.com/ns/yang/adtran-traffic-shapers" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="merge">'
+        for onu_id, gem_ids_and_vid in self._gem_ids_and_vid.iteritems():
+            for gem_id in gem_ids_and_vid[0]:
+                xml += ' <adtn-shaper:shaper>'
+                xml += '  <adtn-shaper:name>{}.{}.{}</adtn-shaper:name>'.format(name, onu_id, gem_id)
+                xml += '  <adtn-shaper:enabled>true</adtn-shaper:enabled>'
+                xml += '  <adtn-shaper:rate>{}</adtn-shaper:rate>'.format(bandwidth)
+                xml += '  <adtn-shaper-evc-map:evc-map xmlns:adtn-shaper-evc-map="http://www.adtran.com/ns/yang/adtran-traffic-shaper-evc-maps">{}.{}.{}</adtn-shaper-evc-map:evc-map>'.format(self.name, onu_id, gem_id)
+                xml += ' </adtn-shaper:shaper>'
+        xml += '</adtn-shaper:shapers>'
+        return xml
+
+    def _shaper_remove_xml(self, name):
+        xml = '<adtn-shaper:shapers xmlns:adtn-shaper="http://www.adtran.com/ns/yang/adtran-traffic-shapers" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete">'
+        for onu_id, gem_ids_and_vid in self._gem_ids_and_vid.iteritems():
+            for gem_id in gem_ids_and_vid[0]:
+                xml += ' <adtn-shaper:shaper >'
+                xml += '  <adtn-shaper:name>{}.{}.{}</adtn-shaper:name>'.format(name, onu_id, gem_id)
+                xml += ' </adtn-shaper:shaper>'
+        xml += '</adtn-shaper:shapers>'
+        return xml
+
+    def _setup_tech_profiles(self):
+        # Set up the TCONT / GEM Ports for this connection (Downstream only of course)
+        # all flows should have same GEM port setup
+        flow = self._flows.itervalues().next()
+        is_pon = flow.handler.is_pon_port(flow.in_port)
+
+        if self._is_ingress_map and is_pon:
+            pon_port = flow.handler.get_southbound_port(flow.in_port)
+
+            if pon_port is None:
+                return
+
+            onu = next((onu for onu in pon_port.onus if onu.logical_port == flow.logical_port), None)
+
+            if onu is None:       # TODO: Add multicast support later (self.onu_id == None)
+                return
+
+            self._pon_id = pon_port.pon_id
+            self._onu_id = onu.onu_id
+
+            # Identify or allocate TCONT and GEM Ports.  If the ONU has been informed of the
+            # GEM PORTs that belong to it, the tech profiles were already set up by a previous
+            # flows
+            onu_gems = onu.gem_ids(self._tech_profile_id)
+
+            if len(onu_gems) > 0:
+                self._gem_ids_and_vid[onu.onu_id] = (onu_gems, flow.vlan_id)
+                return
+
+            uni_id = self._handler.platform.uni_id_from_uni_port(flow.logical_port)
+            pon_profile = self._handler.tech_profiles[self.pon_id]
+            alloc_id = None
+
+            try:
+                (ofp_port_name, ofp_port_no) = self._handler.get_ofp_port_name(self.pon_id,
+                                                                               self.onu_id,
+                                                                               flow.logical_port)
+                if ofp_port_name is None:
+                    log.error("port-name-not-found")
+                    return
+
+                # Check tech profile instance already exists for derived port name
+                tech_profile = pon_profile.get_tech_profile_instance(self._tech_profile_id,
+                                                                     ofp_port_name)
+                log.debug('Get-tech-profile-instance-status',
+                          tech_profile_instance=tech_profile)
+
+                if tech_profile is None:
+                    # create tech profile instance
+                    tech_profile = pon_profile.create_tech_profile_instance(self._tech_profile_id,
+                                                                            ofp_port_name,
+                                                                            self.pon_id)
+                    if tech_profile is None:
+                        raise Exception('Tech-profile-instance-creation-failed')
+                else:
+                    log.debug('Tech-profile-instance-already-exist-for-given port-name',
+                              ofp_port_name=ofp_port_name)
+
+                # upstream scheduler
+                us_scheduler = pon_profile.get_us_scheduler(tech_profile)
+
+                # downstream scheduler
+                ds_scheduler = pon_profile.get_ds_scheduler(tech_profile)
+
+                # create Tcont protobuf
+                pb_tconts = pon_profile.get_tconts(tech_profile, us_scheduler, ds_scheduler)
+
+                # create TCONTs & GEM Ports locally
+                for pb_tcont in pb_tconts:
+                    from ..xpon.olt_tcont import OltTCont
+                    tcont = OltTCont.create(pb_tcont,
+                                            self.pon_id,
+                                            self.onu_id,
+                                            self._tech_profile_id,
+                                            uni_id,
+                                            ofp_port_no)
+                    if tcont is not None:
+                        onu.add_tcont(tcont)
+
+                # Fetch alloc id and gemports from tech profile instance
+                alloc_id = tech_profile.us_scheduler.alloc_id
+
+                onu_gems = [gem.gemport_id for gem in tech_profile.upstream_gem_port_attribute_list]
+
+                for gem in tech_profile.upstream_gem_port_attribute_list:
+                    from ..xpon.olt_gem_port import OltGemPort
+                    gem_port = OltGemPort.create(self._handler,
+                                                 gem,
+                                                 tech_profile.us_scheduler.alloc_id,
+                                                 self._tech_profile_id,
+                                                 self.pon_id,
+                                                 self.onu_id,
+                                                 uni_id,
+                                                 ofp_port_no)
+                    if gem_port is not None:
+                        onu.add_gem_port(gem_port)
+
+                self._gem_ids_and_vid = {onu.onu_id: (onu_gems, flow.vlan_id)}
+
+                # Send technology profile information to ONU
+                reactor.callLater(0, self._handler.setup_onu_tech_profile, self._pon_id,
+                                  self.onu_id, flow.logical_port)
+
+            except BaseException as e:
+                log.exception(exception=e)
+
+            # Update the allocated alloc_id and gem_port_id for the ONU/UNI to KV store
+            pon_intf_onu_id = (self.pon_id, self.onu_id, uni_id)
+            resource_manager = self._handler.resource_mgr.resource_managers[self.pon_id]
+
+            resource_manager.update_alloc_ids_for_onu(pon_intf_onu_id, list([alloc_id]))
+            resource_manager.update_gemport_ids_for_onu(pon_intf_onu_id, onu_gems)
+
+            self._handler.resource_mgr.update_gemports_ponport_to_onu_map_on_kv_store(onu_gems,
+                                                                                      self.pon_id,
+                                                                                      self.onu_id,
+                                                                                      uni_id)
+
+    def _decode(self, evc):
+        from evc import EVC
+        from flow_entry import FlowEntry
+
+        # Only called from initializer, so first flow is only flow
+        flow = self._flows.itervalues().next()
+
+        self._name = EVCMap.create_evc_map_name(flow)
+
+        if evc:
+            self._evc_connection = EVCMap.EvcConnection.EVC
+        else:
+            self._status_message = 'Can only create EVC-MAP if EVC supplied'
+            return False
+
+        is_pon = flow.handler.is_pon_port(flow.in_port)
+        is_uni = flow.handler.is_uni_port(flow.in_port)
+
+        if flow.bandwidth is not None:
+            self._upstream_bandwidth = flow.bandwidth * 1000000
+
+        if is_pon or is_uni:
+            # Preserve CE VLAN tag only if utility VLAN/EVC
+            self._uni_port = flow.handler.get_port_name(flow.in_port)
+            evc.ce_vlan_preservation = evc.ce_vlan_preservation or False
+        else:
+            self._status_message = 'EVC-MAPS without UNI or PON ports are not supported'
+            return False    # UNI Ports handled in the EVC Maps
+
+        # ACL logic
+        self._eth_type = flow.eth_type
+
+        if self._eth_type == FlowEntry.EtherType.IPv4:
+            self._ip_protocol = flow.ip_protocol
+            self._ipv4_dst = flow.ipv4_dst
+
+            if self._ip_protocol == FlowEntry.IpProtocol.UDP:
+                self._udp_dst = flow.udp_dst
+                self._udp_src = flow.udp_src
+
+        # If no match of VLAN this may be for untagged traffic or upstream and needs to
+        # match the gem-port vid
+
+        self._setup_tech_profiles()
+
+        # self._match_untagged = flow.vlan_id is None and flow.inner_vid is None
+        self._c_tag = flow.inner_vid or flow.vlan_id
+
+        # If a push of a single VLAN is present with a POP of the VLAN in the EVC's
+        # flow, then this is a traditional EVC flow
+
+        evc.men_to_uni_tag_manipulation = EVC.Men2UniManipulation.POP_OUT_TAG_ONLY
+        evc.switching_method = EVC.SwitchingMethod.DOUBLE_TAGGED \
+            if self._c_tag is not None else EVC.SwitchingMethod.SINGLE_TAGGED
+
+        try:
+            acl = ACL.create(flow)
+            if acl.name not in self._new_acls:
+                self._new_acls[acl.name] = acl
+
+        except Exception as e:
+            log.exception('ACL-decoding', e=e)
+            return False
+
+        return True
+
+    # Bulk operations
+
+    @staticmethod
+    def remove_all(client, regex_=EVC_MAP_NAME_REGEX_ALL):
+        """
+        Remove all matching EVC Maps from hardware
+
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        # Do a 'get' on the evc-map config an you should get the names
+        get_xml = """
+        <filter>
+          <evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps">
+            <evc-map>
+              <name/>
+            </evc-map>
+          </evc-maps>
+        </filter>
+        """
+        log.debug('query', xml=get_xml, regex=regex_)
+
+        def request_failed(results, operation):
+            log.error('{}-failed'.format(operation), results=results)
+            # No further actions. Periodic poll later on will scrub any old EVC-Maps if needed
+
+        def delete_complete(results):
+            log.debug('delete-complete', results=results)
+
+        def do_delete(rpc_reply, regexpr):
+            log.debug('query-complete', rpc_reply=rpc_reply)
+
+            if rpc_reply.ok:
+                result_dict = xmltodict.parse(rpc_reply.data_xml)
+                entries = result_dict['data']['evc-maps'] if 'evc-maps' in result_dict['data'] else {}
+
+                if 'evc-map' in entries:
+                    p = re.compile(regexpr)
+
+                    if isinstance(entries['evc-map'], list):
+                        names = {entry['name'] for entry in entries['evc-map']
+                                 if 'name' in entry and p.match(entry['name'])}
+                    else:
+                        names = set()
+                        for item in entries['evc-map'].items():
+                            if isinstance(item, tuple) and item[0] == 'name':
+                                names.add(item[1])
+                                break
+
+                    if len(names) > 0:
+                        del_xml = '<evc-maps xmlns="http://www.adtran.com/ns/yang/adtran-evc-maps"' + \
+                                 ' xc:operation = "delete">'
+                        for name in names:
+                            del_xml += '<evc-map>'
+                            del_xml += '<name>{}</name>'.format(name)
+                            del_xml += '</evc-map>'
+                        del_xml += '</evc-maps>'
+                        log.debug('removing', xml=del_xml)
+
+                        return client.edit_config(del_xml)
+
+            return succeed('no entries')
+
+        d = client.get(get_xml)
+        d.addCallbacks(do_delete, request_failed, callbackArgs=[regex_], errbackArgs=['get'])
+        d.addCallbacks(delete_complete, request_failed, errbackArgs=['edit-config'])
+        return d
+
+    def _cancel_deferred(self):
+        d, self._deferred = self._deferred, None
+        try:
+            if d is not None and not d.called:
+                d.cancel()
+        except:
+            pass
diff --git a/adapters/adtran_common/flow/flow_entry.py b/adapters/adtran_common/flow/flow_entry.py
new file mode 100644
index 0000000..cb8dd4a
--- /dev/null
+++ b/adapters/adtran_common/flow/flow_entry.py
@@ -0,0 +1,821 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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.
+
+import structlog
+from evc import EVC
+from evc_map import EVCMap
+from enum import IntEnum
+from utility_evc import UtilityEVC
+import pyvoltha.common.openflow.utils as fd
+from pyvoltha.protos.openflow_13_pb2 import OFPP_MAX, OFPP_CONTROLLER, OFPVID_PRESENT, OFPXMC_OPENFLOW_BASIC
+from twisted.internet.defer import returnValue, inlineCallbacks, gatherResults
+
+log = structlog.get_logger()
+
+# IP Protocol numbers
+_supported_ip_protocols = [
+    1,          # ICMP
+    2,          # IGMP
+    6,          # TCP
+    17,         # UDP
+]
+
+
+class FlowEntry(object):
+    """
+    Provide a class that wraps the flow rule and also provides state/status for a FlowEntry.
+
+    When a new flow is sent, it is first decoded to check for any potential errors. If None are
+    found, the entry is created and it is analyzed to see if it can be combined to with any other flows
+    to create or modify an existing EVC.
+
+    Note: Since only E-LINE is supported, modification of an existing EVC is not performed.
+    """
+    class PortType(IntEnum):
+        NNI = 0         # NNI Port
+        UNI = 1         # UNI Port
+        PON = 2         # PON Port (all UNIs on PON)
+        CONTROLLER = 3  # Controller port (packet in/out)
+
+    class FlowDirection(IntEnum):
+        UPSTREAM = 0          # UNI port to NNI Port
+        DOWNSTREAM = 1        # NNI port to UNI Port
+        CONTROLLER_UNI = 2    # Trap packet on UNI and send to controller
+        NNI_PON = 3           # NNI port to PON Port (all UNIs) - Utility VLAN & multicast
+
+        # The following are not yet supported
+        CONTROLLER_NNI = 4    # Trap packet on NNI and send to controller
+        CONTROLLER_PON = 5    # Trap packet on all UNIs of a PON and send to controller
+        NNI_NNI = 6           # NNI port to NNI Port
+        UNI_UNI = 7           # UNI port to UNI Port
+        OTHER = 9             # Unable to determine
+
+    upstream_flow_types = {FlowDirection.UPSTREAM, FlowDirection.CONTROLLER_UNI}
+    downstream_flow_types = {FlowDirection.DOWNSTREAM, FlowDirection.NNI_PON}
+
+    LEGACY_CONTROL_VLAN = 4000
+
+    # Well known EtherTypes
+    class EtherType(IntEnum):
+        EAPOL = 0x888E
+        IPv4 = 0x0800
+        IPv6 = 0x86DD
+        ARP = 0x0806
+        LLDP = 0x88CC
+
+    # Well known IP Protocols
+    class IpProtocol(IntEnum):
+        IGMP = 2
+        UDP = 17
+
+    def __init__(self, flow, handler):
+        self._flow = flow
+        self._handler = handler
+        self.flow_id = flow.id
+        self.evc = None              # EVC this flow is part of
+        self.evc_map = None          # EVC-MAP this flow is part of
+        self._flow_direction = FlowEntry.FlowDirection.OTHER
+        self._logical_port = None    # Currently ONU VID is logical port if not doing xPON
+        self._is_multicast = False
+        self._is_acl_flow = False
+        self._bandwidth = None
+
+        # A value used to locate possible related flow entries
+        self.signature = None
+        self.downstream_signature = None  # Valid for upstream EVC-MAP Flows
+
+        # Selection properties
+        self.in_port = None
+        self.vlan_id = None
+        self.pcp = None
+        self.eth_type = None
+        self.ip_protocol = None
+        self.ipv4_dst = None
+        self.udp_dst = None         # UDP Port #
+        self.udp_src = None         # UDP Port #
+        self.inner_vid = None
+
+        # Actions
+        self.output = None
+        self.pop_vlan = False
+        self.push_vlan_tpid = None
+        self.push_vlan_id = None
+
+        self._name = self.create_flow_name()
+
+    def __str__(self):
+        return 'flow_entry: {}, in: {}, out: {}, vid: {}, inner:{}, eth: {}, IP: {}'.format(
+            self.name, self.in_port, self.output, self.vlan_id, self.inner_vid,
+            self.eth_type, self.ip_protocol)
+
+    def __repr__(self):
+        return str(self)
+
+    @property
+    def name(self):
+        return self._name    # TODO: Is a name really needed in production?
+
+    def create_flow_name(self):
+        return 'flow-{}-{}'.format(self.device_id, self.flow_id)
+
+    @property
+    def flow(self):
+        return self._flow
+
+    @property
+    def handler(self):
+        return self._handler
+
+    @property
+    def device_id(self):
+        return self.handler.device_id
+
+    @property
+    def bandwidth(self):
+        """ Bandwidth in Mbps (if any) """
+        return self._bandwidth
+
+    @property
+    def flow_direction(self):
+        return self._flow_direction
+
+    @property
+    def is_multicast_flow(self):
+        return self._is_multicast
+
+    @property
+    def is_acl_flow(self):
+        return self._is_acl_flow or self._needs_acl_support
+
+    @property
+    def logical_port(self):
+        return self._logical_port   # NNI or UNI Logical Port
+
+    @staticmethod
+    def create(flow, handler):
+        """
+        Create the appropriate FlowEntry wrapper for the flow.  This method returns a two
+        results.
+
+        The first result is the flow entry that was created. This could be a match to an
+        existing flow since it is a bulk update.  None is returned only if no match to
+        an existing entry is found and decode failed (unsupported field)
+
+        The second result is the EVC this flow should be added to. This could be an
+        existing flow (so your adding another EVC-MAP) or a brand new EVC (no existing
+        EVC-MAPs).  None is returned if there are not a valid EVC that can be created YET.
+
+        :param flow:   (Flow) Flow entry passed to VOLTHA adapter
+        :param handler: (AdtranDeviceHandler) handler for the device
+        :return: (FlowEntry, EVC)
+        """
+        # Exit early if it already exists
+        try:
+            flow_entry = FlowEntry(flow, handler)
+
+            ######################################################################
+            # Decode the flow entry
+            if not flow_entry._decode(flow):
+                # TODO: When we support individual flow mods, we will need to return
+                #       this flow back always
+                return None, None
+
+            ######################################################################
+            # Initialize flow_entry database (dicts) if needed and determine if
+            # the flows have already been handled.
+            downstream_sig_table = handler.downstream_flows
+            upstream_flow_table = handler.upstream_flows
+
+            log.debug('flow-entry-decoded', flow=flow_entry, signature=flow_entry.signature,
+                      downstream_signature=flow_entry.downstream_signature)
+
+            if flow_entry.flow_direction in FlowEntry.upstream_flow_types and\
+                    flow_entry.flow_id in upstream_flow_table:
+                log.debug('flow-entry-upstream-exists', flow=flow_entry)
+                return flow_entry, None
+
+            if flow_entry.flow_direction in FlowEntry.downstream_flow_types:
+                sig_table = downstream_sig_table.get(flow_entry.signature)
+                if sig_table is not None and flow_entry in sig_table.flows:
+                    log.debug('flow-entry-downstream-exists', flow=flow_entry)
+                    return flow_entry, None
+
+            ######################################################################
+            # Look for any matching flows in the other direction that might help
+            # make an EVC and then save it off in the device specific flow table
+            #
+            # TODO: For now, only support for E-LINE services between NNI and UNI
+            downstream_flow = None
+            upstream_flows = None
+            downstream_sig = None
+
+            if flow_entry._is_multicast:        # Uni-directional flow
+                assert flow_entry._flow_direction in FlowEntry.downstream_flow_types, \
+                    'Only downstream Multicast supported'
+                downstream_flow = flow_entry
+                downstream_sig = flow_entry.signature
+                upstream_flows = []
+
+            elif flow_entry.flow_direction in FlowEntry.downstream_flow_types:
+                downstream_flow = flow_entry
+                downstream_sig = flow_entry.signature
+
+            elif flow_entry.flow_direction in FlowEntry.upstream_flow_types:
+                downstream_sig = flow_entry.downstream_signature
+
+            if downstream_sig is None:
+                # TODO: When we support individual flow mods, we will need to return
+                #       this flow back always
+                log.debug('flow-entry-empty-downstream', flow=flow_entry)
+                return None, None
+
+            # Make sure a slot exists for the downstream signature and get its flow table
+            downstream_sig_table = downstream_sig_table.add(downstream_sig)
+            evc = downstream_sig_table.evc
+
+            # Save the new flow_entry to proper flow table
+            if flow_entry.flow_direction in FlowEntry.upstream_flow_types:
+                upstream_flow_table.add(flow_entry)
+                downstream_flow = evc.flow_entry if evc is not None else \
+                    next((_flow for _flow in downstream_sig_table.flows.itervalues()
+                          if isinstance(_flow, FlowEntry)), None)
+
+            elif flow_entry.flow_direction in FlowEntry.downstream_flow_types:
+                downstream_sig_table.flows.add(flow_entry)
+
+            # Now find all the upstream flows
+            if downstream_flow is not None:
+                upstream_flows = [_flow for _flow in upstream_flow_table.itervalues()
+                                  if _flow.downstream_signature == downstream_flow.signature]
+                if len(upstream_flows) == 0 and not downstream_flow.is_multicast_flow:
+                    upstream_flows = None
+
+            log.debug('flow-entry-search-results', flow=flow_entry,
+                      downstream_flow=downstream_flow, upstream_flows=upstream_flows)
+
+            ######################################################################
+            # Compute EVC and and maps
+            evc = FlowEntry._create_evc_and_maps(evc, downstream_flow, upstream_flows)
+
+            # Save off EVC (if we have one) for this flow if it is new
+            if evc is not None and evc.valid and downstream_sig_table.evc is None:
+                downstream_sig_table.evc = evc
+
+            return flow_entry, evc
+
+        except Exception as e:
+            log.exception('flow-entry-processing', e=e)
+            return None, None
+
+    @staticmethod
+    def _create_evc_and_maps(evc, downstream_flow, upstream_flows):
+        """
+        Give a set of flows, find (or create) the EVC and any needed EVC-MAPs
+
+        :param evc: (EVC) Existing EVC for downstream flow. May be null if not created
+        :param downstream_flow: (FlowEntry) NNI -> UNI flow (provides much of the EVC values)
+        :param upstream_flows: (list of FlowEntry) UNI -> NNI flows (provides much of the EVC-MAP values)
+
+        :return: EVC object
+        """
+        log.debug('flow-evc-and-maps', downstream_flow=downstream_flow,
+                  upstream_flows=upstream_flows)
+
+        if (evc is None and downstream_flow is None) or upstream_flows is None:
+            return None
+
+        # Get any existing EVC if a flow is already created
+        if downstream_flow.evc is None:
+            if evc is not None:
+                downstream_flow.evc = evc
+
+            elif downstream_flow.is_multicast_flow:
+                from mcast import MCastEVC
+                downstream_flow.evc = MCastEVC.create(downstream_flow)
+
+            elif downstream_flow.is_acl_flow:
+                downstream_flow.evc = downstream_flow.get_utility_evc()
+            else:
+                downstream_flow.evc = EVC(downstream_flow)
+
+        if not downstream_flow.evc.valid:
+            log.debug('flow-evc-and-maps-downstream-invalid',
+                      downstream_flow=downstream_flow,
+                      upstream_flows=upstream_flows)
+            return None
+
+        # Create EVC-MAPs. Note upstream_flows is empty list for multicast
+        # For Packet In/Out support. The upstream flows for will have matching
+        # signatures. So the first one to get created should create the EVC and
+        # if it needs and ACL, do so then. The second one should just reference
+        # the first map.
+        #
+        #    If the second has and ACL, then it should add it to the map.
+        #    TODO: What to do if the second (or third, ...) is the data one.
+        #          What should it do then?
+        sig_map_map = {f.signature: f.evc_map for f in upstream_flows
+                       if f.evc_map is not None}
+
+        for flow in upstream_flows:
+            if flow.evc_map is None:
+                if flow.signature in sig_map_map:
+                    # Found an explicitly matching existing EVC-MAP. Add flow to this EVC-MAP
+                    flow.evc_map = sig_map_map[flow.signature].add_flow(flow, downstream_flow.evc)
+                else:
+                    # May need to create a MAP or search for an existing ACL/user EVC-Map
+                    # upstream_flow_table = _existing_upstream_flow_entries[flow.device_id]
+                    upstream_flow_table = flow.handler.upstream_flows
+                    existing_flow = EVCMap.find_matching_ingress_flow(flow, upstream_flow_table)
+
+                    if existing_flow is None:
+                        flow.evc_map = EVCMap.create_ingress_map(flow, downstream_flow.evc)
+                    else:
+                        flow.evc_map = existing_flow.add_flow(flow, downstream_flow.evc)
+
+        all_maps_valid = all(flow.evc_map.valid for flow in upstream_flows) \
+            or downstream_flow.is_multicast_flow
+
+        log.debug('flow-evc-and-maps-downstream',
+                  downstream_flow=downstream_flow,
+                  upstream_flows=upstream_flows, all_valid=all_maps_valid)
+
+        return downstream_flow.evc if all_maps_valid else None
+
+    def get_utility_evc(self, use_default_vlan_id=False):
+        assert self.is_acl_flow, 'Utility evcs are for acl flows only'
+        return UtilityEVC.create(self, use_default_vlan_id)
+
+    @property
+    def _needs_acl_support(self):
+        if self.ipv4_dst is not None:  # In case MCAST downstream has ACL on it
+            return False
+
+        return self.eth_type is not None or self.ip_protocol is not None or\
+            self.ipv4_dst is not None or self.udp_dst is not None or self.udp_src is not None
+
+    def _decode(self, flow):
+        """
+        Examine flow rules and extract appropriate settings
+        """
+        log.debug('start-decode')
+        status = self._decode_traffic_selector(flow) and self._decode_traffic_treatment(flow)
+
+        # Determine direction of the flow and apply appropriate modifications
+        # to the decoded flows
+        if status:
+            if not self._decode_flow_direction():
+                return False
+
+            if self._flow_direction in FlowEntry.downstream_flow_types:
+                status = self._apply_downstream_mods()
+
+            elif self._flow_direction in FlowEntry.upstream_flow_types:
+                status = self._apply_upstream_mods()
+
+            else:
+                # TODO: Need to code this - Perhaps this is an NNI_PON for Multicast support?
+                log.error('unsupported-flow-direction')
+                status = False
+
+            log.debug('flow-evc-decode', direction=self._flow_direction, is_acl=self._is_acl_flow,
+                      inner_vid=self.inner_vid, vlan_id=self.vlan_id, pop_vlan=self.pop_vlan,
+                      push_vid=self.push_vlan_id, status=status)
+
+        # Create a signature that will help locate related flow entries on a device.
+        if status:
+            # These are not exact, just ones that may be put together to make an EVC. The
+            # basic rules are:
+            #
+            # 1 - Port numbers in increasing order
+            ports = [self.in_port, self.output]
+            ports.sort()
+            assert len(ports) == 2, 'Invalid port count: {}'.format(len(ports))
+
+            # 3 - The outer VID
+            # 4 - The inner VID.  Wildcard if downstream
+            if self.push_vlan_id is None:
+                outer = self.vlan_id
+                inner = self.inner_vid
+            else:
+                outer = self.push_vlan_id
+                inner = self.vlan_id
+
+            upstream_sig = '{}'.format(ports[0])
+            downstream_sig = '{}'.format(ports[0])
+            upstream_sig += '.{}'.format(ports[1])
+            downstream_sig += '.{}'.format(ports[1] if self.handler.is_nni_port(ports[1]) else '*')
+
+            upstream_sig += '.{}.{}'.format(outer, inner)
+            downstream_sig += '.{}.*'.format(outer)
+
+            if self._flow_direction in FlowEntry.downstream_flow_types:
+                self.signature = downstream_sig
+
+            elif self._flow_direction in FlowEntry.upstream_flow_types:
+                self.signature = upstream_sig
+                self.downstream_signature = downstream_sig
+
+            else:
+                log.error('unsupported-flow')
+                status = False
+
+            log.debug('flow-evc-decode', upstream_sig=self.signature, downstream_sig=self.downstream_signature)
+        return status
+
+    def _decode_traffic_selector(self, flow):
+        """
+        Extract EVC related traffic selection settings
+        """
+        self.in_port = fd.get_in_port(flow)
+
+        if self.in_port > OFPP_MAX:
+            log.warn('logical-input-ports-not-supported', in_port=self.in_port)
+            return False
+
+        for field in fd.get_ofb_fields(flow):
+            if field.type == fd.IN_PORT:
+                if self._handler.is_nni_port(self.in_port) or self._handler.is_uni_port(self.in_port):
+                    self._logical_port = self.in_port
+
+            elif field.type == fd.VLAN_VID:
+                if field.vlan_vid >= OFPVID_PRESENT + 4095:
+                    self.vlan_id = None             # pre-ONOS v1.13.5 or old EAPOL Rule
+                else:
+                    self.vlan_id = field.vlan_vid & 0xfff
+
+                log.debug('*** field.type == VLAN_VID', value=field.vlan_vid, vlan_id=self.vlan_id)
+
+            elif field.type == fd.VLAN_PCP:
+                log.debug('*** field.type == VLAN_PCP', value=field.vlan_pcp)
+                self.pcp = field.vlan_pcp
+
+            elif field.type == fd.ETH_TYPE:
+                log.debug('*** field.type == ETH_TYPE', value=field.eth_type)
+                self.eth_type = field.eth_type
+
+            elif field.type == fd.IP_PROTO:
+                log.debug('*** field.type == IP_PROTO', value=field.ip_proto)
+                self.ip_protocol = field.ip_proto
+
+                if self.ip_protocol not in _supported_ip_protocols:
+                    log.error('Unsupported IP Protocol', protocol=self.ip_protocol)
+                    return False
+
+            elif field.type == fd.IPV4_DST:
+                log.debug('*** field.type == IPV4_DST', value=field.ipv4_dst)
+                self.ipv4_dst = field.ipv4_dst
+
+            elif field.type == fd.UDP_DST:
+                log.debug('*** field.type == UDP_DST', value=field.udp_dst)
+                self.udp_dst = field.udp_dst
+
+            elif field.type == fd.UDP_SRC:
+                log.debug('*** field.type == UDP_SRC', value=field.udp_src)
+                self.udp_src = field.udp_src
+
+            elif field.type == fd.METADATA:
+                if self._handler.is_nni_port(self.in_port):
+                    # Downstream flow
+                    log.debug('*** field.type == METADATA', value=field.table_metadata)
+
+                    if field.table_metadata > 4095:
+                        # ONOS v1.13.5 or later. c-vid in upper 32-bits
+                        vid = field.table_metadata & 0x0FFF
+                        if vid > 0:
+                            self.inner_vid = vid        # CTag is never '0'
+
+                    elif field.table_metadata > 0:
+                        # Pre-ONOS v1.13.5 (vid without the 4096 offset)
+                        self.inner_vid = field.table_metadata
+
+                else:
+                    # Upstream flow
+                    pass   # Not used upstream at this time
+
+                log.debug('*** field.type == METADATA', value=field.table_metadata,
+                          inner_vid=self.inner_vid)
+            else:
+                log.warn('unsupported-selection-field', type=field.type)
+                self._status_message = 'Unsupported field.type={}'.format(field.type)
+                return False
+
+        return True
+
+    def _decode_traffic_treatment(self, flow):
+        # Loop through traffic treatment
+        for act in fd.get_actions(flow):
+            if act.type == fd.OUTPUT:
+                self.output = act.output.port
+
+            elif act.type == fd.POP_VLAN:
+                log.debug('*** action.type == POP_VLAN')
+                self.pop_vlan = True
+
+            elif act.type == fd.PUSH_VLAN:
+                log.debug('*** action.type == PUSH_VLAN', value=act.push)
+                tpid = act.push.ethertype
+                self.push_vlan_tpid = tpid
+
+            elif act.type == fd.SET_FIELD:
+                log.debug('*** action.type == SET_FIELD', value=act.set_field.field)
+                assert (act.set_field.field.oxm_class == OFPXMC_OPENFLOW_BASIC)
+                field = act.set_field.field.ofb_field
+
+                if field.type == fd.VLAN_VID:
+                    self.push_vlan_id = field.vlan_vid & 0xfff
+                else:
+                    log.debug('unsupported-set-field')
+            else:
+                log.warn('unsupported-action', action=act)
+                self._status_message = 'Unsupported action.type={}'.format(act.type)
+                return False
+
+        return True
+
+    def _decode_flow_direction(self):
+        # Determine direction of the flow
+        def port_type(port_number):
+            if port_number in self._handler.northbound_ports:
+                return FlowEntry.PortType.NNI
+
+            elif port_number in self._handler.southbound_ports:
+                return FlowEntry.PortType.PON
+
+            elif port_number <= OFPP_MAX:
+                return FlowEntry.PortType.UNI
+
+            elif port_number in {OFPP_CONTROLLER, 0xFFFFFFFD}:      # OFPP_CONTROLLER is wrong in proto-file
+                return FlowEntry.PortType.CONTROLLER
+
+            return FlowEntry.PortType.OTHER
+
+        flow_dir_map = {
+            (FlowEntry.PortType.UNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.UPSTREAM,
+            (FlowEntry.PortType.NNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.DOWNSTREAM,
+            (FlowEntry.PortType.UNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_UNI,
+            (FlowEntry.PortType.NNI, FlowEntry.PortType.PON):        FlowEntry.FlowDirection.NNI_PON,
+            # The following are not yet supported
+            # (FlowEntry.PortType.NNI, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_NNI,
+            # (FlowEntry.PortType.PON, FlowEntry.PortType.CONTROLLER): FlowEntry.FlowDirection.CONTROLLER_PON,
+            # (FlowEntry.PortType.NNI, FlowEntry.PortType.NNI):        FlowEntry.FlowDirection.NNI_NNI,
+            # (FlowEntry.PortType.UNI, FlowEntry.PortType.UNI):        FlowEntry.FlowDirection.UNI_UNI,
+        }
+        self._flow_direction = flow_dir_map.get((port_type(self.in_port), port_type(self.output)),
+                                                FlowEntry.FlowDirection.OTHER)
+        return self._flow_direction != FlowEntry.FlowDirection.OTHER
+
+    def _apply_downstream_mods(self):
+        # This is a downstream flow.  It could be any one of the following:
+        #
+        #   Legacy control VLAN:
+        #       This is the old VLAN 4000 that was used to attach EAPOL and other
+        #       controller flows to. Eventually these will change to CONTROLLER_UNI
+        #       flows.  For these, use the 'utility' VLAN instead so 4000 if available
+        #       for other uses (AT&T uses it for downstream multicast video).
+        #
+        #   Multicast VLAN:
+        #       This is downstream multicast data.
+        #       TODO: Test this to see if this needs to be in a separate NNI_PON mod-method
+        #
+        #   User Data flow:
+        #       This is for user data.  Eventually we may need to support ACLs?
+        #
+        # May be for to controller flow downstream (no ethType)
+        if self.vlan_id == FlowEntry.LEGACY_CONTROL_VLAN and self.eth_type is None and self.pcp == 0:
+            return False    # Do not install this flow.  Utility VLAN is in charge
+
+        elif self.flow_direction == FlowEntry.FlowDirection.NNI_PON and \
+                self.vlan_id == self.handler.utility_vlan:
+            # Utility VLAN downstream flow/EVC
+            self._is_acl_flow = True
+
+        elif self.vlan_id in self._handler.multicast_vlans:
+            #  multicast (ethType = IP)                         # TODO: May need to be an NNI_PON flow
+            self._is_multicast = True
+            self._is_acl_flow = True
+
+        else:
+            # Currently do not support ACLs on user data flows downstream
+            assert not self._needs_acl_support    # User data, no special modifications needed at this time
+
+        return True
+
+    def _apply_upstream_mods(self):
+        #
+        # This is an upstream flow.  It could be any of the following
+        #
+        #   ACL/Packet capture:
+        #       This is either a legacy (FlowDirection.UPSTREAM) or a new one
+        #       that specifies an output port of controller (FlowDirection.CONTROLLER_UNI).
+        #       Either way, these need to be placed on the Utility VLAN if the ONU attached
+        #       does not have a user-data flow (C-Tag).  If there is a C-Tag available,
+        #       then place it on that VLAN.
+        #
+        #       Once a user-data flow is established, move any of the ONUs ACL flows
+        #       over to that VLAN (this is handled elsewhere).
+        #
+        #   User Data flows:
+        #       No special modifications are needed
+        #
+        try:
+            # Do not handle PON level ACLs in this method
+            assert(self._flow_direction != FlowEntry.FlowDirection.CONTROLLER_PON)
+
+            # Is this a legacy (VLAN 4000) upstream to-controller flow
+            if self._needs_acl_support and FlowEntry.LEGACY_CONTROL_VLAN == self.push_vlan_id:
+                self._flow_direction = FlowEntry.FlowDirection.CONTROLLER_UNI
+                self._is_acl_flow = True
+                self.push_vlan_id = self.handler.utility_vlan
+
+            return True
+
+        except Exception as e:
+            # TODO: Need to support flow retry if the ONU is not yet activated   !!!!
+            log.exception('tag-fixup', e=e)
+            return False
+
+    @staticmethod
+    def drop_missing_flows(handler, valid_flow_ids):
+        dl = []
+        try:
+            flow_table = handler.upstream_flows
+            flows_to_drop = [flow for flow_id, flow in flow_table.items()
+                             if flow_id not in valid_flow_ids]
+            dl.extend([flow.remove() for flow in flows_to_drop])
+
+            for sig_table in handler.downstream_flows.itervalues():
+                flows_to_drop = [flow for flow_id, flow in sig_table.flows.items()
+                                 if isinstance(flow, FlowEntry) and flow_id not in valid_flow_ids]
+                dl.extend([flow.remove() for flow in flows_to_drop])
+
+        except Exception as _e:
+            pass
+
+        return gatherResults(dl, consumeErrors=True) if len(dl) > 0 else returnValue('no-flows-to-drop')
+
+    @inlineCallbacks
+    def remove(self):
+        """
+        Remove this flow entry from the list of existing entries and drop EVC
+        if needed
+        """
+        # Remove from exiting table list
+        flow_id = self.flow_id
+        flow_table = None
+
+        if self.flow_direction in FlowEntry.upstream_flow_types:
+            flow_table = self._handler.upstream_flows
+
+        elif self.flow_direction in FlowEntry.downstream_flow_types:
+            sig_table = self._handler.downstream_flows.get(self.signature)
+            flow_table = sig_table.flows if sig_table is not None else None
+
+        if flow_table is None or flow_id not in flow_table.keys():
+            returnValue('NOP')
+
+        # Remove from flow table and clean up flow table if empty
+        flow_table.remove(flow_id)
+        evc_map, self.evc_map = self.evc_map, None
+        evc = None
+
+        if self.flow_direction in FlowEntry.downstream_flow_types:
+            sig_table = self._handler.downstream_flows.get(self.signature)
+            if len(flow_table) == 0:   # Only 'evc' entry present
+                evc = sig_table.evc
+            else:
+                assert sig_table.evc is not None, 'EVC flow re-assignment error'
+
+        # Remove flow from the hardware
+        try:
+            dl = []
+            if evc_map is not None:
+                dl.append(evc_map.delete(self))
+
+            if evc is not None:
+                dl.append(evc.delete())
+
+            yield gatherResults(dl, consumeErrors=True)
+
+        except Exception as e:
+            log.exception('removal', e=e)
+
+        if self.flow_direction in FlowEntry.downstream_flow_types:
+            # If this flow owns the EVC, assign it to a remaining flow
+            sig_table = self._handler.downstream_flows.get(self.signature)
+            flow_evc = sig_table.evc
+
+            if flow_evc is not None and flow_evc.flow_entry is not None and flow_id == flow_evc.flow_entry.flow_id:
+                flow_evc.flow_entry = next((_flow for _flow in flow_table.itervalues()
+                                           if isinstance(_flow, FlowEntry)
+                                           and _flow.flow_id != flow_id), None)
+
+        # If evc was deleted, remove the signature table since now flows exist with
+        # that signature
+        if evc is not None:
+            self._handler.downstream_flows.remove(self.signature)
+
+        self.evc = None
+        returnValue('Done')
+
+    @staticmethod
+    def find_evc_map_flows(onu):
+        """
+        For a given OLT, find all the EVC Maps for a specific ONU
+        :param onu: (Onu) onu
+        :return: (list) of matching flows
+        """
+        # EVCs are only in the downstream table, EVC Maps are in upstream
+        onu_ports = onu.uni_ports
+
+        all_flow_entries = onu.olt.upstream_flows
+        evc_maps = [flow_entry.evc_map for flow_entry in all_flow_entries.itervalues()
+                    if flow_entry.in_port in onu_ports
+                    and flow_entry.evc_map is not None
+                    and flow_entry.evc_map.valid]
+
+        return evc_maps
+
+    @staticmethod
+    def sync_flows_by_onu(onu, reflow=False):
+        """
+        Check status of all flows on a per-ONU basis. Called when values
+        within the ONU are modified that may affect traffic.
+
+        :param onu: (Onu) ONU to examine
+        :param reflow: (boolean) Flag, if True, requests that the flow be sent to
+                                 hardware even if the values in hardware are
+                                 consistent with the current flow settings
+        """
+        evc_maps = FlowEntry.find_evc_map_flows(onu)
+        evcs = {}
+
+        for evc_map in evc_maps:
+            if reflow or evc_map.reflow_needed():
+                evc_map.needs_update = False
+
+            if not evc_map.installed:
+                evc = evc_map.evc
+                if evc is not None:
+                    evcs[evc.name] = evc
+
+        for evc in evcs.itervalues():
+            evc.installed = False
+            evc.schedule_install(delay=2)
+
+    ######################################################
+    # Bulk operations
+
+    @staticmethod
+    def clear_all(handler):
+        """
+        Remove all flows for the device.
+
+        :param handler: voltha adapter device handler
+        """
+        handler.downstream_flows.clear_all()
+        handler.upstream_flows.clear_all()
+
+    @staticmethod
+    def get_packetout_info(handler, logical_port):
+        """
+        Find parameters needed to send packet out successfully to the OLT.
+
+        :param handler: voltha adapter device handler
+        :param logical_port: (int) logical port number for packet to go out.
+
+        :return: physical port number, ctag, stag, evcmap name
+        """
+        from adapters.adtran_olt.onu import Onu
+
+        for flow_entry in handler.upstream_flows.itervalues():
+            log.debug('get-packetout-info', flow_entry=flow_entry)
+
+            # 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)
+                        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 first 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 None, None, None, None
diff --git a/adapters/adtran_common/flow/flow_tables.py b/adapters/adtran_common/flow/flow_tables.py
new file mode 100644
index 0000000..48e2e7e
--- /dev/null
+++ b/adapters/adtran_common/flow/flow_tables.py
@@ -0,0 +1,163 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 flow_entry import FlowEntry
+from evc import EVC
+
+
+class DeviceFlows(object):
+    """ Tracks existing flows on the device """
+
+    def __init__(self):
+        self._flow_table = dict()   # Key = (str)Flow ID, Value = FlowEntry
+
+    def __getitem__(self, item):
+        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
+        return self._flow_table[flow_id]
+
+    def __iter__(self):
+        for _flow_id, _flow in self._flow_table.items():
+            yield _flow_id, _flow
+
+    def itervalues(self):
+        for _flow in self._flow_table.values():
+            yield _flow
+
+    def iterkeys(self):
+        for _id in self._flow_table.keys():
+            yield _id
+
+    def items(self):
+        return self._flow_table.items()
+
+    def values(self):
+        return self._flow_table.values()
+
+    def keys(self):
+        return self._flow_table.keys()
+
+    def __len__(self):
+        return len(self._flow_table)
+
+    def add(self, flow):
+        assert isinstance(flow, FlowEntry)
+        if flow.flow_id not in self._flow_table:
+            self._flow_table[flow.flow_id] = flow
+        return flow
+
+    def get(self, item):
+        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
+        return self._flow_table.get(flow_id)
+
+    def remove(self, item):
+        flow_id = item.flow_id if isinstance(item, FlowEntry) else item
+        return self._flow_table.pop(flow_id, None)
+
+    def clear_all(self):
+        self._flow_table = dict()
+
+
+class DownstreamFlows(object):
+    """
+    Tracks existing flows that are downstream (NNI as source port)
+
+    The downstream table is slightly different than the base DeviceFlows
+    table as it is used to track flows that will become EVCs.  The base
+    table tracks flows that will be EVC-maps (or related to them).
+
+    The downstream table is also indexed by a downstream signature that
+    is composed as follows:
+
+        <dev-id>.<ingress-port-number>.<s-tag>.*
+
+    In comparison, the upstream flows is similar, but instead of '*' it has the
+    c-tag (if any).
+
+    TODO: Drop device ID from signatures once flow tables are unique to a device handler
+    """
+    def __init__(self):
+        self._signature_table = dict()  # Key = (str)Downstream signature
+                                        #  |
+                                        #  +-> downstream-signature
+                                        #      |
+                                        #      +-> 'evc' -> EVC
+                                        #      |
+                                        #      +-> flow-ids -> flow-entries...
+
+    def __getitem__(self, signature):
+        assert isinstance(signature, str)
+        return self._signature_table[signature]
+
+    def __iter__(self):
+        for _flow_id, _flow in self._signature_table.items():
+            yield _flow_id, _flow
+
+    def itervalues(self):
+        for _flow in self._signature_table.values():
+            yield _flow
+
+    def iterkeys(self):
+        for _id in self._signature_table.keys():
+            yield _id
+
+    def items(self):
+        return self._signature_table.items()
+
+    def values(self):
+        return self._signature_table.values()
+
+    def keys(self):
+        return self._signature_table.keys()
+
+    def __len__(self):
+        return len(self._signature_table)
+
+    def get(self, signature):
+        assert isinstance(signature, str)
+        return self._signature_table.get(signature)
+
+    def add(self, signature):
+        assert isinstance(signature, str)
+        """
+        Can be called by upstream flow to reserve a slot
+        """
+        if signature not in self._signature_table:
+            self._signature_table[signature] = DownstreamFlows.SignatureTableEntry(signature)
+        return self._signature_table[signature]
+
+    def remove(self, signature):
+        assert isinstance(signature, str)
+        return self._signature_table.pop(signature)
+
+    def clear_all(self):
+        self._signature_table = dict()
+
+    class SignatureTableEntry(object):
+        def __init__(self, signature):
+            self._signature = signature
+            self._evc = None
+            self._flow_table = DeviceFlows()
+
+        @property
+        def evc(self):
+            return self._evc
+
+        @evc.setter
+        def evc(self, evc):
+            assert isinstance(evc, (EVC, type(None)))
+            self._evc = evc
+
+        @property
+        def flows(self):
+            return self._flow_table
diff --git a/adapters/adtran_common/flow/mcast.py b/adapters/adtran_common/flow/mcast.py
new file mode 100644
index 0000000..54bf24f
--- /dev/null
+++ b/adapters/adtran_common/flow/mcast.py
@@ -0,0 +1,183 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 pyvoltha.common.openflow.utils import *
+from evc import EVC
+from flow_entry import FlowEntry
+from twisted.internet import defer
+from twisted.internet.defer import returnValue, inlineCallbacks
+
+log = structlog.get_logger()
+
+EVC_NAME_FORMAT = 'VOLTHA-MCAST-{}'                      # format(flow.vlan_id)
+EVC_NAME_REGEX_ALL = EVC_NAME_FORMAT.format('*')
+
+
+_mcast_evcs = {}  # device-id -> flow dictionary
+                  #                  |
+                  #                  +-> vlan-id -> evcs
+
+
+class MCastEVC(EVC):
+    """
+    Class to wrap Multicast EVC and EVC-MAP functionality
+    """
+    def __init__(self, flow_entry):
+        super(MCastEVC, self).__init__(flow_entry)
+        self._downstream_flows = {flow_entry.flow_id}     # Matching Downstream Flow IDs
+
+    def __str__(self):
+        return "MCAST-{}: MEN: {}, VLAN: {}".format(self._name, self._men_ports, self._s_tag)
+
+    def _create_name(self):
+        #
+        # TODO: Take into account selection criteria and output to make the name
+        #
+        return EVC_NAME_FORMAT.format(self._flow.vlan_id)
+
+    def _create_evc_map(self, flow_entry):
+        from evc_map import EVCMap
+        flow = FakeUpstreamFlow(flow_entry.flow, flow_entry.handler)
+        return EVCMap.create_ingress_map(flow, self)
+
+    @staticmethod
+    def create(flow_entry):
+        from evc_map import EVCMap
+
+        device_id = flow_entry.device_id
+        if device_id not in _mcast_evcs:
+            _mcast_evcs[device_id] = {}
+
+        evc_table = _mcast_evcs[device_id]
+
+        try:
+            evc = evc_table.get(flow_entry.vlan_id)
+
+            if evc is None:
+                # Create EVC and initial EVC Map
+                evc = MCastEVC(flow_entry)
+                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
+                    pass
+                else:
+                    evc.add_downstream_flows(flow_entry.flow_id)
+
+            fake_flow = FakeUpstreamFlow(flow_entry.flow, flow_entry.handler)
+            evc_map_name = EVCMap.create_evc_map_name(fake_flow)
+
+            if evc_map_name not in evc.evc_map_names:
+                EVCMap.create_ingress_map(fake_flow, evc)
+
+            return evc
+
+        except Exception as e:
+            log.exception('mcast-create', e=e)
+            return None
+
+    @property
+    def flow_entry(self):
+        return self._flow
+
+    @property
+    def downstream_flows(self):
+        return frozenset(self._downstream_flows)
+
+    def add_downstream_flows(self, flow_id):
+        self._downstream_flows.add(flow_id)
+
+    def remove_downstream_flows(self, flow_id):
+        self._downstream_flows.discard(flow_id)
+
+    @inlineCallbacks
+    def remove(self, remove_maps=True):
+        """
+        Remove EVC (and optional associated EVC-MAPs) from hardware
+        :param remove_maps: (boolean)
+        :return: (deferred)
+        """
+        log.info('removing', evc=self, remove_maps=remove_maps)
+
+        device_id = self._handler.device_id
+        flow_id = self._flow.id
+        evc_table = _mcast_evcs.get(device_id)
+
+        if evc_table is None or flow_id not in evc_table:
+            returnValue('NOP')
+
+        # Remove flow reference
+        if self._flow.flow_id in self._downstream_flows:
+            del self._downstream_flows[self._flow.flow_id]
+
+        if len(self._downstream_flows) == 0:
+            # Use base class to clean up
+            returnValue(super(MCastEVC, self).remove(remove_maps=True))
+
+        returnValue('More references')
+
+    @inlineCallbacks
+    def delete(self, delete_maps=True):
+        """
+        Remove from hardware and delete/clean-up EVC Object
+        """
+        log.info('deleting', evc=self, delete_maps=delete_maps)
+
+        try:
+            dl = [self.remove()]
+            if delete_maps:
+                for evc_map in self.evc_maps:
+                    dl.append(evc_map.delete(self))   # TODO: implement bulk-flow procedures
+
+            yield defer.gatherResults(dl, consumeErrors=True)
+
+        except Exception as e:
+            log.exception('removal', e=e)
+
+        self._evc_maps = None
+        f, self._flow = self._flow, None
+        if f is not None and f.handler is not None:
+            f.handler.remove_evc(self)
+
+    def reflow(self, reflow_maps=True):
+        pass    # TODO: Implement or use base class?
+
+    @staticmethod
+    def remove_all(client, regex_=EVC_NAME_REGEX_ALL):
+        """
+        Remove all matching EVCs from hardware
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        pass    # TODO: ???
+
+
+class FakeUpstreamFlow(FlowEntry):
+    def __init__(self, flow, handler):
+        super(FakeUpstreamFlow, self).__init__(flow, handler)
+        self._decode()
+        # Change name that the base class set
+        self._name = self.create_flow_name()
+        self._flow_direction = FlowEntry.FlowDirection.UPSTREAM
+        self.in_port, self.output = self.output, self.in_port
+        self.flow_id = '{}-MCAST'.format(self.vlan_id)
+        self._logical_port = self.vlan_id
+        self.push_vlan_id = self.vlan_id
+        self.vlan_id = None
+        self.signature = None
+        self.inner_vid = None
+        self.pop_vlan = False
+
+    def create_flow_name(self):
+        return 'flow-{}-{}-MCAST'.format(self.device_id, self.vlan_id)
diff --git a/adapters/adtran_common/flow/utility_evc.py b/adapters/adtran_common/flow/utility_evc.py
new file mode 100644
index 0000000..fc7fd0b
--- /dev/null
+++ b/adapters/adtran_common/flow/utility_evc.py
@@ -0,0 +1,158 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# 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 pyvoltha.common.openflow.utils import *
+from evc import EVC
+from twisted.internet import defer
+from twisted.internet.defer import returnValue, inlineCallbacks
+
+log = structlog.get_logger()
+
+EVC_NAME_FORMAT = 'VOLTHA-UTILITY-{}'                   # format(flow.vlan_id)
+EVC_NAME_REGEX_ALL = EVC_NAME_FORMAT.format('*')
+
+
+_utility_evcs = {}  # device-id -> flow dictionary
+                    #                  |
+                    #                  +-> utility-vlan-id -> evcs
+
+
+class UtilityEVC(EVC):
+    """
+    Class to wrap orphan ingress ACLs EVC functionality
+    """
+    def __init__(self, flow_entry):
+        super(UtilityEVC, self).__init__(flow_entry)
+        self._downstream_flows = {flow_entry.flow_id}     # Matching Downstream Flow IDs
+        self.service_evc = True
+
+    def __str__(self):
+        return "VOLTHA-UTILITY-{}: MEN: {}, VLAN: {}".format(self._name, self._men_ports, self._s_tag)
+
+    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 if vlan_id is None else vlan_id)
+
+    @staticmethod
+    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:
+            _utility_evcs[device_id] = dict()
+            evc_table = _utility_evcs[device_id]
+
+        try:
+            evc = evc_table.get(vlan_id)
+
+            if evc is None:
+                # Create EVC and initial EVC Map
+                evc = UtilityEVC(flow_entry)
+
+                # 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
+                else:
+                    evc.add_downstream_flows(flow_entry.flow_id)
+
+            return evc
+
+        except Exception as e:
+            log.exception('utility-create', e=e)
+            return None
+
+    @property
+    def downstream_flows(self):
+        return frozenset(self._downstream_flows)
+
+    def add_downstream_flows(self, flow_id):
+        self._downstream_flows.add(flow_id)
+
+    def remove_downstream_flows(self, flow_id):
+        self._downstream_flows.discard(flow_id)
+
+    def remove(self, remove_maps=True):
+        """
+        Remove EVC (and optional associated EVC-MAPs) from hardware
+        :param remove_maps: (boolean)
+        :return: (deferred)
+        """
+        log.info('removing', evc=self, remove_maps=remove_maps)
+
+        device_id = self._flow.handler.device_id
+        flow_id = self._flow.flow_id
+        evc_table = _utility_evcs.get(device_id)
+
+        if evc_table is None:
+            return defer.succeed('NOP')
+
+        # Remove flow reference
+        if self._flow.flow_id in self._downstream_flows:
+            self._downstream_flows.discard(self._flow.flow_id)
+
+        if len(self._downstream_flows) == 0:
+            # Use base class to clean up
+            return super(UtilityEVC, self).remove(remove_maps=True)
+
+        return defer.succeed('More references')
+
+    @inlineCallbacks
+    def delete(self, delete_maps=True):
+        """
+        Remove from hardware and delete/clean-up EVC Object
+        :return: (deferred)
+        """
+        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:
+                for evc_map in self.evc_maps:
+                    dl.append(evc_map.delete(None))   # TODO: implement bulk-flow procedures
+
+            yield defer.gatherResults(dl, consumeErrors=True)
+
+            self._evc_maps = None
+            f, self._flow = self._flow, None
+            if f is not None and f.handler is not None:
+                f.handler.remove_evc(self)
+
+        except Exception as e:
+            log.exception('removal', e=e)
+
+        returnValue('Done')
+
+    def reflow(self, reflow_maps=True):
+        pass    # TODO: Implement or use base class?
+
+    @staticmethod
+    def remove_all(client, regex_=EVC_NAME_REGEX_ALL):
+        """
+        Remove all matching EVCs from hardware
+        :param client: (ncclient) NETCONF Client to use
+        :param regex_: (String) Regular expression for name matching
+        :return: (deferred)
+        """
+        _utility_evcs.clear()
+        EVC.remove_all(client, regex_)
\ No newline at end of file