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"