This commit consists of:
1) Add session management to netconf
2) Modularize the rpc call
3) Improve the error handling
4) Small bug fixes
Change-Id: I023edb76e3743b633ac87be4967d656e09e2b970
diff --git a/netconf/capabilities.py b/netconf/capabilities.py
new file mode 100755
index 0000000..86873f8
--- /dev/null
+++ b/netconf/capabilities.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 constants import Constants as C
+
+class Capabilities:
+
+ def __init__(self):
+ self.server_caps = (C.NETCONF_BASE_10, C.NETCONF_BASE_11)
+ self.client_caps = set()
+
+ def add_client_capability(self, cap):
+ self.client_caps.add(cap)
+
diff --git a/netconf/constants.py b/netconf/constants.py
new file mode 100644
index 0000000..7a503bc
--- /dev/null
+++ b/netconf/constants.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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.
+#
+
+class Constants:
+
+ SSH_SUBSYSTEM = "netconf"
+
+ # Secure credentials directories
+ # TODO: In a production environment these locations require better
+ # protection. For now the user_passwords file is just a plain text file.
+ KEYS_DIRECTORY = 'security/keys'
+ CERTS_DIRECTORY = 'security/certificates'
+ CLIENT_CRED_DIRECTORY = 'security/client_credentials'
+
+ # Datastores
+ RUNNING = "running"
+ CANDIDATE = "candidate"
+ STARTUP = "startup"
+
+ # RPC - base netconf
+ GET = "get"
+ GET_CONFIG = "get-config"
+ COPY_CONFIG = "copy-config"
+ EDIT_CONFIG = "edit-config"
+ DELETE_CONFIG = "delete-config"
+ LOCK = "lock"
+ UNLOCK = "unlock"
+ CLOSE_SESSION = "close-session"
+ KILL_SESSION = "kill-session"
+
+ # Operations
+ OPERATION = "operation"
+ DEFAULT_OPERATION = "default-operation"
+ MERGE = "merge"
+ REPLACE = "replace"
+ CREATE = "create"
+ DELETE = "delete"
+ NONE = "none"
+
+ # Netconf namespaces
+ NETCONF_BASE_10 = "urn:ietf:params:netconf:base:1.0"
+ NETCONF_BASE_11 = "urn:ietf:params:netconf:base:1.1"
+
+ # XML
+ XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>"""
+
+ # Capability xpath
+ CAPABILITY_XPATH = "//nc:hello/nc:capabilities/nc:capability"
+ RPC_XPATH = "/nc:rpc"
+
+ NC_SOURCE="nc:source"
+ SOURCE = "source"
+ TARGET = "target"
+ CONFIG = "config"
+
+
+ TEST_OPTION = "test-option"
+ TEST_THEN_SET = "test-then-set"
+ SET = "set"
+
+ ERROR_OPTION = "error-option"
+ STOP_ON_ERROR = "stop-on-error"
+ CONTINUE_ON_ERROR = "continue-on-error"
+ ROLLBACK_ON_ERROR = "rollback-on-error"
+
+ #tags
+ NC = "nc"
+ RPC = "rpc"
+ RPC_REPLY = "rpc-reply"
+ RPC_ERROR = "rpc-error"
+ CAPABILITY = "capability"
+ CAPABILITIES = "capabilities"
+ HELLO = "hello"
+ URL = "url"
+ NC_FILTER="nc:filter"
+ FILTER = "filter"
+ SUBTREE = "subtree"
+ XPATH = "xpath"
+ OK = "ok"
+ SESSION_ID = "session-id"
+ MESSAGE_ID = "message-id"
+ XMLNS = "xmlns"
+ DELIMITER = "]]>]]>"
diff --git a/netconf/error.py b/netconf/error.py
deleted file mode 100644
index 7a33f69..0000000
--- a/netconf/error.py
+++ /dev/null
@@ -1,193 +0,0 @@
-# -*- coding: utf-8 -*-#
-#
-# February 19 2015, Christian Hopps <chopps@gmail.com>
-#
-# Copyright (c) 2015, Deutsche Telekom AG
-#
-# 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 __future__ import absolute_import, division, unicode_literals, \
- print_function, nested_scopes
-from lxml import etree
-from netconf import NSMAP
-
-
-class NetconfException(Exception):
- pass
-
-
-class ChannelClosed(NetconfException):
- pass
-
-
-class FramingError(NetconfException):
- pass
-
-
-class SessionError(NetconfException):
- pass
-
-
-class RPCError(NetconfException):
- def __init__(self, output, tree, error):
- super(RPCError, self).__init__(output)
- self.tree = tree
- self.error = error
-
- def _get_error_val(self, value):
- try:
- return self.error.xpath("nc:" + value, namespaces=NSMAP)[0].text
- except IndexError:
- return None
-
- def get_error_tag(self):
- return self._get_error_val("error-tag")
-
- def get_error_type(self):
- return self._get_error_val("error-type")
-
- def get_error_info(self):
- return self._get_error_val("error-info")
-
- def get_error_severity(self):
- return self._get_error_val("error-severity")
-
-
-# RFC6241
-
-# error-type
-RPCERR_TYPE_TRANSPORT = 0
-RPCERR_TYPE_RPC = 1
-RPCERR_TYPE_PROTOCOL = 2
-RPCERR_TYPE_APPLICATION = 3
-RPCERR_TYPE_ENUM = {
- RPCERR_TYPE_TRANSPORT: "transport",
- RPCERR_TYPE_RPC: "rpc",
- RPCERR_TYPE_PROTOCOL: "protocol",
- RPCERR_TYPE_APPLICATION: "application"
-}
-
-# error-tag
-RPCERR_TAG_IN_USE = "in-use"
-RPCERR_TAG_INVALID_VALUE = "invalid-value"
-RPCERR_TAG_TOO_BIG = "too-big"
-RPCERR_TAG_MISSING_ATTRIBUTE = "missing-attribute"
-RPCERR_TAG_BAD_ATTRIBUTE = "bad-attribute"
-RPCERR_TAG_UNKNOWN_ATTRIBUTE = "unknown-attribute"
-RPCERR_TAG_MISSING_ELEMENT = "missing-element"
-RPCERR_TAG_BAD_ELEMENT = "bad-element"
-RPCERR_TAG_UNKNOWN_ELEMENT = "unknown-element"
-RPCERR_TAG_UNKNOWN_NAMESPACE = "unknown-namespace"
-RPCERR_TAG_ACCESS_DENIED = "access-denied"
-RPCERR_TAG_LOCK_DENIED = "lock-denied"
-RPCERR_TAG_RESOURCE_DENIED = "resource-denied"
-RPCERR_TAG_ROLLBACK_FAILED = "rollback-failed"
-RPCERR_TAG_DATA_EXISTS = "data-exists"
-RPCERR_TAG_DATA_MISSING = "data-missing"
-RPCERR_TAG_OPERATION_NOT_SUPPORTED = "operation-not-supported"
-RPCERR_TAG_OPERATION_FAILED = "operation-failed"
-RPCERR_TAG_MALFORMED_MESSAGE = "malformed-message"
-
-
-# error-app-tag
-# error-path # xpath associated with error.
-# error-message # human readable message describiing error
-# error-info
-
-
-class RPCServerError(NetconfException):
- def __init__(self, origmsg, etype, tag, **kwargs):
- # Add attrib and nsmap from original message.
- self.reply = etree.Element("rpc-reply", attrib=origmsg.attrib,
- nsmap=origmsg.nsmap)
-
- rpcerr = etree.SubElement(self.reply, "rpc-error")
-
- # We require a type, tag, and severity assuming error for severity.
- if etype in RPCERR_TYPE_ENUM:
- etype = RPCERR_TYPE_ENUM[etype]
- etree.SubElement(rpcerr, "error-type").text = str(etype)
-
- etree.SubElement(rpcerr, "error-tag").text = tag
-
- if "severity" not in kwargs:
- etree.SubElement(rpcerr, "error-severity").text = "error"
-
- # Now convert any other arguments to xml
- for key, value in kwargs.items():
- key = key.replace('_', '-')
- etree.SubElement(rpcerr, "error-{}".format(key)).text = str(value)
-
- # This sort of sucks for humans
- super(RPCServerError, self).__init__(self.get_reply_msg())
-
- def get_reply_msg(self):
- return etree.tounicode(self.reply)
-
-
-class RPCSvrErrBadMsg(RPCServerError):
- """If the server raises this exception the and netconf 1.0 is in use, the session will be closed"""
-
- def __init__(self, origmsg):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_RPC,
- RPCERR_TAG_MALFORMED_MESSAGE)
-
-
-class RPCSvrInvalidValue(RPCServerError):
- def __init__(self, origmsg, **kwargs):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_RPC,
- RPCERR_TAG_INVALID_VALUE, **kwargs)
-
-
-class RPCSvrMissingElement(RPCServerError):
- def __init__(self, origmsg, tag, **kwargs):
- try:
- # Old API had this as an element...
- tag = tag.tag
- except AttributeError:
- pass
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_RPC,
- RPCERR_TAG_MISSING_ELEMENT, info=tag, **kwargs)
-
-
-class RPCSvrBadElement(RPCServerError):
- def __init__(self, origmsg, element, **kwargs):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_RPC,
- RPCERR_TAG_BAD_ELEMENT, info=element.tag,
- **kwargs)
-
-
-class RPCSvrUnknownElement(RPCServerError):
- def __init__(self, origmsg, element, **kwargs):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_RPC,
- RPCERR_TAG_UNKNOWN_ELEMENT, info=element.tag,
- **kwargs)
-
-
-class RPCSvrErrNotImpl(RPCServerError):
- def __init__(self, origmsg, **kwargs):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_PROTOCOL,
- RPCERR_TAG_OPERATION_NOT_SUPPORTED, **kwargs)
-
-
-class RPCSvrException(RPCServerError):
- def __init__(self, origmsg, exception, **kwargs):
- RPCServerError.__init__(self, origmsg, RPCERR_TYPE_PROTOCOL,
- RPCERR_TAG_OPERATION_FAILED,
- info=str(exception), **kwargs)
-
-
-__author__ = 'Christian Hopps'
-__date__ = 'February 19 2015'
-__version__ = '1.0'
-__docformat__ = "restructuredtext en"
diff --git a/netconf/grpc_client.py b/netconf/grpc_client.py
index ca6fa42..ebb74eb 100644
--- a/netconf/grpc_client.py
+++ b/netconf/grpc_client.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
#
# Copyright 2016 the original author or authors.
#
@@ -13,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
"""
The gRPC client layer for the Netconf agent
"""
diff --git a/netconf/nc_common/__init__.py b/netconf/nc_common/__init__.py
new file mode 100644
index 0000000..7398217
--- /dev/null
+++ b/netconf/nc_common/__init__.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-#
+#
+# December 23 2014, Christian Hopps <chopps@gmail.com>
+#
+# Copyright (c) 2015, Deutsche Telekom AG
+#
+# 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 __future__ import absolute_import, division, unicode_literals, print_function, nested_scopes
+from lxml.etree import register_namespace
+
+MAXSSHBUF = 16 * 1024
+NSMAP = { }
+
+
+def nsmap_add (prefix, namespace):
+ "Add a prefix namespace mapping to the modules mapping dictionary"
+ NSMAP[prefix] = namespace
+ register_namespace(prefix, namespace)
+
+
+def nsmap_update (nsdict):
+ "Add a dicitonary of prefx namespace mappings to the modules mapping dictionary"
+ NSMAP.update(nsdict)
+ for key, val in nsdict.items():
+ register_namespace(key, val)
+
+
+def qmap (key):
+ return "{" + NSMAP[key] + "}"
+
+
+# Add base spec namespace
+nsmap_add('nc', "urn:ietf:params:xml:ns:netconf:base:1.0")
diff --git a/netconf/nc_common/error.py b/netconf/nc_common/error.py
new file mode 100644
index 0000000..802cbe9
--- /dev/null
+++ b/netconf/nc_common/error.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Adapted from https://github.com/choppsv1/netconf/error.py
+#
+# 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 netconf.constants import Constants as C
+from lxml import etree
+
+class Error(Exception):
+
+ ### Tag ###
+ IN_USE = "in-use"
+ INVALID_VALUE = "invalid-value"
+ TOO_BIG = "too-big"
+ MISSING_ATTRIBUTE = "missing-attribute"
+ BAD_ATTRIBUTE = "bad-attribute"
+ UNKNOWN_ATTRIBUTE = "unknown-attribute"
+ MISSING_ELEMENT = "missing-element"
+ BAD_ELEMENT = "bad-element"
+ UNKNOWN_ELEMENT = "unknown-element"
+ UNKNOWN_NAMESPACE = "unknown-namespace"
+ ACCESS_DENIED = "access-denied"
+ LOCK_DENIED = "lock-denied"
+ RESOURCE_DENIED = "resource-denied"
+ ROLLBACK_FAILED = "rollback-failed"
+ DATA_EXISTS = "data-exists"
+ DATA_MISSING = "data-missing"
+ OPERATION_NOT_SUPPORTED = "operation-not-supported"
+ OPERATION_FAILED = "operation-failed"
+ MALFORMED_MESSAGE = "malformed-message"
+
+ ### Error-type ###
+ TRANSPORT = 0
+ RPC = 1
+ PROTOCOL = 2
+ APPLICATION = 3
+ ERROR_TYPE_ENUM = {
+ TRANSPORT: "transport",
+ RPC: "rpc",
+ PROTOCOL: "protocol",
+ APPLICATION: "application"
+ }
+
+ ### Severity ###
+ ERROR = "error"
+ WARNING = "warning"
+
+ ### Error-info ###
+ BAD_ATTRIBUTE_INFO = "bad-attribute"
+ BAD_ELEMENT_INFO = "bad-element"
+ SESSION_ID_INFO = "session-id"
+ OK_ELEMENT_INFO = "ok-element"
+ ERR_ELEMENT_INFO = "err-element"
+ NOOP_ELEMENT_INFO = "noop-element"
+
+ ### XML Error tags ###
+ XML_ERROR_TYPE = "error-type"
+ XML_ERROR_TAG = "error-tag"
+ XML_ERROR_SEV = "error-severity"
+
+
+ # def __init__(self, replynode=None, error_type=None, error_tag=None,
+ # error_severity=None, error_tag_app=None, error_path=None,
+ # error_message=None):
+ #
+ # self.replynode = replynode
+ # self.error_type = error_type
+ # self.error_tag = error_tag
+ # self.error_severity = error_severity
+ # self.error_tag_app = error_tag_app
+ # self.error_path = error_path
+ # self.error_message = error_message
+ # self.error_info = []
+
+class ChannelClosed(Error):
+ pass
+
+class FramingError(Error):
+ pass
+
+class SessionError(Error):
+ pass
+
+
+class RPCError(Error):
+ def __init__(self, origmsg, error_type, error_tag, **kwargs):
+ # Create the rpc reply
+ # Append original attributes and namespace
+ self.reply = etree.Element(C.RPC_REPLY,
+ attrib=origmsg.attrib,
+ nsmap=origmsg.nsmap)
+
+ rpcerr = etree.SubElement(self.reply, C.RPC_ERROR)
+
+ if error_type in Error.ERROR_TYPE_ENUM:
+ error_type = Error.ERROR_TYPE_ENUM[error_type]
+ else:
+ error_type = Error.ERROR_TYPE_ENUM[Error.RPC]
+
+ etree.SubElement(rpcerr, Error.XML_ERROR_TYPE).text = str(error_type)
+
+ etree.SubElement(rpcerr, Error.XML_ERROR_TAG).text = error_tag
+
+ if "severity" not in kwargs:
+ etree.SubElement(rpcerr, Error.XML_ERROR_SEV).text = "error"
+
+ # Convert all remaining arguments to xml
+ for key, value in kwargs.items():
+ key = key.replace('_', '-')
+ etree.SubElement(rpcerr, "error-{}".format(key)).text = str(value)
+
+ # super(RPCError, self).__init__(self.get_xml_reply())
+
+ def get_xml_reply(self):
+ return etree.tounicode(self.reply)
+
+
+class BadMsg(RPCError):
+ def __init__(self, origmsg):
+ RPCError.__init__(self,
+ origmsg,
+ Error.RPC,
+ Error.MALFORMED_MESSAGE)
+
+
+class InvalidValue(Error):
+ def __init__(self, origmsg, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.RPC,
+ Error.INVALID_VALUE,
+ **kwargs)
+
+
+class MissingElement(RPCError):
+ def __init__(self, origmsg, tag, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.RPC,
+ Error.MISSING_ELEMENT,
+ info=tag,
+ **kwargs)
+
+
+class BadElement(RPCError):
+ def __init__(self, origmsg, element, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.RPC,
+ Error.BAD_ELEMENT,
+ info=element.tag,
+ **kwargs)
+
+
+class UnknownElement(RPCError):
+ def __init__(self, origmsg, element, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.RPC,
+ Error.UNKNOWN_ELEMENT,
+ info=element.tag,
+ **kwargs)
+
+
+class NotImpl(RPCError):
+ def __init__(self, origmsg, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.PROTOCOL,
+ Error.OPERATION_NOT_SUPPORTED,
+ **kwargs)
+
+
+class ServerException(RPCError):
+ def __init__(self, origmsg, exception, **kwargs):
+ RPCError.__init__(self,
+ origmsg,
+ Error.PROTOCOL,
+ Error.OPERATION_FAILED,
+ info=str(exception),
+ **kwargs)
+
diff --git a/netconf/nc_protocol_handler.py b/netconf/nc_protocol_handler.py
deleted file mode 100644
index 2b1fb65..0000000
--- a/netconf/nc_protocol_handler.py
+++ /dev/null
@@ -1,359 +0,0 @@
-#
-# Copyright 2016 the original author or authors.
-#
-# Code adapted from https://github.com/choppsv1/netconf
-#
-# 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 __future__ import absolute_import, division, unicode_literals, \
- print_function, nested_scopes
-import structlog
-import io
-from lxml import etree
-from lxml.builder import E
-import netconf.error as ncerror
-from netconf import NSMAP, qmap
-from utils import elm
-from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
-
-log = structlog.get_logger()
-
-class NetconfProtocolError(Exception): pass
-
-
-NC_BASE_10 = "urn:ietf:params:netconf:base:1.0"
-NC_BASE_11 = "urn:ietf:params:netconf:base:1.1"
-XML_HEADER = """<?xml version="1.0" encoding="utf-8"?>"""
-
-
-class NetconfMethods(object):
- """This is an abstract class that is used to document the server methods functionality
-
- The server return not-implemented if the method is not found in the methods object,
- so feel free to use duck-typing here (i.e., no need to inherit)
- """
-
- def nc_append_capabilities(self, capabilities): # pylint: disable=W0613
- """The server should append any capabilities it supports to capabilities"""
- return
-
- def rpc_get(self, session, rpc, filter_or_none): # pylint: disable=W0613
- """Passed the filter element or None if not present"""
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- def rpc_get_config(self, session, rpc, source_elm,
- filter_or_none): # pylint: disable=W0613
- """Passed the source element"""
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- # TODO: The API WILL CHANGE consider unfinished
- def rpc_copy_config(self, unused_session, rpc, *unused_params):
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- # TODO: The API WILL CHANGE consider unfinished
- def rpc_delete_config(self, unused_session, rpc, *unused_params):
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- # TODO: The API WILL CHANGE consider unfinished
- def rpc_edit_config(self, unused_session, rpc, *unused_params):
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- # TODO: The API WILL CHANGE consider unfinished
- def rpc_lock(self, unused_session, rpc, *unused_params):
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- # TODO: The API WILL CHANGE consider unfinished
- def rpc_unlock(self, unused_session, rpc, *unused_params):
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
-
-class NetconfMethods(NetconfMethods):
- def rpc_get(self, unused_session, rpc, *unused_params):
- return etree.Element("ok")
-
- def rpc_get_config(self, unused_session, rpc, *unused_params):
- return etree.Element("ok")
-
- def rpc_namespaced(self, unused_session, rpc, *unused_params):
- return etree.Element("ok")
-
-
-class NetconfProtocolHandler:
- def __init__(self, nc_server, nc_conn, grpc_stub):
- self.started = True
- self.conn = nc_conn
- self.nc_server = nc_server
- self.grpc_stub = grpc_stub
- self.methods = NetconfMethods()
- self.new_framing = False
- self.capabilities = set()
- self.session_id = 1
- self.session_open = False
- self.exiting = False
- self.connected = Deferred()
- self.connected.addCallback(self.nc_server.client_disconnected,
- self, None)
-
- def send_message(self, msg):
- self.conn.send_msg(XML_HEADER + msg, self.new_framing)
-
- def receive_message(self):
- return self.conn.receive_msg_any(self.new_framing)
-
- def allocate_session_id(self):
- sid = self.session_id
- self.session_id += 1
- return sid
-
- def send_hello(self, caplist, session_id=None):
- log.debug('starting', sessionId=session_id)
- msg = elm("hello", attrib={'xmlns': NSMAP['nc']})
- caps = E.capabilities(*[E.capability(x) for x in caplist])
- if session_id is not None:
- assert hasattr(self, "methods")
- self.methods.nc_append_capabilities(
- caps) # pylint: disable=E1101
- msg.append(caps)
-
- if session_id is not None:
- msg.append(E("session-id", str(session_id)))
- msg = etree.tostring(msg)
- log.info("Sending HELLO", msg=msg)
- msg = msg.decode('utf-8')
- self.send_message(msg)
-
- def send_rpc_reply(self, rpc_reply, origmsg):
- reply = etree.Element(qmap('nc') + "rpc-reply", attrib=origmsg.attrib,
- nsmap=origmsg.nsmap)
- try:
- rpc_reply.getchildren # pylint: disable=W0104
- reply.append(rpc_reply)
- except AttributeError:
- reply.extend(rpc_reply)
- ucode = etree.tounicode(reply, pretty_print=True)
- log.debug("RPC-Reply", reply=ucode)
- self.send_message(ucode)
-
- @inlineCallbacks
- def open_session(self):
- # The transport should be connected at this point.
- try:
- # Send hello message.
- yield self.send_hello((NC_BASE_10, NC_BASE_11), self.session_id)
-
- # Get reply
- reply = yield self.receive_message()
- log.info("reply-received", reply=reply)
-
- # Parse reply
- tree = etree.parse(io.BytesIO(reply.encode('utf-8')))
- root = tree.getroot()
- caps = root.xpath("//nc:hello/nc:capabilities/nc:capability",
- namespaces=NSMAP)
-
- # Store capabilities
- for cap in caps:
- self.capabilities.add(cap.text)
-
- if NC_BASE_11 in self.capabilities:
- self.new_framing = True
- elif NC_BASE_10 not in self.capabilities:
- raise SessionError(
- "Server doesn't implement 1.0 or 1.1 of netconf")
-
- self.session_open = True
-
- log.info('session-opened', session_id=self.session_id,
- framing="1.1" if self.new_framing else "1.0")
-
- except Exception as e:
- self.stop(repr(e))
- raise
-
- @inlineCallbacks
- def start(self):
- log.info('starting')
-
- try:
- yield self.open_session()
- while True:
- if not self.session_open:
- break;
-
- msg = yield self.receive_message()
- self.handle_request(msg)
- except Exception as e:
- log.exception('exception', e=e)
- self.stop(repr(e))
-
- log.info('shutdown')
- returnValue(self)
-
- def handle_request(self, msg):
- if not self.session_open:
- return
-
- # Any error with XML encoding here is going to cause a session close
- # TODO: Return a malformed message.
- try:
- tree = etree.parse(io.BytesIO(msg.encode('utf-8')))
- if not tree:
- raise ncerror.SessionError(msg, "Invalid XML from client.")
- except etree.XMLSyntaxError:
- log.error("Closing-session-malformed-message", msg=msg)
- raise ncerror.SessionError(msg, "Invalid XML from client.")
-
- rpcs = tree.xpath("/nc:rpc", namespaces=NSMAP)
- if not rpcs:
- raise ncerror.SessionError(msg, "No rpc found")
-
- # A message can have multiple rpc requests
- for rpc in rpcs:
- try:
- msg_id = rpc.get('message-id')
- log.info("Received-rpc-message-id", msg_id=msg_id)
- except (TypeError, ValueError):
- raise ncerror.SessionError(msg,
- "No valid message-id attribute found")
-
- try:
- # Get the first child of rpc as the method name
- rpc_method = rpc.getchildren()
- if len(rpc_method) != 1:
- log.error("badly-formatted-rpc-method", msg_id=msg_id)
- raise ncerror.RPCSvrErrBadMsg(rpc)
-
- rpc_method = rpc_method[0]
-
- rpcname = rpc_method.tag.replace(qmap('nc'), "")
- params = rpc_method.getchildren()
-
- log.info("rpc-request", rpc=rpcname)
-
- handler = self.main_handlers.get(rpcname, None)
- if handler:
- handler(self, rpcname, rpc, rpc_method, params)
- else:
- log.error('cannot-handle',
- request=msg, session_id=self.session_id,
- rpc=rpc_method)
-
- except ncerror.RPCSvrErrBadMsg as msgerr:
- if self.new_framing:
- self.send_message(msgerr.get_reply_msg())
- else:
- # If we are 1.0 we have to simply close the connection
- # as we are not allowed to send this error
- log.error(
- "Closing-1-0-session--malformed-message")
- raise ncerror.SessionError(msg, "Malformed message")
- except ncerror.RPCServerError as error:
- self.send_message(error.get_reply_msg())
- except Exception as exception:
- error = ncerror.RPCSvrException(rpc, exception)
- self.send_message(error.get_reply_msg())
-
- @inlineCallbacks
- def handle_close_session_request(self, rpcname, rpc, rpc_method,
- params=None):
- log.info('closing-session')
- yield self.send_rpc_reply(etree.Element("ok"), rpc)
- self.close()
-
- @inlineCallbacks
- def handle_kill_session_request(self, rpcname, rpc, rpc_method,
- params=None):
- log.info('killing-session')
- yield self.send_rpc_reply(etree.Element("ok"), rpc)
- self.close()
-
- @inlineCallbacks
- def handle_get_request(self, rpcname, rpc, rpc_method, params=None):
- log.info('get')
- if len(params) > 1:
- raise ncerror.RPCSvrErrBadMsg(rpc)
- if params and not utils.filter_tag_match(params[0], "nc:filter"):
- raise ncerror.RPCSvrUnknownElement(rpc, params[0])
- if not params:
- params = [None]
-
- reply = yield self.invoke_method(rpcname, rpc, params)
- yield self.send_rpc_reply(reply, rpc)
-
- @inlineCallbacks
- def handle_get_config_request(self, rpcname, rpc, rpc_method, params=None):
- log.info('get-config')
- paramslen = len(params)
- # Verify that the source parameter is present
- if paramslen > 2:
- # TODO: need to specify all elements not known
- raise ncerror.RPCSvrErrBadMsg(rpc)
- source_param = rpc_method.find("nc:source", namespaces=NSMAP)
- if source_param is None:
- raise ncerror.RPCSvrMissingElement(rpc, utils.elm("nc:source"))
- filter_param = None
- if paramslen == 2:
- filter_param = rpc_method.find("nc:filter", namespaces=NSMAP)
- if filter_param is None:
- unknown_elm = params[0] if params[0] != source_param else \
- params[1]
- raise ncerror.RPCSvrUnknownElement(rpc, unknown_elm)
- params = [source_param, filter_param]
-
- reply = yield self.invoke_method(rpcname, rpc, params)
- yield self.send_rpc_reply(reply, rpc)
-
- @inlineCallbacks
- def invoke_method(self, rpcname, rpc, params):
- try:
- # Handle any namespaces or prefixes in the tag, other than
- # "nc" which was removed above. Of course, this does not handle
- # namespace collisions, but that seems reasonable for now.
- rpcname = rpcname.rpartition("}")[-1]
- method_name = "rpc_" + rpcname.replace('-', '_')
- method = getattr(self.methods, method_name,
- self._rpc_not_implemented)
- log.info("invoking-method", method=method_name)
- reply = yield method(self, rpc, *params)
- returnValue(reply)
- except NotImplementedError:
- raise ncerror.RPCSvrErrNotImpl(rpc)
-
- def stop(self, reason):
- if not self.exiting:
- log.debug('stopping')
- self.exiting = True
- if self.open_session:
- # TODO: send a closing message to the far end
- self.conn.close_connection()
- self.connected.callback(None)
- self.open_session = False
- log.info('stopped')
-
- def close(self):
- if not self.exiting:
- log.debug('closing-client')
- self.exiting = True
- if self.open_session:
- self.conn.close_connection()
- self.session_open = False
- self.connected.callback(None)
- self.open_session = False
- log.info('closing-client')
-
- main_handlers = {
- 'get-config': handle_get_config_request,
- 'get': handle_get_request,
- 'kill-session': handle_kill_session_request,
- 'close-session': handle_close_session_request
- }
diff --git a/netconf/nc_rpc/__init__.py b/netconf/nc_rpc/__init__.py
new file mode 100644
index 0000000..7398217
--- /dev/null
+++ b/netconf/nc_rpc/__init__.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-#
+#
+# December 23 2014, Christian Hopps <chopps@gmail.com>
+#
+# Copyright (c) 2015, Deutsche Telekom AG
+#
+# 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 __future__ import absolute_import, division, unicode_literals, print_function, nested_scopes
+from lxml.etree import register_namespace
+
+MAXSSHBUF = 16 * 1024
+NSMAP = { }
+
+
+def nsmap_add (prefix, namespace):
+ "Add a prefix namespace mapping to the modules mapping dictionary"
+ NSMAP[prefix] = namespace
+ register_namespace(prefix, namespace)
+
+
+def nsmap_update (nsdict):
+ "Add a dicitonary of prefx namespace mappings to the modules mapping dictionary"
+ NSMAP.update(nsdict)
+ for key, val in nsdict.items():
+ register_namespace(key, val)
+
+
+def qmap (key):
+ return "{" + NSMAP[key] + "}"
+
+
+# Add base spec namespace
+nsmap_add('nc', "urn:ietf:params:xml:ns:netconf:base:1.0")
diff --git a/netconf/protos/__init__.py b/netconf/nc_rpc/base/__init__.py
similarity index 100%
copy from netconf/protos/__init__.py
copy to netconf/nc_rpc/base/__init__.py
diff --git a/netconf/nc_rpc/base/close_session.py b/netconf/nc_rpc/base/close_session.py
new file mode 100644
index 0000000..43babb8
--- /dev/null
+++ b/netconf/nc_rpc/base/close_session.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class CloseSession(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(CloseSession, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('close-session-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ self.rpc_response.node = etree.Element("ok")
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ for child in self.rpc_method.getchildren():
+ # There cannot be parameters to a close session request
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(self.rpc_request)
+ return
\ No newline at end of file
diff --git a/netconf/nc_rpc/base/commit.py b/netconf/nc_rpc/base/commit.py
new file mode 100644
index 0000000..61b7604
--- /dev/null
+++ b/netconf/nc_rpc/base/commit.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class Commit(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(Commit, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('commit-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/copy_config.py b/netconf/nc_rpc/base/copy_config.py
new file mode 100644
index 0000000..cf2fc82
--- /dev/null
+++ b/netconf/nc_rpc/base/copy_config.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class CopyConfig(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(CopyConfig, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('copy-config-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/delete_config.py b/netconf/nc_rpc/base/delete_config.py
new file mode 100644
index 0000000..7163ee6
--- /dev/null
+++ b/netconf/nc_rpc/base/delete_config.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class DeleteConfig(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(DeleteConfig, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('delete-config-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/discard_changes.py b/netconf/nc_rpc/base/discard_changes.py
new file mode 100644
index 0000000..c41d32e
--- /dev/null
+++ b/netconf/nc_rpc/base/discard_changes.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class DiscardChanges(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(DiscardChanges, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('discard-changes-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/edit_config.py b/netconf/nc_rpc/base/edit_config.py
new file mode 100644
index 0000000..5c7599a
--- /dev/null
+++ b/netconf/nc_rpc/base/edit_config.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class EditConfig(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(EditConfig, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('edit-config-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/get.py b/netconf/nc_rpc/base/get.py
new file mode 100644
index 0000000..c6cdfab
--- /dev/null
+++ b/netconf/nc_rpc/base/get.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+from netconf.constants import Constants as C
+from netconf.utils import filter_tag_match
+
+log = structlog.get_logger()
+
+class Get(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(Get, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('get-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
+ self.params = self.rpc_method.getchildren()
+ if len(self.params) > 1:
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(self.rpc_request)
+ return
+
+ if self.params and not filter_tag_match(self.params[0], C.NC_FILTER):
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.UnknownElement(
+ self.rpc_request, self.params[0])
+ return
+
+ if not self.params:
+ self.params = [None]
+
+
diff --git a/netconf/nc_rpc/base/get_config.py b/netconf/nc_rpc/base/get_config.py
new file mode 100644
index 0000000..dffe0d6
--- /dev/null
+++ b/netconf/nc_rpc/base/get_config.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+from netconf.constants import Constants as C
+from netconf.utils import elm
+from netconf import NSMAP
+
+log = structlog.get_logger()
+
+class GetConfig(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(GetConfig, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('get-config-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
+
+ self.params = self.rpc_method.getchildren()
+ paramslen = len(self.params)
+ # Verify that the source parameter is present
+ if paramslen > 2:
+ # TODO: need to specify all elements not known
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(self.rpc_request)
+ return
+
+ self.source_param = self.rpc_method.find(C.NC_SOURCE, namespaces=NSMAP)
+ if self.source_param is None:
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.MissingElement(
+ self.rpc_request, elm(C.NC_SOURCE))
+ return
+
+ self.filter_param = None
+ if paramslen == 2:
+ self.filter_param = self.rpc_method.find(C.NC_FILTER,
+ namespaces=NSMAP)
+ if self.filter_param is None:
+ unknown_elm = self.params[0] if self.params[0] != \
+ self.source_param else \
+ self.params[1]
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.UnknownElement(
+ self.rpc_request, unknown_elm)
+
+ self.params = [self.source_param, self.filter_param]
diff --git a/netconf/nc_rpc/base/kill_session.py b/netconf/nc_rpc/base/kill_session.py
new file mode 100644
index 0000000..08a2e7a
--- /dev/null
+++ b/netconf/nc_rpc/base/kill_session.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+
+class KillSession(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(KillSession, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('kill-session-request', session=self.session.session_id)
+ if self.rpc_response.error:
+ return self.rpc_response
+
+ self.rpc_response.node = etree.Element("ok")
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/lock.py b/netconf/nc_rpc/base/lock.py
new file mode 100644
index 0000000..fc74e83
--- /dev/null
+++ b/netconf/nc_rpc/base/lock.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class Lock(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(Lock, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('Lock-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/unlock.py b/netconf/nc_rpc/base/unlock.py
new file mode 100644
index 0000000..78c59f1
--- /dev/null
+++ b/netconf/nc_rpc/base/unlock.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class UnLock(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(UnLock, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('UnLock-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/base/validate.py b/netconf/nc_rpc/base/validate.py
new file mode 100644
index 0000000..1cb84af
--- /dev/null
+++ b/netconf/nc_rpc/base/validate.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 lxml import etree
+import structlog
+from netconf.nc_rpc.rpc import Rpc
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
+class Validate(Rpc):
+
+ def __init__(self, rpc_request, rpc_method, session):
+ super(Validate, self).__init__(rpc_request, rpc_method, session)
+ self._validate_parameters()
+
+ def execute(self):
+ log.info('Validate-request', session=self.session.session_id)
+ if self.rpc_response.is_error:
+ return self.rpc_response
+
+ def _validate_parameters(self):
+ log.info('validate-parameters', session=self.session.session_id)
diff --git a/netconf/nc_rpc/rpc.py b/netconf/nc_rpc/rpc.py
new file mode 100644
index 0000000..3dd2f17
--- /dev/null
+++ b/netconf/nc_rpc/rpc.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Code adapted from https://github.com/choppsv1/netconf
+#
+# 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 rpc_response import RpcResponse
+
+class Rpc(object):
+ def __init__(self,rpc_request, rpc_method, session):
+ self.rpc_request = rpc_request
+ self.rpc_method = rpc_method
+ self.rpc_response = RpcResponse()
+ self.session = session
+
+ def execute(self):
+ """ run the command - returns a OperationResponse """
+ pass
+
+ def set_rpc_response(self):
+ self.rpc_response = RpcResponse()
+
+ def _validate_parameters(self, rpc_request):
+ """Sets and validates the node as well"""
+ pass
diff --git a/netconf/nc_rpc/rpc_factory.py b/netconf/nc_rpc/rpc_factory.py
new file mode 100644
index 0000000..f3ec0cc
--- /dev/null
+++ b/netconf/nc_rpc/rpc_factory.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Code adapted from https://github.com/choppsv1/netconf
+#
+# 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 base.commit import Commit
+from base.copy_config import CopyConfig
+from base.delete_config import DeleteConfig
+from base.discard_changes import DiscardChanges
+from base.edit_config import EditConfig
+from base.get import Get
+from base.get_config import GetConfig
+from base.lock import Lock
+from base.unlock import UnLock
+from base.close_session import CloseSession
+from base.kill_session import KillSession
+from netconf import NSMAP, qmap
+import netconf.nc_common.error as ncerror
+log = structlog.get_logger()
+
+class RpcFactory:
+
+ instance = None
+
+ def get_rpc_handler(self, rpc_node, msg, session):
+ try:
+ msg_id = rpc_node.get('message-id')
+ log.info("Received-rpc-message-id", msg_id=msg_id)
+
+ except (TypeError, ValueError):
+ raise ncerror.SessionError(msg,
+ "No valid message-id attribute found")
+
+ # Get the first child of rpc as the method name
+ rpc_method = rpc_node.getchildren()
+ if len(rpc_method) != 1:
+ log.error("badly-formatted-rpc-method", msg_id=msg_id)
+ raise ncerror.BadMsg(rpc_node)
+
+ rpc_method = rpc_method[0]
+
+ rpc_name = rpc_method.tag.replace(qmap('nc'), "")
+
+ log.info("rpc-request", rpc=rpc_name)
+
+ class_handler = self.rpc_class_handlers.get(rpc_name, None)
+ if class_handler is not None:
+ return class_handler(rpc_node, rpc_method, session)
+
+ log.error("rpc-not-implemented", rpc=rpc_name)
+
+
+ rpc_class_handlers = {
+ 'get-config': GetConfig,
+ 'get': Get,
+ 'edit-config': EditConfig,
+ 'copy-config': CopyConfig,
+ 'delete-config': DeleteConfig,
+ 'commit': Commit,
+ 'lock': Lock,
+ 'unlock': UnLock,
+ 'close-session': CloseSession,
+ 'kill-session': KillSession
+ }
+
+
+def get_rpc_factory_instance():
+ if RpcFactory.instance == None:
+ RpcFactory.instance = RpcFactory()
+ return RpcFactory.instance
+
diff --git a/netconf/nc_rpc/rpc_response.py b/netconf/nc_rpc/rpc_response.py
new file mode 100644
index 0000000..cdbe167
--- /dev/null
+++ b/netconf/nc_rpc/rpc_response.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Code adapted from https://github.com/choppsv1/netconf
+#
+# 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.
+#
+
+class RpcResponse():
+ def __init__(self):
+ self.is_error = False
+ # if there is an error then the reply_node will contains an Error
+ # object
+ self.reply_node = None
+ self.close_session = False
\ No newline at end of file
diff --git a/netconf/nc_server.py b/netconf/nc_server.py
index 99cff85..3b22290 100644
--- a/netconf/nc_server.py
+++ b/netconf/nc_server.py
@@ -27,21 +27,16 @@
from twisted.internet.defer import Deferred, inlineCallbacks
# from twisted.python import log as logp
from zope.interface import implementer
-from nc_protocol_handler import NetconfProtocolHandler
+from session.nc_protocol_handler import NetconfProtocolHandler
+from session.nc_connection import NetconfConnection
+from session.session_mgr import get_session_manager_instance
+from constants import Constants as C
-from nc_connection import NetconfConnection
# logp.startLogging(sys.stderr)
log = structlog.get_logger()
-# Secure credentials directories
-# TODO: In a production environment these locations require better
-# protection. For now the user_passwords file is just a plain text file.
-KEYS_DIRECTORY = 'security/keys'
-CERTS_DIRECTORY = 'security/certificates'
-CLIENT_CRED_DIRECTORY = 'security/client_credentials'
-
# @implementer(conchinterfaces.ISession)
class NetconfAvatar(avatar.ConchUser):
@@ -60,6 +55,9 @@
def get_nc_server(self):
return self.nc_server
+ def get_user(self):
+ return self.username
+
def logout(self):
log.info('netconf-avatar-logout', username=self.username)
@@ -95,6 +93,7 @@
self.server_public_key_file = server_public_key_file
self.client_public_keys_file = client_public_keys_file
self.client_passwords_file = client_passwords_file
+ self.session_mgr = get_session_manager_instance()
self.grpc_stub = grpc_stub
self.connector = None
self.nc_client_map = {}
@@ -128,8 +127,11 @@
def client_connected(self, client_conn):
assert isinstance(client_conn, NetconfConnection)
log.info('client-connected')
+
+ #create a session
+ session = self.session_mgr.create_session(client_conn.avatar.get_user())
handler = NetconfProtocolHandler(self, client_conn,
- self.grpc_stub)
+ session, self.grpc_stub)
client_conn.proto_handler = handler
reactor.callLater(0, handler.start)
@@ -139,19 +141,19 @@
portal = portal.Portal(NetconfRealm(self, self.grpc_stub))
# setup userid-password access
- password_file = '{}/{}'.format(CLIENT_CRED_DIRECTORY,
+ password_file = '{}/{}'.format(C.CLIENT_CRED_DIRECTORY,
self.client_passwords_file)
portal.registerChecker(FilePasswordDB(password_file))
# setup access when client uses keys
- keys_file = '{}/{}'.format(CLIENT_CRED_DIRECTORY,
+ keys_file = '{}/{}'.format(C.CLIENT_CRED_DIRECTORY,
self.client_public_keys_file)
with open(keys_file) as f:
users = [line.rstrip('\n') for line in f]
users_dict = {}
for user in users:
users_dict[user.split(':')[0]] = [
- keys.Key.fromFile('{}/{}'.format(CLIENT_CRED_DIRECTORY,
+ keys.Key.fromFile('{}/{}'.format(C.CLIENT_CRED_DIRECTORY,
user.split(':')[1]))]
sshDB = SSHPublicKeyChecker(InMemorySSHKeyDB(users_dict))
portal.registerChecker(sshDB)
@@ -180,7 +182,7 @@
return SSHServerTransport()
def getPublicKeys(self):
- key_file_name = '{}/{}'.format(KEYS_DIRECTORY,
+ key_file_name = '{}/{}'.format(C.KEYS_DIRECTORY,
self.server_public_key_file)
try:
publicKeys = {
@@ -192,7 +194,7 @@
filename=key_file_name, exception=repr(e))
def getPrivateKeys(self):
- key_file_name = '{}/{}'.format(KEYS_DIRECTORY,
+ key_file_name = '{}/{}'.format(C.KEYS_DIRECTORY,
self.server_private_key_file)
try:
privateKeys = {
diff --git a/netconf/protos/__init__.py b/netconf/session/__init__.py
similarity index 100%
rename from netconf/protos/__init__.py
rename to netconf/session/__init__.py
diff --git a/netconf/nc_connection.py b/netconf/session/nc_connection.py
similarity index 87%
rename from netconf/nc_connection.py
rename to netconf/session/nc_connection.py
index 2db1baf..d8a2afe 100644
--- a/netconf/nc_connection.py
+++ b/netconf/session/nc_connection.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
#
# Copyright 2016 the original author or authors.
#
@@ -18,6 +19,7 @@
from twisted.internet import protocol
from twisted.internet.defer import inlineCallbacks, returnValue
from common.utils.message_queue import MessageQueue
+from netconf.constants import Constants as C
log = structlog.get_logger()
@@ -70,14 +72,14 @@
# Apparently ssh has a bug that requires minimum of 64 bytes?
# This may not be sufficient to fix this.
if new_framing:
- msg = "\n#{}\n{}\n##\n".format(len(msg), msg)
+ msg = "#{}\n{}\n##\n".format(len(msg), msg)
else:
- msg += "]]>]]>"
+ msg += C.DELIMITER
for chunk in self.chunkit(msg, self.max_chunk - 64):
- log.debug('sending', chunk=chunk,
+ log.info('sending', chunk=chunk,
framing="1.1" if new_framing else "1.0")
# out = hexdump(chunk, result='return')
- self.transport.write('{}\r\n'.format(chunk))
+ self.transport.write('{}\n'.format(chunk))
@inlineCallbacks
def receive_msg_any(self, new_framing):
@@ -91,7 +93,7 @@
def _receive_10(self, msg):
# search for message end indicator
searchfrom = 0
- eomidx = msg.find(b"]]>]]>", searchfrom)
+ eomidx = msg.find(C.DELIMITER, searchfrom)
if eomidx != -1:
log.info('received-msg', msg=msg[:eomidx])
return msg[:eomidx]
@@ -101,11 +103,14 @@
def _receive_11(self, msg):
# Message is received in the format "\n#{len}\n{msg}\n##\n"
+ # A message may have return characters within it
if msg:
+ log.info('received-msg-full', msg=msg)
msg = msg.split('\n')
if len(msg) > 2:
- log.info('received-msg', msg=msg[2])
- return msg[2]
+ msg = ''.join(msg[2:(len(msg)-2)])
+ log.info('parsed-msg\n', msg=msg)
+ return msg
return None
def close_connection(self):
diff --git a/netconf/session/nc_protocol_handler.py b/netconf/session/nc_protocol_handler.py
new file mode 100644
index 0000000..e0a4baf
--- /dev/null
+++ b/netconf/session/nc_protocol_handler.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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
+import io
+from lxml import etree
+from lxml.builder import E
+import netconf.nc_common.error as ncerror
+from netconf import NSMAP, qmap
+from utils import elm
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+from capabilities import Capabilities
+from netconf.nc_rpc.rpc_factory import get_rpc_factory_instance
+from netconf.constants import Constants as C
+
+log = structlog.get_logger()
+
+
+class NetconfProtocolError(Exception): pass
+
+
+class NetconfProtocolHandler:
+ def __init__(self, nc_server, nc_conn, session, grpc_stub):
+ self.started = True
+ self.conn = nc_conn
+ self.nc_server = nc_server
+ self.grpc_stub = grpc_stub
+ self.new_framing = False
+ self.capabilities = Capabilities()
+ self.session = session
+ self.exiting = False
+ self.connected = Deferred()
+ self.connected.addCallback(self.nc_server.client_disconnected,
+ self, None)
+
+ def send_message(self, msg):
+ self.conn.send_msg(C.XML_HEADER + msg, self.new_framing)
+
+ def receive_message(self):
+ return self.conn.receive_msg_any(self.new_framing)
+
+ def send_hello(self, caplist, session=None):
+ msg = elm(C.HELLO, attrib={C.XMLNS: NSMAP[C.NC]})
+ caps = E.capabilities(*[E.capability(x) for x in caplist])
+ msg.append(caps)
+
+ if session is not None:
+ msg.append(E(C.SESSION_ID, str(session.session_id)))
+ msg = etree.tostring(msg)
+ log.info("Sending HELLO", msg=msg)
+ msg = msg.decode('utf-8')
+ self.send_message(msg)
+
+ def send_rpc_reply(self, rpc_reply, origmsg):
+ reply = etree.Element(qmap(C.NC) + C.RPC_REPLY, attrib=origmsg.attrib,
+ nsmap=origmsg.nsmap)
+ try:
+ rpc_reply.getchildren
+ reply.append(rpc_reply)
+ except AttributeError:
+ reply.extend(rpc_reply)
+ ucode = etree.tounicode(reply, pretty_print=True)
+ log.info("RPC-Reply", reply=ucode)
+ self.send_message(ucode)
+
+ def set_framing_version(self):
+ if C.NETCONF_BASE_11 in self.capabilities.client_caps:
+ self.new_framing = True
+ elif C.NETCONF_BASE_10 not in self.capabilities.client_caps:
+ raise SessionError(
+ "Client doesn't implement 1.0 or 1.1 of netconf")
+
+ @inlineCallbacks
+ def open_session(self):
+ # The transport should be connected at this point.
+ try:
+ # Send hello message.
+ yield self.send_hello(self.capabilities.server_caps, self.session)
+ # Get reply
+ reply = yield self.receive_message()
+ log.info("reply-received", reply=reply)
+
+ # Parse reply
+ tree = etree.parse(io.BytesIO(reply.encode('utf-8')))
+ root = tree.getroot()
+ caps = root.xpath(C.CAPABILITY_XPATH, namespaces=NSMAP)
+
+ # Store capabilities
+ for cap in caps:
+ self.capabilities.add_client_capability(cap.text)
+
+ self.set_framing_version()
+ self.session.session_opened = True
+
+ log.info('session-opened', session_id=self.session.session_id,
+ framing="1.1" if self.new_framing else "1.0")
+ except Exception as e:
+ log.error('hello-failure', exception=repr(e))
+ self.stop(repr(e))
+ raise
+
+ @inlineCallbacks
+ def start(self):
+ log.info('starting')
+
+ try:
+ yield self.open_session()
+ while True:
+ if not self.session.session_opened:
+ break;
+ msg = yield self.receive_message()
+ yield self.handle_request(msg)
+
+ except Exception as e:
+ log.exception('exception', exception=repr(e))
+ self.stop(repr(e))
+
+ log.info('shutdown')
+ returnValue(self)
+
+ @inlineCallbacks
+ def handle_request(self, msg):
+ if not self.session.session_opened:
+ return
+
+ # Any error with XML encoding here is going to cause a session close
+ try:
+ tree = etree.parse(io.BytesIO(msg.encode('utf-8')))
+ if not tree:
+ raise ncerror.SessionError(msg, "Invalid XML from client.")
+ except etree.XMLSyntaxError:
+ log.error("malformed-message", msg=msg)
+ try:
+ error = ncerror.BadMsg(msg)
+ self.send_message(error.get_reply_msg())
+ except AttributeError:
+ log.error("attribute-error", msg=msg)
+ # close session
+ self.close()
+ return
+
+ rpcs = tree.xpath(C.RPC_XPATH, namespaces=NSMAP)
+ if not rpcs:
+ raise ncerror.SessionError(msg, "No rpc found")
+
+ # A message can have multiple rpc requests
+ rpc_factory = get_rpc_factory_instance()
+ for rpc in rpcs:
+ try:
+ # Validate message id is received
+ try:
+ msg_id = rpc.get(C.MESSAGE_ID)
+ log.info("Received-rpc-message-id", msg_id=msg_id)
+ except (TypeError, ValueError):
+ log.error('no-message-id', rpc=rpc)
+ raise ncerror.MissingElement(msg, C.MESSAGE_ID)
+
+ # Get a rpc handler
+ rpc_handler = rpc_factory.get_rpc_handler(rpc,
+ msg,
+ self.session)
+ if rpc_handler:
+ # set the parameters for this handler
+ response = yield rpc_handler.execute()
+ log.info('handler',
+ rpc_handler=rpc_handler,
+ is_error=response.is_error,
+ response=response)
+ self.send_rpc_reply(response.node, rpc)
+ if response.close_session:
+ log.info('response-closing-session', response=response)
+ self.close()
+ else:
+ log.error('no-rpc-handler',
+ request=msg,
+ session_id=self.session.session_id)
+ raise ncerror.NotImpl(msg)
+
+ except ncerror.BadMsg as err:
+ log.info('ncerror.BadMsg')
+ if self.new_framing:
+ self.send_message(err.get_reply_msg())
+ else:
+ # If we are 1.0 we have to simply close the connection
+ # as we are not allowed to send this error
+ log.error("Closing-1-0-session--malformed-message")
+ self.close()
+ except (ncerror.NotImpl, ncerror.MissingElement) as e:
+ log.info('error', repr(e))
+ self.send_message(e.get_reply_msg())
+ except Exception as ex:
+ log.info('Exception', repr(ex))
+ error = ncerror.ServerException(rpc, ex)
+ self.send_message(error.get_reply_msg())
+
+ # @inlineCallbacks
+ # def invoke_method(self, rpcname, rpc, params):
+ # try:
+ # # Handle any namespaces or prefixes in the tag, other than
+ # # "nc" which was removed above. Of course, this does not handle
+ # # namespace collisions, but that seems reasonable for now.
+ # rpcname = rpcname.rpartition("}")[-1]
+ # method_name = "rpc_" + rpcname.replace('-', '_')
+ # method = getattr(self.methods, method_name,
+ # self._rpc_not_implemented)
+ # log.info("invoking-method", method=method_name)
+ # reply = yield method(self, rpc, *params)
+ # returnValue(reply)
+ # except NotImplementedError:
+ # raise ncerror.NotImpl(rpc)
+
+ def stop(self, reason):
+ if not self.exiting:
+ log.debug('stopping')
+ self.exiting = True
+ if self.session.session_opened:
+ # TODO: send a closing message to the far end
+ self.conn.close_connection()
+ self.nc_server.session_mgr.remove_session(self.session)
+ self.session.session_opened = False
+ self.connected.callback(None)
+ log.info('stopped')
+
+ def close(self):
+ if not self.exiting:
+ log.debug('closing-client')
+ self.exiting = True
+ if self.session.session_opened:
+ self.conn.close_connection()
+ self.nc_server.session_mgr.remove_session(self.session)
+ self.session.session_opened = False
+ self.connected.callback(None)
+ log.info('closing-client')
diff --git a/netconf/session/session.py b/netconf/session/session.py
new file mode 100644
index 0000000..51979f7
--- /dev/null
+++ b/netconf/session/session.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 time import time
+import structlog
+
+log = structlog.get_logger()
+
+
+class Session:
+ def __init__(self, session_id, user):
+ self.session_id = session_id
+ self.user = user
+ self.started_at = time()
+ self.session_opened = False
diff --git a/netconf/session/session_mgr.py b/netconf/session/session_mgr.py
new file mode 100644
index 0000000..ee4382f
--- /dev/null
+++ b/netconf/session/session_mgr.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# 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 session import Session
+import structlog
+
+log = structlog.get_logger()
+
+class SessionManager:
+ instance = None
+
+ def __init__(self):
+ self.next_session_id = 1
+ self.sessions = {}
+
+ def create_session(self, user):
+ session = Session(self.next_session_id, user)
+ self.sessions[self.next_session_id] = session
+ self.next_session_id += 1
+ return session
+
+ def remove_session(self, session):
+ session_id = session.session_id
+ if session_id in self.sessions.keys():
+ del self.sessions[session_id]
+ log.info('remove-session', session_id=session_id)
+ else:
+ log.error('invalid-session', session_id=session_id)
+
+
+def get_session_manager_instance():
+ if SessionManager.instance == None:
+ SessionManager.instance = SessionManager()
+ return SessionManager.instance
\ No newline at end of file