Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 142 additions & 6 deletions src/argparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
Expand All @@ -31,12 +32,15 @@
#include <zlog.h>

#include "argparse.h"
#include "define.h"
#include "persist/persist.h"
#include "utils/log.h"
#include "version.h"
#include "json/json.h"
#include "json/neu_json_param.h"

#include <sqlite3.h>

#define OPTIONAL_ARGUMENT_IS_PRESENT \
((optarg == NULL && optind < argc && argv[optind][0] != '-') \
? (bool) (optarg = argv[optind++]) \
Expand Down Expand Up @@ -65,6 +69,9 @@ const char *usage_text =
" -d, --daemon run as daemon process\n"
" -h, --help show this help message\n"
" stop stop running neuron\n"
" node stop --node <NAME>\n"
" set a south/north node state to STOPPED in the DB\n"
" (offline; restart neuron manually afterwards)\n"
" --log log to the stdout\n"
" --log_level <LEVEL> default log level(DEBUG,NOTICE)\n"
" --reset-password reset dashboard to use default password\n"
Expand Down Expand Up @@ -112,6 +119,120 @@ static inline int reset_password()
return rv;
}

static inline bool file_exists(const char *const path)
{
struct stat buf = { 0 };
return -1 != stat(path, &buf);
}

#define NEURON_DB_FILE "persistence/sqlite.db"

