关于使用libnetconf自定义RPC的问题记录

1. SDN-C采集性能数据失败

SDN-C控制器通过RPC方法采集vCPE性能数据失败,已知信息如下所示:

下发的rpc消息:

1
2
3
4
5
6
7
<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2">
<getdataflow xmlns="http://www.raisecom.com/cloudvpn">
<entrys>
<entry-name>cloud</entry-name>
</entrys>
</getdataflow>
</rpc>

返回的rpc-reply消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2">
<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<dataflow>
<entrys>
<entryname>cloud</entryname>
<leftBandwidth>0</leftBandwidth>
<leftTotalFlow>0</leftTotalFlow>
<rightBandwidth>0</rightBandwidth>
<rightTotalFlow>0</rightTotalFlow>
<time-stamp>1516095468</time-stamp>
</entrys>
</dataflow>
</data>
</rpc-reply>

返回的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<errors xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf">
<error>
<error-type>application</error-type>
<error-tag>operation-failed</error-tag>
<error-message>The operation encountered an unexpected error while executing.</error-message>
<error-info>java.lang.IllegalStateException: Unknown child(ren) node(s) detected, identified by: (urn:ietf:params:xml:ns:netconf:base:1.0)data, in: RPC Output output
at com.google.common.base.Preconditions.checkState(Preconditions.java:197)
at org.opendaylight.yangtools.yang.data.impl.schema.SchemaUtils.findSchemaForChild(SchemaUtils.java:109)
at org.opendaylight.yangtools.yang.data.impl.schema.SchemaUtils.findSchemaForChild(SchemaUtils.java:91)
at org.opendaylight.yangtools.yang.data.impl.schema.SchemaUtils.findSchemaForChild(SchemaUtils.java:97)
at org.opendaylight.yangtools.yang.data.impl.schema.transform.base.parser.ContainerNodeBaseParser.getSchemaForChild(ContainerNodeBaseParser.java:57)
at org.opendaylight.yangtools.yang.data.impl.schema.transform.base.parser.ContainerNodeBaseParser.getSchemaForChild(ContainerNodeBaseParser.java:29)
at org.opendaylight.yangtools.yang.data.impl.schema.transform.base.parser.BaseDispatcherParser.parse(BaseDispatcherParser.java:160)
at org.opendaylight.yangtools.yang.data.impl.schema.transform.base.parser.ContainerNodeBaseParser.parse(ContainerNodeBaseParser.java:47)
at org.opendaylight.yangtools.yang.data.impl.schema.transform.base.parser.ContainerNodeBaseParser.parse(ContainerNodeBaseParser.java:29)
at org.opendaylight.netconf.sal.connect.netconf.schema.mapping.NetconfMessageTransformer.toRpcResult(NetconfMessageTransformer.java:362)
at org.opendaylight.netconf.sal.connect.netconf.schema.mapping.NetconfMessageTransformer.toRpcResult(NetconfMessageTransformer.java:72)
at org.opendaylight.netconf.sal.connect.netconf.sal.NetconfDeviceRpc$2.apply(NetconfDeviceRpc.java:69)
at org.opendaylight.netconf.sal.connect.netconf.sal.NetconfDeviceRpc$2.apply(NetconfDeviceRpc.java:65)
at com.google.common.util.concurrent.Futures$2.apply(Futures.java:760)
at com.google.common.util.concurrent.Futures$ChainingListenableFuture.run(Futures.java:906)
at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:457)
at com.google.common.util.concurrent.ExecutionList.executeListener(ExecutionList.java:156)
at com.google.common.util.concurrent.ExecutionList.execute(ExecutionList.java:145)
at com.google.common.util.concurrent.AbstractFuture.set(AbstractFuture.java:185)
at org.opendaylight.netconf.sal.connect.netconf.listener.UncancellableFuture.set(UncancellableFuture.java:44)
at org.opendaylight.netconf.sal.connect.netconf.listener.NetconfDeviceCommunicator.processMessage(NetconfDeviceCommunicator.java:295)
at org.opendaylight.netconf.sal.connect.netconf.listener.NetconfDeviceCommunicator.onMessage(NetconfDeviceCommunicator.java:237)
at org.opendaylight.netconf.sal.connect.netconf.listener.NetconfDeviceCommunicator.onMessage(NetconfDeviceCommunicator.java:47)
at org.opendaylight.netconf.nettyutil.AbstractNetconfSession.handleMessage(AbstractNetconfSession.java:64)
at org.opendaylight.netconf.nettyutil.AbstractNetconfSession.handleMessage(AbstractNetconfSession.java:35)
at org.opendaylight.protocol.framework.AbstractProtocolSession.channelRead0(AbstractProtocolSession.java:53)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:105)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:318)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:304)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:276)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:263)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:318)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:304)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:276)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:263)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:318)
at io.netty.channel.AbstractChannelHandlerContext.access$600(AbstractChannelHandlerContext.java:42)
at io.netty.channel.AbstractChannelHandlerContext$7.run(AbstractChannelHandlerContext.java:309)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:358)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:357)
at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:112)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
at java.lang.Thread.run(Thread.java:748)
</error-info>
</error>
</errors>

