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/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/api.c b/lib/api.c index 9c990ca76..ebca6d24a 100644 --- a/lib/api.c +++ b/lib/api.c @@ -55,14 +55,20 @@ 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; + + 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", 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: ret = api_session_destroy(s); if (ret) return -1; + + list_remove(&w->api->sessions, s); debug(LOG_API, "Closed API session"); @@ -117,8 +123,12 @@ 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; + + 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", 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" @@ -143,6 +153,10 @@ int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void ret = api_session_destroy(s); if (ret) return -1; + + if (w->api->sessions.state == STATE_INITIALIZED) + list_remove(&w->api->sessions, s); + break; case LWS_CALLBACK_HTTP_BODY: @@ -166,7 +180,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: @@ -209,6 +223,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/api/actions/restart.c b/lib/api/actions/restart.c new file mode 100644 index 000000000..b9473a950 --- /dev/null +++ b/lib/api/actions/restart.c @@ -0,0 +1,93 @@ +/** The "restart" 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" +#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"); +} + +static int api_restart(struct api_action *h, json_t *args, json_t **resp, struct api_session *s) +{ + int ret; + json_error_t err; + + /* If no config is provided via request, we will use the previous one */ + 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); + 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 = { + .name = "restart", + .description = "restart VILLASnode with new configuration", + .type = PLUGIN_TYPE_API, + .api.cb = api_restart +}; + +REGISTER_PLUGIN(&p) diff --git a/lib/api/actions/reload.c b/lib/api/actions/shutdown.c similarity index 78% rename from lib/api/actions/reload.c rename to lib/api/actions/shutdown.c index e0ea4392a..36e670ebc 100644 --- a/lib/api/actions/reload.c +++ b/lib/api/actions/shutdown.c @@ -1,4 +1,4 @@ -/** The "restart" API ressource. +/** The "shutdown" API action. * * @author Steffen Vogel * @copyright 2017, Institute for Automation of Complex Power Systems, EONERC @@ -23,17 +23,18 @@ #include "plugin.h" #include "api.h" -/** @todo not implemented yet */ -static int api_restart(struct api_action *h, json_t *args, json_t **resp, struct api_session *s) +static int api_shutdown(struct api_action *h, json_t *args, json_t **resp, struct api_session *s) { - return -1; + killme(SIGTERM); + + return 0; } static struct plugin p = { - .name = "restart", - .description = "restart VILLASnode with new configuration", + .name = "shutdown", + .description = "stop VILLASnode", .type = PLUGIN_TYPE_API, - .api.cb = api_restart + .api.cb = api_shutdown }; REGISTER_PLUGIN(&p) 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); diff --git a/lib/super_node.c b/lib/super_node.c index 0ac613af1..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,32 +159,27 @@ 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); else if (f != stdin) fclose(f); + + sn->uri = strdup(uri); return super_node_parse(sn, cfg_root); } @@ -429,11 +433,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); @@ -459,6 +463,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; 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 }; 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"); 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