From 62bc153270d9ae0906baf7b254b521b212249561 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 12:55:20 +0200 Subject: [PATCH 01/10] api: added new action "shutdown" to shutdown the instance --- lib/api/actions/shutdown.c | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/api/actions/shutdown.c diff --git a/lib/api/actions/shutdown.c b/lib/api/actions/shutdown.c new file mode 100644 index 000000000..36e670ebc --- /dev/null +++ b/lib/api/actions/shutdown.c @@ -0,0 +1,40 @@ +/** The "shutdown" API action. + * + * @author Steffen Vogel + * @copyright 2017, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include "plugin.h" +#include "api.h" + +static int api_shutdown(struct api_action *h, json_t *args, json_t **resp, struct api_session *s) +{ + killme(SIGTERM); + + return 0; +} + +static struct plugin p = { + .name = "shutdown", + .description = "stop VILLASnode", + .type = PLUGIN_TYPE_API, + .api.cb = api_shutdown +}; + +REGISTER_PLUGIN(&p) From 9d7d9badfcf1f82af34d82f6049a8f03fc96db56 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:31:34 +0200 Subject: [PATCH 02/10] api: allow failed actions to return a response --- lib/api/session.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/api/session.c b/lib/api/session.c index aa96195b8..ee27f4064 100644 --- a/lib/api/session.c +++ b/lib/api/session.c @@ -64,7 +64,8 @@ int api_session_run_command(struct api_session *s, json_t *json_in, json_t **jso char *id; struct plugin *p; - json_t *json_args = NULL, *json_resp; + json_t *json_args = NULL; + json_t *json_resp = NULL; ret = json_unpack(json_in, "{ s: s, s: s, s?: o }", "action", &action, @@ -99,10 +100,12 @@ int api_session_run_command(struct api_session *s, json_t *json_in, json_t **jso "code", ret, "error", "command failed"); else - *json_out = json_pack("{ s: s, s: s, s: o }", + *json_out = json_pack("{ s: s, s: s }", "action", action, - "id", id, - "response", json_resp); + "id", id); + + if (json_resp) + json_object_set(*json_out, "response", json_resp); out: debug(LOG_API, "API request completed with code: %d", ret); From 0c98a01820c1a4d4d91de303119c31739ebf4352 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:33:00 +0200 Subject: [PATCH 03/10] keep reference to orginial config uri --- include/villas/super_node.h | 2 ++ lib/super_node.c | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/include/villas/super_node.h b/include/villas/super_node.h index 1f33dd31a..cf435dfa3 100644 --- a/include/villas/super_node.h +++ b/include/villas/super_node.h @@ -56,6 +56,8 @@ struct super_node { } cli; enum state state; + + char *uri; /**< URI of configuration */ config_t cfg; /**< Pointer to configuration file */ json_t *json; /**< JSON representation of the same config. */ diff --git a/lib/super_node.c b/lib/super_node.c index 0ac613af1..f6dd10888 100644 --- a/lib/super_node.c +++ b/lib/super_node.c @@ -176,6 +176,8 @@ int super_node_parse_uri(struct super_node *sn, const char *uri) afclose(af); else if (f != stdin) fclose(f); + + sn->uri = strdup(uri); return super_node_parse(sn, cfg_root); } @@ -459,6 +461,9 @@ int super_node_destroy(struct super_node *sn) log_destroy(&sn->log); config_destroy(&sn->cfg); + + if (sn->uri) + free(sn->uri); sn->state = STATE_DESTROYED; From 34b384c23d9ed57be605c75440488e0766e468bc Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:34:07 +0200 Subject: [PATCH 04/10] api: show remote address in log --- include/villas/api/session.h | 5 +++++ lib/api.c | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/include/villas/api/session.h b/include/villas/api/session.h index 6c239e501..ccf69b1f7 100644 --- a/include/villas/api/session.h +++ b/include/villas/api/session.h @@ -54,6 +54,11 @@ struct api_session { struct web_buffer body; /**< HTTP body / WS payload */ struct web_buffer headers; /**< HTTP headers */ } response; + + struct { + char name[64]; + char ip[64]; + } peer; bool completed; /**< Did we receive the complete body yet? */ diff --git a/lib/api.c b/lib/api.c index 9c990ca76..b11e1db01 100644 --- a/lib/api.c +++ b/lib/api.c @@ -55,8 +55,10 @@ int api_ws_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void * ret = api_session_init(s, w->api, API_MODE_WS); if (ret) return -1; + + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), s->peer.name, sizeof(s->peer.name), s->peer.ip, sizeof(s->peer.ip)); - debug(LOG_API, "New API session initiated: version=%d, mode=websocket", s->version); + debug(LOG_API, "New API session initiated: version=%d, mode=websocket, remote=%s (%s)", s->version, s->peer.name, s->peer.ip); break; case LWS_CALLBACK_CLOSED: @@ -117,8 +119,10 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void ret = api_session_init(s, w->api, API_MODE_HTTP); if (ret) return -1; + + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), s->peer.name, sizeof(s->peer.name), s->peer.ip, sizeof(s->peer.ip)); - debug(LOG_API, "New API session initiated: version=%d, mode=http", s->version); + debug(LOG_API, "New API session initiated: version=%d, mode=http, remote=%s (%s)", s->version, s->peer.name, s->peer.ip); /* Prepare HTTP response header */ const char headers[] = "HTTP/1.1 200 OK\r\n" From 3db13744042ec2b2713aa71e6bbb232ab69c12ac Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:35:08 +0200 Subject: [PATCH 05/10] api: wait up to 1 sec for all outstanding API requests to be completed. --- lib/api.c | 16 +++++++++++++++- lib/super_node.c | 6 +++--- lib/web.c | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/api.c b/lib/api.c index b11e1db01..a8bdc6504 100644 --- a/lib/api.c +++ b/lib/api.c @@ -56,6 +56,8 @@ int api_ws_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void * if (ret) return -1; + list_push(&w->api->sessions, s); + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), s->peer.name, sizeof(s->peer.name), s->peer.ip, sizeof(s->peer.ip)); debug(LOG_API, "New API session initiated: version=%d, mode=websocket, remote=%s (%s)", s->version, s->peer.name, s->peer.ip); @@ -65,6 +67,8 @@ int api_ws_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void * ret = api_session_destroy(s); if (ret) return -1; + + list_remove(&w->api->sessions, s); debug(LOG_API, "Closed API session"); @@ -120,6 +124,8 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void if (ret) return -1; + list_push(&w->api->sessions, s); + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), s->peer.name, sizeof(s->peer.name), s->peer.ip, sizeof(s->peer.ip)); debug(LOG_API, "New API session initiated: version=%d, mode=http, remote=%s (%s)", s->version, s->peer.name, s->peer.ip); @@ -147,6 +153,9 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void ret = api_session_destroy(s); if (ret) return -1; + + list_remove(&w->api->sessions, s); + break; case LWS_CALLBACK_HTTP_BODY: @@ -170,7 +179,7 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void web_buffer_write(&s->response.body, wsi); if (s->completed && s->response.body.len == 0) - return -1; + return -1; /* Close connection */ break; default: @@ -213,6 +222,11 @@ int api_start(struct api *a) int api_stop(struct api *a) { info("Stopping API sub-system"); + + for (int i = 0; i < 10 && list_length(&a->sessions) > 0; i++) { + info("Wait for API requests to complete"); + usleep(100 * 1e-3); + } list_destroy(&a->sessions, (dtor_cb_t) api_session_destroy, false); diff --git a/lib/super_node.c b/lib/super_node.c index f6dd10888..0541fb023 100644 --- a/lib/super_node.c +++ b/lib/super_node.c @@ -431,11 +431,11 @@ int super_node_stop(struct super_node *sn) } } -#ifdef WITH_WEB - web_stop(&sn->web); -#endif #ifdef WITH_API api_stop(&sn->api); +#endif +#ifdef WITH_WEB + web_stop(&sn->web); #endif log_stop(&sn->log); diff --git a/lib/web.c b/lib/web.c index 5e0f7a3ee..a6e8a7a8d 100644 --- a/lib/web.c +++ b/lib/web.c @@ -236,7 +236,7 @@ int web_start(struct web *w) int web_stop(struct web *w) { - if (w->state == STATE_STARTED) + if (w->state != STATE_STARTED) return 0; info("Stopping Web sub-system"); From c72582c20e68aeb17a52a3ff6e9359d28de7cf62 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:35:44 +0200 Subject: [PATCH 06/10] api: implement restart action --- lib/api/actions/reload.c | 59 ++++++++++++++++++++++++++++++++++++++-- lib/utils.c | 2 +- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/api/actions/reload.c b/lib/api/actions/reload.c index e0ea4392a..02431e16d 100644 --- a/lib/api/actions/reload.c +++ b/lib/api/actions/reload.c @@ -1,4 +1,4 @@ -/** The "restart" API ressource. +/** The "restart" API action. * * @author Steffen Vogel * @copyright 2017, Institute for Automation of Complex Power Systems, EONERC @@ -22,11 +22,64 @@ #include "plugin.h" #include "api.h" +#include "super_node.h" + +#include "log.h" + +static char *config; + +void api_restart_handler() +{ + int ret; + + char *argv[] = { "villas-node", config, NULL }; + + ret = execvpe("/proc/self/exe", argv, environ); + if (ret) + serror("Failed to restart"); +} -/** @todo not implemented yet */ static int api_restart(struct api_action *h, json_t *args, json_t **resp, struct api_session *s) { - return -1; + int ret; + json_error_t err; + + /* If no config is provided via request, we will use the previous one */ + config = strdup(s->api->super_node->uri); + + if (args) { + ret = json_unpack_ex(args, &err, 0, "{ s?: s }", "config", &config); + if (ret < 0) { + *resp = json_string("failed to parse request"); + return -1; + } + } + + /* Increment API restart counter */ + char *scnt = getenv("VILLAS_API_RESTART_COUNT"); + int cnt = scnt ? atoi(scnt) : 0; + char buf[32]; + snprintf(buf, sizeof(buf), "%d", cnt + 1); + + /* We pass some env variables to the new process */ + setenv("VILLAS_API_RESTART_COUNT", buf, 1); + setenv("VILLAS_API_RESTART_REMOTE", s->peer.ip, 1); + + *resp = json_pack("{ s: i, s: s, s: s }", + "restarts", cnt, + "config", config, + "remote", s->peer.ip + ); + + /* Register exit handler */ + ret = atexit(api_restart_handler); + if (ret) + return 0; + + /* Properly terminate current instance */ + killme(SIGTERM); + + return 0; } static struct plugin p = { diff --git a/lib/utils.c b/lib/utils.c index 38e1063e0..a94e7c45b 100644 --- a/lib/utils.c +++ b/lib/utils.c @@ -302,7 +302,7 @@ int signals_init(void (*cb)(int signal, siginfo_t *sinfo, void *ctx)) info("Initialize signals"); struct sigaction sa_quit = { - .sa_flags = SA_SIGINFO, + .sa_flags = SA_SIGINFO | SA_NODEFER, .sa_sigaction = cb }; From a43e430b1abef18a6630874a1bd1031a6a6182c3 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 13:36:08 +0200 Subject: [PATCH 07/10] api: rename reload -> restart --- lib/api/actions/{reload.c => restart.c} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/api/actions/{reload.c => restart.c} (100%) diff --git a/lib/api/actions/reload.c b/lib/api/actions/restart.c similarity index 100% rename from lib/api/actions/reload.c rename to lib/api/actions/restart.c From 889076706872f6ded0e372c2045e2aa52e6f3f67 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 17:57:00 +0200 Subject: [PATCH 08/10] allow loading of json configs --- lib/super_node.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/super_node.c b/lib/super_node.c index 0541fb023..034f8ca67 100644 --- a/lib/super_node.c +++ b/lib/super_node.c @@ -134,6 +134,14 @@ int super_node_parse_uri(struct super_node *sn, const char *uri) config_set_destructor(&sn->cfg, config_dtor); config_set_auto_convert(&sn->cfg, 1); + cfg_root = config_root_setting(&sn->cfg); + + /* Little hack to properly report configuration filename in error messages + * We add the uri as a "hook" object to the root setting. + * See cerror() on how this info is used. + */ + config_setting_set_hook(cfg_root, strdup(uri)); + /* Parse config */ ret = config_read(&sn->cfg, f); if (ret != CONFIG_TRUE) { @@ -143,6 +151,7 @@ int super_node_parse_uri(struct super_node *sn, const char *uri) json_error_t err; json_t *json; + rewind(f); json = json_loadf(f, 0, &err); if (json) { ret = json_to_config(json, cfg_root); @@ -150,27 +159,20 @@ int super_node_parse_uri(struct super_node *sn, const char *uri) error("Failed t convert JSON to configuration file"); } else { - error("Failed to parse configuration"); { INDENT warn("conf: %s in %s:%d", config_error_text(&sn->cfg), uri, config_error_line(&sn->cfg)); warn("json: %s in %s:%d:%d", err.text, err.source, err.line, err.column); } + error("Failed to parse configuration"); } #else - error("Failed to parse configuration"); { INDENT warn("%s in %s:%d", config_error_text(&sn->cfg), uri, config_error_line(&sn->cfg)); } + error("Failed to parse configuration"); #endif } - /* Little hack to properly report configuration filename in error messages - * We add the uri as a "hook" object to the root setting. - * See cerror() on how this info is used. - */ - cfg_root = config_root_setting(&sn->cfg); - config_setting_set_hook(cfg_root, strdup(uri)); - /* Close configuration file */ if (af) afclose(af); From b4af9f029080c5b54ca35ce0f17f5f6600bc6491 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 17:57:36 +0200 Subject: [PATCH 09/10] api: bug fixes --- lib/api.c | 3 ++- lib/api/actions/restart.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/api.c b/lib/api.c index a8bdc6504..ebca6d24a 100644 --- a/lib/api.c +++ b/lib/api.c @@ -154,7 +154,8 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void if (ret) return -1; - list_remove(&w->api->sessions, s); + if (w->api->sessions.state == STATE_INITIALIZED) + list_remove(&w->api->sessions, s); break; diff --git a/lib/api/actions/restart.c b/lib/api/actions/restart.c index 02431e16d..b9473a950 100644 --- a/lib/api/actions/restart.c +++ b/lib/api/actions/restart.c @@ -45,7 +45,8 @@ static int api_restart(struct api_action *h, json_t *args, json_t **resp, struct json_error_t err; /* If no config is provided via request, we will use the previous one */ - config = strdup(s->api->super_node->uri); + if (s->api->super_node->uri) + config = strdup(s->api->super_node->uri); if (args) { ret = json_unpack_ex(args, &err, 0, "{ s?: s }", "config", &config); From 3feb5a9dab69a355a25c8811a97c74d957b70377 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Thu, 10 Aug 2017 17:58:01 +0200 Subject: [PATCH 10/10] tests: add integration tests for new API actions --- tests/integration/api-restart.sh | 73 +++++++++++++++++++++++++++++++ tests/integration/api-shutdown.sh | 39 +++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100755 tests/integration/api-restart.sh create mode 100755 tests/integration/api-shutdown.sh diff --git a/tests/integration/api-restart.sh b/tests/integration/api-restart.sh new file mode 100755 index 000000000..fd6f033cb --- /dev/null +++ b/tests/integration/api-restart.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# Integration test for remote API +# +# @author Steffen Vogel +# @copyright 2017, Institute for Automation of Complex Power Systems, EONERC +# @license GNU General Public License (version 3) +# +# VILLASnode +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +################################################################################## + +set -e + +LOCAL_CONF=$(mktemp) +FETCHED_CONF=$(mktemp) + +cat < ${LOCAL_CONF} +{ + "nodes" : { + "node1" : { + "type" : "socket", + "local" : "*:12000", + "remote" : "127.0.0.1:12001" + } + }, + "paths" : [ + { + "in" : "node1", "out" : "node1", + "hooks" : [ { "type" : "print" } ] + } + ] +} +EOF + +# Start without a configuration +villas-node & + +# Wait for node to complete init +sleep 0.2 + +# Restart with configuration +curl -sX POST --data '{ "action" : "restart", "request" : { "config": "'${LOCAL_CONF}'" }, "id" : "5a786626-fbc6-4c04-98c2-48027e68c2fa" }' http://localhost/api/v1 + +# Wait for node to complete init +sleep 0.2 + +# Fetch config via API +curl -sX POST --data '{ "action" : "config", "id" : "5a786626-fbc6-4c04-98c2-48027e68c2fa" }' http://localhost/api/v1 > ${FETCHED_CONF} + +# Shutdown VILLASnode +kill %% + + +# Compare local config with the fetched one +diff -u <(jq -S .response < ${FETCHED_CONF}) <(jq -S . < ${LOCAL_CONF}) +RC=$? + +rm -f ${LOCAL_CONF} ${FETCHED_CONF} + +exit $RC \ No newline at end of file diff --git a/tests/integration/api-shutdown.sh b/tests/integration/api-shutdown.sh new file mode 100755 index 000000000..8e91b96ae --- /dev/null +++ b/tests/integration/api-shutdown.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Integration test for remote API +# +# @author Steffen Vogel +# @copyright 2017, Institute for Automation of Complex Power Systems, EONERC +# @license GNU General Public License (version 3) +# +# VILLASnode +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +################################################################################## + +set -e + +# Start without a configuration +timeout -s SIGKILL 3 villas-node & + +# Wait for node to complete init +sleep 1 + +# Restart with configuration +curl -sX POST --data '{ "action" : "shutdown", "id" : "5a786626-fbc6-4c04-98c2-48027e68c2fa" }' http://localhost/api/v1 + +# Wait returns the return code of villas-node +# which will be 0 (success) in case of normal shutdown +# or <>0 (fail) in case the 3 second timeout was reached +wait $! \ No newline at end of file