From aa14f8ac2c35e5a762be95b7e4fc735710dab3cf Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 6 Aug 2019 15:47:46 +0200 Subject: [PATCH] RESTCONF PUT/POST -d {} media is enforced --- CHANGELOG.md | 11 + apps/restconf/clixon_restconf.h | 11 +- apps/restconf/restconf_lib.c | 89 +++++--- apps/restconf/restconf_lib.h | 35 +++- apps/restconf/restconf_main.c | 153 ++++++++------ apps/restconf/restconf_methods.c | 60 +++--- apps/restconf/restconf_methods.h | 5 +- apps/restconf/restconf_methods_get.c | 89 ++++---- apps/restconf/restconf_methods_get.h | 7 +- apps/restconf/restconf_methods_patch.c | 110 +++++++++- apps/restconf/restconf_methods_patch.h | 3 +- apps/restconf/restconf_methods_post.c | 110 +++++----- apps/restconf/restconf_methods_post.h | 9 +- apps/restconf/restconf_stream.c | 18 +- doc/DEVELOP.md | 11 +- lib/src/clixon_yang.c | 2 +- test/jukebox.sh | 269 ++++++++++++++++++++++++ test/test_choice.sh | 6 +- test/test_identity.sh | 6 +- test/test_nacm.sh | 10 +- test/test_nacm_default.sh | 2 +- test/test_nacm_ext.sh | 8 +- test/test_nacm_module_read.sh | 10 +- test/test_nacm_module_write.sh | 11 +- test/test_nacm_protocol.sh | 8 +- test/test_restconf.sh | 51 ++--- test/test_restconf2.sh | 36 ++-- test/test_restconf_err.sh | 6 + test/test_restconf_jukebox.sh | 270 +------------------------ test/test_restconf_listkey.sh | 39 ++-- test/test_restconf_patch.sh | 69 +++++++ test/test_restconf_startup.sh | 6 +- test/test_rpc.sh | 31 ++- test/test_submodule.sh | 8 +- test/test_yang_namespace.sh | 4 +- 35 files changed, 933 insertions(+), 640 deletions(-) create mode 100755 test/jukebox.sh create mode 100755 test/test_restconf_patch.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 60efc564..85e2065b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. +* RESTCONF PUT/POST -d {} media is enforced + * Before accepted JSON as default, now Content-Type must be explicit, such as `Content-Type: application/yang-data+json` * RESTCONF identities has been changed to use module names instead of prefixes. * Eg, `curl -X POST -d '{"type":"ex:eth"}` --> `curl -X POST -d '{"type":"ietf-interfaces:eth"`} * JSON changes @@ -45,6 +47,15 @@ * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs +* Corrected CLI bug with lists of multiple keys (netconf/restconf works). + * Worked in 3.10, but broke in 4.0 + * Example: `yang list x { key "a b";...}` + CLI error example: + ``` + set x a 1 b 1; #OK + set x a 1 b 2; #OK + set x a 1 b # Error + ``` * Fixed RESTCONF api-path leaf-list selection was not made properly * Requesting eg `mod:x/y=42` returned the whole list: `{"y":[41,42,43]}` whereas it should only return one element: `{"y":42}` * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) diff --git a/apps/restconf/clixon_restconf.h b/apps/restconf/clixon_restconf.h index 38385d40..1499a4f3 100644 --- a/apps/restconf/clixon_restconf.h +++ b/apps/restconf/clixon_restconf.h @@ -38,6 +38,15 @@ #ifndef _CLIXON_RESTCONF_H_ #define _CLIXON_RESTCONF_H_ +/* + * Types (also in restconf_lib.h) + */ +enum restconf_media{ + YANG_DATA_JSON, /* "application/yang-data+json" (default for RESTCONF) */ + YANG_DATA_XML /* "application/yang-data+xml" */ +}; +typedef enum restconf_media restconf_media; + /* * Prototypes (also in restconf_lib.h) */ @@ -55,7 +64,7 @@ int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, - int pretty, int use_xml, int code); + int pretty, restconf_media media, int code); #endif /* _CLIXON_RESTCONF_H_ */ diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 2e8966fb..c340b724 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -139,6 +139,14 @@ static const map_str2int http_reason_phrase_map[] = { {NULL, -1} }; +/* See RFC 8040 + */ +static const map_str2int http_media_map[] = { + {"application/yang-data+xml", YANG_DATA_XML}, + {"application/yang-data+json", YANG_DATA_JSON}, + {NULL, -1} +}; + int restconf_err2code(char *tag) { @@ -151,11 +159,23 @@ restconf_code2reason(int code) return clicon_int2str(http_reason_phrase_map, code); } +const restconf_media +restconf_media_str2int(char *media) +{ + return clicon_str2int(http_media_map, media); +} + +const char * +restconf_media_int2str(restconf_media media) +{ + return clicon_int2str(http_media_map, media); +} + /*! HTTP error 400 * @param[in] r Fastcgi request handle */ int -badrequest(FCGX_Request *r) +restconf_badrequest(FCGX_Request *r) { char *path; @@ -173,7 +193,7 @@ badrequest(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -unauthorized(FCGX_Request *r) +restconf_unauthorized(FCGX_Request *r) { char *path; @@ -190,7 +210,7 @@ unauthorized(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -forbidden(FCGX_Request *r) +restconf_forbidden(FCGX_Request *r) { char *path; @@ -207,7 +227,7 @@ forbidden(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -notfound(FCGX_Request *r) +restconf_notfound(FCGX_Request *r) { char *path; @@ -227,7 +247,7 @@ notfound(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -notacceptable(FCGX_Request *r) +restconf_notacceptable(FCGX_Request *r) { char *path; @@ -247,7 +267,7 @@ notacceptable(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -conflict(FCGX_Request *r) +restconf_conflict(FCGX_Request *r) { clicon_debug(1, "%s", __FUNCTION__); FCGX_FPrintF(r->out, "Status: 409\r\n"); /* 409 Conflict */ @@ -256,11 +276,25 @@ conflict(FCGX_Request *r) return 0; } +/*! HTTP error 409 + * @param[in] r Fastcgi request handle + */ +int +restconf_unsupported_media(FCGX_Request *r) +{ + clicon_debug(1, "%s", __FUNCTION__); + FCGX_SetExitStatus(415, r->out); + FCGX_FPrintF(r->out, "Status: 415 Unsupported Media Type\r\n"); + FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(r->out, "

Unsupported Media Type

