diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index df98c59d..ef1d7611 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -591,7 +591,7 @@ api_data_write(clicon_handle h, * @param[in] data Stream input data * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media - + * @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource * @note restconf PUT is mapped to edit-config replace. * @see RFC8040 Sec 4.5 PUT * @see api_data_post @@ -645,6 +645,7 @@ api_data_put(clicon_handle h, * @param[in] data Stream input data * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media + * @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource * Netconf: (nc:operation="merge") * See RFC8040 Sec 4.6.1 * Plain patch can be used to create or update, but not delete, a child @@ -678,7 +679,7 @@ api_data_patch(clicon_handle h, case YANG_PATCH_XML: #ifdef YANG_PATCH ret = api_data_yang_patch(h, req, api_path0, pcvec, pi, qvec, data, pretty, - media_out, ds); + media_in, media_out, ds); #else ret = restconf_notimplemented(h, req, pretty, media_out); #endif diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 11aa644e..881884e5 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -318,6 +318,7 @@ api_data_get2(clicon_handle h, * @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] ds 0 if "data" resource, 1 if rfc8527 "ds" resource * * The HEAD method is sent by the client to retrieve just the header fields * that would be returned for the comparable GET method, without the @@ -348,11 +349,10 @@ api_data_head(clicon_handle h, * @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] ds RFC8527 datastore * @code * curl -G http://localhost/restconf/data/interfaces/interface=eth0 * @endcode - * XXX: cant find a way to use Accept request field to choose Content-Type - * I would like to support both xml and json. * Request may contain * Accept: application/yang.data+json,application/yang.data+xml * Response contains one of: diff --git a/apps/restconf/restconf_methods_patch.c b/apps/restconf/restconf_methods_patch.c index 86de4197..dfa8e066 100644 --- a/apps/restconf/restconf_methods_patch.c +++ b/apps/restconf/restconf_methods_patch.c @@ -4,6 +4,7 @@ Copyright (C) 2009-2019 Olof Hagsand Copyright (C) 2020-2021 Olof Hagsand and Rubicon Communications, LLC(Netgate) + Copyright (C) 2021 Siklu Ltd (YANG patch code) This file is part of CLIXON. @@ -32,7 +33,7 @@ ***** END LICENSE BLOCK ***** - * Restconf YANG PATCH implementation + * Restconf YANG PATCH implementation (RFC8072) */ @@ -68,42 +69,42 @@ #ifdef YANG_PATCH -/*! Return a value within XML tags - * @param [in] nsc namespace context - * @param [in] xn cxobj containing XML with the current edit - * @param [in] val cbuf to which the value will be written - * @param [in] key string containing the tag - * @retval 0 success - * @retval <0 failure - */ -static int -yang_patch_get_xval(cvec *nsc, - cxobj *xn, - cbuf *val, - const char *key) -{ - cxobj **vec = NULL; - size_t veclen = 0; - char* tmp_val = NULL; - int ret; - cxobj *xn_tmp = NULL; +enum yang_patch_op{ + YANG_PATCH_OP_CREATE, + YANG_PATCH_OP_DELETE, + YANG_PATCH_OP_INSERT, + YANG_PATCH_OP_MERGE, + YANG_PATCH_OP_MOVE, + YANG_PATCH_OP_REPLACE, + YANG_PATCH_OP_REMOVE +}; +typedef enum yang_patch_op yang_patch_op_t; - if ((ret = xpath_vec(xn, nsc, "%s", &vec, &veclen, key)) < 0) - return ret; - if (veclen == 1){ //veclen should always be 1 - xn_tmp = vec[0]; - tmp_val = xml_body(xn_tmp); - cbuf_append_str(val, tmp_val); - } - return 0; +/* Yang patch operations according to RFC 8072 + */ +static const map_str2int yang_patch_op_map[] = { + {"create", YANG_PATCH_OP_CREATE}, + {"delete", YANG_PATCH_OP_DELETE}, + {"insert", YANG_PATCH_OP_INSERT}, + {"merge", YANG_PATCH_OP_MERGE}, + {"move", YANG_PATCH_OP_MOVE}, + {"replace", YANG_PATCH_OP_REPLACE}, + {"remove", YANG_PATCH_OP_REMOVE}, + {NULL, -1} +}; + +static const yang_patch_op_t +yang_patch_op2int(char *op) +{ + return clicon_str2int(yang_patch_op_map, op); } /*! Add square brackets after the surrounding curly brackets in JSON * Needed, in order to modify the result of xml2json_cbuf() to be valid input * to api_data_post() and api_data_write() - * @param [in] x_simple_patch a cxobj to pass to xml2json_cbuf() - * @retval new cbuf with the modified json - * @retval NULL Error + * @param[in] x_simple_patch a cxobj to pass to xml2json_cbuf() + * @retva cbuf With the modified json + * @retva NULL Error */ static cbuf* yang_patch_xml2json_modified_cbuf(cxobj *x_simple_patch) @@ -152,12 +153,12 @@ yang_patch_xml2json_modified_cbuf(cxobj *x_simple_patch) * so that e.g. "/interface=eth2" becomes "/" * or "/interface_list=mylist/interface=eth2" becomes "/interface_list=mylist/" * - * @param[in] val value to strip - * @retval new cbuf with the stripped string - * @retval NULL error + * @param[in] val value to strip + * @retval cbuf with the stripped string + * @retval NULL error */ static cbuf* -yang_patch_strip_after_last_slash(cbuf* val) +yang_patch_strip_after_last_slash(char* val) { cbuf *cb; cbuf *val_tmp; @@ -165,7 +166,7 @@ yang_patch_strip_after_last_slash(cbuf* val) cb = cbuf_new(); val_tmp = cbuf_new(); - cbuf_append_str(val_tmp, cbuf_get(val)); + cbuf_append_str(val_tmp, val); idx = cbuf_len(val_tmp); for (int l = cbuf_len(val_tmp) - 1; l>= 0; l--) { if (cbuf_get(val_tmp)[l] == '/') { @@ -197,56 +198,63 @@ yang_patch_strip_after_last_slash(cbuf* val) * @param[in] x_simple_patch pointer to XML containing module name, e.g. */ static int -yang_patch_do_replace (clicon_handle h, - void *req, - int pi, - cvec *qvec, - int pretty, - restconf_media media_out, - ietf_ds_t ds, - cbuf *simple_patch_request_uri, - cbuf *target_val, - int value_vec_len, - cxobj **value_vec, - cxobj *x_simple_patch - ) +yang_patch_do_replace(clicon_handle h, + void *req, + int pi, + cvec *qvec, + int pretty, + restconf_media media_out, + ietf_ds_t ds, + cbuf *simple_patch_request_uri, + char *target_val, + int value_vec_len, + cxobj **value_vec, + cxobj *x_simple_patch + ) { + int retval = -1; cxobj *value_vec_tmp = NULL; cbuf *delete_req_uri = NULL; - int ret; cbuf *post_req_uri = NULL; cbuf *json_simple_patch = NULL; - delete_req_uri = cbuf_new(); - if (delete_req_uri == NULL) - return 1; - + if ((delete_req_uri = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((json_simple_patch = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } // Make delete_req_uri something like "/restconf/data/ietf-interfaces:interfaces" - if (cbuf_append_str(delete_req_uri, cbuf_get(simple_patch_request_uri)) < 0) - return 1; + if (cbuf_append_str(delete_req_uri, cbuf_get(simple_patch_request_uri)) < 0){ + clicon_err(OE_UNIX, errno, "cbuf_append_str"); + goto done; + } // Add the target to delete_req_uri, // so it's something like "/restconf/data/ietf-interfaces:interfaces/interface=eth2" - if (cbuf_append_str(delete_req_uri, cbuf_get(target_val)) < 0) - return 1; + if (cbuf_append_str(delete_req_uri, target_val) < 0){ + clicon_err(OE_UNIX, errno, "cbuf_append_str"); + goto done; + } // Delete the object with the old values - ret = api_data_delete(h, req, cbuf_get(delete_req_uri), pi, pretty, YANG_DATA_JSON, ds ); - cbuf_free(delete_req_uri); - if (ret != 0) - return ret; + if (api_data_delete(h, req, cbuf_get(delete_req_uri), pi, pretty, YANG_DATA_JSON, ds) < 0) + goto done; // Now set up for the post request. // Strip /... from end of target val // so that e.g. "/interface=eth2" becomes "/" // or "/interface_list=mylist/interface=eth2" becomes "/interface_list=mylist/" - post_req_uri = yang_patch_strip_after_last_slash(target_val); + if ((post_req_uri = yang_patch_strip_after_last_slash(target_val)) == NULL) + goto done; // Make post_req_uri something like "/restconf/data/ietf-interfaces:interfaces" - if (cbuf_append_str(simple_patch_request_uri, cbuf_get(post_req_uri))) - return 1; - cbuf_free(post_req_uri); - + if (cbuf_append_str(simple_patch_request_uri, cbuf_get(post_req_uri)) < 0){ + clicon_err(OE_UNIX, errno, "cbuf_append_str"); + goto done; + } // Now insert the new values into the data // (which will include the key value and all other mandatory values) for (int k = 0; k < value_vec_len; k++) { @@ -256,17 +264,22 @@ yang_patch_do_replace (clicon_handle h, } } // Convert the data to json - json_simple_patch = cbuf_new(); - if (json_simple_patch == NULL) - return 1; xml2json_cbuf(json_simple_patch, x_simple_patch, 1); // Send the POST request - ret = api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, ds ); - - cbuf_free(json_simple_patch); - xml_free(value_vec_tmp); - return ret; + if (api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, ds ) < 0) + goto done; + retval = 0; + done: + if (post_req_uri) + cbuf_free(post_req_uri); + if (delete_req_uri) + cbuf_free(delete_req_uri); + if (json_simple_patch) + cbuf_free(json_simple_patch); + if (value_vec_tmp) + xml_free(value_vec_tmp); + return retval; } /*! YANG PATCH create method @@ -283,45 +296,45 @@ yang_patch_do_replace (clicon_handle h, * @param[in] x_simple_patch pointer to XML containing module name, e.g. */ static int -yang_patch_do_create (clicon_handle h, - void *req, - int pi, - cvec *qvec, - int pretty, - restconf_media media_out, - ietf_ds_t ds, - cbuf *simple_patch_request_uri, - int value_vec_len, - cxobj **value_vec, - cxobj *x_simple_patch - ) +yang_patch_do_create(clicon_handle h, + void *req, + int pi, + cvec *qvec, + int pretty, + restconf_media media_out, + ietf_ds_t ds, + cbuf *simple_patch_request_uri, + int value_vec_len, + cxobj **value_vec, + cxobj *x_simple_patch + ) { int retval = -1; cxobj *value_vec_tmp = NULL; cbuf *cb = NULL; - char *json_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); - } - } // Send the POST request if ((cb = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } + for (int k = 0; k < value_vec_len; k++) { + if (value_vec[k] != NULL) { + if ((value_vec_tmp = xml_dup(value_vec[k])) == NULL) + goto done; + xml_addsub(x_simple_patch, value_vec_tmp); + } + } if (xml2json_cbuf(cb, x_simple_patch, 1) < 0) goto done; - json_simple_patch = cbuf_get(cb); if (api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec, - json_simple_patch, pretty, YANG_DATA_JSON, media_out, ds) < 0) + cbuf_get(cb), pretty, YANG_DATA_JSON, media_out, ds) < 0) goto done; - xml_free(value_vec_tmp); retval = 0; done: + if (cb) + cbuf_free(cb); return retval; } @@ -341,66 +354,76 @@ yang_patch_do_create (clicon_handle h, * @param[in] point_val value in "point" field of edit in YANG patch */ static int -yang_patch_do_insert (clicon_handle h, - void *req, - int pi, - int pretty, - restconf_media media_out, - ietf_ds_t ds, - cbuf *simple_patch_request_uri, - int value_vec_len, - cxobj **value_vec, - cxobj *x_simple_patch, - cbuf *where_val, - char *api_path, - cbuf *point_val - ) +yang_patch_do_insert(clicon_handle h, + void *req, + int pi, + int pretty, + restconf_media media_out, + ietf_ds_t ds, + cbuf *simple_patch_request_uri, + int value_vec_len, + cxobj **value_vec, + cxobj *x_simple_patch, + char *where_val, + char *api_path, + char *point_val + ) { - cxobj *value_vec_tmp = NULL; - cbuf *json_simple_patch; - cg_var *cv; - cbuf *point_str = NULL; - int ret; - - // 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); - } - } - json_simple_patch = yang_patch_xml2json_modified_cbuf(x_simple_patch); - if (json_simple_patch == NULL) - return 1; + int retval = -1; + cxobj *value_vec_tmp = NULL; + cbuf *json_simple_patch; + cg_var *cv; + cbuf *point_str = NULL; + cvec *qvec_tmp = NULL; + + if ((point_str = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((qvec_tmp = cvec_new(0)) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + // 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); + } + } + if ((json_simple_patch = yang_patch_xml2json_modified_cbuf(x_simple_patch)) == NULL) + goto done; - // Set the insert attributes - cvec* qvec_tmp = NULL; - qvec_tmp = cvec_new(0); - if (qvec_tmp == NULL) - return 1; + // Set the insert attributes + if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_add"); + goto done; + } + cv_name_set(cv, "insert"); + if (where_val) + cv_string_set(cv, where_val); + cbuf_append_str(point_str, api_path); + if (point_val) + cbuf_append_str(point_str, point_val); + if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_add"); + goto done; + } + cv_name_set(cv, "point"); + cv_string_set(cv, cbuf_get(point_str)); - if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ - return 1; - } - cv_name_set(cv, "insert"); - cv_string_set(cv, cbuf_get(where_val)); - point_str = cbuf_new(); - if (point_str == NULL) - return 1; - cbuf_append_str(point_str, api_path); - cbuf_append_str(point_str, cbuf_get(point_val)); - if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ - return 1; - } - cv_name_set(cv, "point"); - cv_string_set(cv, cbuf_get(point_str)); - - // Send the POST request - ret = api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec_tmp, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, ds ); - xml_free(value_vec_tmp); - cbuf_free(point_str); - cbuf_free(json_simple_patch); - return ret; + // Send the POST request + if (api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec_tmp, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, ds)< 0) + goto done; + retval = 0; + done: + if (qvec_tmp) + cvec_free(qvec_tmp); + if (point_str) + cbuf_free(point_str); + if (json_simple_patch) + cbuf_free(json_simple_patch); + return retval; } /*! YANG PATCH merge method @@ -420,26 +443,30 @@ yang_patch_do_insert (clicon_handle h, * @param[in] key_xn XML with key tag and value, e.g. "Foo-One" */ static int -yang_patch_do_merge(clicon_handle h, - void *req, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, +yang_patch_do_merge(clicon_handle h, + void *req, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, restconf_media media_out, - ietf_ds_t ds, - cbuf* simple_patch_request_uri, - int value_vec_len, - cxobj** value_vec, - cxobj *x_simple_patch, - cxobj *key_xn + ietf_ds_t ds, + cbuf *simple_patch_request_uri, + int value_vec_len, + cxobj **value_vec, + cxobj *x_simple_patch, + cxobj *key_xn ) { - int ret = -1; + int retval = -1; cxobj *value_vec_tmp = NULL; cbuf *cb = NULL; cbuf *json_simple_patch = NULL; + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } if (key_xn != NULL) xml_addsub(x_simple_patch, key_xn); @@ -449,19 +476,251 @@ yang_patch_do_merge(clicon_handle h, value_vec_tmp = xml_dup(value_vec[k]); xml_addsub(x_simple_patch, value_vec_tmp); } - cb = cbuf_new(); + cbuf_reset(cb); /* reuse cb */ xml2json_cbuf(cb, x_simple_patch, 1); - json_simple_patch = yang_patch_xml2json_modified_cbuf(x_simple_patch); - if (json_simple_patch == NULL) - return 1; - xml_free(value_vec_tmp); + if ((json_simple_patch = yang_patch_xml2json_modified_cbuf(x_simple_patch)) == NULL) + goto done; // Send the simple patch request - ret = api_data_write(h, req, cbuf_get(simple_patch_request_uri), pcvec, pi, qvec, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, 1, ds ); - cbuf_free(cb); - cbuf_free(json_simple_patch); + if (api_data_write(h, req, cbuf_get(simple_patch_request_uri), pcvec, pi, qvec, cbuf_get(json_simple_patch), pretty, YANG_DATA_JSON, media_out, 1, ds ) < 0) + goto done; + if (json_simple_patch){ + cbuf_free(json_simple_patch); + json_simple_patch = NULL; + } } - return ret; + retval = 0; + done: + if (cb) + cbuf_free(cb); + if (json_simple_patch) + cbuf_free(json_simple_patch); + return retval; +} + +/*! Process a single yang patch "edit/value" element + * + * @param[in] xn XML value element + * @retval 0 OK + * @retval -1 Error + */ +static int +yang_patch_do_value(clicon_handle h, + void *req, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + restconf_media media_out, + ietf_ds_t ds, + cxobj *xn, + char *modname, + yang_patch_op_t operation, + char *where_val, + char *point_val, + cbuf *simple_patch_request_uri, + char *target_val, + char *api_path, + cxobj *key_xn + ) +{ + int retval = -1; + cxobj **values_child_vec; + char *key_node_id; + cbuf *patch_header = NULL; + cxobj *x_simple_patch = NULL; + int value_vec_len; + cxobj **value_vec; + + values_child_vec = xml_childvec_get(xn); + key_node_id = xml_name(*values_child_vec); + /* Create cbufs:s */ + if ((patch_header = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(patch_header, "%s:%s", modname, key_node_id); + if ((x_simple_patch = xml_new(cbuf_get(patch_header), NULL, CX_ELMNT)) == NULL) + goto done; + value_vec_len = xml_child_nr(*values_child_vec); + value_vec = xml_childvec_get(*values_child_vec); + switch (operation){ + case YANG_PATCH_OP_REPLACE: + if (yang_patch_do_replace(h, req, pi, qvec, pretty, media_out, ds, simple_patch_request_uri, target_val, value_vec_len, value_vec, x_simple_patch) < 0) + goto done; + break; + case YANG_PATCH_OP_CREATE: + if (yang_patch_do_create(h, req, pi, qvec, pretty, media_out, ds, simple_patch_request_uri, value_vec_len, value_vec, x_simple_patch) < 0) + goto done; + break; + case YANG_PATCH_OP_INSERT: + if (yang_patch_do_insert(h, req, pi, pretty, media_out, ds, simple_patch_request_uri, value_vec_len, value_vec, x_simple_patch, where_val, api_path, point_val) < 0) + goto done; + break; + case YANG_PATCH_OP_MERGE: + if (yang_patch_do_merge(h, req, pcvec, pi, qvec, pretty, media_out, ds, simple_patch_request_uri, value_vec_len, value_vec, x_simple_patch, key_xn) < 0) + goto done; + break; + default: + break; + } + retval = 0; + done: + if (x_simple_patch) + xml_free(x_simple_patch); + if (patch_header) + cbuf_free(patch_header); + return retval; +} + +/*! Process a single yang patch "edit" element + * + * @param[in] h Clixon handle + * @param[in] req Generic Www handle + * @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] pretty Set to 1 for pretty-printed xml/json output + * @param[in] media_out Output media + * @param[in] xn XML edit element + * @retval 0 OK + * @retval -1 Error + */ +static int +yang_patch_do_edit(clicon_handle h, + void *req, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + restconf_media media_out, + ietf_ds_t ds, + yang_stmt *yspec, + cxobj *xn, + char *uripath0, + char *api_path + ) +{ + int retval = -1; + cxobj **vec = NULL; + size_t veclen = 0; + int ret; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xtop; + cxobj *xbot = NULL; + yang_patch_op_t operation; + char *where_val = NULL; + char *point_val = NULL; + char *target_val = NULL; + char *modname; + cxobj **key_vec = NULL; + cxobj *key_xn = NULL; + int i; + cxobj *x; /* general purpose xml-tree pointer */ + cbuf *simple_patch_request_uri = NULL; + cbuf *api_path_target = NULL; + yang_stmt *ybot = NULL; + yang_stmt *ymod; + + + clicon_log_xml(LOG_DEBUG, xn, "%s %d xn:", __FUNCTION__, __LINE__); + /* Create cbufs:s */ + if ((simple_patch_request_uri = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((api_path_target = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((x = xpath_first(xn, NULL, "target")) == NULL){ + clicon_err(OE_YANG, 0, "target mandatory element not found"); + goto done; + } + target_val = xml_body(x); + if ((x = xpath_first(xn, NULL, "operation")) == NULL){ + clicon_err(OE_YANG, 0, "operation mandatory element not found"); + goto done; + } + operation = yang_patch_op2int(xml_body(x)); + /* target and operation are mandatory */ + if (target_val == NULL){ + clicon_err(OE_YANG, 0, "operation/target: mandatory element not found"); + goto done; + } + if (operation == YANG_PATCH_OP_INSERT){ + if ((x = xpath_first(xn, NULL, "point")) != NULL) + point_val = xml_body(x); + if ((x = xpath_first(xn, NULL, "where")) != NULL) + where_val = xml_body(x); + if (point_val == NULL || where_val == NULL){ + clicon_err(OE_YANG, 0, "point/where: expected element not found"); + goto done; + } + } + // Construct request URI + cprintf(simple_patch_request_uri, "%s", uripath0); + cprintf(api_path_target, "%s", api_path); + if (operation == YANG_PATCH_OP_MERGE) { + cbuf_append_str(api_path_target, target_val); + cbuf_append_str(simple_patch_request_uri, target_val); + } + + if ((xtop = xml_new(NETCONF_INPUT_CONFIG, NULL, CX_ELMNT)) == NULL) + goto done; + + // Get key field + /* Translate api_path to xml in the form of xtop/xbot */ + xbot = xtop; + if ((ret = api_path2xml(cbuf_get(api_path_target), yspec, xtop, YC_DATANODE, 1, &xbot, &ybot, &xerr)) < 0) + goto done; + if (ret == 0){ /* validation failed */ + if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0) + goto done; + goto ok; + } + /* Get module name */ + if (ys_real_module(ybot, &ymod) < 0) + goto done; + modname = yang_argument_get(ymod); + // XXX this seems to be hardcoded to a yang list? + if ((key_vec = xml_childvec_get(xbot)) != NULL) + key_xn = key_vec[0]; + // Get values (for "delete" and "remove", there are no values) + xpath_vec(xn, NULL, "value", &vec, &veclen); + + // Loop through the values + for (i = 0; i < veclen; i++) { + if (yang_patch_do_value(h, req, pcvec, pi, qvec, pretty, media_out, ds, + vec[i], modname, + operation, where_val, point_val, simple_patch_request_uri, target_val, + api_path, key_xn) < 0) + goto done; + } + if (operation == YANG_PATCH_OP_DELETE || + operation == YANG_PATCH_OP_REMOVE){ + cbuf_append_str(simple_patch_request_uri, target_val); + if (operation == YANG_PATCH_OP_DELETE) { + // TODO - send error + } else { + // TODO - do not send error + } + api_data_delete(h, req, cbuf_get(simple_patch_request_uri), pi, pretty, YANG_DATA_JSON, ds); + } + ok: + retval = 0; + done: + if (vec) + free(vec); + if (simple_patch_request_uri) + cbuf_free(simple_patch_request_uri); + if (api_path_target) + cbuf_free(api_path_target); + if (xtop) + xml_free(xtop); + if (xerr) + xml_free(xerr); + return retval; } /*! YANG PATCH method @@ -473,47 +732,38 @@ yang_patch_do_merge(clicon_handle h, * @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_in Input media * @param[in] media_out Output media + * @param[in] ds 0 if "data" resource, 1 if rfc8527 "ds" resource + * @retval 0 OK + * @retval -1 Error * Netconf: (nc:operation="merge") - * See RFC8072 + * @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 */ int -api_data_yang_patch(clicon_handle h, - void *req, - char *api_path0, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, +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_in, restconf_media media_out, - ietf_ds_t ds) + 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; + cxobj *xpatch = 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; - cbuf *path_orig_1 = NULL; - char yang_patch_path[] = "/ietf-yang-patch:yang-patch"; - int nrchildren0 = 0; - cxobj *x = NULL; + char *uripath0 = NULL; size_t veclen; cxobj **vec = NULL; @@ -526,31 +776,27 @@ api_data_yang_patch(clicon_handle h, /* strip /... from start */ for (i=0; i $dir/example-system.yang } EOF -# Yang Patch spec (fyangpatch must be set) -. ./yang-patch.sh - # Common Jukebox spec (fjukebox must be set) . ./jukebox.sh diff --git a/test/test_restconf_yang_patch_xml.sh b/test/test_restconf_yang_patch_xml.sh index 93fa2727..58b16c40 100755 --- a/test/test_restconf_yang_patch_xml.sh +++ b/test/test_restconf_yang_patch_xml.sh @@ -15,7 +15,6 @@ APPNAME=example cfg=$dir/conf.xml startupdb=$dir/startup_db fjukebox=$dir/example-jukebox.yang -fyangpatch=$dir/ietf-yang-patch.yang finterfaces=$dir/ietf-interfaces.yang fexample=$dir/clixon-example.yang @@ -105,9 +104,6 @@ cat < $dir/example-system.yang } EOF -# Yang Patch spec (fyangpatch must be set) -. ./yang-patch.sh - # Interfaces spec (finterfaces must be set) . ./interfaces.sh diff --git a/test/yang-patch.sh b/test/yang-patch.sh deleted file mode 100755 index 1d718725..00000000 --- a/test/yang-patch.sh +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/env bash -# for rfc 8072 -# Assumes fyangpatch is set to name of yang file - -cat < $fyangpatch - module ietf-yang-patch { - yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:ietf-yang-patch"; - prefix "ypatch"; - - import ietf-restconf { prefix rc; } - - organization - "IETF NETCONF (Network Configuration) Working Group"; - - contact - "WG Web: - WG List: - - Author: Andy Bierman - - - Author: Martin Bjorklund - - - Author: Kent Watsen - "; - - description - "This module contains conceptual YANG specifications - for the YANG Patch and YANG Patch Status data structures. - - 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) 2017 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 8072; see - the RFC itself for full legal notices."; - - revision 2017-02-22 { - description - "Initial revision."; - reference - "RFC 8072: YANG Patch Media Type."; - } - - typedef target-resource-offset { - type string; - description - "Contains a data resource identifier string representing - a sub-resource within the target resource. - The document root for this expression is the - target resource that is specified in the - protocol operation (e.g., the URI for the PATCH request). - - This string is encoded according to the same rules as those - for a data resource identifier in a RESTCONF request URI."; - reference - "RFC 8040, Section 3.5.3."; - } - - rc:yang-data "yang-patch" { - uses yang-patch; - } - - rc:yang-data "yang-patch-status" { - uses yang-patch-status; - } - - grouping yang-patch { - - description - "A grouping that contains a YANG container representing the - syntax and semantics of a YANG Patch edit request message."; - - container yang-patch { - description - "Represents a conceptual sequence of datastore edits, - called a patch. Each patch is given a client-assigned - patch identifier. Each edit MUST be applied - in ascending order, and all edits MUST be applied. - If any errors occur, then the target datastore MUST NOT - be changed by the YANG Patch operation. - - It is possible for a datastore constraint violation to occur - due to any node in the datastore, including nodes not - included in the 'edit' list. Any validation errors MUST - be reported in the reply message."; - - reference - "RFC 7950, Section 8.3."; - - leaf patch-id { - type string; - mandatory true; - description - "An arbitrary string provided by the client to identify - the entire patch. Error messages returned by the server - that pertain to this patch will be identified by this - 'patch-id' value. A client SHOULD attempt to generate - unique 'patch-id' values to distinguish between - transactions from multiple clients in any audit logs - maintained by the server."; - } - - leaf comment { - type string; - description - "An arbitrary string provided by the client to describe - the entire patch. This value SHOULD be present in any - audit logging records generated by the server for the - patch."; - } - - list edit { - key edit-id; - ordered-by user; - - description - "Represents one edit within the YANG Patch request message. - The 'edit' list is applied in the following manner: - - - The first edit is conceptually applied to a copy - of the existing target datastore, e.g., the - running configuration datastore. - - Each ascending edit is conceptually applied to - the result of the previous edit(s). - - After all edits have been successfully processed, - the result is validated according to YANG constraints. - - If successful, the server will attempt to apply - the result to the target datastore."; - - leaf edit-id { - type string; - description - "Arbitrary string index for the edit. - Error messages returned by the server that pertain - to a specific edit will be identified by this value."; - } - - leaf operation { - type enumeration { - enum create { - description - "The target data node is created using the supplied - value, only if it does not already exist. The - 'target' leaf identifies the data node to be - created, not the parent data node."; - } - enum delete { - description - "Delete the target node, only if the data resource - currently exists; otherwise, return an error."; - } - - enum insert { - description - "Insert the supplied value into a user-ordered - list or leaf-list entry. The target node must - represent a new data resource. If the 'where' - parameter is set to 'before' or 'after', then - the 'point' parameter identifies the insertion - point for the target node."; - } - enum merge { - description - "The supplied value is merged with the target data - node."; - } - enum move { - description - "Move the target node. Reorder a user-ordered - list or leaf-list. The target node must represent - an existing data resource. If the 'where' parameter - is set to 'before' or 'after', then the 'point' - parameter identifies the insertion point to move - the target node."; - } - enum replace { - description - "The supplied value is used to replace the target - data node."; - } - enum remove { - description - "Delete the target node if it currently exists."; - } - } - mandatory true; - description - "The datastore operation requested for the associated - 'edit' entry."; - } - - leaf target { - type target-resource-offset; - mandatory true; - description - "Identifies the target data node for the edit - operation. If the target has the value '/', then - the target data node is the target resource. - The target node MUST identify a data resource, - not the datastore resource."; - } - - leaf point { - when "(../operation = 'insert' or ../operation = 'move')" - + "and (../where = 'before' or ../where = 'after')" { - description - "This leaf only applies for 'insert' or 'move' - operations, before or after an existing entry."; - } - type target-resource-offset; - description - "The absolute URL path for the data node that is being - used as the insertion point or move point for the - target of this 'edit' entry."; - } - - leaf where { - when "../operation = 'insert' or ../operation = 'move'" { - description - "This leaf only applies for 'insert' or 'move' - operations."; - } - type enumeration { - enum before { - description - "Insert or move a data node before the data resource - identified by the 'point' parameter."; - } - enum after { - description - "Insert or move a data node after the data resource - identified by the 'point' parameter."; - } - - enum first { - description - "Insert or move a data node so it becomes ordered - as the first entry."; - } - enum last { - description - "Insert or move a data node so it becomes ordered - as the last entry."; - } - } - default last; - description - "Identifies where a data resource will be inserted - or moved. YANG only allows these operations for - list and leaf-list data nodes that are - 'ordered-by user'."; - } - - anydata value { - when "../operation = 'create' " - + "or ../operation = 'merge' " - + "or ../operation = 'replace' " - + "or ../operation = 'insert'" { - description - "The anydata 'value' is only used for 'create', - 'merge', 'replace', and 'insert' operations."; - } - description - "Value used for this edit operation. The anydata 'value' - contains the target resource associated with the - 'target' leaf. - - For example, suppose the target node is a YANG container - named foo: - - container foo { - leaf a { type string; } - leaf b { type int32; } - } - - The 'value' node contains one instance of foo: - - - - some value - 42 - - - "; - } - } - } - - } // grouping yang-patch - - grouping yang-patch-status { - - description - "A grouping that contains a YANG container representing the - syntax and semantics of a YANG Patch Status response - message."; - - container yang-patch-status { - description - "A container representing the response message sent by the - server after a YANG Patch edit request message has been - processed."; - - leaf patch-id { - type string; - mandatory true; - description - "The 'patch-id' value used in the request."; - } - - choice global-status { - description - "Report global errors or complete success. - If there is no case selected, then errors - are reported in the 'edit-status' container."; - - case global-errors { - uses rc:errors; - description - "This container will be present if global errors that - are unrelated to a specific edit occurred."; - } - leaf ok { - type empty; - description - "This leaf will be present if the request succeeded - and there are no errors reported in the 'edit-status' - container."; - } - } - - container edit-status { - description - "This container will be present if there are - edit-specific status responses to report. - If all edits succeeded and the 'global-status' - returned is 'ok', then a server MAY omit this - container."; - - list edit { - key edit-id; - - description - "Represents a list of status responses, - corresponding to edits in the YANG Patch - request message. If an 'edit' entry was - skipped or not reached by the server, - then this list will not contain a corresponding - entry for that edit."; - - leaf edit-id { - type string; - description - "Response status is for the 'edit' list entry - with this 'edit-id' value."; - } - - choice edit-status-choice { - description - "A choice between different types of status - responses for each 'edit' entry."; - leaf ok { - type empty; - description - "This 'edit' entry was invoked without any - errors detected by the server associated - with this edit."; - } - case errors { - uses rc:errors; - description - "The server detected errors associated with the - edit identified by the same 'edit-id' value."; - } - } - } - } - } - } // grouping yang-patch-status - - } -EOF1