分析:

我们自定义RPC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 	rpc getdataflow {
description
"Get the tenant data flowing.";
input {
list entrys {
key "entry-name";

leaf entry-name {
type string;
mandatory true;
description
"the name of service.";
}
}
}

output {
container dataflow {
list entrys {
key "entryname";

leaf entryname {
type string;
}

leaf leftBandwidth {
type uint64;
}

leaf leftTotalFlow {
type uint64;
}

leaf rightBandwidth {
type uint64;
}

leaf rightTotalwidth {
type uint64;
}

leaf time-stamp {
type yang:date-and-time;
}
}
}
}
}

根据上面定义的RPC中output和rpc-reply的消息来看,没有什么问题,但是从SDN-C返回的错误来看,SDN-C不识别rpc-reply中的data节点。
那就有个问题,在处理RPC方法reply的时候,调用的都是libnetconf提供的接口

libnetconf提供的reply的接口:

  • nc_reply_data()
  • ncxml_reply_data()
  • nc_reply_data_ns()
  • ncxml_reply_data_ns()
  • 而这些接口都是带data的

背景:

我们在使用的时候,也都是直接调用这些接口去reply的,在自测试的时候,都是使用的是netopeer-server和netopeer-cli,顶多就是EMS/ASC(Juniper提供的客户端),而不是真正意义上的SDN-C(目前来看,libnetconf对于RPC这块的支持不是很好,没有根据标准的netconf协议来对reply做校验),因此,自测试不会报错。而和北京联调采集vCPE性能数据的时候,北京使用的是SDN-C,SDN-C是完全按照netconf协议的。

测试:

我们在RPC定义的output中加入data节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 	rpc getdataflow {
description
"Get the tenant data flowing.";
input {
list entrys {
key "entry-name";

leaf entry-name {
type string;
mandatory true;
description
"the name of service.";
}
}
}

output {
container data {
container dataflow {
list entrys {
key "entryname";

leaf entryname {
type string;
}

leaf leftBandwidth {
type uint64;
}

leaf leftTotalFlow {
type uint64;
}

leaf rightBandwidth {
type uint64;
}

leaf rightTotalwidth {
type uint64;
}

leaf time-stamp {
type yang:date-and-time;
}
}
}
}
}
}

这样进行了测试之后,发现SDN-C是可以识别reply的。
那这个问题就简单了,有两种做法:

  1. 每次写RPC的output时,在需要返回的内容外包一层container data即可。
  2. 能不能在reply的时候去掉这个data,如果可以去掉,那对以后写RPC就很方便了。

    那么,这个data到底是否需要呢?

data在RPC里output中是否需要?

需要回答这个问题,很简单,可以直接翻看netconf协议,看官方文档中是如何描述的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[rfc 6020]
4.2.9. RPC Definitions
YANG allows the definition of NETCONF RPCs. The operations’ names,
input parameters, and output parameters are modeled using YANG data
definition statements.
YANG Example:
rpc activate-software-image {
input {
leaf image-name {
type string;
}
}
output {
leaf status {
type string;
}
}
}
NETCONF XML Example:
<rpc message-id="101"
xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<activate-software-image xmlns="http://acme.example.com/system">
<image-name>acmefw-2.3</image-name>
</activate-software-image>
</rpc>
<rpc-reply message-id="101"
xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<status xmlns="http://acme.example.com/system">
The image acmefw-2.3 is being installed.
</status>
</rpc-reply>

从rfc给的rpc example来看,rpc-reply不带data,那就跟着标准协议走。

解决方法1:

通过在github上查找答案得之,libnetconf当前对自定义RPC支持不是很好,具体可以查看
github上作者的回答,最终给出的结论是使用nc_reply_build()这个接口,查了一下函数原型:

nc_reply nc_reply_build(const char reply_dump);

函数入参为const char*,那么 reply_dump需要自己去拼接xml字符串。通过验证,这样做是可行的(不带data的rpc-reply)。

