diff --git a/.github/workflows/on_push.yml b/.github/workflows/on_push.yml index d924db9..5ed655f 100644 --- a/.github/workflows/on_push.yml +++ b/.github/workflows/on_push.yml @@ -40,10 +40,22 @@ jobs: libcmocka-dev \ libasan5 \ iproute2 \ + cmake \ libxml2-utils && sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/lua-5.3.pc \ /usr/lib/x86_64-linux-gnu/pkgconfig/lua.pc && pip3 install 'gcovr == 6' + - name: libubox + run: | + git clone https://github.com/openwrt/libubox.git + cd libubox && mkdir build && cd build + cmake -DBUILD_LUA=off .. && make install + - name: ubus + run: | + git clone https://github.com/openwrt/ubus.git + cd ubus && mkdir ubus && cd ubus + cmake -DBUILD_LUA=off .. + make install - name: mosquitto run: | mosquitto_passwd -c -b /tmp/pwdfile user password @@ -61,10 +73,16 @@ jobs: export ASAN_OPTIONS=verify_asan_link_order=0 ./autogen.sh ./configure --enable-mqtt \ - --enable-mosquitto-auth + --enable-mosquitto-auth \ + --enable-openwrt + build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} \ + make clean all + - name: run unit tests + run: | rm -f /tmp/tmp_file_uuid bash ./test/test_bin_upload.sh & - build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make clean all check + sudo LD_LIBRARY_PATH=/usr/local/lib /usr/local/sbin/ubusd & + sudo LD_LIBRARY_PATH=/usr/local/lib make check - name: generate coverage data run: | mkdir coverage-dir diff --git a/Makefile.am b/Makefile.am index dd0f4ba..ec5a23a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -131,7 +131,9 @@ if ENABLE_MQTT check_PROGRAMS += check_mqtt endif - +if ENABLE_OPENWRT +check_PROGRAMS += check_openwrt +endif check_LTLIBRARIES = check_umplg_plugin_01.la \ check_umplg_plugin_02.la @@ -146,6 +148,10 @@ if ENABLE_MQTT TESTS += check_mqtt endif +if ENABLE_OPENWRT +TESTS += check_openwrt +endif + # umlua tester check_umlua_SOURCES = test/check_umlua.c \ src/utils/umdb.c \ @@ -261,6 +267,33 @@ check_mqtt_LDADD = -lcmocka \ ${SQLITE_LIBS} endif +# openwrt tester +if ENABLE_OPENWRT +check_openwrt_SOURCES = test/check_openwrt.c \ + src/umd/umdaemon.c \ + src/utils/umcounters.c \ + src/services/sysagent/umlua.c \ + src/services/sysagent/umlua_m.c \ + src/utils/umdb.c \ + src/utils/umink_plugin.c +check_openwrt_CFLAGS = ${COMMON_INCLUDES} \ + ${JSON_C_CFLAGS} \ + -DLUA_COMPAT_ALL \ + -DLUA_COMPAT_5_1 \ + -DLUA_COMPAT_5_2 \ + -DLUA_COMPAT_5_3 \ + ${LUA_CFLAGS} \ + ${ASAN_FLAGS} +check_openwrt_LDFLAGS = -export-dynamic +check_openwrt_LDADD = -lcmocka \ + ${JSON_C_LIBS} \ + ${LUA_LIBS} \ + ${SQLITE_LIBS} \ + ${UBUS_LIBS} \ + ${UBOX_LIBS} \ + ${BLOBMSG_JSON_LIBS} +endif + # cleanup rule distclean-local: distclean-ax-prefix-umink-pkg-config-h distclean-ax-prefix-umink-pkg-config-h: @@ -276,3 +309,8 @@ endif if ENABLE_MOSQUITTO_AUTH include src/services/sysagent/plugins/mosquitto_auth/Makefile.am endif + +# OpenWRT support plugin +if ENABLE_OPENWRT +include src/services/sysagent/plugins/openwrt/Makefile.am +endif diff --git a/configure.ac b/configure.ac index dfd5b2b..f1a0ed6 100644 --- a/configure.ac +++ b/configure.ac @@ -154,6 +154,31 @@ if test "x$enable_coap" != "xno"; then PKG_CHECK_MODULES([COAP], [libcoap-3], [], [AC_MSG_ERROR([libcoap-3 not found!])]) fi +# /***********/ +# /* OpenWRT */ +# /***********/ +AC_ARG_ENABLE(openwrt, + [AS_HELP_STRING([--enable-openwrt], [Enable OpenWRT support [default=no]])],, + [enable_openwrt=no]) +AM_CONDITIONAL(ENABLE_OPENWRT, test "x$enable_openwrt" = "xyes") +if test "x$enable_openwrt" != "xno"; then + AC_DEFINE([ENABLE_OPENWRT], [1], [Enable OpenWRT support]) + AC_CHECK_LIB([ubus], + [ubus_connect], + [AC_SUBST([UBUS_LIBS], ["-lubus"])], + [AC_MSG_ERROR([libubus not found!])]) + + AC_CHECK_LIB([ubox], + [blob_buf_init], + [AC_SUBST([UBOX_LIBS], ["-lubox"])], + [AC_MSG_ERROR([libubox not found!])]) + + AC_CHECK_LIB([blobmsg_json], + [blobmsg_add_object], + [AC_SUBST([BLOBMSG_JSON_LIBS], ["-lblobmsg_json"])], + [AC_MSG_ERROR([libblobmsg_json not found!])]) +fi + # /****************************/ # /* Checks for header files. */ # /****************************/ diff --git a/src/services/sysagent/plugins/openwrt/Makefile.am b/src/services/sysagent/plugins/openwrt/Makefile.am new file mode 100644 index 0000000..7958046 --- /dev/null +++ b/src/services/sysagent/plugins/openwrt/Makefile.am @@ -0,0 +1,32 @@ +pkglib_LTLIBRARIES += plg_sysagent_openwrt.la +plg_sysagent_openwrt_la_SOURCES = %reldir%/plg_sysagent_openwrt.c +plg_sysagent_openwrt_la_CFLAGS = ${COMMON_INCLUDES} \ + ${JSON_C_CFLAGS} \ + -Isrc/services/sysagent +plg_sysagent_openwrt_la_LDFLAGS = -version-info 1:0:0 \ + -shared \ + -module \ + -export-dynamic +plg_sysagent_openwrt_la_LIBADD = ${JSON_C_LIBS} \ + ${LUA_LIBS} \ + ${UBUS_LIBS} \ + ${BLOBMSG_JSON_LIBS} + +check_LTLIBRARIES += check_plg_sysagent_openwrt.la +check_plg_sysagent_openwrt_la_SOURCES = %reldir%/plg_sysagent_openwrt.c +check_plg_sysagent_openwrt_la_CFLAGS = ${COMMON_INCLUDES} \ + ${ASAN_FLAGS} \ + ${JSON_C_CFLAGS} \ + -Isrc/services/sysagent +check_plg_sysagent_openwrt_la_LDFLAGS = -version-info 1:0:0 \ + -shared \ + -module \ + -export-dynamic \ + -rpath /tmp +check_plg_sysagent_openwrt_la_LIBADD = ${JSON_C_LIBS} \ + ${LUA_LIBS} \ + ${UBUS_LIBS} \ + ${BLOBMSG_JSON_LIBS} + + + diff --git a/src/services/sysagent/plugins/openwrt/plg_sysagent_openwrt.c b/src/services/sysagent/plugins/openwrt/plg_sysagent_openwrt.c new file mode 100644 index 0000000..e6df90a --- /dev/null +++ b/src/services/sysagent/plugins/openwrt/plg_sysagent_openwrt.c @@ -0,0 +1,319 @@ +/* + * _____ ____ __ + * __ ____ _ / _/ |/ / //_/ + * / // / ' \_/ // / ,< + * \_,_/_/_/_/___/_/|_/_/|_| + * + * SPDX-License-Identifier: MIT + * + */ + +#include "libubox/blobmsg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/*************/ +/* Plugin ID */ +/*************/ +static const char *PLG_ID = "plg_sysagent_openwrt.so"; + +/**********************************************/ +/* list of command implemented by this plugin */ +/**********************************************/ +int COMMANDS[] = { + // end of list marker + -1 +}; + +// globals +static struct ubus_context *uctx; +static pthread_t ubus_th; +static struct blob_buf b; +static umplg_mngr_t *umplgm; + +// list_signals policiy +static const struct blobmsg_policy list_signals_policy[] = {}; + +// run_signal policy order +enum { + RUN_SIGNAL_ID, + RUN_SIGNAL_ARGS, + __RUN_SIGNAL_MAX +}; + +// run_signal policiy +static const struct blobmsg_policy run_signal_policy[] = { + [RUN_SIGNAL_ID] = { .name = "id", .type = BLOBMSG_TYPE_STRING }, + [RUN_SIGNAL_ARGS] = { .name = "args", .type = BLOBMSG_TYPE_STRING } + +}; + +// match umsignal callback +static void +signal_match_cb(umplg_sh_t *shd, void *args) +{ + // skip special signals + if (shd->id[0] == '@') { + return; + } + void *json_list = blobmsg_open_table(&b, NULL); + blobmsg_add_string(&b, "name", shd->id); + // get lua env + struct lua_env_d **env = utarray_eltptr(shd->args, 1); + blobmsg_add_string(&b, "path", ((*env)->path)); + blobmsg_add_u32 (&b, "interval", ((*env)->interval)); + blobmsg_add_u8(&b, "auto_start", UM_ATOMIC_GET(&(*env)->active)); + blobmsg_close_table(&b, json_list); +} + +// run signal method +static int +run_signal(struct ubus_context *ctx, + struct ubus_object *obj, + struct ubus_request_data *req, + const char *method, + struct blob_attr *msg) +{ + blob_buf_init(&b, 0); + struct blob_attr *tb[__RUN_SIGNAL_MAX]; + + blobmsg_parse(run_signal_policy, + __RUN_SIGNAL_MAX, + tb, + blob_data(msg), + blob_len(msg)); + + if (tb[RUN_SIGNAL_ID] != NULL) { + // id and args + char *id = blobmsg_get_string(tb[RUN_SIGNAL_ID]); + char *args = ""; + // check args + if (tb[RUN_SIGNAL_ARGS] != NULL) { + args = blobmsg_get_string(tb[RUN_SIGNAL_ARGS]); + } + + // output buffer + char *buff = NULL; + size_t b_sz = 0; + + // input data + umplg_data_std_t e_d = { .items = NULL }; + umplg_data_std_items_t items = { .table = NULL }; + umplg_data_std_item_t item = { .name = "", .value = args }; + umplg_data_std_item_t auth_item = { .name = "", .value = "" }; + // init std data + umplg_stdd_init(&e_d); + umplg_stdd_item_add(&items, &item); + umplg_stdd_item_add(&items, &auth_item); + umplg_stdd_items_add(&e_d, &items); + + // run signal (set) + int r = umplg_proc_signal(umplgm, id, &e_d, &buff, &b_sz, 0, NULL); + + switch (r) { + case UMPLG_RES_SUCCESS: + if (buff && !blobmsg_add_json_from_string(&b, buff)) { + blobmsg_add_string(&b, "result", buff); + } + break; + case UMPLG_RES_AUTH_ERROR: + blobmsg_add_string(&b, "result", "authentication error"); + break; + case UMPLG_RES_UNKNOWN_SIGNAL: + blobmsg_add_string(&b, "result", "unknown signal"); + break; + default: + blobmsg_add_string(&b, "result", "unknown error"); + break; + } + HASH_CLEAR(hh, items.table); + umplg_stdd_free(&e_d); + free(buff); + + // missing args + } else { + blobmsg_add_string(&b, "result", "missing arguments"); + } + + return ubus_send_reply(ctx, req, b.head); +} + +// list signals method +static int +list_signals(struct ubus_context *ctx, + struct ubus_object *obj, + struct ubus_request_data *req, + const char *method, + struct blob_attr *msg) +{ + + blob_buf_init(&b, 0); + void *json_uri = blobmsg_open_array(&b, "signals"); + + // init target/function specific lua modules + umplg_match_signal(umplgm, "*", &signal_match_cb, NULL); + + blobmsg_close_array(&b, json_uri); + + return ubus_send_reply(ctx, req, b.head); +} + +// ubus object methods +static const struct ubus_method umink_methods[] = { + UBUS_METHOD("list_signals", &list_signals, list_signals_policy), + UBUS_METHOD("run_signal", &run_signal, run_signal_policy), +}; + +// ubus object type +static struct ubus_object_type umink_obj_type = + UBUS_OBJECT_TYPE("signals", umink_methods); + +// umink ubus object +static struct ubus_object umink_object = { + .name = "umink", + .type = &umink_obj_type, + .methods = umink_methods, + .n_methods = ARRAY_SIZE(umink_methods), + +}; + +// ubus thread +void * +thread_ubus(void *args) +{ + // setup ubus socket polling + struct pollfd pfd = { .fd = uctx->sock.fd, .events = POLLIN }; + umd_log(UMD, UMD_LLT_INFO, "plg_openwrt: [ubus thread starting"); + + while (!umd_is_terminating()) { + // poll ubus socket + int r = poll(&pfd, 1, 1000); + + // critical error + if (r == -1) { + umd_log(UMD, + UMD_LLT_ERROR, + "plg_openwrt: [ubus socket error (%s)]", + strerror(errno)); + break; + + // socket inactive + } else if (pfd.revents == 0) { + continue; + + // data or error pending + } else if (pfd.revents & POLLIN) { + ubus_handle_event(uctx); + + // socket error POLLERR/POLLHUP + } else { + umd_log(UMD, UMD_LLT_ERROR, "plg_openwrt: [ubus socket error]"); + break; + } + } + + umd_log(UMD, UMD_LLT_INFO, "plg_openwrt: [ubus thread terminating"); + return NULL; +} + +/****************/ +/* init handler */ +/****************/ +int +init(umplg_mngr_t *pm, umplgd_t *pd) +{ + umplgm = pm; + signal(SIGPIPE, SIG_IGN); + uctx = ubus_connect(NULL); + if (uctx == NULL) { + umd_log(UMD, UMD_LLT_ERROR, "plg_openwrt: [cannot connect to ubusd]"); + return 1; + } + int r = ubus_add_object(uctx, &umink_object); + if (r) { + umd_log(UMD, + UMD_LLT_ERROR, + "plg_openwrt: [cannot add umink object, %s]", + ubus_strerror(r)); + ubus_free(uctx); + uctx = NULL; + return 1; + } + pthread_create(&ubus_th, NULL, &thread_ubus, NULL); + + return 0; +} + +/*********************/ +/* terminate handler */ +/*********************/ +static void +term_phase_0(umplg_mngr_t *pm, umplgd_t *pd) +{ + if (uctx != NULL) { + pthread_join(ubus_th, NULL); + } +} + +static void +term_phase_1(umplg_mngr_t *pm, umplgd_t *pd) +{ + if (uctx != NULL) { + ubus_free(uctx); + } + blob_buf_free(&b); +} + +int +terminate(umplg_mngr_t *pm, umplgd_t *pd, int phase) +{ + switch (phase) { + case 0: + term_phase_0(pm, pd); + break; + case 1: + term_phase_1(pm, pd); + break; + default: + break; + } + return 0; +} + + +/*************************/ +/* local command handler */ +/*************************/ +// GCOVR_EXCL_START +int +run_local(umplg_mngr_t *pm, umplgd_t *pd, int cmd_id, umplg_idata_t *data) +{ + // not used + return 0; +} + +/*******************/ +/* command handler */ +/*******************/ +int +run(umplg_mngr_t *pm, umplgd_t *pd, int cmd_id, umplg_idata_t *data) +{ + // not used + return 0; +} +// GCOVR_EXCL_STOP diff --git a/test/check_openwrt.c b/test/check_openwrt.c new file mode 100644 index 0000000..c5e4f2f --- /dev/null +++ b/test/check_openwrt.c @@ -0,0 +1,357 @@ +/* + * _____ ____ __ + * __ ____ _ / _/ |/ / //_/ + * / // / ' \_/ // / ,< + * \_,_/_/_/_/___/_/|_/_/|_| + * + * SPDX-License-Identifier: MIT + * + */ + +#include "libubox/blobmsg.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// dummy struct for state passing +typedef struct { + umdaemon_t *umd; + umplg_mngr_t *m; +} test_t; + +// fwd declarations +void umlua_shutdown(); +int umlua_init(umplg_mngr_t *pm); +void umlua_start(umplg_mngr_t *pm); +static int run_init(void **state); +static int run_dtor(void **state); + +// globals +static char plg_cfg_fname[128]; +static struct ubus_context *ctx; +static struct blob_buf b; + +static void +load_cfg(umplg_mngr_t *m) +{ + // load plugins configuration + char *fname = plg_cfg_fname; + FILE *f = fopen(fname, "r"); + assert_non_null(f); + if (fseek(f, 0, SEEK_END) < 0) { + fclose(f); + fail(); + } + int32_t fsz = ftell(f); + if (fsz <= 0) { + fclose(f); + fail(); + } + rewind(f); + char *b = calloc(fsz + 1, 1); + fread(b, fsz, 1, f); + fclose(f); + + // process plugins configurataion + m->cfg = json_tokener_parse(b); + if (m->cfg == NULL) { + fail_msg("ERROR: Invalid plugins configuration file"); + } + free(b); +} + +static int +run_init(void **state) +{ + test_t *data = malloc(sizeof(test_t)); + data->umd = umd_create("test_id", "test_type"); + data->m = umplg_new_mngr(); + data->umd->perf = umc_new_ctx(); + + // load dummy cfg + load_cfg(data->m); + + // init lua core + umlua_init(data->m); + + // load MQTT plugin + umplgd_t *p = umplg_load(data->m, ".libs/check_plg_sysagent_openwrt.so"); + assert_non_null(p); + + // start lua envs + umlua_start(data->m); + + // connect to ubus + ctx = ubus_connect(NULL); + assert_non_null(ctx); + ubus_add_uloop(ctx); + + *state = data; + + return 0; +} + +static int +run_dtor(void **state) +{ + test_t *data = *state; + // stop + umd_signal_handler(SIGTERM); + // free + umlua_shutdown(); + json_object_put(data->m->cfg); + umplg_free_mngr(data->m); + umc_free_ctx(data->umd->perf); + umd_destroy(data->umd); + free(data); + ubus_free(ctx); + blob_buf_free(&b); + return 0; +} + +static void +list_signals_cb(struct ubus_request *req, int type, struct blob_attr *msg) +{ + // ubus output + char *str = blobmsg_format_json(msg, true); + assert_non_null(str); + + // add result + blobmsg_add_string(&b, "result", str); + + //free + free(str); +} + +static void +run_signal_w_static_output_cb(struct ubus_request *req, + int type, + struct blob_attr *msg) +{ + // ubus output + char *str = blobmsg_format_json(msg, true); + assert_non_null(str); + + // compare + assert_string_equal(str, "{\"result\":\"test_data\"}"); + + // free + free(str); +} + +static void +run_missing_signal_cb(struct ubus_request *req, int type, struct blob_attr *msg) +{ + // ubus output + char *str = blobmsg_format_json(msg, true); + assert_non_null(str); + + // compare + assert_string_equal(str, "{\"result\":\"unknown signal\"}"); + + // free + free(str); +} + +static void +run_signal_w_args_return_named_arg_cb(struct ubus_request *req, + int type, + struct blob_attr *msg) +{ + // ubus output + char *str = blobmsg_format_json(msg, true); + assert_non_null(str); + + // add result + blobmsg_add_string(&b, "result", str); + + // free + free(str); +} +enum { + RESULT, + __MAX +}; +static const struct blobmsg_policy run_signal_policy[] = { + [RESULT] = { .name = "result", .type = BLOBMSG_TYPE_STRING } +}; + +static void +call_list_signals(void **state) +{ + uint32_t id; + + // look for umink object + int r = ubus_lookup_id(ctx, "umink", &id); + assert_int_equal(r, 0); + + blob_buf_init(&b, 0); + blobmsg_add_u32(&b, "id", id); + r = ubus_invoke(ctx, + id, + "list_signals", + b.head, + list_signals_cb, + NULL, + 2000); + assert_int_equal(r, 0); + + // get result + struct blob_attr *tb[__MAX]; + + // result generated in callback + blobmsg_parse(run_signal_policy, + __MAX, + tb, + blob_data(b.head), + blob_len(b.head)); + assert_non_null(tb[RESULT]); + + // expected result + FILE *f = fopen("./test/ubus_output_01.json", "r"); + assert_non_null(f); + if (fseek(f, 0, SEEK_END) < 0) { + fclose(f); + fail(); + } + int32_t fsz = ftell(f); + if (fsz <= 0) { + fclose(f); + fail(); + } + rewind(f); + char *buff = calloc(fsz, 1); + fread(buff, fsz, 1, f); + fclose(f); + + // get result from ubus call + char *res_str = blobmsg_get_string(tb[RESULT]); + assert_non_null(res_str); + assert_string_equal(res_str, buff); + + // free + free(buff); +} + +static void +call_run_signal_w_static_output(void **state) +{ + uint32_t id; + + // look for umink object + int r = ubus_lookup_id(ctx, "umink", &id); + assert_int_equal(r, 0); + + blob_buf_init(&b, 0); + blobmsg_add_u32(&b, "id", id); + blobmsg_add_json_from_string(&b, "{\"id\": \"TEST_EVENT_01\"}"); + + r = ubus_invoke(ctx, + id, + "run_signal", + b.head, + run_signal_w_static_output_cb, + NULL, + 2000); + assert_int_equal(r, 0); +} + +static void +call_run_missing_signal(void **state) +{ + uint32_t id; + + // look for umink object + int r = ubus_lookup_id(ctx, "umink", &id); + assert_int_equal(r, 0); + + blob_buf_init(&b, 0); + blobmsg_add_u32(&b, "id", id); + blobmsg_add_json_from_string(&b, "{\"id\": \"TEST_EVENT_XX\"}"); + + r = ubus_invoke(ctx, + id, + "run_signal", + b.head, + run_missing_signal_cb, + NULL, + 2000); + assert_int_equal(r, 0); +} + +static void +call_run_signal_w_args_return_named_arg(void **state) +{ + uint32_t id; + + // look for umink object + int r = ubus_lookup_id(ctx, "umink", &id); + assert_int_equal(r, 0); + + blob_buf_init(&b, 0); + blobmsg_add_u32(&b, "id", id); + blobmsg_add_json_from_string( + &b, + "{\"id\": \"TEST_EVENT_03_UBUS\", " + "\"args\": \"{\\\"test_key\\\": \\\"test_arg_data\\\"}\"}"); + + r = ubus_invoke(ctx, + id, + "run_signal", + b.head, + run_signal_w_args_return_named_arg_cb, + NULL, + 2000); + assert_int_equal(r, 0); + + // get result + struct blob_attr *tb[__MAX]; + + // result generated in callback + blobmsg_parse(run_signal_policy, + __MAX, + tb, + blob_data(b.head), + blob_len(b.head)); + assert_non_null(tb[RESULT]); + + // get result from ubus call + char *res_str = blobmsg_get_string(tb[RESULT]); + assert_non_null(res_str); + assert_string_equal(res_str, "{\"test_key\":\"test_arg_data\"}"); +} + + +int +main(int argc, char **argv) +{ + strcpy(plg_cfg_fname, "test/plg_cfg.json"); + + const struct CMUnitTest tests[] = { + cmocka_unit_test(call_list_signals), + cmocka_unit_test(call_run_signal_w_static_output), + cmocka_unit_test(call_run_missing_signal), + cmocka_unit_test(call_run_signal_w_args_return_named_arg), + }; + + int r = cmocka_run_group_tests(tests, run_init, run_dtor); + return r; + +} + diff --git a/test/plg_cfg.json b/test/plg_cfg.json index 102eb90..b548cf5 100644 --- a/test/plg_cfg.json +++ b/test/plg_cfg.json @@ -42,6 +42,15 @@ "TEST_EVENT_03" ] }, + { + "name": "TEST_EVENT_03_UBUS", + "auto_start": false, + "interval": 0, + "path": "test/test_event_03_ubus.lua", + "events": [ + "TEST_EVENT_03_UBUS" + ] + }, { "name": "TEST_EVENT_04", "auto_start": false, diff --git a/test/test_event_03_ubus.lua b/test/test_event_03_ubus.lua new file mode 100644 index 0000000..b8a2147 --- /dev/null +++ b/test/test_event_03_ubus.lua @@ -0,0 +1,2 @@ +local data = M.get_args() +return data[1][1] diff --git a/test/ubus_output_01.json b/test/ubus_output_01.json new file mode 100644 index 0000000..da1a8e1 Binary files /dev/null and b/test/ubus_output_01.json differ