int neu_cli_stop_node_offline(const char *node_name)
{
if (NULL == node_name || '\0' == node_name[0] ||
strlen(node_name) >= NEU_NODE_NAME_LEN) {
fprintf(stderr, "node stop requires --node with 1-%d bytes\n",
NEU_NODE_NAME_LEN - 1);
return 1;
}

for (const unsigned char *p = (const unsigned char *) node_name; *p; ++p) {
if (iscntrl(*p)) {
fprintf(stderr, "node name must not contain control characters\n");
return 1;
}
}

if (!file_exists(NEURON_DB_FILE)) {
fprintf(stderr, "database `%s` not exists\n", NEURON_DB_FILE);
return 1;
}

if (0 != access(NEURON_DB_FILE, R_OK | W_OK)) {
fprintf(stderr,
"no read/write permission on `%s` (errno=%d: %s).\n"
"If Neuron was started with sudo, run this command with the "
"same user, e.g. `sudo ./neuron node stop --node ...`\n",
NEURON_DB_FILE, errno, strerror(errno));
return 1;
}

sqlite3 *db = NULL;
int rc =
sqlite3_open_v2(NEURON_DB_FILE, &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, NULL);
if (SQLITE_OK != rc) {
fprintf(stderr, "failed to open `%s`: %s\n", NEURON_DB_FILE,
db ? sqlite3_errmsg(db) : sqlite3_errstr(rc));
if (db) {
sqlite3_close(db);
}
return 1;
}

sqlite3_busy_timeout(db, 3000);

sqlite3_stmt *stmt = NULL;
const char * select_sql = "SELECT type FROM nodes WHERE name=? LIMIT 1";
rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, NULL);
if (SQLITE_OK != rc) {
fprintf(stderr, "prepare failed: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 1;
}
sqlite3_bind_text(stmt, 1, node_name, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (SQLITE_ROW != rc) {
fprintf(stderr, "node `%s` not found\n", node_name);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
int type = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
if (NEU_NA_TYPE_DRIVER != type && NEU_NA_TYPE_APP != type) {
fprintf(stderr, "node `%s` is not a south/north node\n", node_name);
sqlite3_close(db);
return 1;
}

const char *update_sql =
"UPDATE nodes SET state=? WHERE name=? AND type IN (?,?)";
rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
if (SQLITE_OK != rc) {
fprintf(stderr, "prepare update failed: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return 1;
}
sqlite3_bind_int(stmt, 1, NEU_NODE_RUNNING_STATE_STOPPED);
sqlite3_bind_text(stmt, 2, node_name, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 3, NEU_NA_TYPE_DRIVER);
sqlite3_bind_int(stmt, 4, NEU_NA_TYPE_APP);
rc = sqlite3_step(stmt);
if (SQLITE_DONE != rc) {
fprintf(stderr,
"failed to update node `%s`: %s\n"
"If Neuron is still running, stop it first (`sudo ./neuron "
"stop`), then retry.\n",
node_name, sqlite3_errmsg(db));
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
if (0 == sqlite3_changes(db)) {
fprintf(stderr, "node `%s` was not updated\n", node_name);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
sqlite3_finalize(stmt);
sqlite3_close(db);

printf("node `%s` state set to STOPPED (uid=%u); restart neuron manually\n",
node_name, (unsigned) getuid());
return 0;
}

static inline size_t parse_restart_policy(const char *s, size_t *out)
{
if (0 == strcmp(s, "always")) {
Expand All @@ -135,17 +256,14 @@ static inline size_t parse_restart_policy(const char *s, size_t *out)
return 0;
}

static inline bool file_exists(const char *const path)
{
struct stat buf = { 0 };
return -1 != stat(path, &buf);
}

static inline int load_spec_arg(int argc, char *argv[], neu_cli_args_t *args)
{
int ret = 0;
if (argc > 1 && strcmp(argv[1], "stop") == 0) {
args->stop = true;
} else if (argc > 2 && strcmp(argv[1], "node") == 0 &&
strcmp(argv[2], "stop") == 0) {
args->node_stop = true;
}
return ret;
}
Expand Down Expand Up @@ -450,6 +568,7 @@ void neu_cli_args_init(neu_cli_args_t *args, int argc, char *argv[])
{ "syslog_host", required_argument, NULL, 'S' },
{ "syslog_port", required_argument, NULL, 'P' },
{ "sub_filter_error", no_argument, NULL, 'f' },
{ "node", required_argument, NULL, 'n' },
{ NULL, 0, NULL, 0 },
};

Expand Down Expand Up @@ -546,6 +665,10 @@ void neu_cli_args_init(neu_cli_args_t *args, int argc, char *argv[])
case 'f':
args->sub_filter_err = true;
break;
case 'n':
free(args->node_name);
args->node_name = strdup(optarg);
break;
case '?':
default:
usage();
Expand Down Expand Up @@ -596,6 +719,18 @@ void neu_cli_args_init(neu_cli_args_t *args, int argc, char *argv[])
goto quit;
}

if (args->node_stop) {
if (args->node_name == NULL || args->node_name[0] == '\0') {
fprintf(stderr, "node stop requires --node <NAME>\n");
ret = 1;
goto quit;
}
} else if (args->node_name != NULL) {
fprintf(stderr, "--node requires `node stop`\n");
ret = 1;
goto quit;
}

if (log_level != NULL) {
if (strcmp(log_level, "DEBUG") == 0) {
default_log_level = ZLOG_LEVEL_DEBUG;
Expand All @@ -619,6 +754,7 @@ void neu_cli_args_fini(neu_cli_args_t *args)
free(args->log_init_file);
free(args->config_dir);
free(args->plugin_dir);
free(args->node_name);
free(args->ip);
free(args->syslog_host);
}
Expand Down
5 changes: 5 additions & 0 deletions src/argparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ typedef struct {
char * config_dir;
char * plugin_dir;
bool stop;
bool node_stop;
char * node_name;
char * ip;
int port;
char * syslog_host;
Expand All @@ -72,6 +74,9 @@ typedef struct {
*/
void neu_cli_args_init(neu_cli_args_t *args, int argc, char *argv[]);

/** Offline: set node state to STOPPED in sqlite.db. */
int neu_cli_stop_node_offline(const char *node_name);

/** Clean up the command line arguments.
*/
void neu_cli_args_fini(neu_cli_args_t *args);
Expand Down
12 changes: 11 additions & 1 deletion src/base/group.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ typedef struct tag_elem {
char *name;

neu_datatag_t *tag;
uint64_t order;

UT_hash_handle hh;
} tag_elem_t;
Expand All @@ -37,15 +38,22 @@ struct neu_group {
char *name;

tag_elem_t *tags;
uint64_t next_tag_order;
uint32_t interval;

int64_t timestamp;
pthread_mutex_t mtx;
};

static int tag_order_cmp(tag_elem_t *left, tag_elem_t *right);
static UT_array *to_array(tag_elem_t *tags);
static void update_timestamp(neu_group_t *group);

static int tag_order_cmp(tag_elem_t *left, tag_elem_t *right)
{
return (left->order > right->order) - (left->order < right->order);
}

neu_group_t *neu_group_new(const char *name, uint32_t interval)
{
neu_group_t *group = calloc(1, sizeof(neu_group_t));
Expand Down Expand Up @@ -166,6 +174,7 @@ int neu_group_add_tag(neu_group_t *group, const neu_datatag_t *tag)
return NEU_ERR_EINTERNAL;
}

el->order = group->next_tag_order++;
HASH_ADD_STR(group->tags, name, el);
update_timestamp(group);
pthread_mutex_unlock(&group->mtx);
Expand Down Expand Up @@ -232,7 +241,8 @@ int neu_group_rename_tag(neu_group_t *group, const char *old_name,
el->name = new_el_name;
free(el->tag->name);
el->tag->name = new_tag_name;
HASH_ADD_STR(group->tags, name, el);
HASH_ADD_KEYPTR_INORDER(hh, group->tags, el->name, strlen(el->name), el,
tag_order_cmp);

update_timestamp(group);
ret = NEU_ERR_SUCCESS;
Expand Down
10 changes: 9 additions & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ int main(int argc, char *argv[])
global_timestamp = neu_time_ms();
neu_cli_args_init(&args, argc, argv);

if (args.node_stop) {
rv = neu_cli_stop_node_offline(args.node_name);
neu_cli_args_fini(&args);
return rv;
}

disable_jwt = args.disable_auth;
sub_filter_err = args.sub_filter_err;
snprintf(host_port, sizeof(host_port), "http://%s:%d", args.ip, args.port);
Expand All @@ -191,7 +197,9 @@ int main(int argc, char *argv[])
}

neuron = zlog_get_category("neuron");
zlog_level_switch(neuron, default_log_level);
if (neuron) {
zlog_level_switch(neuron, default_log_level);
}

if (neu_modbus_simulator_init(args.config_dir) != 0) {
nlog_warn("modbus simulator init failed");
Expand Down
43 changes: 42 additions & 1 deletion tests/ft/persist/test_persist.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from neuron.error import *
import shutil
import os
import subprocess
import time

hold_bit = [{"name": "hold_bit", "address": "1!400001.15",
Expand Down Expand Up @@ -507,4 +508,44 @@ def test_all_datatag_type(self):
p.stop()

os.remove(dst_file)
os.rename(dst_backup, dst_file)
os.rename(dst_backup, dst_file)


class TestOfflineNodeStopCli:

def test_offline_node_stop_persists_stopped(self):
process.remove_persistence()
p = process.NeuronProcess()
p.start()
api.add_node_check(node='cli-offline-stop', plugin=PLUGIN_MODBUS_TCP)
response = api.modbus_tcp_node_setting(node='cli-offline-stop',
port=60502)
assert 200 == response.status_code
p.stop()

result = subprocess.run(
['./neuron', 'node', 'stop', '--node', 'cli-offline-stop'],
cwd='build', text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
assert 'STOPPED' in result.stdout

p.start()
try:
time.sleep(1)
response = api.get_nodes_state(node='cli-offline-stop')
assert 200 == response.status_code
assert NEU_NODE_STATE_STOPPED == response.json()['running']
finally:
p.stop()

def test_offline_node_stop_rejects_missing_node(self):
process.remove_persistence()
p = process.NeuronProcess()
p.start()
p.stop()

result = subprocess.run(
['./neuron', 'node', 'stop', '--node', 'missing-node'],
cwd='build', text=True, capture_output=True, check=False)
assert result.returncode != 0
assert 'not found' in result.stderr
Loading
Loading