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

Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
diff --git a/adapters/adtran_common/net/adtran_netconf.py b/adapters/adtran_common/net/adtran_netconf.py
new file mode 100644
index 0000000..4e39a6a
--- /dev/null
+++ b/adapters/adtran_common/net/adtran_netconf.py
@@ -0,0 +1,373 @@
+# 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 lxml import etree
+from ncclient import manager
+from ncclient.operations import RPCError
+from ncclient.transport.errors import SSHError
+from twisted.internet import defer, threads
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+log = structlog.get_logger('ncclient')
+
+ADTRAN_NS = 'http://www.adtran.com/ns/yang'
+
+
+def adtran_module_url(module):
+    return '{}/{}'.format(ADTRAN_NS, module)
+
+
+def phys_entities_rpc():
+    return """
+    <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+      <physical-entities-state xmlns="{}">
+        <physical-entity/>
+      </physical-entities-state>
+    </filter>
+    """.format(adtran_module_url('adtran-physical-entities'))
+
+
+class AdtranNetconfClient(object):
+    """
+    Performs NETCONF requests
+    """
+    def __init__(self, host_ip, port=830, username='', password='', timeout=10):
+        self._ip = host_ip
+        self._port = port
+        self._username = username
+        self._password = password
+        self._timeout = timeout
+        self._session = None
+
+    def __str__(self):
+        return "AdtranNetconfClient {}@{}:{}".format(self._username, self._ip, self._port)
+
+    @property
+    def capabilities(self):
+        """
+        Get the server's NETCONF capabilities
+
+        :return: (ncclient.capabilities.Capabilities) object representing the server's capabilities.
+        """
+        return self._session.server_capabilities if self._session else None
+
+    @property
+    def connected(self):
+        """
+        Is this client connected to a NETCONF server
+        :return: (boolean) True if connected
+        """
+        return self._session is not None and self._session.connected
+
+    def connect(self, connect_timeout=None):
+        """
+        Connect to the NETCONF server
+
+          o To disable attempting publickey authentication altogether, call with
+            allow_agent and look_for_keys as False.
+
+          o hostkey_verify enables hostkey verification from ~/.ssh/known_hosts
+
+        :return: (deferred) Deferred request
+        """
+        timeout = connect_timeout or self._timeout
+
+        return threads.deferToThread(self._do_connect, timeout)
+
+    def _do_connect(self, timeout):
+        try:
+            self._session = manager.connect(host=self._ip,
+                                            port=self._port,
+                                            username=self._username,
+                                            password=self._password,
+                                            allow_agent=False,
+                                            look_for_keys=False,
+                                            hostkey_verify=False,
+                                            timeout=timeout)
+
+        except SSHError as e:
+            # Log and rethrow exception so any errBack is called
+            log.warn('SSHError-during-connect', e=e)
+            raise e
+
+        except Exception as e:
+            # Log and rethrow exception so any errBack is called
+            log.exception('Connect-failed: {}', e=e)
+            raise e
+
+        # If debug logging is enabled, decrease the level, DEBUG is a significant
+        # performance hit during response XML decode
+
+        if log.isEnabledFor('DEBUG'):
+            log.setLevel('INFO')
+
+        # TODO: ncclient also supports RaiseMode:NONE to limit exceptions.  To set use:
+        #
+        #  self._session.raise_mode = RaiseMode:NONE
+        #
+        # and the when you get a response back, you can check   'response.ok' to
+        # see if it is 'True' if it is not, you can enumerate the 'response.errors'
+        # list for more information
+
+        return self._session
+
+    def close(self):
+        """
+        Close the connection to the NETCONF server
+        :return:  (deferred) Deferred request
+        """
+        s, self._session = self._session, None
+
+        if s is None or not s.connected:
+            return defer.returnValue(True)
+
+        return threads.deferToThread(self._do_close, s)
+
+    def _do_close(self, old_session):
+        return old_session.close_session()
+
+    @inlineCallbacks
+    def _reconnect(self):
+        try:
+            yield self.close()
+        except:
+            pass
+
+        try:
+            yield self.connect()
+        except:
+            pass
+
+    def get_config(self, source='running'):
+        """
+        Get the configuration from the specified source
+
+        :param source: (string) Configuration source, 'running', 'candidate', ...
+
+        :return: (deferred) Deferred request that wraps the GetReply class
+        """
+        if not self._session:
+            raise NotImplemented('No SSH Session')
+
+        if not self._session.connected:
+            self._reconnect()
+
+        return threads.deferToThread(self._do_get_config, source)
+
+    def _do_get_config(self, source):
+        """
+        Get the configuration from the specified source
+
+        :param source: (string) Configuration source, 'running', 'candidate', ...
+
+        :return: (GetReply) The configuration.
+        """
+        return self._session.get_config(source)
+
+    def get(self, payload):
+        """
+        Get the requested data from the server
+
+        :param payload: Payload/filter
+        :return: (deferred) for GetReply
+        """
+        log.debug('get', filter=payload)
+
+        if not self._session:
+            raise NotImplemented('No SSH Session')
+
+        if not self._session.connected:
+            self._reconnect()
+
+        return threads.deferToThread(self._do_get, payload)
+
+    def _do_get(self, payload):
+        """
+        Get the requested data from the server
+
+        :param payload: Payload/filter
+        :return: (GetReply) response
+        """
+        try:
+            log.debug('get', payload=payload)
+            response = self._session.get(payload)
+            # To get XML, use response.xml
+            log.debug('response', response=response)
+
+        except RPCError as e:
+            log.exception('get', e=e)
+            raise
+
+        return response
+
+    def lock(self, source, lock_timeout):
+        """
+        Lock the configuration system
+        :return: (deferred) for RpcReply
+        """
+        log.info('lock', source=source, timeout=lock_timeout)
+
+        if not self._session or not self._session.connected:
+            raise NotImplemented('TODO: Support auto-connect if needed')
+
+        return threads.deferToThread(self._do_lock, source, lock_timeout)
+
+    def _do_lock(self, source, lock_timeout):
+        """
+        Lock the configuration system
+        """
+        try:
+            response = self._session.lock(source, timeout=lock_timeout)
+            # To get XML, use response.xml
+
+        except RPCError as e:
+            log.exception('lock', e=e)
+            raise
+
+        return response
+
+    def unlock(self, source):
+        """
+        Get the requested data from the server
+        :param source: RPC request
+
+        :return: (deferred) for RpcReply
+        """
+        log.info('unlock', source=source)
+
+        if not self._session or not self._session.connected:
+            raise NotImplemented('TODO: Support auto-connect if needed')
+
+        return threads.deferToThread(self._do_unlock, source)
+
+    def _do_unlock(self, source):
+        """
+        Lock the configuration system
+        """
+        try:
+            response = self._session.unlock(source)
+            # To get XML, use response.xml
+
+        except RPCError as e:
+            log.exception('unlock', e=e)
+            raise
+
+        return response
+
+    @inlineCallbacks
+    def edit_config(self, config, target='running', default_operation='none',
+                    test_option=None, error_option=None, ignore_delete_error=False):
+        """
+        Loads all or part of the specified config to the target configuration datastore
+        with the ability to lock the datastore during the edit.
+
+        :param config is the configuration, which must be rooted in the config element.
+                      It can be specified either as a string or an Element.format="xml"
+        :param target is the name of the configuration datastore being edited
+        :param default_operation if specified must be one of { 'merge', 'replace', or 'none' }
+        :param test_option if specified must be one of { 'test_then_set', 'set' }
+        :param error_option if specified must be one of { 'stop-on-error',
+                            'continue-on-error', 'rollback-on-error' } The
+                            'rollback-on-error' error_option depends on the
+                            :rollback-on-error capability.
+        :param ignore_delete_error: (bool) For some startup deletes/clean-ups, we do a
+                                    delete high up in the config to get whole lists. If
+                                    these lists are empty, this helps suppress any error
+                                    message from NETConf on failure to delete an empty list
+
+        :return: (deferred) for RpcReply
+        """
+        if not self._session:
+            raise NotImplemented('No SSH Session')
+
+        if not self._session.connected:
+            try:
+                yield self._reconnect()
+
+            except Exception as e:
+                log.exception('edit-config-connect', e=e)
+
+        try:
+            if config[:7] != '<config':
+                config = '<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0"' + \
+                         ' xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \
+                         config + '</config>'
+
+            log.debug('netconf-request', config=config, target=target,
+                      default_operation=default_operation)
+
+            rpc_reply = yield threads.deferToThread(self._do_edit_config, target,
+                                                    config, default_operation,
+                                                    test_option, error_option)
+        except Exception as e:
+            if ignore_delete_error and 'operation="delete"' in config.lower():
+                returnValue('ignoring-delete-error')
+            log.exception('edit_config', e=e, config=config, target=target)
+            raise
+
+        returnValue(rpc_reply)
+
+    def _do_edit_config(self, target, config, default_operation, test_option, error_option,
+                        ignore_delete_error=False):
+        """
+        Perform actual edit-config operation
+        """
+        try:
+            log.debug('edit-config', target=target, config=config)
+            
+            response = self._session.edit_config(target=target, config=config
+                                                 # TODO: Support additional options later
+                                                 # ,default_operation=default_operation,
+                                                 # test_option=test_option,
+                                                 # error_option=error_option
+                                                 )
+
+            log.debug('netconf-response', response=response)
+            # To get XML, use response.xml
+            # To check status, use response.ok  (boolean)
+
+        except RPCError as e:
+            if not ignore_delete_error or 'operation="delete"' not in config.lower():
+                log.exception('do_edit_config', e=e, config=config, target=target)
+            raise
+
+        return response
+
+    def rpc(self, rpc_string):
+        """
+        Custom RPC request
+        :param rpc_string: (string) RPC request
+        :return: (deferred) for GetReply
+        """
+        log.debug('rpc', rpc=rpc_string)
+
+        if not self._session:
+            raise NotImplemented('No SSH Session')
+
+        if not self._session.connected:
+            self._reconnect()
+
+        return threads.deferToThread(self._do_rpc, rpc_string)
+
+    def _do_rpc(self, rpc_string):
+        try:
+            response = self._session.dispatch(etree.fromstring(rpc_string))
+            # To get XML, use response.xml
+
+        except RPCError as e:
+            log.exception('rpc', e=e)
+            raise
+
+        return response