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