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

Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
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