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

Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
diff --git a/adapters/adtran_olt/nni_port.py b/adapters/adtran_olt/nni_port.py
new file mode 100644
index 0000000..af11e9b
--- /dev/null
+++ b/adapters/adtran_olt/nni_port.py
@@ -0,0 +1,457 @@
+#
+# 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 random
+import arrow
+
+import structlog
+import xmltodict
+from adapters.adtran_common.port import AdtnPort
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
+from twisted.python.failure import Failure
+from pyvoltha.protos.common_pb2 import OperStatus, AdminState
+from pyvoltha.protos.device_pb2 import Port
+from pyvoltha.protos.logical_device_pb2 import LogicalPort
+from pyvoltha.protos.openflow_13_pb2 import OFPPF_100GB_FD, OFPPF_FIBER, OFPPS_LIVE, ofp_port
+
+
+class NniPort(AdtnPort):
+    """
+    Northbound network port, often Ethernet-based
+    """
+    def __init__(self, parent, **kwargs):
+        super(NniPort, self).__init__(parent, **kwargs)
+
+        # TODO: Weed out those properties supported by common 'Port' object
+
+        self.log = structlog.get_logger(port_no=kwargs.get('port_no'))
+        self.log.info('creating')
+
+        # ONOS/SEBA wants 'nni-<port>' for port names, OLT NETCONF wants their
+        # name (something like  hundred-gigabit-ethernet 0/1) which is reported
+        # when we enumerated the ports
+        self._physical_port_name = kwargs.get('name', 'nni-{}'.format(self._port_no))
+        self._logical_port_name = 'nni-{}'.format(self._port_no)
+
+        self._logical_port = None
+
+        self.sync_tick = 10.0
+
+        self._stats_tick = 5.0
+        self._stats_deferred = None
+
+        # Local cache of NNI configuration
+        self._ianatype = '<type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>'
+
+        # And optional parameters
+        # TODO: Currently cannot update admin/oper status, so create this enabled and active
+        # self._admin_state = kwargs.pop('admin_state', AdminState.UNKNOWN)
+        # self._oper_status = kwargs.pop('oper_status', OperStatus.UNKNOWN)
+        self._enabled = True
+        self._admin_state = AdminState.ENABLED
+        self._oper_status = OperStatus.ACTIVE
+
+        self._label = self._physical_port_name
+        self._mac_address = kwargs.pop('mac_address', '00:00:00:00:00:00')
+        # TODO: Get with JOT and find out how to pull out MAC Address via NETCONF
+        # TODO: May need to refine capabilities into current, advertised, and peer
+
+        self._ofp_capabilities = kwargs.pop('ofp_capabilities', OFPPF_100GB_FD | OFPPF_FIBER)
+        self._ofp_state = kwargs.pop('ofp_state', OFPPS_LIVE)
+        self._current_speed = kwargs.pop('current_speed', OFPPF_100GB_FD)
+        self._max_speed = kwargs.pop('max_speed', OFPPF_100GB_FD)
+        self._device_port_no = kwargs.pop('device_port_no', self._port_no)
+
+        # Statistics
+        self.rx_dropped = 0
+        self.rx_error_packets = 0
+        self.rx_ucast_packets = 0
+        self.rx_bcast_packets = 0
+        self.rx_mcast_packets = 0
+        self.tx_dropped = 0
+        self.rx_ucast_packets = 0
+        self.tx_bcast_packets = 0
+        self.tx_mcast_packets = 0
+
+    def __str__(self):
+        return "NniPort-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
+                                                                    self._admin_state,
+                                                                    self._oper_status,
+                                                                    self._parent)
+
+    def get_port(self):
+        """
+        Get the VOLTHA PORT object for this port
+        :return: VOLTHA Port object
+        """
+        self.log.debug('get-port-status-update', port=self._port_no,
+                       label=self._label)
+        if self._port is None:
+            self._port = Port(port_no=self._port_no,
+                              label=self._label,
+                              type=Port.ETHERNET_NNI,
+                              admin_state=self._admin_state,
+                              oper_status=self._oper_status)
+
+        if self._port.admin_state != self._admin_state or\
+           self._port.oper_status != self._oper_status:
+
+            self.log.debug('get-port-status-update', admin_state=self._admin_state,
+                           oper_status=self._oper_status)
+            self._port.admin_state = self._admin_state
+            self._port.oper_status = self._oper_status
+
+        return self._port
+
+    @property
+    def iana_type(self):
+        return self._ianatype
+
+    def cancel_deferred(self):
+        super(NniPort, self).cancel_deferred()
+
+        d, self._stats_deferred = self._stats_deferred, None
+        try:
+            if d is not None and d.called:
+                d.cancel()
+        except:
+            pass
+
+    def _update_adapter_agent(self):
+        # adapter_agent add_port also does an update of port status
+        self.log.debug('update-adapter-agent', admin_state=self._admin_state,
+                       oper_status=self._oper_status)
+        self.adapter_agent.add_port(self.olt.device_id, self.get_port())
+
+    def get_logical_port(self):
+        """
+        Get the VOLTHA logical port for this port
+        :return: VOLTHA logical port or None if not supported
+        """
+        def mac_str_to_tuple(mac):
+            """
+            Convert 'xx:xx:xx:xx:xx:xx' MAC address string to a tuple of integers.
+            Example: mac_str_to_tuple('00:01:02:03:04:05') == (0, 1, 2, 3, 4, 5)
+            """
+            return tuple(int(d, 16) for d in mac.split(':'))
+
+        if self._logical_port is None:
+            openflow_port = ofp_port(port_no=self._port_no,
+                                     hw_addr=mac_str_to_tuple(self._mac_address),
+                                     name=self._logical_port_name,
+                                     config=0,
+                                     state=self._ofp_state,
+                                     curr=self._ofp_capabilities,
+                                     advertised=self._ofp_capabilities,
+                                     peer=self._ofp_capabilities,
+                                     curr_speed=self._current_speed,
+                                     max_speed=self._max_speed)
+
+            self._logical_port = LogicalPort(id=self._logical_port_name,
+                                             ofp_port=openflow_port,
+                                             device_id=self._parent.device_id,
+                                             device_port_no=self._device_port_no,
+                                             root_port=True)
+        return self._logical_port
+
+    @inlineCallbacks
+    def finish_startup(self):
+
+        if self.state != AdtnPort.State.INITIAL:
+            returnValue('Done')
+
+        self.log.debug('final-startup')
+        # TODO: Start status polling of NNI interfaces
+        self.deferred = None  # = reactor.callLater(3, self.do_stuff)
+
+        # Begin statistics sync
+        self._stats_deferred = reactor.callLater(self._stats_tick * 2, self._update_statistics)
+
+        try:
+            yield self.set_config('enabled', True)
+
+            super(NniPort, self).finish_startup()
+
+        except Exception as e:
+            self.log.exception('nni-start', e=e)
+            self._oper_status = OperStatus.UNKNOWN
+            self._update_adapter_agent()
+
+        returnValue('Enabled')
+
+    def finish_stop(self):
+
+        # NOTE: Leave all NNI ports active (may have inband management)
+        # TODO: Revisit leaving NNI Ports active on disable
+
+        return self.set_config('enabled', False)
+
+    @inlineCallbacks
+    def reset(self):
+        """
+        Set the NNI Port to a known good state on initial port startup.  Actual
+        NNI 'Start' is done elsewhere
+        """
+        # if self.state != AdtnPort.State.INITIAL:
+        #     self.log.error('reset-ignored', state=self.state)
+        #     returnValue('Ignored')
+
+        self.log.info('resetting', label=self._label)
+
+        # Always enable our NNI ports
+
+        try:
+            results = yield self.set_config('enabled', True)
+            self._admin_state = AdminState.ENABLED
+            self._enabled = True
+            returnValue(results)
+
+        except Exception as e:
+            self.log.exception('reset', e=e)
+            self._admin_state = AdminState.UNKNOWN
+            raise
+
+    @inlineCallbacks
+    def set_config(self, leaf, value):
+        if isinstance(value, bool):
+            value = 'true' if value else 'false'
+
+        config = '<interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \
+                 ' <interface>' + \
+                 '  <name>{}</name>'.format(self._physical_port_name) + \
+                 '  {}'.format(self._ianatype) + \
+                 '  <{}>{}</{}>'.format(leaf, value, leaf) + \
+                 ' </interface>' + \
+                 '</interfaces>'
+        try:
+            results = yield self._parent.netconf_client.edit_config(config)
+            returnValue(results)
+
+        except Exception as e:
+            self.log.exception('set', leaf=leaf, value=value, e=e)
+            raise
+
+    def get_nni_config(self):
+        config = '<filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \
+                 ' <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \
+                 '  <interface>' + \
+                 '   <name>{}</name>'.format(self._physical_port_name) + \
+                 '   <enabled/>' + \
+                 '  </interface>' + \
+                 ' </interfaces>' + \
+                 '</filter>'
+        return self._parent.netconf_client.get(config)
+
+    def get_nni_statistics(self):
+        state = '<filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \
+                 ' <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">' + \
+                 '  <interface>' + \
+                 '   <name>{}</name>'.format(self._physical_port_name) + \
+                 '   <admin-status/>' + \
+                 '   <oper-status/>' + \
+                 '   <statistics/>' + \
+                 '  </interface>' + \
+                 ' </interfaces>' + \
+                 '</filter>'
+        return self._parent.netconf_client.get(state)
+
+    def sync_hardware(self):
+        if self.state == AdtnPort.State.RUNNING or self.state == AdtnPort.State.STOPPED:
+            def read_config(results):
+                #self.log.debug('read-config', results=results)
+                try:
+                    result_dict = xmltodict.parse(results.data_xml)
+                    interfaces = result_dict['data']['interfaces']
+                    if 'if:interface' in interfaces:
+                        entries = interfaces['if:interface']
+                    else:
+                        entries = interfaces['interface']
+
+                    enabled = entries.get('enabled',
+                                          str(not self.enabled).lower()) == 'true'
+
+                    if self.enabled == enabled:
+                        return succeed('in-sync')
+
+                    self.set_config('enabled', self.enabled)
+                    self._oper_status = OperStatus.ACTIVE
+                    self._update_adapter_agent()
+
+                except Exception as e:
+                    self.log.exception('read-config', e=e)
+                    return fail(Failure())
+
+            def failure(reason):
+                self.log.error('hardware-sync-failed', reason=reason)
+
+            def reschedule(_):
+                delay = self.sync_tick
+                delay += random.uniform(-delay / 10, delay / 10)
+                self.sync_deferred = reactor.callLater(delay, self.sync_hardware)
+
+            self.sync_deferred = self.get_nni_config()
+            self.sync_deferred.addCallbacks(read_config, failure)
+            self.sync_deferred.addBoth(reschedule)
+
+    def _decode_nni_statistics(self, entry):
+        # admin_status = entry.get('admin-status')
+        # oper_status = entry.get('oper-status')
+        # admin_status = entry.get('admin-status')
+        # phys_address = entry.get('phys-address')
+
+        stats = entry.get('statistics')
+        if stats is not None:
+            self.timestamp = arrow.utcnow().float_timestamp
+            self.rx_bytes = int(stats.get('in-octets', 0))
+            self.rx_ucast_packets = int(stats.get('in-unicast-pkts', 0))
+            self.rx_bcast_packets = int(stats.get('in-broadcast-pkts', 0))
+            self.rx_mcast_packets = int(stats.get('in-multicast-pkts', 0))
+            self.rx_error_packets = int(stats.get('in-errors', 0)) + int(stats.get('in-discards', 0))
+
+            self.tx_bytes = int(stats.get('out-octets', 0))
+            self.tx_ucast_packets = int(stats.get('out-unicast-pkts', 0))
+            self.tx_bcast_packets = int(stats.get('out-broadcast-pkts', 0))
+            self.tx_mcasy_packets = int(stats.get('out-multicast-pkts', 0))
+            self.tx_error_packets = int(stats.get('out-errors', 0)) + int(stats.get('out-discards', 0))
+
+            self.rx_packets = self.rx_ucast_packets + self.rx_mcast_packets + self.rx_bcast_packets
+            self.tx_packets = self.tx_ucast_packets + self.tx_mcast_packets + self.tx_bcast_packets
+            # No support for rx_crc_errors or bip_errors
+
+    def _update_statistics(self):
+        if self.state == AdtnPort.State.RUNNING:
+            def read_state(results):
+                # self.log.debug('read-state', results=results)
+                try:
+                    result_dict = xmltodict.parse(results.data_xml)
+                    entry = result_dict['data']['interfaces-state']['interface']
+                    self._decode_nni_statistics(entry)
+                    return succeed('done')
+
+                except Exception as e:
+                    self.log.exception('read-state', e=e)
+                    return fail(Failure())
+
+            def failure(reason):
+                self.log.error('update-stats-failed', reason=reason)
+
+            def reschedule(_):
+                delay = self._stats_tick
+                delay += random.uniform(-delay / 10, delay / 10)
+                self._stats_deferred = reactor.callLater(delay, self._update_statistics)
+
+            try:
+                self._stats_deferred = self.get_nni_statistics()
+                self._stats_deferred.addCallbacks(read_state, failure)
+                self._stats_deferred.addBoth(reschedule)
+
+            except Exception as e:
+                self.log.exception('nni-sync', port=self.name, e=e)
+                self._stats_deferred = reactor.callLater(self._stats_tick, self._update_statistics)
+
+
+class MockNniPort(NniPort):
+    """
+    A class similar to the 'Port' class in the VOLTHA but for a non-existent (virtual OLT)
+
+    TODO: Merge this with the Port class or cleanup where possible
+          so we do not duplicate fields/properties/methods
+    """
+
+    def __init__(self, parent, **kwargs):
+        super(MockNniPort, self).__init__(parent, **kwargs)
+
+    def __str__(self):
+        return "NniPort-mock-{}: Admin: {}, Oper: {}, parent: {}".format(self._port_no,
+                                                                         self._admin_state,
+                                                                         self._oper_status,
+                                                                         self._parent)
+
+    @staticmethod
+    def get_nni_port_state_results():
+        from ncclient.operations.retrieve import GetReply
+        raw = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
+        xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
+        message-id="urn:uuid:59e71979-01bb-462f-b17a-b3a45e1889ac">
+          <data>
+            <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+              <interface><name>hundred-gigabit-ethernet 0/1</name></interface>
+            </interfaces-state>
+          </data>
+        </rpc-reply>
+        """
+        return GetReply(raw)
+
+    @staticmethod
+    def get_pon_port_state_results():
+        from ncclient.operations.retrieve import GetReply
+        raw = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
+        xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
+        message-id="urn:uuid:59e71979-01bb-462f-b17a-b3a45e1889ac">
+          <data>
+            <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+              <interface><name>XPON 0/1</name></interface>
+              <interface><name>XPON 0/2</name></interface>
+              <interface><name>XPON 0/3</name></interface>
+              <interface><name>XPON 0/4</name></interface>
+              <interface><name>XPON 0/5</name></interface>
+              <interface><name>XPON 0/6</name></interface>
+              <interface><name>XPON 0/7</name></interface>
+              <interface><name>XPON 0/8</name></interface>
+              <interface><name>XPON 0/9</name></interface>
+              <interface><name>XPON 0/10</name></interface>
+              <interface><name>XPON 0/11</name></interface>
+              <interface><name>XPON 0/12</name></interface>
+              <interface><name>XPON 0/13</name></interface>
+              <interface><name>XPON 0/14</name></interface>
+              <interface><name>XPON 0/15</name></interface>
+              <interface><name>XPON 0/16</name></interface>
+            </interfaces-state>
+          </data>
+        </rpc-reply>
+        """
+        return GetReply(raw)
+
+    def reset(self):
+        """
+        Set the NNI Port to a known good state on initial port startup.  Actual
+        NNI 'Start' is done elsewhere
+        """
+        if self.state != AdtnPort.State.INITIAL:
+            self.log.error('reset-ignored', state=self.state)
+            return fail()
+
+        self.log.info('resetting', label=self._label)
+
+        # Always enable our NNI ports
+
+        self._enabled = True
+        self._admin_state = AdminState.ENABLED
+        return succeed('Enabled')
+
+    def set_config(self, leaf, value):
+
+        if leaf == 'enabled':
+            self._enabled = value
+        else:
+            raise NotImplemented("Leaf '{}' is not supported".format(leaf))
+
+        return succeed('Success')