关于使用libnetconf自定义RPC的问题记录
1. SDN-C采集性能数据失败
SDN-C控制器通过RPC方法采集vCPE性能数据失败,已知信息如下所示:
下发的rpc消息:
1 | <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2"> |
返回的rpc-reply消息
1 | <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2"> |
返回的错误:
1 | <errors xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"> |
分析:
我们自定义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的。
那这个问题就简单了,有两种做法:
- 每次写RPC的output时,在需要返回的内容外包一层container data即可。
- 能不能在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
29API 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 | API nc_reply *ncxml_reply_no_data_ns(const xmlNodePtr data, const char* ns) |
一切都是这么顺利的进行,没有问题,但是在最后测试时候,每次netopeer-cli下发获取流量的rpc时,netopeer-server就会宕机。
小背景:
在说明这个问题之前,我先说明一下程序的编译环境和运行环境:
如上图所示,运行我们的cloudvpn程序,需要netopeer、libnetconf、cloudvpn三个模块:
- netopeer:在VM1上编译;
- libnetconf:加入ncxml_reply_no_data_ns()在VM2上面编译;
- 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
38case 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
12netopeer-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后,问题就很明朗了,这样就会少走一些弯路。
可是现实没有如果,那么就需要平时在解决问题的问题,多思考,多动手,大胆猜想,小心求证。