那么就有一个问题,采集一条业务的流量带宽信息可以使用字符串拼接的方法,那么采集10条、100条难道也是用拼接吗?所以就有了方法2。

解决方法2:

libnetconf中提供了返回data的reply,那么我们看一下源代码,看这些个方法是怎么实现的,以ncxml_reply_data_ns()为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
API nc_reply *ncxml_reply_data_ns(const xmlNodePtr data, const char* ns)
{
nc_reply *reply;
xmlNodePtr content;
xmlNsPtr xmlns;

content = xmlNewNode(NULL, BAD_CAST "data");
if (content == NULL) {
ERROR("xmlNewNode failed (%s:%d).", __FILE__, __LINE__);
return (NULL);
}

if (data && xmlAddChildList(content, xmlCopyNodeList(data)) == NULL) {
ERROR("xmlAddChildList failed (%s:%d).", __FILE__, __LINE__);
xmlFreeNode(content);
return (NULL);
}

/* set namespace */
xmlns = xmlNewNs(content, (xmlChar *) ns, NULL);
xmlSetNs(content, xmlns);

reply = (nc_reply*)nc_msg_create(content,"rpc-reply");
reply->type.reply = NC_REPLY_DATA;
xmlFreeNode(content);

return (reply);

}

从代码中可以看到,我们在调用这个接口reply的时候,会自动加上data这个节点,那么我们给它去掉即可。
我们仿照这个代码,写一个ncxml_reply_no_data_ns()的接口,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
API nc_reply *ncxml_reply_no_data_ns(const xmlNodePtr data, const char* ns)
{
nc_reply *reply;
xmlNsPtr xmlns;

/* set namespace */
xmlns = xmlNewNs(data, (xmlChar *) ns, NULL);
xmlSetNs(data, xmlns);

reply = (nc_reply*)nc_msg_create(data, "rpc-reply");
reply->type.reply = NC_REPLY_DATA;

return (reply);
}

一切都是这么顺利的进行,没有问题,但是在最后测试时候,每次netopeer-cli下发获取流量的rpc时,netopeer-server就会宕机。

小背景:

在说明这个问题之前,我先说明一下程序的编译环境和运行环境:

如上图所示,运行我们的cloudvpn程序,需要netopeer、libnetconf、cloudvpn三个模块:

  1. netopeer:在VM1上编译;
  2. libnetconf:加入ncxml_reply_no_data_ns()在VM2上面编译;
  3. cloudvpn:在VM3上面编译

注:cloudvpn编译和netopeer编译&运行都需要依赖libnetconf

最终所有的程序在VM4上运行。

netopeer-server宕机问题:

通过产生的core文件来看:

1
2
3
4
5
6
7
#0  0x00007f833e8ad47d in ncds_apply_rpc (id=1681692778, session=session@entry=0x7f8330005cb0, rpc=rpc@entry=0x7f8330005460, 
shared_filter=shared_filter@entry=0x0) at src/datastore.c:6367
#1 0x00007f833e8afcd1 in ncds_apply_rpc2all (session=0x7f8330005cb0, rpc=0x7f8330005460, ids=0x0) at src/datastore.c:6589
#2 0x0000000000408970 in np_ssh_client_netconf_rpc ()
#3 0x0000000000404362 in client_main_thread ()
#4 0x00007f833e2f9dc5 in start_thread () from /lib64/libpthread.so.0
#5 0x00007f833e026ced in clone () from /lib64/libc.so.6

找到在libnetconf中调用rpc的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
case NC_OP_UNKNOWN:
/* get operation name */
op_name = nc_rpc_get_op_name (rpc);

printf("==%s, %d.\n", __func__, __LINE__);
/* prepare for case RPC is not supported by this datastore */
reply = NCDS_RPC_NOT_APPLICABLE;
/* go through all RPC implemented by datastore's transAPI modules */
for (tapi_iter = ds->transapis; tapi_iter != NULL; tapi_iter = tapi_iter->next) {
for (i = 0; i < tapi_iter->tapi->rpc_clbks->callbacks_count; i++) {
/* find matching rpc and call rpc callback function */
rpc_name = tapi_iter->tapi->rpc_clbks->callbacks[i].name;
if (strcmp(op_name, rpc_name) == 0) {
nc_verb_verbose("rpc_name:%s", rpc_name);
/* get operation node */
op_node = ncxml_rpc_get_op_content(rpc);
op_input = xmlCopyNodeList(op_node->children);
xmlFreeNode(op_node);

/* call RPC callback function */
VERB("Calling %s RPC function\n", rpc_name);
/* 请注意这里reply*/
reply = tapi_iter->tapi->rpc_clbks->callbacks[i].func(op_input);
VERB("reply[%p]\n", reply);
xmlFreeNodeList(op_input);

/* end RPC search, there can be only one RPC with name == op_name */
break;
}
}
if (i >= tapi_iter->tapi->rpc_clbks->callbacks_count) {
/* propagate break to outer for loop */
break;
}
}

