diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e53110..f6fd5f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ * See clicon_stream.[ch] for details * Added stream discovery according to RFC 5277 for netconf and RFC 8040 for restconf * Enabled by CLICON_STREAM_DISCOVERY_RFC5277 and CLICON_STREAM_DISCOVERY_RFC8040. + * Set access/subscribe base URL with: CLICON_STREAM_URL_PREFIX (default https://localhost/streams). + * Example: new stream "foo" will get access URL: https://localhost/streams/foo + * Optional pub/sub support enabled by ./configure --enable-publish + * Set publish URL base with: CLICON_STREAM_PUB_PREFIX (default http://localhost/pub) + * Example: new stream "foo" will get pub URL: https://localhost/pub/foo ### API changes on existing features (you may need to change your code) * Netconf hello capability updated to YANG 1.1 RFC7950 Sec 5.6.4 diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 18f3f942..70517440 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -81,14 +81,20 @@ ce_find_bypid(struct client_entry *ce_list, return NULL; } +/*! Stream callback for netconf stream notification (RFC 5277) + * @param[in] h Clicon handle + * @param[in] event Event as XML + * @param[in] arg Extra argument provided in stream_cb_add + * @see stream_cb_add + */ static int ce_event_cb(clicon_handle h, - void *event, + cxobj *event, void *arg) { struct client_entry *ce = (struct client_entry *)arg; - if (send_msg_notify_xml(ce->ce_s, (cxobj*)event) < 0){ + if (send_msg_notify_xml(ce->ce_s, event) < 0){ if (errno == ECONNRESET || errno == EPIPE){ clicon_log(LOG_WARNING, "client %d reset", ce->ce_nr); #if 0 diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 3d71c2fd..efa705fe 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -91,6 +91,7 @@ backend_terminate(clicon_handle h) yspec_free(yspec); if ((x = clicon_conf_xml(h)) != NULL) xml_free(x); + stream_publish_exit(); clixon_plugin_exit(h); /* Delete all backend plugin RPC callbacks */ rpc_callback_delete_all(); @@ -720,9 +721,9 @@ main(int argc, return -1; } - if (stream_register(h, "NETCONF", "default NETCONF event stream") < 0) + /* Publish stream on pubsub channels XXX conditional? */ + if (stream_publish_init() < 0) goto done; - if ((xmldb_plugin = clicon_xmldb_plugin(h)) == NULL){ clicon_log(LOG_ERR, "No xmldb plugin given (specify option CLICON_XMLDB_PLUGIN).\n"); goto done; diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 3eaf0681..aef61257 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -664,6 +664,7 @@ main(int argc, clicon_err(OE_CFG, errno, "FCGX_Init"); goto done; } + clicon_debug(1, "restconf_main: Opening FCGX socket: %s", sockpath); if ((sock = FCGX_OpenSocket(sockpath, 10)) < 0){ clicon_err(OE_CFG, errno, "FCGX_OpenSocket"); goto done; diff --git a/configure b/configure index ebf7fcef..c43ccbcb 100755 --- a/configure +++ b/configure @@ -686,7 +686,6 @@ infodir docdir oldincludedir includedir -runstatedir localstatedir sharedstatedir sysconfdir @@ -711,6 +710,7 @@ ac_user_opts=' enable_option_checking enable_debug with_cligen +enable_publish with_restconf with_configfile ' @@ -763,7 +763,6 @@ datadir='${datarootdir}' sysconfdir='${prefix}/etc' sharedstatedir='${prefix}/com' localstatedir='${prefix}/var' -runstatedir='${localstatedir}/run' includedir='${prefix}/include' oldincludedir='/usr/include' docdir='${datarootdir}/doc/${PACKAGE}' @@ -1016,15 +1015,6 @@ do | -silent | --silent | --silen | --sile | --sil) silent=yes ;; - -runstatedir | --runstatedir | --runstatedi | --runstated \ - | --runstate | --runstat | --runsta | --runst | --runs \ - | --run | --ru | --r) - ac_prev=runstatedir ;; - -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ - | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ - | --run=* | --ru=* | --r=*) - runstatedir=$ac_optarg ;; - -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ @@ -1162,7 +1152,7 @@ fi for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ - libdir localedir mandir runstatedir + libdir localedir mandir do eval ac_val=\$$ac_var # Remove trailing slashes. @@ -1315,7 +1305,6 @@ Fine tuning of the installation directories: --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] - --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] @@ -1349,6 +1338,8 @@ Optional Features: --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) --enable-FEATURE[=ARG] include FEATURE [ARG=yes] --enable-debug Build with debug symbols, default: no + --enable-publish Enable publish of notification streams using SSE and + curl Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] @@ -2195,6 +2186,7 @@ _ACEOF +# AC_SUBST(var) makes @var@ appear in makefiles. # clixon versions spread to Makefile's (.so files) and variable in build.c @@ -3687,6 +3679,77 @@ if test "${with_cligen}"; then test -d "$with_cligen" && CLIGEN_PREFIX="$with_cligen" fi +# Experimental: Curl publish notification stream to eg Nginx nchan. +# Check whether --enable-publish was given. +if test "${enable_publish+set}" = set; then : + enableval=$enable_publish; + if test "$enableval" = no; then + ac_enable_publish=no + else + ac_enable_publish=yes + fi + +else + ac_enable_publish=no +fi + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: publish is $ac_enable_publish" >&5 +$as_echo "publish is $ac_enable_publish" >&6; } + +if test "$ac_enable_publish" = "yes"; then + # publish streams uses libcurl + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init in -lcurl" >&5 +$as_echo_n "checking for curl_global_init in -lcurl... " >&6; } +if ${ac_cv_lib_curl_curl_global_init+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lcurl $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char curl_global_init (); +int +main () +{ +return curl_global_init (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_curl_curl_global_init=yes +else + ac_cv_lib_curl_curl_global_init=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_global_init" >&5 +$as_echo "$ac_cv_lib_curl_curl_global_init" >&6; } +if test "x$ac_cv_lib_curl_curl_global_init" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBCURL 1 +_ACEOF + + LIBS="-lcurl $LIBS" + +else + as_fn_error $? "libcurl missing" "$LINENO" 5 +fi + + +$as_echo "#define CLIXON_PUBLISH_STREAMS 1" >>confdefs.h + +fi + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5 $as_echo_n "checking for grep that handles long lines and -e... " >&6; } diff --git a/configure.ac b/configure.ac index d5625abc..1a84825f 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,7 @@ AC_DEFINE_UNQUOTED(CLIXON_VERSION_MINOR, $CLIXON_VERSION_MINOR, [Clixon minor re AC_DEFINE_UNQUOTED(CLIXON_VERSION_PATCH, $CLIXON_VERSION_PATCH, [Clixon path version]) +# AC_SUBST(var) makes @var@ appear in makefiles. # clixon versions spread to Makefile's (.so files) and variable in build.c AC_SUBST(CLIXON_VERSION) AC_SUBST(CLIXON_VERSION_STRING) @@ -144,6 +145,23 @@ if test "${with_cligen}"; then test -d "$with_cligen" && CLIGEN_PREFIX="$with_cligen" fi +# Experimental: Curl publish notification stream to eg Nginx nchan. +AC_ARG_ENABLE(publish, AS_HELP_STRING([--enable-publish],[Enable publish of notification streams using SSE and curl]),[ + if test "$enableval" = no; then + ac_enable_publish=no + else + ac_enable_publish=yes + fi + ], + [ ac_enable_publish=no]) +AC_MSG_RESULT(publish is $ac_enable_publish) + +if test "$ac_enable_publish" = "yes"; then + # publish streams uses libcurl + AC_CHECK_LIB(curl, curl_global_init,, AC_MSG_ERROR([libcurl missing])) + AC_DEFINE(CLIXON_PUBLISH_STREAMS, 1, [Enable publish of notification streams using SSE and curl]) +fi + AC_CHECK_HEADERS(cligen/cligen.h,, AC_MSG_ERROR(cligen missing. Try: git clone https://github.com/olofhagsand/cligen.git)) AC_CHECK_LIB(cligen, cligen_init,, AC_MSG_ERROR([CLIgen${CLIGEN_VERSION} missing. Try: git clone https://github.com/olofhagsand/cligen.git])) diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 2ca71910..a7059514 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -125,7 +125,7 @@ text_db2file(struct text_handle *th, char **filename) { int retval = -1; - cbuf *cb; + cbuf *cb = NULL; char *dir; if ((cb = cbuf_new()) == NULL){ diff --git a/example/Makefile.in b/example/Makefile.in index 26345185..bbbd9070 100644 --- a/example/Makefile.in +++ b/example/Makefile.in @@ -59,7 +59,6 @@ NETCONF_PLUGIN = $(APPNAME)_netconf.so RESTCONF_PLUGIN = $(APPNAME)_restconf.so - # Example docker image. PLEASE CHANGE THIS IMAGE = olofhagsand/clixon_example diff --git a/example/example.yang b/example/example.yang index e90bfdef..1bba52d3 100644 --- a/example/example.yang +++ b/example/example.yang @@ -22,12 +22,38 @@ module example { base if:interface-type; } /* Translation function example - See also example_cli */ - list translate{ leaf value{ type string; } } + /* State data (not config) for the example application*/ + container state { + config false; + description "state data for the example application (must be here for example get operation)"; + leaf-list op { + type string; + } + } + /* Example notification as used in RFC 5277 and RFC 8040 */ + notification event { + description "Example notification event."; + leaf event-class { + type string; + description "Event class identifier."; + } + container reportingEntity { + description "Event specific information."; + leaf card { + type string; + description "Line card identifier."; + } + } + leaf severity { + type string; + description "Event severity description."; + } + } rpc client-rpc { description "Example local client-side RPC that is processed by the the netconf/restconf and not sent to the backend. diff --git a/example/example_backend.c b/example/example_backend.c index 334ddf15..1eb6bedb 100644 --- a/example/example_backend.c +++ b/example/example_backend.c @@ -31,17 +31,14 @@ ***** END LICENSE BLOCK ***** - * - * IETF yang routing example */ - - #include #include #include #include #include #include +#include #include /* clicon */ @@ -54,7 +51,7 @@ #include /* forward */ -static int notification_timer_setup(clicon_handle h); +static int example_stream_timer_setup(clicon_handle h); /*! This is called on validate (and commit). Check validity of candidate */ @@ -93,33 +90,33 @@ transaction_commit(clicon_handle h, /*! Routing example notifcation timer handler. Here is where the periodic action is */ static int -notification_timer(int fd, - void *arg) +example_stream_timer(int fd, + void *arg) { int retval = -1; clicon_handle h = (clicon_handle)arg; /* XXX Change to actual netconf notifications */ - if (stream_notify(h, "NETCONF", "faultEthernet0major") < 0) + if (stream_notify(h, "EXAMPLE", "faultEthernet0major") < 0) goto done; - if (notification_timer_setup(h) < 0) + if (example_stream_timer_setup(h) < 0) goto done; retval = 0; done: return retval; } -/*! Set up routing notification timer +/*! Set up example stream notification timer */ static int -notification_timer_setup(clicon_handle h) +example_stream_timer_setup(clicon_handle h) { struct timeval t, t1; gettimeofday(&t, NULL); t1.tv_sec = 5; t1.tv_usec = 0; timeradd(&t, &t1, &t); - return event_reg_timeout(t, notification_timer, h, "notification timer"); + return event_reg_timeout(t, example_stream_timer, h, "example stream timer"); } /*! IETF Routing fib-route rpc @@ -230,8 +227,8 @@ example_reset(clicon_handle h, cxobj *xt = NULL; if (xml_parse_string("" - "loex:loopback" - "", NULL, &xt) < 0) + "loex:loopback" + "", NULL, &xt) < 0) goto done; /* Replace parent w fiorst child */ if (xml_rootchild(xt, 0, &xt) < 0) @@ -266,13 +263,19 @@ example_start(clicon_handle h, return 0; } +int +example_exit(clicon_handle h) +{ + return 0; +} + clixon_plugin_api *clixon_plugin_init(clicon_handle h); static clixon_plugin_api api = { "example", /* name */ clixon_plugin_init, /* init - must be called clixon_plugin_init */ example_start, /* start */ - NULL, /* exit */ + example_exit, /* exit */ .ca_reset=example_reset, /* reset */ .ca_statedata=example_statedata, /* statedata */ .ca_trans_begin=NULL, /* trans begin */ @@ -292,8 +295,21 @@ clixon_plugin_api * clixon_plugin_init(clicon_handle h) { clicon_debug(1, "%s backend", __FUNCTION__); - if (notification_timer_setup(h) < 0) + + /* Example stream initialization: + * 1) Register EXAMPLE stream + * 2) setup timer for notifications, so something happens on stream + * 3) setup stream callbacks for notification to push channel + */ + if (stream_register(h, "EXAMPLE", "Example event stream") < 0) goto done; + /* assumes: CLIXON_PUBLISH_STREAMS, eg configure --enable-publish + */ + if (stream_publish(h, "EXAMPLE") < 0) + goto done; + if (example_stream_timer_setup(h) < 0) + goto done; + /* Register callback for routing rpc calls */ if (rpc_callback_register(h, fib_route, NULL, @@ -310,6 +326,7 @@ clixon_plugin_init(clicon_handle h) "empty"/* Xml tag when callback is made */ ) < 0) goto done; + /* Return plugin API */ return &api; done: diff --git a/include/clixon_config.h.in b/include/clixon_config.h.in index 5b0cb825..f33c59ad 100644 --- a/include/clixon_config.h.in +++ b/include/clixon_config.h.in @@ -6,6 +6,9 @@ /* Location for apps to find default config file */ #undef CLIXON_DEFAULT_CONFIG +/* Enable publish of notification streams using SSE and curl */ +#undef CLIXON_PUBLISH_STREAMS + /* Clixon major release */ #undef CLIXON_VERSION_MAJOR diff --git a/lib/clixon/clixon_stream.h b/lib/clixon/clixon_stream.h index d88c208a..94450cd9 100644 --- a/lib/clixon/clixon_stream.h +++ b/lib/clixon/clixon_stream.h @@ -39,8 +39,13 @@ /* * Types */ -/* subscription callback */ -typedef int (*stream_fn_t)(clicon_handle h, void *event, void *arg); +/* Subscription callback + * @param[in] h Clicon handle + * @param[in] event Event as XML + * @param[in] arg Extra argument provided in stream_cb_add + * @see stream_cb_add + */ +typedef int (*stream_fn_t)(clicon_handle h, cxobj *event, void *arg); struct stream_subscription{ struct stream_subscription *ss_next; @@ -76,6 +81,11 @@ int stream_notify(clicon_handle h, char *stream, const char *event, ...) __attr int stream_notify(clicon_handle h, char *stream, const char *event, ...); #endif +/* Experimental publish streams using SSE */ +int stream_publish(clicon_handle h, char *stream); +int stream_publish_init(); +int stream_publish_exit(); + /* Backward compatible macro for <1.8 */ #define backend_notify_xml(h, stream, level, x) stream_notify_xml(h, stream, x) #define backend_notify(h, stream, level, event) stream_notify(h, stream, event) diff --git a/lib/src/clixon_stream.c b/lib/src/clixon_stream.c index 32d9317d..158bbe97 100644 --- a/lib/src/clixon_stream.c +++ b/lib/src/clixon_stream.c @@ -44,6 +44,7 @@ #include #include #include +#include #include /* cligen */ @@ -52,6 +53,7 @@ /* clicon */ #include "clixon_queue.h" #include "clixon_err.h" +#include "clixon_log.h" #include "clixon_string.h" #include "clixon_hash.h" #include "clixon_handle.h" @@ -146,6 +148,7 @@ stream_get_xml(clicon_handle h, cbuf *cb) { event_stream_t *es = NULL; + char *url_prefix; cprintf(cb, ""); for (es=clicon_stream(h); es; es=es->es_next){ @@ -157,11 +160,9 @@ stream_get_xml(clicon_handle h, if (access){ cprintf(cb, ""); cprintf(cb, "xml"); - /* Note /stream need to be in http proxy declaration - * XXX - */ - cprintf(cb, "/stream/%s", es->es_name); - + url_prefix = clicon_option_str(h, "CLICON_STREAM_URL_PREFIX"); + cprintf(cb, "%s/%s", + url_prefix, es->es_name); cprintf(cb, ""); } cprintf(cb, ""); @@ -377,3 +378,207 @@ stream_notify(clicon_handle h, free(str); return retval; } + +#ifdef CLIXON_PUBLISH_STREAMS + +#include + +/* + * Types (curl) + */ +struct curlbuf{ + size_t b_len; + char *b_buf; +}; + +/* + * For the asynchronous case. I think we must handle the case where of many of these + * come in before we can handle them in the upper-level polling routine. + * realloc. Therefore, we append new data to the userdata buffer. + */ +static size_t +curl_get_cb(void *ptr, + size_t size, + size_t nmemb, + void *userdata) +{ + struct curlbuf *buf = (struct curlbuf *)userdata; + int len; + + len = size*nmemb; + if ((buf->b_buf = realloc(buf->b_buf, buf->b_len+len+1)) == NULL) + return 0; + memcpy(buf->b_buf+buf->b_len, ptr, len); + buf->b_len += len; + buf->b_buf[buf->b_len] = '\0'; + fprintf(stderr, "%s: %s\n", __FUNCTION__, buf->b_buf); + return len; +} + +/*! Send a curl POST request + * @retval -1 fatal error + * @retval 0 expect set but did not expected return or other non-fatal error + * @retval 1 ok + * Note: curl_easy_perform blocks + * Note: New handle is created every time, the handle can be re-used for better TCP performance + * @see same function (url_post) in grideye_curl.c + */ +static int +url_post(char *url, + char *postfields, + char **getdata) +{ + int retval = -1; + CURL *curl = NULL; + char *err = NULL; + struct curlbuf cb = {0, }; + CURLcode errcode; + + /* Try it with curl -X PUT -d '*/ + clicon_debug(1, "%s: curl -X POST -d '%s' %s", + __FUNCTION__, postfields, url); + /* Set up curl for doing the communication with the controller */ + if ((curl = curl_easy_init()) == NULL) { + clicon_debug(1, "curl_easy_init"); + goto done; + } + if ((err = malloc(CURL_ERROR_SIZE)) == NULL) { + clicon_debug(1, "%s: malloc", __FUNCTION__); + goto done; + } + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_get_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &cb); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, err); + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(postfields)); + + if (debug) + 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); + retval = 0; + goto done; + } + if (getdata && cb.b_buf){ + *getdata = cb.b_buf; + cb.b_buf = NULL; + } + retval = 1; + done: + if (err) + free(err); + if (cb.b_buf) + free(cb.b_buf); + if (curl) + curl_easy_cleanup(curl); /* cleanup */ + return retval; +} + +/*! Stream callback for example stream notification + * Push via curl_post to publish stream event + * @param[in] h Clicon handle + * @param[in] event Event as XML + * @param[in] arg Extra argument provided in stream_cb_add + * @see stream_cb_add + */ +static int +stream_publish_cb(clicon_handle h, + cxobj *event, + void *arg) +{ + int retval = -1; + cbuf *u = NULL; /* stream pub (push) url */ + cbuf *d = NULL; /* (XML) data to push */ + char *pub_prefix; + char *result = NULL; + char *stream = (char*)arg; + + clicon_debug(1, "%s", __FUNCTION__); + + /* Create pub url */ + if ((u = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if ((pub_prefix = clicon_option_str(h, "CLICON_STREAM_PUB_PREFIX")) == NULL){ + clicon_err(OE_CFG, ENOENT, "CLICON_STREAM_PUB_PREFIX not defined"); + goto done; + } + + cprintf(u, "%s/%s", pub_prefix, stream); + /* Create XML data as string */ + if ((d = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (clicon_xml2cbuf(d, event, 0, 0) < 0) + goto done; + if (url_post(cbuf_get(u), /* url+stream */ + cbuf_get(d), /* postfields */ + &result) < 0) /* result as xml */ + goto done; + if (result) + clicon_debug(1, "%s: %s", __FUNCTION__, result); + retval = 0; + done: + if (u) + cbuf_free(u); + if (d) + cbuf_free(d); + if (result) + free(result); + return retval; +} +#endif /* CLIXON_PUBLISH_STREAMS */ + +/*! Publish all streams on a pubsub channel, eg using SSE + */ +int +stream_publish(clicon_handle h, + char *stream) +{ +#ifdef CLIXON_PUBLISH_STREAMS + int retval = -1; + + if (stream_cb_add(h, stream, NULL, stream_publish_cb, (void*)stream) < 0) + goto done; + retval = 0; + done: + return retval; +#else + clicon_log(LOG_WARNING, "%s called but CLIXON_PUBLISH_STREAMS not enabled (enable with configure --enable-publish)", __FUNCTION__); + clicon_log_init("xpath", LOG_WARNING, CLICON_LOG_STDERR); + return 0; +#endif +} + +int +stream_publish_init() +{ +#ifdef CLIXON_PUBLISH_STREAMS + int retval = -1; + + if (curl_global_init(CURL_GLOBAL_ALL) != 0){ + clicon_err(OE_PLUGIN, errno, "curl_global_init"); + goto done; + } + retval = 0; + done: + return retval; +#else + return 0; +#endif +} + +int +stream_publish_exit() +{ +#ifdef CLIXON_PUBLISH_STREAMS + curl_global_cleanup(); +#endif + return 0; +} + + diff --git a/test/test_event.sh b/test/test_event.sh index ce403bc7..b3aebb52 100755 --- a/test/test_event.sh +++ b/test/test_event.sh @@ -1,6 +1,11 @@ #!/bin/bash -# Restconf basic functionality -# Assume http server setup, such as nginx described in apps/restconf/README.md +# Tests for event streams using notifications +# Assumptions: +# 1. http server setup, such as nginx described in apps/restconf/README.md +# especially SSE - ngchan setup +# 2. Example stream as Clixon example which needs registration, callback and +# notification generating code + APPNAME=example # include err() and new() functions and creates $dir . ./lib.sh @@ -8,6 +13,7 @@ cfg=$dir/conf.xml fyang=$dir/restconf.yang xml=$dir/xml.xml + # example cat < $cfg @@ -62,7 +68,7 @@ cat < $fyang } container state { config false; - description "state data for example application"; + description "state data for the example application (must be here for example get operation)"; leaf-list op { type string; } @@ -92,28 +98,30 @@ sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c sleep 1 new "netconf event stream discovery RFC5277 Sec 3.2.5" -expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 ']]>]]>' 'NETCONFdefault NETCONF event streamfalse]]>]]>' +expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 ']]>]]>' 'EXAMPLEExample event streamfalse]]>]]>' new "netconf event stream discovery RFC8040 Sec 6.2" -expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 ']]>]]>' 'NETCONFdefault NETCONF event streamfalsexml/stream/NETCONF]]>]]>' +expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 ']]>]]>' 'EXAMPLEExample event streamfalsexmlhttps://localhost/streams/EXAMPLE]]>]]>' new "restconf event stream discovery RFC8040 Sec 6.2" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams" 0 '{"streams": {"stream": \[{"name": "NETCONF","description": "default NETCONF event stream","replay-support": false,"access": \[{"encoding": "xml","location": "/stream/NETCONF"}\]}\]}' +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams" 0 '{"streams": {"stream": \[{"name": "EXAMPLE","description": "Example event stream","replay-support": false,"access": \[{"encoding": "xml","location": "https://localhost/streams/EXAMPLE"}\]}\]}' new "restconf subscribe RFC8040 Sec 6.3, get location" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams/stream=NETCONF/access=xml/location" 0 '{"location": "/stream/NETCONF"}' +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams/stream=EXAMPLE/access=xml/location" 0 '{"location": "https://localhost/streams/EXAMPLE"}' -new "netconf NETCONF subscription" -expectwait "$clixon_netconf -qf $cfg -y $fyang" 'NETCONF]]>]]>' '^]]>]]>20' 5 +if false; then +new "netconf EXAMPLE subscription" +expectwait "$clixon_netconf -qf $cfg -y $fyang" 'EXAMPLE]]>]]>' '^]]>]]>20' 5 -new "netconf NETCONF subscription with simple filter" -expectwait "$clixon_netconf -qf $cfg -y $fyang" "NETCONF]]>]]>" '^]]>]]>20' 5 +new "netconf EXAMPLE subscription with simple filter" +expectwait "$clixon_netconf -qf $cfg -y $fyang" "EXAMPLE]]>]]>" '^]]>]]>20' 5 -new "netconf NETCONF subscription with filter classifier" -expectwait "$clixon_netconf -qf $cfg -y $fyang" "NETCONF]]>]]>" '^]]>]]>20' 5 +new "netconf EXAMPLE subscription with filter classifier" +expectwait "$clixon_netconf -qf $cfg -y $fyang" "EXAMPLE]]>]]>" '^]]>]]>20' 5 +fi -#new "restconf monitor event stream RFC8040 Sec 6.3" -#XXX expectfn "curl -s -X GET http://localhost/stream/NETCONF" 0 '' +new "restconf monitor event stream RFC8040 Sec 6.3" +expectfn "curl -H \"Accept: text/event-stream\" -s -X GET http://localhost/streams/EXAMPLE" 0 '2018-10-14T14:17:50.875370faultEthernet0major' new "Kill restconf daemon" sudo pkill -u www-data clixon_restconf diff --git a/yang/clixon-config@2018-04-30.yang b/yang/clixon-config@2018-04-30.yang index 1d39ffb8..03783e59 100644 --- a/yang/clixon-config@2018-04-30.yang +++ b/yang/clixon-config@2018-04-30.yang @@ -390,5 +390,23 @@ module clixon-config { sections 3.2. If enabled, available streams will appear when doing netconf get or restconf GET"; } + leaf CLICON_STREAM_URL_PREFIX { + type string; + default "https://localhost/streams"; + description "See RFC 8040 Sec 9.3 location leaf: + 'Contains a URL that represents the entry point for + establishing notification delivery via server-sent events.' + Prepend this constant to name of stream. + Example: https://localhost/streams/NETCONF. Note this is the + external URL, not local behind a reverse-proxy"; + } + leaf CLICON_STREAM_PUB_PREFIX { + type string; + default "http://localhost/pub"; + description "For stream publish using eg nchan, the base address + to publish to. + Example: http://localhost/pub/NETCONF. Note this may + be local URL behind reverse-proxy"; + } } }