diff --git a/apps/backend/backend_state.c b/apps/backend/backend_state.c index 34e49403..0ca210d4 100644 --- a/apps/backend/backend_state.c +++ b/apps/backend/backend_state.c @@ -105,6 +105,7 @@ restconf_client_get_capabilities(clixon_handle h, cprintf(cb, ""); cprintf(cb, "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit"); cprintf(cb, "urn:ietf:params:restconf:capability:depth:1.0"); + cprintf(cb, "urn:ietf:params:restconf:capability:fields:1.0"); cprintf(cb, "urn:ietf:params:restconf:capability:with-defaults:1.0"); cprintf(cb, ""); if (clixon_xml_parse_string(cbuf_get(cb), YB_PARENT, NULL, &xrstate, NULL) < 0) diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 70c5e965..4b5ad43d 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -66,6 +66,79 @@ /* Forward */ static int api_data_pagination(clixon_handle h, void *req, char *api_path, int pi, cvec *qvec, int pretty, restconf_media media_out); +/*! Filter top-level data nodes based on "fields" query parameter (RFC 8040 Sec 4.8.3) + * + * Simple implementation: only top-level module:node or module:* selection at data root. + * Removes children of xdata that do not match any entry in the semicolon-separated + * fields list. + * @param[in] xdata XML data root whose children are top-level data nodes + * @param[in] fields_str Value of the "fields" query parameter, e.g. "mod1:node1;mod2:node2" + * @retval 0 OK + * @retval -1 Error + * @see RFC 8040 Sec 4.8.3 + */ +static int +restconf_fields_filter(cxobj *xdata, + char *fields_str) +{ + int retval = -1; + char *str = NULL; + char *s; + char *saveptr; + char *colon; + cxobj *x; + yang_stmt *ynode; + yang_stmt *ymod; + char *modname; + char *nodename; + int i; + int found; + + for (i = xml_child_nr(xdata)-1; i >= 0; i--){ + x = xml_child_i(xdata, i); + if (xml_type(x) != CX_ELMNT) + continue; + ynode = xml_spec(x); + ymod = (ynode != NULL) ? ys_module(ynode) : NULL; + modname = (ymod != NULL) ? yang_argument_get(ymod) : NULL; + nodename = xml_name(x); + found = 0; + if ((str = strdup(fields_str)) == NULL){ + clixon_err(OE_UNIX, errno, "strdup"); + goto done; + } + s = strtok_r(str, ";", &saveptr); + while (s != NULL){ + colon = strchr(s, ':'); + if (colon != NULL){ + *colon = '\0'; + if (modname != NULL && + strcmp(s, modname) == 0 && + strcmp(colon+1, nodename) == 0) + found = 1; + *colon = ':'; + } + else{ + if (modname != NULL && strcmp(s, modname) == 0) + found = 1; + } + if (found) + break; + s = strtok_r(NULL, ";", &saveptr); + } + free(str); + str = NULL; + if (!found) + if (xml_purge(x) < 0) + goto done; + } + retval = 0; + done: + if (str) + free(str); + return retval; +} + /*! Generic GET (both HEAD and GET) * * According to restconf @@ -125,6 +198,7 @@ api_data_get2(clixon_handle h, cxobj *xtop = NULL; yang_stmt *y = NULL; char *defaults = NULL; + char *fields_str = NULL; cvec *nscd = NULL; int ret; @@ -207,6 +281,11 @@ api_data_get2(clixon_handle h, clixon_debug(CLIXON_DBG_RESTCONF, "with_defaults=%s", attr); defaults = attr; } + /* Check for fields attribute: simple root-level module:node filter, RFC 8040 Sec 4.8.3 */ + if ((attr = cvec_find_str(qvec, "fields")) != NULL){ + clixon_debug(CLIXON_DBG_RESTCONF, "fields=%s", attr); + fields_str = attr; + } clixon_debug(CLIXON_DBG_RESTCONF, "path:%s", xpath); if ((ret = clicon_rpc_get(h, xpath, nsc, content, depth, defaults, &xret)) < 0){ @@ -235,6 +314,11 @@ api_data_get2(clixon_handle h, goto done; } if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ + /* Apply fields filter if requested */ + if (fields_str != NULL){ + if (restconf_fields_filter(xret, fields_str) < 0) + goto done; + } switch (media_out){ case YANG_DATA_XML: if (clixon_xml2cbuf(cbx, xret, 0, pretty, NULL, -1, 0) < 0) /* Dont print top object? */ diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 3e19d0ee..9f564ebb 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -189,13 +189,30 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCP # Maybe this is not correct w [null,null]but I have no good examples new 'B.3.2. "depth" Parameter depth=3' -expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/$HVER 200" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} - ' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/$HVER 200" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} + +' new "restconf DELETE whole datastore" expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data)" 0 "HTTP/$HVER 204" -#new 'B.3.3. "fields" Parameter' +new 'B.3.3. "fields" Parameter: preamble (create system and jukebox data)' +expectpart "$(curl $CURLOPTS -X PATCH -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data -d 'trueFields Artist')" 0 "HTTP/$HVER 204" + +new 'B.3.3. "fields" Parameter: system only' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data?fields=example-system:system)" 0 "HTTP/$HVER 200" \ + '"example-system:system"' --not-- '"example-jukebox:jukebox"' + +new 'B.3.3. "fields" Parameter: jukebox only' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data?fields=example-jukebox:jukebox)" 0 "HTTP/$HVER 200" \ + '"example-jukebox:jukebox"' --not-- '"example-system:system"' + +new 'B.3.3. "fields" Parameter: two modules' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' "$RCPROTO://localhost/restconf/data?fields=example-system:system%3Bexample-jukebox:jukebox")" 0 "HTTP/$HVER 200" \ + '"example-system:system"' '"example-jukebox:jukebox"' + +new "restconf DELETE whole datastore (after fields test)" +expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data)" 0 "HTTP/$HVER 204" new 'B.3.4. "insert" Parameter' JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" @@ -251,7 +268,6 @@ new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 "HTTP/$HVER 200" '2431' #new "B.2.2. Detect Datastore Resource Entity-Tag Change" # XXX done except entity-changed -#new 'B.3.3. "fields" Parameter' #new 'B.3.6. "filter" Parameter' #new 'B.3.7. "start-time" Parameter' #new 'B.3.8. "stop-time" Parameter' diff --git a/test/test_yang_with_defaults.sh b/test/test_yang_with_defaults.sh index e8ce796e..eebeed60 100755 --- a/test/test_yang_with_defaults.sh +++ b/test/test_yang_with_defaults.sh @@ -509,14 +509,14 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCP 0 \ "HTTP/$HVER 200" "Content-Type: application/yang-data+json" \ 'Cache-Control: no-cache' \ -'{"ietf-restconf-monitoring:capabilities":{"capability":\["urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit","urn:ietf:params:restconf:capability:depth:1.0","urn:ietf:params:restconf:capability:with-defaults:1.0"\]}}' +'{"ietf-restconf-monitoring:capabilities":{"capability":\["urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit","urn:ietf:params:restconf:capability:depth:1.0","urn:ietf:params:restconf:capability:fields:1.0","urn:ietf:params:restconf:capability:with-defaults:1.0"\]}}' new "rfc8040 B.1.3. Retrieve the Server Capability Information xml" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" \ 0 \ "HTTP/$HVER 200" "Content-Type: application/yang-data+xml" \ 'Cache-Control: no-cache' \ -'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth:1.0urn:ietf:params:restconf:capability:with-defaults:1.0' +'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth:1.0urn:ietf:params:restconf:capability:fields:1.0urn:ietf:params:restconf:capability:with-defaults:1.0' new "rfc8040 B.3.9. RESTCONF with-defaults parameter = report-all json" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example:interfaces/interface=eth1?with-defaults=report-all)" \