Hello,
My data provider application provides access to an external database through the Conf-D Data Provider API with cb_get_object() and cb_find_next_object() callbacks. The application is written in Python and uses the Conf-D Python API.
When the data provider application throws an exception, Conf-D handles NETCONF error reporting differently depending on the callbacks. When an exception is raised in cb_get_object(), the Conf-D NETCONF server returns an . When the same exception is raised in cb_find_next_object(), the Conf-D NETCONF server terminates the connection without sending an . Please see the example provided below.
Conf-D should be sending an for exceptions raised in cb_find_next_object(), correct? Is there something missing from my cb_find_next_object() to trigger Conf-D to send the ?
Response for cb_next_object with exception
$ make query-one
/opt/confd/bin/netconf-console --host localhost --port 830 --get-config -x /my-list/my-entry[name=\'TEST00000002\']
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1">
<rpc-error>
<error-type>application</error-type>
<error-tag>operation-failed</error-tag>
<error-severity>error</error-severity>
<error-path xmlns:my-test-module="urn:my:yang:my-test-module">
/my-test-module:my-list/my-test-module:my-entry[my-test-module:name='TEST00000002']
</error-path>
<error-message xml:lang="en">/my-list/my-entry[name='TEST00000002']: Python cb_get_object error. ('intt-data',)</error-message>
<error-info>
<bad-element>my-entry</bad-element>
</error-info>
</rpc-error>
</rpc-reply>
Response for cb_find_next_object with exception
/opt/confd/bin/netconf-console --host localhost --port 830 --get-config -x /my-list/my-entry
No data received in the reply.
my_test_module.py
from __future__ import print_function
import argparse
import select
import socket
import sys
import textwrap
import traceback
import _confd
import _confd.dp as dp
import _confd.maapi as maapi
from my_test_module_ns import ns as my_test_module_ns
def ERROR(*args, **kwargs):
print("ERROR:", end = '')
print(*args, **kwargs)
# Test Data
my_list = {
"TEST00000001": {
"str-data": "some data (1)",
"int-data": 1,
},
"TEST00000002": {
"str-data": "some data (2)",
"int-data": 2,
},
"TEST00000003": {
"str-data": "some data (3)",
"int-data": 3,
}
}
my_list_keys = {}
prev_key = None
for next_key in my_list.keys():
if prev_key:
my_list_keys[prev_key] = next_key
prev_key = next_key
my_list_keys[prev_key] = None
def get_first_key():
global my_list_keys
if my_list_keys:
next_key = next(iter(my_list_keys))
else:
next_key = None
return next_key
def get_next_key(key):
global my_list_keys
if my_list_keys:
next_key = my_list_keys[key]
else:
next_key = None
return next_key
def make_tag_value(tag, init, vtype):
"""
Wrapper to create a _confd.TagValue
"""
return _confd.TagValue(
_confd.XmlTag(my_test_module_ns.hash, tag),
_confd.Value(init, vtype))
def dp_op_to_str(op):
op_to_str = {
1: "C_SET_ELEM",
2: "C_CREATE",
3: "C_REMOVE",
4: "C_SET_CASE",
5: "C_SET_ATTR",
6: "C_MOVE_AFTER",
}
try:
op_str = op_to_str[op]
except KeyError:
op_str = 'UNKNOWN'
return op_str
V = _confd.Value
# call statistics for each fo the registered data callbacks to keep tabs on
# how many times we access the different cb functions to show in the CLI
K_GET_OBJ = 0
K_FIND_NEXT = 1
K_FIND_NEXT_OBJ = 2
K_NUM_INSTANCES = 3
K_SET_ELEM = 4
K_CREATE = 5
K_REMOVE = 6
calls_keys = [K_GET_OBJ, K_FIND_NEXT, K_FIND_NEXT_OBJ,
K_NUM_INSTANCES, K_SET_ELEM, K_CREATE, K_REMOVE]
dp_calls = {k: 0 for k in calls_keys}
class TransCbs(object):
# transaction callbacks
#
# The installed init() function gets called every time ConfD
# wants to establish a new transaction, Each NETCONF
# command will be a transaction
#
# We can choose to create threads here or whatever, we
# can choose to allocate this transaction to an already existing
# thread. We must tell ConfD which file descriptor should be
# Used for all future communication in this transaction
# this has to be done through the call confd_trans_set_fd();
def __init__(self, workersocket):
self._workersocket = workersocket
def cb_init(self, tctx):
dp.trans_set_fd(tctx, self._workersocket)
return _confd.CONFD_OK
# This callback gets invoked at the end of the transaction
# when ConfD has accumulated all write operations
# we're guaranteed that
# a) no more read ops will occur
# b) no other transactions will run between here and tr_finish()
# for this transaction, i.e ConfD will serialize all transactions
# since we need to be prepared for abort(), we may not write
# our data to the actual database, we can choose to either
# copy the entire database here and write to the copy in the
# following write operations _or_ let the write operations
# accumulate operations create(), set(), delete() instead of actually
# writing
# If our db supports transactions (which it doesn't in this
# silly example, this is the place to do START TRANSACTION
def cb_write_start(self, tctx):
return _confd.CONFD_OK
def cb_prepare(self, tctx):
return _confd.CONFD_OK
def cb_commit(self, tctx):
return _confd.CONFD_OK
def cb_abort(self, tctx):
return _confd.CONFD_OK
def cb_finish(self, tctx):
return _confd.CONFD_OK
class MyTestModuleDataCbs(object):
""" Data provider callbacks for the my-test-list list of YANG model. """
def __init__(self):
pass
def cb_get_object(self, tctx, kp):
dp_calls[K_GET_OBJ] += 1
print()
print(f" cb_get_object '{kp}' {type(kp)}")
list_filter = dp.data_get_list_filter(tctx)
print(f" filter '{list_filter}' {type(list_filter)}")
key = str(kp[0][0])
if key in my_list:
entry = my_list[key]
vals = [
make_tag_value(my_test_module_ns.my_test_module_name, key, _confd.C_STR),
make_tag_value(my_test_module_ns.my_test_module_str_data, entry['str-data'], _confd.C_STR),
# The following results in a Python KeyError exception
make_tag_value(my_test_module_ns.my_test_module_int_data, entry['intt-data'], _confd.C_INT32),
]
dp.data_reply_tag_value_array(tctx, vals)
else:
dp.data_reply_not_found(tctx)
return _confd.CONFD_OK
def cb_find_next(self, tctx, kp, type_, keys):
dp_calls[K_FIND_NEXT] += 1
print()
print(f" cb_find_next '{kp}' {type(kp)}, {type_}, {keys}")
list_filter = dp.data_get_list_filter(tctx)
print(f" filter '{list_filter}' {type(list_filter)}")
try:
key = None
if keys:
key = keys[0].as_pyval()
if key:
next_key = get_next_key(key)
else:
next_key = get_first_key()
if next_key:
next_key_list = [V(next_key, _confd.C_BUF)] # key of the host is its name
else:
next_key_list = None
dp.data_reply_next_key(tctx, next_key_list, -1)
except Exception as e:
ERROR(e)
ERROR(traceback.format_exc())
raise
return _confd.CONFD_OK
def cb_find_next_object(self, tctx, kp, type_, keys):
dp_calls[K_FIND_NEXT_OBJ] += 1
print()
print(f" cb_find_next_object '{kp}' {type(kp)}, {type_}, {keys}")
list_filter = dp.data_get_list_filter(tctx)
print(f" filter '{list_filter}' {type(list_filter)}")
objs = []
idx = 0
for key, entry in my_list.items():
objs.append(([
make_tag_value(my_test_module_ns.my_test_module_name, key, _confd.C_STR),
make_tag_value(my_test_module_ns.my_test_module_str_data, entry['str-data'], _confd.C_STR),
# The following results in a Python KeyError exception
make_tag_value(my_test_module_ns.my_test_module_int_data, entry['intt-data'], _confd.C_INT32),
], idx))
idx += 1
if objs:
objs.append((None, -1))
dp.data_reply_next_object_tag_value_arrays(tctx, objs, 0)
else:
dp.data_reply_next_object_tag_value_arrays(tctx, None, -1)
return _confd.CONFD_OK
def cb_num_instances(self, tctx, kp):
dp_calls[K_NUM_INSTANCES] += 1
print()
print(f" cb_num_instances '{kp}'")
list_filter = dp.data_get_list_filter(tctx)
print(f" filter '{list_filter}' {type(list_filter)}")
count = len(my_list)
v_count = V(count, _confd.C_INT32)
dp.data_reply_value(tctx, v_count)
return _confd.CONFD_OK
def cb_set_elem(self, tctx, kp, newval):
dp_calls[K_SET_ELEM] += 1
return _confd.ACCUMULATE
def cb_create(self, tctx, kp):
dp_calls[K_CREATE] += 1
return _confd.ACCUMULATE
def cb_remove(self, tctx, kp):
dp_calls[K_REMOVE] += 1
return _confd.ACCUMULATE
def run(debuglevel):
# In C we use confd_init() which sets the debug-level, but for Python the
# call to confd_init() is done when we do 'import confd'.
# Therefore we need to set the debug level here:
_confd.set_debug(debuglevel, sys.stderr)
# init library
daemon_ctx = dp.init_daemon('my_daemon')
confd_addr = '127.0.0.1'
confd_port = _confd.PORT
managed_path = '/'
maapisock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
ctlsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
wrksock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
try:
dp.connect(daemon_ctx, ctlsock, dp.CONTROL_SOCKET, confd_addr,
confd_port, managed_path)
dp.connect(daemon_ctx, wrksock, dp.WORKER_SOCKET, confd_addr,
confd_port, managed_path)
maapi.connect(maapisock, confd_addr, confd_port, managed_path)
maapi.load_schemas(maapisock)
transaction_cb = TransCbs(wrksock)
dp.register_trans_cb(daemon_ctx, transaction_cb)
# database_cb = DatabaseCbs(wrksock, db)
# dp.register_db_cb(daemon_ctx, database_cb)
data_cb = MyTestModuleDataCbs()
dp.register_data_cb(
daemon_ctx,
my_test_module_ns.callpoint_my_test_list,
data_cb,
flags=dp.DATA_WANT_FILTER)
dp.register_done(daemon_ctx)
try:
_r = [ctlsock, wrksock]
_w = []
_e = []
while True:
print("Waiting for requests...")
(r, w, e) = select.select(_r, _w, _e, 1)
for rs in r:
if rs.fileno() == ctlsock.fileno():
try:
dp.fd_ready(daemon_ctx, ctlsock)
except _confd.error.Error as e:
if e.confd_errno is not _confd.ERR_EXTERNAL:
raise e
elif rs.fileno() == wrksock.fileno():
try:
dp.fd_ready(daemon_ctx, wrksock)
except _confd.error.Error as e:
if e.confd_errno is not _confd.ERR_EXTERNAL:
raise e
except KeyboardInterrupt:
print('\nCtrl-C pressed\n')
finally:
ctlsock.close()
wrksock.close()
dp.release_daemon(daemon_ctx)
if __name__ == '__main__':
debug_levels = {
'q': _confd.SILENT,
'd': _confd.DEBUG,
't': _confd.TRACE,
'p': _confd.PROTO_TRACE,
}
parser = argparse.ArgumentParser(add_help=False,formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument( "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit.")
parser.add_argument("-dl", "--debuglevel", dest="debuglevel", action="store", default='t', required=False, choices=debug_levels.keys(),
help=textwrap.dedent(
'''\
set the debug level:
q = quiet (i.e. no) debug
d = debug level debug
t = trace level debug
p = proto level debug
'''))
parser.parse_args()
args = parser.parse_args()
confd_debug_level = debug_levels.get(args.debuglevel, _confd.TRACE)
run(confd_debug_level)
Makefile
######################################################################
# Introduction example 1-2-3-start-query-model
# (C) 2006 Tail-f Systems
#
# See the README files for more information
######################################################################
usage:
@echo "See README files for more instructions"
@echo "make all Build all example files"
@echo "make clean Remove all built and intermediary files"
@echo "make start Start CONFD daemon and example agent"
@echo "make stop Stop any CONFD daemon and example agent"
@echo "make query-one Run query for one object, cb_get_object"
@echo "make query-all Run query for one object, cb_find_next_object"
######################################################################
# Where is ConfD installed? Make sure CONFD_DIR points it out
CONFD_DIR ?= ../../..
# Include standard ConfD build definitions and rules
include $(CONFD_DIR)/src/confd/build/include.mk
# In case CONFD_DIR is not set (correctly), this rule will trigger
$(CONFD_DIR)/src/confd/build/include.mk:
@echo 'Where is ConfD installed? Set $$CONFD_DIR to point it out!'
@echo ''
######################################################################
# Example specific definitions and rules
CONFD_FLAGS = --addloadpath $(CONFD_DIR)/etc/confd
START_FLAGS ?=
all: c-all
@echo "Build complete"
common-all: $(CDB_DIR) ssh-keydir
c-all: common-all my-test-module.fxs my_test_module_ns.py
@echo "C build complete"
my-test-module.fxs: my-test-module.yang my-test-module-ann.yang
$(CONFDC) --fail-on-warnings -a my-test-module-ann.yang -c -o my-test-module.fxs my-test-module.yang
my_test_module_ns.py: my-test-module.fxs
$(CONFDC) --emit-python my_test_module_ns.py my-test-module.fxs
######################################################################
clean: iclean
-rm -rf *log *trace cli-history 2> /dev/null || true
-rm -rf my_test_module_ns.py *.pyc __init__.py __pycache__ 2> /dev/null || true
######################################################################
#start: stop start_confd start_subscriber
start: stop
$(CONFD) -c ./confd.conf $(CONFD_FLAGS)
### * In one terminal window, run: tail -f ./confd.log
### * In another terminal window, run queries
### (try 'make query' for an example)
### * In this window, the HOSTS confd daemon now starts:
$(PYTHON) my_test_module.py --debuglevel t
######################################################################
stop:
### Killing any confd daemon or my-test-module confd agents
$(CONFD) --stop || true
$(KILLALL) `pgrep -f "$(PYTHON) my_test_module.py"` || true
######################################################################
query-one:
$(CONFD_DIR)/bin/netconf-console --host localhost --port 830 --get-config -x /my-list/my-entry[name=\'TEST00000002\']
######################################################################
query-all:
$(CONFD_DIR)/bin/netconf-console --host localhost --port 830 --get-config -x /my-list/my-entry
######################################################################
Module
module my-test-module {
yang-version 1.1;
namespace "urn:my:yang:my-test-module";
prefix "my-test-module";
organization "My Test YANG Module.";
contact
"My Test YANG Module.
";
description
"My Test YANG Module.";
revision 2022-08-26 {
reference "My Test YANG Module.";
}
container my-list {
description "An example of a list.";
list my-entry {
key name;
description "An example of a list entry.";
leaf name {
type string;
description "Name identifying a list entry.";
}
leaf str-data {
type string;
description "String data for this list entry.";
}
leaf int-data {
type int32;
description "Integer data for this list entry.";
}
}
}
}