From ff5462ecac4dc300aab48af389a3bd270d7a71cf Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 9 Jun 2020 16:04:49 +0200 Subject: [PATCH 1/5] Replaced the global variable `debug` with access function: `clicon_debug_get()`. --- CHANGELOG.md | 1 + apps/backend/backend_client.c | 4 +- apps/backend/backend_commit.c | 2 +- apps/backend/backend_main.c | 16 ++-- apps/backend/backend_plugin.c | 2 +- apps/cli/cli_main.c | 11 +-- apps/cli/clixon_cli_api.h | 13 +--- apps/netconf/netconf_lib.c | 2 +- apps/netconf/netconf_main.c | 11 +-- apps/restconf/restconf_fcgi_main.c | 11 +-- apps/restconf/restconf_methods.c | 2 +- apps/restconf/restconf_methods_get.c | 2 +- apps/restconf/restconf_methods_post.c | 12 +-- example/main/example_backend.c | 2 +- lib/clixon/clixon_log.h | 7 +- lib/src/clixon_datastore_read.c | 6 +- lib/src/clixon_log.c | 23 ++++-- lib/src/clixon_path.c | 4 +- lib/src/clixon_proto.c | 4 +- lib/src/clixon_stream.c | 2 +- lib/src/clixon_xml_map.c | 2 +- lib/src/clixon_xpath.c | 2 +- lib/src/clixon_xpath_eval.c | 4 +- lib/src/clixon_yang_parse.y | 7 -- test/test_cli_gen.sh | 106 ++++++++++++++++++++++++++ util/clixon_util_datastore.c | 7 +- util/clixon_util_json.c | 7 +- util/clixon_util_path.c | 5 +- util/clixon_util_regexp.c | 11 ++- util/clixon_util_socket.c | 7 +- util/clixon_util_stream.c | 4 +- util/clixon_util_xml.c | 9 ++- util/clixon_util_xml_mod.c | 8 +- util/clixon_util_xpath.c | 5 +- util/clixon_util_yang.c | 6 +- 35 files changed, 227 insertions(+), 100 deletions(-) create mode 100755 test/test_cli_gen.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 16267558..eb09c81d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Expected: July 2020 ### C-API changes on existing features (For developers) +* Replaced the global variable `debug` with access function: `clicon_debug_get()`. * Due to name collision with libevent, all clixon event functions prepended with `clixon_`. You need to rename your event functions as follows: * event_reg_fd() -> clixon_event_reg_fd() * event_unreg_fd() -> clixon_event_unreg_fd() diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 43c4f6b2..43b7d61a 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -1066,7 +1066,7 @@ from_client_get(clicon_handle h, (ret = xml_yang_validate_add(h, xret, &xerr)) < 0) goto done; if (ret == 0){ - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xret, "VALIDATE_STATE"); if (clixon_netconf_internal_error(xerr, ". Internal error, state callback returned invalid XML", @@ -1362,7 +1362,7 @@ from_client_debug(clicon_handle h, clicon_debug_init(level, NULL); /* 0: dont debug, 1:debug */ setlogmask(LOG_UPTO(level?LOG_DEBUG:LOG_INFO)); /* for syslog */ - clicon_log(LOG_NOTICE, "%s debug:%d", __FUNCTION__, debug); + clicon_log(LOG_NOTICE, "%s debug:%d", __FUNCTION__, clicon_debug_get()); cprintf(cbret, ""); ok: retval = 0; diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c index c020dc90..823eb878 100644 --- a/apps/backend/backend_commit.c +++ b/apps/backend/backend_commit.c @@ -453,7 +453,7 @@ from_validate_common(clicon_handle h, &td->td_tcvec, /* changed: wanted values */ &td->td_clen) < 0) goto done; - if (debug>1) + if (clicon_debug_get()>1) transaction_print(stderr, td); /* Mark as changed in tree */ for (i=0; itd_dlen; i++){ /* Also down */ diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 55fb6ac9..7afe54af 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -461,6 +461,7 @@ main(int argc, cvec *nsctx_global = NULL; /* Global namespace context */ size_t cligen_buflen; size_t cligen_bufthreshold; + int dbg; /* In the startup, logs to stderr & syslog and debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, logdst); @@ -472,6 +473,7 @@ main(int argc, once = 0; zap = 0; extraxml_file = NULL; + dbg = 0; /* * Command-line options for help, debug, and config-file @@ -489,7 +491,7 @@ main(int argc, help = 1; break; case 'D' : /* debug */ - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(h, argv[0]); break; case 'f': /* config file */ @@ -513,8 +515,8 @@ main(int argc, * XXX: if started in a start-daemon script, there will be irritating * double syslogs until fork below. */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); - clicon_debug_init(debug, NULL); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); /* Find and read configfile */ if (clicon_options_main(h) < 0){ @@ -622,7 +624,7 @@ main(int argc, /* Access the remaining argv/argc options (after --) w clicon-argv_get() */ clicon_argv_set(h, argv0, argc, argv); - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); /* Defer: Wait to the last minute to print help message */ if (help) @@ -880,7 +882,7 @@ main(int argc, demonized errors OK. Before this stage, errors are logged on stderr also */ if (foreground==0){ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst==CLICON_LOG_FILE?CLICON_LOG_FILE:CLICON_LOG_SYSLOG); if (daemon(0, 0) < 0){ fprintf(stderr, "config: daemon"); @@ -911,8 +913,8 @@ main(int argc, goto done; if (clicon_socket_set(h, ss) < 0) goto done; - if (debug) - clicon_option_dump(h, debug); + if (dbg) + clicon_option_dump(h, dbg); /* Depending on configure setting, privileges may be dropped here after * initializations */ if (check_drop_priv(h, gid) < 0) diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 1eaceee1..e2b3bcc3 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -246,7 +246,7 @@ clixon_plugin_statedata_all(clicon_handle h, continue; } #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, x, "%s STATE:", __FUNCTION__); #endif /* XXX: ret == 0 invalid yang binding should be handled as internal error */ diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 622a6023..881f14d6 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -294,6 +294,7 @@ main(int argc, cvec *nsctx_global = NULL; /* Global namespace context */ size_t cligen_buflen; size_t cligen_bufthreshold; + int dbg=0; /* Defaults */ once = 0; @@ -332,7 +333,7 @@ main(int argc, help = 1; break; case 'D' : /* debug */ - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(h, argv[0]); break; case 'f': /* config file */ @@ -352,9 +353,9 @@ main(int argc, /* * Logs, error and debug to stderr or syslog, set debug level */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); - clicon_debug_init(debug, NULL); + clicon_debug_init(dbg, NULL); /* Find, read and parse configfile */ if (clicon_options_main(h) < 0){ @@ -588,8 +589,8 @@ main(int argc, if (logclisyntax) cli_logsyntax_set(h, logclisyntax); - if (debug) - clicon_option_dump(h, debug); + if (dbg) + clicon_option_dump(h, dbg); /* Join rest of argv to a single command */ restarg = clicon_strjoin(argc, argv, " "); diff --git a/apps/cli/clixon_cli_api.h b/apps/cli/clixon_cli_api.h index 80653f66..c7c31040 100644 --- a/apps/cli/clixon_cli_api.h +++ b/apps/cli/clixon_cli_api.h @@ -69,54 +69,44 @@ int cli_notification_register(clicon_handle h, char *stream, enum format_enum fo /* cli_common.c: CLIgen new vector callbacks */ - int cli_set(clicon_handle h, cvec *vars, cvec *argv); int cli_merge(clicon_handle h, cvec *vars, cvec *argv); int cli_create(clicon_handle h, cvec *vars, cvec *argv); + int cli_remove(clicon_handle h, cvec *vars, cvec *argv); int cli_del(clicon_handle h, cvec *vars, cvec *argv); int cli_debug_cli(clicon_handle h, cvec *vars, cvec *argv); - int cli_debug_backend(clicon_handle h, cvec *vars, cvec *argv); - int cli_debug_restconf(clicon_handle h, cvec *vars, cvec *argv); int cli_set_mode(clicon_handle h, cvec *vars, cvec *argv); - int cli_start_shell(clicon_handle h, cvec *vars, cvec *argv); - int cli_quit(clicon_handle h, cvec *vars, cvec *argv); - int cli_commit(clicon_handle h, cvec *vars, cvec *argv); int cli_validate(clicon_handle h, cvec *vars, cvec *argv); - int compare_dbs(clicon_handle h, cvec *vars, cvec *argv); int load_config_file(clicon_handle h, cvec *vars, cvec *argv); int save_config_file(clicon_handle h, cvec *vars, cvec *argv); - int delete_all(clicon_handle h, cvec *vars, cvec *argv); - int discard_changes(clicon_handle h, cvec *vars, cvec *argv); - int cli_notify(clicon_handle h, cvec *cvv, cvec *argv); - int db_copy(clicon_handle h, cvec *cvv, cvec *argv); int cli_lock(clicon_handle h, cvec *cvv, cvec *argv); @@ -133,7 +123,6 @@ int expand_dbvar(void *h, char *name, cvec *cvv, cvec *argv, /* cli_show.c: CLIgen new vector arg callbacks */ int show_yang(clicon_handle h, cvec *vars, cvec *argv); - int show_conf_xpath(clicon_handle h, cvec *cvv, cvec *argv); int cli_show_config(clicon_handle h, cvec *cvv, cvec *argv); diff --git a/apps/netconf/netconf_lib.c b/apps/netconf/netconf_lib.c index a42dde08..995fa69e 100644 --- a/apps/netconf/netconf_lib.c +++ b/apps/netconf/netconf_lib.c @@ -197,7 +197,7 @@ netconf_output(int s, int retval = -1; clicon_debug(1, "SEND %s", msg); - if (debug > 1){ /* XXX: below only works to stderr, clicon_debug may log to syslog */ + if (clicon_debug_get() > 1){ /* XXX: below only works to stderr, clicon_debug may log to syslog */ cxobj *xt = NULL; if (clixon_xml_parse_string(buf, YB_NONE, NULL, &xt, NULL) == 0){ clicon_xml2file(stderr, xml_child_i(xt, 0), 0, 0); diff --git a/apps/netconf/netconf_main.c b/apps/netconf/netconf_main.c index 376edd89..5d30ec02 100644 --- a/apps/netconf/netconf_main.c +++ b/apps/netconf/netconf_main.c @@ -401,6 +401,7 @@ main(int argc, cvec *nsctx_global = NULL; /* Global namespace context */ size_t cligen_buflen; size_t cligen_bufthreshold; + int dbg = 0; /* Create handle */ if ((h = clicon_handle_init()) == NULL) @@ -421,7 +422,7 @@ main(int argc, usage(h, argv[0]); break; case 'D' : /* debug */ - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(h, argv[0]); break; case 'f': /* override config file */ @@ -442,8 +443,8 @@ main(int argc, /* * Logs, error and debug to stderr or syslog, set debug level */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); - clicon_debug_init(debug, NULL); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); /* Find, read and parse configfile */ if (clicon_options_main(h) < 0) @@ -592,8 +593,8 @@ main(int argc, send_hello(h, 1, id); if (clixon_event_reg_fd(0, netconf_input_cb, h, "netconf socket") < 0) goto done; - if (debug) - clicon_option_dump(h, debug); + if (dbg) + clicon_option_dump(h, dbg); if (tv.tv_sec || tv.tv_usec){ struct timeval t; gettimeofday(&t, NULL); diff --git a/apps/restconf/restconf_fcgi_main.c b/apps/restconf/restconf_fcgi_main.c index f5f07c23..24f45c7c 100644 --- a/apps/restconf/restconf_fcgi_main.c +++ b/apps/restconf/restconf_fcgi_main.c @@ -551,6 +551,7 @@ main(int argc, cvec *nsctx_global = NULL; /* Global namespace context */ size_t cligen_buflen; size_t cligen_bufthreshold; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, logdst); @@ -567,7 +568,7 @@ main(int argc, usage(h, argv[0]); break; case 'D' : /* debug */ - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(h, argv[0]); break; case 'f': /* override config file */ @@ -587,9 +588,9 @@ main(int argc, /* * Logs, error and debug to stderr or syslog, set debug level */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); - clicon_debug_init(debug, NULL); + clicon_debug_init(dbg, NULL); clicon_log(LOG_NOTICE, "%s: %u Started", __PROGRAM__, getpid()); if (set_signal(SIGTERM, restconf_sig_term, NULL) < 0){ clicon_err(OE_DAEMON, errno, "Setting signal"); @@ -737,8 +738,8 @@ main(int argc, goto done; /* Dump configuration options on debug */ - if (debug) - clicon_option_dump(h, debug); + if (dbg) + clicon_option_dump(h, dbg); /* Call start function in all plugins before we go interactive */ diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 83b45906..e646fbad 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -306,7 +306,7 @@ api_data_write(clicon_handle h, } #if 0 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xret, "%s xret:", __FUNCTION__); #endif if (xml_child_nr(xret) == 0){ /* Object does not exist */ diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index deb7e94e..43b36414 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -221,7 +221,7 @@ api_data_get2(clicon_handle h, * We need to cut that tree to only the object. */ #if 0 /* DEBUG */ - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xret, "%s xret:", __FUNCTION__); #endif /* Check if error return */ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index 60d535ec..9a384a82 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -307,7 +307,7 @@ api_data_post(clicon_handle h, if (restconf_insert_attributes(xdata, qvec) < 0) goto done; #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xdata, "%s xdata:", __FUNCTION__); #endif @@ -512,7 +512,7 @@ api_operations_post_input(clicon_handle h, * ... */ #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xdata, "%s xdata:", __FUNCTION__); #endif /* Validate that exactly only tag */ @@ -614,7 +614,7 @@ api_operations_post_output(clicon_handle h, xml_name_set(xoutput, "output"); /* xoutput should now look: 0 */ #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xoutput, "%s xoutput:", __FUNCTION__); #endif @@ -843,7 +843,7 @@ api_operations_post(clicon_handle h, /* Here xtop is: 42 */ #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xtop, "%s 5. Translate input args:", __FUNCTION__); #endif /* 6. Validate outgoing RPC and fill in defaults */ @@ -874,7 +874,7 @@ api_operations_post(clicon_handle h, * 4299 */ #if 0 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xtop, "%s 6. Validate and defaults:", __FUNCTION__); #endif /* 7. Send to RPC handler, either local or backend @@ -909,7 +909,7 @@ api_operations_post(clicon_handle h, * 0 */ #if 1 - if (debug) + if (clicon_debug_get()) clicon_log_xml(LOG_DEBUG, xret, "%s Receive reply:", __FUNCTION__); #endif youtput = yang_find(yrpc, Y_OUTPUT, NULL); diff --git a/example/main/example_backend.c b/example/main/example_backend.c index bbb8c48c..832a0c73 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -159,7 +159,7 @@ main_commit(clicon_handle h, /* Get all added i/fs */ if (xpath_vec_flag(target, nsc, "//interface", XML_FLAG_ADD, &vec, &len) < 0) return -1; - if (debug) + if (clicon_debug_get()) for (i=0; i1) + if (clicon_debug_get()>1) clicon_xml2file(stderr, xt, 0, 1); *xtop = xt; xt = NULL; @@ -565,7 +565,7 @@ xmldb_get_cache(clicon_handle h, /* Copy the matching parts of the (relevant) XML tree. * If cache was empty, also update to datastore cache */ - if (debug>1) + if (clicon_debug_get()>1) clicon_xml2file(stderr, x1t, 0, 1); *xtop = x1t; retval = 0; @@ -639,7 +639,7 @@ xmldb_get_zerocopy(clicon_handle h, /* Apply default values (removed in clear function) */ if (xml_default_recurse(x0t) < 0) goto done; - if (debug>1) + if (clicon_debug_get()>1) clicon_xml2file(stderr, x0t, 0, 1); *xtop = x0t; retval = 0; diff --git a/lib/src/clixon_log.c b/lib/src/clixon_log.c index 74c19b36..c3a752e0 100644 --- a/lib/src/clixon_log.c +++ b/lib/src/clixon_log.c @@ -58,8 +58,12 @@ #include "clixon_err.h" #include "clixon_log.h" -/* The global debug level. 0 means no debug */ -int debug = 0; +/* The global debug level. 0 means no debug + * @note There are pros and cons in having the debug state as a global variable. The + * alternative to bind it to the clicon handle (h) was considered but it limits its + * usefulness, since not all functions have h + */ +static int _clixon_debug = 0; /* Bitmask whether to log to syslog or stderr: CLICON_LOG_STDERR | CLICON_LOG_SYSLOG */ static int _logflags = 0x0; @@ -67,6 +71,7 @@ static int _logflags = 0x0; /* Set to open file to write debug messages directly to file */ static FILE *_logfile = NULL; + /*! Initialize system logger. * * Make syslog(3) calls with specified ident and gates calls of level upto specified level (upto). @@ -217,7 +222,7 @@ clicon_log_str(int level, /* syslog makes own filtering, we do it here: * if normal (not debug) then filter loglevels >= debug */ - if (debug == 0 && level >= LOG_DEBUG) + if (_clixon_debug == 0 && level >= LOG_DEBUG) goto done; if (_logflags & CLICON_LOG_STDERR){ flogtime(stderr); @@ -231,6 +236,7 @@ clicon_log_str(int level, flogtime(_logfile); fprintf(_logfile, "%s\n", msg); fflush(_logfile); + } /* Enable this if you want syslog in a stream. But there are problems with @@ -288,7 +294,6 @@ clicon_log(int level, return retval; } - /*! Initialize debug messages. Set debug level. * * Initialize debug module. The level is used together with clicon_debug(dbglevel) calls as follows: @@ -309,10 +314,16 @@ int clicon_debug_init(int dbglevel, FILE *f) { - debug = dbglevel; /* Global variable */ + _clixon_debug = dbglevel; /* Global variable */ return 0; } +int +clicon_debug_get(void) +{ + return _clixon_debug; +} + /*! Print a debug message with debug-level. Settings determine where msg appears. * * If the dbglevel passed in the function is equal to or lower than the one set by @@ -335,7 +346,7 @@ clicon_debug(int dbglevel, char *msg = NULL; int retval = -1; - if (dbglevel > debug) /* debug mask */ + if (dbglevel > _clixon_debug) /* compare debug mask with global variable */ return 0; /* first round: compute length of debug message */ va_start(args, format); diff --git a/lib/src/clixon_path.c b/lib/src/clixon_path.c index 539747a5..ff17384c 100644 --- a/lib/src/clixon_path.c +++ b/lib/src/clixon_path.c @@ -1559,7 +1559,7 @@ clixon_xml_find_api_path(cxobj *xt, /* Parse api-path string to structured clixon-path data */ if (api_path_parse(api_path, &cplist) < 0) goto done; - if (debug) + if (clicon_debug_get()) clixon_path_print(stderr, cplist); /* Resolve module:name to yang-stmt, fail if not successful */ if ((ret = api_path_resolve(cplist, yt)) < 0) @@ -1652,7 +1652,7 @@ clixon_xml_find_instance_id(cxobj *xt, va_end(ap); if (instance_id_parse(path, &cplist) < 0) goto done; - if (debug) + if (clicon_debug_get()) clixon_path_print(stderr, cplist); /* Resolve module:name to pointer to yang-stmt, fail if not successful */ if ((ret = instance_id_resolve(cplist, yt)) < 0) diff --git a/lib/src/clixon_proto.c b/lib/src/clixon_proto.c index 1c24dcc3..84082877 100644 --- a/lib/src/clixon_proto.c +++ b/lib/src/clixon_proto.c @@ -326,7 +326,7 @@ clicon_msg_send(int s, clicon_debug(2, "%s: send msg len=%d", __FUNCTION__, ntohl(msg->op_len)); - if (debug > 2) + if (clicon_debug_get() > 2) msg_dump(msg); if (atomicio((ssize_t (*)(int, void *, size_t))write, s, msg, ntohl(msg->op_len)) < 0){ @@ -400,7 +400,7 @@ clicon_msg_rcv(int s, clicon_err(OE_CFG, errno, "body too short"); goto done; } - if (debug > 1) + if (clicon_debug_get() > 1) msg_dump(*msg); retval = 0; done: diff --git a/lib/src/clixon_stream.c b/lib/src/clixon_stream.c index c43b3048..1e6c7bab 100644 --- a/lib/src/clixon_stream.c +++ b/lib/src/clixon_stream.c @@ -927,7 +927,7 @@ url_post(char *url, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(postfields)); - if (debug) + if (clicon_debug_get()) curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); if ((errcode = curl_easy_perform(curl)) != CURLE_OK){ clicon_debug(1, "%s: curl: %s(%d)", __FUNCTION__, err, errcode); diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 3f1428da..47ee536e 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -358,7 +358,7 @@ xml2cvec(cxobj *xt, } } } - if (debug > 1){ + if (clicon_debug_get() > 1){ clicon_debug(2, "%s cvv:\n", __FUNCTION__); cvec_print(stderr, cvv); } diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index 8a6f7076..70a72361 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -504,7 +504,7 @@ xpath_parse(char *xpath, clicon_err(OE_XML, 0, "XPATH parser error with no error code (should not happen)"); goto done; } - if (debug > 1){ + if (clicon_debug_get() > 1){ if ((cb = cbuf_new()) == NULL){ clicon_err(OE_XML, errno, "cbuf_new"); goto done; diff --git a/lib/src/clixon_xpath_eval.c b/lib/src/clixon_xpath_eval.c index 69b547f6..8d155ffc 100644 --- a/lib/src/clixon_xpath_eval.c +++ b/lib/src/clixon_xpath_eval.c @@ -940,7 +940,7 @@ xp_eval(xp_ctx *xc, xp_ctx *xr2 = NULL; int use_xr0 = 0; /* In 2nd child use transitively result of 1st child */ - if (debug > 1) + if (clicon_debug_get() > 1) ctx_print(stderr, xc, xpath_tree_int2str(xs->xs_type)); /* Pre-actions before check first child c0 */ @@ -1096,7 +1096,7 @@ xp_eval(xp_ctx *xc, xr0 = NULL; } ok: - if (debug>1) + if (clicon_debug_get() > 1) ctx_print(stderr, *xrp, xpath_tree_int2str(xs->xs_type)); retval = 0; done: diff --git a/lib/src/clixon_yang_parse.y b/lib/src/clixon_yang_parse.y index de147a03..7e3add06 100644 --- a/lib/src/clixon_yang_parse.y +++ b/lib/src/clixon_yang_parse.y @@ -195,13 +195,6 @@ extern int clixon_yang_parseget_lineno (void); -int -clicon_yang_debug(int d) -{ - debug = d; - return 0; -} - /* clixon_yang_parseerror also called from yacc generated code * diff --git a/test/test_cli_gen.sh b/test/test_cli_gen.sh new file mode 100755 index 00000000..4d78fa77 --- /dev/null +++ b/test/test_cli_gen.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Tests for using the generated cli. +# In particular setting a config, displaying as cli commands and reconfigure it using that. +# Tests: +# Make a config in CLI. Show output as CLI, save it and ensure it is the same + +# 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 + +# include err() and new() functions and creates $dir + +cfg=$dir/conf_yang.xml +fyang=$dir/$APPNAME.yang +clidir=$dir/cli +if [ -d $clidir ]; then + rm -rf $clidir/* +else + mkdir $clidir +fi + +# Use yang in example + +cat < $cfg + + $cfg + /usr/local/share/clixon + $dir + $fyang + $clidir + /usr/local/lib/$APPNAME/cli + $APPNAME + ALL + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + /usr/local/var/$APPNAME + +EOF + +cat < $fyang +module $APPNAME { + namespace "urn:example:clixon"; + prefix ex; + container table{ + list parameter{ + key name; + leaf name{ + type string; + } + leaf value{ + type string; + } + } + } +} +EOF + +cat < $clidir/ex.cli +CLICON_MODE="example"; +CLICON_PROMPT="%U@%H> "; + +set @datamodel, cli_set(); +merge @datamodel, cli_merge(); +create @datamodel, cli_create(); +delete @datamodel, cli_del(); +show config @datamodel, cli_show_auto("candidate", "cli"); + +EOF + +new "test params: -f $cfg" +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -z -f $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg + + new "waiting" + wait_backend +fi + +# Set a config in CLI +new "set a" +expectfn "$clixon_cli -1 -f $cfg set table parameter name a value x" 0 "" + +new "set b" +expectfn "$clixon_cli -1 -f $cfg set table parameter name b value y" 0 "" + +new "set b" +expectfn "$clixon_cli -1 -f $cfg set table parameter name b value y" 0 "" + + + +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/util/clixon_util_datastore.c b/util/clixon_util_datastore.c index 037f2298..c29131c9 100644 --- a/util/clixon_util_datastore.c +++ b/util/clixon_util_datastore.c @@ -118,6 +118,7 @@ main(int argc, char **argv) int i; char *xpath; cbuf *cbret = NULL; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR); @@ -135,7 +136,7 @@ main(int argc, char **argv) usage(argv0); break; case 'D' : /* debug */ - debug = 1; + dbg = 1; break; case 'd': /* db symbolic: running|candidate|startup */ if (!optarg) @@ -166,8 +167,8 @@ main(int argc, char **argv) /* * Logs, error and debug to stderr, set debug level */ - clicon_log_init(__FILE__, debug?LOG_DEBUG:LOG_INFO, CLICON_LOG_STDERR); - clicon_debug_init(debug, NULL); + clicon_log_init(__FILE__, dbg?LOG_DEBUG:LOG_INFO, CLICON_LOG_STDERR); + clicon_debug_init(dbg, NULL); argc -= optind; argv += optind; diff --git a/util/clixon_util_json.c b/util/clixon_util_json.c index 18a43fea..1d40cf8d 100644 --- a/util/clixon_util_json.c +++ b/util/clixon_util_json.c @@ -96,6 +96,7 @@ main(int argc, cxobj *xerr = NULL; /* malloced must be freed */ int ret; int pretty = 0; + int dbg = 0; optind = 1; opterr = 0; @@ -105,7 +106,7 @@ main(int argc, usage(argv[0]); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv[0]); break; case 'j': @@ -125,7 +126,9 @@ main(int argc, usage(argv[0]); break; } - clicon_log_init(__FILE__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__FILE__, dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); + if (yang_filename){ if ((yspec = yspec_new()) == NULL) goto done; diff --git a/util/clixon_util_path.c b/util/clixon_util_path.c index 5d3f9961..2805b5c8 100644 --- a/util/clixon_util_path.c +++ b/util/clixon_util_path.c @@ -109,6 +109,7 @@ main(int argc, cxobj *xcfg = NULL; cxobj *xerr = NULL; /* malloced must be freed */ int nr = 1; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init("api-path", LOG_DEBUG, CLICON_LOG_STDERR); @@ -129,7 +130,7 @@ main(int argc, usage(argv0); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv0); break; case 'f': /* XML file */ @@ -159,6 +160,8 @@ main(int argc, usage(argv[0]); break; } + clicon_debug_init(dbg, NULL); + /* Parse yang */ if (yang_file_dir){ if ((yspec = yspec_new()) == NULL) diff --git a/util/clixon_util_regexp.c b/util/clixon_util_regexp.c index 997df129..e8413986 100644 --- a/util/clixon_util_regexp.c +++ b/util/clixon_util_regexp.c @@ -163,6 +163,7 @@ main(int argc, int ret = 0; int nr = 1; int mode = 0; /* 0 is posix, 1 is libxml */ + int dbg = 0; optind = 1; opterr = 0; @@ -172,7 +173,7 @@ main(int argc, usage(argv0); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv0); break; case 'p': /* xsd->posix */ @@ -195,7 +196,9 @@ main(int argc, usage(argv[0]); break; } - clicon_log_init(__FILE__, debug?LOG_DEBUG:LOG_INFO, CLICON_LOG_STDERR); + clicon_log_init(__FILE__, dbg?LOG_DEBUG:LOG_INFO, CLICON_LOG_STDERR); + clicon_debug_init(dbg, NULL); + if (regexp == NULL){ fprintf(stderr, "-r mandatory\n"); usage(argv0); @@ -211,12 +214,12 @@ main(int argc, clicon_debug(1, "regexp:%s", regexp); clicon_debug(1, "content:%s", content); if (mode == 0){ - if ((ret = regex_posix(regexp, content, nr, debug)) < 0) + if ((ret = regex_posix(regexp, content, nr, dbg)) < 0) goto done; } else if (mode == 1){ - if ((ret = regex_libxml2(regexp, content, nr, debug)) < 0) + if ((ret = regex_libxml2(regexp, content, nr, dbg)) < 0) goto done; } else diff --git a/util/clixon_util_socket.c b/util/clixon_util_socket.c index 101221a2..e51238e2 100644 --- a/util/clixon_util_socket.c +++ b/util/clixon_util_socket.c @@ -95,6 +95,7 @@ main(int argc, int ret; cbuf *cb = cbuf_new(); clicon_handle h; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR); @@ -110,7 +111,7 @@ main(int argc, usage(argv[0]); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv[0]); break; case 's': @@ -129,7 +130,9 @@ main(int argc, usage(argv[0]); break; } - clicon_log_init(__FILE__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__FILE__, dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); + if (sockpath == NULL){ fprintf(stderr, "Mandatory option missing: -s \n"); usage(argv[0]); diff --git a/util/clixon_util_stream.c b/util/clixon_util_stream.c index 48dbecf7..38c1ff19 100644 --- a/util/clixon_util_stream.c +++ b/util/clixon_util_stream.c @@ -219,6 +219,7 @@ main(int argc, char **argv) int c; char *argv0 = argv[0]; struct timeval now; + int dbg = 0; clicon_log_init("xpath", LOG_DEBUG, CLICON_LOG_STDERR); gettimeofday(&now, NULL); @@ -230,7 +231,7 @@ main(int argc, char **argv) usage(argv0); break; case 'D': - debug++; + dbg = 1; break; case 'u': /* URL */ url = optarg; @@ -264,6 +265,7 @@ main(int argc, char **argv) usage(argv[0]); break; } + clicon_debug_init(dbg, NULL); if (url == NULL) usage(argv[0]); curl_global_init(0); diff --git a/util/clixon_util_xml.c b/util/clixon_util_xml.c index ddc8d3cd..6693df8c 100644 --- a/util/clixon_util_xml.c +++ b/util/clixon_util_xml.c @@ -163,6 +163,7 @@ main(int argc, cxobj *xbot; /* Place in xtop where base cxobj is parsed */ cvec *nsc = NULL; yang_bind yb; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR); @@ -184,7 +185,7 @@ main(int argc, usage(argv[0]); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv[0]); break; case 'f': @@ -239,7 +240,9 @@ main(int argc, fprintf(stderr, "-t requires -T\n"); usage(argv[0]); } - clicon_log_init(__FILE__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__FILE__, dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); + /* 1. Parse yang */ if (yang_file_dir){ if ((yspec = yspec_new()) == NULL) @@ -319,7 +322,7 @@ main(int argc, } /* Dump data structures (for debug) */ - if (debug){ + if (clicon_debug_get()){ cbuf_reset(cb); xmltree2cbuf(cb, xt, 0); fprintf(stderr, "%s\n", cbuf_get(cb)); diff --git a/util/clixon_util_xml_mod.c b/util/clixon_util_xml_mod.c index f2eae659..010f64ea 100644 --- a/util/clixon_util_xml_mod.c +++ b/util/clixon_util_xml_mod.c @@ -129,6 +129,7 @@ main(int argc, char **argv) clicon_handle h; enum opx opx = OPX_ERROR; char *reason = NULL; + int dbg = 0; clicon_log_init("clixon_insert", LOG_DEBUG, CLICON_LOG_STDERR); if ((h = clicon_handle_init()) == NULL) @@ -141,7 +142,7 @@ main(int argc, char **argv) usage(argv0); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv0); break; case 'o': /* Operation */ @@ -171,6 +172,7 @@ main(int argc, char **argv) usage(argv0); if (opx == OPX_ERROR) usage(argv0); + clicon_debug_init(dbg, NULL); if ((yspec = yspec_new()) == NULL) goto done; if (yang_spec_parse_file(h, yangfile, yspec) < 0) @@ -189,7 +191,7 @@ main(int argc, char **argv) clicon_err(OE_XML, 0, "xpath: %s not found in x0", xpath); goto done; } - if (debug){ + if (clicon_debug_get()){ clicon_debug(1, "xb:"); xml_print(stderr, xb); } @@ -256,7 +258,7 @@ main(int argc, char **argv) default: usage(argv0); } - if (debug){ + if (clicon_debug_get()){ clicon_debug(1, "x0:"); xml_print(stderr, x0); } diff --git a/util/clixon_util_xpath.c b/util/clixon_util_xpath.c index f8afdb71..1513633d 100644 --- a/util/clixon_util_xpath.c +++ b/util/clixon_util_xpath.c @@ -140,6 +140,7 @@ main(int argc, cxobj *xcfg = NULL; cbuf *cbret = NULL; cxobj *xerr = NULL; /* malloced must be freed */ + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init("xpath", LOG_DEBUG, CLICON_LOG_STDERR); @@ -160,7 +161,7 @@ main(int argc, usage(argv0); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv0); break; case 'f': /* XML file */ @@ -210,6 +211,8 @@ main(int argc, usage(argv[0]); break; } + clicon_debug_init(dbg, NULL); + /* Parse yang */ if (yang_file_dir){ if ((yspec = yspec_new()) == NULL) diff --git a/util/clixon_util_yang.c b/util/clixon_util_yang.c index 7918cac3..d3261ab6 100644 --- a/util/clixon_util_yang.c +++ b/util/clixon_util_yang.c @@ -81,6 +81,7 @@ main(int argc, char **argv) yang_stmt *yspec = NULL; int c; int logdst = CLICON_LOG_STDERR; + int dbg = 0; optind = 1; opterr = 0; @@ -90,7 +91,7 @@ main(int argc, char **argv) usage(argv[0]); break; case 'D': - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(argv[0]); break; case 'l': /* Log destination: s|e|o|f */ @@ -101,7 +102,8 @@ main(int argc, char **argv) usage(argv[0]); break; } - clicon_log_init("clixon_util_yang", debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init("clixon_util_yang", dbg?LOG_DEBUG:LOG_INFO, logdst); + clicon_debug_init(dbg, NULL); if ((yspec = yspec_new()) == NULL) goto done; if (yang_parse_file(0, "yang test", yspec) == NULL){ From e2d9c046af990b4ea8471a7fe1488ac0943f201e Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 11 Jun 2020 21:54:09 +0200 Subject: [PATCH 2/5] further restconf refactoring --- apps/backend/backend_main.c | 2 +- apps/restconf/Makefile.in | 25 +- apps/restconf/restconf_api.c | 66 +++++ apps/restconf/restconf_api.h | 54 ++++ apps/restconf/restconf_api_evhtp.c | 183 ++++++++++++ apps/restconf/restconf_api_fcgi.c | 217 ++++++++++++++ apps/restconf/restconf_evhtp_main.c | 425 +++++++++++++++++++++------- apps/restconf/restconf_fcgi_lib.c | 3 +- apps/restconf/restconf_fcgi_lib.h | 8 +- apps/restconf/restconf_fcgi_main.c | 146 ++++++---- apps/restconf/restconf_lib.c | 83 ++++++ apps/restconf/restconf_lib.h | 17 +- apps/restconf/restconf_root.c | 118 ++++++++ apps/restconf/restconf_root.h | 57 ++++ configure.ac | 1 + test/lib.sh | 16 +- test/test_restconf.sh | 112 ++++---- test/test_restconf2.sh | 64 ++--- test/test_restconf_err.sh | 30 +- test/test_restconf_jukebox.sh | 92 +++--- test/test_restconf_listkey.sh | 48 ++-- test/test_restconf_patch.sh | 48 ++-- test/test_restconf_startup.sh | 8 +- 23 files changed, 1424 insertions(+), 399 deletions(-) create mode 100644 apps/restconf/restconf_api.c create mode 100644 apps/restconf/restconf_api.h create mode 100644 apps/restconf/restconf_api_evhtp.c create mode 100644 apps/restconf/restconf_api_fcgi.c create mode 100644 apps/restconf/restconf_root.c create mode 100644 apps/restconf/restconf_root.h diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 7afe54af..7afa8091 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -297,7 +297,7 @@ check_drop_priv(clicon_handle h, clicon_err(OE_DAEMON, EPERM, "Privileges can only be dropped from root user (uid is %u)\n", uid); goto done; } - /* When dropping priveleges, datastores are created if they do not exist. + /* When dropping privileges, datastores are created if they do not exist. * But when drops are not made, datastores are created on demand. * XXX: move the creation to top-level so they are always created at init? */ diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 92a9124b..2804f70d 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -78,35 +78,30 @@ CPPFLAGS = @CPPFLAGS@ -fPIC INCLUDES = -I. -I$(top_srcdir)/lib/src -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ -# Applications -ifeq ($(with_restconf),fcgi) -APPL = clixon_restconf # fcgi / nginx -else -APPL = clixon_restconf_$(with_restconf) -endif +# Application +APPL = clixon_restconf # Common source - not accessible from plugin - independent of restconf package (fcgi|evhtp) -#APPSRC = restconf_lib.c +APPSRC = +APPSRC += restconf_api.c # maybe empty +APPSRC += restconf_api_$(with_restconf).c # cant be .so since libevhtp is a. +APPSRC += restconf_root.c +APPSRC += restconf_$(with_restconf)_main.c # Fcgi-specific source including main ifeq ($(with_restconf),fcgi) -APPSRC = restconf_fcgi_lib.c -APPSRC += restconf_fcgi_main.c -APPSRC += restconf_methods.c # These should be moved ^ +APPSRC += restconf_fcgi_lib.c # Most of these should be made generic +APPSRC += restconf_methods.c APPSRC += restconf_methods_post.c APPSRC += restconf_methods_get.c APPSRC += restconf_stream.c endif -# Evhtp-specific source including main -ifeq ($(with_restconf),evhtp) -APPSRC = restconf_evhtp_main.c -endif - APPOBJ = $(APPSRC:.c=.o) # Accessible from plugin LIBSRC = restconf_lib.c + LIBOBJ = $(LIBSRC:.c=.o) # This lib is very small but used for clixon restconf applications to access clixon restconf lib diff --git a/apps/restconf/restconf_api.c b/apps/restconf/restconf_api.c new file mode 100644 index 00000000..26638858 --- /dev/null +++ b/apps/restconf/restconf_api.c @@ -0,0 +1,66 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * Generic restconf API functions + */ + + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* chmod */ + +/* cligen */ +#include + +/* clicon */ +#include + +/* restconf */ +#include "restconf_lib.h" +#include "restconf_api.h" + diff --git a/apps/restconf/restconf_api.h b/apps/restconf/restconf_api.h new file mode 100644 index 00000000..50084654 --- /dev/null +++ b/apps/restconf/restconf_api.h @@ -0,0 +1,54 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * + * Virtual clixon restconf API functions. + */ + +#ifndef _RESTCONF_API_H_ +#define _RESTCONF_API_H_ + +/* + * Prototypes + */ +int restconf_reply_status_code(void *req, int code); + +#if defined(__GNUC__) && __GNUC__ >= 3 +int restconf_reply_header_add(void *req, char *name, char *vfmt, ...) __attribute__ ((format (printf, 3, 4))); +#else +int restconf_reply_header_add(FCGX_Request *req, char *name, char *vfmt, ...); +#endif + +int restconf_reply_send(void *req, cbuf *cb); + +#endif /* _RESTCONF_API_H_ */ diff --git a/apps/restconf/restconf_api_evhtp.c b/apps/restconf/restconf_api_evhtp.c new file mode 100644 index 00000000..be3d4aa6 --- /dev/null +++ b/apps/restconf/restconf_api_evhtp.c @@ -0,0 +1,183 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2020 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Concrete functions for libevhtp of the + * Virtual clixon restconf API functions. + * @see restconf_api.h for virtual API + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* evhtp */ +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include "restconf_lib.h" +#include "restconf_api.h" /* Virtual api */ + + +/*! Add HTTP header field name and value to reply, evhtp specific + * @param[in] req Evhtp http request handle + * @param[in] code HTTP status code + * @see eg RFC 7230 + */ +int +restconf_reply_status_code(void *req0, + int code) +{ + evhtp_request_t *req = (evhtp_request_t *)req0; + + req->status = code; + return 0; +} + +/*! Add HTTP header field name and value to reply, evhtp specific + * @param[in] req Evhtp http request handle + * @param[in] name HTTP header field name + * @param[in] vfmt HTTP header field value format string w variable parameter + * @see eg RFC 7230 + */ +int +restconf_reply_header_add(void *req0, + char *name, + char *vfmt, + ...) + +{ + evhtp_request_t *req = (evhtp_request_t *)req0; + int retval = -1; + size_t vlen; + char *value = NULL; + va_list ap; + evhtp_header_t *evhdr; + + if (req == NULL || name == NULL || vfmt == NULL){ + clicon_err(OE_CFG, EINVAL, "req, name or value is NULL"); + return -1; + } + va_start(ap, vfmt); + vlen = vsnprintf(NULL, 0, vfmt, ap); + va_end(ap); + /* allocate value string exactly fitting */ + if ((value = malloc(vlen+1)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + /* second round: compute actual value */ + va_start(ap, vfmt); + if (vsnprintf(value, vlen+1, vfmt, ap) < 0){ + clicon_err(OE_UNIX, errno, "vsnprintf"); + va_end(ap); + goto done; + } + va_end(ap); + if ((evhdr = evhtp_header_new(name, value, 0, 1)) == NULL){ /* 1: free after use */ + clicon_err(OE_CFG, errno, "evhttp_header_new"); + goto done; + } + value = NULL; /* freed by evhtp */ + evhtp_headers_add_header(req->headers_out, evhdr); + retval = 0; + done: + if (value) + free(value); + return retval; +} + +/*! Send HTTP reply with potential message body + * @param[in] req Evhtp http request handle + * @param[in] cb Body as a cbuf, send if + * + * Prerequisites: status code set, headers given, body if wanted set + */ +int +restconf_reply_send(void *req0, + cbuf *cb) +{ + evhtp_request_t *req = (evhtp_request_t *)req0; + int retval = -1; + evhtp_connection_t *conn; + struct evbuffer *eb = NULL; + +#if 1 /* Optional? */ + if ((conn = evhtp_request_get_connection(req)) == NULL){ + clicon_err(OE_DAEMON, EFAULT, "evhtp_request_get_connection"); + goto done; + } + htp_sslutil_add_xheaders(req->headers_out, conn->ssl, HTP_SSLUTILS_XHDR_ALL); +#endif + + /* create evbuffer* : bufferevent_write_buffer/ drain, + ie send everything , except body */ + evhtp_send_reply_start(req, req->status); + + /* Write a body if cbuf is nonzero */ + if (cb != NULL && cbuf_len(cb)){ + /* Suboptimal, copy from cbuf to evbuffer */ + if ((eb = evbuffer_new()) == NULL){ + clicon_err(OE_CFG, errno, "evbuffer_new"); + goto done; + } + if (evbuffer_add(eb, cbuf_get(cb), cbuf_len(cb)) < 0){ + clicon_err(OE_CFG, errno, "evbuffer_add"); + goto done; + } + evhtp_send_reply_body(req, eb); /* conn->bev = eb, body is different */ + } + evhtp_send_reply_end(req); /* just flag finished */ + retval = 0; + done: + if (eb) + evhtp_safe_free(eb, evbuffer_free); + return retval; +} diff --git a/apps/restconf/restconf_api_fcgi.c b/apps/restconf/restconf_api_fcgi.c new file mode 100644 index 00000000..ef5be280 --- /dev/null +++ b/apps/restconf/restconf_api_fcgi.c @@ -0,0 +1,217 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2020 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Concrete functions for FCGI of the + * Virtual clixon restconf API functions. + * @see restconf_api.h for virtual API + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml-h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_api.h" /* Virtual api */ + +/*! Add HTTP header field name and value to reply, fcgi specific + * @param[in] req Fastcgi request handle + * @param[in] code HTTP status code + * @see eg RFC 7230 + */ +int +restconf_reply_status_code(void *req0, + int code) +{ + FCGX_Request *req = (FCGX_Request *)req0; + + FCGX_SetExitStatus(code, req->out); + return 0; +} + +/*! HTTP headers done, if there is a message body coming next + * @param[in] req Fastcgi request handle + * @retval body Handle for body handling (in fcgi same as req) + * + * HTTP-message = start-line *( header-field CRLF ) CRLF [ message-body ] + * @see eg RFC 7230 + * XXX may be unecessary (or body start or something) + */ +FCGX_Request * +restconf_reply_body_start(void *req0) +{ + FCGX_Request *req = (FCGX_Request *)req0; + + FCGX_FPrintF(req->out, "\r\n"); + return req; +} + +/*! Add HTTP header field name and value to reply, fcgi specific + * @param[in] req Fastcgi request handle + * @param[in] name HTTP header field name + * @param[in] vfmt HTTP header field value format string w variable parameter + * @see eg RFC 7230 + */ +int +restconf_reply_header_add(void *req0, + char *name, + char *vfmt, + ...) + +{ + FCGX_Request *req = (FCGX_Request *)req0; + int retval = -1; + size_t vlen; + char *value = NULL; + va_list ap; + + if (req == NULL || name == NULL || vfmt == NULL){ + clicon_err(OE_CFG, EINVAL, "req, name or value is NULL"); + return -1; + } + va_start(ap, vfmt); + vlen = vsnprintf(NULL, 0, vfmt, ap); + va_end(ap); + /* allocate value string exactly fitting */ + if ((value = malloc(vlen+1)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + /* second round: compute actual value */ + va_start(ap, vfmt); + if (vsnprintf(value, vlen+1, vfmt, ap) < 0){ + clicon_err(OE_UNIX, errno, "vsnprintf"); + va_end(ap); + goto done; + } + va_end(ap); + FCGX_FPrintF(req->out, "%s: %s\r\n", name, value); + retval = 0; + done: + if (value) + free(value); + return retval; +} + +/*! Add HTTP message body to reply, fcgi specific + * @param[in] req Fastcgi request handle + * @param[in,out] content_len This is for Content-Length header + * @param[in] bfmt HTTP message body format string w variable parameter + * @see eg RFC 7230 + */ +int +restconf_reply_body_add(void *req0, + size_t *content_len, + char *bfmt, + ...) +{ + FCGX_Request *req = (FCGX_Request *)req0; + int retval = -1; + size_t sz; + size_t blen; + char *body = NULL; + va_list ap; + + if (req == NULL || bfmt == NULL){ + clicon_err(OE_CFG, EINVAL, "req or body is NULL"); + return -1; + } + va_start(ap, bfmt); + blen = vsnprintf(NULL, 0, bfmt, ap); + va_end(ap); + /* allocate body string exactly fitting */ + if ((body = malloc(blen+1)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + /* second round: compute actual body */ + va_start(ap, bfmt); + if (vsnprintf(body, blen+1, bfmt, ap) < 0){ + clicon_err(OE_UNIX, errno, "vsnprintf"); + va_end(ap); + goto done; + } + va_end(ap); + FCGX_FPrintF(req->out, "%s", body); + /* Increment in/out Content-Length parameter */ + if (content_len){ + sz = strlen(body); + *content_len += sz; + } + retval = 0; + done: + if (body) + free(body); + return retval; +} + +/*! Send HTTP reply with potential message body + * @param[in] req Fastcgi request handle + * @param[in] cb Body as a cbuf, send if + * + * Prerequisites: status code set, headers given, body if wanted set + */ +int +restconf_reply_send(void *req0, + cbuf *cb) +{ + FCGX_Request *req = (FCGX_Request *)req0; + int retval = -1; + + /* Write a body if cbuf is nonzero */ + if (cb != NULL && cbuf_len(cb)){ + FCGX_FPrintF(req->out, "\r\n"); + FCGX_FPrintF(req->out, "%s", cbuf_get(cb)); + } + retval = 0; + return retval; +} diff --git a/apps/restconf/restconf_evhtp_main.c b/apps/restconf/restconf_evhtp_main.c index da646841..2cdcd7e1 100644 --- a/apps/restconf/restconf_evhtp_main.c +++ b/apps/restconf/restconf_evhtp_main.c @@ -31,7 +31,8 @@ the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** - + + * libevhtp code */ /* XXX temp constant should go away, */ @@ -43,7 +44,7 @@ /* compilation withotu threading support * XXX: could be disabled already in configure? */ -#define EVHTP_DISABLE_EVTHR +//#define EVHTP_DISABLE_EVTHR #define EVHTP_DISABLE_REGEX #include @@ -73,13 +74,9 @@ /* restconf */ -#include "restconf_lib.h" -#if 0 /* These are all dependent on FCGX */ -#include "restconf_methods.h" -#include "restconf_methods_get.h" -#include "restconf_methods_post.h" -#include "restconf_stream.h" -#endif +#include "restconf_lib.h" /* generic shared with plugins */ +#include "restconf_api.h" /* generic not shared with plugins */ +#include "restconf_root.h" /* Command line options to be passed to getopt(3) */ #define RESTCONF_OPTS "hD:f:l:p:d:y:a:u:o:P:c:k:" @@ -122,6 +119,178 @@ restconf_sig_child(int arg) } } +static char* +evhtp_method2str(enum htp_method m) +{ + switch (m){ + case htp_method_GET: + return "GET"; + break; + case htp_method_HEAD: + return "HEAD"; + break; + case htp_method_POST: + return "POST"; + break; + case htp_method_PUT: + return "PUT"; + break; + case htp_method_DELETE: + return "DELETE"; + break; + case htp_method_PATCH: + return "PATCH"; + break; + default: + return "XXX"; + break; + } +} + +static int +query_iterator(evhtp_header_t *hdr, + void *arg) +{ + cvec *qvec = (cvec *)arg; + char *key; + char *val; + char *valu = NULL; /* unescaped value */ + cg_var *cv; + + key = hdr->key; + val = hdr->val; + if (uri_percent_decode(val, &valu) < 0) + return -1; + if ((cv = cvec_add(qvec, CGV_STRING)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_add"); + return -1; + } + cv_name_set(cv, key); + cv_string_set(cv, valu); + if (valu) + free(valu); + return 0; +} + +/*! Map from evhtp information to "fcgi" type parameters used in clixon code + * + * While all these params come via one call in fcgi, the information must be taken from + * several different places in evhtp + * @param[in] h Clicon handle + * @param[in] req Evhtp request struct + * @retval 0 OK + * @retval -1 Error + * The following parameters are set: + * QUERY_STRING + * REQUEST_METHOD + * REQUEST_URI + * HTTPS + * HTTP_HOST + * HTTP_ACCEPT + * HTTP_CONTENT_TYPE + * @note there may be more used by an application plugin + */ +static int +evhtp_params_set(clicon_handle h, + evhtp_request_t *req, + cvec *qvec) +{ + int retval = -1; + htp_method meth; + evhtp_uri_t *uri; + evhtp_path_t *path; + evhtp_header_t *hdr; + + if ((uri = req->uri) == NULL){ + clicon_err(OE_DAEMON, EFAULT, "No uri"); + goto done; + } + if ((path = uri->path) == NULL){ + clicon_err(OE_DAEMON, EFAULT, "No path"); + goto done; + } + meth = evhtp_request_get_method(req); + + /* QUERY_STRING */ + if (qvec && uri->query) + if (evhtp_kvs_for_each(uri->query, query_iterator, qvec) < 0){ + clicon_err(OE_CFG, errno, "evhtp_kvs_for_each"); + goto done; + } + + if (clixon_restconf_param_set(h, "REQUEST_METHOD", evhtp_method2str(meth)) < 0) + goto done; + if (clixon_restconf_param_set(h, "REQUEST_URI", path->full) < 0) + goto done; + if (clixon_restconf_param_set(h, "HTTPS", "https") < 0) /* some string or NULL */ + goto done; + if ((hdr = evhtp_headers_find_header(req->headers_in, "Host")) != NULL){ + if (clixon_restconf_param_set(h, "HTTP_HOST", hdr->val) < 0) + goto done; + } + if ((hdr = evhtp_headers_find_header(req->headers_in, "Accept")) != NULL){ + if (clixon_restconf_param_set(h, "HTTP_ACCEPT", hdr->val) < 0) + goto done; + } + if ((hdr = evhtp_headers_find_header(req->headers_in, "Content-Type")) != NULL){ + if (clixon_restconf_param_set(h, "HTTP_CONTENT_TYPE", hdr->val) < 0) + goto done; + } + retval = 0; + done: + return retval; +} + +static int +evhtp_params_clear(clicon_handle h) +{ + int retval = -1; + char *params[] = {"QUERY_STRING", "REQUEST_METHOD", "REQUEST_URI", + "HTTPS", "HTTP_HOST", "HTTP_ACCEPT", "HTTP_CONTENT_TYPE", NULL}; + char *param; + int i=0; + + while((param=params[i]) != NULL) + if (clixon_restconf_param_del(h, param) < 0) + goto done; + retval = 0; + done: + return retval; +} + +static int +print_header(evhtp_header_t *header, + void *arg) +{ + // clicon_handle h = (clicon_handle)arg; + + clicon_debug(1, "%s %s %s", + __FUNCTION__, header->key, header->val); + return 0; +} + +static evhtp_res +cx_pre_accept(evhtp_connection_t *conn, + void *arg) +{ + // clicon_handle h = (clicon_handle)arg; + + clicon_debug(1, "%s", __FUNCTION__); + return EVHTP_RES_OK; +} + +static evhtp_res +cx_post_accept(evhtp_connection_t *conn, + void *arg) +{ + // clicon_handle h = (clicon_handle)arg; + + clicon_debug(1, "%s", __FUNCTION__); + return EVHTP_RES_OK; +} + +/*! Generic callback called if no other callbacks are matched + */ static void cx_gencb(evhtp_request_t *req, void *arg) @@ -129,7 +298,7 @@ cx_gencb(evhtp_request_t *req, evhtp_connection_t *conn; // clicon_handle h = arg; - fprintf(stderr, "%s\n", __FUNCTION__); + clicon_debug(1, "%s", __FUNCTION__); if (req == NULL){ errno = EINVAL; return; @@ -145,67 +314,105 @@ cx_gencb(evhtp_request_t *req, return; /* void */ } -static evhtp_res -cx_pre_accept(evhtp_connection_t *conn, - void *arg) +/*! /.well-known callback + * @see cx_genb + */ +static void +cx_path_wellknown(evhtp_request_t *req, + void *arg) { - fprintf(stderr, "%s\n", __FUNCTION__); - return EVHTP_RES_OK; + clicon_handle h = arg; + + /* input debug */ + if (clicon_debug_get()) + evhtp_headers_for_each(req->headers_in, print_header, h); + /* get accepted connection */ + + /* set fcgi-like paramaters (ignore query vector) */ + if (evhtp_params_set(h, req, NULL) < 0) + goto done; + /* call generic function */ + if (api_well_known(h, req) < 0) + goto done; + + /* Clear (fcgi) paramaters from this request */ + if (evhtp_params_clear(h) < 0) + goto done; + done: + return; /* void */ } -static evhtp_res -cx_post_accept(evhtp_connection_t *conn, - void *arg) -{ - fprintf(stderr, "%s\n", __FUNCTION__); - return EVHTP_RES_OK; -} - -static int -print_header_(evhtp_header_t * header, void * arg) { - fprintf(stderr, "%s: %s\n", header->key, header->val); - return 0; -} - -/*! Generic callback called if no other callbacks are matched +/*! /restconf callback + * @see cx_genb */ static void cx_path_restconf(evhtp_request_t *req, void *arg) { evhtp_connection_t *conn; - // clicon_handle h = arg; + clicon_handle h = arg; struct evbuffer *b = NULL; - htp_method meth; + cvec *qvec = NULL; + size_t len = 0; + cbuf *cblen = NULL; - fprintf(stderr, "%s\n", __FUNCTION__); + clicon_debug(1, "%s", __FUNCTION__); if (req == NULL){ errno = EINVAL; goto done; } - if ((conn = evhtp_request_get_connection(req)) == NULL) - goto done; - meth = evhtp_request_get_method(req); - fprintf(stderr, "%s method:%d\n", __FUNCTION__, meth); - evhtp_headers_for_each(req->headers_in, print_header_, NULL); + /* input debug */ + if (clicon_debug_get()) + evhtp_headers_for_each(req->headers_in, print_header, h); - if ((b = evbuffer_new()) == NULL){ + if ((cblen = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } - htp_sslutil_add_xheaders( - req->headers_out, - conn->ssl, - HTP_SSLUTILS_XHDR_ALL); + /* Query vector, ie the ?a=x&b=y stuff */ + if ((qvec = cvec_new(0)) ==NULL){ + clicon_err(OE_UNIX, errno, "cvec_new"); + goto done; + } + /* get accepted connection */ + if ((conn = evhtp_request_get_connection(req)) == NULL){ + clicon_err(OE_DAEMON, EFAULT, "evhtp_request_get_connection"); + goto done; + } + /* Get all parameters from this request (resembling fcgi) */ + if (evhtp_params_set(h, req, qvec) < 0) + goto done; + + /* 1. create body */ + if ((b = evbuffer_new()) == NULL){ + clicon_err(OE_DAEMON, errno, "evbuffer_new"); + goto done; + } + cprintf(cblen, "%lu", len); + + /* 2. add headers (can mix with body) */ + evhtp_headers_add_header(req->headers_out, evhtp_header_new("Cache-Control", "no-cache", 0, 0)); + evhtp_headers_add_header(req->headers_out, evhtp_header_new("Content-Type", "application/xrd+xml", 0, 0)); + evhtp_headers_add_header(req->headers_out, evhtp_header_new("Content-Length", cbuf_get(cblen), 0, 0)); + + /* Optional? */ + htp_sslutil_add_xheaders(req->headers_out, conn->ssl, HTP_SSLUTILS_XHDR_ALL); + + /* 3. send reply */ evhtp_send_reply_start(req, EVHTP_RES_OK); - evbuffer_add(b, "hej\n", strlen("hej\n\n")); evhtp_send_reply_body(req, b); evhtp_send_reply_end(req); - // evhtp_headers_add_header(request->headers_out, evhtp_header_new("Host", "localhost", 0, 0)); evhtp_headers_add_headers(request->headers_out, headers); - - - + /* Clear (fcgi)paramaters */ + if (evhtp_params_clear(h) < 0) + goto done; done: + if (qvec) + cvec_free(qvec); + if (cblen) + cbuf_free(cblen); + if (b) + evhtp_safe_free(b, evbuffer_free); return; /* void */ } @@ -246,26 +453,27 @@ int main(int argc, char **argv) { - int retval = -1; - char *argv0 = argv[0]; - int c; - clicon_handle h; - char *dir; - int logdst = CLICON_LOG_SYSLOG; - yang_stmt *yspec = NULL; - char *str; - clixon_plugin *cp = NULL; - cvec *nsctx_global = NULL; /* Global namespace context */ - size_t cligen_buflen; - size_t cligen_bufthreshold; - uint16_t port = 443; + int retval = -1; + char *argv0 = argv[0]; + int c; + clicon_handle h; + char *dir; + int logdst = CLICON_LOG_SYSLOG; + yang_stmt *yspec = NULL; + char *str; + clixon_plugin *cp = NULL; + cvec *nsctx_global = NULL; /* Global namespace context */ + size_t cligen_buflen; + size_t cligen_bufthreshold; + uint16_t port = 443; #ifdef _EVHTP_NYI - char *stream_path; + char *stream_path; #endif evhtp_t *htp = NULL; struct event_base *evbase = NULL; evhtp_ssl_cfg_t *ssl_config = NULL; struct stat f_stat; + int dbg = 0; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, logdst); @@ -282,7 +490,7 @@ main(int argc, usage(h, argv0); break; case 'D' : /* debug */ - if (sscanf(optarg, "%d", &debug) != 1) + if (sscanf(optarg, "%d", &dbg) != 1) usage(h, argv0); break; case 'f': /* override config file */ @@ -303,9 +511,9 @@ main(int argc, /* * Logs, error and debug to stderr or syslog, set debug level */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, logdst); + clicon_log_init(__PROGRAM__, dbg?LOG_DEBUG:LOG_INFO, logdst); - clicon_debug_init(debug, NULL); + clicon_debug_init(dbg, NULL); clicon_log(LOG_NOTICE, "%s: %u Started", __PROGRAM__, getpid()); if (set_signal(SIGTERM, restconf_sig_term, NULL) < 0){ clicon_err(OE_DAEMON, errno, "Setting signal"); @@ -390,7 +598,6 @@ main(int argc, } argc -= optind; argv += optind; - /* Check ssl mandatory options */ if (ssl_config->pemfile == NULL || ssl_config->privfile == NULL) usage(h, argv0); @@ -412,6 +619,53 @@ main(int argc, /* Access the remaining argv/argc options (after --) w clicon-argv_get() */ clicon_argv_set(h, argv0, argc, argv); + /* Init evhtp */ + if ((evbase = event_base_new()) == NULL){ + clicon_err(OE_UNIX, errno, "event_base_new"); + goto done; + } + /* create a new evhtp_t instance */ + if ((htp = evhtp_new(evbase, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + if (evhtp_ssl_init(htp, ssl_config) < 0){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } +#ifndef EVHTP_DISABLE_EVTHR + evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL); +#endif + + /* Callback before the connection is accepted. */ + evhtp_set_pre_accept_cb(htp, cx_pre_accept, h); + + /* Callback right after a connection is accepted. */ + evhtp_set_post_accept_cb(htp, cx_post_accept, h); + + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Generic callback called if no other callbacks are matched */ + evhtp_set_gencb(htp, cx_gencb, h); + + /* bind to a socket, optionally with specific protocol support formatting + * If port is proteced must be done as root? + */ + if (evhtp_bind_socket(htp, "127.0.0.1", port, 128) < 0){ + clicon_err(OE_UNIX, errno, "evhtp_bind_socket"); + goto done; + } + if (restconf_drop_privileges(h, WWWUSER) < 0) + goto done; + /* Init cligen buffers */ cligen_buflen = clicon_option_int(h, "CLICON_CLI_BUF_START"); cligen_bufthreshold = clicon_option_int(h, "CLICON_CLI_BUF_THRESHOLD"); @@ -422,7 +676,6 @@ main(int argc, */ if (netconf_module_features(h) < 0) goto done; - /* Create top-level yang spec and store as option */ if ((yspec = yspec_new()) == NULL) goto done; @@ -491,8 +744,8 @@ main(int argc, goto done; /* Dump configuration options on debug */ - if (debug) - clicon_option_dump(h, debug); + if (dbg) + clicon_option_dump(h, dbg); /* Call start function in all plugins before we go interactive */ @@ -503,40 +756,6 @@ main(int argc, if (clicon_options_main(h) < 0) goto done; - /* Init evhtp */ - if ((evbase = event_base_new()) == NULL){ - clicon_err(OE_UNIX, errno, "event_base_new"); - goto done; - } - /* create a new evhtp_t instance */ - if ((htp = evhtp_new(evbase, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - if (evhtp_ssl_init(htp, ssl_config) < 0){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - /* Generic callback called if no other callbacks are matched */ - evhtp_set_gencb(htp, cx_gencb, h); - - /* Callback before the connection is accepted. */ - evhtp_set_pre_accept_cb(htp, cx_pre_accept, h); - - /* Callback right after a connection is accepted. */ - evhtp_set_post_accept_cb(htp, cx_post_accept, h); - - /* Callback to be executed on a specific path */ - if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ - clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); - goto done; - } - - /* bind to a socket, optionally with specific protocol support formatting */ - if (evhtp_bind_socket(htp, "127.0.0.1", port, 128) < 0){ - clicon_err(OE_UNIX, errno, "evhtp_bind_socket"); - goto done; - } event_base_loop(evbase, 0); diff --git a/apps/restconf/restconf_fcgi_lib.c b/apps/restconf/restconf_fcgi_lib.c index 1ad9678c..5ed0d86b 100644 --- a/apps/restconf/restconf_fcgi_lib.c +++ b/apps/restconf/restconf_fcgi_lib.c @@ -194,7 +194,7 @@ restconf_internal_server_error(clicon_handle h, FCGX_Request *r) { char *path; - + clicon_debug(1, "%s", __FUNCTION__); path = clixon_restconf_param_get(h, "REQUEST_URI"); FCGX_FPrintF(r->out, "Status: 500 Internal Server Error\r\n"); /* 500 internal server error */ @@ -478,4 +478,3 @@ http_location(clicon_handle h, return retval; } - diff --git a/apps/restconf/restconf_fcgi_lib.h b/apps/restconf/restconf_fcgi_lib.h index 23142089..af7a7910 100644 --- a/apps/restconf/restconf_fcgi_lib.h +++ b/apps/restconf/restconf_fcgi_lib.h @@ -49,12 +49,12 @@ int restconf_conflict(FCGX_Request *r); int restconf_unsupported_media(FCGX_Request *r); int restconf_internal_server_error(clicon_handle h, FCGX_Request *r); int restconf_notimplemented(FCGX_Request *r); -int restconf_test(FCGX_Request *r, int dbg); -int clixon_restconf_params_set(clicon_handle h, char **envp); +int restconf_test(FCGX_Request *r, int dbg); +int clixon_restconf_params_set(clicon_handle h, + char **envp); int clixon_restconf_params_clear(clicon_handle h, char **envp); cbuf *readdata(FCGX_Request *r); -int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, - int pretty, enum restconf_media media, int code); +int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int pretty, restconf_media media, int code0); int http_location(clicon_handle h, FCGX_Request *r, cxobj *xobj); #endif /* _RESTCONF_FCGI_LIB_H_ */ diff --git a/apps/restconf/restconf_fcgi_main.c b/apps/restconf/restconf_fcgi_main.c index 24f45c7c..fa4422be 100644 --- a/apps/restconf/restconf_fcgi_main.c +++ b/apps/restconf/restconf_fcgi_main.c @@ -77,9 +77,12 @@ #include /* Need to be after clixon_xml.h due to attribute format */ /* restconf */ -#include "restconf_lib.h" -#include "restconf_fcgi_lib.h" -#include "restconf_methods.h" +#include "restconf_lib.h" /* generic shared with plugins */ +#include "restconf_api.h" /* generic not shared with plugins */ + +#include "restconf_root.h" /* generic not shared with plugins */ +#include "restconf_fcgi_lib.h" /* fcgi specific */ +#include "restconf_methods.h" /* fcgi specific */ #include "restconf_methods_get.h" #include "restconf_methods_post.h" #include "restconf_stream.h" @@ -101,7 +104,7 @@ */ static int api_data(clicon_handle h, - FCGX_Request *r, + FCGX_Request *req, char *api_path, cvec *pcvec, int pi, @@ -117,21 +120,21 @@ api_data(clicon_handle h, request_method = clixon_restconf_param_get(h, "REQUEST_METHOD"); clicon_debug(1, "%s method:%s", __FUNCTION__, request_method); if (strcmp(request_method, "OPTIONS")==0) - retval = api_data_options(h, r); + retval = api_data_options(h, req); else if (strcmp(request_method, "HEAD")==0) - retval = api_data_head(h, r, api_path, pcvec, pi, qvec, pretty, media_out); + retval = api_data_head(h, req, api_path, pcvec, pi, qvec, pretty, media_out); else if (strcmp(request_method, "GET")==0) - retval = api_data_get(h, r, api_path, pcvec, pi, qvec, pretty, media_out); + retval = api_data_get(h, req, api_path, pcvec, pi, qvec, pretty, media_out); else if (strcmp(request_method, "POST")==0) - retval = api_data_post(h, r, api_path, pi, qvec, data, pretty, media_out); + retval = api_data_post(h, req, api_path, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "PUT")==0) - retval = api_data_put(h, r, api_path, pcvec, pi, qvec, data, pretty, media_out); + retval = api_data_put(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "PATCH")==0) - retval = api_data_patch(h, r, api_path, pcvec, pi, qvec, data, pretty, media_out); + retval = api_data_patch(h, req, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "DELETE")==0) - retval = api_data_delete(h, r, api_path, pi, pretty, media_out); + retval = api_data_delete(h, req, api_path, pi, pretty, media_out); else - retval = restconf_notfound(h, r); + retval = restconf_notfound(h, req); clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); return retval; } @@ -181,27 +184,44 @@ api_operations(clicon_handle h, * In line with the best practices defined by [RFC7320], RESTCONF * enables deployments to specify where the RESTCONF API is located. */ +#if 0 static int api_well_known(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { - clicon_debug(1, "%s", __FUNCTION__); - FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/xrd+xml\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "\n"); - FCGX_FPrintF(r->out, " \n"); - FCGX_FPrintF(r->out, "\r\n"); + char *request_method; + FCGX_Request *body; + + /* call generic function */ + if (api_well_known(h, req) < 0) + goto done; + clicon_debug(1, "%s", __FUNCTION__); + if (req == NULL){ + errno = EINVAL; + goto done; + } + request_method = clixon_restconf_param_get(h, "REQUEST_METHOD"); + if (strcmp(request_method, "GET") !=0 ) + return restconf_method_notallowed(req, "GET"); + restconf_reply_status_code(req, 200); /* OK */ + restconf_reply_header_add(req, "Cache-Control", "no-cache"); + restconf_reply_header_add(req, "Content-Type", "application/xrd+xml"); + body = restconf_reply_body_start(req); + + restconf_reply_body_add(body, NULL, "\n"); + restconf_reply_body_add(body, NULL, " \n"); + restconf_reply_body_add(body, NULL, "\r\n"); + done: return 0; } - +#endif /*! Retrieve the Top-Level API Resource * @param[in] h Clicon handle * @param[in] r Fastcgi request handle * @note Only returns null for operations and data,... * See RFC8040 3.3 + * XXX doesnt check method */ static int api_root(clicon_handle h, @@ -318,11 +338,11 @@ api_yang_library_version(clicon_handle h, */ static int api_restconf(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { int retval = -1; char *path; - char *query; + char *query = NULL; char *method; char **pvec = NULL; int pn; @@ -357,7 +377,7 @@ api_restconf(clicon_handle h, if (strcmp(media_str, "*/*") == 0) /* catch-all */ media_out = YANG_DATA_JSON; else{ - retval = restconf_unsupported_media(r); + retval = restconf_unsupported_media(req); goto done; } } @@ -367,34 +387,35 @@ api_restconf(clicon_handle h, goto done; /* Sanity check of path. Should be /restconf/ */ if (pn < 2){ - restconf_notfound(h, r); + restconf_notfound(h, req); goto ok; } if (strlen(pvec[0]) != 0){ - retval = restconf_notfound(h, r); + retval = restconf_notfound(h, req); goto done; } if (strcmp(pvec[1], RESTCONF_API)){ - retval = restconf_notfound(h, r); + retval = restconf_notfound(h, req); goto done; } - restconf_test(r, 1); + restconf_test(req, 1); if (pn == 2){ - retval = api_root(h, r, pretty, media_out); + retval = api_root(h, req, pretty, media_out); goto done; } if ((method = pvec[2]) == NULL){ - retval = restconf_notfound(h, r); + retval = restconf_notfound(h, req); goto done; } clicon_debug(1, "%s: method=%s", __FUNCTION__, method); - if (str2cvec(query, '&', '=', &qvec) < 0) - goto done; + if (query != NULL && strlen(query)) + if (str2cvec(query, '&', '=', &qvec) < 0) + goto done; if (str2cvec(path, '/', '=', &pcvec) < 0) /* rest url eg /album=ricky/foo */ goto done; /* data */ - if ((cb = readdata(r)) == NULL) + if ((cb = readdata(req)) == NULL) goto done; data = cbuf_get(cb); clicon_debug(1, "%s DATA=%s", __FUNCTION__, data); @@ -404,7 +425,7 @@ api_restconf(clicon_handle h, /* If present, check credentials. See "plugin_credentials" in plugin * See RFC 8040 section 2.5 */ - if ((authenticated = clixon_plugin_auth_all(h, r)) < 0) + if ((authenticated = clixon_plugin_auth_all(h, req)) < 0) goto done; clicon_debug(1, "%s auth:%d %s", __FUNCTION__, authenticated, clicon_username_get(h)); @@ -417,7 +438,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, NULL, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xerr, pretty, media_out, 0) < 0) + if (api_return_err(h, req, xerr, pretty, media_out, 0) < 0) goto done; goto ok; } @@ -425,23 +446,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, pretty, media_out) < 0) + if (api_yang_library_version(h, req, 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, + if (api_data(h, req, path, pcvec, 2, qvec, data, pretty, media_out) < 0) goto done; } else if (strcmp(method, "operations") == 0){ /* rpc */ - if (api_operations(h, r, path, pcvec, 2, qvec, data, + if (api_operations(h, req, path, pcvec, 2, qvec, data, pretty, media_out) < 0) goto done; } else if (strcmp(method, "test") == 0) - restconf_test(r, 0); + restconf_test(req, 0); else - restconf_notfound(h, r); + restconf_notfound(h, req); ok: retval = 0; done: @@ -534,7 +555,7 @@ main(int argc, int sock; char *argv0 = argv[0]; FCGX_Request request; - FCGX_Request *r = &request; + FCGX_Request *req = &request; int c; char *sockpath; char *path; @@ -664,6 +685,7 @@ main(int argc, cligen_bufthreshold = clicon_option_int(h, "CLICON_CLI_BUF_THRESHOLD"); cbuf_alloc_set(cligen_buflen, cligen_bufthreshold); + /* Add (hardcoded) netconf features in case ietf-netconf loaded here * Otherwise it is loaded in netconf_module_load below */ @@ -759,7 +781,21 @@ main(int argc, clicon_err(OE_CFG, errno, "FCGX_OpenSocket"); goto done; } - +#if 1 + { + /* Change group of fcgi sock fronting reverse proxy to WWWUSER, the effective group is clicon + * which is backend. */ + gid_t wgid = -1; + if (group_name2gid(WWWUSER, &wgid) < 0){ + clicon_log(LOG_ERR, "'%s' does not seem to be a valid user group.", WWWUSER); + goto done; + } + if (chown(sockpath, -1, wgid) < 0){ + clicon_err(OE_CFG, errno, "chown"); + goto done; + } + } +#endif if (clicon_socket_set(h, sock) < 0) goto done; /* umask settings may interfer: we want group to write: this is 774 */ @@ -767,14 +803,18 @@ main(int argc, clicon_err(OE_UNIX, errno, "chmod"); goto done; } - if (FCGX_InitRequest(r, sock, 0) != 0){ +#if 1 + if (restconf_drop_privileges(h, WWWUSER) < 0) + goto done; +#endif + if (FCGX_InitRequest(req, sock, 0) != 0){ clicon_err(OE_CFG, errno, "FCGX_InitRequest"); goto done; } while (1) { finish = 1; /* If zero, dont finish request, initiate new */ - if (FCGX_Accept_r(r) < 0) { + if (FCGX_Accept_r(req) < 0) { clicon_err(OE_CFG, errno, "FCGX_Accept_r"); goto done; } @@ -794,32 +834,32 @@ main(int argc, /* Translate from FCGI parameter form to Clixon runtime data * XXX: potential name collision? */ - if (clixon_restconf_params_set(h, r->envp) < 0) + if (clixon_restconf_params_set(h, req->envp) < 0) goto done; if ((path = clixon_restconf_param_get(h, "REQUEST_URI")) != NULL){ clicon_debug(1, "path: %s", path); if (strncmp(path, "/" RESTCONF_API, strlen("/" RESTCONF_API)) == 0) - api_restconf(h, r); /* This is the function */ + api_restconf(h, req); /* This is the function */ else if (strncmp(path+1, stream_path, strlen(stream_path)) == 0) { - api_stream(h, r, stream_path, &finish); + api_stream(h, req, stream_path, &finish); } else if (strncmp(path, RESTCONF_WELL_KNOWN, strlen(RESTCONF_WELL_KNOWN)) == 0) { - api_well_known(h, r); /* */ + api_well_known(h, req); /* */ } else{ clicon_debug(1, "top-level %s not found", path); - restconf_notfound(h, r); + restconf_notfound(h, req); } } else clicon_debug(1, "NULL URI"); - if (clixon_restconf_params_clear(h, r->envp) < 0) + if (clixon_restconf_params_clear(h, req->envp) < 0) goto done; if (finish) - FCGX_Finish_r(r); + FCGX_Finish_r(req); else{ /* A handler is forked so we initiate a new request after instead of finnishing the old */ - if (FCGX_InitRequest(r, sock, 0) != 0){ + if (FCGX_InitRequest(req, sock, 0) != 0){ clicon_err(OE_CFG, errno, "FCGX_InitRequest"); goto done; } diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index a361f581..750be387 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -59,6 +59,7 @@ /* clicon */ #include +#include "restconf_api.h" #include "restconf_lib.h" /* See RFC 8040 Section 7: Mapping from NETCONF to Status Code @@ -443,6 +444,13 @@ clixon_restconf_param_set(clicon_handle h, return clicon_data_set(h, param, val); } +/*! Delete restconf http parameter + * @param[in] h Clicon handle + * @param[in] name Data name + * @retval 0 OK + * @retval -1 Error + * Currently using clixon runtime data but there is risk for colliding names + */ int clixon_restconf_param_del(clicon_handle h, char *param) @@ -469,3 +477,78 @@ restconf_uripath(clicon_handle h) *q = '\0'; return path; } + + +/*! Drop privileges from root to user (or already at user) + * @param[in] h Clicon handle + * @param[in] user Drop to this level + * Group set to clicon to communicate with backend + */ +int +restconf_drop_privileges(clicon_handle h, + char *user) +{ + int retval = -1; + uid_t newuid = -1; + uid_t uid; + char *group; + gid_t gid = -1; + + clicon_debug(1, "%s", __FUNCTION__); + /* Sanity check: backend group exists */ + if ((group = clicon_sock_group(h)) == NULL){ + clicon_err(OE_FATAL, 0, "clicon_sock_group option not set"); + return -1; + } + if (group_name2gid(group, &gid) < 0){ + clicon_log(LOG_ERR, "'%s' does not seem to be a valid user group.\n" /* \n required here due to multi-line log */ + "The config demon requires a valid group to create a server UNIX socket\n" + "Define a valid CLICON_SOCK_GROUP in %s or via the -g option\n" + "or create the group and add the user to it. Check documentation for how to do this on your platform", + group, + clicon_configfile(h)); + goto done; + } + /* Get (wanted) new www user id */ + if (name2uid(user, &newuid) < 0){ + clicon_err(OE_DAEMON, errno, "'%s' is not a valid user .\n", user); + goto done; + } + /* get current backend userid, if already at this level OK */ + if ((uid = getuid()) == newuid) + goto ok; + if (uid != 0){ + clicon_err(OE_DAEMON, EPERM, "Privileges can only be dropped from root user (uid is %u)\n", uid); + goto done; + } + if (setgid(gid) == -1) { + clicon_err(OE_DAEMON, errno, "setgid %d", gid); + goto done; + } + if (drop_priv_perm(newuid) < 0) + goto done; + /* Verify you cannot regain root privileges */ + if (setuid(0) != -1){ + clicon_err(OE_DAEMON, EPERM, "Could regain root privilieges"); + goto done; + } + clicon_debug(1, "%s dropped privileges from root to %s(%d)", + __FUNCTION__, user, newuid); + ok: + retval = 0; + done: + return retval; +} + +/*! HTTP error 405 + * @param[in] req Generic Www handle + * @param[in] allow Which methods are allowed + */ +int +restconf_method_notallowed(void *req, + char *allow) +{ + restconf_reply_status_code(req, 405); + restconf_reply_header_add(req, "Allow", "%s", allow); + return 0; +} diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index fa9f56bf..5b570c34 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -38,21 +38,8 @@ #define _RESTCONF_LIB_H_ /* - * Constants + * Types */ -#define RESTCONF_API "restconf" - -/* RESTCONF enables deployments to specify where the RESTCONF API is - located. The client discovers this by getting the "/.well-known/host-meta" - resource -*/ -#define RESTCONF_WELL_KNOWN "/.well-known/host-meta" - - -/* - * Variables - */ - /*! RESTCONF media types * @see http_media_map * (also in clixon_restconf.h) @@ -81,5 +68,7 @@ char *clixon_restconf_param_get(clicon_handle h, char *param); int clixon_restconf_param_set(clicon_handle h, char *param, char *val); int clixon_restconf_param_del(clicon_handle h, char *param); char *restconf_uripath(clicon_handle h); +int restconf_drop_privileges(clicon_handle h, char *user); +int restconf_method_notallowed(void *req, char *allow); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_root.c b/apps/restconf/restconf_root.c new file mode 100644 index 00000000..56640608 --- /dev/null +++ b/apps/restconf/restconf_root.c @@ -0,0 +1,118 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * Generic restconf root handlers eg for /restconf /.well-known, etc + */ + + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* chmod */ + +/* cligen */ +#include + +/* clicon */ +#include + +/* restconf */ +#include "restconf_lib.h" +#include "restconf_api.h" +#include "restconf_root.h" + + +/*! Determine the root of the RESTCONF API + * @param[in] h Clicon handle + * @param[in] req Generic Www handle (can be part of clixon handle) + * @param[in] cb Body buffer + * @see RFC8040 3.1 and RFC7320 + * In line with the best practices defined by [RFC7320], RESTCONF + * enables deployments to specify where the RESTCONF API is located. + */ +int +api_well_known(clicon_handle h, + void *req) +{ + int retval = -1; + char *request_method; + cbuf *cb = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + if (req == NULL){ + errno = EINVAL; + goto done; + } + request_method = clixon_restconf_param_get(h, "REQUEST_METHOD"); + if (strcmp(request_method, "GET") != 0){ + restconf_method_notallowed(req, "GET"); + goto ok; + } + restconf_reply_status_code(req, 200); /* OK */ + restconf_reply_header_add(req, "Cache-Control", "no-cache"); + restconf_reply_header_add(req, "Content-Type", "application/xrd+xml"); + /* Create body */ + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(cb, "\n"); + cprintf(cb, " \n"); + cprintf(cb, "\r\n"); + + /* Must be after body */ + restconf_reply_header_add(req, "Content-Length", "%d", cbuf_len(cb)); + if (restconf_reply_send(req, cb) < 0) + goto done; + ok: + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + diff --git a/apps/restconf/restconf_root.h b/apps/restconf/restconf_root.h new file mode 100644 index 00000000..d7a06160 --- /dev/null +++ b/apps/restconf/restconf_root.h @@ -0,0 +1,57 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * + * Generic restconf root handlers eg for /restconf /.well-known, etc + */ + +#ifndef _RESTCONF_ROOT_H_ +#define _RESTCONF_ROOT_H_ + +/* + * Constants + */ +#define RESTCONF_API "restconf" + +/* RESTCONF enables deployments to specify where the RESTCONF API is + located. The client discovers this by getting the "/.well-known/host-meta" + resource +*/ +#define RESTCONF_WELL_KNOWN "/.well-known/host-meta" + +/* + * Prototypes + */ +int api_well_known(clicon_handle h, void *req); + +#endif /* _RESTCONF_ROOT_H_ */ diff --git a/configure.ac b/configure.ac index a596e841..11c5a897 100644 --- a/configure.ac +++ b/configure.ac @@ -235,6 +235,7 @@ AC_ARG_WITH([restconf], # Common actions for all restconf packages if test "x${with_restconf}" != "x"; then # This is for changing web user default www-data + # Should this be a runtime option? AC_ARG_WITH([wwwuser], [AS_HELP_STRING([--with-wwwuser=],[Set www user different from www-data])]) if test "${with_wwwuser}"; then diff --git a/test/lib.sh b/test/lib.sh index a31d8336..fc8b5438 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -90,9 +90,6 @@ fi # RESTCONF protocol, eg http or https : ${RCPROTO:=http} -# RESTCONF port -: ${RCPORT:=80} - # RESTCONF error message (if not up) : ${RCERROR:="HTTP/1.1 502 Bad Gateway"} @@ -229,8 +226,11 @@ wait_backend(){ done } +# Start restconf daemon +# @see wait_restconf start_restconf(){ # Start in background +# echo "sudo -u $wwwuser -s $clixon_restconf $RCLOG -D $DBG $*" sudo -u $wwwuser -s $clixon_restconf $RCLOG -D $DBG $* & if [ $? -ne 0 ]; then err @@ -246,12 +246,16 @@ stop_restconf(){ } # Wait for restconf to stop sending 502 Bad Gateway +# @see start_restconf wait_restconf(){ - hdr=$(curl --head -sS $RCPROTO://localhost:$RCPORT/restconf) +# echo "curl -kis $RCPROTO://localhost/restconf" + hdr=$(curl -kis $RCPROTO://localhost/restconf) +# echo "hdr:\"$hdr\"" let i=0; - while [[ $hdr == "$RCERROR"* ]]; do + while [[ $hdr != *"200 OK"* ]]; do sleep 1 - hdr=$(curl --head -sS http://localhost/restconf) + hdr=$(curl -kis $RCPROTO://localhost/restconf) +# echo "hdr:\"$hdr\"" let i++; # echo "wait_restconf $i" if [ $i -ge $RCWAIT ]; then diff --git a/test/test_restconf.sh b/test/test_restconf.sh index e0500dbc..72d81247 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -57,26 +57,26 @@ if [ $RC -ne 0 ]; then new "start restconf daemon" start_restconf -f $cfg - new "waiting" - wait_restconf fi +new "waiting" +wait_restconf new "restconf root discovery. RFC 8040 3.1 (xml+xrd)" -expectpart "$(curl -si -X GET http://localhost/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "" "" "" +expectpart "$(curl -sik -X GET $RCPROTO://localhost/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "" "" "" new "restconf get restconf resource. RFC 8040 3.3 (json)" -expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" http://localhost/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}}' +expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}}' new "restconf get restconf resource. RFC 8040 3.3 (xml)" # Get XML instead of JSON? -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf)" 0 'HTTP/1.1 200 OK' '2016-06-21' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '2016-06-21' # Should be alphabetically ordered new "restconf get restconf/operations. RFC8040 3.3.2 (json)" -expectpart "$(curl -si -X GET http://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\],"clixon-rfc5277:create-subscription":\[null\]}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\],"clixon-rfc5277:create-subscription":\[null\]}}' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" -ret=$(curl -s -X GET -H "Accept: application/yang-data+xml" http://localhost/restconf/operations) +ret=$(curl -s -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/operations) expect='' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -84,10 +84,10 @@ if [ -z "$match" ]; then fi new "restconf get restconf/yang-library-version. RFC8040 3.3.3" -expectpart "$(curl -si -X GET http://localhost/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2016-06-21"}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2016-06-21"}' new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)" -ret=$(curl -s -X GET -H "Accept: application/yang-data+xml" http://localhost/restconf/yang-library-version) +ret=$(curl -s -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/yang-library-version) expect="2016-06-21" match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -95,46 +95,46 @@ if [ -z "$match" ]; then fi new "restconf schema resource, RFC 8040 sec 3.7 according to RFC 7895 (explicit resource)" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}' new "restconf options. RFC 8040 4.1" -expectpart "$(curl -is -X OPTIONS http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" +expectpart "$(curl -is -X OPTIONS $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" # -I means HEAD new "restconf HEAD. RFC 8040 4.2" -expectpart "$(curl -si -I -H "Accept: application/yang-data+json" http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json" +expectpart "$(curl -si -I -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json" new "restconf empty rpc JSON" -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" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" new "restconf empty rpc XML" -expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+xml" -d '' http://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+xml" -d '' $RCPROTO://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' +expectpart "$(curl -si -X POST -d {\"clixon-example:input\":null} $RCPROTO://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' +expectpart "$(curl -si -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' new "restconf empty rpc with extra args (should fail)" -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 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}' +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}' # Irritiating to get debugs on the terminal #new "restconf debug rpc" -#expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} http://localhost/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content" +#expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} $RCPROTO://localhost/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content" new "restconf get empty config + state json" -expecteq "$(curl -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["41","42","43"]}} +expecteq "$(curl -sS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["41","42","43"]}} ' new "restconf get empty config + state json with wrong module name" -expectpart "$(curl -siSG http://localhost/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}' +expectpart "$(curl -siSG $RCPROTO://localhost/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}' #'HTTP/1.1 404 Not Found' #'{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"No such yang module: badmodule"}}}' new "restconf get empty config + state xml" -ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/clixon-example:state) +ret=$(curl -s -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state) expect='414243' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -142,12 +142,12 @@ if [ -z "$match" ]; then fi new "restconf get data type json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} +expecteq "$(curl -s -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} ' new "restconf get state operation" # Cant get shell macros to work, inline matching from lib.sh -ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/clixon-example:state/op=42) +ret=$(curl -s -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) expect='42' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -155,12 +155,12 @@ if [ -z "$match" ]; then fi new "restconf get state operation type json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} +expecteq "$(curl -s -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} ' new "restconf get state operation type xml" # Cant get shell macros to work, inline matching from lib.sh -ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/clixon-example:state/op=42) +ret=$(curl -s -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) expect='42' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -168,94 +168,94 @@ if [ -z "$match" ]; then fi new "restconf GET datastore" -expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["41","42","43"]}} +expecteq "$(curl -s -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["41","42","43"]}} ' # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" -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' +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}}}' $RCPROTO://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" -expectpart "$(curl -si -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"}}}' +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $RCPROTO://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 Check interfaces eth/0/0 added" -expectfn "curl -s -X GET 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"}\]}}} +expectfn "curl -s -X GET $RCPROTO://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 delete interfaces" -expectpart "$(curl -si -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content" new "restconf Check empty config" -expectfn "curl -sG http://localhost/restconf/data/clixon-example:state" 0 "$state +expectfn "curl -sG $RCPROTO://localhost/restconf/data/clixon-example:state" 0 "$state " new "restconf Add interfaces subtree eth/0/0 using POST" -expectpart "$(curl -si -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 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X POST $RCPROTO://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 "HTTP/1.1 201 Created" new "restconf Check eth/0/0 added config" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' new "restconf Check eth/0/0 GET augmented state level 1" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}' new "restconf Check eth/0/0 GET augmented state level 2" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}' new "restconf Check eth/0/0 added state XXXXXXX" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}' new "restconf Re-post eth/0/0 which should generate error" -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"}}} ' +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}}' $RCPROTO://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" -expectpart "$(curl -si -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 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" new "Add nothing using POST (expect fail)" -expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' new "restconf Check description added" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}]}} +expecteq "$(curl -s -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}]}} ' new "restconf delete eth/0/0" -expectpart "$(curl -si -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content" new "Check deleted eth/0/0" -expectfn 'curl -s -G http://localhost/restconf/data' 0 "$state" +expectfn "curl -s -X GET http://localhost/restconf/data" 0 "$state" 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"}}} ' +expecteq "$(curl -s -X DELETE $RCPROTO://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" -expectpart "$(curl -si -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 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" 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","clixon-example:my-status":{"int":42,"str":"foo"}}]}} +expecteq "$(curl -s -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}]}} ' new "restconf rpc using POST json" -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"}} +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"42","y":"42"}} ' if ! $YANG_UNKNOWN_ANYDATA ; then new "restconf rpc using POST json wrong" -expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' http://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}' +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}' fi new "restconf rpc non-existing rpc without namespace" -expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"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" -expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"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" -expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}' +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}' new "restconf rpc missing input" -expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' http://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}}' +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"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 "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) +ret=$(curl -s -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example) expect='4242' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -263,10 +263,10 @@ if [ -z "$match" ]; then fi new "restconf rpc using wrong prefix" -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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' $RCPROTO://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 "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) +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"}}' $RCPROTO://localhost/restconf/operations/clixon-example:client-rpc) expect='example' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -274,10 +274,10 @@ if [ -z "$match" ]; then fi new "restconf Add subtree without key (expected error)" -expectpart "$(curl -is -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 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =interface, expected' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =interface, expected' new "restconf Add subtree with too many keys (expected error)" -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"}}} ' +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}}' $RCPROTO://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"}}} ' if [ $RC -ne 0 ]; then new "Kill restconf daemon" diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index 50a66ddb..6a602263 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -94,22 +94,22 @@ if [ $RC -ne 0 ]; then fi new "restconf POST tree without 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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"type":"regular"}}}' $RCPROTO://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" -expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}' http://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}' $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" new "restconf POST top without namespace" -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":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Top-level JSON object cont1 is not qualified with namespace which is a MUST according to RFC 7951"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"cont1":{"interface":{"name":"local0","type":"regular"}}}' $RCPROTO://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Top-level JSON object cont1 is not qualified with namespace which is a MUST according to RFC 7951"}}} ' new "restconf GET datastore initial" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf GET interface subtree" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"regular"}\]}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"regular"}\]}' new "restconf GET interface subtree xml" -ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/example:cont1/interface=local0) +ret=$(curl -s -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/example:cont1/interface=local0) expect='local0regular' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -117,83 +117,83 @@ if [ -z "$match" ]; then fi new "restconf GET if-type" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0/type" 0 '{"example:type":"regular"}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1/interface=local0/type" 0 '{"example:type":"regular"}' new "restconf POST interface without mandatory type" -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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://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" -expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X POST -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' $RCPROTO://localhost/restconf/data/example:cont1)" 0 "HTTP/1.1 201 Created" new "restconf POST interface without 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":"Top-level JSON object interface is not qualified with namespace which is a MUST according to RFC 7951"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"interface":{"name":"TEST2","type":"eth0"}}' $RCPROTO://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Top-level JSON object interface is not qualified with namespace which is a MUST according to RFC 7951"}}} ' new "restconf POST again" -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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' $RCPROTO://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 -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"}}} ' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"TEST","type":"eth0"}}}' $RCPROTO://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 -si -X DELETE http://localhost/restconf/data/example:cont1' 0 "HTTP/1.1 204 No Content" +expectfn "curl -si -X DELETE $RCPROTO://localhost/restconf/data/example:cont1" 0 "HTTP/1.1 204 No Content" new "restconf GET null datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' new "restconf POST initial tree" -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 "" +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}' $RCPROTO://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"}\]}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf DELETE whole datastore" -expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" +expectfn "curl -s -X DELETE $RCPROTO://localhost/restconf/data" 0 "" new "restconf GET null datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' new "restconf PUT initial datastore" -expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' http://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" new "restconf GET datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf PUT replace datastore" -expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont2":{"name":"foo"}}}' http://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont2":{"name":"foo"}}}' $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" new "restconf GET replaced datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont2" 0 '{"example:cont2":{"name":"foo"}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont2" 0 '{"example:cont2":{"name":"foo"}}' new "restconf PUT initial datastore again" -expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' http://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-restconf:data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}}' $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" new "restconf PUT change interface" -expectpart "$(curl -si -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 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"local0","type":"atm0"}}' $RCPROTO://localhost/restconf/data/example:cont1/interface=local0)" 0 "HTTP/1.1 204 No Content" new "restconf GET datastore atm" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"atm0"}\]}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"atm0"}\]}}' new "restconf PUT add interface" -expectpart "$(curl -si -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 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"TEST","type":"eth0"}}' $RCPROTO://localhost/restconf/data/example:cont1/interface=TEST)" 0 "HTTP/1.1 201 Created" new "restconf PUT change key error" -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"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:interface":{"name":"ALPHA","type":"eth0"}}' $RCPROTO://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)" -expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:type":"eth0"}' http://localhost/restconf/data/example:cont1/interface=local0/type)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H "Content-Type: application/yang-data+json" -d '{"example:type":"eth0"}' $RCPROTO://localhost/restconf/data/example:cont1/interface=local0/type)" 0 "HTTP/1.1 204 No Content" 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"}\]}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"eth0"}\]}' #--------------- json type tests new "restconf POST type x3" -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 '' +expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}}' $RCPROTO://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"}}' +expectfn "curl -s -X GET $RCPROTO://localhost/restconf/data/example:types" 0 '{"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}}' if [ $RC -ne 0 ]; then new "Kill restconf daemon" diff --git a/test/test_restconf_err.sh b/test/test_restconf_err.sh index 59fb0ecd..dd585d7a 100755 --- a/test/test_restconf_err.sh +++ b/test/test_restconf_err.sh @@ -169,54 +169,54 @@ if [ $RC -ne 0 ]; then fi new "restconf POST initial tree" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' -d "$XML" http://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' -d "$XML" $RCPROTO://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' new "restconf GET initial datastore" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/example:a=0)" 0 'HTTP/1.1 200 OK' "$XML" +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example:a=0)" 0 'HTTP/1.1 200 OK' "$XML" new "restconf GET non-qualified list" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example:a)" 0 'HTTP/1.1 400 Bad Request' "{\"ietf-restconf:errors\":{\"error\":{\"error-type\":\"rpc\",\"error-tag\":\"malformed-message\",\"error-severity\":\"error\",\"error-message\":\"malformed key =example:a, expected '=restval'\"}}}" +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example:a)" 0 'HTTP/1.1 400 Bad Request' "{\"ietf-restconf:errors\":{\"error\":{\"error-type\":\"rpc\",\"error-tag\":\"malformed-message\",\"error-severity\":\"error\",\"error-message\":\"malformed key =example:a, expected '=restval'\"}}}" new "restconf GET non-qualified list subelements" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example:a/k)" 0 'HTTP/1.1 400 Bad Request' "^{\"ietf-restconf:errors\":{\"error\":{\"error-type\":\"rpc\",\"error-tag\":\"malformed-message\",\"error-severity\":\"error\",\"error-message\":\"malformed key =example:a, expected '=restval'\"}}}" +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example:a/k)" 0 'HTTP/1.1 400 Bad Request' "^{\"ietf-restconf:errors\":{\"error\":{\"error-type\":\"rpc\",\"error-tag\":\"malformed-message\",\"error-severity\":\"error\",\"error-message\":\"malformed key =example:a, expected '=restval'\"}}}" new "restconf GET non-existent container body" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example:a=0/c)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example:a=0/c)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' new "restconf GET invalid (no yang) container body" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example:a=0/xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example:a=0/xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' new "restconf GET invalid (no yang) element" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example:xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example:xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' new "restconf POST non-existent (no yang) element" # should be invalid element -expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+xml' -d "$XML" http://localhost/restconf/data/example:a=23/xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' +expectpart "$(curl -is -X POST -H 'Content-Type: application/yang-data+xml' -d "$XML" $RCPROTO://localhost/restconf/data/example:a=23/xxx)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"xxx"},"error-severity":"error","error-message":"Unknown element"}}}' # Test for multi-module path where an augment stretches across modules new "restconf POST augment multi-namespace path" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' -d '23' http://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' -d '23' $RCPROTO://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' new "restconf GET augment multi-namespace top" -expectpart "$(curl -si -X GET http://localhost/restconf/data/augment:route-config)" 0 'HTTP/1.1 200 OK' '{"augment:route-config":{"dynamic":{"example:ospf":{"reference-bandwidth":23}}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/augment:route-config)" 0 'HTTP/1.1 200 OK' '{"augment:route-config":{"dynamic":{"example:ospf":{"reference-bandwidth":23}}}}' new "restconf GET augment multi-namespace level 1" -expectpart "$(curl -si -X GET http://localhost/restconf/data/augment:route-config/dynamic)" 0 'HTTP/1.1 200 OK' '{"augment:dynamic":{"example:ospf":{"reference-bandwidth":23}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/augment:route-config/dynamic)" 0 'HTTP/1.1 200 OK' '{"augment:dynamic":{"example:ospf":{"reference-bandwidth":23}}}' new "restconf GET augment multi-namespace cross" -expectpart "$(curl -si -X GET http://localhost/restconf/data/augment:route-config/dynamic/example:ospf)" 0 'HTTP/1.1 200 OK' '{"example:ospf":{"reference-bandwidth":23}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/augment:route-config/dynamic/example:ospf)" 0 'HTTP/1.1 200 OK' '{"example:ospf":{"reference-bandwidth":23}}' new "restconf GET augment multi-namespace cross level 2" -expectpart "$(curl -si -X GET http://localhost/restconf/data/augment:route-config/dynamic/example:ospf/reference-bandwidth)" 0 'HTTP/1.1 200 OK' '{"example:reference-bandwidth":23}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/augment:route-config/dynamic/example:ospf/reference-bandwidth)" 0 'HTTP/1.1 200 OK' '{"example:reference-bandwidth":23}' # XXX actually no such element new "restconf GET augment multi-namespace, no 2nd module in api-path, fail" -expectpart "$(curl -si -X GET http://localhost/restconf/data/augment:route-config/dynamic/ospf)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/augment:route-config/dynamic/ospf)" 0 'HTTP/1.1 404 Not Found' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' #---------------------------------------------- # Also generate an invalid state XML. This should generate an "Internal" error and the name of the new "restconf GET failed state" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data?content=nonconfig)" 0 '412 Precondition Failed' 'applicationoperation-failedmystateerrorNo such yang module. Internal error, state callback returned invalid XML: example_backend' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data?content=nonconfig)" 0 '412 Precondition Failed' 'applicationoperation-failedmystateerrorNo such yang module. Internal error, state callback returned invalid XML: example_backend' if [ $RC -ne 0 ]; then new "Kill restconf daemon" diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index ce9560d4..0cd91137 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -89,160 +89,160 @@ if [ $RC -ne 0 ]; then fi new "B.1.1. Retrieve the Top-Level API Resource root" -expectpart "$(curl -si -X GET -H 'Accept: application/xrd+xml' http://localhost/.well-known/host-meta)" 0 "HTTP/1.1 200 OK" "Content-Type: application/xrd+xml" "" "" "" +expectpart "$(curl -si -X GET -H 'Accept: application/xrd+xml' $RCPROTO://localhost/.well-known/host-meta)" 0 "HTTP/1.1 200 OK" "Content-Type: application/xrd+xml" "" "" "" d='{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}}' new "B.1.1. Retrieve the Top-Level API Resource /restconf json" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" "$d" +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" "$d" new "B.1.1. Retrieve the Top-Level API Resource /restconf xml (not in RFC)" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+xml" '2016-06-21' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+xml" '2016-06-21' # This just catches the header and the jukebox module, the RFC has foo and bar which # seems wrong to recreate new "B.1.2. Retrieve the Server Module Information" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-lib","revision":"2020-04-23","namespace":"http://clicon.org/lib","conformance-type":"implement"},{"name":"clixon-rfc5277","revision":"2008-07-01","namespace":"urn:ietf:params:xml:ns:netmod:notification","conformance-type":"implement"},{"name":"example-events","revision":"","namespace":"urn:example:events","conformance-type":"implement"},{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"},{"name":"example-system","revision":"","namespace":"http://example.com/ns/example-system","conformance-type":"implement"},{"name":"ietf-inet-types","revision":"2013-07-15","namespace":"urn:ietf:params:xml:ns:yang:ietf-inet-types","conformance-type":"implement"},' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-lib","revision":"2020-04-23","namespace":"http://clicon.org/lib","conformance-type":"implement"},{"name":"clixon-rfc5277","revision":"2008-07-01","namespace":"urn:ietf:params:xml:ns:netmod:notification","conformance-type":"implement"},{"name":"example-events","revision":"","namespace":"urn:example:events","conformance-type":"implement"},{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"},{"name":"example-system","revision":"","namespace":"http://example.com/ns/example-system","conformance-type":"implement"},{"name":"ietf-inet-types","revision":"2013-07-15","namespace":"urn:ietf:params:xml:ns:yang:ietf-inet-types","conformance-type":"implement"},' new "B.1.3. Retrieve the Server Capability Information" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth ' new "B.2.1. Create New Data Resources (artist+json)" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters" +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters" new "B.2.1. Create New Data Resources (album+xml)" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light" +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light" new "B.2.1. Add Data Resources again (conflict - not in RFC)" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 409 Conflict" +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 409 Conflict" new "4.5. PUT replace content (xml encoding)" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" new "4.5. PUT create new identity" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" new "4.5. Check jukebox content: 1 Clash and 1 Foo fighters album" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersWasting Lightjbox:alternative2011' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersWasting Lightjbox:alternative2011' new "B.2.2. Added genre (preamble to actual test)" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" # First use of PATCH new "B.2.2. Detect Datastore Resource Entity-Tag Change (XXX if-unmodified)" -expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light/genre -d '{"example-jukebox:genre":"example-jukebox:alternative"}')" 0 'HTTP/1.1 204 No Content' +expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light/genre -d '{"example-jukebox:genre":"example-jukebox:alternative"}')" 0 'HTTP/1.1 204 No Content' new "B.2.3. Edit a Datastore Resource (Add 1 Foo fighter and Nick cave album)" -expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'trueFoo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 'HTTP/1.1 204 No Content' +expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data -d 'trueFoo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 'HTTP/1.1 204 No Content' new "B.2.3. Check patch system" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-system:system -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'true' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-system:system -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'true' new "B.2.3. Check jukebox: 1 Clash, 2 Foo Fighters, 1 Nick Cave" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersOne by One2012Wasting Lightalternative2011Nick Cave and the Bad SeedsTender Prey1988' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersOne by One2012Wasting Lightalternative2011Nick Cave and the Bad SeedsTender Prey1988' new "B.2.4. Replace a Datastore Resource" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data -d 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 "HTTP/1.1 204 No Content" new "B.2.4. Check replace" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988' new "B.2.5. Edit a Data Resource (add Nick cave album The good son)" -expectpart "$(curl -si -X PATCH http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Nick%20Cave%20and%20the%20Bad%20Seeds -H 'Content-Type: application/yang-data+xml' -d 'Nick Cave and the Bad SeedsThe Good Son1990')" 0 'HTTP/1.1 204 No Content' +expectpart "$(curl -si -X PATCH $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Nick%20Cave%20and%20the%20Bad%20Seeds -H 'Content-Type: application/yang-data+xml' -d 'Nick Cave and the Bad SeedsThe Good Son1990')" 0 'HTTP/1.1 204 No Content' new "B.2.5. Check edit" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Nick%20Cave%20and%20the%20Bad%20Seeds -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Nick Cave and the Bad SeedsTender Prey1988The Good Son1990' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Nick%20Cave%20and%20the%20Bad%20Seeds -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Nick Cave and the Bad SeedsTender Prey1988The Good Son1990' # note reverse order of down/up as it is ordered by system and down is before up new 'B.3.1. "content" Parameter (preamble, add content)' -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-events:events -d '{"example-events:events":{"event":[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}]}}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-events:events -d '{"example-events:events":{"event":[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}]}}')" 0 "HTTP/1.1 201 Created" new 'B.3.1. "content" Parameter (wrong content)' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=kalle -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-attribute","error-info":{"bad-attribute":"content"},"error-severity":"error","error-message":"Unrecognized value of content attribute"}}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-events:events?content=kalle -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-attribute","error-info":{"bad-attribute":"content"},"error-severity":"error","error-message":"Unrecognized value of content attribute"}}}' new 'B.3.1. "content" Parameter example 1: content=all' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=all -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count","event-count":90},{"name":"interface-up","description":"Interface up notification count","event-count":77}\]}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-events:events?content=all -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count","event-count":90},{"name":"interface-up","description":"Interface up notification count","event-count":77}\]}}' new 'B.3.1. "content" Parameter example 2: content=config' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=config -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}\]}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-events:events?content=config -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","description":"Interface down notification count"},{"name":"interface-up","description":"Interface up notification count"}\]}}' new 'B.3.1. "content" Parameter example 3: content=nonconfig' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-events:events?content=nonconfig -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","event-count":90},{"name":"interface-up","event-count":77}\]}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-events:events?content=nonconfig -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-events:events":{"event":\[{"name":"interface-down","event-count":90},{"name":"interface-up","event-count":77}\]}}' #new "restconf DELETE whole datastore" -#expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" +#expectfn "curl -s -X DELETE $RCPROTO://localhost/restconf/data" 0 "" new 'B.3.2. "depth" Parameter example 1 unbound' -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox?depth=unbounded)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Foo Fighters","album":\[{"name":"One by One","year":2012}\]},{"name":"Nick Cave and the Bad Seeds","album":\[{"name":"Tender Prey","year":1988},{"name":"The Good Son","year":1990}\]}\]}}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=unbounded)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Foo Fighters","album":\[{"name":"One by One","year":2012}\]},{"name":"Nick Cave and the Bad Seeds","album":\[{"name":"Tender Prey","year":1988},{"name":"The Good Son","year":1990}\]}\]}}}' new 'B.3.2. "depth" Parameter example 2 depth=1' -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox?depth=1)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=1)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{}}' new 'B.3.2. "depth" Parameter depth=2' -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox?depth=2)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"library":{}}}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=2)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"library":{}}}' # Maybe this is not correct w [null,null]but I have no good examples new 'B.3.2. "depth" Parameter depth=3' -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/1.1 200 OK" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} ' new "restconf DELETE whole datastore" -expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" +expectfn "curl -s -X DELETE $RCPROTO://localhost/restconf/data" 0 "" #new 'B.3.3. "fields" Parameter' new 'B.3.4. "insert" Parameter' JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=1' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=1' new 'B.3.4. "insert" Parameter first (RFC example says after)' JSON="{\"example-jukebox:song\":[{\"index\":0,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=0' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=0' new 'B.3.4. "insert" Parameter check order' -RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" +RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new 'B.3.5. "point" Parameter (before for more interesting order: 0,2,1)' JSON="{\"example-jukebox:song\":[{\"index\":2,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' new 'B.3.5. "point" check order (0,2,1)' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new 'B.3.5. "point" Parameter 3 after 2 (using PUT)' JSON="{\"example-jukebox:song\":[{\"index\":3,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Something else']\"}]}" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=3?insert=after\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D2 )" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' -d "$JSON" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=3?insert=after\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D2 )" 0 "HTTP/1.1 201 Created" new 'B.3.5. "point" check order (0,2,3,1)' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]3/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Something else'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new "restconf DELETE whole datastore" -expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" +expectfn "curl -s -X DELETE $RCPROTO://localhost/restconf/data" 0 "" new 'B.3.4. "insert/point" leaf-list 3 (not in RFC)' -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=3' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=3' new 'B.3.4. "insert/point" leaf-list 2 first' -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=2' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=2' new 'B.3.4. "insert/point" leaf-list 1 last' -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' #new 'B.3.4. "insert/point" move leaf-list 1 last' #- restconf cannot move a leaf-list(list?) item -#expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' +#expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' new 'B.3.5. "insert/point" leaf-list check order (2,3,1)' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' new 'B.3.5. "point" Parameter leaf-list 4 before 3' -expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=4' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' $RCPROTO://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=4' new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431' new "B.2.2. Detect Datastore Resource Entity-Tag Change" # XXX done except entity-changed new 'B.3.3. "fields" Parameter' diff --git a/test/test_restconf_listkey.sh b/test/test_restconf_listkey.sh index 26fb7f1c..1150d599 100755 --- a/test/test_restconf_listkey.sh +++ b/test/test_restconf_listkey.sh @@ -92,78 +92,78 @@ if [ $RC -ne 0 ]; then fi new "restconf PUT add whole list entry" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y -d '{"list:a":{"b":"x","c":"y","nonkey":"0"}}')" 0 '' # GETs to ensure you get list [] in JSON new "restconf GET whole list entry" -expecteq "$(curl -s -X GET -H "Accept: application/yang-data+json" http://localhost/restconf/data/list:c/)" 0 '{"list:c":{"a":[{"b":"x","c":"y","nonkey":"0"}]}} +expecteq "$(curl -s -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/)" 0 '{"list:c":{"a":[{"b":"x","c":"y","nonkey":"0"}]}} ' new "restconf GET list entry itself (should fail)" -expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" http://localhost/restconf/data/list:c/a)" 0 'HTTP/1.1 400 Bad Request' '^{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =a, expected ' +expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a)" 0 'HTTP/1.1 400 Bad Request' '^{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =a, expected ' new "restconf GET list entry" -expecteq "$(curl -s -X GET -H "Accept: application/yang-data+json" http://localhost/restconf/data/list:c/a=x,y)" 0 '{"list:a":[{"b":"x","c":"y","nonkey":"0"}]} +expecteq "$(curl -s -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y)" 0 '{"list:a":[{"b":"x","c":"y","nonkey":"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 '' +expecteq "$(curl -s -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xxxy0' $RCPROTO://localhost/restconf/data/list:c/a=xx,xy)" 0 '' -expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" http://localhost/restconf/data/list:c/a/b)" 0 'HTTP/1.1 400 Bad Request' '^{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =a, expected ' +expectpart "$(curl -si -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a/b)" 0 'HTTP/1.1 400 Bad Request' '^{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =a, expected ' new "restconf PUT change whole list entry (same keys)" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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)" -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":"Top-level JSON object a is not qualified with namespace which is a MUST according to RFC 7951"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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":"Top-level JSON object a is not qualified with namespace which is a MUST according to RFC 7951"}}} ' new "restconf PUT change list entry (wrong keys)(expect fail)" -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"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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 ' +expecteq "$(curl -s -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xyxz0' $RCPROTO://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)" -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"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/nonkey -d '{"list:nonkey":"u"}')" 0 '' new "restconf PUT sub key same value" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/b -d '{"list:b":"x"}')" 0 '' 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"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/d=x -d '{"list:d":"x"}')" 0 '' new "restconf PUT change leaf-list entry (expect fail)" -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"}}}' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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)" -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","nonkey":"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" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=z -d '{"list:e":{"f":"wrong","nonkey":"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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=z/nonkey -d '{"list:nonkey":"u"}')" 0 '' new "restconf PUT list-list single first key" -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"}}} ' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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)" -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"}}} ' +expectpart "$(curl -is -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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" -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 '' +expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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)" -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"}}}' +expectpart "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://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"}}}' if [ $RC -ne 0 ]; then new "Kill restconf daemon" diff --git a/test/test_restconf_patch.sh b/test/test_restconf_patch.sh index 6facdc0c..2bf14e38 100755 --- a/test/test_restconf_patch.sh +++ b/test/test_restconf_patch.sh @@ -119,80 +119,80 @@ wait_restconf # also in test_restconf.sh new "MUST support the PATCH method for a plain patch" -expectpart "$(curl -u andy:bar -is -X OPTIONS http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" "Accept-Patch: application/yang-data+xml,application/yang-data+json" +expectpart "$(curl -u andy:bar -is -X OPTIONS $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" "Accept-Patch: application/yang-data+xml,application/yang-data+json" new "If the target resource instance does not exist, the server MUST NOT create it." -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 400 Bad Request" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 400 Bad Request" new "Create it with PUT instead" -expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 201 Created" new "THEN change it with PATCH" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":{"library":{"artist":{"name":"Clash"}}}}')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":{"library":{"artist":{"name":"Clash"}}}}')" 0 "HTTP/1.1 204 No Content" new "Check content (json)" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Clash"}\]}}}' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Clash"}\]}}}' new "Check content (xml)" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Clash' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Clash' new 'If the user is not authorized, "403 Forbidden" SHOULD be returned.' -expectpart "$(curl -u wilma:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 403 Forbidden" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}}' +expectpart "$(curl -u wilma:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 403 Forbidden" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}}' new 'user is authorized' -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 204 No Content" # 4.6.1. Plain Patch new "restconf DELETE whole datastore" -expectpart "$(curl -u andy:bar -is -X DELETE http://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -u andy:bar -is -X DELETE $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" new "Create album London Calling with PUT" -expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling"}}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling"}}')" 0 "HTTP/1.1 201 Created" new "The message-body for a plain patch MUST be present" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Beatles -d '')" 0 "HTTP/1.1 400 Bad Request" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Beatles -d '')" 0 "HTTP/1.1 400 Bad Request" # Plain patch can be used to create or update, but not delete, a child # resource within the target resource. new "Create a child resource (genre and year)" -expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling","genre":"example-jukebox:rock","year":"2129"}}')" 0 'HTTP/1.1 204 No Content' +expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling","genre":"example-jukebox:rock","year":"2129"}}')" 0 'HTTP/1.1 204 No Content' new "Update a child resource (year)" -expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling","year":"1979"}}')" 0 'HTTP/1.1 204 No Content' +expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling","year":"1979"}}')" 0 'HTTP/1.1 204 No Content' new "Check content xml" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'London Callingrock1979' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'London Callingrock1979' new "Check content json" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:album":\[{"name":"London Calling","genre":"rock","year":1979}\]}' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:album":\[{"name":"London Calling","genre":"rock","year":1979}\]}' new "The message-body MUST be represented by the media type application/yang-data+xml (or +json ^)" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 204 No Content" new "Check content (xml)" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Callingjazz1979' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Callingjazz1979' new "not implemented media type" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-patch+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 501 Not Implemented" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-patch+xml' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 501 Not Implemented" new "wrong media type" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: text/html' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 415 Unsupported Media Type" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: text/html' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 415 Unsupported Media Type" # If the target resource represents a YANG leaf-list, then the PATCH # method MUST NOT change the value of the leaf-list instance. # leaf-list extra{ new "Create leaf-list a" -expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"a"}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"a"}')" 0 "HTTP/1.1 201 Created" new "Create leaf-list b" -expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=b -d '{"example-jukebox:extra":"b"}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:extra=b -d '{"example-jukebox:extra":"b"}')" 0 "HTTP/1.1 201 Created" new "Check content" -expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:extra":\["a","b"\]}' +expectpart "$(curl -u andy:bar -si -X GET $RCPROTO://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:extra":\["a","b"\]}' new "MUST NOT change the value of the leaf-list instance" -expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"b"}')" 0 'HTTP/1.1 412 Precondition Failed' +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"b"}')" 0 'HTTP/1.1 412 Precondition Failed' # If the target resource represents a YANG list instance, then the key # leaf values, in message-body representation, MUST be the same as the @@ -200,7 +200,7 @@ expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-d # used to change the key leaf values for a data resource instance. new "The key leaf values MUST be the same as the key leaf values in the request" -expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"The Clash"}}')" 0 'HTTP/1.1 412 Precondition Failed' +expectpart "$(curl -u andy:bar -si -X PATCH -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"The Clash"}}')" 0 'HTTP/1.1 412 Precondition Failed' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf_startup.sh b/test/test_restconf_startup.sh index 5aaeb4da..87480978 100755 --- a/test/test_restconf_startup.sh +++ b/test/test_restconf_startup.sh @@ -74,16 +74,16 @@ testrun(){ wait_restconf new "restconf put 42" - 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 "" + expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/example:x/y=42 -d '{"example:y":{"a":"42","b":"42"}}')" 0 "" new "restconf put 99" - 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 "" + expecteq "$(curl -s -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/example:x/y=99 -d '{"example:y":{"a":"99","b":"99"}}')" 0 "" new "restconf post 123" - 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 "" + expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://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 "" + expecteq "$(curl -s -X DELETE $RCPROTO://localhost/restconf/data/example:x/y=42)" 0 "" new "Kill restconf daemon" stop_restconf From e898dda0161d6e6537cc2d2f6e4c9e750ea55fb9 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 13 Jun 2020 12:05:26 +0200 Subject: [PATCH 3/5] * Auto-CLI enhancements * A generated clispec including state (default @datanodestate) also generated along with the config clispec tree (default @datanode) * New mode `GT_HIDE` set by option `CLICON_CLI_GENMODEL_TYPE` to collapse non-presence containers that only contain a single list * Added a prfix for cli_show_config/cli_show_auto so that it can produce parseable output * Thanks dcornejo@netgate.com for trying it out and suggestions --- CHANGELOG.md | 8 +- apps/cli/cli_generate.c | 83 ++++++++----- apps/cli/cli_generate.h | 5 +- apps/cli/cli_main.c | 32 ++++- apps/cli/cli_show.c | 32 +++-- apps/restconf/Makefile.in | 3 + lib/clixon/clixon_options.h | 10 +- lib/clixon/clixon_yang.h | 1 + lib/src/clixon_netconf_lib.c | 5 +- lib/src/clixon_options.c | 1 + lib/src/clixon_xml_map.c | 9 +- lib/src/clixon_yang.c | 48 ++++++++ test/lib.sh | 2 + test/test_cli_gen.sh | 138 ++++++++++++++++++---- test/test_leafref_state.sh | 2 +- yang/clixon/clixon-config@2020-04-23.yang | 26 ++-- 16 files changed, 319 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb09c81d..c08d934f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ Expected: July 2020 ### Major New features +* Auto-CLI enhancements + * A generated clispec including state (default @datanodestate) also generated along with the config clispec tree (default @datanode) + * New mode `GT_HIDE` set by option `CLICON_CLI_GENMODEL_TYPE` to collapse non-presence containers that only contain a single list + * Added a prfix for cli_show_config/cli_show_auto so that it can produce parseable output + * Thanks dcornejo@netgate.com for trying it out and suggestions * Embedding restconf into the existing [libevhtp](https://github.com/criticalstack/libevhtp) embedded web server. Experimental. * The existing FCGI restconf solution will continue to be supported for NGINX and other reverese proxies with an fast CGI API. * The restconf code has been refactored to support both modes. Hopefully, it should be straightforward to add another embedded server, such as GNU microhttpd. @@ -36,8 +41,9 @@ Expected: July 2020 * `--without-restconf Disable restconf altogether` -### C-API changes on existing features (For developers) +### C/CLI-API changes on existing features (For developers) +* Added prefix for cli_show_config/cli_show_auto so that it can produce parseable output * Replaced the global variable `debug` with access function: `clicon_debug_get()`. * Due to name collision with libevent, all clixon event functions prepended with `clixon_`. You need to rename your event functions as follows: * event_reg_fd() -> clixon_event_reg_fd() diff --git a/apps/cli/cli_generate.c b/apps/cli/cli_generate.c index 0e7224c1..15ef937f 100644 --- a/apps/cli/cli_generate.c +++ b/apps/cli/cli_generate.c @@ -2,7 +2,8 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2020 Olof Hagsand + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) This file is part of CLIXON. @@ -340,8 +341,8 @@ yang2cli_var_pattern(clicon_handle h, } /* Forward */ -static int yang2cli_stmt(clicon_handle h, yang_stmt *ys, - enum genmodel_type gt, int level, cbuf *cb); +static int yang2cli_stmt(clicon_handle h, yang_stmt *ys, enum genmodel_type gt, + int level, int state, cbuf *cb); static int yang2cli_var_union(clicon_handle h, yang_stmt *ys, char *origtype, yang_stmt *ytype, char *helptext, cbuf *cb); @@ -657,7 +658,7 @@ yang2cli_leaf(clicon_handle h, *s = '\0'; } cprintf(cb, "%*s", level*3, ""); - if (gt == GT_VARS|| gt == GT_ALL){ + if (gt == GT_VARS|| gt == GT_ALL || gt == GT_HIDE){ cprintf(cb, "%s", yang_argument_get(ys)); if (helptext) cprintf(cb, "(\"%s\")", helptext); @@ -686,6 +687,7 @@ yang2cli_leaf(clicon_handle h, * @param[in] ys Yang statement * @param[in] gt CLI Generate style * @param[in] level Indentation level + * @param[in] state Include syntax for state not only config * @param[out] cb Buffer where cligen code is written */ static int @@ -693,6 +695,7 @@ yang2cli_container(clicon_handle h, yang_stmt *ys, enum genmodel_type gt, int level, + int state, cbuf *cb) { yang_stmt *yc; @@ -700,26 +703,35 @@ yang2cli_container(clicon_handle h, int retval = -1; char *helptext = NULL; char *s; + int hide = 0; - cprintf(cb, "%*s%s", level*3, "", yang_argument_get(ys)); - if ((yd = yang_find(ys, Y_DESCRIPTION, NULL)) != NULL){ - if ((helptext = strdup(yang_argument_get(yd))) == NULL){ - clicon_err(OE_UNIX, errno, "strdup"); - goto done; + /* If non-presence container && HIDE mode && only child is + * a list, then skip container keyword + * See also xml2cli + */ + if ((hide = yang_container_cli_hide(ys, gt)) == 0){ + cprintf(cb, "%*s%s", level*3, "", yang_argument_get(ys)); + if ((yd = yang_find(ys, Y_DESCRIPTION, NULL)) != NULL){ + if ((helptext = strdup(yang_argument_get(yd))) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + if ((s = strstr(helptext, "\n\n")) != NULL) + *s = '\0'; + cprintf(cb, "(\"%s\")", helptext); } - if ((s = strstr(helptext, "\n\n")) != NULL) - *s = '\0'; - cprintf(cb, "(\"%s\")", helptext); + if (cli_callback_generate(h, ys, cb) < 0) + goto done; + cprintf(cb, ";{\n"); } - if (cli_callback_generate(h, ys, cb) < 0) - goto done; - cprintf(cb, ";{\n"); + yc = NULL; while ((yc = yn_each(ys, yc)) != NULL) - if (yang2cli_stmt(h, yc, gt, level+1, cb) < 0) + if (yang2cli_stmt(h, yc, gt, level+1, state, cb) < 0) goto done; - cprintf(cb, "%*s}\n", level*3, ""); + if (hide == 0) + cprintf(cb, "%*s}\n", level*3, ""); retval = 0; done: if (helptext) @@ -732,6 +744,7 @@ yang2cli_container(clicon_handle h, * @param[in] ys Yang statement * @param[in] gt CLI Generate style * @param[in] level Indentation level + * @param[in] state Include syntax for state not only config * @param[out] cb Buffer where cligen code is written */ static int @@ -739,6 +752,7 @@ yang2cli_list(clicon_handle h, yang_stmt *ys, enum genmodel_type gt, int level, + int state, cbuf *cb) { yang_stmt *yc; @@ -775,7 +789,8 @@ yang2cli_list(clicon_handle h, /* Print key variable now, and skip it in loop below Note, only print callback on last statement */ - if (yang2cli_leaf(h, yleaf, gt==GT_VARS?GT_NONE:gt, level+1, + if (yang2cli_leaf(h, yleaf, + (gt==GT_VARS||gt==GT_HIDE)?GT_NONE:gt, level+1, cvec_next(cvk, cvi)?0:1, cb) < 0) goto done; } @@ -794,7 +809,7 @@ yang2cli_list(clicon_handle h, } if (cvi != NULL) continue; - if (yang2cli_stmt(h, yc, gt, level+1, cb) < 0) + if (yang2cli_stmt(h, yc, gt, level+1, state, cb) < 0) goto done; } cprintf(cb, "%*s}\n", level*3, ""); @@ -811,6 +826,7 @@ yang2cli_list(clicon_handle h, * @param[in] ys Yang statement * @param[in] gt CLI Generate style * @param[in] level Indentation level + * @param[in] state Include syntax for state not only config * @param[out] cb Buffer where cligen code is written @example choice interface-type { @@ -826,6 +842,7 @@ yang2cli_choice(clicon_handle h, yang_stmt *ys, enum genmodel_type gt, int level, + int state, cbuf *cb) { int retval = -1; @@ -835,7 +852,7 @@ yang2cli_choice(clicon_handle h, while ((yc = yn_each(ys, yc)) != NULL) { switch (yang_keyword_get(yc)){ case Y_CASE: - if (yang2cli_stmt(h, yc, gt, level+2, cb) < 0) + if (yang2cli_stmt(h, yc, gt, level+2, state, cb) < 0) goto done; break; case Y_CONTAINER: @@ -843,45 +860,47 @@ yang2cli_choice(clicon_handle h, case Y_LEAF_LIST: case Y_LIST: default: - if (yang2cli_stmt(h, yc, gt, level+1, cb) < 0) + if (yang2cli_stmt(h, yc, gt, level+1, state, cb) < 0) goto done; break; } } retval = 0; - done: + done: return retval; } /*! Generate CLI code for Yang statement * @param[in] h Clixon handle * @param[in] ys Yang statement - * @param[out] cb Buffer where cligen code is written * @param[in] gt CLI Generate style * @param[in] level Indentation level + * @param[in] state Include syntax for state not only config + * @param[out] cb Buffer where cligen code is written */ static int yang2cli_stmt(clicon_handle h, yang_stmt *ys, enum genmodel_type gt, - int level, /* indentation level for pretty-print */ + int level, + int state, cbuf *cb) { yang_stmt *yc; int retval = -1; - if (yang_config(ys)){ + if (state || yang_config(ys)){ switch (yang_keyword_get(ys)){ case Y_CONTAINER: - if (yang2cli_container(h, ys, gt, level, cb) < 0) + if (yang2cli_container(h, ys, gt, level, state, cb) < 0) goto done; break; case Y_LIST: - if (yang2cli_list(h, ys, gt, level, cb) < 0) + if (yang2cli_list(h, ys, gt, level, state, cb) < 0) goto done; break; case Y_CHOICE: - if (yang2cli_choice(h, ys, gt, level, cb) < 0) + if (yang2cli_choice(h, ys, gt, level, state, cb) < 0) goto done; break; case Y_LEAF_LIST: @@ -894,7 +913,7 @@ yang2cli_stmt(clicon_handle h, case Y_MODULE: yc = NULL; while ((yc = yn_each(ys, yc)) != NULL) - if (yang2cli_stmt(h, yc, gt, level+1, cb) < 0) + if (yang2cli_stmt(h, yc, gt, level+1, state, cb) < 0) goto done; break; default: /* skip */ @@ -902,7 +921,7 @@ yang2cli_stmt(clicon_handle h, } } retval = 0; - done: + done: return retval; } @@ -911,6 +930,7 @@ yang2cli_stmt(clicon_handle h, * @param[in] yspec Yang specification * @param[in] gt CLI Generate style * @param[in] printgen Log generated CLIgen syntax + * @param[in] state Also include state syntax * @param[out] ptnew CLIgen parse-tree * * Code generation styles: @@ -922,6 +942,7 @@ yang2cli(clicon_handle h, yang_stmt *yspec, enum genmodel_type gt, int printgen, + int state, parse_tree *ptnew) { cbuf *cb = NULL; @@ -936,7 +957,7 @@ yang2cli(clicon_handle h, /* Traverse YANG, loop through all modules and generate CLI */ ymod = NULL; while ((ymod = yn_each(yspec, ymod)) != NULL) - if (yang2cli_stmt(h, ymod, gt, 0, cb) < 0) + if (yang2cli_stmt(h, ymod, gt, 0, state, cb) < 0) goto done; if (printgen) clicon_log(LOG_NOTICE, "%s: Generated CLI spec:\n%s", __FUNCTION__, cbuf_get(cb)); diff --git a/apps/cli/cli_generate.h b/apps/cli/cli_generate.h index 84ee329c..10a4de15 100644 --- a/apps/cli/cli_generate.h +++ b/apps/cli/cli_generate.h @@ -2,7 +2,8 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2020 Olof Hagsand + Copyright (C) 2009-2019 Olof Hagsand + Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC(Netgate) This file is part of CLIXON. @@ -40,6 +41,6 @@ * Prototypes */ int yang2cli(clicon_handle h, yang_stmt *yspec, enum genmodel_type gt, - int printgen, parse_tree *ptnew); + int printgen, int state, parse_tree *ptnew); #endif /* _CLI_GENERATE_H_ */ diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 881f14d6..f93b8e2a 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -547,17 +547,37 @@ main(int argc, */ if (clicon_cli_genmodel(h)){ parse_tree *pt = NULL; /* cli parse tree */ - char *treeref; - + cbuf *cbtreename; + parse_tree *pts = NULL; /* cli parse tree */ + if ((pt = pt_new()) == NULL){ clicon_err(OE_UNIX, errno, "pt_new"); goto done; } - treeref = clicon_cli_model_treename(h); - /* Create cli command tree from dbspec */ - if (yang2cli(h, yspec, clicon_cli_genmodel_type(h), printgen, pt) < 0) + if ((cbtreename = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; - cligen_tree_add(cli_cligen(h), treeref, pt); + } + cprintf(cbtreename, "%s", clicon_cli_model_treename(h)); + /* Create cli command tree from dbspec + * label this tree @datamodel per default + */ + if (yang2cli(h, yspec, clicon_cli_genmodel_type(h), printgen, 0, pt) < 0) + goto done; + cligen_tree_add(cli_cligen(h), cbuf_get(cbtreename), pt); + /* same for config+state + * label this tree @datamodelstate per default + */ + if ((pts = pt_new()) == NULL){ + clicon_err(OE_UNIX, errno, "pt_new"); + goto done; + } + cprintf(cbtreename, "state"); + if (yang2cli(h, yspec, clicon_cli_genmodel_type(h), 0, 1, pts) < 0) + goto done; + cligen_tree_add(cli_cligen(h), cbuf_get(cbtreename), pts); + if (cbtreename) + cbuf_free(cbtreename); } /* Initialize cli syntax */ diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index e8dbf71a..2ddbf623 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -421,10 +421,12 @@ show_yang(clicon_handle h, * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) * xpath expression, that may contain one %, eg "/sender[name='foo']" * If xpath set, the namespace the symbols in xpath belong to (optional) + * to print before cli syntax output (optional) * @code * show config id , cli_show_config("running","xml","iface[name='foo']","urn:example:example"); * @endcode * @note if state parameter is set, then db must be running + * @see cli_show_auto1 */ static int cli_show_config1(clicon_handle h, @@ -446,9 +448,10 @@ cli_show_config1(clicon_handle h, yang_stmt *yspec; char *namespace = NULL; cvec *nsc = NULL; + char *prefix = NULL; - if (cvec_len(argv) != 3 && cvec_len(argv) != 4){ - clicon_err(OE_PLUGIN, 0, "Got %d arguments. Expected: ,,[,]", cvec_len(argv)); + if (cvec_len(argv) < 3 && cvec_len(argv) > 5){ + clicon_err(OE_PLUGIN, 0, "Got %d arguments. Expected: ,,[,, []]", cvec_len(argv)); goto done; } @@ -474,11 +477,14 @@ cli_show_config1(clicon_handle h, } cprintf(cbxpath, "%s", xpath); /* Fourth argument is namespace */ - if (cvec_len(argv) == 4){ + if (cvec_len(argv) > 3){ namespace = cv_string_get(cvec_i(argv, 3)); if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) goto done; } + if (cvec_len(argv) > 4){ + prefix = cv_string_get(cvec_i(argv, 4)); + } if (state == 0){ /* Get configuration-only from database */ if (clicon_rpc_get_config(h, NULL, db, cbuf_get(cbxpath), nsc, &xt) < 0) goto done; @@ -516,7 +522,7 @@ cli_show_config1(clicon_handle h, goto done; xc = NULL; /* Dont print xt itself */ while ((xc = xml_child_each(xt, xc, CX_ELMNT)) != NULL) - xml2cli(stdout, xc, NULL, gt); /* cli syntax */ + xml2cli(stdout, xc, prefix, gt); /* cli syntax */ break; case FORMAT_NETCONF: fprintf(stdout, "\n"); @@ -549,6 +555,7 @@ done: * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) * xpath expression, that may contain one %, eg "/sender[name="%s"]" * If xpath set, the namespace the symbols in xpath belong to (optional) + * to print before cli syntax output * @code * show config id , cli_show_config("running","xml","iface[name='foo']","urn:example:example"); * @endcode @@ -675,8 +682,10 @@ int cli_show_version(clicon_handle h, * Generated API PATH * "running"|"candidate"|"startup" * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) + * to print before cli syntax output * @note if state parameter is set, then db must be running * @note that first argument is generated by code. + * @see cli_show_config1 */ static int cli_show_auto1(clicon_handle h, @@ -687,7 +696,6 @@ cli_show_auto1(clicon_handle h, int retval = 1; yang_stmt *yspec; char *api_path_fmt; /* xml key format */ - // char *api_path = NULL; /* xml key */ char *db; char *xpath = NULL; cvec *nsc = NULL; @@ -698,9 +706,10 @@ cli_show_auto1(clicon_handle h, cxobj *xerr; enum genmodel_type gt; char *api_path = NULL; + char *prefix = NULL; - if (cvec_len(argv) != 3){ - clicon_err(OE_PLUGIN, 0, "Usage: * . (*) generated."); + if (cvec_len(argv) < 3 && cvec_len(argv) > 4){ + clicon_err(OE_PLUGIN, 0, "Usage: * . (*) generated."); goto done; } /* First argv argument: API_path format */ @@ -709,6 +718,10 @@ cli_show_auto1(clicon_handle h, db = cv_string_get(cvec_i(argv, 1)); /* Third format: output format */ formatstr = cv_string_get(cvec_i(argv, 2)); + if (cvec_len(argv) > 3){ + /* Fourth format: prefix to print before cli syntax */ + prefix = cv_string_get(cvec_i(argv, 3)); + } if ((int)(format = format_str2int(formatstr)) < 0){ clicon_err(OE_PLUGIN, 0, "Not valid format: %s", formatstr); goto done; @@ -758,7 +771,7 @@ cli_show_auto1(clicon_handle h, case FORMAT_CLI: if ((gt = clicon_cli_genmodel_type(h)) == GT_ERR) goto done; - xml2cli(stdout, xp, NULL, gt); /* cli syntax */ + xml2cli(stdout, xp, prefix, gt); /* cli syntax */ break; case FORMAT_NETCONF: fprintf(stdout, "\n"); @@ -786,6 +799,7 @@ cli_show_auto1(clicon_handle h, * Generated API PATH * "running"|"candidate"|"startup" * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) + * to print before cli syntax outptu * @see cli_show_auto_state For config and state */ int @@ -801,7 +815,9 @@ cli_show_auto(clicon_handle h, * Generated API PATH * "running" * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) + * to print before cli syntax output * @see cli_show_auto For config only + * @see cli_show_config_state Not auto-generated */ int cli_show_auto_state(clicon_handle h, diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 2804f70d..b0680280 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -100,6 +100,9 @@ endif APPOBJ = $(APPSRC:.c=.o) # Accessible from plugin +# XXX actually this does not work properly, there are functions in lib +# (eg restconf_method_notallowed) that uses symbols in restconf_api that +# are not in the lib. LIBSRC = restconf_lib.c LIBOBJ = $(LIBSRC:.c=.o) diff --git a/lib/clixon/clixon_options.h b/lib/clixon/clixon_options.h index dde7e93f..9628d9f2 100644 --- a/lib/clixon/clixon_options.h +++ b/lib/clixon/clixon_options.h @@ -58,20 +58,24 @@ */ /*! Controls how keywords a generated in CLI syntax / prints from object model * Example YANG: + * container c{ * list a { * key x; * leaf x; * leaf y; * } - * NONE: a ; - * VARS: a y ; - * ALL: a x y ; + * } + * NONE: c a ; + * VARS: c a y ; + * ALL: c a x y ; + * HIDE: a x y ; */ enum genmodel_type{ GT_ERR =-1, /* Error */ GT_NONE=0, /* No extra keywords */ GT_VARS, /* Keywords on non-key variables */ GT_ALL, /* Keywords on all variables */ + GT_HIDE, /* Keywords on all variables and hide container around lists */ }; /*! See clixon-config.yang type startup_mode */ diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index dcd758c6..d054872f 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -245,6 +245,7 @@ int yang_config(yang_stmt *ys); int yang_config_ancestor(yang_stmt *ys); int yang_features(clicon_handle h, yang_stmt *yt); cvec *yang_arg2cvec(yang_stmt *ys, char *delimi); +int yang_container_cli_hide(yang_stmt *ys, int gt); int yang_key_match(yang_stmt *yn, char *name); int yang_type_cache_regexp_set(yang_stmt *ytype, int rxmode, cvec *regexps); diff --git a/lib/src/clixon_netconf_lib.c b/lib/src/clixon_netconf_lib.c index e6d175a1..f1a789b1 100644 --- a/lib/src/clixon_netconf_lib.c +++ b/lib/src/clixon_netconf_lib.c @@ -1305,8 +1305,9 @@ netconf_module_load(clicon_handle h) /* Load yang spec */ if (yang_spec_parse_module(h, "ietf-netconf", NULL, yspec)< 0) goto done; - if (yang_spec_parse_module(h, "clixon-rfc5277", NULL, yspec)< 0) - goto done; + if (clicon_option_bool(h, "CLICON_STREAM_DISCOVERY_RFC5277")) + if (yang_spec_parse_module(h, "clixon-rfc5277", NULL, yspec)< 0) + goto done; /* YANG module revision change management */ if (clicon_option_bool(h, "CLICON_XML_CHANGELOG")) if (yang_spec_parse_module(h, "clixon-xml-changelog", NULL, yspec)< 0) diff --git a/lib/src/clixon_options.c b/lib/src/clixon_options.c index c6a45f9d..9416639d 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -85,6 +85,7 @@ static const map_str2int cli_genmodel_map[] = { {"NONE", GT_NONE}, {"VARS", GT_VARS}, {"ALL", GT_ALL}, + {"HIDE", GT_HIDE}, {NULL, -1} }; diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 47ee536e..8a9a441c 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -206,7 +206,7 @@ xml2cli(FILE *f, if (yang_keyword_get(ys) == Y_LEAF || yang_keyword_get(ys) == Y_LEAF_LIST){ if (prepend0) fprintf(f, "%s", prepend0); - if (gt == GT_ALL || gt == GT_VARS) + if (gt == GT_ALL || gt == GT_VARS || gt == GT_HIDE) fprintf(f, "%s ", xml_name(x)); if ((body = xml_body(x)) != NULL){ if (index(body, ' ')) @@ -224,7 +224,12 @@ xml2cli(FILE *f, } if (prepend0) cprintf(cbpre, "%s", prepend0); - cprintf(cbpre, "%s ", xml_name(x)); + + /* If non-presence container && HIDE mode && only child is + * a list, then skip container keyword + * See also yang2cli_container */ + if (yang_container_cli_hide(ys, gt) == 0) + cprintf(cbpre, "%s ", xml_name(x)); if (yang_keyword_get(ys) == Y_LIST){ /* If list then first loop through keys */ diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 6b333c49..db23a747 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -2537,6 +2537,54 @@ yang_arg2cvec(yang_stmt *ys, return cvv; } +/*! Check if yang is subject to generated cli GT_HIDE boolean + * The yang should be: + * 1) a non-presence container + * 2) parent of a (single) list XXX: or could multiple lists work? + * 3) no other data node children + * @retval 0 No, does not satisfy the GT_HIDE condition + * @retval 1 Yes, satisfies the GT_HIDE condition + * @see clixon-config.yang HIDE enumeration type + */ +int +yang_container_cli_hide(yang_stmt *ys, + enum genmodel_type gt) +{ + yang_stmt *yc = NULL; + int i; + enum rfc_6020 keyw; + + keyw = yang_keyword_get(ys); + /* HIDE mode */ + if (gt != GT_HIDE) + return 0; + /* A container */ + if (yang_keyword_get(ys) != Y_CONTAINER) + return 0; + /* Non-presence */ + if (yang_find(ys, Y_PRESENCE, NULL) != NULL) + return 0; + /* Ensure a single list child and no other data nodes */ + i = 0; /* Number of list nodes */ + while ((yc = yn_each(ys, yc)) != NULL) { + keyw = yang_keyword_get(yc); + /* case/choice could hide anything so disqualify those */ + if (keyw == Y_CASE || keyw == Y_CHOICE) + break; + if (!yang_datanode(yc)) /* Allowed, check next */ + continue; + if (keyw != Y_LIST) /* Another datanode than list */ + break; + if (i++>0) /* More than one list (or could this work?) */ + break; + } + if (yc != NULL) /* break from loop */ + return 0; + if (i != 1) /* List found */ + return 0; + return 1; /* Passed all tests: yes you can hide this keyword */ +} + /*! Check if yang node yn has key-stmt as child which matches name * * The function looks at the LIST argument string (not actual children) diff --git a/test/lib.sh b/test/lib.sh index fc8b5438..26b6746b 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -370,6 +370,8 @@ expecteq(){ # - expected stdout outcome* # - the token "--not--" # - not expected stdout outcome* +# Example: +# expectpart "$(a-shell-cmd arg)" 0 'expected match 1' 'expected match 2' --not-- 'not expected 1' # @note need to escape \[\] expectpart(){ r=$? diff --git a/test/test_cli_gen.sh b/test/test_cli_gen.sh index 4d78fa77..ea61433b 100755 --- a/test/test_cli_gen.sh +++ b/test/test_cli_gen.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Tests for using the generated cli. -# In particular setting a config, displaying as cli commands and reconfigure it using that. +# In particular setting a config, displaying as cli commands and reconfigure it # Tests: # Make a config in CLI. Show output as CLI, save it and ensure it is the same @@ -13,6 +13,7 @@ APPNAME=example cfg=$dir/conf_yang.xml fyang=$dir/$APPNAME.yang +fstate=$dir/state.xml clidir=$dir/cli if [ -d $clidir ]; then rm -rf $clidir/* @@ -28,13 +29,15 @@ cat < $cfg /usr/local/share/clixon $dir $fyang + /usr/local/lib/$APPNAME/backend $clidir /usr/local/lib/$APPNAME/cli $APPNAME - ALL + VARS /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/var/$APPNAME/$APPNAME.pidfile /usr/local/var/$APPNAME + false EOF @@ -43,19 +46,37 @@ module $APPNAME { namespace "urn:example:clixon"; prefix ex; container table{ - list parameter{ - key name; - leaf name{ - type string; - } - leaf value{ - type string; - } - } + list parameter{ + key name; + leaf name{ + type string; + } + leaf value{ + type string; + } + } } + container exstate{ + config false; + list sender{ + key ref; + leaf ref{ + type string; + } + } + } } EOF +# This is state data written to file that backend reads from (on request) +cat < $fstate + + + x + + +EOF + cat < $clidir/ex.cli CLICON_MODE="example"; CLICON_PROMPT="%U@%H> "; @@ -64,7 +85,13 @@ set @datamodel, cli_set(); merge @datamodel, cli_merge(); create @datamodel, cli_create(); delete @datamodel, cli_del(); -show config @datamodel, cli_show_auto("candidate", "cli"); +show config, cli_show_config("candidate", "cli", "/", 0, "set "); +show config @datamodel, cli_show_auto("candidate", "cli", "set "); +show state, cli_show_config_state("running", "cli", "/", "set "); +show state @datamodelstate, cli_show_auto_state("running", "cli", "set "); +show xml, cli_show_config("candidate", "xml", "/"); +commit, cli_commit(); +discard, discard_changes(); EOF @@ -75,24 +102,93 @@ if [ $BE -ne 0 ]; then if [ $? -ne 0 ]; then err fi - new "start backend -s init -f $cfg" - start_backend -s init -f $cfg + new "start backend -s init -f $cfg -- -sS $fstate" + start_backend -s init -f $cfg -- -sS $fstate new "waiting" wait_backend fi -# Set a config in CLI +# Simple run trying setting a config, +# then deleting it, and reloading it +# 1. mode - either VARS Keywords on non-key variables: a y or +# ALL Keywords on all variables: a x y +testrun() +{ + mode=$1 + if [ $mode = ALL ]; then + table=" table" + name=" name" + elif [ $mode = HIDE ]; then + table= + name= + else + table=" table" + name= + fi + + new "set a" + echo "$clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg set$table parameter$name a value x)" 0 "" + + new "set b" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg set$table parameter$name b value y)" 0 "" + + new "reset b" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg set$table parameter$name b value z)" 0 "" + + new "show match a & b" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config)" 0 "set$table parameter$name a" "set$table parameter$name a value x" "set$table parameter$name b" "set$table parameter$name b value z" --not-- "set$table parameter$name b value y" +SAVED=$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config) +# awkward having pretty-printed xml in matching strings + + new "show match a & b xml" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show xml)" 0 "" "" "a" "x" "" "" "b" "z" "" "
" + + new "delete a" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg delete$table parameter$name a)" 0 "" + + new "show match b" +echo "$clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config)" 0 "$table parameter$name b" "$table parameter$name b value z" --not-- "$table parameter$name a" "$table parameter$name a value x" "$table parameter$name b value y" + + new "discard" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg discard)" 0 "" + + new "show match empty" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config)" 0 --not-- "$table parameter$name b" "$table parameter$name b value z" "$table parameter$name a" "$table parameter$name a value x" "$table parameter$name b value y" + + new "load saved cli config" + expectpart "$(echo "$SAVED" | $clixon_cli -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg)" 0 "" + + new "show saved a & b" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg show config)" 0 "set$table parameter$name a" "set$table parameter$name a value x" "set$table parameter$name b" "set$table parameter$name b value z" --not-- "set$table parameter$name b value y" + + new "discard" + expectpart "$($clixon_cli -1 -o CLICON_CLI_GENMODEL_TYPE=$mode -f $cfg discard)" 0 "" +} # testrun + +new "keywords=HIDE" +testrun HIDE + +new "keywords=ALL" +testrun ALL + +new "keywords=VARS" +testrun VARS + +# show state new "set a" -expectfn "$clixon_cli -1 -f $cfg set table parameter name a value x" 0 "" +expectpart "$($clixon_cli -1 -f $cfg set$table parameter a value x)" 0 "" -new "set b" -expectfn "$clixon_cli -1 -f $cfg set table parameter name b value y" 0 "" - -new "set b" -expectfn "$clixon_cli -1 -f $cfg set table parameter name b value y" 0 "" +new "commit" +expectpart "$($clixon_cli -1 -f $cfg commit)" 0 "" +new "show state" +expectpart "$($clixon_cli -1 -f $cfg show state)" 0 "exstate sender x" "table parameter a" "table parameter a value x" +new "show state exstate" +expectpart "$($clixon_cli -1 -f $cfg show state exstate)" 0 "state sender x" --not-- "table parameter a" "table parameter a value x" new "Kill backend" # Check if premature kill diff --git a/test/test_leafref_state.sh b/test/test_leafref_state.sh index 249656b9..7c2e5b6e 100755 --- a/test/test_leafref_state.sh +++ b/test/test_leafref_state.sh @@ -65,7 +65,7 @@ module leafref{ } EOF -# This is state data writte to file that backend reads from (on request) +# This is state data written to file that backend reads from (on request) cat < $fstate x diff --git a/yang/clixon/clixon-config@2020-04-23.yang b/yang/clixon/clixon-config@2020-04-23.yang index aa75bc91..4bee48f5 100644 --- a/yang/clixon/clixon-config@2020-04-23.yang +++ b/yang/clixon/clixon-config@2020-04-23.yang @@ -148,16 +148,19 @@ module clixon-config { typedef cli_genmodel_type{ description "How to generate CLI from YANG model, - eg list a{ key x; leaf x; leaf y;}"; + eg {container c {list a{ key x; leaf x; leaf y;}}"; type enumeration{ enum NONE{ - description "No extra keywords: a "; + description "No extra keywords: c a "; } enum VARS{ - description "Keywords on non-key variables: a y "; + description "Keywords on non-key variables: c a y "; } enum ALL{ - description "Keywords on all variables: a x y "; + description "Keywords on all variables: c a x y "; + } + enum HIDE{ + description "Keywords on non-key variables and hide container around lists: a y "; } } } @@ -411,7 +414,9 @@ module clixon-config { description "If set, CLI specs can reference the model syntax using this reference. - Example: set @datamodel, cli_set();"; + Example: set @datamodel, cli_set(); + A second tree called eg @datamodelstate is created that + also contains state together with config."; } leaf CLICON_CLI_GENMODEL_COMPLETION { type int32; @@ -595,7 +600,8 @@ module clixon-config { description "If set, tag datastores with RFC 7895 YANG Module Library info. When loaded at startup, a check is made if the system - yang modules match"; + yang modules match. + See also CLICON_MODULE_LIBRARY_RFC7895"; } leaf CLICON_XML_CHANGELOG { type boolean; @@ -669,9 +675,11 @@ module clixon-config { leaf CLICON_MODULE_LIBRARY_RFC7895 { type boolean; default true; - description "Enable RFC 7895 YANG Module library support as state - data. If enabled, module info will appear when doing - netconf get or restconf GET"; + description + "Enable RFC 7895 YANG Module library support as state data. If + enabled, module info will appear when doing netconf get or + restconf GET. + See also CLICON_XMLDB_MODSTATE"; } leaf CLICON_MODULE_SET_ID { type string; From cea574659750329e53ce5426966d16220adfce88 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 14 Jun 2020 18:01:31 +0200 Subject: [PATCH 4/5] Fixed: The module `clixon-rfc5277` was always enabled, but should only be enabled when `CLICON_STREAM_DISCOVERY_RFC5277` is enabled. --- CHANGELOG.md | 4 + apps/restconf/restconf_fcgi_lib.c | 174 +++++++++++++++--------------- test/test_restconf.sh | 4 +- test/test_restconf_jukebox.sh | 2 +- 4 files changed, 94 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08d934f..3a0b3b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,10 @@ Expected: July 2020 * Added new function `clicon_xml2str()` to complement xml_print and others that returns a malloced string. * Added new function `xml_child_index_each()` to iterate over the children of an XML node according to the order defined by an explicit index variable. This is a complement to `xml_child_each()` which iterates using the default order. +### Corrected Bugs + +* Fixed: The module `clixon-rfc5277` was always enabled, but should only be enabled when `CLICON_STREAM_DISCOVERY_RFC5277` is enabled. + ## 4.5.0 12 May 2020 diff --git a/apps/restconf/restconf_fcgi_lib.c b/apps/restconf/restconf_fcgi_lib.c index 5ed0d86b..34ae8c0c 100644 --- a/apps/restconf/restconf_fcgi_lib.c +++ b/apps/restconf/restconf_fcgi_lib.c @@ -65,166 +65,166 @@ #include "restconf_fcgi_lib.h" /*! HTTP error 400 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_badrequest(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_SetExitStatus(400, r->out); - FCGX_FPrintF(r->out, "Status: 400 Bad Request\r\n"); /* 400 bad request */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Clixon Bad request/h1>\n"); - FCGX_FPrintF(r->out, "The requested URL %s or data is in some way badly formed.\n", + FCGX_SetExitStatus(400, req->out); + FCGX_FPrintF(req->out, "Status: 400 Bad Request\r\n"); /* 400 bad request */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Clixon Bad request/h1>\n"); + FCGX_FPrintF(req->out, "The requested URL %s or data is in some way badly formed.\n", path); return 0; } /*! HTTP error 401 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_unauthorized(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_SetExitStatus(401, r->out); - FCGX_FPrintF(r->out, "Status: 401 Unauthorized\r\n"); /* 401 unauthorized */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "access-denied\n"); - FCGX_FPrintF(r->out, "The requested URL %s was unauthorized.\n", path); + FCGX_SetExitStatus(401, req->out); + FCGX_FPrintF(req->out, "Status: 401 Unauthorized\r\n"); /* 401 unauthorized */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "access-denied\n"); + FCGX_FPrintF(req->out, "The requested URL %s was unauthorized.\n", path); return 0; } /*! HTTP error 403 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_forbidden(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_SetExitStatus(403, r->out); - FCGX_FPrintF(r->out, "Status: 403 Forbidden\r\n"); /* 403 forbidden */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Forbidden

\n"); - FCGX_FPrintF(r->out, "The requested URL %s was forbidden.\n", path); + FCGX_SetExitStatus(403, req->out); + FCGX_FPrintF(req->out, "Status: 403 Forbidden\r\n"); /* 403 forbidden */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Forbidden

\n"); + FCGX_FPrintF(req->out, "The requested URL %s was forbidden.\n", path); return 0; } /*! HTTP error 404 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_notfound(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_SetExitStatus(404, r->out); - FCGX_FPrintF(r->out, "Status: 404 Not Found\r\n"); /* 404 not found */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Not Found

\n"); - FCGX_FPrintF(r->out, "Not Found\n"); - FCGX_FPrintF(r->out, "The requested URL %s was not found on this server.\n", + FCGX_SetExitStatus(404, req->out); + FCGX_FPrintF(req->out, "Status: 404 Not Found\r\n"); /* 404 not found */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Not Found

\n"); + FCGX_FPrintF(req->out, "Not Found\n"); + FCGX_FPrintF(req->out, "The requested URL %s was not found on this server.\n", path); return 0; } /*! HTTP error 406 Not acceptable - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_notacceptable(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_SetExitStatus(406, r->out); - FCGX_FPrintF(r->out, "Status: 406 Not Acceptable\r\n"); /* 406 not acceptible */ + FCGX_SetExitStatus(406, req->out); + FCGX_FPrintF(req->out, "Status: 406 Not Acceptable\r\n"); /* 406 not acceptible */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Not Acceptable

\n"); - FCGX_FPrintF(r->out, "Not Acceptable\n"); - FCGX_FPrintF(r->out, "The target resource does not have a current representation that would be acceptable to the user agent.\n", + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Not Acceptable

\n"); + FCGX_FPrintF(req->out, "Not Acceptable\n"); + FCGX_FPrintF(req->out, "The target resource does not have a current representation that would be acceptable to the user agent.\n", path); return 0; } /*! HTTP error 409 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int -restconf_conflict(FCGX_Request *r) +restconf_conflict(FCGX_Request *req) { - FCGX_SetExitStatus(409, r->out); - FCGX_FPrintF(r->out, "Status: 409 Conflict\r\n"); /* 409 Conflict */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Data resource already exists

\n"); + FCGX_SetExitStatus(409, req->out); + FCGX_FPrintF(req->out, "Status: 409 Conflict\r\n"); /* 409 Conflict */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Data resource already exists

\n"); return 0; } /*! HTTP error 409 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int -restconf_unsupported_media(FCGX_Request *r) +restconf_unsupported_media(FCGX_Request *req) { - 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"); + FCGX_SetExitStatus(415, req->out); + FCGX_FPrintF(req->out, "Status: 415 Unsupported Media Type\r\n"); + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Unsupported Media Type

\n"); return 0; } /*! HTTP error 500 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int restconf_internal_server_error(clicon_handle h, - FCGX_Request *r) + FCGX_Request *req) { char *path; clicon_debug(1, "%s", __FUNCTION__); path = clixon_restconf_param_get(h, "REQUEST_URI"); - FCGX_FPrintF(r->out, "Status: 500 Internal Server Error\r\n"); /* 500 internal server error */ - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Internal server error when accessing %s

\n", path); + FCGX_FPrintF(req->out, "Status: 500 Internal Server Error\r\n"); /* 500 internal server error */ + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Internal server error when accessing %s

\n", path); return 0; } /*! HTTP error 501 - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ int -restconf_notimplemented(FCGX_Request *r) +restconf_notimplemented(FCGX_Request *req) { clicon_debug(1, "%s", __FUNCTION__); - FCGX_FPrintF(r->out, "Status: 501 Not Implemented\r\n"); - FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); - FCGX_FPrintF(r->out, "

Not Implemented/h1>\n"); + FCGX_FPrintF(req->out, "Status: 501 Not Implemented\r\n"); + FCGX_FPrintF(req->out, "Content-Type: text/html\r\n\r\n"); + FCGX_FPrintF(req->out, "

Not Implemented/h1>\n"); return 0; } /*! Print all FCGI headers - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https */ int -restconf_test(FCGX_Request *r, +restconf_test(FCGX_Request *req, int dbg) { - char **environ = r->envp; + char **environ = req->envp; int i; clicon_debug(1, "All environment vars:"); @@ -303,24 +303,24 @@ clixon_restconf_params_clear(clicon_handle h, } /*! - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle */ cbuf * -readdata(FCGX_Request *r) +readdata(FCGX_Request *req) { int c; cbuf *cb; if ((cb = cbuf_new()) == NULL) return NULL; - while ((c = FCGX_GetChar(r->in)) != -1) + while ((c = FCGX_GetChar(req->in)) != -1) cprintf(cb, "%c", c); return cb; } /*! Return restconf error on get/head request * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle + * @param[in] req 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] media Output media @@ -329,7 +329,7 @@ readdata(FCGX_Request *r) */ int api_return_err(clicon_handle h, - FCGX_Request *r, + FCGX_Request *req, cxobj *xerr, int pretty, restconf_media media, @@ -381,23 +381,23 @@ api_return_err(clicon_handle h, } if ((reason_phrase = restconf_code2reason(code)) == NULL) reason_phrase=""; - FCGX_SetExitStatus(code, r->out); /* Created */ - FCGX_FPrintF(r->out, "Status: %d %s\r\n", code, reason_phrase); - FCGX_FPrintF(r->out, "Content-Type: %s\r\n\r\n", restconf_media_int2str(media)); + FCGX_SetExitStatus(code, req->out); /* Created */ + FCGX_FPrintF(req->out, "Status: %d %s\r\n", code, reason_phrase); + FCGX_FPrintF(req->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, -1) < 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)); - FCGX_FPrintF(r->out, " \r\n"); + FCGX_FPrintF(req->out, " \n", cbuf_get(cb)); + FCGX_FPrintF(req->out, "%s", cbuf_get(cb)); + FCGX_FPrintF(req->out, " \r\n"); } else { - FCGX_FPrintF(r->out, "", cbuf_get(cb)); - FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); - FCGX_FPrintF(r->out, "\r\n"); + FCGX_FPrintF(req->out, "", cbuf_get(cb)); + FCGX_FPrintF(req->out, "%s", cbuf_get(cb)); + FCGX_FPrintF(req->out, "\r\n"); } break; case YANG_DATA_JSON: @@ -405,16 +405,16 @@ api_return_err(clicon_handle h, 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", + FCGX_FPrintF(req->out, "{\n"); + FCGX_FPrintF(req->out, " \"ietf-restconf:errors\" : %s\n", cbuf_get(cb)); - FCGX_FPrintF(r->out, "}\r\n"); + FCGX_FPrintF(req->out, "}\r\n"); } else{ - FCGX_FPrintF(r->out, "{"); - FCGX_FPrintF(r->out, "\"ietf-restconf:errors\":"); - FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); - FCGX_FPrintF(r->out, "}\r\n"); + FCGX_FPrintF(req->out, "{"); + FCGX_FPrintF(req->out, "\"ietf-restconf:errors\":"); + FCGX_FPrintF(req->out, "%s", cbuf_get(cb)); + FCGX_FPrintF(req->out, "}\r\n"); } break; default: @@ -434,14 +434,14 @@ api_return_err(clicon_handle h, } /*! Print location header from FCGI environment - * @param[in] r Fastcgi request handle + * @param[in] req Fastcgi request handle * @param[in] xobj If set (eg POST) add to api-path * $https “on” if connection operates in SSL mode, or an empty string otherwise * @note ports are ignored */ int http_location(clicon_handle h, - FCGX_Request *r, + FCGX_Request *req, cxobj *xobj) { int retval = -1; @@ -460,14 +460,14 @@ http_location(clicon_handle h, } if (xml2api_path_1(xobj, cb) < 0) goto done; - FCGX_FPrintF(r->out, "Location: http%s://%s%s%s\r\n", + FCGX_FPrintF(req->out, "Location: http%s://%s%s%s\r\n", https?"s":"", host, request_uri, cbuf_get(cb)); } else - FCGX_FPrintF(r->out, "Location: http%s://%s%s\r\n", + FCGX_FPrintF(req->out, "Location: http%s://%s%s\r\n", https?"s":"", host, request_uri); diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 72d81247..9bb0ff5c 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -73,11 +73,11 @@ expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO:// # Should be alphabetically ordered new "restconf get restconf/operations. RFC8040 3.3.2 (json)" -expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\],"clixon-rfc5277:create-subscription":\[null\]}}' +expectpart "$(curl -si -X GET $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\]}}' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" ret=$(curl -s -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/operations) -expect='' +expect='' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then err "$expect" "$ret" diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 0cd91137..6301d2aa 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -101,7 +101,7 @@ expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO:// # This just catches the header and the jukebox module, the RFC has foo and bar which # seems wrong to recreate new "B.1.2. Retrieve the Server Module Information" -expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-lib","revision":"2020-04-23","namespace":"http://clicon.org/lib","conformance-type":"implement"},{"name":"clixon-rfc5277","revision":"2008-07-01","namespace":"urn:ietf:params:xml:ns:netmod:notification","conformance-type":"implement"},{"name":"example-events","revision":"","namespace":"urn:example:events","conformance-type":"implement"},{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"},{"name":"example-system","revision":"","namespace":"http://example.com/ns/example-system","conformance-type":"implement"},{"name":"ietf-inet-types","revision":"2013-07-15","namespace":"urn:ietf:params:xml:ns:yang:ietf-inet-types","conformance-type":"implement"},' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-lib","revision":"2020-04-23","namespace":"http://clicon.org/lib","conformance-type":"implement"},{"name":"example-events","revision":"","namespace":"urn:example:events","conformance-type":"implement"},{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"},{"name":"example-system","revision":"","namespace":"http://example.com/ns/example-system","conformance-type":"implement"},{"name":"ietf-inet-types","revision":"2013-07-15","namespace":"urn:ietf:params:xml:ns:yang:ietf-inet-types","conformance-type":"implement"},' new "B.1.3. Retrieve the Server Capability Information" expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth From 5120409a5658a5af2d3a98132291430bcd2ed9cb Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 15 Jun 2020 16:07:04 +0200 Subject: [PATCH 5/5] Auto-cli updates and sync with clixon-docs --- CHANGELOG.md | 3 +- apps/cli/cli_generate.c | 15 ++- apps/cli/cli_generate.h | 12 ++ apps/cli/cli_main.c | 143 +++++++++++++++------- apps/cli/cli_plugin.c | 10 +- apps/cli/cli_show.c | 5 +- doc/FAQ.md | 7 +- example/main/example_cli.cli | 4 +- lib/src/clixon_options.c | 2 +- test/test_choice.sh | 4 +- test/test_cli.sh | 43 +++---- test/test_cli_gen.sh | 18 +-- yang/clixon/clixon-config@2020-04-23.yang | 11 +- 13 files changed, 178 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0b3b9a..40fa5a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ Expected: July 2020 * Auto-CLI enhancements * A generated clispec including state (default @datanodestate) also generated along with the config clispec tree (default @datanode) * New mode `GT_HIDE` set by option `CLICON_CLI_GENMODEL_TYPE` to collapse non-presence containers that only contain a single list - * Added a prfix for cli_show_config/cli_show_auto so that it can produce parseable output + * Added a prefix for cli_show_config/cli_show_auto so that it can produce parseable output * Thanks dcornejo@netgate.com for trying it out and suggestions * Embedding restconf into the existing [libevhtp](https://github.com/criticalstack/libevhtp) embedded web server. Experimental. * The existing FCGI restconf solution will continue to be supported for NGINX and other reverese proxies with an fast CGI API. @@ -40,7 +40,6 @@ Expected: July 2020 * `--with-restconf=evhtp Integrate restconf with libevhtp server` * `--without-restconf Disable restconf altogether` - ### C/CLI-API changes on existing features (For developers) * Added prefix for cli_show_config/cli_show_auto so that it can produce parseable output diff --git a/apps/cli/cli_generate.c b/apps/cli/cli_generate.c index 15ef937f..39d95289 100644 --- a/apps/cli/cli_generate.c +++ b/apps/cli/cli_generate.c @@ -63,9 +63,9 @@ #include "cli_plugin.h" #include "cli_generate.h" -/* This is the default callback function. But this is typically overwritten */ -#define GENERATE_CALLBACK "overwrite_me" - +/* + * Constants + */ /* variable expand function */ #define GENERATE_EXPAND_XMLDB "expand_dbvar" @@ -725,7 +725,6 @@ yang2cli_container(clicon_handle h, cprintf(cb, ";{\n"); } - yc = NULL; while ((yc = yn_each(ys, yc)) != NULL) if (yang2cli_stmt(h, yc, gt, level+1, state, cb) < 0) @@ -972,10 +971,10 @@ yang2cli(clicon_handle h, goto done; cvec_free(globals); /* Resolve the expand callback functions in the generated syntax. - This "should" only be GENERATE_EXPAND_XMLDB - handle=NULL for global namespace, this means expand callbacks must be in - CLICON namespace, not in a cli frontend plugin. - */ + * This "should" only be GENERATE_EXPAND_XMLDB + * handle=NULL for global namespace, this means expand callbacks must be in + * CLICON namespace, not in a cli frontend plugin. + */ if (cligen_expandv_str2fn(ptnew, (expandv_str2fn_t*)clixon_str2fn, NULL) < 0) goto done; diff --git a/apps/cli/cli_generate.h b/apps/cli/cli_generate.h index 10a4de15..819082d3 100644 --- a/apps/cli/cli_generate.h +++ b/apps/cli/cli_generate.h @@ -37,6 +37,18 @@ #ifndef _CLI_GENERATE_H_ #define _CLI_GENERATE_H_ +/* + * Constants + */ +/* This is the default "virtual" callback function of the auto-cli. It should be overwritten by + * a callback specified in a clispec, such as: + * @code + * set @datamodel, cli_set(); + * @endcode + * where the virtual callback (overwrite_me) is overwritten by cli_set. + */ +#define GENERATE_CALLBACK "overwrite_me" + /* * Prototypes */ diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index f93b8e2a..eb29197d 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -236,6 +236,103 @@ cli_interactive(clicon_handle h) return retval; } +/*! Generate one autocli clispec tree + * + * @param[in] h Clixon handle + * @param[in] name Name of tree + * @param[in] gt genmodel-type, ie HOW to generate the CLI + * @param[in] printgen Print CLI syntax to stderr + * + * Generate clispec (datamodel) from YANG dataspec and add to the set of cligen trees + * (as a separate mode) + * This tree is referenced from the main CLI spec (CLICON_CLISPEC_DIR) using the "tree reference" + * syntax, ie @datamodel + * @param[in] h Clixon handle + * @param[in] printgen Print CLI syntax generated from dbspec + * @retval 0 OK + * @retval -1 Error + * + * @note that yang2cli generates syntax for ALL modules under the loaded yangspec. + */ +static int +autocli_tree(clicon_handle h, + char *name, + enum genmodel_type gt, + int state, + int printgen) +{ + int retval = -1; + parse_tree *pt = NULL; /* cli parse tree */ + yang_stmt *yspec; + + if ((pt = pt_new()) == NULL){ + clicon_err(OE_UNIX, errno, "pt_new"); + goto done; + } + yspec = clicon_dbspec_yang(h); + /* Generate tree (this is where the action is) */ + if (yang2cli(h, yspec, gt, printgen, state, pt) < 0) + goto done; + /* Append cligen tree and name it */ + if (cligen_tree_add(cli_cligen(h), name, pt) < 0) + goto done; + retval = 0; + done: + return retval; +} + +/*! Generate autocli, ie if enabled, generate clispec from YANG and add to cligen parse-trees + * + * Generate clispec (datamodel) from YANG dataspec and add to the set of cligen trees + * (as a separate mode) + * This tree is referenced from the main CLI spec (CLICON_CLISPEC_DIR) using the "tree reference" + * syntax, ie @datamodel + * Also (if enabled) generate a second "state" tree called @datamodelstate + * + * @param[in] h Clixon handle + * @param[in] printgen Print CLI syntax generated from dbspec + * @retval 0 OK + * @retval -1 Error + */ +static int +autocli_start(clicon_handle h, + int printgen) +{ + int retval = -1; + int autocli_model = 0; + cbuf *treename = NULL; + enum genmodel_type gt; + + /* If autocli disabled quit */ + if ((autocli_model = clicon_cli_genmodel(h)) == 0) + goto ok; + /* Get the autocli type, ie HOW the cli is generated (could be much more here) */ + gt = clicon_cli_genmodel_type(h); + /* Create treename cbuf */ + if ((treename = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + /* The tree name is by default @datamodel but can be changed by option (why would one do that?) */ + cprintf(treename, "%s", clicon_cli_model_treename(h)); + if (autocli_tree(h, cbuf_get(treename), gt, 0, printgen) < 0) + goto done; + + /* Create a tree for config+state. This tree's name has appended "state" to @datamodel (XXX) + */ + if (autocli_model > 1){ + cprintf(treename, "state"); + if (autocli_tree(h, cbuf_get(treename), gt, 1, printgen) < 0) + goto done; + } + ok: + retval = 0; + done: + if (treename) + cbuf_free(treename); + return retval; +} + static void usage(clicon_handle h, char *argv0) @@ -536,49 +633,9 @@ main(int argc, if (clicon_nsctx_global_set(h, nsctx_global) < 0) goto done; - /* Create tree generated from dataspec. If no other trees exists, this is - * the only one. - * The following code creates the tree @datamodel - * This tree is referenced from the main CLI spec (CLICON_CLISPEC_DIR) - * using the "tree reference" - * syntax, ie @datamodel - * But note that yang2cli generates syntax for ALL modules, not just for - * . - */ - if (clicon_cli_genmodel(h)){ - parse_tree *pt = NULL; /* cli parse tree */ - cbuf *cbtreename; - parse_tree *pts = NULL; /* cli parse tree */ - - if ((pt = pt_new()) == NULL){ - clicon_err(OE_UNIX, errno, "pt_new"); - goto done; - } - if ((cbtreename = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - cprintf(cbtreename, "%s", clicon_cli_model_treename(h)); - /* Create cli command tree from dbspec - * label this tree @datamodel per default - */ - if (yang2cli(h, yspec, clicon_cli_genmodel_type(h), printgen, 0, pt) < 0) - goto done; - cligen_tree_add(cli_cligen(h), cbuf_get(cbtreename), pt); - /* same for config+state - * label this tree @datamodelstate per default - */ - if ((pts = pt_new()) == NULL){ - clicon_err(OE_UNIX, errno, "pt_new"); - goto done; - } - cprintf(cbtreename, "state"); - if (yang2cli(h, yspec, clicon_cli_genmodel_type(h), 0, 1, pts) < 0) - goto done; - cligen_tree_add(cli_cligen(h), cbuf_get(cbtreename), pts); - if (cbtreename) - cbuf_free(cbtreename); - } + /* Create autocli from YANG */ + if (autocli_start(h, printgen) < 0) + goto done; /* Initialize cli syntax */ if (cli_syntax_load(h) < 0) diff --git a/apps/cli/cli_plugin.c b/apps/cli/cli_plugin.c index e5a956c4..0870926f 100644 --- a/apps/cli/cli_plugin.c +++ b/apps/cli/cli_plugin.c @@ -66,6 +66,7 @@ #include "clixon_cli_api.h" #include "cli_plugin.h" #include "cli_handle.h" +#include "cli_generate.h" /* * @@ -177,7 +178,7 @@ cli_syntax_unload(clicon_handle h) * @param[in] handle Handle to plugin .so module as returned by dlopen * @param[out] error Static error string, if set indicates error * @retval fn Function pointer - * @retval NULL FUnction not found or symbol NULL (check error for proper handling) + * @retval NULL Function not found or symbol NULL (check error for proper handling) * @see see cli_plugin_load where (optional) handle opened * @note the returned function is not type-checked which may result in segv at runtime */ @@ -188,8 +189,15 @@ clixon_str2fn(char *name, { void *fn = NULL; + + /* Reset error */ *error = NULL; + /* Special check for auto-cli. If the virtual callback is used, it should be overwritten later + * by a callback given in the clispec, eg: set @datamodel, cli_set(); + */ + if (strcmp(name, GENERATE_CALLBACK) == 0) + return NULL; /* First check given plugin if any */ if (handle) { diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index 2ddbf623..11146ba0 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -450,7 +450,7 @@ cli_show_config1(clicon_handle h, cvec *nsc = NULL; char *prefix = NULL; - if (cvec_len(argv) < 3 && cvec_len(argv) > 5){ + if (cvec_len(argv) < 3 || cvec_len(argv) > 5){ clicon_err(OE_PLUGIN, 0, "Got %d arguments. Expected: ,,[,, []]", cvec_len(argv)); goto done; @@ -708,7 +708,7 @@ cli_show_auto1(clicon_handle h, char *api_path = NULL; char *prefix = NULL; - if (cvec_len(argv) < 3 && cvec_len(argv) > 4){ + if (cvec_len(argv) < 3 || cvec_len(argv) > 4){ clicon_err(OE_PLUGIN, 0, "Usage: * . (*) generated."); goto done; } @@ -801,6 +801,7 @@ cli_show_auto1(clicon_handle h, * "text"|"xml"|"json"|"cli"|"netconf" (see format_enum) * to print before cli syntax outptu * @see cli_show_auto_state For config and state + * @note SHOULD be used: ... @datamodel, cli_show_auto(,...) to get correct #args */ int cli_show_auto(clicon_handle h, diff --git a/doc/FAQ.md b/doc/FAQ.md index 34c48651..2aaf2793 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -68,11 +68,11 @@ Clixon is written in C. The plugins are written in C. The CLI specification uses [CLIgen](http://github.com/olofhagsand/cligen) ## How to best understand Clixon? -Run the Clixon example, in the [example](../example) directory. +Run the Clixon main example, in the [example](../example) directory or [examples repo](https://github.com/clicon/clixon-examples), or [main documentation](https://clixon-docs.readthedocs.io) ## Hello world? -One of the examples is [a hello world example](../example/hello). Please start with that. +One of the examples is [a hello world example](https://github.com/clicon/clixon-examples/hello). Please start with that. ## How do you build and install Clixon? Clixon: @@ -128,8 +128,7 @@ clicon:x:1001:,www-data ## How do I use the CLI? -The easiest way to use Clixon is via the CLI. Once the backend is started -Example: +The easiest way to use Clixon is via the CLI. In the main example, once the backend is started you can start the auto-cli. Example: ``` clixon_cli -f /usr/local/etc/example.xml cli> set interfaces interface eth9 ? diff --git a/example/main/example_cli.cli b/example/main/example_cli.cli index 7e1a5f11..f6f1e661 100644 --- a/example/main/example_cli.cli +++ b/example/main/example_cli.cli @@ -78,8 +78,8 @@ show("Show a particular state of the system"){ xml("Show configuration as XML"), cli_show_config("candidate", "xml", "/");{ @datamodel, cli_show_auto("candidate", "xml"); } - cli("Show configuration as CLI commands"), cli_show_config("candidate", "cli", "/");{ - @datamodel, cli_show_auto("candidate", "cli"); + cli("Show configuration as CLI commands"), cli_show_config("candidate", "cli", "/", 0, "set ");{ + @datamodel, cli_show_auto("candidate", "cli", "set "); } netconf("Show configuration as netconf edit-config operation"), cli_show_config("candidate", "netconf", "/");{ @datamodel, cli_show_auto("candidate", "netconf"); diff --git a/lib/src/clixon_options.c b/lib/src/clixon_options.c index 9416639d..2bd085e1 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -613,7 +613,7 @@ clicon_option_del(clicon_handle h, * But sometimes there are type conversions, etc which makes it more * convenient to make wrapper functions. Or not? *-----------------------------------------------------------------*/ -/*! Wether to generate CLIgen syntax from datamodel or not (0 or 1) +/*! Whether to generate CLIgen syntax from datamodel or not (0, 1 or 2) * Must be used with a previous clicon_option_exists(). * @param[in] h Clicon handle * @retval flag If set, generate CLI code from yang model, otherwise not diff --git a/test/test_choice.sh b/test/test_choice.sh index 433e296c..a75a7bfd 100755 --- a/test/test_choice.sh +++ b/test/test_choice.sh @@ -189,13 +189,13 @@ new "cli set protocol udp" expectfn "$clixon_cli -1 -f $cfg -l o set system protocol udp" 0 "^$" new "cli get protocol udp" -expectfn "$clixon_cli -1 -f $cfg -l o show configuration cli " 0 "^system protocol udp$" +expectfn "$clixon_cli -1 -f $cfg -l o show configuration cli " 0 "^set system protocol udp$" new "cli change protocol to tcp" expectfn "$clixon_cli -1 -f $cfg -l o set system protocol tcp" 0 "^$" new "cli get protocol tcp" -expectfn "$clixon_cli -1 -f $cfg -l o show configuration cli " 0 "^system protocol tcp$" +expectfn "$clixon_cli -1 -f $cfg -l o show configuration cli " 0 "^set system protocol tcp$" new "cli delete all" expectfn "$clixon_cli -1 -f $cfg -l o delete all" 0 "^$" diff --git a/test/test_cli.sh b/test/test_cli.sh index 6a088e15..c033deb7 100755 --- a/test/test_cli.sh +++ b/test/test_cli.sh @@ -49,24 +49,25 @@ if [ $BE -ne 0 ]; then fi new "cli configure top" -expectfn "$clixon_cli -1 -f $cfg set interfaces" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg set interfaces)" 0 "^$" new "cli show configuration top (no presence)" -expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg show conf cli)" 0 "^$" new "cli configure delete top" -expectfn "$clixon_cli -1 -f $cfg delete interfaces" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg delete interfaces)" 0 "^$" new "cli show configuration delete top" -expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg show conf cli)" 0 "^$" new "cli configure set interfaces" -expectfn "$clixon_cli -1 -f $cfg set interfaces interface eth/0/0" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg set interfaces interface eth/0/0)" 0 "^$" new "cli show configuration" -expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 '^interfaces interface eth/0/0 interfaces interface eth/0/0 enabled true' +expectpart "$($clixon_cli -1 -f $cfg show conf cli)" 0 "^set interfaces interface eth/0/0" "^set interfaces interface eth/0/0 enabled true" new "cli configure using encoded chars data <&" +# problems in changing to expectpart with escapes expectfn "$clixon_cli -1 -f $cfg set interfaces interface eth/0/0 description \"foo<&bar\"" 0 "" new "cli configure using encoded chars name <&" @@ -76,53 +77,53 @@ new "cli failed validate" expectpart "$($clixon_cli -1 -f $cfg -l o validate)" 255 "Validate failed. Edit and try again or discard changes: application missing-element Mandatory variable type" new "cli configure ip addr" -expectfn "$clixon_cli -1 -f $cfg set interfaces interface eth/0/0 ipv4 address 1.2.3.4 prefix-length 24" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg set interfaces interface eth/0/0 ipv4 address 1.2.3.4 prefix-length 24)" 0 "^$" new "cli configure ip descr" -expectfn "$clixon_cli -1 -f $cfg set interfaces interface eth/0/0 description mydesc" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg set interfaces interface eth/0/0 description mydesc)" 0 "^$" new "cli configure ip type" -expectfn "$clixon_cli -1 -f $cfg set interfaces interface eth/0/0 type ex:eth" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg set interfaces interface eth/0/0 type ex:eth)" 0 "^$" new "cli show xpath description" -expectfn "$clixon_cli -1 -f $cfg -l o show xpath /interfaces/interface/description urn:ietf:params:xml:ns:yang:ietf-interfaces" 0 "mydesc" +expectpart "$($clixon_cli -1 -f $cfg -l o show xpath /interfaces/interface/description urn:ietf:params:xml:ns:yang:ietf-interfaces)" 0 "mydesc" new "cli delete description" -expectfn "$clixon_cli -1 -f $cfg -l o delete interfaces interface eth/0/0 description mydesc" 0 "" +expectpart "$($clixon_cli -1 -f $cfg -l o delete interfaces interface eth/0/0 description mydesc)" 0 "" new "cli show xpath no description" -expectfn "$clixon_cli -1 -f $cfg -l o show xpath /interfaces/interface/description urn:ietf:params:xml:ns:yang:ietf-interfaces" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o show xpath /interfaces/interface/description urn:ietf:params:xml:ns:yang:ietf-interfaces)" 0 "^$" new "cli copy interface" -expectfn "$clixon_cli -1 -f $cfg copy interface eth/0/0 to eth99" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg copy interface eth/0/0 to eth99)" 0 "^$" new "cli success validate" -expectfn "$clixon_cli -1 -f $cfg -l o validate" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o validate)" 0 "^$" new "cli commit" -expectfn "$clixon_cli -1 -f $cfg -l o commit" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o commit)" 0 "^$" new "cli save" -expectfn "$clixon_cli -1 -f $cfg -l o save /tmp/foo" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o save /tmp/foo)" 0 "^$" new "cli delete all" -expectfn "$clixon_cli -1 -f $cfg -l o delete all" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o delete all)" 0 "^$" new "cli load" -expectfn "$clixon_cli -1 -f $cfg -l o load /tmp/foo" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o load /tmp/foo)" 0 "^$" new "cli check load" -expectfn "$clixon_cli -1 -f $cfg -l o show conf cli" 0 "interfaces interface eth/0/0 ipv4 enabled true" +expectpart "$($clixon_cli -1 -f $cfg -l o show conf cli)" 0 "interfaces interface eth/0/0 ipv4 enabled true" new "cli debug set" -expectfn "$clixon_cli -1 -f $cfg -l o debug level 1" 0 "^$" +expectpart "$($clixon_cli -1 -f $cfg -l o debug level 1)" 0 "^$" # How to test this? new "cli debug reset" expectfn "$clixon_cli -1 -f $cfg -l o debug level 0" 0 "^$" new "cli rpc" -expectfn "$clixon_cli -1 -f $cfg -l o rpc ipv4" 0 'ipv442' +expectpart "$($clixon_cli -1 -f $cfg -l o rpc ipv4)" 0 'ipv442' if [ $BE -eq 0 ]; then exit # BE diff --git a/test/test_cli_gen.sh b/test/test_cli_gen.sh index ea61433b..c95245f3 100755 --- a/test/test_cli_gen.sh +++ b/test/test_cli_gen.sh @@ -33,6 +33,7 @@ cat < $cfg $clidir /usr/local/lib/$APPNAME/cli $APPNAME + 2 VARS /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/var/$APPNAME/$APPNAME.pidfile @@ -57,14 +58,14 @@ module $APPNAME { } } container exstate{ - config false; - list sender{ - key ref; - leaf ref{ - type string; - } - } - } + config false; + list sender{ + key ref; + leaf ref{ + type string; + } + } + } } EOF @@ -90,6 +91,7 @@ show config @datamodel, cli_show_auto("candidate", "cli", "set "); show state, cli_show_config_state("running", "cli", "/", "set "); show state @datamodelstate, cli_show_auto_state("running", "cli", "set "); show xml, cli_show_config("candidate", "xml", "/"); +show xml @datamodel, cli_show_auto("candidate", "xml"); commit, cli_commit(); discard, discard_changes(); diff --git a/yang/clixon/clixon-config@2020-04-23.yang b/yang/clixon/clixon-config@2020-04-23.yang index 4bee48f5..ecad2d21 100644 --- a/yang/clixon/clixon-config@2020-04-23.yang +++ b/yang/clixon/clixon-config@2020-04-23.yang @@ -402,11 +402,12 @@ module clixon-config { type int32; default 1; description - "If set, generate CLI specification for CLI completion of - loaded Yang modules. This CLI tree can be accessed in CLI - spec files using the tree reference syntax (eg @datamodel). - See also CLICON_CLI_MODEL_TREENAME. - (consider boolean)"; + "0: Do not generate CLISPEC syntax for the auto-cli. + 1: Generate a CLI specification for CLI completion of all loaded Yang modules. + This CLI tree can be accessed in CLI-spec files using the tree reference syntax (eg + @datamodel). + 2: Same including state syntax in a tree called @datamodelstate. + See also CLICON_CLI_MODEL_TREENAME."; } leaf CLICON_CLI_MODEL_TREENAME { type string;