free(op_name);
break;

调用rpc方法返回一个结构体指针,nc_reply* reply;
返回的reply在下面使用的时候宕机了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* if transapi used, rpc affected running and succeeded get its actual content */
/*
* skip transapi if <edit-config> was performed with test-option set
* to test-only value
*/
if (ds->transapis != NULL && ds->tapi_callbacks_count
&& (op == NC_OP_COMMIT || op == NC_OP_COPYCONFIG || (op == NC_OP_EDITCONFIG && (nc_rpc_get_testopt(rpc) != NC_EDIT_TESTOPT_TEST))) &&
(nc_rpc_get_target(rpc) == NC_DATASTORE_RUNNING && nc_reply_get_type(reply) == NC_REPLY_OK)) {

if (op == NC_OP_EDITCONFIG) {
erropt = nc_rpc_get_erropt(rpc);
} else { /* commit or copy-config */
/* try rollback to keep transactions atomic */
erropt = NC_EDIT_ERROPT_ROLLBACK;
}

if ((new_reply = ncds_apply_transapi(ds, session, old, erropt, NULL)) != NULL) {
nc_reply_free(reply);
reply = new_reply;
}
}

在上面代码中调用nc_reply_get_type(reply)产生宕机,说明返回的reply的指针不对或者是NULL,后经加入判断,发现reply指针不是NULL。那么解决这个问题只需要把这个reply地址打出来就好。

1
2
3
4
5
6
7
8
9
10
11
12
netopeer-server[9196]: 9196-V  1-15 15:22:59 start netopeer_send_info_to_ipsec.
netopeer-server[9196]: 9196-V 1-15 15:22:59 end netopeer_send_info_to_ipsec.
netopeer-server[9196]: 9196-V 1-15 15:22:59 enter recv_bandwidth_msg_from_ipsec.
netopeer-server[9196]: 9196-V 1-15 15:22:59 stReplyNum.usMsgNum[1]
netopeer-server[9196]: 9196-V 1-15 15:22:59 end recv_throughput_msg_from_ipsec
netopeer-server[9196]: 9196-V 1-15 15:22:59 ncxml_reply_no_data_ns, reply[0x7feaf000dea0]
netopeer-server[9196]: 9196-V 1-15 15:22:59 Calling getthroughput RPC function end
netopeer-server[9196]: 9196-V 1-15 15:22:59 reply[0xfffffffff000dea0]
netopeer-server[9196]: 9196-V 1-15 15:22:59 -------------1
netopeer-server[9196]: 9196-V 1-15 15:22:59 -------------10
netopeer-server[9196]: 9196-V 1-15 15:22:59 start nc_reply_get_type
netopeer-server[9196]: 9196-V 1-15 15:22:59 --2 nc_reply_get_type, reply[0xfffffffff000dea0]

从上面的reply打印来看,在函数ncxml_reply_no_data_ns()return之前reply为地址为0x7feaf000dea0,return之后reply地址为0xfffffffff000dea0,发现地址被截短了。通过查阅资料关于64位机指针返回截短问题,然后我看了编译过程,确实发现了这样了一个告警ncxml_reply_no_data_ns() warning:assignment makes pointer from integer without a cast。

得出结论:
在头文件中没有声明这个函数,只是在C文件中实现了,编译器生成了默认声明,并默认返回值为 integer。

warning解决方法

小背景中提到过当前的编译环境,我在VM2上libnetconf编译,然后编译cloudvpn需要将libnetconf.so放在VM3上面,结果没有放messages_xml.h【这个头文件有ncxml_reply_no_data()函数原型声明】,就导致编译cloudvpn.so的时候,出现的warning,从而导致调用ncxml_reply_no_data()函数宕机。

拨云见日,雨过天晴,至此所有问题全部解决。

结束语

这个问题确实解决了好几天,在刚开始解决这个问题的时候,其实还走了其他的一些弯路。现在想起来,都是由于没有查看netconf的协议标准,才导致多浪费了好长时间,如果当时查看了RFC后,问题就很明朗了,这样就会少走一些弯路。

可是现实没有如果,那么就需要平时在解决问题的问题,多思考,多动手,大胆猜想,小心求证。