diff --git a/CHANGELOG.md b/CHANGELOG.md index 919d820f..61d19acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ ## 5.3.0 Expected: September, 2021 +### New features + +* Restconf YANG PATCH according to RFC 8072 + * Experimental: enable by setting YANG_PATCH in include/clixon_custom.h + * Thanks to Alan Yaniger for providing this patch + ### C/CLI-API changes on existing features Developers may need to change their code diff --git a/apps/restconf/restconf_main_fcgi.c b/apps/restconf/restconf_main_fcgi.c index 88622b6a..b8508102 100644 --- a/apps/restconf/restconf_main_fcgi.c +++ b/apps/restconf/restconf_main_fcgi.c @@ -419,6 +419,12 @@ main(int argc, if (yang_spec_parse_module(h, "ietf-restconf", NULL, yspec)< 0) goto done; +#ifdef YANG_PATCH + /* Load yang restconf patch module */ + if (yang_spec_parse_module(h, "ietf-yang-patch", NULL, yspec)< 0) + goto done; +#endif // YANG_PATCH + /* Add netconf yang spec, used as internal protocol */ if (netconf_module_load(h) < 0) goto done; diff --git a/apps/restconf/restconf_main_native.c b/apps/restconf/restconf_main_native.c index 29e65b7d..80a5a7a8 100644 --- a/apps/restconf/restconf_main_native.c +++ b/apps/restconf/restconf_main_native.c @@ -1737,6 +1737,12 @@ restconf_clixon_init(clicon_handle h, if (yang_spec_parse_module(h, "ietf-restconf", NULL, yspec)< 0) goto done; +#ifdef YANG_PATCH + /* Load yang restconf patch module */ + if (yang_spec_parse_module(h, "ietf-yang-patch", NULL, yspec)< 0) + goto done; +#endif // YANG_PATCH + /* Add netconf yang spec, used as internal protocol */ if (netconf_module_load(h) < 0) goto done; diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 10844869..3d9630bb 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -74,6 +74,7 @@ #include "restconf_api.h" #include "restconf_err.h" #include "restconf_methods.h" +#include "restconf_methods_post.h" /*! REST OPTIONS method * According to restconf @@ -579,6 +580,536 @@ api_data_write(clicon_handle h, return retval; } /* api_data_write */ +#ifdef YANG_PATCH +/*! YANG PATCH method + * @param[in] h Clixon handle + * @param[in] req Generic Www handle + * @param[in] api_path0 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 to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] media_out Output media + * Netconf: (nc:operation="merge") + * See RFC8072 + * YANG patch can be used to "create", "delete", "insert", "merge", "move", "replace", and/or + "remove" a resource within the target resource. + * Currently "move" not supported + */ +static int +api_data_yang_patch(clicon_handle h, + void *req, + char *api_path0, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + restconf_media media_out, + ietf_ds_t ds) +{ + int retval = -1; + int i; + cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ + cbuf *cbx = NULL; + cxobj *xtop = NULL; /* top of api-path */ + cxobj *xbot = NULL; /* bottom of api-path */ + yang_stmt *ybot = NULL; /* yang of xbot */ + cxobj *xbot_tmp = NULL; + yang_stmt *yspec; + char *api_path; + cxobj *xret = NULL; + cxobj *xretcom = NULL; /* return from commit */ + cxobj *xretdis = NULL; /* return from discard-changes */ + cxobj *xerr = NULL; /* malloced must be freed */ + int ret; + cvec *nsc = NULL; + yang_bind yb; + char *xpath = NULL; + const int temp_str_malloc_size = 5000; + char *path_orig_1 = NULL; + + clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path0); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + api_path=api_path0; + /* strip /... from start */ + for (i=0; i= 0; l--) { + if (target_val[l] == '/') { + idx = l; + break; + } + } + strncpy(post_req_uri, target_val, idx); + strcat(simple_patch_request_uri, post_req_uri); + free(post_req_uri); + for (int l = strlen(json_simple_patch); l>= 0; l--) { + char c = json_simple_patch[l]; + if (c == '}') { + json_simple_patch[l] = ']'; + json_simple_patch[l + 1] = '}'; + break; + } + } + + // Send the POST request + ret = api_data_post(h, req, simple_patch_request_uri, pi, qvec, json_simple_patch, pretty, YANG_DATA_JSON, media_out, ds ); + if (value_vec_tmp != NULL) + free(value_vec_tmp); + free(x_simple_patch); + free(patch_header); // NULL check was already done before + if (ret != 0) + goto done; + break; + } + + // For "create", put all the data values into a single POST request + if (strcmp(op_val,"create") == 0) { + for (int k = 0; k < value_vec_len; k++) { + if (value_vec[k] != NULL) { + value_vec_tmp = xml_dup(value_vec[k]); + xml_addsub(x_simple_patch, value_vec_tmp); + } + } + + // Send the POST request + cbuf* cb = cbuf_new(); + xml2json_cbuf(cb, x_simple_patch, 1); + char *json_simple_patch = cbuf_get(cb); + ret = api_data_post(h, req, simple_patch_request_uri, pi, qvec, json_simple_patch, pretty, YANG_DATA_JSON, media_out, ds ); + if (value_vec_tmp != NULL) + free(value_vec_tmp); + free(x_simple_patch); + free(patch_header); // NULL check was already done before + if (ret != 0) + goto done; + break; + } + // For "insert", make a api_data_post request + if (strcmp(op_val, "insert") == 0) { + char *json_simple_patch = malloc(temp_str_malloc_size); + if (json_simple_patch == NULL) + goto done; + memset(json_simple_patch, 0, temp_str_malloc_size); + + // Loop through the XML, and get each value + for (int k = 0; k < value_vec_len; k++) { + if (value_vec[k] != NULL) { + value_vec_tmp = xml_dup(value_vec[k]); + xml_addsub(x_simple_patch, value_vec_tmp); + } + } + cbuf* cb = cbuf_new(); + xml2json_cbuf(cb, x_simple_patch, 1); + + // Some ugly text processing to get the JSON to match what api_data_post() expects + char *json_simple_patch_tmp = cbuf_get(cb); + int brace_count = 0; + for (int l = 0; l < strlen(json_simple_patch_tmp); l++) { + char c = json_simple_patch_tmp[l]; + if (c == '{') { + brace_count++; + if (brace_count == 2) { + json_simple_patch[strlen(json_simple_patch)] = '['; + } + } + json_simple_patch[strlen(json_simple_patch)] = c; + } + for (int l = strlen(json_simple_patch); l>= 0; l--) { + char c = json_simple_patch[l]; + if (c == '}') { + json_simple_patch[l] = ']'; + json_simple_patch[l + 1] = '}'; + break; + } + } + + // Set the insert attributes + cvec* qvec_tmp = NULL; + qvec_tmp = cvec_new(0); + if (qvec_tmp == NULL) + goto done; + cg_var *cv; + if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ + goto done; + } + cv_name_set(cv, "insert"); + cv_string_set(cv, where_val); + char *point_str = malloc(temp_str_malloc_size); + if (point_str == NULL) + goto done; + memset(point_str, 0, temp_str_malloc_size); + strcpy(point_str, api_path); + strcat(point_str, point_val); + if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ + goto done; + } + cv_name_set(cv, "point"); + cv_string_set(cv, point_str); + + // Send the POST request + ret = api_data_post(h, req, simple_patch_request_uri, pi, qvec_tmp, json_simple_patch, pretty, YANG_DATA_JSON, media_out, ds ); + if (cb != NULL) + cbuf_free(cb); + if (value_vec_tmp != NULL) + free(value_vec_tmp); + free(point_str); // NULL check was already done above + free(json_simple_patch); // NULL check was already done above + free(patch_header); // NULL check was already done before + if (x_simple_patch != NULL) + free(x_simple_patch); + break; + } + + // For merge", make single simple patch requests for each value + if (strcmp(op_val,"merge") == 0) { + if (key_xn != NULL) + xml_addsub(x_simple_patch, key_xn); + + char *json_simple_patch = malloc(temp_str_malloc_size); + if (json_simple_patch == NULL) + goto done; + + // Loop through the XML, create JSON from each one, and submit a simple patch + for (int k = 0; k < value_vec_len; k++) { + if (value_vec[k] != NULL) { + value_vec_tmp = xml_dup(value_vec[k]); + xml_addsub(x_simple_patch, value_vec_tmp); + } + cbuf* cb = cbuf_new(); + xml2json_cbuf(cb, x_simple_patch, 1); + + // Some ugly text processing to get the JSON to match what api_data_write() expects for a simple patch + char *json_simple_patch_tmp = cbuf_get(cb); + memset(json_simple_patch, 0, temp_str_malloc_size); + int brace_count = 0; + for (int l = 0; l < strlen(json_simple_patch_tmp); l++) { + char c = json_simple_patch_tmp[l]; + if (c == '{') { + brace_count++; + if (brace_count == 2) { + json_simple_patch[strlen(json_simple_patch)] = '['; + } + } + json_simple_patch[strlen(json_simple_patch)] = c; + } + for (int l = strlen(json_simple_patch); l>= 0; l--) { + char c = json_simple_patch[l]; + if (c == '}') { + json_simple_patch[l] = ']'; + json_simple_patch[l + 1] = '}'; + break; + } + } + if (value_vec_tmp != NULL) + free(value_vec_tmp); + // Send the simple patch request + ret = api_data_write(h, req, simple_patch_request_uri, pcvec, pi, qvec, json_simple_patch, pretty, YANG_DATA_JSON, media_out, plain_patch_val, ds ); + cbuf_free(cb); + } + free(json_simple_patch); // NULL check was already done above + free(patch_header); // NULL check was already done before + if (x_simple_patch != NULL) + free(x_simple_patch); + } + } + if ((strcmp(op_val, "delete") == 0) || + (strcmp(op_val, "remove") == 0)) { + strcat(simple_patch_request_uri, target_val); + if (strcmp(op_val, "delete") == 0) { + // TODO - send error + } else { + // TODO - do not send error + } + api_data_delete(h, req, simple_patch_request_uri, pi, pretty, YANG_DATA_JSON, ds); + } + if (simple_patch_request_uri) + free(simple_patch_request_uri); + if (api_path_target) + free(api_path_target); + } + ok: + retval = 0; + done: + if (path_orig_1 != NULL) + free(path_orig_1); + if (vec) + free(vec); + if (xpath) + free(xpath); + if (nsc) + xml_nsctx_free(nsc); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (xretcom) + xml_free(xretcom); + if (xretdis) + xml_free(xretdis); + if (xtop) + xml_free(xtop); + if (xdata0) + xml_free(xdata0); + if (cbx) + cbuf_free(cbx); + return retval; +} +#endif // YANG_PATCH + /*! Generic REST PUT method * @param[in] h Clixon handle * @param[in] req Generic Www handle @@ -673,9 +1204,16 @@ api_data_patch(clicon_handle h, media_in, media_out, 1, ds); break; case YANG_PATCH_XML: - case YANG_PATCH_JSON: /* RFC 8072 patch */ ret = restconf_notimplemented(h, req, pretty, media_out); break; + case YANG_PATCH_JSON: /* RFC 8072 patch */ +#ifdef YANG_PATCH + ret = api_data_yang_patch(h, req, api_path0, pcvec, pi, qvec, data, pretty, + media_out, ds); +#else + ret = restconf_unsupported_media(h, req, pretty, media_out); +#endif + break; default: ret = restconf_unsupported_media(h, req, pretty, media_out); break; diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index bc0e2bfb..d018b9e7 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -155,6 +155,7 @@ api_data_post(clicon_handle h, cvec *qvec, char *data, int pretty, + restconf_media media_in, restconf_media media_out, ietf_ds_t ds) { @@ -178,7 +179,6 @@ api_data_post(clicon_handle h, cxobj *x; char *username; int ret; - restconf_media media_in; int nrchildren0 = 0; yang_bind yb; @@ -231,7 +231,6 @@ api_data_post(clicon_handle h, * If xbot is top-level (api_path=null) it does not have a spec therefore look for * top-level (yspec) otherwise assume parent (xbot) is populated. */ - media_in = restconf_content_type(h); switch (media_in){ case YANG_DATA_XML: if ((ret = clixon_xml_parse_string(data, yb, yspec, &xbot, &xerr)) < 0){ diff --git a/apps/restconf/restconf_methods_post.h b/apps/restconf/restconf_methods_post.h index f01e53ee..3317d3ea 100644 --- a/apps/restconf/restconf_methods_post.h +++ b/apps/restconf/restconf_methods_post.h @@ -44,6 +44,7 @@ int api_data_post(clicon_handle h, void *req, char *api_path, int pi, cvec *qvec, char *data, int pretty, + restconf_media media_in, restconf_media media_out, ietf_ds_t ds); int api_operations_post(clicon_handle h, void *req, char *api_path, diff --git a/apps/restconf/restconf_root.c b/apps/restconf/restconf_root.c index aa79953e..068acb4b 100644 --- a/apps/restconf/restconf_root.c +++ b/apps/restconf/restconf_root.c @@ -331,7 +331,7 @@ api_data(clicon_handle h, retval = api_data_get(h, req, api_path, pcvec, pi, qvec, pretty, media_out, ds); } else if (strcmp(request_method, "POST")==0) { - retval = api_data_post(h, req, api_path, pi, qvec, data, pretty, media_out, ds); + retval = api_data_post(h, req, api_path, pi, qvec, data, pretty, restconf_content_type(h), media_out, ds); } else if (strcmp(request_method, "PUT")==0) { if (read_only) diff --git a/include/clixon_custom.h b/include/clixon_custom.h index d4af4463..920b45f4 100644 --- a/include/clixon_custom.h +++ b/include/clixon_custom.h @@ -112,3 +112,8 @@ * solve all usecases, such as absolute usecases where the added node is looked for */ #define XML_PARENT_CANDIDATE + +/*! Enable yang patch RFC 8072 + * Remove this when regression test + */ +#undef YANG_PATCH diff --git a/yang/mandatory/Makefile.in b/yang/mandatory/Makefile.in index ca68b32a..f2fb8c9d 100644 --- a/yang/mandatory/Makefile.in +++ b/yang/mandatory/Makefile.in @@ -50,6 +50,7 @@ YANGSPECS += ietf-restconf-monitoring@2017-01-26.yang YANGSPECS += ietf-yang-library@2019-01-04.yang YANGSPECS += ietf-yang-types@2013-07-15.yang YANGSPECS += ietf-datastores@2018-02-14.yang +YANGSPECS += ietf-yang-patch@2017-02-22.yang all: