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