diff --git a/CHANGELOG.md b/CHANGELOG.md index b49318fa..c361c0fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -533,6 +533,14 @@ Developers may need to change their code * See [namespace prefix nc is not supported in full #154](https://github.com/clicon/clixon/issues/154) * Fixed [Clixon backend generates wrong XML on empty string value #144](https://github.com/clicon/clixon/issues/144) +### New features + +* Prototype of collection draft + * This is prototype work for ietf netconf work + * See draft-ietf-netconf-restconf-collection-00.txt + * New yang: ietf-restconf-collection@2020-10-22.yang + * New http media: application/yang.collection+xml/json + ## 4.8.0 18 October 2020 diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index be7b9377..d4f63a82 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -1335,6 +1335,305 @@ from_client_kill_session(clicon_handle h, return retval; } +/*! Help function for parsing restconf query parameter and setting netconf attribute + * + * If not "unbounded", parse and set a numeric value + * @param[in] h Clixon handle + * @param[in] name Name of attribute + * @param[in,out] cbret Output buffer for internal RPC message + * @param[out] value Value + * @retval -1 Error + * @retval 0 Invalid, cbret set + * @retval 1 OK + */ +static int +element2value(clicon_handle h, + cxobj *xe, + char *name, + cbuf *cbret, + uint32_t *value) +{ + int retval = -1; + char *valstr; + int ret; + char *reason = NULL; + cxobj *x; + + *value = 0; + if ((x = xml_find_type(xe, NULL, name, CX_ELMNT)) != NULL && + (valstr = xml_body(x)) != NULL && + strcmp(valstr, "unbounded") != 0){ + if ((ret = parse_uint32(valstr, value, &reason)) < 0){ + clicon_err(OE_XML, errno, "parse_uint32"); + goto done; + } + if (ret == 0){ + if (netconf_bad_attribute(cbret, "application", + "count", "Unrecognized value of count attribute") < 0) + goto done; + goto fail; + } + } + retval = 1; + done: + if (reason) + free(reason); + return retval; + fail: + retval = 0; + goto done; +} + +/*! Retrieve collection configuration and device state information + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., 0 && + (ret = xml_yang_validate_add(h, xret, &xerr)) < 0) + goto done; + if (ret == 0){ + if (clicon_debug_get()) + clicon_log_xml(LOG_DEBUG, xret, "VALIDATE_STATE"); + if (clixon_netconf_internal_error(xerr, + ". Internal error, state callback returned invalid XML", + NULL) < 0) + goto done; + if (clicon_xml2cbuf(cbret, xerr, 0, 0, -1) < 0) + goto done; + goto ok; + } + } /* CLICON_VALIDATE_STATE_XML */ + + if (content == CONTENT_NONCONFIG){ /* state only, all config should be removed now */ + /* Keep state data only, remove everything that is not config. Note that state data + * may be a sub-part in a config tree, we need to traverse to find all + */ + if (xml_non_config_data(xret, NULL) < 0) + goto done; + if (xml_tree_prune_flagged_sub(xret, XML_FLAG_MARK, 1, NULL) < 0) + goto done; + if (xml_apply(xret, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0) + goto done; + } + /* Code complex to filter out anything that is outside of xpath + * Actually this is a safety catch, should really be done in plugins + * and modules_state functions. + */ + if (xpath_vec(xret, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) + goto done; + + /* Pre-NACM access step */ + xnacm = clicon_nacm_cache(h); + if (xnacm != NULL){ /* Do NACM validation */ + /* NACM datanode/module read validation */ + if (nacm_datanode_read(h, xret, xvec, xlen, username, xnacm) < 0) + goto done; + } + cprintf(cbret, "", + NETCONF_BASE_NAMESPACE, NETCONF_COLLECTION_NAMESPACE); /* OK */ + if ((ns = yang_find_mynamespace(y)) != NULL) + for (i=0; i0?depth+1:depth) < 0) + goto done; + } + cprintf(cbret, ""); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (xtop) + xml_free(xtop); + if (cb) + cbuf_free(cb); + if (reason) + free(reason); + if (xerr) + xml_free(xerr); + if (xvec) + free(xvec); + if (nsc) + xml_nsctx_free(nsc); + if (xret) + xml_free(xret); + return retval; +} + /*! Create a notification subscription * @param[in] h Clicon handle * @param[in] xe Request: @@ -1960,7 +2259,10 @@ backend_rpc_init(clicon_handle h) if (rpc_callback_register(h, from_client_validate, NULL, NETCONF_BASE_NAMESPACE, "validate") < 0) goto done; - + /* draft-ietf-netconf-restconf-collection-00 */ + if (rpc_callback_register(h, from_client_get_collection, NULL, + NETCONF_COLLECTION_NAMESPACE, "get-collection") < 0) + goto done; /* In backend_client.? RPC from RFC 5277 */ if (rpc_callback_register(h, from_client_create_subscription, NULL, EVENT_RFC5277_NAMESPACE, "create-subscription") < 0) diff --git a/apps/restconf/clixon_restconf.h b/apps/restconf/clixon_restconf.h index 069d37a7..c7d6a368 100644 --- a/apps/restconf/clixon_restconf.h +++ b/apps/restconf/clixon_restconf.h @@ -42,13 +42,19 @@ extern "C" { #define _CLIXON_RESTCONF_H_ /* - * Types (also in restconf_lib.h) + * Types + */ +/*! RESTCONF media types + * @see http_media_map + * @note DUPLICATED in clixon_lib.h */ enum restconf_media{ - YANG_DATA_JSON, /* "application/yang-data+json" */ - YANG_DATA_XML, /* "application/yang-data+xml" */ - YANG_PATCH_JSON, /* "application/yang-patch+json" */ - YANG_PATCH_XML /* "application/yang-patch+xml" */ + YANG_DATA_JSON, /* "application/yang-data+json" */ + YANG_DATA_XML, /* "application/yang-data+xml" */ + YANG_PATCH_JSON, /* "application/yang-patch+json" */ + YANG_PATCH_XML, /* "application/yang-patch+xml" */ + YANG_COLLECTION_XML, /* draft-ietf-netconf-restconf-collection-00.txt */ + YANG_COLLECTION_JSON /* draft-ietf-netconf-restconf-collection-00.txt */ }; typedef enum restconf_media restconf_media; diff --git a/apps/restconf/restconf_err.c b/apps/restconf/restconf_err.c index 29c87ceb..553fc16e 100644 --- a/apps/restconf/restconf_err.c +++ b/apps/restconf/restconf_err.c @@ -275,6 +275,7 @@ api_return_err(clicon_handle h, switch (media){ case YANG_DATA_XML: case YANG_PATCH_XML: + case YANG_COLLECTION_XML: clicon_debug(1, "%s code:%d", __FUNCTION__, code); if (pretty){ cprintf(cb, " \n"); @@ -291,6 +292,7 @@ api_return_err(clicon_handle h, break; case YANG_DATA_JSON: case YANG_PATCH_JSON: + case YANG_COLLECTION_JSON: clicon_debug(1, "%s code:%d", __FUNCTION__, code); if (pretty){ cprintf(cb, "{\n\"ietf-restconf:errors\" : "); @@ -306,9 +308,7 @@ api_return_err(clicon_handle h, cprintf(cb, "}\r\n"); } break; - default: - clicon_err(OE_YANG, EINVAL, "Invalid media type %d", media); - goto done; + default: /* Just ignore the body so that there is a reply */ break; } /* switch media */ assert(cbuf_len(cb)); diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 1384aee0..50779512 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -203,10 +203,12 @@ static const map_str2int http_reason_phrase_map[] = { * @see restconf_media_str2int */ static const map_str2int http_media_map[] = { - {"application/yang-data+xml", YANG_DATA_XML}, - {"application/yang-data+json", YANG_DATA_JSON}, - {"application/yang-patch+xml", YANG_PATCH_XML}, - {"application/yang-patch+json", YANG_PATCH_JSON}, + {"application/yang-data+xml", YANG_DATA_XML}, + {"application/yang-data+json", YANG_DATA_JSON}, + {"application/yang-patch+xml", YANG_PATCH_XML}, + {"application/yang-patch+json", YANG_PATCH_JSON}, + {"application/yang.collection+xml", YANG_COLLECTION_XML}, + {"application/yang.collection+json", YANG_COLLECTION_JSON}, {NULL, -1} }; diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 53292f5c..8f5418f7 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -46,13 +46,15 @@ extern "C" { */ /*! RESTCONF media types * @see http_media_map - * (also in clixon_restconf.h) + * @note DUPLICATED in clixon_restconf.h */ enum restconf_media{ YANG_DATA_JSON, /* "application/yang-data+json" */ YANG_DATA_XML, /* "application/yang-data+xml" */ YANG_PATCH_JSON, /* "application/yang-patch+json" */ - YANG_PATCH_XML /* "application/yang-patch+xml" */ + YANG_PATCH_XML, /* "application/yang-patch+xml" */ + YANG_COLLECTION_XML, /* draft-ietf-netconf-restconf-collection-00.txt */ + YANG_COLLECTION_JSON /* draft-ietf-netconf-restconf-collection-00.txt */ }; typedef enum restconf_media restconf_media; diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 881884e5..5ff22dd6 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -75,7 +75,7 @@ * @param[in] media_out Output media * @param[in] head If 1 is HEAD, otherwise GET * @code - * curl -G http://localhost/restconf/data/interfaces/interface=eth0 + * curl -X GET http://localhost/restconf/data/interfaces/interface=eth0 * @endcode * See RFC8040 Sec 4.2 and 4.3 * XXX: cant find a way to use Accept request field to choose Content-Type @@ -215,7 +215,8 @@ api_data_get2(clicon_handle h, goto ok; } /* Normal return, no error */ - if ((cbx = cbuf_new()) == NULL) + if ((cbx = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ switch (media_out){ @@ -228,6 +229,9 @@ api_data_get2(clicon_handle h, goto done; break; default: + if (restconf_unsupported_media(req) < 0) + goto done; + goto ok; break; } } @@ -309,6 +313,227 @@ api_data_get2(clicon_handle h, return retval; } +/*! GET Collection + * According to restconf collection draft. Lists, work in progress + * @param[in] h Clixon handle + * @param[in] req Generic Www handle + * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] media_out Output media + * @param[in] head If 1 is HEAD, otherwise GET + * @code + * curl -X GET http://localhost/restconf/data/interfaces + * @endcode + * A collection resource contains a set of data resources. It is used + * to represent a all instances or a subset of all instances in a YANG + * list or leaf-list. + * @see draft-ietf-netconf-restconf-collection-00.txt + */ +static int +api_data_collection(clicon_handle h, + void *req, + char *api_path, + cvec *pcvec, /* XXX remove? */ + int pi, + cvec *qvec, + int pretty, + restconf_media media_out) +{ + int retval = -1; + char *xpath = NULL; + cbuf *cbx = NULL; + yang_stmt *yspec; + cxobj *xret = NULL; + cxobj *xerr = NULL; /* malloced */ + cxobj *xe = NULL; /* not malloced */ + cxobj **xvec = NULL; + int i; + int ret; + cvec *nsc = NULL; + char *attr; /* attribute value string */ + netconf_content content = CONTENT_ALL; + cxobj *xtop = NULL; + cxobj *xbot = NULL; + yang_stmt *y = NULL; + cbuf *cbrpc = NULL; + char *depth; + char *count; + char *skip; + char *direction; + char *sort; + char *where; + + clicon_debug(1, "%s", __FUNCTION__); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + /* strip /... from start */ + for (i=0; i"); + /* If xpath, add a filter */ if (xpath && strlen(xpath)) { cprintf(cb, "<%s:filter %s:type=\"xpath\" %s:select=\"%s\"", NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, @@ -877,7 +878,134 @@ clicon_rpc_get(clicon_handle h, return retval; } +/*! Get database configuration and state data collection + * @param[in] h Clicon handle + * @param[in] apipath To identify a list/leaf-list + * @param[in] yli Yang-stmt of list/leaf-list of collection + * @param[in] namespace Namespace associated w xpath + * @param[in] nsc Namespace context for filter + * @param[in] content Clixon extension: all, config, noconfig. -1 means all + * @param[in] depth Nr of XML levels to get, -1 is all, 0 is none + * @param[in] count Collection/clixon extension + * @param[in] skip Collection/clixon extension + * @param[in] direction Collection/clixon extension + * @param[in] sort Collection/clixon extension + * @param[in] where Collection/clixon extension + * @param[out] xt XML tree. Free with xml_free. + * Either or . + * @retval 0 OK + * @retval -1 Error, fatal or xml + + * @see clicon_rpc_get + * @see draft-ietf-netconf-restconf-collection-00 + * @note the netconf return message is yang populated, as well as the return data + */ +int +clicon_rpc_get_collection(clicon_handle h, + char *apipath, + yang_stmt *yli, + cvec *nsc, /* namespace context for filter */ + netconf_content content, + char *depth, + char *count, + char *skip, + char *direction, + char *sort, + char *where, + cxobj **xt) +{ + int retval = -1; + struct clicon_msg *msg = NULL; + cbuf *cb = NULL; + cxobj *xret = NULL; + cxobj *xerr = NULL; + cxobj *xr; + char *username; + uint32_t session_id; + int ret; + yang_stmt *yspec; + cxobj *x; + + if (session_id_check(h, &session_id) < 0) + goto done; + if ((cb = cbuf_new()) == NULL) + goto done; + cprintf(cb, ""); + if (count) + cprintf(cb, "%s", apipath); + if (count) + cprintf(cb, "%s", count); + if (skip) + cprintf(cb, "%s", skip); + if (direction) + cprintf(cb, "%s", direction); + if (sort) + cprintf(cb, "%s", sort); + if (where) + cprintf(cb, "%s", where); + cprintf(cb, ""); + if ((msg = clicon_msg_encode(session_id, "%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 ((xr = xpath_first(xret, NULL, "/rpc-reply/rpc-error")) != NULL) + xr = xml_parent(xr); /* point to rpc-reply */ + else if ((xr = xpath_first(xret, NULL, "/rpc-reply/collection")) == NULL){ + if ((xr = xml_new("collection", NULL, CX_ELMNT)) == NULL) + goto done; + } + else{ + yspec = clicon_dbspec_yang(h); + /* Populate all children with yco */ + x = NULL; + while ((x = xml_child_each(xr, x, CX_ELMNT)) != NULL){ + xml_spec_set(x, yli); + if ((ret = xml_bind_yang(x, YB_PARENT, yspec, &xerr)) < 0) + goto done; + if (ret == 0){ + if (clixon_netconf_internal_error(xerr, + ". Internal error, backend returned invalid XML.", + NULL) < 0) + goto done; + if ((xr = xpath_first(xerr, NULL, "rpc-error")) == NULL){ + clicon_err(OE_XML, ENOENT, "Expected rpc-error tag but none found(internal)"); + goto done; + } + } + } + } + if (xr){ + if (xml_rm(xr) < 0) + goto done; + *xt = xr; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + if (xerr) + xml_free(xerr); + if (xret) + xml_free(xret); + if (msg) + free(msg); + return retval; +} + /*! Send a close a netconf user session. Socket is also closed if still open + * * @param[in] h CLICON handle * @retval 0 OK * @retval -1 Error and logged to syslog diff --git a/lib/src/clixon_xml_bind.c b/lib/src/clixon_xml_bind.c index 7d38bff6..c625e93d 100644 --- a/lib/src/clixon_xml_bind.c +++ b/lib/src/clixon_xml_bind.c @@ -743,4 +743,3 @@ xml_bind_yang_rpc_reply(cxobj *xrpc, retval = 0; goto done; } - diff --git a/test/test_collection.sh b/test/test_collection.sh new file mode 100755 index 00000000..5debe9a3 --- /dev/null +++ b/test/test_collection.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Restconf RFC8040 Appendix A and B "jukebox" example +# For collection / scaling activity +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +APPNAME=example + +cfg=$dir/conf.xml +fjukebox=$dir/example-jukebox.yang + +cat < $cfg + + $cfg + ietf-netconf:startup + /usr/local/share/clixon + $IETFRFC + $dir + false + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/lib/$APPNAME/backend + $dir/restconf.pidfile + $dir + true + +EOF + +cat < $dir/startup_db + + + + + Foo Fighters + + Crime and Punishment + 1995 + + + One by One + 2002 + + + The Color and the Shape + 1997 + + + There is Nothing Left to Loose + 1999 + + + White and Black + 1998 + + + + + +EOF + +# Common Jukebox spec (fjukebox must be set) +. ./jukebox.sh + +new "test params: -f $cfg -- -s" # XXX: -sS state file + +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + sudo pkill -f clixon_backend # to be sure + new "start backend -s startup -f $cfg" + start_backend -s startup -f "$cfg" +fi + +new "waiting" +wait_backend + +if [ $RC -ne 0 ]; then + new "kill old restconf daemon" + stop_restconf_pre + + new "start restconf daemon" + start_restconf -f $cfg + + new "waiting" + wait_restconf +fi + +new "C.1. 'count' Parameter RESTCONF" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang.collection+xml" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album/?count=2)" 0 "HTTP/1.1 200 OK" "application/yang.collection+xml" 'Crime and Punishment1995One by One2002' + +new "C.1. 'count' Parameter NETCONF" +expecteof "$clixon_netconf -qf $cfg" 0 "runningexample-jukebox/example-jukebox:jukebox/library/artist=Foo Fighters/album2]]>]]>" '^Crime and Punishment1995One by One2002]]>]]>$' + +if [ $RC -ne 0 ]; then + new "Kill restconf daemon" + stop_restconf +fi + +if [ $BE -eq 0 ]; then + exit # BE +fi + +new "Kill backend" +# Check if premature kill +pid=$(pgrep -u root -f clixon_backend) +if [ -z "$pid" ]; then + err "backend already dead" +fi +# kill backend +stop_backend -f $cfg + +rm -rf $dir diff --git a/yang/clixon/ietf-netconf-collection@2020-10-22.yang b/yang/clixon/ietf-netconf-collection@2020-10-22.yang new file mode 100644 index 00000000..f9fc125a --- /dev/null +++ b/yang/clixon/ietf-netconf-collection@2020-10-22.yang @@ -0,0 +1,125 @@ +module ietf-netconf-collection { + namespace "urn:ietf:params:xml:ns:yang:ietf-netconf-collection"; + prefix "rcoll"; + + organization + "IETF NETCONF (Network Configuration) Working Group"; + + contact + "WG Web: + WG List: + + WG Chair: Mehmet Ersue + + + WG Chair: Mahesh Jethanandani + + + Editor: Andy Bierman + + + Editor: Martin Bjorklund + + + Editor: Kent Watsen + "; + + description + "This module contains conceptual YANG specifications + for the RESTCONF Collection resource type. + Note that the YANG definitions within this module do not + represent configuration data of any kind. + The YANG grouping statements provide a normative syntax + for XML and JSON message encoding purposes. + + Copyright (c) 2015 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC XXXX; see + the RFC itself for full legal notices."; + /* + module: ietf-netconf-collection + rpcs: + + +---x get-collection + +--w input + +--w module-name string + +--w datastore string + +--w list-target string + +--w count uint32 + +--w skip uint32 + +--w direction enum + +--w sort string + +--w where string + +--ro output + +-ro collection anydata + */ + + + revision 2020-10-22 { + description + "Draft by Olof Hagsand / Clixon."; + } + rpc get-collection { + input { + leaf module-name { + /* Not needed with proper list-target */ + type string; + } + leaf datastore { + type string; + default "running"; + } + leaf list-target { + description "api-path"; + mandatory true; + type string; + } + leaf count { + type union { + type uint32; + type string { + pattern 'unbounded'; + } + } + } + leaf skip { + type union { + type uint32; + type string { + pattern 'unbounded'; + } + } + } + leaf direction { + type enumeration { + enum forward; + enum reverse; + } + } + leaf sort { + type string; + } + leaf where { + type string; + } + } + output { + anyxml collection { + 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."; + } + } + } + +}