mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
Merge branch 'fix-api' into 'develop'
Add new API actions for remote management Closes #116 See merge request !33
This commit is contained in:
commit
10e2175582
11 changed files with 270 additions and 28 deletions
|
@ -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? */
|
||||
|
||||
|
|
|
@ -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. */
|
||||
|
|
25
lib/api.c
25
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);
|
||||
|
||||
|
|
93
lib/api/actions/restart.c
Normal file
93
lib/api/actions/restart.c
Normal file
|
@ -0,0 +1,93 @@
|
|||
/** The "restart" API action.
|
||||
*
|
||||
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*********************************************************************************/
|
||||
|
||||
#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)
|
|
@ -1,4 +1,4 @@
|
|||
/** The "restart" API ressource.
|
||||
/** The "shutdown" API action.
|
||||
*
|
||||
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
|
||||
* @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)
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
73
tests/integration/api-restart.sh
Executable file
73
tests/integration/api-restart.sh
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Integration test for remote API
|
||||
#
|
||||
# @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
|
||||
# @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 <http://www.gnu.org/licenses/>.
|
||||
##################################################################################
|
||||
|
||||
set -e
|
||||
|
||||
LOCAL_CONF=$(mktemp)
|
||||
FETCHED_CONF=$(mktemp)
|
||||
|
||||
cat <<EOF > ${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
|
39
tests/integration/api-shutdown.sh
Executable file
39
tests/integration/api-shutdown.sh
Executable file
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Integration test for remote API
|
||||
#
|
||||
# @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
|
||||
# @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 <http://www.gnu.org/licenses/>.
|
||||
##################################################################################
|
||||
|
||||
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 $!
|
Loading…
Add table
Reference in a new issue