\n"); + return 0; +} + /*! HTTP error 500 * @param[in] r Fastcgi request handle */ int -internal_server_error(FCGX_Request *r) +restconf_internal_server_error(FCGX_Request *r) { char *path; @@ -276,7 +310,7 @@ internal_server_error(FCGX_Request *r) * @param[in] r Fastcgi request handle */ int -notimplemented(FCGX_Request *r) +restconf_notimplemented(FCGX_Request *r) { clicon_debug(1, "%s", __FUNCTION__); FCGX_FPrintF(r->out, "Status: 501\r\n"); @@ -285,7 +319,6 @@ notimplemented(FCGX_Request *r) return 0; } - /*! * @param[in] r Fastcgi request handle */ @@ -396,16 +429,16 @@ get_user_cookie(char *cookiestr, * @param[in] r Fastcgi request handle * @param[in] xerr XML error message from backend * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @param[in] code If 0 use rfc8040 sec 7 netconf2restconf error-tag mapping - * otherwise use this code + * @param[in] media Output media + * @param[in] code If 0 use rfc8040 sec 7 netconf2restconf error-tag mapping + * otherwise use this code */ int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int pretty, - int use_xml, + restconf_media media, int code0) { int retval = -1; @@ -419,7 +452,7 @@ api_return_err(clicon_handle h, if ((cb = cbuf_new()) == NULL) goto done; if ((xtag = xpath_first(xerr, "//error-tag")) == NULL){ - notfound(r); + restconf_notfound(r); goto ok; } tagstr = xml_body(xtag); @@ -433,20 +466,14 @@ api_return_err(clicon_handle h, reason_phrase=""; if (xml_name_set(xerr, "error") < 0) goto done; - if (use_xml){ - if (clicon_xml2cbuf(cb, xerr, 2, pretty) < 0) - goto done; - } - else - if (xml2json_cbuf(cb, xerr, pretty) < 0) - goto done; - clicon_debug(1, "%s code:%d err:%s", __FUNCTION__, code, cbuf_get(cb)); FCGX_SetExitStatus(code, r->out); /* Created */ FCGX_FPrintF(r->out, "Status: %d %s\r\n", code, reason_phrase); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n\r\n", - use_xml?"xml":"json"); - - if (use_xml){ + FCGX_FPrintF(r->out, "Content-Type: %s\r\n\r\n", restconf_media_int2str(media)); + switch (media){ + case YANG_DATA_XML: + if (clicon_xml2cbuf(cb, xerr, 2, pretty) < 0) + goto done; + clicon_debug(1, "%s code:%d err:%s", __FUNCTION__, code, cbuf_get(cb)); if (pretty){ FCGX_FPrintF(r->out, " \n", cbuf_get(cb)); FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); @@ -457,8 +484,11 @@ api_return_err(clicon_handle h, FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); FCGX_FPrintF(r->out, "\r\n"); } - } - else{ + break; + case YANG_DATA_JSON: + if (xml2json_cbuf(cb, xerr, pretty) < 0) + goto done; + clicon_debug(1, "%s code:%d err:%s", __FUNCTION__, code, cbuf_get(cb)); if (pretty){ FCGX_FPrintF(r->out, "{\n"); FCGX_FPrintF(r->out, " \"ietf-restconf:errors\" : %s\n", @@ -471,7 +501,7 @@ api_return_err(clicon_handle h, FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); FCGX_FPrintF(r->out, "}\r\n"); } - } + } /* switch media */ ok: retval = 0; done: @@ -683,3 +713,4 @@ restconf_uripath(FCGX_Request *r) *q = '\0'; return path; } + diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 6fe968e1..436b5004 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -41,26 +41,43 @@ */ #define RESTCONF_API "restconf" +/* + * Types + */ + +/*! RESTCONF media types + * @see http_media_map + */ +enum restconf_media{ + YANG_DATA_JSON, /* "application/yang-data+json" (default for RESTCONF) */ + YANG_DATA_XML /* "application/yang-data+xml" */ +}; +typedef enum restconf_media restconf_media; + /* * Prototypes (also in clixon_restconf.h) */ int restconf_err2code(char *tag); const char *restconf_code2reason(int code); -int badrequest(FCGX_Request *r); -int unauthorized(FCGX_Request *r); -int forbidden(FCGX_Request *r); -int notfound(FCGX_Request *r); -int notacceptable(FCGX_Request *r); -int conflict(FCGX_Request *r); -int internal_server_error(FCGX_Request *r); -int notimplemented(FCGX_Request *r); +const restconf_media restconf_media_str2int(char *media); +const char *restconf_media_int2str(restconf_media media); + +int restconf_badrequest(FCGX_Request *r); +int restconf_unauthorized(FCGX_Request *r); +int restconf_forbidden(FCGX_Request *r); +int restconf_notfound(FCGX_Request *r); +int restconf_notacceptable(FCGX_Request *r); +int restconf_conflict(FCGX_Request *r); +int restconf_unsupported_media(FCGX_Request *r); +int restconf_internal_server_error(FCGX_Request *r); +int restconf_notimplemented(FCGX_Request *r); int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, - int pretty, int use_xml, int code); + int pretty, enum restconf_media media, int code); int http_location(FCGX_Request *r, cxobj *xobj); int restconf_terminate(clicon_handle h); int restconf_insert_attributes(cxobj *xdata, cvec *qvec); diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 6f495f89..8fb809cd 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -101,8 +101,8 @@ * @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] dvec Stream input daat * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * @param[in] media_in Input media + * @param[in] media_out Output media */ static int api_data(clicon_handle h, @@ -113,8 +113,8 @@ api_data(clicon_handle h, cvec *qvec, char *data, int pretty, - int use_xml, - int parse_xml) + restconf_media media_in, + restconf_media media_out) { int retval = -1; char *request_method; @@ -125,19 +125,19 @@ api_data(clicon_handle h, if (strcmp(request_method, "OPTIONS")==0) retval = api_data_options(h, r); else if (strcmp(request_method, "HEAD")==0) - retval = api_data_head(h, r, pcvec, pi, qvec, pretty, use_xml); + retval = api_data_head(h, r, pcvec, pi, qvec, pretty, media_out); else if (strcmp(request_method, "GET")==0) - retval = api_data_get(h, r, pcvec, pi, qvec, pretty, use_xml); + retval = api_data_get(h, r, pcvec, pi, qvec, pretty, media_out); else if (strcmp(request_method, "POST")==0) - retval = api_data_post(h, r, api_path, pcvec, pi, qvec, data, pretty, use_xml, parse_xml); + retval = api_data_post(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); else if (strcmp(request_method, "PUT")==0) - retval = api_data_put(h, r, api_path, pcvec, pi, qvec, data, pretty, use_xml, parse_xml); + retval = api_data_put(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); else if (strcmp(request_method, "PATCH")==0) - retval = api_data_patch(h, r, api_path, pcvec, pi, qvec, data); + retval = api_data_patch(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); else if (strcmp(request_method, "DELETE")==0) - retval = api_data_delete(h, r, api_path, pi, pretty, use_xml); + retval = api_data_delete(h, r, api_path, pi, pretty, media_out); else - retval = notfound(r); + retval = restconf_notfound(r); clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); return retval; } @@ -150,7 +150,8 @@ api_data(clicon_handle h, * @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] parse_xml Set to 0 for JSON and 1 for XML for input data + * @param[in] media_in Input media media + * @param[in] media_out Output media */ static int api_operations(clicon_handle h, @@ -161,8 +162,8 @@ api_operations(clicon_handle h, cvec *qvec, char *data, int pretty, - int use_xml, - int parse_xml) + restconf_media media_in, + restconf_media media_out) { int retval = -1; char *request_method; @@ -171,12 +172,12 @@ api_operations(clicon_handle h, request_method = FCGX_GetParam("REQUEST_METHOD", r->envp); clicon_debug(1, "%s method:%s", __FUNCTION__, request_method); if (strcmp(request_method, "GET")==0) - retval = api_operations_get(h, r, path, pcvec, pi, qvec, data, pretty, use_xml); + retval = api_operations_get(h, r, path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "POST")==0) retval = api_operations_post(h, r, path, pcvec, pi, qvec, data, - pretty, use_xml, parse_xml); + pretty, media_in, media_out); else - retval = notfound(r); + retval = restconf_notfound(r); return retval; } @@ -211,15 +212,15 @@ api_well_known(clicon_handle h, * See RFC8040 3.3 */ static int -api_root(clicon_handle h, - FCGX_Request *r) +api_root(clicon_handle h, + FCGX_Request *r, + int pretty, + restconf_media media_out) + { int retval = -1; - char *media_accept; - int use_xml = 0; /* By default use JSON */ cxobj *xt = NULL; cbuf *cb = NULL; - int pretty; yang_stmt *yspec; clicon_debug(1, "%s", __FUNCTION__); @@ -227,14 +228,10 @@ api_root(clicon_handle h, clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } - pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp); - if (strcmp(media_accept, "application/yang-data+xml")==0) - use_xml++; - clicon_debug(1, "%s use-xml:%d media-accept:%s", __FUNCTION__, use_xml, media_accept); FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); if (xml_parse_string("2016-06-21", NULL, &xt) < 0) @@ -247,13 +244,16 @@ api_root(clicon_handle h, } if (xml_rootchild(xt, 0, &xt) < 0) goto done; - if (use_xml){ + switch (media_out){ + case YANG_DATA_XML: if (clicon_xml2cbuf(cb, xt, 0, pretty) < 0) goto done; - } - else + break; + case YANG_DATA_JSON: if (xml2json_cbuf(cb, xt, pretty) < 0) goto done; + break; + } FCGX_FPrintF(r->out, "%s", cb?cbuf_get(cb):""); FCGX_FPrintF(r->out, "\r\n\r\n"); retval = 0; @@ -270,24 +270,20 @@ api_root(clicon_handle h, */ static int api_yang_library_version(clicon_handle h, - FCGX_Request *r) + FCGX_Request *r, + int pretty, + restconf_media media_out) + { int retval = -1; - char *media_accept; - int use_xml = 0; /* By default use JSON */ cxobj *xt = NULL; cbuf *cb = NULL; - int pretty; char *ietf_yang_library_revision = "2016-06-21"; /* XXX */ clicon_debug(1, "%s", __FUNCTION__); - pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp); - if (strcmp(media_accept, "application/yang-data+xml")==0) - use_xml++; FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); if (xml_parse_va(&xt, NULL, "%s", ietf_yang_library_revision) < 0) goto done; @@ -296,13 +292,15 @@ api_yang_library_version(clicon_handle h, if ((cb = cbuf_new()) == NULL){ goto done; } - if (use_xml){ + switch (media_out){ + case YANG_DATA_XML: if (clicon_xml2cbuf(cb, xt, 0, pretty) < 0) goto done; - } - else{ + break; + case YANG_DATA_JSON: if (xml2json_cbuf(cb, xt, pretty) < 0) goto done; + break; } clicon_debug(1, "%s cb%s", __FUNCTION__, cbuf_get(cb)); FCGX_FPrintF(r->out, "%s\n", cb?cbuf_get(cb):""); @@ -335,11 +333,10 @@ api_restconf(clicon_handle h, cbuf *cb = NULL; char *data; int authenticated = 0; - char *media_accept; - char *media_content_type; + char *media_str = NULL; + restconf_media media_in = YANG_DATA_JSON; /* XXX defaults should be set in methods, here is too generic */ + restconf_media media_out = YANG_DATA_JSON; int pretty; - int parse_xml = 0; /* By default expect and parse JSON */ - int use_xml = 0; /* By default use JSON */ cbuf *cbret = NULL; cxobj *xret = NULL; cxobj *xerr; @@ -348,37 +345,61 @@ api_restconf(clicon_handle h, path = restconf_uripath(r); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - /* get xml/json in put and output */ - media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp); - if (media_accept && strcmp(media_accept, "application/yang-data+xml")==0) - use_xml++; - media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp); - if (media_content_type && - strcmp(media_content_type, "application/yang-data+xml")==0) - parse_xml++; + /* Get media for input (Content-Type) + * This is for methods that have input, such as PUT/POST, etc + */ + if ((media_str = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp)) == NULL){ + retval = restconf_unsupported_media(r); + goto done; + } + else if ((media_in = restconf_media_str2int(media_str)) == -1){ + clicon_debug(1, "%s Content_Type: %s (unsupported)", __FUNCTION__, media_str); + retval = restconf_unsupported_media(r); + goto done; + } + clicon_debug(1, "%s CONTENT_TYPE: %s %s", __FUNCTION__, media_str, restconf_media_int2str(media_in)); + /* Get media for output (proactive negotiation) RFC7231 by using + * Accept:. This is for methods that have output, such as GET, + * operation POST, etc + * If accept is * default is yang-json + */ + if ((media_str = FCGX_GetParam("HTTP_ACCEPT", r->envp)) == NULL){ + // retval = restconf_unsupported_media(r); + // goto done; + } + else if ((media_out = restconf_media_str2int(media_str)) == -1){ + if (strcmp(media_str, "*/*") == 0) /* catch-all */ + media_out = YANG_DATA_JSON; + else{ + retval = restconf_unsupported_media(r); + goto done; + } + } + clicon_debug(1, "%s ACCEPT: %s %s", __FUNCTION__, media_str, restconf_media_int2str(media_out)); + if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) goto done; /* Sanity check of path. Should be /restconf/ */ if (pn < 2){ - notfound(r); + restconf_notfound(r); goto ok; } if (strlen(pvec[0]) != 0){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } if (strcmp(pvec[1], RESTCONF_API)){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } restconf_test(r, 1); if (pn == 2){ - retval = api_root(h, r); + retval = api_root(h, r, pretty, media_out); goto done; } if ((method = pvec[2]) == NULL){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } clicon_debug(1, "%s: method=%s", __FUNCTION__, method); @@ -410,7 +431,7 @@ api_restconf(clicon_handle h, if (netconf_access_denied_xml(&xret, "protocol", "The requested URL was unauthorized") < 0) goto done; if ((xerr = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xerr, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -418,23 +439,23 @@ api_restconf(clicon_handle h, } clicon_debug(1, "%s auth2:%d %s", __FUNCTION__, authenticated, clicon_username_get(h)); if (strcmp(method, "yang-library-version")==0){ - if (api_yang_library_version(h, r) < 0) + if (api_yang_library_version(h, r, pretty, media_out) < 0) goto done; } else if (strcmp(method, "data") == 0){ /* restconf, skip /api/data */ if (api_data(h, r, path, pcvec, 2, qvec, data, - pretty, use_xml, parse_xml) < 0) + pretty, media_in, media_out) < 0) goto done; } else if (strcmp(method, "operations") == 0){ /* rpc */ if (api_operations(h, r, path, pcvec, 2, qvec, data, - pretty, use_xml, parse_xml) < 0) + pretty, media_in, media_out) < 0) goto done; } else if (strcmp(method, "test") == 0) restconf_test(r, 0); else - notfound(r); + restconf_notfound(r); ok: retval = 0; done: @@ -796,7 +817,7 @@ main(int argc, } else{ clicon_debug(1, "top-level %s not found", path); - notfound(r); + restconf_notfound(r); } } else diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index e795d58a..299e3341 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -224,8 +224,8 @@ match_list_keys(yang_stmt *y, * @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] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * @param[in] media_in Input media media + * @param[in] media_out Output media * @note restconf PUT is mapped to edit-config replace. * @see RFC8040 Sec 4.5 PUT @@ -260,8 +260,8 @@ api_data_put(clicon_handle h, cvec *qvec, char *data, int pretty, - int use_xml, - int parse_xml) + restconf_media media_in, + restconf_media media_out) { int retval = -1; enum operation_type op; @@ -302,13 +302,13 @@ api_data_put(clicon_handle h, /* Create config top-of-tree */ if ((xtop = xml_new("config", NULL, NULL)) == NULL) goto done; - /* Translate api_path to xtop/xbot */ + /* Translate api_path to xml in the form of xtop/xbot */ xbot = xtop; - if (api_path){ + if (api_path){ /* If URI, otherwise top data/config object */ if ((ret = api_path2xml(api_path, yspec, xtop, YC_DATANODE, 1, &xbot, &ybot)) < 0) goto done; if (ybot) - ymodapi=ys_module(ybot); + ymodapi = ys_module(ybot); if (ret == 0){ /* validation failed */ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; @@ -317,14 +317,15 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } } /* Parse input data as json or xml into xml */ - if (parse_xml){ + switch (media_in){ + case YANG_DATA_XML: if (xml_parse_string(data, yspec, &xdata0) < 0){ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; @@ -332,12 +333,12 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } - } - else{ + break; + case YANG_DATA_JSON: /* Data here cannot cannot be Yang populated since it is loosely * hanging without top symbols. * And if it is not yang populated, it cant be translated properly @@ -351,7 +352,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -360,11 +361,11 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } - } + } /* switch media_in */ /* The message-body MUST contain exactly one instance of the * expected data resource. @@ -376,7 +377,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -404,7 +405,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -445,7 +446,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -470,7 +471,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -494,7 +495,7 @@ api_data_put(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -509,7 +510,7 @@ api_data_put(clicon_handle h, /* xbot is already populated, resolve yang for added xdata too */ if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0) goto done; - if (!parse_xml && nullspec){ + if (media_in == YANG_DATA_JSON && nullspec){ /* json2xml decode could not be done above in json_parse, * need to be done here instead * UNLESS it is root resource, then json-parse has already done it @@ -517,7 +518,7 @@ api_data_put(clicon_handle h, if ((ret = json2xml_decode(xdata, &xerr)) < 0) goto done; if (ret == 0){ - if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xerr, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -561,7 +562,7 @@ api_data_put(clicon_handle h, */ if (xpath_first(xe, ".[error-tag=\"data-exists\"]") == NULL || op == OP_REPLACE){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -598,7 +599,7 @@ api_data_put(clicon_handle h, /* log errors from discard, but ignore */ if ((xpath_first(xretdis, "//rpc-error")) != NULL) clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -656,14 +657,13 @@ api_data_put(clicon_handle h, return retval; } /* api_data_put */ - /*! Generic REST DELETE method translated to edit-config * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) * @param[in] pi Offset, where path starts * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML + * @param[in] media_out Output media * See RFC 8040 Sec 4.7 * Example: * curl -X DELETE http://127.0.0.1/restconf/data/interfaces/interface=eth0 @@ -675,7 +675,7 @@ api_data_delete(clicon_handle h, char *api_path, int pi, int pretty, - int use_xml) + restconf_media media_out) { int retval = -1; int i; @@ -716,7 +716,7 @@ api_data_delete(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -744,7 +744,7 @@ api_data_delete(clicon_handle h, if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -766,7 +766,7 @@ api_data_delete(clicon_handle h, /* log errors from discard, but ignore */ if ((xpath_first(xretdis, "//rpc-error")) != NULL) clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 55aa17f0..a1859fb1 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -45,8 +45,9 @@ int api_data_options(clicon_handle h, FCGX_Request *r); int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); + int pretty, + restconf_media media_in, restconf_media media_out); int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi, - int pretty, int use_xml); + int pretty, restconf_media media_out); #endif /* _RESTCONF_METHODS_H_ */ diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 60038c1a..ecd2cd1a 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -70,7 +70,7 @@ * @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] use_xml Set to 0 for JSON and 1 for XML + * @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 @@ -96,7 +96,7 @@ api_data_get2(clicon_handle h, int pi, cvec *qvec, int pretty, - int use_xml, + restconf_media media_out, int head) { int retval = -1; @@ -131,7 +131,7 @@ api_data_get2(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -148,7 +148,7 @@ api_data_get2(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -167,7 +167,7 @@ api_data_get2(clicon_handle h, #endif /* Check if error return */ if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -176,28 +176,20 @@ api_data_get2(clicon_handle h, goto done; if (head){ FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); goto ok; } if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ - if (use_xml){ + switch (media_out){ + case YANG_DATA_XML: if (clicon_xml2cbuf(cbx, xret, 0, pretty) < 0) /* Dont print top object? */ goto done; - } - else{ -#if 0 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) - goto done; - clicon_debug(1, "%s xret: %s", - __FUNCTION__, cbuf_get(ccc)); - cbuf_free(ccc); - } -#endif - if (xml2json_cbuf(cbx, xret, pretty) < 0) - goto done; + break; + case YANG_DATA_JSON: + if (xml2json_cbuf(cbx, xret, pretty) < 0) + goto done; + break; } } else{ @@ -208,7 +200,7 @@ api_data_get2(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -221,11 +213,12 @@ api_data_get2(clicon_handle h, if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0) goto done; /* override invalid-value default 400 with 404 */ - if (api_return_err(h, r, xerr, pretty, use_xml, 404) < 0) + if (api_return_err(h, r, xerr, pretty, media_out, 404) < 0) goto done; goto ok; } - if (use_xml){ + switch (media_out){ + case YANG_DATA_XML: for (i=0; i0 * Out: {"example:x": {"0"}} */ if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) goto done; + break; } } clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); FCGX_FPrintF(r->out, "\r\n\r\n"); @@ -282,7 +276,7 @@ api_data_get2(clicon_handle h, * @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] use_xml Set to 0 for JSON and 1 for XML + * @param[in] media_out Output media * * 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 @@ -296,9 +290,9 @@ api_data_head(clicon_handle h, int pi, cvec *qvec, int pretty, - int use_xml) + restconf_media media_out) { - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 1); + return api_data_get2(h, r, pcvec, pi, qvec, pretty, media_out, 1); } /*! REST GET method @@ -309,7 +303,7 @@ api_data_head(clicon_handle h, * @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] use_xml Set to 0 for JSON and 1 for XML + * @param[in] media_out Output media * @code * curl -G http://localhost/restconf/data/interfaces/interface=eth0 * @endcode @@ -333,9 +327,9 @@ api_data_get(clicon_handle h, int pi, cvec *qvec, int pretty, - int use_xml) + restconf_media media_out) { - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 0); + return api_data_get2(h, r, pcvec, pi, qvec, pretty, media_out, 0); } /*! GET restconf/operations resource @@ -347,7 +341,7 @@ api_data_get(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] use_xml Set to 0 for JSON and 1 for XML + * @param[in] media_out Output media * * @code * curl -G http://localhost/restconf/operations @@ -372,7 +366,7 @@ api_operations_get(clicon_handle h, cvec *qvec, char *data, int pretty, - int use_xml) + restconf_media media_out) { int retval = -1; yang_stmt *yspec; @@ -387,10 +381,14 @@ api_operations_get(clicon_handle h, yspec = clicon_dbspec_yang(h); if ((cbx = cbuf_new()) == NULL) goto done; - if (use_xml) + switch (media_out){ + case YANG_DATA_XML: cprintf(cbx, ""); - else + break; + case YANG_DATA_JSON: cprintf(cbx, "{\"operations\": {"); + break; + } ymod = NULL; i = 0; while ((ymod = yn_each(yspec, ymod)) != NULL) { @@ -399,21 +397,28 @@ api_operations_get(clicon_handle h, while ((yc = yn_each(ymod, yc)) != NULL) { if (yang_keyword_get(yc) != Y_RPC) continue; - if (use_xml) + switch (media_out){ + case YANG_DATA_XML: cprintf(cbx, "<%s xmlns=\"%s\"/>", yang_argument_get(yc), namespace); - else{ + break; + case YANG_DATA_JSON: if (i++) cprintf(cbx, ","); cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); + break; } } } - if (use_xml) + switch (media_out){ + case YANG_DATA_XML: cprintf(cbx, ""); - else + break; + case YANG_DATA_JSON: cprintf(cbx, "}}"); + break; + } FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); FCGX_FPrintF(r->out, "\r\n\r\n"); diff --git a/apps/restconf/restconf_methods_get.h b/apps/restconf/restconf_methods_get.h index 5354d581..f2fddf6f 100644 --- a/apps/restconf/restconf_methods_get.h +++ b/apps/restconf/restconf_methods_get.h @@ -34,7 +34,6 @@ * Restconf method implementation for operations get and data get and head */ - #ifndef _RESTCONF_METHODS_GET_H_ #define _RESTCONF_METHODS_GET_H_ @@ -42,12 +41,12 @@ * Prototypes */ int api_data_head(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); + cvec *qvec, int pretty, restconf_media media_out); int api_data_get(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); + cvec *qvec, int pretty, restconf_media media_out); int api_operations_get(clicon_handle h, FCGX_Request *r, char *path, cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml); + int pretty, restconf_media media_out); #endif /* _RESTCONF_METHODS_GET_H_ */ diff --git a/apps/restconf/restconf_methods_patch.c b/apps/restconf/restconf_methods_patch.c index 39f72028..48338198 100644 --- a/apps/restconf/restconf_methods_patch.c +++ b/apps/restconf/restconf_methods_patch.c @@ -125,7 +125,7 @@ Mapping netconf error-tag -> status code #include "restconf_methods_patch.h" -/*! Generic REST PATCH method +/*! Generic REST PATCH method for plain patch * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) @@ -133,19 +133,117 @@ Mapping netconf error-tag -> status code * @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 RFC8040 Sec 4.6 + * See RFC8040 Sec 4.6.1 */ int api_data_patch(clicon_handle h, FCGX_Request *r, - char *api_path, + char *api_path0, cvec *pcvec, int pi, cvec *qvec, - char *data) + char *data, + int pretty, + restconf_media media_in, + restconf_media media_out) { - notimplemented(r); - return 0; + int retval = -1; + yang_stmt *yspec; + // yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ + cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ + int i; + char *api_path; + cxobj *xtop = NULL; /* top of api-path */ + cxobj *xbot = NULL; /* bottom of api-path */ + yang_stmt *ybot = NULL; /* yang of xbot */ + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; + int ret; + + clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path0); + clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + api_path=api_path0; + for (i=0; i... @@ -504,7 +507,7 @@ api_operations_post_input(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } @@ -539,7 +542,7 @@ api_operations_post_input(clicon_handle h, * @param[in] yspec Yang top-level specification * @param[in] youtput Yang rpc output specification * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] media_out Output media * @param[out] xoutputp Restconf JSON/XML output * @retval 1 OK * @retval 0 Fail, Error message sent @@ -554,7 +557,7 @@ api_operations_post_output(clicon_handle h, yang_stmt *youtput, char *namespace, int pretty, - int use_xml, + restconf_media media_out, cxobj **xoutputp) { @@ -578,7 +581,7 @@ api_operations_post_output(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } @@ -616,7 +619,7 @@ api_operations_post_output(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } @@ -673,8 +676,8 @@ api_operations_post_output(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] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * @param[in] media_in Input media media + * @param[in] media_out Output media * See RFC 8040 Sec 3.6 / 4.4.2 * @note We map post to edit-config create. * POST {+restconf}/operations/ @@ -705,8 +708,8 @@ api_operations_post(clicon_handle h, cvec *qvec, char *data, int pretty, - int use_xml, - int parse_xml) + restconf_media media_in, + restconf_media media_out) { int retval = -1; int i; @@ -749,7 +752,7 @@ api_operations_post(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -768,7 +771,7 @@ api_operations_post(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -779,7 +782,7 @@ api_operations_post(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -808,7 +811,7 @@ api_operations_post(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -822,7 +825,7 @@ api_operations_post(clicon_handle h, clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); if (data && strlen(data)){ if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, - pretty, use_xml, parse_xml)) < 0) + pretty, media_in, media_out)) < 0) goto done; if (ret == 0) goto ok; @@ -849,7 +852,7 @@ api_operations_post(clicon_handle h, clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto ok; } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -879,7 +882,7 @@ api_operations_post(clicon_handle h, goto done; /* Local error: return it and quit */ if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -888,7 +891,7 @@ api_operations_post(clicon_handle h, if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -907,24 +910,27 @@ api_operations_post(clicon_handle h, #endif youtput = yang_find(yrpc, Y_OUTPUT, NULL); if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, - pretty, use_xml, &xoutput)) < 0) + pretty, media_out, &xoutput)) < 0) goto done; if (ret == 0) goto ok; /* xoutput should now look: 0 */ FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + + FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); cbuf_reset(cbret); - if (use_xml){ + switch (media_out){ + case YANG_DATA_XML: if (clicon_xml2cbuf(cbret, xoutput, 0, pretty) < 0) goto done; /* xoutput should now look: 0 */ - } - else{ + break; + case YANG_DATA_JSON: if (xml2json_cbuf(cbret, xoutput, pretty) < 0) goto done; /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ + break; } FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); FCGX_FPrintF(r->out, "\r\n\r\n"); diff --git a/apps/restconf/restconf_methods_post.h b/apps/restconf/restconf_methods_post.h index cfb5e743..b782df1a 100644 --- a/apps/restconf/restconf_methods_post.h +++ b/apps/restconf/restconf_methods_post.h @@ -44,11 +44,16 @@ int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); + int pretty, + restconf_media media_in, + restconf_media media_out); int api_operations_post(clicon_handle h, FCGX_Request *r, char *path, cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); + int pretty, + restconf_media media_in, + restconf_media media_out); + #endif /* _RESTCONF_METHODS_POST_H_ */ diff --git a/apps/restconf/restconf_stream.c b/apps/restconf/restconf_stream.c index 92d0b9c2..a53db543 100644 --- a/apps/restconf/restconf_stream.c +++ b/apps/restconf/restconf_stream.c @@ -231,7 +231,7 @@ restconf_stream(clicon_handle h, char *name, cvec *qvec, int pretty, - int use_xml, + restconf_media media_out, int *sp) { int retval = -1; @@ -269,7 +269,7 @@ restconf_stream(clicon_handle h, if (clicon_rpc_netconf(h, cbuf_get(cb), &xret, &s) < 0) goto done; if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -355,7 +355,7 @@ api_stream(clicon_handle h, char *data; int authenticated = 0; int pretty; - int use_xml = 1; /* default */ + restconf_media media_out = YANG_DATA_XML; /* XXX default */ cbuf *cbret = NULL; cxobj *xret = NULL; cxobj *xerr; @@ -374,20 +374,20 @@ api_stream(clicon_handle h, goto done; /* Sanity check of path. Should be /stream/ */ if (pn != 3){ - notfound(r); + restconf_notfound(r); goto ok; } if (strlen(pvec[0]) != 0){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } if (strcmp(pvec[1], streampath)){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } if ((method = pvec[2]) == NULL){ - retval = notfound(r); + retval = restconf_notfound(r); goto done; } clicon_debug(1, "%s: method=%s", __FUNCTION__, method); @@ -418,14 +418,14 @@ api_stream(clicon_handle h, if (netconf_access_denied_xml(&xret, "protocol", "The requested URL was unauthorized") < 0) goto done; if ((xerr = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0) + if (api_return_err(h, r, xerr, pretty, media_out, 0) < 0) goto done; goto ok; } goto ok; } clicon_debug(1, "%s auth2:%d %s", __FUNCTION__, authenticated, clicon_username_get(h)); - if (restconf_stream(h, r, method, qvec, pretty, use_xml, &s) < 0) + if (restconf_stream(h, r, method, qvec, pretty, media_out, &s) < 0) goto done; if (s != -1){ #ifdef STREAM_FORK diff --git a/doc/DEVELOP.md b/doc/DEVELOP.md index 7e17d086..5653251b 100644 --- a/doc/DEVELOP.md +++ b/doc/DEVELOP.md @@ -1,7 +1,7 @@ # README for Clixon developers * [Code documentation](#documentation) - * [How to work in git (branching)](#branching) + * [How to work in git (how-to-work-in-git)](#how-to-work-in-git) * [How the meta-configure stuff works](#meta-configure) * [How to debug](#debug) * [New release](#new-release) @@ -28,12 +28,15 @@ How to document the code */ ``` -## Branching -How to work in git (branching) +## How to work in git + +Clixon uses semantic versioning (https://semver.org). Try to keep a single master branch always working. Currently testing is made using [Travis CI](https://travis-ci.org/clicon/clixon). -However, releases are made periodically (ca every 3 months) which is more tested. +However, releases are made periodically (ca every 1 month) which is more tested. + +A release branch can be made, eg release-4.0 where 4.0.0, 4.0.1 are tagged ## How the meta-configure stuff works ``` diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 30c11d57..b3c6e89e 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -321,7 +321,7 @@ ys_prune(yang_stmt *yp, memmove(&yp->ys_stmt[i], &yp->ys_stmt[i+1], size); - yp->ys_stmt[yp->ys_len--] = NULL;; + yp->ys_stmt[yp->ys_len--] = NULL; done: return yc; } diff --git a/test/jukebox.sh b/test/jukebox.sh new file mode 100755 index 00000000..9d796a1e --- /dev/null +++ b/test/jukebox.sh @@ -0,0 +1,269 @@ +#!/bin/bash +# Jukebox example from rfc 8040 Appendix A.1 +# ASsumes fjukebox is set to name of yang file + +cat < $fjukebox + module example-jukebox { + + namespace "http://example.com/ns/example-jukebox"; + prefix "jbox"; + + organization "Example, Inc."; + contact "support at example.com"; + description "Example Jukebox Data Model Module."; + revision "2016-08-15" { + description "Initial version."; + reference "example.com document 1-4673."; + } + + identity genre { + description + "Base for all genre types."; + } + + // abbreviated list of genre classifications + identity alternative { + base genre; + description + "Alternative music."; + } + identity blues { + base genre; + description + "Blues music."; + } + identity country { + base genre; + description + "Country music."; + } + identity jazz { + base genre; + description + "Jazz music."; + } + identity pop { + base genre; + description + "Pop music."; + } + identity rock { + base genre; + description + "Rock music."; + } + + container jukebox { + presence + "An empty container indicates that the jukebox + service is available."; + + description + "Represents a 'jukebox' resource, with a library, playlists, + and a 'play' operation."; + + container library { + + description + "Represents the 'jukebox' library resource."; + + list artist { + key name; + description + "Represents one 'artist' resource within the + 'jukebox' library resource."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the artist."; + } + + list album { + key name; + description + "Represents one 'album' resource within one + 'artist' resource, within the jukebox library."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the album."; + } + + leaf genre { + type identityref { base genre; } + description + "The genre identifying the type of music on + the album."; + } + + leaf year { + type uint16 { + range "1900 .. max"; + } + description + "The year the album was released."; + } + + container admin { + description + "Administrative information for the album."; + + leaf label { + type string; + description + "The label that released the album."; + } + leaf catalogue-number { + type string; + description + "The album's catalogue number."; + } + } + + list song { + key name; + description + "Represents one 'song' resource within one + 'album' resource, within the jukebox library."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the song."; + } + + leaf location { + type string; + mandatory true; + description + "The file location string of the + media file for the song."; + } + leaf format { + type string; + description + "An identifier string for the media type + for the file associated with the + 'location' leaf for this entry."; + } + leaf length { + type uint32; + units "seconds"; + description + "The duration of this song in seconds."; + } + } // end list 'song' + } // end list 'album' + } // end list 'artist' + + leaf artist-count { + type uint32; + units "artists"; + config false; + description + "Number of artists in the library."; + } + leaf album-count { + type uint32; + units "albums"; + config false; + description + "Number of albums in the library."; + } + leaf song-count { + type uint32; + units "songs"; + config false; + description + "Number of songs in the library."; + } + } // end library + + list playlist { + key name; + description + "Example configuration data resource."; + + leaf name { + type string; + description + "The name of the playlist."; + } + leaf description { + type string; + description + "A comment describing the playlist."; + } + list song { + key index; + ordered-by user; + + description + "Example nested configuration data resource."; + + leaf index { // not really needed + type uint32; + description + "An arbitrary integer index for this playlist song."; + } + leaf id { + type instance-identifier; + mandatory true; + description + "Song identifier. Must identify an instance of + /jukebox/library/artist/album/song/name."; + } + } + } + + container player { + description + "Represents the jukebox player resource."; + + leaf gap { + type decimal64 { + fraction-digits 1; + range "0.0 .. 2.0"; + } + units "tenths of seconds"; + description + "Time gap between each song."; + } + } + } + + rpc play { + description + "Control function for the jukebox player."; + input { + leaf playlist { + type string; + mandatory true; + description + "The playlist name."; + } + leaf song-number { + type uint32; + mandatory true; + description + "Song number in playlist to play."; + } + } + } + leaf-list extra{ + type string; + ordered-by user; + description "Extra added to test ordered-by user inserts on leaf-lists"; + } + } +EOF + + diff --git a/test/test_choice.sh b/test/test_choice.sh index 5ead0d93..aa982a8f 100755 --- a/test/test_choice.sh +++ b/test/test_choice.sh @@ -170,17 +170,17 @@ new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new "restconf set protocol tcp+udp fail" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' new "restconf set protocol tcp" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d {\"system:protocol\":{\"tcp\":[null]}})" 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/system:system/protocol -d {\"system:protocol\":{\"tcp\":[null]}})" 0 "" new "restconf get protocol tcp" expecteq "$(curl -s -X GET http://localhost/restconf/data/system:system)" 0 '{"system:system":{"protocol":{"tcp":[null]}}} ' new "restconf set protocol tcp+udp fail" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' new "cli set protocol udp" expectfn "$clixon_cli -1 -f $cfg -l o set system protocol udp" 0 "^$" diff --git a/test/test_identity.sh b/test/test_identity.sh index 489567d4..7a4fd6ba 100755 --- a/test/test_identity.sh +++ b/test/test_identity.sh @@ -267,7 +267,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^< # 2. set identity in other module with restconf , read it with restconf and netconf # 3. set identity in other module with netconf, read it with restconf and netconf new "restconf add own identity" -expectpart "$(curl -s -i -X PUT http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example:aes"}')" 0 'HTTP/1.1 201 Created' +expectpart "$(curl -s -i -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example:aes"}')" 0 'HTTP/1.1 201 Created' new "restconf get own identity" expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example:crypto)" 0 'HTTP/1.1 200 OK' '{"example:crypto":"aes"}' @@ -280,10 +280,10 @@ expectpart "$(curl -s -i -X DELETE http://localhost/restconf/data/example:crypt # 2. set identity in other module with restconf , read it with restconf and netconf new "restconf add POST instead of PUT (should fail)" -expectpart "$(curl -s -i -X POST http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"crypto"},"error-severity":"error","error-message":"Leaf contains sub-element"}}}' +expectpart "$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"crypto"},"error-severity":"error","error-message":"Leaf contains sub-element"}}}' new "restconf add other (des) identity using POST" -expectpart "$(curl -s -i -X POST http://localhost/restconf/data -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 201 Created' 'Location: http://localhost/restconf/data/example:crypto' +expectpart "$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 201 Created' 'Location: http://localhost/restconf/data/example:crypto' new "restconf get other identity" expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example:crypto)" 0 'HTTP/1.1 200 OK' '{"example:crypto":"example-des:des3"}' diff --git a/test/test_nacm.sh b/test/test_nacm.sh index 982ddcd4..754be257 100755 --- a/test/test_nacm.sh +++ b/test/test_nacm.sh @@ -136,7 +136,7 @@ expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-exam # explicitly disable nacm (regression on netgate bug) new "disable nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": false}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": false}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" new "auth set authentication config" expecteof "$clixon_netconf -qf $cfg" 0 "$RULES]]>]]>" "^]]>]]>$" @@ -157,7 +157,7 @@ expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-exam #----------------Enable NACM new "enable nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" new "admin get nacm" expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} @@ -171,13 +171,13 @@ new "guest get nacm" expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "admin edit nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x":1}' http://localhost/restconf/data/nacm-example:x)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x":1}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "limited edit nacm" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest edit nacm" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index c7ca696e..dba6fcbb 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -130,7 +130,7 @@ EOF ;; esac new "edit new 99" - expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 99}' http://localhost/restconf/data/nacm-example:x)" 0 "$ret" + expecteq "$(curl -u guest:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 99}' http://localhost/restconf/data/nacm-example:x)" 0 "$ret" #----------- Then second get case "$ret3" in diff --git a/test/test_nacm_ext.sh b/test/test_nacm_ext.sh index d265d927..97e4e9dd 100755 --- a/test/test_nacm_ext.sh +++ b/test/test_nacm_ext.sh @@ -157,7 +157,7 @@ expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data)" 0 '{"da ' new "Set x to 0" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 0}' http://localhost/restconf/data/nacm-example:x)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 0}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "auth get (no user: access denied)" expecteq "$(curl -sS -X GET -H \"Accept:\ application/yang-data+json\" http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}} ' @@ -181,13 +181,13 @@ new "guest get nacm" expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "admin edit nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "limited edit nacm" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest edit nacm" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "cli show conf as admin" expectfn "$clixon_cli -1 -U andy -l o -f $cfg show conf" 0 "^x 1;$" diff --git a/test/test_nacm_module_read.sh b/test/test_nacm_module_read.sh index f95cac52..cba2c096 100755 --- a/test/test_nacm_module_read.sh +++ b/test/test_nacm_module_read.sh @@ -153,7 +153,7 @@ new "commit it" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "enable nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" #--------------- nacm enabled @@ -233,21 +233,21 @@ expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data)" 0 '{"i #------- RPC operation new "admin rpc ok" -expecteq "$(curl -u andy:bar -s -X POST -d '{"clixon-example:input":{"x":"78"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"78","y":"42"}} +expecteq "$(curl -u andy:bar -s -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":"78"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"78","y":"42"}} ' new "admin rpc netconf ok" expecteof "$clixon_netconf -U andy -qf $cfg" 0 '0]]>]]>' 0 '^042]]>]]>$' new "limit rpc ok" -expecteq "$(curl -u wilma:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"clixon-example:output":{"x":"42","y":"42"}} +expecteq "$(curl -u wilma:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' )" 0 '{"clixon-example:output":{"x":"42","y":"42"}} ' new "limit rpc netconf ok" expecteof "$clixon_netconf -U wilma -qf $cfg" 0 '0]]>]]>' 0 '^042]]>]]>$' new "guest rpc fail" -expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' +expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' )" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "guest rpc netconf fail" expecteof "$clixon_netconf -U guest -qf $cfg" 0 '0]]>]]>' 0 '^applicationaccess-deniederroraccess denied]]>]]>$' @@ -255,7 +255,7 @@ expecteof "$clixon_netconf -U guest -qf $cfg" 0 ']]>]]>" "^]]>]]>$" new "enable nacm" - expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" + expectpart "$(curl -u andy:bar -sS -X PUT -H 'Content-Type: application/yang-data+json' -d '{"ietf-netconf-acm:enable-nacm":true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +# -H 'Content-Type: application/yang-data+json' } #--------------- enable nacm @@ -179,7 +180,7 @@ nacm # replace all, then must include NACM rules as well MSG="$RULES" new "update root list permit" -expecteq "$(curl -u andy:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d "$MSG")" 0 '' +expectpart "$(curl -u andy:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d "$MSG")" 0 '' new "delete root list deny" expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' @@ -238,13 +239,13 @@ expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/nacm- #----- default deny (clixon-example limit and guest have default access) new "default create list deny" -expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "create list permit" -expecteq "$(curl -u andy:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '' +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '' new "default update list deny" -expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val99"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val99"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "default delete list deny" expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/clixon-example:translate=key42)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' diff --git a/test/test_nacm_protocol.sh b/test/test_nacm_protocol.sh index ee063a57..c7f2353f 100755 --- a/test/test_nacm_protocol.sh +++ b/test/test_nacm_protocol.sh @@ -162,7 +162,7 @@ new "commit it" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "enable nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" #--------------- nacm enabled @@ -205,14 +205,14 @@ new "commit it" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "enable nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" # Rule 3: permit-edit-config new "permit-edit-config: limited ok restconf" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '' +expecteq "$(curl -u wilma:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '' new "permit-edit-config: guest fail restconf" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -H "Content-Type: application/yang-data+json" -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 20ada66d..c266f627 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -64,7 +64,7 @@ expecteq "$(curl -s -X GET http://localhost/.well-known/host-meta)" 0 " " new "restconf get restconf resource. RFC 8040 3.3 (json)" -expecteq "$(curl -sG http://localhost/restconf)" 0 '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}} +expecteq "$(curl -sG -H "Accept: application/yang-data+json" http://localhost/restconf)" 0 '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}} ' new "restconf get restconf resource. RFC 8040 3.3 (xml)" @@ -108,10 +108,16 @@ expectfn "curl -s -I http://localhost/restconf/data" 0 "HTTP/1.1 200 OK" #Content-Type: application/yang-data+json" new "restconf empty rpc" -expecteq "$(curl -s -X POST -d {\"clixon-example:input\":null} http://localhost/restconf/operations/clixon-example:empty)" 0 "" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} http://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" + +new "restconf empty rpc, default media type should fail" +expectpart "$(curl -si -X POST -d {\"clixon-example:input\":null} http://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' + +new "restconf empty rpc, default media type should fail (JSON)" +expectpart "$(curl -si -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} http://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' new "restconf empty rpc with extra args (should fail)" -expecteq "$(curl -s -X POST -d {\"clixon-example:input\":{\"extra\":null}} http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error"}}} ' +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error"}}} ' new "restconf get empty config + state json" expecteq "$(curl -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} @@ -160,10 +166,10 @@ expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" -expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created' +expectpart "$(curl -s -i -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' http://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' 'Location: http://localhost/restconf/data/ietf-interfaces:interfaces' new "restconf Re-add subtree eth/0/0 which should give error" -expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' +expectpart "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' # XXX Cant get this to work #expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"clixon-example:eth\",\"enabled\":true}}} http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' @@ -180,22 +186,21 @@ expectfn "curl -sG http://localhost/restconf/data/clixon-example:state" 0 "$stat " new "restconf Add interfaces subtree eth/0/0 using POST" -expectpart "$(curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "" -#expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d {"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' 0 "" +expectpart "$(curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "" new "restconf Check eth/0/0 added config" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} +expectpart "$(curl -s -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}\]}} ' new "restconf Check eth/0/0 added state" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} +expectpart "$(curl -s -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":\["42","41","43"\]}} ' new "restconf Re-post eth/0/0 which should generate error" -expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' +expectpart "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "Add leaf description using POST" -expecteq "$(curl -s -X POST -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "Add nothing using POST" expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":" on line 1: syntax error at or before:' @@ -214,33 +219,33 @@ new "restconf Re-Delete eth/0/0 using none should generate error" expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}} ' new "restconf Add subtree eth/0/0 using PUT" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "restconf get subtree" expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf rpc using POST json" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"42","y":"42"}} +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"42","y":"42"}} ' new "restconf rpc using POST json wrong" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error"}}} ' new "restconf rpc non-existing rpc without namespace" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' new "restconf rpc non-existing rpc" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/clixon-example:kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' new "restconf rpc missing name" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}} ' new "restconf rpc missing input" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}} ' new "restconf rpc using POST xml" -ret=$(curl -s -X POST -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example) +ret=$(curl -s -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example) expect='4242' match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then @@ -248,10 +253,10 @@ if [ -z "$match" ]; then fi new "restconf rpc using wrong prefix" -expecteq "$(curl -s -X POST -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/wrong:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/wrong:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}} ' new "restconf local client rpc using POST xml" -ret=$(curl -s -i -X POST -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' http://localhost/restconf/operations/clixon-example:client-rpc) +ret=$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' http://localhost/restconf/operations/clixon-example:client-rpc) expect='example' match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then @@ -259,10 +264,10 @@ if [ -z "$match" ]; then fi new "restconf Add subtree without key (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}} ' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}} ' new "restconf Add subtree with too many keys (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}} ' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index ba39e427..17ce5164 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -90,13 +90,13 @@ wait_backend wait_restconf new "restconf POST tree without key" -expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"type":"regular"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' new "restconf POST initial tree" -expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 "" +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}' http://localhost/restconf/data)" 0 "" new "restconf POST top without namespace" -expectfn 'curl -s -X POST -d {"cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"cont1"},"error-severity":"error","error-message":"Unassigned yang spec"}}}' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"cont1":{"interface":{"name":"local0","type":"regular"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"cont1"},"error-severity":"error","error-message":"Unassigned yang spec"}}} ' new "restconf GET datastore initial" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' @@ -116,22 +116,22 @@ new "restconf GET if-type" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0/type" 0 '{"example:type":"regular"}' new "restconf POST interface without mandatory type" -expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"name":"TEST"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"type"},"error-severity":"error","error-message":"Mandatory variable"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:cont1 -d '{"example:interface":{"name":"TEST"}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"type"},"error-severity":"error","error-message":"Mandatory variable"}}} ' new "restconf POST interface without mandatory key" -expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"type":"regular"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:cont1 -d '{"example:interface":{"type":"regular"}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' new "restconf POST interface" -expectfn 'curl -s -X POST -d {"example:interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/example:cont1' 0 "" +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 "" new "restconf POST interface without namespace" -expectfn 'curl -s -X POST -d {"interface":{"name":"TEST2","type":"eth0"}} http://localhost/restconf/data/example:cont1' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}}' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"interface":{"name":"TEST2","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}} ' new "restconf POST again" -expecteq "$(curl -s -X POST -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "restconf POST from top" -expecteq "$(curl -s -X POST -d '{"example:cont1":{"interface":{"name":"TEST","type":"eth0"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"TEST","type":"eth0"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "restconf DELETE" expectfn 'curl -s -X DELETE http://localhost/restconf/data/example:cont1' 0 "" @@ -140,7 +140,7 @@ new "restconf GET null datastore" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}}' new "restconf POST initial tree" -expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 "" +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}' http://localhost/restconf/data)" 0 "" new "restconf GET initial tree" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' @@ -152,41 +152,41 @@ new "restconf GET null datastore" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}}' new "restconf PUT initial datastore" -expectfn 'curl -s -X PUT -d {"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}} http://localhost/restconf/data' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' http://localhost/restconf/data)" 0 "" new "restconf GET datastore" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf PUT replace datastore" -expectfn 'curl -s -X PUT -d {"data":{"example:cont2":{"name":"foo"}}} http://localhost/restconf/data' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"data":{"example:cont2":{"name":"foo"}}}' http://localhost/restconf/data)" 0 "" new "restconf GET replaced datastore" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont2" 0 '{"example:cont2":{"name":"foo"}}' new "restconf PUT initial datastore again" -expectfn 'curl -s -X PUT -d {"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}} http://localhost/restconf/data' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' http://localhost/restconf/data)" 0 "" new "restconf PUT change interface" -expectfn 'curl -s -X PUT -d {"example:interface":{"name":"local0","type":"atm0"}} http://localhost/restconf/data/example:cont1/interface=local0' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"local0","type":"atm0"}}' http://localhost/restconf/data/example:cont1/interface=local0)" 0 "" new "restconf GET datastore atm" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"atm0"}\]}}' new "restconf PUT add interface" -expectfn 'curl -s -X PUT -d {"example:interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/example:cont1/interface=TEST' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1/interface=TEST)" 0 "" new "restconf PUT change key error" -expectfn 'curl -is -X PUT -d {"example:interface":{"name":"ALPHA","type":"eth0"}} http://localhost/restconf/data/example:cont1/interface=TEST' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"ALPHA","type":"eth0"}}' http://localhost/restconf/data/example:cont1/interface=TEST)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}} ' new "restconf PUT change type to eth0 (non-key sub-element to list)" -expectfn 'curl -s -X PUT -d {"example:type":"eth0"} http://localhost/restconf/data/example:cont1/interface=local0/type' 0 "" +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:type":"eth0"}' http://localhost/restconf/data/example:cont1/interface=local0/type)" 0 "" new "restconf GET datastore eth" expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"eth0"}\]}' #--------------- json type tests new "restconf POST type x3" -expectfn 'curl -s -X POST -d {"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}} http://localhost/restconf/data' 0 '' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}}' http://localhost/restconf/data)" 0 '' new "restconf POST type x3" expectfn 'curl -s -X GET http://localhost/restconf/data/example:types' 0 '{"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}}' diff --git a/test/test_restconf_err.sh b/test/test_restconf_err.sh index 8c218c29..35b26b17 100755 --- a/test/test_restconf_err.sh +++ b/test/test_restconf_err.sh @@ -6,6 +6,12 @@ # instance that does not exist, then an error response containing a "404 Not # Found" status-line MUST be returned by the server. The error-tag # value "invalid-value" is used in this case. +# RFC 7231: +# Response messages with an error status code +# usually contain a payload that represents the error condition, such +# that it describes the error state and what next steps are suggested +# for resolving it. + # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 5dbb7b93..88e78b28 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -7,8 +7,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi APPNAME=example cfg=$dir/conf.xml -fyang=$dir/example-jukebox.yang -fxml=$dir/initial.xml +fjukebox=$dir/example-jukebox.yang # example cat < $cfg @@ -16,7 +15,7 @@ cat < $cfg $cfg /usr/local/share/clixon $IETFRFC - $fyang + $fjukebox false /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/lib/$APPNAME/backend @@ -26,269 +25,8 @@ cat < $cfg EOF -cat < $fyang - module example-jukebox { - - namespace "http://example.com/ns/example-jukebox"; - prefix "jbox"; - - organization "Example, Inc."; - contact "support at example.com"; - description "Example Jukebox Data Model Module."; - revision "2016-08-15" { - description "Initial version."; - reference "example.com document 1-4673."; - } - - identity genre { - description - "Base for all genre types."; - } - - // abbreviated list of genre classifications - identity alternative { - base genre; - description - "Alternative music."; - } - identity blues { - base genre; - description - "Blues music."; - } - identity country { - base genre; - description - "Country music."; - } - identity jazz { - base genre; - description - "Jazz music."; - } - identity pop { - base genre; - description - "Pop music."; - } - identity rock { - base genre; - description - "Rock music."; - } - - container jukebox { - presence - "An empty container indicates that the jukebox - service is available."; - - description - "Represents a 'jukebox' resource, with a library, playlists, - and a 'play' operation."; - - container library { - - description - "Represents the 'jukebox' library resource."; - - list artist { - key name; - description - "Represents one 'artist' resource within the - 'jukebox' library resource."; - - leaf name { - type string { - length "1 .. max"; - } - description - "The name of the artist."; - } - - list album { - key name; - description - "Represents one 'album' resource within one - 'artist' resource, within the jukebox library."; - - leaf name { - type string { - length "1 .. max"; - } - description - "The name of the album."; - } - - leaf genre { - type identityref { base genre; } - description - "The genre identifying the type of music on - the album."; - } - - leaf year { - type uint16 { - range "1900 .. max"; - } - description - "The year the album was released."; - } - - container admin { - description - "Administrative information for the album."; - - leaf label { - type string; - description - "The label that released the album."; - } - leaf catalogue-number { - type string; - description - "The album's catalogue number."; - } - } - - list song { - key name; - description - "Represents one 'song' resource within one - 'album' resource, within the jukebox library."; - - leaf name { - type string { - length "1 .. max"; - } - description - "The name of the song."; - } - - leaf location { - type string; - mandatory true; - description - "The file location string of the - media file for the song."; - } - leaf format { - type string; - description - "An identifier string for the media type - for the file associated with the - 'location' leaf for this entry."; - } - leaf length { - type uint32; - units "seconds"; - description - "The duration of this song in seconds."; - } - } // end list 'song' - } // end list 'album' - } // end list 'artist' - - leaf artist-count { - type uint32; - units "artists"; - config false; - description - "Number of artists in the library."; - } - leaf album-count { - type uint32; - units "albums"; - config false; - description - "Number of albums in the library."; - } - leaf song-count { - type uint32; - units "songs"; - config false; - description - "Number of songs in the library."; - } - } // end library - - list playlist { - key name; - description - "Example configuration data resource."; - - leaf name { - type string; - description - "The name of the playlist."; - } - leaf description { - type string; - description - "A comment describing the playlist."; - } - list song { - key index; - ordered-by user; - - description - "Example nested configuration data resource."; - - leaf index { // not really needed - type uint32; - description - "An arbitrary integer index for this playlist song."; - } - leaf id { - type instance-identifier; - mandatory true; - description - "Song identifier. Must identify an instance of - /jukebox/library/artist/album/song/name."; - } - } - } - - container player { - description - "Represents the jukebox player resource."; - - leaf gap { - type decimal64 { - fraction-digits 1; - range "0.0 .. 2.0"; - } - units "tenths of seconds"; - description - "Time gap between each song."; - } - } - } - - rpc play { - description - "Control function for the jukebox player."; - input { - leaf playlist { - type string; - mandatory true; - description - "The playlist name."; - } - leaf song-number { - type uint32; - mandatory true; - description - "Song number in playlist to play."; - } - } - } - leaf-list extra{ - type string; - ordered-by user; - description "Extra added to test ordered-by user inserts on leaf-lists"; - } - } -EOF +# Common Jukebox spec (fjukebox must be set) +. ./jukebox.sh new "test params: -f $cfg" diff --git a/test/test_restconf_listkey.sh b/test/test_restconf_listkey.sh index 294f18f6..7b176f0d 100755 --- a/test/test_restconf_listkey.sh +++ b/test/test_restconf_listkey.sh @@ -88,65 +88,64 @@ wait_backend wait_restconf new "restconf PUT add whole list entry" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"x","c":"y","nonkey":"0"}}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y -d '{"list:a":{"b":"x","c":"y","nonkey":"0"}}')" 0 '' new "restconf PUT add whole list entry XML" expecteq "$(curl -s -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xxxy0' http://localhost/restconf/data/list:c/a=xx,xy)" 0 '' - new "restconf PUT change whole list entry (same keys)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"x","c":"y","nonkey":"z"}}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y -d '{"list:a":{"b":"x","c":"y","nonkey":"z"}}')" 0 '' new "restconf PUT change whole list entry (no namespace)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"a":{"b":"x","c":"y","nonkey":"z"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y -d '{"a":{"b":"x","c":"y","nonkey":"z"}}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}} ' new "restconf PUT change list entry (wrong keys)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"y","c":"x"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y -d '{"list:a":{"b":"y","c":"x"}}')" 0 '412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}} ' new "restconf PUT change list entry (wrong keys)(expect fail) XML" expecteq "$(curl -s -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xyxz0' http://localhost/restconf/data/list:c/a=xx,xy)" 0 'protocoloperation-failederrorapi-path keys do not match data keys ' new "restconf PUT change list entry (just one key)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x -d {"list:a":{"b":"x"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x -d '{"list:a":{"b":"x"}}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}} ' new "restconf PUT sub non-key" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/nonkey -d {"list:nonkey":"u"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/nonkey -d '{"list:nonkey":"u"}')" 0 '' new "restconf PUT sub key same value" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/b -d {"list:b":"x"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/b -d '{"list:b":"x"}')" 0 '' -new "restconf PUT just key other value (should fail)ZX" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/b -d {"b":"y"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}}' +new "restconf PUT just key other value (should fail)" +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x/b -d '{"b":"y"}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}}' new "restconf PUT add leaf-list entry" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/d=x -d {"list:d":"x"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/d=x -d '{"list:d":"x"}')" 0 '' new "restconf PUT change leaf-list entry (expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/d=x -d {"list:d":"y"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/d=x -d '{"list:d":"y"}')" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT list-list" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z -d {"list:e":{"f":"z","nonkey":"0"}}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/e=z -d '{"list:e":{"f":"z","nonkey":"0"}}')" 0 '' new "restconf PUT change list-lst entry (wrong keys)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z -d {"list:e":{"f":"wrong","nonley":"0"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/e=z -d '{"list:e":{"f":"wrong","nonley":"0"}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}} ' new "restconf PUT list-list sub non-key" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/nonkey -d {"list:nonkey":"u"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/e=z/nonkey -d '{"list:nonkey":"u"}')" 0 '' new "restconf PUT list-list single first key" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/e=z/f -d {"f":"z"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}}' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x/e=z/f -d '{"f":"z"}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}} ' new "restconf PUT list-list just key ok" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/f -d {"list:f":"z"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/e=z/f -d '{"list:f":"z"}')" 0 '' new "restconf PUT list-list just key just key wrong value (should fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/f -d {"list:f":"wrong"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/e=z/f -d '{"list:f":"wrong"}')" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}} ' new "restconf PUT add list+leaf-list entry" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/f=u -d {"list:f":"u"}' 0 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/f=u -d '{"list:f":"u"}')" 0 '' new "restconf PUT change list+leaf-list entry (expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/f=u -d {"list:f":"w"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' +expectpart "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y/f=u -d '{"list:f":"w"}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "Kill restconf daemon" diff --git a/test/test_restconf_patch.sh b/test/test_restconf_patch.sh new file mode 100755 index 00000000..ec539163 --- /dev/null +++ b/test/test_restconf_patch.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Restconf RFC8040 plain patch + +# 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 + /usr/local/share/clixon + $IETFRFC + $fjukebox + false + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/lib/$APPNAME/backend + $dir/restconf.pidfile + /usr/local/var/$APPNAME + +EOF + +# Common Jukebox spec (fjukebox must be set) +. ./jukebox.sh + +new "test params: -f $cfg" + +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + sudo pkill clixon_backend # to be sure + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg +fi + +new "kill old restconf daemon" +sudo pkill -u www-data -f "/www-data/clixon_restconf" + +new "start restconf daemon" +start_restconf -f $cfg + +new "waiting" +wait_backend +wait_restconf + + +new "Kill restconf daemon" +stop_restconf + +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/test/test_restconf_startup.sh b/test/test_restconf_startup.sh index 2513c7a8..a7d07977 100755 --- a/test/test_restconf_startup.sh +++ b/test/test_restconf_startup.sh @@ -72,13 +72,13 @@ testrun(){ wait_restconf new "restconf put 42" - expecteq "$(curl -s -X PUT http://localhost/restconf/data/example:x/y=42 -d '{"example:y":{"a":"42","b":"42"}}')" 0 "" + expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:x/y=42 -d '{"example:y":{"a":"42","b":"42"}}')" 0 "" new "restconf put 99" - expecteq "$(curl -s -X PUT http://localhost/restconf/data/example:x/y=99 -d '{"example:y":{"a":"99","b":"99"}}')" 0 "" + expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:x/y=99 -d '{"example:y":{"a":"99","b":"99"}}')" 0 "" new "restconf post 123" - expecteq "$(curl -s -X POST http://localhost/restconf/data/example:x -d '{"example:y":{"a":"123","b":"123"}}')" 0 "" + expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example:x -d '{"example:y":{"a":"123","b":"123"}}')" 0 "" new "restconf delete 42" expecteq "$(curl -s -X DELETE http://localhost/restconf/data/example:x/y=42)" 0 "" diff --git a/test/test_rpc.sh b/test/test_rpc.sh index 0c956e03..35e8a1e9 100755 --- a/test/test_rpc.sh +++ b/test/test_rpc.sh @@ -70,36 +70,35 @@ new "netconf empty rpc" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' new "restconf example rpc json/json default - no http media headers" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 200 OK' 'Content-Type: application/yang-data+json' '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc json/json change y default" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0","y":"99"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"99"}} +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":"0","y":"99"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'Content-Type: application/yang-data+json' '{"clixon-example:output":{"x":"0","y":"99"}} ' new "restconf example rpc json/json" # XXX example:input example:output -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+json' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+json' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'Content-Type: application/yang-data+json' '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc xml/json" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+json' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+json' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 'Content-Type: application/yang-data+json' '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc json/xml" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+xml' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '042 +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+xml' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'Content-Type: application/yang-data+xml' '042 ' new "restconf example rpc xml/xml" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 '042 +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 'Content-Type: application/yang-data+xml' '042 ' new "restconf example rpc xml in w json encoding (expect fail)" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+xml' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 "rpcmalformed-messageerror on line 1: syntax error at or before: '<' " - +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+xml' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Reques' "rpcmalformed-messageerror on line 1: syntax error at or before: '<' " new "restconf example rpc json in xml encoding (expect fail)" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'rpcmalformed-messageerrorxml_parse: line 0: syntax error: at or before: " ' +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Reques' 'rpcmalformed-messageerrorxml_parse: line 0: syntax error: at or before: " ' new "netconf example rpc" expecteof "$clixon_netconf -qf $cfg" 0 '0]]>]]>' '^042]]>]]>$' @@ -107,7 +106,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^applicationmissing-elementsession-iderrorMandatory variable]]>]]>$' diff --git a/test/test_submodule.sh b/test/test_submodule.sh index 02dd3923..cb05d9c6 100755 --- a/test/test_submodule.sh +++ b/test/test_submodule.sh @@ -204,16 +204,16 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^< # Now same with restconf new "restconf edit main" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:main":{"x":"foo","ext":"foo"}}' 0 'HTTP/1.1 201 Created' +expectpart "$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data -d '{"main:main":{"x":"foo","ext":"foo"}}')" 0 'HTTP/1.1 201 Created' new "restconf edit sub1" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub1":{"x":"foo","ext1":"foo"}}' 0 'HTTP/1.1 201 Created' +expectpart "$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data -d '{"main:sub1":{"x":"foo","ext1":"foo"}}')" 0 'HTTP/1.1 201 Created' new "restconf edit sub2" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 201 Created' +expectpart "$(curl -s -i -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data -d '{"main:sub2":{"x":"foo","ext2":"foo"}}')" 0 'HTTP/1.1 201 Created' new "restconf check main/sub1/sub2 contents" -expectfn "curl -s -X GET http://localhost/restconf/data" 0 '{"data":{"main:main":{"ext":"foo","x":"foo"},"main:sub1":{"ext1":"foo","x":"foo"},"main:sub2":{"ext2":"foo","x":"foo"}}}' +expectpart "$(curl -s -X GET http://localhost/restconf/data)" 0 '{"data":{"main:main":{"ext":"foo","x":"foo"},"main:sub1":{"ext1":"foo","x":"foo"},"main:sub2":{"ext2":"foo","x":"foo"}}}' new "Kill restconf daemon" stop_restconf diff --git a/test/test_yang_namespace.sh b/test/test_yang_namespace.sh index 07691ebe..b0a302b8 100755 --- a/test/test_yang_namespace.sh +++ b/test/test_yang_namespace.sh @@ -102,14 +102,14 @@ new "netconf discard-changes" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "restconf set x in example1" -expecteq "$(curl -s -X POST -d '{"example1:x":42}' http://localhost/restconf/data)" 0 '' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example1:x":42}' http://localhost/restconf/data)" 0 '' new "restconf get config example1" expecteq "$(curl -s -X GET http://localhost/restconf/data/example1:x)" 0 '{"example1:x":42} ' new "restconf set x in example2" -expecteq "$(curl -s -X POST -d '{"example2:x":{"y":99}}' http://localhost/restconf/data)" 0 '' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example2:x":{"y":99}}' http://localhost/restconf/data)" 0 '' # XXX GET ../example1:x is translated to select=/x which gets both example1&2 #new "restconf get config example1"