From 8b7b7b0f605c2b961c7bac7ae8c2880301168e4c Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 13 Aug 2019 13:21:11 +0200 Subject: [PATCH] * RESTCONF "content" query parameter supported * New clixon-lib@2019-08-13.yang revision * Bugfix: If `ietf-netconf.yang` was imported from any yang module, client/backend communication stops working. --- CHANGELOG.md | 5 + apps/backend/backend_client.c | 93 +++++++++++++++++- apps/backend/backend_main.c | 9 +- apps/cli/cli_main.c | 6 ++ apps/netconf/netconf_main.c | 6 ++ apps/restconf/restconf_lib.c | 23 +++++ apps/restconf/restconf_lib.h | 12 +++ apps/restconf/restconf_main.c | 6 ++ apps/restconf/restconf_methods_get.c | 35 ++++++- example/main/example_backend.c | 94 +++++++++++++----- lib/clixon/clixon_netconf_lib.h | 2 + lib/clixon/clixon_proto_client.h | 1 + lib/src/clixon_netconf_lib.c | 97 +++++++++++++++---- lib/src/clixon_proto_client.c | 79 +++++++++++++++ lib/src/clixon_xml_map.c | 2 +- test/test_restconf.sh | 4 +- test/test_restconf_jukebox.sh | 43 +++++++- test/test_yang.sh | 16 +-- yang/clixon/Makefile.in | 2 +- ...-06-05.yang => clixon-lib@2019-08-13.yang} | 34 +++++++ 20 files changed, 507 insertions(+), 62 deletions(-) rename yang/clixon/{clixon-lib@2019-06-05.yang => clixon-lib@2019-08-13.yang} (67%) diff --git a/CHANGELOG.md b/CHANGELOG.md index efc0cf5b..fdb7ae89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Restconf RFC 8040 increased feature compliance * RESTCONF PATCH (plain patch) is being implemented according to RFC 8040 Section 4.6.1 * Note RESTCONF plain patch is different from RFC 8072 "YANG Patch Media Type" which is not implemented + * RESTCONF "content" query parameter supported * RESTCONF "insert" and "point" query parameters supported * Applies to ordered-by-user leaf and leaf-lists * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: @@ -25,6 +26,8 @@ * The main example explains how to implement a Yang extension in a backend plugin. ### API changes on existing features (you may need to change your code) +* New clixon-lib@2019-08-13.yang revision + * Added new rpc: `get-state` to get only state info in the internal Restconf/backend communication * Netconf edit-config "operation" attribute namespace check is enforced * This is enforced: ` * This was previously allowed: ` @@ -51,6 +54,8 @@ * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs +* If `ietf-netconf.yang` was imported from any yang module, client/backend communication stops working. + * Fixed by adding supported netconf features before loading other yang modules * RESTCONF JSON identity had wrong namespace in sub-objetcs * Showed if you GET an object with JSON encoding that have identities * Fixed Segv in nacm write when MERGE and creating object diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 07329adc..1d532db1 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -942,6 +942,94 @@ from_client_get(clicon_handle h, return retval; } +/*! Retrieve device state information only + * + * This is a CLIXON specific RPC + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., + * The set of namespace declarations are those in scope on the + * element. + */ + else + if (xml_nsctx_node(xfilter, &nsc) < 0) + goto done; + } + /* Get state data from plugins as defined by plugin_statedata(), if any */ + clicon_err_reset(); + if ((ret = client_statedata(h, xpath, nsc, &xret)) < 0) + goto done; + if (ret == 0){ /* Error from callback (error in xret) */ + if (clicon_xml2cbuf(cbret, xret, 0, 0) < 0) + goto done; + goto ok; + } + /* Pre-NACM access step */ + if ((ret = nacm_access_pre(h, username, NACM_DATA, &xnacm)) < 0) + goto done; + if (ret == 0){ /* Do NACM validation */ + if (xpath_vec_nsc(xret, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) + goto done; + /* NACM datanode/module read validation */ + if (nacm_datanode_read(xret, xvec, xlen, username, xnacm) < 0) + goto done; + } + cprintf(cbret, ""); /* OK */ + if (xret==NULL) + cprintf(cbret, ""); + else{ + if (xml_name_set(xret, "data") < 0) + goto done; + if (clicon_xml2cbuf(cbret, xret, 0, 0) < 0) + goto done; + } + cprintf(cbret, ""); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (xnacm) + xml_free(xnacm); + if (xvec) + free(xvec); + if (nsc) + xml_nsctx_free(nsc); + if (xret) + xml_free(xret); + return retval; +} + + /*! Request graceful termination of a NETCONF session. * @param[in] h Clicon handle * @param[in] xe Request: @@ -1283,7 +1371,7 @@ from_client_msg(clicon_handle h, goto reply; /* Dont quit here on user callbacks */ } if (ret == 0){ /* not handled by callback */ - if (netconf_operation_failed(cbret, "application", "Callback not recognized")< 0) + if (netconf_operation_not_supported(cbret, "application", "RPC operation not supported")< 0) goto done; goto reply; } @@ -1424,6 +1512,9 @@ backend_rpc_init(clicon_handle h) "urn:ietf:params:xml:ns:netmod:notification", "create-subscription") < 0) goto done; /* Clixon RPC */ + if (rpc_callback_register(h, from_client_get_state, NULL, + "http://clicon.org/lib", "get-state") < 0) + goto done; if (rpc_callback_register(h, from_client_debug, NULL, "http://clicon.org/lib", "debug") < 0) goto done; diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 26fb145b..5a18f89f 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -570,6 +570,12 @@ main(int argc, if (xmldb_connect(h) < 0) goto done; + /* Add (hardcoded) netconf features in case ietf-netconf loaded here + * Otherwise it is loaded in netconf_module_load below + */ + if (netconf_module_features(h) < 0) + goto done; + /* Create top-level yang spec and store as option */ if ((yspec = yspec_new()) == NULL) goto done; @@ -601,7 +607,8 @@ main(int argc, /* Load yang module library, RFC7895 */ if (yang_modules_init(h) < 0) goto done; - /* Add netconf yang spec, used by netconf client and as internal protocol */ + /* Add netconf yang spec, used by netconf client and as internal protocol + */ if (netconf_module_load(h) < 0) goto done; /* Load yang restconf module */ diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 4651c5fc..d44973e4 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -472,6 +472,12 @@ main(int argc, char **argv) clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) goto done; + /* Add (hardcoded) netconf features in case ietf-netconf loaded here + * Otherwise it is loaded in netconf_module_load below + */ + if (netconf_module_features(h) < 0) + goto done; + /* Create top-level and store as option */ if ((yspec = yspec_new()) == NULL) goto done; diff --git a/apps/netconf/netconf_main.c b/apps/netconf/netconf_main.c index 32e81496..f1fadb92 100644 --- a/apps/netconf/netconf_main.c +++ b/apps/netconf/netconf_main.c @@ -484,6 +484,12 @@ main(int argc, /* Access the remaining argv/argc options (after --) w clicon-argv_get() */ clicon_argv_set(h, argv0, argc, argv); + /* Add (hardcoded) netconf features in case ietf-netconf loaded here + * Otherwise it is loaded in netconf_module_load below + */ + if (netconf_module_features(h) < 0) + goto done; + /* Create top-level yang spec and store as option */ if ((yspec = yspec_new()) == NULL) goto done; diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index ddde425e..b4a245db 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -143,6 +143,7 @@ static const map_str2int http_reason_phrase_map[] = { }; /* See RFC 8040 + * @see restconf_media_str2int */ static const map_str2int http_media_map[] = { {"application/yang-data+xml", YANG_DATA_XML}, @@ -152,6 +153,16 @@ static const map_str2int http_media_map[] = { {NULL, -1} }; +/* See RFC 8040 4.8.1 + * @see query_content_str2int + */ +static const map_str2int query_content_map[] = { + {"config", CONTENT_CONFIG}, + {"nonconfig", CONTENT_NONCONFIG}, + {"all", CONTENT_ALL}, + {NULL, -1} +}; + int restconf_err2code(char *tag) { @@ -176,6 +187,18 @@ restconf_media_int2str(restconf_media media) return clicon_int2str(http_media_map, media); } +const query_content +query_content_str2int(char *str) +{ + return clicon_str2int(query_content_map, str); +} + +const char * +query_content_int2str(query_content nr) +{ + return clicon_int2str(query_content_map, nr); +} + /*! Return media_in from Content-Type, -1 if not found or unrecognized * @note media-type syntax does not support parameters * @see RFC7231 Sec 3.1.1.1 for media-type syntax type: diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 6c4a2613..a7da11d5 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -57,6 +57,16 @@ enum restconf_media{ }; typedef enum restconf_media restconf_media; +/*! Content query parameter RFC 8040 Sec 4.8.1 + */ +enum query_content{ + CONTENT_CONFIG, + CONTENT_NONCONFIG, + CONTENT_ALL /* default */ + +}; +typedef enum query_content query_content; + /* * Prototypes (also in clixon_restconf.h) */ @@ -66,6 +76,8 @@ const char *restconf_code2reason(int code); const restconf_media restconf_media_str2int(char *media); const char *restconf_media_int2str(restconf_media media); restconf_media restconf_content_type(FCGX_Request *r); +const query_content query_content_str2int(char *str); +const char *query_content_int2str(query_content nr); int restconf_badrequest(FCGX_Request *r); int restconf_unauthorized(FCGX_Request *r); int restconf_forbidden(FCGX_Request *r); diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 31177590..eb93c6eb 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -695,6 +695,12 @@ main(int argc, /* Access the remaining argv/argc options (after --) w clicon-argv_get() */ clicon_argv_set(h, argv0, argc, argv); + /* Add (hardcoded) netconf features in case ietf-netconf loaded here + * Otherwise it is loaded in netconf_module_load below + */ + if (netconf_module_features(h) < 0) + goto done; + /* Create top-level yang spec and store as option */ if ((yspec = yspec_new()) == NULL) goto done; diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 1f7e8554..dc9f5918 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -114,12 +114,31 @@ api_data_get2(clicon_handle h, int ret; char *namespace = NULL; cvec *nsc = NULL; + char *str; + query_content content = CONTENT_ALL; clicon_debug(1, "%s", __FUNCTION__); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } + /* Check for content attribute */ + if ((str = cvec_find_str(qvec, "content")) != NULL){ + clicon_debug(1, "%s content=%s", __FUNCTION__, str); + if ((content = query_content_str2int(str)) == -1){ + if (netconf_bad_attribute_xml(&xerr, "application", + "content", "Unrecognized value of content attribute") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) + goto done; + goto ok; + } + } + if ((cbpath = cbuf_new()) == NULL) goto done; cprintf(cbpath, "/"); @@ -144,7 +163,21 @@ api_data_get2(clicon_handle h, * xpath expressions */ if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) goto done; - if (clicon_rpc_get(h, xpath, namespace, &xret) < 0){ + switch (content){ + case CONTENT_CONFIG: + ret = clicon_rpc_get_config(h, "running", xpath, namespace, &xret); + break; + case CONTENT_NONCONFIG: + ret = clicon_rpc_get_state(h, xpath, namespace, &xret); + break; + case CONTENT_ALL: + ret = clicon_rpc_get(h, xpath, namespace, &xret); + break; + default: + clicon_err(OE_XML, EINVAL, "Invalid content attribute %d", content); + break; + } + if (ret < 0){ if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) goto done; if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ diff --git a/example/main/example_backend.c b/example/main/example_backend.c index 647e892b..234c8d92 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -31,6 +31,12 @@ ***** END LICENSE BLOCK ***** + * The example have the following optional arguments that you can pass as + * argc/argv after -- in clixon_backend: + * -r enable the reset function + * -s enable the state function + * -u enable upgrade function - auto-upgrade testing + * -t enable transaction logging (cal syslog for every transaction) */ #include #include @@ -284,7 +290,7 @@ example_copy_extra(clicon_handle h, /* Clicon handle */ type string; } } - * This yang snippet is present in clixon-example.yang for exampl. + * This yang snippet is present in clixon-example.yang for example. */ int example_statedata(clicon_handle h, @@ -300,9 +306,12 @@ example_statedata(clicon_handle h, cxobj *xt = NULL; char *name; cvec *nsc1 = NULL; + cvec *nsc2 = NULL; + yang_stmt *yspec = NULL; if (!_state) goto ok; + yspec = clicon_dbspec_yang(h); /* Example of statedata, in this case merging state data with * state information. In this case adding dummy interface operation state @@ -311,37 +320,75 @@ example_statedata(clicon_handle h, if (xmldb_get0(h, "running", nsc, xpath, 1, &xt, NULL) < 0) goto done; - /* Here a separate namespace context nsc1 is created. The original nsc - * created by the system cannot be used trivially, since we dont know - * the prefixes, although we could by a complex mechanism find the prefix - * (if it exists) and use that when creating our xpath. - * But it is easier creating a new namespace context nsc1. - */ - if ((nsc1 = xml_nsctx_init(NULL, "urn:ietf:params:xml:ns:yang:ietf-interfaces")) == NULL) - goto done; - if (xpath_vec_nsc(xt, nsc1, "/interfaces/interface/name", &xvec, &xlen) < 0) - goto done; - if (xlen){ - cprintf(cb, ""); - for (i=0; i%sup", name); - } - cprintf(cb, ""); - if (xml_parse_string(cbuf_get(cb), NULL, &xstate) < 0) - goto done; - } - if (xml_parse_string("" + if (yang_find_module_by_namespace(yspec, "urn:ietf:params:xml:ns:yang:ietf-interfaces") != NULL){ + /* Here a separate namespace context nsc1 is created. The original nsc + * created by the system cannot be used trivially, since we dont know + * the prefixes, although we could by a complex mechanism find the prefix + * (if it exists) and use that when creating our xpath. + * But it is easier creating a new namespace context nsc1. + */ + if ((nsc1 = xml_nsctx_init(NULL, "urn:ietf:params:xml:ns:yang:ietf-interfaces")) == NULL) + goto done; + if (xpath_vec_nsc(xt, nsc1, "/interfaces/interface/name", &xvec, &xlen) < 0) + goto done; + if (xlen){ + cprintf(cb, ""); + for (i=0; i%sup", name); + } + cprintf(cb, ""); + if (xml_parse_string(cbuf_get(cb), NULL, &xstate) < 0) + goto done; + } + } + /* State in test_yang.sh , test_restconf.sh and test_order.sh */ + if (yang_find_module_by_namespace(yspec, "urn:example:clixon") != NULL){ + if (xml_parse_string("" "42" "41" "43" /* should not be ordered */ "", NULL, &xstate) < 0) - goto done; + goto done; /* For the case when urn:example:clixon is not loaded */ + } + /* Event state from RFC8040 Appendix B.3.1 + * Note: (1) order is by-system so is different, + * (2) event-count is XOR on name, so is not 42 and 4 + */ + if (yang_find_module_by_namespace(yspec, "urn:example:events") != NULL){ + if ((nsc2 = xml_nsctx_init(NULL, "urn:example:events")) == NULL) + goto done; + if (xvec){ + free(xvec); + xvec = NULL; + } + if (xpath_vec_nsc(xt, nsc2, "/events/event/name", &xvec, &xlen) < 0) + goto done; + if (xlen){ + int j = 0; + int c; + cprintf(cb, ""); + + for (i=0; i%s%d", name, c); + } + cprintf(cb, ""); + if (xml_parse_string(cbuf_get(cb), NULL, &xstate) < 0) + goto done; + } + } + ok: retval = 0; done: if (nsc1) xml_nsctx_free(nsc1); + if (nsc2) + xml_nsctx_free(nsc2); if (xt) xml_free(xt); if (cb) @@ -389,7 +436,6 @@ example_extension(clicon_handle h, return retval; } - /*! Testcase upgrade function moving interfaces-state to interfaces * @param[in] h Clicon handle * @param[in] xn XML tree to be updated diff --git a/lib/clixon/clixon_netconf_lib.h b/lib/clixon/clixon_netconf_lib.h index 83fc2117..ccd9b528 100644 --- a/lib/clixon/clixon_netconf_lib.h +++ b/lib/clixon/clixon_netconf_lib.h @@ -47,6 +47,7 @@ int netconf_invalid_value_xml(cxobj **xret, char *type, char *message); int netconf_too_big(cbuf *cb, char *type, char *message); int netconf_missing_attribute(cbuf *cb, char *type, char *info, char *message); int netconf_bad_attribute(cbuf *cb, char *type, char *info, char *message); +int netconf_bad_attribute_xml(cxobj **xret, char *type, char *info, char *message); int netconf_unknown_attribute(cbuf *cb, char *type, char *info, char *message); int netconf_missing_element(cbuf *cb, char *type, char *element, char *message); int netconf_missing_element_xml(cxobj **xret, char *type, char *element, char *message); @@ -72,6 +73,7 @@ int netconf_malformed_message_xml(cxobj **xret, char *message); int netconf_data_not_unique_xml(cxobj **xret, cxobj *x, cvec *cvk); int netconf_minmax_elements_xml(cxobj **xret, cxobj *x, int max); int netconf_trymerge(cxobj *x, yang_stmt *yspec, cxobj **xret); +int netconf_module_features(clicon_handle h); int netconf_module_load(clicon_handle h); char *netconf_db_find(cxobj *xn, char *name); int netconf_err2cb(cxobj *xerr, cbuf **cberr); diff --git a/lib/clixon/clixon_proto_client.h b/lib/clixon/clixon_proto_client.h index 0ac3783b..d49d1314 100644 --- a/lib/clixon/clixon_proto_client.h +++ b/lib/clixon/clixon_proto_client.h @@ -53,6 +53,7 @@ int clicon_rpc_delete_config(clicon_handle h, char *db); int clicon_rpc_lock(clicon_handle h, char *db); int clicon_rpc_unlock(clicon_handle h, char *db); int clicon_rpc_get(clicon_handle h, char *xpath, char *namespace, cxobj **xret); +int clicon_rpc_get_state(clicon_handle h, char *xpath, char *namespace, cxobj **xret); int clicon_rpc_close_session(clicon_handle h); int clicon_rpc_kill_session(clicon_handle h, int session_id); int clicon_rpc_validate(clicon_handle h, char *db); diff --git a/lib/src/clixon_netconf_lib.c b/lib/src/clixon_netconf_lib.c index bb28debf..c237cc00 100644 --- a/lib/src/clixon_netconf_lib.c +++ b/lib/src/clixon_netconf_lib.c @@ -271,34 +271,67 @@ netconf_bad_attribute(cbuf *cb, char *info, char *message) { - int retval = -1; - char *encstr = NULL; + int retval = -1; + cxobj *xret = NULL; - if (cprintf(cb, "" - "%s" - "bad-attribute" - "%s" - "error", - type, info) <0) - goto err; + if (netconf_bad_attribute_xml(&xret, type, info, message) < 0) + goto done; + if (clicon_xml2cbuf(cb, xret, 0, 0) < 0) + goto done; + retval = 0; + done: + if (xret) + xml_free(xret); + return retval; +} + +/*! Create Netconf bad-attribute error XML tree according to RFC 6241 App A + * + * An attribute value is not correct; e.g., wrong type, + * out of range, pattern mismatch. + * @param[out] xret Error XML tree. Free with xml_free after use + * @param[in] type Error type: "rpc", "application" or "protocol" + * @param[in] info bad-attribute or bad-element xml + * @param[in] message Error message (will be XML encoded) + */ +int +netconf_bad_attribute_xml(cxobj **xret, + char *type, + char *info, + char *message) +{ + int retval = -1; + cxobj *xerr = NULL; + char *encstr = NULL; + + if (*xret == NULL){ + if ((*xret = xml_new("rpc-reply", NULL, NULL)) == NULL) + goto done; + } + else if (xml_name_set(*xret, "rpc-reply") < 0) + goto done; + if ((xerr = xml_new("rpc-error", *xret, NULL)) == NULL) + goto done; + if (xml_parse_va(&xerr, NULL, "%s" + "bad-attribute" + "%s" + "error", type, info) < 0) + goto done; if (message){ if (xml_chardata_encode(&encstr, "%s", message) < 0) goto done; - if (cprintf(cb, "%s", encstr) < 0) - goto err; + if (xml_parse_va(&xerr, NULL, "%s", + encstr) < 0) + goto done; } - if (cprintf(cb, "") <0) - goto err; retval = 0; done: if (encstr) free(encstr); return retval; - err: - clicon_err(OE_XML, errno, "cprintf"); - goto done; } + /*! Create Netconf unknwon-attribute error XML tree according to RFC 6241 App A * * An unexpected attribute is present. @@ -462,6 +495,7 @@ netconf_bad_element(cbuf *cb, xml_free(xret); return retval; } + int netconf_bad_element_xml(cxobj **xret, char *type, @@ -1208,19 +1242,26 @@ netconf_trymerge(cxobj *x, } /*! Load ietf netconf yang module and set enabled features - * The features added are (in order): + * + * This function should be called after options loaded but before yang modules. + * (a yang module may import ietf-netconf and then features must be set) + * @param[in] h Clixon handle + * @retval 0 OK + * @retval -1 Error + * The features added are (in order) (numbers are section# in RFC6241): * candidate (8.3) * validate (8.6) * startup (8.7) * xpath (8.9) + * @see netconf_module_load that is called later */ int -netconf_module_load(clicon_handle h) +netconf_module_features(clicon_handle h) { int retval = -1; cxobj *xc; yang_stmt *yspec; - + yspec = clicon_dbspec_yang(h); if ((xc = clicon_conf_xml(h)) == NULL){ clicon_err(OE_CFG, ENOENT, "Clicon configuration not loaded"); @@ -1233,6 +1274,24 @@ netconf_module_load(clicon_handle h) goto done; if (xml_parse_string("ietf-netconf:xpath", yspec, &xc) < 0) goto done; + retval = 0; + done: + return retval; +} + +/*! Load ietf netconf yang module and set enabled features + * @param[in] h Clixon handle + * @retval 0 OK + * @retval -1 Error + * @see netconf_module_feature should be called before any yang modules + */ +int +netconf_module_load(clicon_handle h) +{ + int retval = -1; + yang_stmt *yspec; + + yspec = clicon_dbspec_yang(h); /* Load yang spec */ if (yang_spec_parse_module(h, "ietf-netconf", NULL, yspec)< 0) goto done; diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c index 98ed0fdd..2cedc722 100644 --- a/lib/src/clixon_proto_client.c +++ b/lib/src/clixon_proto_client.c @@ -615,6 +615,85 @@ clicon_rpc_get(clicon_handle h, return retval; } +/*! Get database state data, clixon extension + * @param[in] h Clicon handle + * @param[in] xpath XPath in a filter stmt (or NULL/"" for no filter) + * @param[in] namespace Namespace associated w xpath + * @param[out] xt XML tree. Free with xml_free. + * Either or . + * @retval 0 OK + * @retval -1 Error, fatal or xml + * @note if xpath is set but namespace is NULL, the default, netconf base + * namespace will be used which is most probably wrong. + * @code + * cxobj *xt = NULL; + * if (clicon_rpc_get_state(h, "/hello/world", "urn:example:hello", &xt) < 0) + * err; + * if ((xerr = xpath_first(xt, "/rpc-error")) != NULL){ + * clicon_rpc_generate_error(xerr); + * err; + * } + * if (xt) + * xml_free(xt); + * @endcode + * @see clicon_rpc_generate_error + */ +int +clicon_rpc_get_state(clicon_handle h, + char *xpath, + char *namespace, + cxobj **xt) +{ + int retval = -1; + struct clicon_msg *msg = NULL; + cbuf *cb = NULL; + cxobj *xret = NULL; + cxobj *xd; + char *username; + + if ((cb = cbuf_new()) == NULL) + goto done; + cprintf(cb, ""); + if (xpath && strlen(xpath)) { + if (namespace) + cprintf(cb, "", + xpath, namespace); + else /* If xpath != /, this will probably yield an error later */ + cprintf(cb, "", xpath); + } + cprintf(cb, ""); + if ((msg = clicon_msg_encode("%s", cbuf_get(cb))) == NULL) + goto done; + if (clicon_rpc_msg(h, msg, &xret, NULL) < 0) + goto done; + /* Send xml error back: first check error, then ok */ + if ((xd = xpath_first(xret, "/rpc-reply/rpc-error")) != NULL) + xd = xml_parent(xd); /* point to rpc-reply */ + else if ((xd = xpath_first(xret, "/rpc-reply/data")) == NULL) + if ((xd = xml_new("data", NULL, NULL)) == NULL) + goto done; + if (xt){ + if (xml_rm(xd) < 0) + goto done; + *xt = xd; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + if (xret) + xml_free(xret); + if (msg) + free(msg); + return retval; +} + + /*! Close a (user) session * @param[in] h CLICON handle * @retval 0 OK diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 83a9403a..6344245e 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -3196,7 +3196,7 @@ xml_merge(cxobj *x0, goto done; if (ymod == NULL){ if (reason && - (*reason = strdup("No namespace in XML tree found")) == NULL){ + (*reason = strdup("Namespace not found or yang spec not loaded")) == NULL){ clicon_err(OE_UNIX, errno, "strdup"); goto done; } diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 38ca60a6..6fa21d4c 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -74,12 +74,12 @@ expecteq "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/r # Should be alphabetically ordered new "restconf get restconf/operations. RFC8040 3.3.2 (json)" -expecteq "$(curl -sG http://localhost/restconf/operations)" 0 '{"operations": {"clixon-example:client-rpc": null,"clixon-example:empty": null,"clixon-example:optional": null,"clixon-example:example": null,"clixon-lib:debug": null,"clixon-lib:ping": null,"ietf-netconf:get-config": null,"ietf-netconf:edit-config": null,"ietf-netconf:copy-config": null,"ietf-netconf:delete-config": null,"ietf-netconf:lock": null,"ietf-netconf:unlock": null,"ietf-netconf:get": null,"ietf-netconf:close-session": null,"ietf-netconf:kill-session": null,"ietf-netconf:commit": null,"ietf-netconf:discard-changes": null,"ietf-netconf:validate": null,"clixon-rfc5277:create-subscription": null}} +expecteq "$(curl -sG http://localhost/restconf/operations)" 0 '{"operations": {"clixon-example:client-rpc": null,"clixon-example:empty": null,"clixon-example:optional": null,"clixon-example:example": null,"clixon-lib:debug": null,"clixon-lib:ping": null,"clixon-lib:get-state": null,"ietf-netconf:get-config": null,"ietf-netconf:edit-config": null,"ietf-netconf:copy-config": null,"ietf-netconf:delete-config": null,"ietf-netconf:lock": null,"ietf-netconf:unlock": null,"ietf-netconf:get": null,"ietf-netconf:close-session": null,"ietf-netconf:kill-session": null,"ietf-netconf:commit": null,"ietf-netconf:discard-changes": null,"ietf-netconf:validate": null,"clixon-rfc5277:create-subscription": null}} ' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/operations) -expect='' +expect='' match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then err "$expect" "$ret" diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 411adf9c..3d5f2edf 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -8,6 +8,7 @@ APPNAME=example cfg=$dir/conf.xml fjukebox=$dir/example-jukebox.yang +fcontent=$dir/example-events.yang # A "system" module as defined in B.2.4 cat < $dir/example-system.yang @@ -38,6 +39,25 @@ cat < $cfg EOF +# yang B.3.1. "content" Parameter +cat < $fcontent + module example-events { + namespace "urn:example:events"; + prefix "ex"; + container events { + list event { + key name; + leaf name { type string; } + leaf description { type string; } + leaf event-count { + type uint32; + config false; + } + } + } +} +EOF + # Common Jukebox spec (fjukebox must be set) . ./jukebox.sh @@ -50,8 +70,8 @@ if [ $BE -ne 0 ]; then err fi sudo pkill clixon_backend # to be sure - new "start backend -s init -f $cfg" - start_backend -s init -f $cfg + new "start backend -s init -f $cfg -- -s" + start_backend -s init -f "$cfg" -- -s fi new "kill old restconf daemon" @@ -77,7 +97,7 @@ expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://loca # This just catches the header and the jukebox module, the RFC has foo and bar which # seems wrong to recreate new "B.1.2. Retrieve the Server Module Information" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":' '"module":\[{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":' '"module":\[{"name":"example-events","revision":\[null\],"namespace":"urn:example:events","conformance-type":"implement"},{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"}' new "B.1.3. Retrieve the Server Capability Information" expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit' @@ -129,6 +149,22 @@ expectpart "$(curl -si -X PATCH http://localhost/restconf/data/example-jukebox:j new "B.2.5. Check edit" expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Nick%20Cave%20and%20the%20Bad%20Seeds -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Nick Cave and the Bad SeedsTender Prey1988The Good Son1990' +# note reverse order of down/up as it is ordered by system and down is before up +new 'B.3.1. "content" Parameter (preamble, add content)' +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-events:events -d '{"example-events:events":{"event":[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}]}}')" 0 "HTTP/1.1 201 Created" + +new 'B.3.1. "content" Parameter (wrong content)' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=kalle -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-attribute","error-info":{"bad-attribute":"content"},"error-severity":"error","error-message":"Unrecognized value of content attribute"}}}' + +new 'B.3.1. "content" Parameter example 1: content=all' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=all -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count","event-count":90},{"name":"interface-up","description":"Interface up notification count","event-count":77}\]}}' + +new 'B.3.1. "content" Parameter example 2: content=config' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=config -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}\]}}' + +new 'B.3.1. "content" Parameter example 3: content=nonconfig' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=nonconfig -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","event-count":90},{"name":"interface-up","event-count":77}\]}}' + new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" @@ -188,7 +224,6 @@ expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:ext if false; then # NYI new "B.2.2. Detect Datastore Resource Entity-Tag Change" # XXX done except entity-changed -new 'B.3.1. "content" Parameter' new 'B.3.2. "depth" Parameter' new 'B.3.3. "fields" Parameter' new 'B.3.6. "filter" Parameter' diff --git a/test/test_yang.sh b/test/test_yang.sh index b962201a..3c1891ee 100755 --- a/test/test_yang.sh +++ b/test/test_yang.sh @@ -2,7 +2,7 @@ # Yang test: multi-keys and empty type # Magic line must be first in script (see README.md) -s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi +s="$_" ; . ./lib.sh || if [ "$s" = "$0" ]; then exit 0; else return 0; fi APPNAME=example @@ -143,14 +143,14 @@ EOF new "test params: -f $cfg" -if [ $BE -ne 0 ]; then +if [ "$BE" -ne 0 ]; then new "kill old backend" - sudo clixon_backend -zf $cfg + sudo clixon_backend -zf "$cfg" if [ $? -ne 0 ]; then err fi new "start backend -s init -f $cfg" - start_backend -s init -f $cfg + start_backend -s init -f "$cfg" new "waiting" wait_backend @@ -280,18 +280,18 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" -if [ $BE -eq 0 ]; then +if [ "$BE" -eq 0 ]; then exit # BE fi new "Kill backend" # Check if premature kill -pid=`pgrep -u root -f clixon_backend` +pid=$(pgrep -u root -f clixon_backend) if [ -z "$pid" ]; then err "backend already dead" fi # kill backend -stop_backend -f $cfg +stop_backend -f "$cfg" sudo pkill -u root -f clixon_backend -rm -rf $dir +rm -rf "$dir" diff --git a/yang/clixon/Makefile.in b/yang/clixon/Makefile.in index c982ddc2..87d5e65b 100644 --- a/yang/clixon/Makefile.in +++ b/yang/clixon/Makefile.in @@ -42,7 +42,7 @@ datarootdir = @datarootdir@ YANG_INSTALLDIR = @YANG_INSTALLDIR@ YANGSPECS = clixon-config@2019-06-05.yang -YANGSPECS += clixon-lib@2019-06-05.yang +YANGSPECS += clixon-lib@2019-08-13.yang YANGSPECS += clixon-rfc5277@2008-07-01.yang YANGSPECS += clixon-xml-changelog@2019-03-21.yang diff --git a/yang/clixon/clixon-lib@2019-06-05.yang b/yang/clixon/clixon-lib@2019-08-13.yang similarity index 67% rename from yang/clixon/clixon-lib@2019-06-05.yang rename to yang/clixon/clixon-lib@2019-08-13.yang index 370819c8..343ca634 100644 --- a/yang/clixon/clixon-lib@2019-06-05.yang +++ b/yang/clixon/clixon-lib@2019-08-13.yang @@ -40,6 +40,10 @@ module clixon-lib { ***** END LICENSE BLOCK *****"; + revision 2019-08-13 { + description + "get-state added for restconf content=nonconfig internal rpc"; + } revision 2019-06-05 { description "ping rpc added for liveness"; @@ -59,4 +63,34 @@ module clixon-lib { rpc ping { description "Check aliveness of backend daemon."; } + rpc get-state { + description + "Retrieve device state information only. This is a clixon extension + to ietf-netconf to implement RESTCONF GET with attribute + content=nonconfig. + The reason is that netconf only has and neither + which retrieves state only"; + + reference "RFC 8040 4.8.1"; + + input { + anyxml filter { + description + "This parameter specifies the portion of the system + configuration and state data to retrieve."; + nc:get-filter-element-attributes; + } + } + + output { + anyxml data { + description + "Copy of the running datastore subset and/or state + data that matched the filter criteria (if any). + An empty data container indicates that the request did not + produce any results."; + } + } + } + }