/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2019 Olof Hagsand Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate) Copyright (C) 2021 Siklu Ltd (YANG patch code) This file is part of CLIXON. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Alternatively, the contents of this file may be used under the terms of the GNU General Public License Version 3 or later (the "GPL"), in which case the provisions of the GPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the GPL, and not to allow others to use your version of this file under the terms of Apache License version 2, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** * Restconf YANG PATCH implementation (RFC8072) */ #ifdef HAVE_CONFIG_H #include "clixon_config.h" /* generated by config & autoconf */ #endif #include #include #include #include #include #include #include #include #include #include #include #include /* cligen */ #include /* clixon */ #include #include "restconf_lib.h" #include "restconf_handle.h" #include "restconf_api.h" #include "restconf_err.h" #include "restconf_methods.h" #include "restconf_methods_post.h" #include "restconf_methods_patch.h" #ifdef CLIXON_YANG_PATCH 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; /* 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 clixon_json2cbuf() to be valid input * to api_data_post() and api_data_write() * @param[in] x_simple_patch a cxobj to pass to clixon_json2cbuf() * @retval cbuf With the modified json * @retval NULL Error */ static cbuf* yang_patch_xml2json_modified_cbuf(cxobj *x_simple_patch) { cbuf *json_simple_patch = NULL; cbuf *cb = NULL; char *json_simple_patch_tmp; int brace_count = 0; size_t len; json_simple_patch = cbuf_new(); if (json_simple_patch == NULL) return NULL; cb = cbuf_new(); if (clixon_json2cbuf(cb, x_simple_patch, 0, 0) < 0) goto done; // Insert a '[' after the first '{' to get the JSON to match what api_data_post/write() expect json_simple_patch_tmp = cbuf_get(cb); len = strlen(json_simple_patch_tmp); for (int l = 0; l < len; l++) { char c = json_simple_patch_tmp[l]; if (c == '{') { brace_count++; if (brace_count == 2) { // We've reached the second brace, insert a '[' before it cbuf_append(json_simple_patch,(int)'['); } } cbuf_append(json_simple_patch,(int)c); } // Insert a ']' before the last '}' to get the JSON to match what api_data_post() expects for (int l = cbuf_len(json_simple_patch) - 1; l >= 0; l--) { char c = cbuf_get(json_simple_patch)[l]; if (c == '}') { // Truncate and add a string, as there is not a function to insert a char into a cbuf cbuf_trunc(json_simple_patch, l); cbuf_append_str(json_simple_patch, "]}"); break; } } cbuf_free(cb); return json_simple_patch; } /*! yang_patch_strip_after_last_slash * * Strip /... from end of val * so that e.g. "/interface=eth2" becomes "/" * or "/interface_list=mylist/interface=eth2" becomes "/interface_list=mylist/" * * @param[in] val value to strip * @retval cbuf with the stripped string * @retval NULL error */ static cbuf* yang_patch_strip_after_last_slash(char* val) { cbuf *cb; cbuf *val_tmp; int idx; cb = cbuf_new(); val_tmp = cbuf_new(); 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] == '/') { idx = l; break; } } if (idx == cbuf_len(val_tmp)) // Didn't find a slash in the loop above return NULL; cbuf_trunc(val_tmp, idx + 1); if (cbuf_append_str(cb, cbuf_get(val_tmp)) < 0) return NULL; cbuf_free(val_tmp); return cb; } /*! YANG PATCH replace method * * @param[in] h Clixon handle * @param[in] req Generic Www handle * @param[in] pi Offset, where to start api-path * @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 * @param[in] simplepatch_request_uri URI for patch request, e.g. "/restconf/data/ietf-interfaces:interfaces" * @param[in] target_val value in "target" field of edit in YANG patch * @param[in] value_vec_len number of elements in the "value" array of an edit in YANG patch * @param[in] value_vec pointer to the "value" array of an edit in YANG patch * @param[in] x_simple_patch pointer to XML containing module name, e.g. * @retval 0 OK * @retval -1 Error */ static int yang_patch_do_replace(clixon_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; cbuf *post_req_uri = NULL; cbuf *json_simple_patch = NULL; if ((delete_req_uri = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if ((json_simple_patch = cbuf_new()) == NULL){ clixon_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){ clixon_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, target_val) < 0){ clixon_err(OE_UNIX, errno, "cbuf_append_str"); goto done; } // Delete the object with the old values 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/" 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)) < 0){ clixon_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++) { if (value_vec[k] != NULL) { value_vec_tmp = xml_dup(value_vec[k]); xml_addsub(x_simple_patch, value_vec_tmp); } } // Convert the data to json if (clixon_json2cbuf(json_simple_patch, x_simple_patch, 0, 0) < 0) goto done; // Send the POST request 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 * * @param[in] h Clixon handle * @param[in] req Generic Www handle * @param[in] pi Offset, where to start api-path * @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 * @param[in] simplepatch_request_uri URI for patch request, e.g. "/restconf/data/ietf-interfaces:interfaces" * @param[in] value_vec_len number of elements in the "value" array of an edit in YANG patch * @param[in] value_vec pointer to the "value" array of an edit in YANG patch * @param[in] x_simple_patch pointer to XML containing module name, e.g. * @retval 0 OK * @retval -1 Error */ static int yang_patch_do_create(clixon_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; // Send the POST request if ((cb = cbuf_new()) == NULL){ clixon_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 (clixon_json2cbuf(cb, x_simple_patch, 0, 0) < 0) goto done; if (api_data_post(h, req, cbuf_get(simple_patch_request_uri), pi, qvec, cbuf_get(cb), pretty, YANG_DATA_JSON, media_out, ds) < 0) goto done; retval = 0; done: if (cb) cbuf_free(cb); return retval; } /*! YANG PATCH insert method * * @param[in] h Clixon handle * @param[in] req Generic Www handle * @param[in] pi Offset, where to start api-path * @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 * @param[in] simple_patch_request_uri URI for patch request, e.g. "/restconf/data/ietf-interfaces:interfaces" * @param[in] value_vec_len number of elements in the "value" array of an edit in YANG patch * @param[in] value_vec pointer to the "value" array of an edit in YANG patch * @param[in] x_simple_patch pointer to XML containing module name, e.g. * @param[in] where_val value in "where" field of edit in YANG patch * @param[in] api_path full API path, e.g. "/restconf/data/example-jukebox:jukebox/playlist=Foo-One" * @param[in] point_val value in "point" field of edit in YANG patch * @retval 0 OK * @retval -1 Error */ static int yang_patch_do_insert(clixon_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 ) { 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){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if ((qvec_tmp = cvec_new(0)) == NULL){ clixon_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 if ((cv = cvec_add(qvec_tmp, CGV_STRING)) == NULL){ clixon_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){ clixon_err(OE_UNIX, errno, "cvec_add"); goto done; } cv_name_set(cv, "point"); cv_string_set(cv, cbuf_get(point_str)); // 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 * * @param[in] h Clixon handle * @param[in] req Generic Www handle * @param[in] pi Offset, where to start api-path * @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 * @param[in] simple_patch_request_uri URI for patch request, e.g. "/restconf/data/ietf-interfaces:interfaces" * @param[in] value_vec_len number of elements in the "value" array of an edit in YANG patch * @param[in] value_vec pointer to the "value" array of an edit in YANG patch * @param[in] x_simple_patch pointer to XML containing module name, e.g. "" * @param[in] where_val value in "where" field of edit in YANG patch * @param[in] key_xn XML with key tag and value, e.g. "Foo-One" * @retval 0 OK * @retval -1 Error */ static int yang_patch_do_merge(clixon_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, cxobj *key_xn ) { int retval = -1; cxobj *value_vec_tmp = NULL; cbuf *cb = NULL; cbuf *json_simple_patch = NULL; if ((cb = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if (key_xn != NULL) xml_addsub(x_simple_patch, key_xn); // 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_reset(cb); /* reuse cb */ if (clixon_json2cbuf(cb, x_simple_patch, 0, 0) < 0) goto done; if ((json_simple_patch = yang_patch_xml2json_modified_cbuf(x_simple_patch)) == NULL) goto done; // Send the simple patch request if (api_data_write(h, req, cbuf_get(simple_patch_request_uri), 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; } } 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(clixon_handle h, void *req, 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){ clixon_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, 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] pi Offset, where to start api-path * @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(clixon_handle h, void *req, 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; clixon_debug_xml(CLIXON_DBG_DEFAULT, xn, "%s %d xn:", __FUNCTION__, __LINE__); /* Create cbufs:s */ if ((simple_patch_request_uri = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if ((api_path_target = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if ((x = xpath_first(xn, NULL, "target")) == NULL){ clixon_err(OE_YANG, 0, "target mandatory element not found"); goto done; } target_val = xml_body(x); if ((x = xpath_first(xn, NULL, "operation")) == NULL){ clixon_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){ clixon_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){ clixon_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, 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 * * @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] pi Offset, where to start api-path * @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 * 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(clixon_handle h, void *req, char *api_path0, int pi, cvec *qvec, char *data, int pretty, restconf_media media_in, restconf_media media_out, ietf_ds_t ds) { int retval = -1; int i; cxobj *xpatch = NULL; yang_stmt *yspec; char *api_path; cxobj *xerr = NULL; /* malloced must be freed */ int ret; char *uripath0 = NULL; size_t veclen; cxobj **vec = NULL; clixon_debug(CLIXON_DBG_DEFAULT, "%s api_path:\"%s\"", __FUNCTION__, api_path0); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clixon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } api_path=api_path0; /* strip /... from start */ for (i=0; i