This is the initial commit of the netconf server code.  It consists
of the following:
1) The server is built using Twisted Conch
2) It adapted an existing opensource netconf server (https://github.com/choppsv1/netconf)
   to handle some low-level protocols.  The adaptation is mostly around
   using Twisted Conch instead of Python Threads
3) A microservice to interface with Voltha on the SB and Netconf client on
   the NB
4) A set of credentials for the server and clients.  At this time these
   credentials are local and in files.  Additional work is required to
   secure these files
5) A rough-in to handle the rpc requests from Netconf clients
6) Code for initial handshaking is in place (hello)

Change-Id: I1ca0505d0ac35ff06066b107019ae87ae30e38f8
diff --git a/netconf/__init__.py b/netconf/__init__.py
new file mode 100644
index 0000000..7398217
--- /dev/null
+++ b/netconf/__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/connection_mgr.py b/netconf/connection_mgr.py
new file mode 100644
index 0000000..20dc184
--- /dev/null
+++ b/netconf/connection_mgr.py
@@ -0,0 +1,111 @@
+#
+# 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 twisted.internet.defer import inlineCallbacks, returnValue
+
+from common.utils.consulhelpers import get_endpoint_from_consul
+from structlog import get_logger
+from netconf.nc_server import NCServer
+
+log = get_logger()
+
+
+class ConnectionManager(object):
+    def __init__(self,
+                 consul_endpoint,
+                 voltha_endpoint,
+                 netconf_port,
+                 server_private_key_file,
+                 server_public_key_file,
+                 client_public_keys_file,
+                 client_passwords_file,
+                 voltha_retry_interval=0.5,
+                 devices_refresh_interval=5):
+
+        log.info('init-connection-manager')
+        self.netconf_port = netconf_port
+        self.server_private_key_file = server_private_key_file
+        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.consul_endpoint = consul_endpoint
+        self.voltha_endpoint = voltha_endpoint
+
+        self.channel = None
+        self.grpc_client = None  # single, shared gRPC client to Voltha
+
+        self.nc_server = None
+
+        self.voltha_retry_interval = voltha_retry_interval
+        self.devices_refresh_interval = devices_refresh_interval
+
+        self.running = False
+
+    @inlineCallbacks
+    def start(self):
+
+        if self.running:
+            return
+
+        log.debug('starting')
+
+        self.running = True
+
+        # # Get voltha grpc endpoint
+        # self.channel = self.get_grpc_channel_with_voltha()
+        #
+        # # Create shared gRPC API object
+        # self.grpc_client = GrpcClient(self, self.channel).start()
+
+        # Start the netconf server
+        self.nc_server = yield self.start_netconf_server().start()
+
+        log.info('started')
+
+        returnValue(self)
+
+    def stop(self):
+        log.debug('stopping')
+        self.running = False
+        # clean the netconf server
+        self.nc_server.stop()
+        log.info('stopped')
+
+    def resolve_endpoint(self, endpoint):
+        ip_port_endpoint = endpoint
+        if endpoint.startswith('@'):
+            try:
+                ip_port_endpoint = get_endpoint_from_consul(
+                    self.consul_endpoint, endpoint[1:])
+                log.debug('found-service-from-consul', endpoint=endpoint,
+                          ip_port=ip_port_endpoint)
+
+            except Exception as e:
+                log.error('not-found-service-from-consul',
+                          endpoint=endpoint, exception=repr(e))
+
+                return
+        if ip_port_endpoint:
+            host, port = ip_port_endpoint.split(':', 2)
+            return host, int(port)
+
+    def start_netconf_server(self):
+        return NCServer(self.netconf_port,
+                        self.server_private_key_file,
+                        self.server_public_key_file,
+                        self.client_public_keys_file,
+                        self.client_passwords_file,
+                        self.grpc_client)
diff --git a/netconf/error.py b/netconf/error.py
new file mode 100644
index 0000000..7a33f69
--- /dev/null
+++ b/netconf/error.py
@@ -0,0 +1,193 @@
+# -*- 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
new file mode 100644
index 0000000..ca6fa42
--- /dev/null
+++ b/netconf/grpc_client.py
@@ -0,0 +1,40 @@
+#
+# 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.
+#
+
+"""
+The gRPC client layer for the Netconf agent
+"""
+from Queue import Queue, Empty
+from structlog import get_logger
+from twisted.internet.defer import inlineCallbacks, returnValue, DeferredQueue
+
+
+log = get_logger()
+
+
+class GrpcClient(object):
+
+    def __init__(self, connection_manager, channel):
+
+        self.connection_manager = connection_manager
+        self.channel = channel
+        self.logical_stub = None
+
+        self.stopped = False
+
+        self.packet_out_queue = Queue()  # queue to send out PacketOut msgs
+        self.packet_in_queue = DeferredQueue()  # queue to receive PacketIn
+
diff --git a/netconf/main.py b/netconf/main.py
new file mode 100755
index 0000000..06edfe4
--- /dev/null
+++ b/netconf/main.py
@@ -0,0 +1,294 @@
+#!/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 argparse
+import os
+
+import yaml
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+
+from common.structlog_setup import setup_logging
+from common.utils.dockerhelpers import get_my_containers_name
+from common.utils.nethelpers import get_my_primary_local_ipv4
+from connection_mgr import ConnectionManager
+
+defs = dict(
+    config=os.environ.get('CONFIG', './netconf.yml'),
+    consul=os.environ.get('CONSUL', 'localhost:8500'),
+    external_host_address=os.environ.get('EXTERNAL_HOST_ADDRESS',
+                                         get_my_primary_local_ipv4()),
+    netconf_port=os.environ.get('NETCONF_PORT', 1830),
+    server_private_key_file=os.environ.get('SERVER_PRIVATE_KEY_FILE',
+                                           'server.key'),
+    server_public_key_file=os.environ.get('SERVER_PRIVATE_KEY_FILE',
+                                          'server.key.pub'),
+    client_public_keys_file=os.environ.get('CLIENT_PUBLIC_KEYS_FILE',
+                                           'client_keys'),
+    client_passwords_file=os.environ.get('CLIENT_PASSWORD_FILE',
+                                         'client_passwords'),
+    grpc_endpoint=os.environ.get('GRPC_ENDPOINT', 'localhost:50055'),
+    fluentd=os.environ.get('FLUENTD', None),
+    instance_id=os.environ.get('INSTANCE_ID', os.environ.get('HOSTNAME', '1')),
+    internal_host_address=os.environ.get('INTERNAL_HOST_ADDRESS',
+                                         get_my_primary_local_ipv4()),
+    work_dir=os.environ.get('WORK_DIR', '/tmp/netconf')
+)
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+
+    _help = ('Path to netconf.yml config file (default: %s). '
+             'If relative, it is relative to main.py of ofagent.'
+             % defs['config'])
+    parser.add_argument('-c', '--config',
+                        dest='config',
+                        action='store',
+                        default=defs['config'],
+                        help=_help)
+
+    _help = '<hostname>:<port> to consul agent (default: %s)' % defs['consul']
+    parser.add_argument(
+        '-C', '--consul', dest='consul', action='store',
+        default=defs['consul'],
+        help=_help)
+
+    _help = ('<hostname> or <ip> at which netconf is reachable from '
+             'outside the cluster (default: %s)' % defs[
+                 'external_host_address'])
+    parser.add_argument('-E', '--external-host-address',
+                        dest='external_host_address',
+                        action='store',
+                        default=defs['external_host_address'],
+                        help=_help)
+
+    _help = ('<port> of netconf server (default: %s). (If not '
+             'specified (None), the port from the config file is used'
+             % defs['netconf_port'])
+    parser.add_argument('-N', '--netconf_port',
+                        dest='netconf_port',
+                        action='store',
+                        default=defs['netconf_port'],
+                        help=_help)
+
+    _help = (
+    '<server private key file name> used by the netconf server. (If not '
+    'specified (None), the file name from the config file is used (default: %s)'
+    % defs['server_private_key_file'])
+    parser.add_argument('-S', '--server_private_key_file',
+                        dest='server_private_key_file',
+                        action='store',
+                        default=defs['server_private_key_file'],
+                        help=_help)
+
+    _help = ('<server public key file name> used by the netconf server. (If '
+             'not specified (None), the file name from the config file is '
+             'used (default: %s) '
+             % defs['server_public_key_file'])
+    parser.add_argument('-P', '--server_public_key_file',
+                        dest='server_public_key_file',
+                        action='store',
+                        default=defs['server_public_key_file'],
+                        help=_help)
+
+    _help = ('<client public key file name> used by the netconf server. (If '
+             'not specified (None), the file name from the config file is '
+             'used(default: %s) '
+             % defs['client_public_keys_file'])
+    parser.add_argument('-X', '--client_public_keys_file',
+                        dest='client_public_keys_file',
+                        action='store',
+                        default=defs['client_public_keys_file'],
+                        help=_help)
+
+    _help = ('<client password file name> used by the netconf server. (If '
+             'not specified (None), the file name from the config file is '
+             'used (default: %s) '
+             % defs['client_passwords_file'])
+    parser.add_argument('-U', '--client_passwords_file',
+                        dest='client_passwords_file',
+                        action='store',
+                        default=defs['client_passwords_file'],
+                        help=_help)
+
+    _help = ('<hostname>:<port> to fluentd server (default: %s). (If not '
+             'specified (None), the address from the config file is used'
+             % defs['fluentd'])
+    parser.add_argument('-F', '--fluentd',
+                        dest='fluentd',
+                        action='store',
+                        default=defs['fluentd'],
+                        help=_help)
+
+    _help = ('gRPC end-point to connect to. It can either be a direct'
+             'definition in the form of <hostname>:<port>, or it can be an'
+             'indirect definition in the form of @<service-name> where'
+             '<service-name> is the name of the grpc service as registered'
+             'in consul (example: @voltha-grpc). (default: %s'
+             % defs['grpc_endpoint'])
+    parser.add_argument('-G', '--grpc-endpoint',
+                        dest='grpc_endpoint',
+                        action='store',
+                        default=defs['grpc_endpoint'],
+                        help=_help)
+
+    _help = ('<hostname> or <ip> at which netconf server is reachable from '
+             'inside the cluster (default: %s)' % defs[
+                 'internal_host_address'])
+    parser.add_argument('-H', '--internal-host-address',
+                        dest='internal_host_address',
+                        action='store',
+                        default=defs['internal_host_address'],
+                        help=_help)
+
+    _help = ('unique string id of this netconf server instance (default: %s)'
+             % defs['instance_id'])
+    parser.add_argument('-i', '--instance-id',
+                        dest='instance_id',
+                        action='store',
+                        default=defs['instance_id'],
+                        help=_help)
+
+    _help = 'omit startup banner log lines'
+    parser.add_argument('-n', '--no-banner',
+                        dest='no_banner',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    _help = "suppress debug and info logs"
+    parser.add_argument('-q', '--quiet',
+                        dest='quiet',
+                        action='count',
+                        help=_help)
+
+    _help = 'enable verbose logging'
+    parser.add_argument('-v', '--verbose',
+                        dest='verbose',
+                        action='count',
+                        help=_help)
+
+    _help = ('work dir to compile and assemble generated files (default=%s)'
+             % defs['work_dir'])
+    parser.add_argument('-w', '--work-dir',
+                        dest='work_dir',
+                        action='store',
+                        default=defs['work_dir'],
+                        help=_help)
+
+    _help = ('use docker container name as netconf server instance id'
+             ' (overrides -i/--instance-id option)')
+    parser.add_argument('--instance-id-is-container-name',
+                        dest='instance_id_is_container_name',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    args = parser.parse_args()
+
+    # post-processing
+
+    if args.instance_id_is_container_name:
+        args.instance_id = get_my_containers_name()
+
+    return args
+
+
+def load_config(args):
+    path = args.config
+    if path.startswith('.'):
+        dir = os.path.dirname(os.path.abspath(__file__))
+        path = os.path.join(dir, path)
+    path = os.path.abspath(path)
+    with open(path) as fd:
+        config = yaml.load(fd)
+    return config
+
+
+banner = r'''
+ _   _      _                   __   ____
+| \ | | ___| |_ ___ ___  _ __  / _| / ___|  ___ _ ____   _____ _ __
+|  \| |/ _ \ __/ __/ _ \| '_ \| |_  \___ \ / _ \ '__\ \ / / _ \ '__|
+| |\  |  __/ || (_| (_) | | | |  _|  ___) |  __/ |   \ V /  __/ |
+|_| \_|\___|\__\___\___/|_| |_|_|   |____/ \___|_|    \_/ \___|_|
+'''
+
+
+def print_banner(log):
+    for line in banner.strip('\n').splitlines():
+        log.info(line)
+    log.info('(to stop: press Ctrl-C)')
+
+
+class Main(object):
+    def __init__(self):
+
+        self.args = args = parse_args()
+        self.config = load_config(args)
+
+        verbosity_adjust = (args.verbose or 0) - (args.quiet or 0)
+        self.log = setup_logging(self.config.get('logging', {}),
+                                 args.instance_id,
+                                 verbosity_adjust=verbosity_adjust,
+                                 fluentd=args.fluentd)
+
+        # components
+        self.connection_manager = None
+
+        self.exiting = False
+
+        if not args.no_banner:
+            print_banner(self.log)
+
+        self.startup_components()
+
+    def start(self):
+        self.start_reactor()  # will not return except Keyboard interrupt
+
+    @inlineCallbacks
+    def startup_components(self):
+        self.log.info('starting-netconf-server')
+        args = self.args
+        self.connection_manager = yield ConnectionManager(
+            args.consul,
+            args.grpc_endpoint,
+            args.netconf_port,
+            args.server_private_key_file,
+            args.server_public_key_file,
+            args.client_public_keys_file,
+            args.client_passwords_file).start()
+        self.log.info('started-netconf-server')
+
+    @inlineCallbacks
+    def shutdown_components(self):
+        """Execute before the reactor is shut down"""
+        self.log.info('exiting-on-keyboard-interrupt')
+        self.exiting = True
+        if self.connection_manager is not None:
+            yield self.connection_manager.stop()
+
+    def start_reactor(self):
+        reactor.callWhenRunning(
+            lambda: self.log.info('twisted-reactor-started'))
+
+        reactor.addSystemEventTrigger('before', 'shutdown',
+                                      self.shutdown_components)
+        reactor.run()
+
+
+if __name__ == '__main__':
+    Main().start()
diff --git a/netconf/nc_connection.py b/netconf/nc_connection.py
new file mode 100644
index 0000000..2db1baf
--- /dev/null
+++ b/netconf/nc_connection.py
@@ -0,0 +1,124 @@
+#
+# 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
+from hexdump import hexdump
+from twisted.internet import protocol
+from twisted.internet.defer import inlineCallbacks, returnValue
+from common.utils.message_queue import MessageQueue
+
+log = structlog.get_logger()
+
+from netconf import MAXSSHBUF
+
+
+class NetconfConnection(protocol.Protocol):
+    def __init__(self, data=None, avatar=None, max_chunk=MAXSSHBUF):
+        self.avatar = avatar
+        self.nc_server = self.avatar.get_nc_server()
+        self.rx = MessageQueue()
+        self.max_chunk = max_chunk
+        self.connected = True
+        self.proto_handler = None
+        self.exiting = False
+
+    def connectionLost(self, reason):
+        log.info('connection-lost')
+        self.connected = False
+        if not self.exiting:
+            self.proto_handler.stop('Connection-Lost')
+
+    def connectionMade(self):
+        log.info('connection-made')
+        self.nc_server.client_connected(self)
+
+    def dataReceived(self, data):
+        log.debug('data-received', len=len(data),
+                 received=hexdump(data, result='return'))
+        assert len(data)
+        self.rx.put(data)
+
+    def processEnded(self, reason=None):
+        log.info('process-ended', reason=reason)
+        self.connected = False
+
+    def chunkit(self, msg, maxsend):
+        sz = len(msg)
+        left = 0
+        for unused in range(0, sz // maxsend):
+            right = left + maxsend
+            chunk = msg[left:right]
+            left = right
+            yield chunk
+        msg = msg[left:]
+        yield msg
+
+    def send_msg(self, msg, new_framing):
+        assert self.connected
+        # 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)
+        else:
+            msg += "]]>]]>"
+        for chunk in self.chunkit(msg, self.max_chunk - 64):
+            log.debug('sending', chunk=chunk,
+                     framing="1.1" if new_framing else "1.0")
+            # out = hexdump(chunk, result='return')
+            self.transport.write('{}\r\n'.format(chunk))
+
+    @inlineCallbacks
+    def receive_msg_any(self, new_framing):
+        assert self.connected
+        msg = yield self.recv(lambda _: True)
+        if new_framing:
+            returnValue(self._receive_11(msg))
+        else:
+            returnValue(self._receive_10(msg))
+
+    def _receive_10(self, msg):
+        # search for message end indicator
+        searchfrom = 0
+        eomidx = msg.find(b"]]>]]>", searchfrom)
+        if eomidx != -1:
+            log.info('received-msg', msg=msg[:eomidx])
+            return msg[:eomidx]
+        else:
+            log.error('no-message-end-indicators', msg=msg)
+            return msg
+
+    def _receive_11(self, msg):
+        # Message is received in the format "\n#{len}\n{msg}\n##\n"
+        if msg:
+            msg = msg.split('\n')
+            if len(msg) > 2:
+                log.info('received-msg', msg=msg[2])
+                return msg[2]
+        return None
+
+    def close_connection(self):
+        log.info('closing-connection')
+        self.exiting = True
+        self.transport.loseConnection()
+
+    def recv(self, predicate):
+        assert self.connected
+        return self.rx.get(predicate)
+
+    def recv_any(self, new_framing):
+        return self.recv(lambda _: True)
+
+    def recv_xid(self, xid):
+        return self.recv(lambda msg: msg.xid == xid)
diff --git a/netconf/nc_protocol_handler.py b/netconf/nc_protocol_handler.py
new file mode 100644
index 0000000..2b1fb65
--- /dev/null
+++ b/netconf/nc_protocol_handler.py
@@ -0,0 +1,359 @@
+#
+# 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_server.py b/netconf/nc_server.py
new file mode 100644
index 0000000..99cff85
--- /dev/null
+++ b/netconf/nc_server.py
@@ -0,0 +1,204 @@
+#
+# 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 sys
+from twisted.conch import avatar
+from twisted.cred import portal
+from twisted.conch.checkers import SSHPublicKeyChecker, InMemorySSHKeyDB
+from twisted.conch.ssh import factory, userauth, connection, keys, session
+from twisted.conch.ssh.transport import SSHServerTransport
+
+from twisted.cred.checkers import FilePasswordDB
+from twisted.internet import reactor
+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 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):
+    def __init__(self, username, nc_server, grpc_stub):
+        avatar.ConchUser.__init__(self)
+        self.username = username
+        self.nc_server = nc_server
+        self.grpc_stub = grpc_stub
+        self.channelLookup.update({'session': session.SSHSession})
+        self.subsystemLookup.update(
+            {b"netconf": NetconfConnection})
+
+    def get_grpc_stub(self):
+        return self.grpc_stub
+
+    def get_nc_server(self):
+        return self.nc_server
+
+    def logout(self):
+        log.info('netconf-avatar-logout', username=self.username)
+
+
+@implementer(portal.IRealm)
+class NetconfRealm(object):
+    def __init__(self, nc_server, grpc_stub):
+        self.grpc_stub = grpc_stub
+        self.nc_server = nc_server
+
+    def requestAvatar(self, avatarId, mind, *interfaces):
+        user = NetconfAvatar(avatarId, self.nc_server, self.grpc_stub)
+        return interfaces[0], user, user.logout
+
+
+class NCServer(factory.SSHFactory):
+    #
+    services = {
+        'ssh-userauth': userauth.SSHUserAuthServer,
+        'ssh-connection': connection.SSHConnection
+    }
+
+    def __init__(self,
+                 netconf_port,
+                 server_private_key_file,
+                 server_public_key_file,
+                 client_public_keys_file,
+                 client_passwords_file,
+                 grpc_stub):
+
+        self.netconf_port = netconf_port
+        self.server_private_key_file = server_private_key_file
+        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.grpc_stub = grpc_stub
+        self.connector = None
+        self.nc_client_map = {}
+        self.running = False
+        self.exiting = False
+
+    def start(self):
+        log.debug('starting')
+        if self.running:
+            return
+        self.running = True
+        reactor.callLater(0, self.start_ssh_server)
+        log.info('started')
+        return self
+
+    def stop(self):
+        log.debug('stopping')
+        self.exiting = True
+        self.connector.disconnect()
+        self.d_stopped.callback(None)
+        log.info('stopped')
+
+    def client_disconnected(self, result, handler, reason):
+        assert isinstance(handler, NetconfProtocolHandler)
+
+        log.info('client-disconnected', reason=reason)
+
+        # For now just nullify the handler
+        handler.close()
+
+    def client_connected(self, client_conn):
+        assert isinstance(client_conn, NetconfConnection)
+        log.info('client-connected')
+        handler = NetconfProtocolHandler(self, client_conn,
+                                         self.grpc_stub)
+        client_conn.proto_handler = handler
+        reactor.callLater(0, handler.start)
+
+    def setup_secure_access(self):
+        try:
+            from twisted.cred import portal
+            portal = portal.Portal(NetconfRealm(self, self.grpc_stub))
+
+            # setup userid-password access
+            password_file = '{}/{}'.format(CLIENT_CRED_DIRECTORY,
+                                           self.client_passwords_file)
+            portal.registerChecker(FilePasswordDB(password_file))
+
+            # setup access when client uses keys
+            keys_file = '{}/{}'.format(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,
+                                                     user.split(':')[1]))]
+            sshDB = SSHPublicKeyChecker(InMemorySSHKeyDB(users_dict))
+            portal.registerChecker(sshDB)
+            return portal
+        except Exception as e:
+            log.error('setup-secure-access-fail', exception=repr(e))
+
+    @inlineCallbacks
+    def start_ssh_server(self):
+        try:
+            log.debug('starting', port=self.netconf_port)
+            self.portal = self.setup_secure_access()
+            self.connector = reactor.listenTCP(self.netconf_port, self)
+            log.debug('started', port=self.netconf_port)
+            self.d_stopped = Deferred()
+            self.d_stopped.callback(self.stop)
+            yield self.d_stopped
+        except Exception as e:
+            log.error('netconf-server-not-started', port=self.netconf_port,
+                      exception=repr(e))
+
+    # Methods from SSHFactory
+    #
+
+    def protocol(self):
+        return SSHServerTransport()
+
+    def getPublicKeys(self):
+        key_file_name = '{}/{}'.format(KEYS_DIRECTORY,
+                                       self.server_public_key_file)
+        try:
+            publicKeys = {
+                'ssh-rsa': keys.Key.fromFile(key_file_name)
+            }
+            return publicKeys
+        except Exception as e:
+            log.error('cannot-retrieve-server-public-key',
+                      filename=key_file_name, exception=repr(e))
+
+    def getPrivateKeys(self):
+        key_file_name = '{}/{}'.format(KEYS_DIRECTORY,
+                                       self.server_private_key_file)
+        try:
+            privateKeys = {
+                'ssh-rsa': keys.Key.fromFile(key_file_name)
+            }
+            return privateKeys
+        except Exception as e:
+            log.error('cannot-retrieve-server-private-key',
+                      filename=key_file_name, exception=repr(e))
diff --git a/netconf/netconf.yml b/netconf/netconf.yml
new file mode 100644
index 0000000..2b1d69e
--- /dev/null
+++ b/netconf/netconf.yml
@@ -0,0 +1,44 @@
+logging:
+    version: 1
+
+    formatters:
+      brief:
+        format: '%(message)s'
+      default:
+        format: '%(asctime)s.%(msecs)03d %(levelname)-8s %(module)s.%(funcName)s %(message)s'
+        datefmt: '%Y%m%dT%H%M%S'
+      fluent_fmt:
+        '()': fluent.handler.FluentRecordFormatter
+        format:
+          level: '%(levelname)s'
+          hostname: '%(hostname)s'
+          where: '%(module)s.%(funcName)s'
+
+    handlers:
+        console:
+            class : logging.StreamHandler
+            level: DEBUG
+            formatter: default
+            stream: ext://sys.stdout
+        fluent:
+            class: fluent.handler.FluentHandler
+            host: localhost
+            port: 24224
+            tag: voltha.logging
+            formatter: fluent_fmt
+            level: DEBUG
+        null:
+            class: logging.NullHandler
+
+    loggers:
+        amqp:
+            handlers: [null]
+            propagate: False
+        conf:
+            handlers: [null]
+            propagate: False
+        '': # root logger
+            handlers: [console, fluent]
+            level: INFO # this can be bumped up/down by -q and -v command line
+                        # options
+            propagate: False
diff --git a/netconf/protos/Makefile b/netconf/protos/Makefile
new file mode 100644
index 0000000..2d9c069
--- /dev/null
+++ b/netconf/protos/Makefile
@@ -0,0 +1,37 @@
+#
+# 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.
+#
+
+# Makefile to build all protobuf and gRPC related artifacts
+
+ifeq ($(VOLTHA_BASE)_set,_set)
+  $(error To get started, please source the env.sh file from Voltha top level directory)
+endif
+
+# This makefile is used only to copy relevant *_pb2.py files from Voltha
+# to allow ofagent to function properly.
+
+PB2_FILES := \
+    voltha_pb2.py \
+    openflow_13_pb2.py
+
+TARGET_PROTO_DIR := $(VOLTHA_BASE)/ofagent/protos
+SOURCE_PROTO_DIR := $(VOLTHA_BASE)/voltha/protos
+
+build: copyfiles
+
+copyfiles:
+	rsync -av --include '*/' --exclude='third_party/__init__.py' --include '*.py' --exclude='*' $(SOURCE_PROTO_DIR)/ $(TARGET_PROTO_DIR)
+
diff --git a/netconf/protos/README b/netconf/protos/README
new file mode 100644
index 0000000..65c5e93
--- /dev/null
+++ b/netconf/protos/README
@@ -0,0 +1 @@
+Protoc-generated *.py files are populated here from Voltha via make
diff --git a/netconf/protos/__init__.py b/netconf/protos/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/netconf/protos/__init__.py
diff --git a/netconf/protos/third_party/__init__.py b/netconf/protos/third_party/__init__.py
new file mode 100644
index 0000000..6dab4e7
--- /dev/null
+++ b/netconf/protos/third_party/__init__.py
@@ -0,0 +1,50 @@
+#
+# 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.
+#
+
+"""
+This helps loading http_pb2 and annotations_pb2.
+Without this, the Python importer will not be able to process the lines:
+from google.api import http_pb2 or
+from google.api import annotations_pb2
+(Without importing these, the protobuf loader will not recognize http options
+in the protobuf definitions.)
+"""
+
+from importlib import import_module
+import os
+import sys
+
+
+class GoogleApiImporter(object):
+
+    def find_module(self, full_name, path=None):
+        if full_name == 'google.api':
+            self.path = [os.path.dirname(__file__)]
+            return self
+
+    def load_module(self, name):
+        if name in sys.modules:
+            return sys.modules[name]
+        full_name = 'ofagent.protos.third_party.' + name
+        import_module(full_name)
+        module = sys.modules[full_name]
+        sys.modules[name] = module
+        return module
+
+
+sys.meta_path.append(GoogleApiImporter())
+from google.api import http_pb2, annotations_pb2
+_ = http_pb2, annotations_pb2
diff --git a/netconf/protos/third_party/google/__init__.py b/netconf/protos/third_party/google/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/netconf/protos/third_party/google/__init__.py
diff --git a/netconf/protos/third_party/google/api/__init__.py b/netconf/protos/third_party/google/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/netconf/protos/third_party/google/api/__init__.py
diff --git a/netconf/security/certificates/server.crt b/netconf/security/certificates/server.crt
new file mode 100644
index 0000000..200597d
--- /dev/null
+++ b/netconf/security/certificates/server.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDejCCAmICCQDB3buSyupywzANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJD
+QTELMAkGA1UECAwCT04xDzANBgNVBAcMBk90dGF3YTEOMAwGA1UECgwFQ2llbmEx
+CzAJBgNVBAsMAkJQMRIwEAYDVQQDDAlraGVuYWlkb28xITAfBgkqhkiG9w0BCQEW
+EmtudXJzaW11QGNpZW5hLmNvbTAeFw0xNjExMjIxNjQ4MzdaFw0xNzExMjIxNjQ4
+MzdaMH8xCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJPTjEPMA0GA1UEBwwGT3R0YXdh
+MQ4wDAYDVQQKDAVDaWVuYTELMAkGA1UECwwCQlAxEjAQBgNVBAMMCWtoZW5haWRv
+bzEhMB8GCSqGSIb3DQEJARYSa251cnNpbXVAY2llbmEuY29tMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA25erUWmgLl0L8KgGjYEBicFs1Q7rXU9OnsF2
+Me1/hWYwExIrKwTXf/D6GY76k5BcNAlkmpmkeF9qio6+IokTGSzUoG9knCCfGITm
+ziHCAN0m7xsm9xXiREtZYW5hyW7USo0RHwXb/ZrvlGzbXyDb0KnYovyVVPihPrEO
+gtvOnXZGQ5h7Hr9+3swdjgwdn+xkO66g4Yr9ul+hhTAqyT8CchFvdZxgyplgbpfu
+DWd308pjv02ebDmDUQrcev78VRQHrIWOQcf6AW9a7OUus3l18rjG4DwNYEYxtDEm
+EgR+hDVwGuePVxvna/b+tapUaZT+cktSGjQyP5VG2tR5WMNPTQIDAQABMA0GCSqG
+SIb3DQEBCwUAA4IBAQA8TqSLW98Imz7xH9tSWVfs1Rhb7DuEl47vwOY99ocuG/cI
+8IjEmG6DGC/Q+AhgFKXgpMwGz0vxlYCEn3nSTnkUKqOTiKvuFFTHP7AKVdKAIYpI
+cV2DEw7S+EOeYAicAgIYb885yImB79ZjuSe6HgZG7kAqjPIjzefM43Fe9ESgIHA7
+e6za7cz+AJcpU2o/rxeS3p0nxrW9cEUSM0vhqBHvTTepe52IThnYbNIMbuhC1+fk
+if8STlOIe3maYc5Drt1njk3lXCPEuOFLTZxpG4qtNSBXPjRj0SqmLBly5QmdEuKc
+wj14dWBntVTvWeO9Z15DZjzLubNF7CJmqlAuRCON
+-----END CERTIFICATE-----
diff --git a/netconf/security/certificates/server.csr b/netconf/security/certificates/server.csr
new file mode 100644
index 0000000..163917b
--- /dev/null
+++ b/netconf/security/certificates/server.csr
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICxDCCAawCAQAwfzELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMQ8wDQYDVQQH
+DAZPdHRhd2ExDjAMBgNVBAoMBUNpZW5hMQswCQYDVQQLDAJCUDESMBAGA1UEAwwJ
+a2hlbmFpZG9vMSEwHwYJKoZIhvcNAQkBFhJrbnVyc2ltdUBjaWVuYS5jb20wggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDbl6tRaaAuXQvwqAaNgQGJwWzV
+DutdT06ewXYx7X+FZjATEisrBNd/8PoZjvqTkFw0CWSamaR4X2qKjr4iiRMZLNSg
+b2ScIJ8YhObOIcIA3SbvGyb3FeJES1lhbmHJbtRKjREfBdv9mu+UbNtfINvQqdii
+/JVU+KE+sQ6C286ddkZDmHsev37ezB2ODB2f7GQ7rqDhiv26X6GFMCrJPwJyEW91
+nGDKmWBul+4NZ3fTymO/TZ5sOYNRCtx6/vxVFAeshY5Bx/oBb1rs5S6zeXXyuMbg
+PA1gRjG0MSYSBH6ENXAa549XG+dr9v61qlRplP5yS1IaNDI/lUba1HlYw09NAgMB
+AAGgADANBgkqhkiG9w0BAQsFAAOCAQEAbqT2sRFEpJuCqH/tQZhK2gFhBQOdRCRu
+GTzQdNN+vic0EjIsbL7c9boM0O55KMldDAhbQk55Y80CRCTwJXmfxIdlWAOLnING
+JeTwTaIhQsj2pMnsHKmtOHzGMkMiLdr6IxVowNQQ0KmJcuEPto908/0VuPfwSrSN
+Y/vZ0w1y9QdwpX5cj8EeYznNpHhPnKLpdMC5sXPZ16HgiFY8kW2MQDi4Oqx5ng/1
+Wyb9/uoAsFXERSNbw7Q0VNe3CT1l4LnfVKyVpND2rslBVISkZmfdOekBySM3PVeu
+3xUR3bBrYCccgxTSVHXjwdgpa58jOuKV1yO+QjrYYObGkUl+FknW0w==
+-----END CERTIFICATE REQUEST-----
diff --git a/netconf/security/certificates/server.pem b/netconf/security/certificates/server.pem
new file mode 100644
index 0000000..e445c87
--- /dev/null
+++ b/netconf/security/certificates/server.pem
@@ -0,0 +1,47 @@
+-----BEGIN CERTIFICATE-----
+MIIDLjCCAhYCCQCqY7W/syEZVjANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJD
+QTELMAkGA1UECAwCT04xDDAKBgNVBAcMA290dDEOMAwGA1UECgwFY2llbmExCzAJ
+BgNVBAsMAmJwMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTYxMTIzMDMyNTM0WhcN
+MTcxMTIzMDMyNTM0WjBZMQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xDDAKBgNV
+BAcMA290dDEOMAwGA1UECgwFY2llbmExCzAJBgNVBAsMAmJwMRIwEAYDVQQDDAls
+b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBzi+HBwhS
+v6886UDR7nZB6kVxOJ5i3WWWfXuo61DyXD8WDh7tQSjeG4IllZkpDj20DmcKsZ4K
+ms8c2pl4ttsvyoEz0lsKeP9xnRne85cRehgkbAw+SUASner1a3mM3RovvUE+NMMW
+vlyEtMLnYxa3GV8nBiwXmwqcu9FBpsNNPsF92+M+BgFzBntzlunTTB1zjEBkt3uc
+wJYVp07XSn4QZ+DcO+omSCgD1Y3m7vActmUbfxoCLVyWzxSuyLBaJ7rYJw4AQ9ma
++jUYQBvz4rToOxboDUMGUy/zQMJ5u8GDxctgmH/bZ4CJXE1wlakR5ynouSoPhX/i
+uB841K4543sPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKRPdXbSu3L6qxDQW/xe
+FZ1wwB/Oy/vXrBUkEjAWlrgEjbuWOdTwoIX1xsdNFCtKlUkga7zZ3+DmZicDXR1L
+DDbO1PSx1mvB5pSuUliT186BpG7lubecPaZTGaLo6YRHKbTlW0Uy31nlOeleUDNA
+oWd/daFUyc2zIC/LND4euqVRXUnlZx1UlswCar8e9lNbXd/BE9KQF6yYM5+V4eIE
+7HuRCrBERl6St3FNwMXoX9++V+m4nRMplHGCnM4zCQ7c7OptIYayuXq0O2TZGJ27
+zlI3/PjhOLc3IMdV1fo5AwUUZqN3qgJLl9B/XwlaaKToBwrX2SKwy6K8zNAEE2XG
+o+Q=
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAwc4vhwcIUr+vPOlA0e52QepFcTieYt1lln17qOtQ8lw/Fg4e
+7UEo3huCJZWZKQ49tA5nCrGeCprPHNqZeLbbL8qBM9JbCnj/cZ0Z3vOXEXoYJGwM
+PklAEp3q9Wt5jN0aL71BPjTDFr5chLTC52MWtxlfJwYsF5sKnLvRQabDTT7Bfdvj
+PgYBcwZ7c5bp00wdc4xAZLd7nMCWFadO10p+EGfg3DvqJkgoA9WN5u7wHLZlG38a
+Ai1cls8UrsiwWie62CcOAEPZmvo1GEAb8+K06DsW6A1DBlMv80DCebvBg8XLYJh/
+22eAiVxNcJWpEecp6LkqD4V/4rgfONSuOeN7DwIDAQABAoIBAFKArrThpoZreXMJ
+rQFxbUBUNamkO7DEpSlyU8EuI7B42pZN/pZrhbU/qqDLVX8nw9axxuXDhQ7opkCX
+QnwkA50NrQDdjMohhkTcZxYFRos3Ga3vnSqqzi7esRY24XC48xvdijesCFzXPtVz
+igEsqo/ZqisXso99/f+I6OT00/E+hzDCPXn0VzWhNE5g2oDd6OMcn4y5SQ/OQfDl
+bx0sA2H+3bJCkGYAwt/ebMYxStNYx7eJFggq06O+aCKoilrI9U/kkN++qbjkp+p1
+p7FrKypHXuFRuerXOCFs8o9gotT/VEuEZKYfbjMe4442LXmm1lx2wlMtNJ0y9Fbt
+4wiUoJECgYEA5Uo3X7/V1nzHH5+O8odOz7XKyyWAAQHcCyM+CKYL5r+K6GVzVu1S
+pYKKWCWgP7d9HnOGa3GLNQSSjQYaZb4A2p/RTAtPA1EZVuCYfJubwWJQGobhY43m
+FdaBhb507c5EmgTBmCNcNsT+1wAcwSS7ODthYYd1oaYglifvoIB+gwkCgYEA2GHE
+JnJqxez8M1H6lFGAG/PJyKAr35E35kO+U3bjisple+1Aev4mG1WA3XVZI+oyNb3w
+wgGqBSf6e4S92DXYpAlM1zupUI2X7hvt0S3EJt4fGDtAMgx/UQidP+dIYo3yFPhB
+dG/IX8ZtyiaSU4QfWGiKXPZcfUCWYiuRMeQ7G1cCgYB5ifyOOY6tEhkuzxIjxstk
+2LU1N5yBbC3qjXTNes7mMnP8OgKIemYLq4WPseoevDGLWatP+rDSmqjgLfMCKVQk
+jwCPcZMha/Slk4ngy08lk+poFUUV01q6Cqk/brRmdabxiUHhViUENynqZ3nWe5Tt
++rPk0bvtwrSVQ3gGuPXWmQKBgFCN5QYjJljU5vb8F/tLdBPz6DUGXA748wSIR7yu
+z/sFak5+frjLEMiwRi8r1qmmMFcNhSnUkMUmovuE6WAqe6Jly/N8xIW2kM8WGqz7
+CsElhBHzTOzU9jjmqhP2OF5oVa001rlQPBpbttfBEEyDLWAuWwRMuxXuC440hfM6
+8j41AoGASAnCEYRsvWp47YEUCSX/7m1hM6/dm+OQo53w4GtNEgr3iTdSWWTkBilw
+RCvFikM/qW7PMl1/RGs2vX2Ur/0+tFweF1dJ+OJDlio26hD3BpRPPNxpmwEIHoF9
+zZBWjfarNsxkK4AYx9bNifhtEJuaqm1duTYG/OSc9tEJVjoU/bk=
+-----END RSA PRIVATE KEY-----
diff --git a/netconf/security/client_credentials/client_keys b/netconf/security/client_credentials/client_keys
new file mode 100644
index 0000000..c2d9c96
--- /dev/null
+++ b/netconf/security/client_credentials/client_keys
@@ -0,0 +1,3 @@
+vagrant:id_rsa
+voltha:id_rsa
+user:id_rsa
\ No newline at end of file
diff --git a/netconf/security/client_credentials/client_passwords b/netconf/security/client_credentials/client_passwords
new file mode 100644
index 0000000..15776b3
--- /dev/null
+++ b/netconf/security/client_credentials/client_passwords
@@ -0,0 +1,3 @@
+vagrant:vagrant
+voltha:voltha
+user:password
\ No newline at end of file
diff --git a/netconf/security/client_credentials/id_rsa b/netconf/security/client_credentials/id_rsa
new file mode 100644
index 0000000..935df1f
--- /dev/null
+++ b/netconf/security/client_credentials/id_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAwc4vhwcIUr+vPOlA0e52QepFcTieYt1lln17qOtQ8lw/Fg4e
+7UEo3huCJZWZKQ49tA5nCrGeCprPHNqZeLbbL8qBM9JbCnj/cZ0Z3vOXEXoYJGwM
+PklAEp3q9Wt5jN0aL71BPjTDFr5chLTC52MWtxlfJwYsF5sKnLvRQabDTT7Bfdvj
+PgYBcwZ7c5bp00wdc4xAZLd7nMCWFadO10p+EGfg3DvqJkgoA9WN5u7wHLZlG38a
+Ai1cls8UrsiwWie62CcOAEPZmvo1GEAb8+K06DsW6A1DBlMv80DCebvBg8XLYJh/
+22eAiVxNcJWpEecp6LkqD4V/4rgfONSuOeN7DwIDAQABAoIBAFKArrThpoZreXMJ
+rQFxbUBUNamkO7DEpSlyU8EuI7B42pZN/pZrhbU/qqDLVX8nw9axxuXDhQ7opkCX
+QnwkA50NrQDdjMohhkTcZxYFRos3Ga3vnSqqzi7esRY24XC48xvdijesCFzXPtVz
+igEsqo/ZqisXso99/f+I6OT00/E+hzDCPXn0VzWhNE5g2oDd6OMcn4y5SQ/OQfDl
+bx0sA2H+3bJCkGYAwt/ebMYxStNYx7eJFggq06O+aCKoilrI9U/kkN++qbjkp+p1
+p7FrKypHXuFRuerXOCFs8o9gotT/VEuEZKYfbjMe4442LXmm1lx2wlMtNJ0y9Fbt
+4wiUoJECgYEA5Uo3X7/V1nzHH5+O8odOz7XKyyWAAQHcCyM+CKYL5r+K6GVzVu1S
+pYKKWCWgP7d9HnOGa3GLNQSSjQYaZb4A2p/RTAtPA1EZVuCYfJubwWJQGobhY43m
+FdaBhb507c5EmgTBmCNcNsT+1wAcwSS7ODthYYd1oaYglifvoIB+gwkCgYEA2GHE
+JnJqxez8M1H6lFGAG/PJyKAr35E35kO+U3bjisple+1Aev4mG1WA3XVZI+oyNb3w
+wgGqBSf6e4S92DXYpAlM1zupUI2X7hvt0S3EJt4fGDtAMgx/UQidP+dIYo3yFPhB
+dG/IX8ZtyiaSU4QfWGiKXPZcfUCWYiuRMeQ7G1cCgYB5ifyOOY6tEhkuzxIjxstk
+2LU1N5yBbC3qjXTNes7mMnP8OgKIemYLq4WPseoevDGLWatP+rDSmqjgLfMCKVQk
+jwCPcZMha/Slk4ngy08lk+poFUUV01q6Cqk/brRmdabxiUHhViUENynqZ3nWe5Tt
++rPk0bvtwrSVQ3gGuPXWmQKBgFCN5QYjJljU5vb8F/tLdBPz6DUGXA748wSIR7yu
+z/sFak5+frjLEMiwRi8r1qmmMFcNhSnUkMUmovuE6WAqe6Jly/N8xIW2kM8WGqz7
+CsElhBHzTOzU9jjmqhP2OF5oVa001rlQPBpbttfBEEyDLWAuWwRMuxXuC440hfM6
+8j41AoGASAnCEYRsvWp47YEUCSX/7m1hM6/dm+OQo53w4GtNEgr3iTdSWWTkBilw
+RCvFikM/qW7PMl1/RGs2vX2Ur/0+tFweF1dJ+OJDlio26hD3BpRPPNxpmwEIHoF9
+zZBWjfarNsxkK4AYx9bNifhtEJuaqm1duTYG/OSc9tEJVjoU/bk=
+-----END RSA PRIVATE KEY-----
diff --git a/netconf/security/client_credentials/id_rsa.pub b/netconf/security/client_credentials/id_rsa.pub
new file mode 100644
index 0000000..ef0da9f
--- /dev/null
+++ b/netconf/security/client_credentials/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBzi+HBwhSv6886UDR7nZB6kVxOJ5i3WWWfXuo61DyXD8WDh7tQSjeG4IllZkpDj20DmcKsZ4Kms8c2pl4ttsvyoEz0lsKeP9xnRne85cRehgkbAw+SUASner1a3mM3RovvUE+NMMWvlyEtMLnYxa3GV8nBiwXmwqcu9FBpsNNPsF92+M+BgFzBntzlunTTB1zjEBkt3ucwJYVp07XSn4QZ+DcO+omSCgD1Y3m7vActmUbfxoCLVyWzxSuyLBaJ7rYJw4AQ9ma+jUYQBvz4rToOxboDUMGUy/zQMJ5u8GDxctgmH/bZ4CJXE1wlakR5ynouSoPhX/iuB841K4543sP vagrant@voltha
diff --git a/netconf/security/keys/server.key b/netconf/security/keys/server.key
new file mode 100644
index 0000000..924a0df
--- /dev/null
+++ b/netconf/security/keys/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA25erUWmgLl0L8KgGjYEBicFs1Q7rXU9OnsF2Me1/hWYwExIr
+KwTXf/D6GY76k5BcNAlkmpmkeF9qio6+IokTGSzUoG9knCCfGITmziHCAN0m7xsm
+9xXiREtZYW5hyW7USo0RHwXb/ZrvlGzbXyDb0KnYovyVVPihPrEOgtvOnXZGQ5h7
+Hr9+3swdjgwdn+xkO66g4Yr9ul+hhTAqyT8CchFvdZxgyplgbpfuDWd308pjv02e
+bDmDUQrcev78VRQHrIWOQcf6AW9a7OUus3l18rjG4DwNYEYxtDEmEgR+hDVwGueP
+Vxvna/b+tapUaZT+cktSGjQyP5VG2tR5WMNPTQIDAQABAoIBAQC2KKnP/jwHWd5W
+wzu0VIjPGmpKj3bFT82bOkbvAeHf6jYFC/RNWlXd5yvA2e+ZS46BCC2xZbSeuiAq
+UmD9SxJRtvOnJuCh9xmjAFRqOGyzxMDQZoi7PScWmh7KCQQLUem+8ih6wKSOSuFl
+EawkzKAsB0kTY04JT7qKw9uSx/4sdW83gDN/GfEh6mVzoYSkRfD9yfCFmV5kmaca
+C78LYkwrVAIPm5R03l9S+wvuU1CzvArgFYoF1RCqoFmZo1B96aiu8Ld3Mu+als6A
+tE/5IPVL5OEmPUu6rY+WD6N8udLgJ/5k8+EiUraCRyjnc2WsdMA//ZEaKaex8AYB
+xiHG25dhAoGBAP+a+ucND40GC34q7zjGNeELFEUkMOPfeIDwgn264eCZ00FSXIxG
+bMEwyGUYjQGaZivHa5Rjxl8/2MLfK0p8UKvD+gKy2HyZPRrEsSg46CwO6q8/q4MV
+FAuJsVDjLAIchHSc+vNJSIzmcrULxeilug5K+Uje7zWys33viMrFAW4VAoGBANvu
+dMaTUR2UOwY6y5kW+R+cnJHw//Qgn25HfGbKAygLG5PKcOHwdhSFG0hwGm/cudtT
+FoKViHcXaVDPs2GtKtRwiCmIh40NYNcUeUfiLF/o0IMxPAK707XqsBUXM2N/7Iwr
+M5Yr72L0i4d5gvDxdYOkhGSJ4ZY2pO/BJT/6oGJZAoGAJux24NCAWb1d6qynGUI1
+2jtRruW//DN/qkL13CelVgqWvQ/JwCLJVPIU0jBW6LOWc3w+ya4X+1FBtQs/EGy1
+7DI/1EFf7jl3sKfbQoDH+k2bN2y+tYt0Lf2d3u95OCkXkpqFLJ8wSSSeuys7zB3Q
+o3SPWaV5T/gSlU7wydAOU7ECgYEA1Pix3kVz7EfJbHvtAn9LmQAIYg7VXKtml9zj
+q4n9yn4PI2uLKk+3mKFYzSm5gA9nbuqjxqpU7rYeBuJKm9CbxAKA6G9MjBujZkNu
+OlzbibiiihhYO3F1TxlHj/Taj4SmJQoEt2wuMU4L+5GAah/4IxViGxWl/NdHmuAX
+WUAypWkCgYBKTm2Saam8hRi6Pc0T63a3RrBx0pu6TxLv9+7rqBYUaP9kRwZbVnc2
+AH7DW73ZLuVnQL0ymkkVzwDIn6skT322H3g0Cc+ducwLXZ2AaObQNflHoFtSiO85
+a0oWQOtmjqdYfbRIulSMbjQhZ/wAA5Ju4lJI5/0MOxFvfxK5WBNhZQ==
+-----END RSA PRIVATE KEY-----
diff --git a/netconf/security/keys/server.key.pub b/netconf/security/keys/server.key.pub
new file mode 100644
index 0000000..ddab57a
--- /dev/null
+++ b/netconf/security/keys/server.key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDbl6tRaaAuXQvwqAaNgQGJwWzVDutdT06ewXYx7X+FZjATEisrBNd/8PoZjvqTkFw0CWSamaR4X2qKjr4iiRMZLNSgb2ScIJ8YhObOIcIA3SbvGyb3FeJES1lhbmHJbtRKjREfBdv9mu+UbNtfINvQqdii/JVU+KE+sQ6C286ddkZDmHsev37ezB2ODB2f7GQ7rqDhiv26X6GFMCrJPwJyEW91nGDKmWBul+4NZ3fTymO/TZ5sOYNRCtx6/vxVFAeshY5Bx/oBb1rs5S6zeXXyuMbgPA1gRjG0MSYSBH6ENXAa549XG+dr9v61qlRplP5yS1IaNDI/lUba1HlYw09N vagrant@voltha
diff --git a/netconf/utils.py b/netconf/utils.py
new file mode 100644
index 0000000..455a395
--- /dev/null
+++ b/netconf/utils.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-#
+#
+# March 31 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 netconf import NSMAP
+import copy
+from lxml import etree
+
+
+# Tries to somewhat implement RFC6241 filtering
+
+
+def qname(tag):
+    try:
+        return etree.QName(tag)
+    except ValueError:
+        prefix, base = tag.split(":")
+        return etree.QName(NSMAP[prefix], base)
+
+
+def elm(tag, attrib=None, **extra):
+    if attrib is None:
+        attrib = dict()
+    return etree.Element(qname(tag), attrib, **extra)
+
+
+def leaf_elm(tag, value, attrib=None, **extra):
+    e = elm(tag, attrib, **extra)
+    e.text = str(value)
+    return e
+
+
+leaf = leaf_elm
+
+
+def subelm(pelm, tag, attrib=None, **extra):
+    if attrib is None:
+        attrib = dict()
+    return etree.SubElement(pelm, qname(tag), attrib, **extra)
+
+
+def is_selection_node(felm):
+    ftext = felm.text
+    return ftext is None or not ftext.strip()
+
+
+def filter_tag_match(filter_tag, elm_tag):
+    fqname = etree.QName(filter_tag)
+    eqname = qname(elm_tag)
+    if not fqname.namespace:
+        return fqname.localname == eqname.localname
+    return fqname == eqname
+
+
+def filter_node_match_no_value(filter_node, match_elm):
+    # First check to see if tag matches.
+    if not filter_tag_match(filter_node.tag, match_elm.tag):
+        return False
+
+    # Next check for attribute matches.
+    # XXX does this need to filter out namespace attributes?
+    if filter_node.attrib and filter_node.attrib != match_elm.attrib:
+        return False
+
+    return True
+
+
+def filter_node_match(filter_node, match_elm):
+    """Given a filter node element and a nodename and attribute dictionary
+    return true if the filter element matches the elmname, attributes and value
+    (if not None).
+
+    The filter element can use a wildcard namespace or a specific namespace
+    the attributes can be missing from the filter node but otherwise must match
+    and the value is only checked for a match if it is not None.
+    """
+    if not filter_node_match_no_value(filter_node, match_elm):
+        return False
+
+    # Finally check for matching value.
+    ftext = filter_node.text
+    if ftext is None:
+        return True
+
+    ftext = ftext.strip()
+    if not ftext:
+        return True
+
+    return ftext == match_elm.text
+
+
+def filter_leaf_values(fcontain_elm, dest_node, leaf_elms, append_to):
+    """Given a containment element (or None) verify that all leaf elements
+    in leaf_elms either match, have corresponding selection nodes (empty)
+    or are not present.
+
+    Additionally the correct leaf data will be added to dest_node, and dest_node
+    will be appended to append_to if append_to is not None.
+
+    The return value with be True, False, or a possibly empty set of selection/containment nodes
+    The only failing value is False, if True is returned then the caller should include all
+    containment sibling nodes, otherwise the caller should process the list of containment/selection
+    nodes.
+    """
+    children = fcontain_elm.getchildren() if fcontain_elm is not None else []
+    selected_elms = []
+    if not children:
+        selected_elms = leaf_elms
+
+    # Now look at all the leaf filter selector or match nodes
+    include_all_leaves = True
+    othernodes = []
+    for felm in children:
+        fchildren = felm.getchildren()
+        for lelm in leaf_elms:
+            if fchildren:
+                # Verify that this doesn't match a leaf node.
+                if filter_node_match_no_value(felm, lelm):
+                    # XXX this is an error we should raise some exception.
+                    return False
+                continue
+            elif filter_node_match(felm, lelm):
+                if not felm.text:
+                    # This was a selection node.
+                    include_all_leaves = False
+
+                selected_elms.append(lelm)
+                break
+        else:
+            if fchildren:
+                # This is OK we verified a containment filter didn't match leaf by getting here.
+                if felm.text:
+                    # XXX verify that there is no text on this node, report violation?
+                    return False
+
+                # Track selection/filter nodes
+                include_all_leaves = False
+                othernodes.append(felm)
+            elif not felm.text:
+                # This is OK as it means this is a selection node include it in othernodes
+                include_all_leaves = False
+                othernodes.append(felm)
+            else:
+                # We've exhausted all leaf elements to match this leaf filter so we failed.
+                return False
+
+    # Everything matched so add in the leaf data.
+    if append_to is not None:
+        append_to.append(dest_node)
+
+    if include_all_leaves:
+        dest_node.extend(leaf_elms)
+    else:
+        dest_node.extend(selected_elms)
+
+    if include_all_leaves:
+        return True
+    return othernodes
+
+
+def filter_containment_iter(fcontain_elm, dest_node, containment_nodes,
+                            leaf_elms, append_to):
+    """Given a containment filter node (or None) verify that all leaf elements
+    either match, have corresponding selection nodes (empty) or are not present.
+
+    If all leaf criteria are met then the iterator will return a triple of
+    (new_filter_node, new_dest_node, new_data). new_filter_node corresponds to the
+    matched containment node which is returned in new_dest_node, and new_data will be
+    an element corresponding to the passed in dest_node.
+
+    These should be processed by calling filter_containment_iter again.
+
+    Additionally the correct leaf data will be added to dest_node, and dest_node
+    will be appended to append_to if append_to is not None.
+
+    This implements RFC6241 section 6.2.5
+    """
+    # No containment node so add everything.
+    if fcontain_elm is None:
+        # Add in the leaf data
+        for e in leaf_elms:
+            dest_node.append(e)
+
+        # Append the match_node to the data
+        if append_to is not None:
+            append_to.append(dest_node)
+
+        for node in containment_nodes:
+            yield None, copy.copy(node), dest_node
+
+    else:
+        othernodes = filter_leaf_values(fcontain_elm, dest_node, leaf_elms,
+                                        append_to)
+        if othernodes is False:
+            # No match
+            pass
+        elif othernodes is True:
+            # All leaf values have matched and have been added and we should include all containers
+            for node in containment_nodes:
+                yield None, copy.copy(node), dest_node
+        else:
+            for felm in othernodes:
+                for node in containment_nodes:
+                    if filter_node_match_no_value(felm, node):
+                        yield felm, copy.copy(node), dest_node
+
+
+def filter_leaf_allows_add(filter_elm, tag, data, value):
+    if filter_leaf_allows(filter_elm, tag, value):
+        data.append(leaf_elm(tag, value))
+        return True
+    return False
+
+
+def filter_leaf_allows(filter_elm, xpath, value):
+    """Check the value at the xpath specified leaf matches the value.
+
+    If filter_elm is None then allow.
+    If there is no xpath element then allow if there are no other children.
+    XXX what about xpath that has embedded predicates!
+        perhaps what we want to call this is a normal path not an xpath.
+    """
+    if filter_elm is None:
+        return True
+
+    # If there are no children then allow everything.
+    if not filter_elm.getchildren():
+        return True
+
+    # No match or multiple matches not allowed for leaf.
+    flist = filter_elm.xpath(xpath, namespaces=NSMAP)
+    if not flist or len(flist) > 1:
+        return False
+    felm = flist[0]
+
+    # No children for leaf allowed (leaf)
+    if felm.getchildren():
+        return False
+
+    # Allowed if empty or if value matches.
+    if not felm.text or felm.text == str(value):
+        return True
+
+    return False
+
+
+def filter_list_iter(filter_list, key_xpath, keys):
+    """Return key, elm pairs that are allowed by keys using the values found using the given key_xpath"""
+    # If we have no filter elm then return all keys.
+    if filter_list is None:
+        for key in keys:
+            yield key, None
+
+    try:
+        # If this an element then make it a list of elements
+        filter_list.xpath  # pylint: disable=W0104
+        filter_list = [filter_list]
+    except AttributeError:
+        pass
+
+    for filter_elm in filter_list:
+        filter_elms = [x for x in
+                       filter_elm.xpath(key_xpath, namespaces=NSMAP)]
+        filter_keys = [x.text for x in filter_elms]
+        if not filter_keys:
+            for key in keys:
+                yield key, filter_elm
+        else:
+            # Now walk our keys returning any that are in the filter list.
+            for key in keys:
+                if key in filter_keys:
+                    yield key, filter_elm
+                    # try:
+                    #     idx = filter_keys.index(str(key))
+                    #     yield key, filter_elm
+                    # except ValueError:
+                    #     pass
+
+
+__author__ = 'Christian Hopps'
+__date__ = 'March 31 2015'
+__version__ = '1.0'
+__docformat__ = "restructuredtext en"