From c3ab4104a06df88f241837719ecb2df38255bc8f Mon Sep 17 00:00:00 2001 From: Michael Engel Date: Thu, 17 Oct 2024 16:52:49 +0200 Subject: [PATCH] Implemented bluechi-is-online tool Signed-off-by: Michael Engel --- meson.build | 1 + src/is-online/help.c | 36 ++++ src/is-online/help.h | 11 + src/is-online/is-online.c | 422 ++++++++++++++++++++++++++++++++++++++ src/is-online/is-online.h | 12 ++ src/is-online/main.c | 121 +++++++++++ src/is-online/meson.build | 27 +++ src/is-online/opt.h | 17 ++ 8 files changed, 647 insertions(+) create mode 100644 src/is-online/help.c create mode 100644 src/is-online/help.h create mode 100644 src/is-online/is-online.c create mode 100644 src/is-online/is-online.h create mode 100644 src/is-online/main.c create mode 100644 src/is-online/meson.build create mode 100644 src/is-online/opt.h diff --git a/meson.build b/meson.build index 15f3014e8a..4894380016 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ subdir('src/controller') subdir('src/agent') subdir('src/client') subdir('src/proxy') +subdir('src/is-online') # Subdirectory for the API description subdir('data') diff --git a/src/is-online/help.c b/src/is-online/help.c new file mode 100644 index 0000000000..aaeb0d1819 --- /dev/null +++ b/src/is-online/help.c @@ -0,0 +1,36 @@ +/* + * Copyright Contributors to the Eclipse BlueChi project + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#include "help.h" +#include "opt.h" + +static void usage_print_header() { + printf("bluechi-is-online checks and monitors the connection state of BlueChi components\n"); + printf("\n"); +} + +static void usage_print_usage(const char *usage) { + printf("Usage: \n"); + printf(" %s\n", usage); + printf("\n"); +} + + +void usage() { + usage_print_header(); + usage_print_usage("bluechi-is-online [agent|node|system] [OPTIONS]"); + printf("Available commands:\n"); + printf(" help: \t shows this help message\n"); + printf(" version: \t shows the version of bluechi-is-online\n"); + printf("Available options:\n"); + printf(" --%s: \t keeps monitoring as long as [agent|node|system] is online and exits if it detects an offline state.\n", + ARG_MONITOR); + printf(" --%s: \t Wait n seconds for [agent|node|system] to get online.\n", ARG_WAIT); +} + +int method_help(UNUSED Command *command, UNUSED void *userdata) { + usage(); + return 0; +} diff --git a/src/is-online/help.h b/src/is-online/help.h new file mode 100644 index 0000000000..921c8ec585 --- /dev/null +++ b/src/is-online/help.h @@ -0,0 +1,11 @@ +/* + * Copyright Contributors to the Eclipse BlueChi project + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#pragma once + +#include "libbluechi/cli/command.h" + +int method_help(Command *command, void *userdata); +void usage(); diff --git a/src/is-online/is-online.c b/src/is-online/is-online.c new file mode 100644 index 0000000000..14a1419324 --- /dev/null +++ b/src/is-online/is-online.c @@ -0,0 +1,422 @@ +#include "libbluechi/bus/utils.h" +#include "libbluechi/common/event-util.h" +#include "libbluechi/common/parse-util.h" +#include "libbluechi/service/shutdown.h" + +#include "is-online.h" +#include "opt.h" + +#define DBUS_SERVICE_NAME "org.freedesktop.DBus" +#define DBUS_SERVICE_PATH "/org/freedesktop/DBus" +#define DBUS_SERVICE_IFACE "org.freedesktop.DBus" +#define DBUS_METHOD_GETNAMEOWNER "GetNameOwner" + + +typedef struct ConnectionState { + bool has_name; + bool is_connected; +} ConnectionState; + +typedef struct ParsedParams { + long initial_wait; + bool should_monitor; + const char *service; +} ParsedParams; + +typedef struct IsOnlineCli { + sd_event *event; + ConnectionState *state; + ParsedParams *params; +} IsOnlineCli; + +bool connection_state_is_online(ConnectionState *state) { + return state->has_name && state->is_connected; +} + +static int parse_status_from_changed_properties(sd_bus_message *m, char **ret_connection_status) { + if (ret_connection_status == NULL) { + fprintf(stderr, "NULL pointer to connection status not allowed\n"); + return -EINVAL; + } + + int r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "{sv}"); + if (r < 0) { + fprintf(stderr, "Failed to read changed properties: %s\n", strerror(-r)); + return r; + } + + for (;;) { + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_DICT_ENTRY, "sv"); + if (r <= 0) { + break; + } + + const char *key = NULL; + r = sd_bus_message_read(m, "s", &key); + if (r < 0) { + fprintf(stderr, "Failed to read next unit changed property: %s\n", strerror(-r)); + return r; + } + if (r == 0) { + break; + } + + /* only process property changes for the node status */ + if (!streq(key, "Status")) { + break; + } + + /* node status is always of type string */ + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_VARIANT, "s"); + if (r < 0) { + fprintf(stderr, "Failed to enter content of variant type: %s\n", strerror(-r)); + return r; + } + char *status = NULL; + r = sd_bus_message_read(m, "s", &status); + if (r < 0) { + fprintf(stderr, "Failed to read value of changed property: %s\n", strerror(-r)); + return r; + } + *ret_connection_status = strdup(status); + + r = sd_bus_message_exit_container(m); + if (r < 0) { + fprintf(stderr, "Failed to exit container: %s\n", strerror(-r)); + return r; + } + } + + return sd_bus_message_exit_container(m); +} + +static int on_connection_state_changed(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *error) { + (void) sd_bus_message_skip(m, "s"); + + _cleanup_free_ char *con_state = NULL; + int r = parse_status_from_changed_properties(m, &con_state); + if (r < 0) { + return r; + } + if (con_state == NULL) { + fprintf(stderr, "Received connection status change signal with missing 'Status' key.\n"); + return 0; + } + + IsOnlineCli *is_online_cli = (IsOnlineCli *) userdata; + is_online_cli->state->is_connected = (streq(con_state, "online") || streq(con_state, "up")); + + if ((is_online_cli->params->should_monitor && !connection_state_is_online(is_online_cli->state)) || + (!is_online_cli->params->should_monitor && is_online_cli->params->initial_wait > 0 && + connection_state_is_online(is_online_cli->state))) { + return sd_event_exit(is_online_cli->event, 0); + } + + return 0; +} + +static int on_name_owner_changed(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *error) { + const char *name = NULL; + const char *old = NULL; + const char *new = NULL; + int r = sd_bus_message_read(m, "sss", &name, &old, &new); + if (r < 0) { + return r; + } + + IsOnlineCli *is_online_cli = (IsOnlineCli *) userdata; + if (name != NULL && new != NULL && streq(name, is_online_cli->params->service) && !streq(new, "")) { + is_online_cli->state->has_name = true; + } + + return 0; +} + +static int timer_callback(UNUSED sd_event_source *event_source, UNUSED uint64_t usec, void *userdata) { + IsOnlineCli *is_online_cli = (IsOnlineCli *) userdata; + if (connection_state_is_online(is_online_cli->state) && is_online_cli->params->should_monitor) { + return 0; + } + return sd_event_exit(is_online_cli->event, 0); +} + +static int setup_timer(IsOnlineCli *is_online_cli) { + _cleanup_(sd_event_source_unrefp) sd_event_source *event_source = NULL; + int r = 0; + + r = event_reset_time_relative( + is_online_cli->event, + &event_source, + CLOCK_BOOTTIME, + is_online_cli->params->initial_wait * USEC_PER_MSEC, + 0, + timer_callback, + is_online_cli, + 0, + "bluechi-is-online-timer-source", + false); + if (r < 0) { + fprintf(stderr, "Failed to reset agent heartbeat timer: %s\n", strerror(-r)); + return r; + } + + return sd_event_source_set_floating(event_source, true); +} + +static int setup_event_loop(sd_bus *bus, const char *service_name, const char *path, IsOnlineCli *is_online_cli) { + int r = 0; + _cleanup_sd_event_ sd_event *event = NULL; + + r = sd_event_default(&event); + if (r < 0) { + fprintf(stderr, "Failed to create event loop: %s\n", strerror(-r)); + return r; + } + + r = sd_bus_attach_event(bus, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) { + fprintf(stderr, "Failed to attach bus to event: %s\n", strerror(-r)); + return r; + } + + r = event_loop_add_shutdown_signals(event, NULL); + if (r < 0) { + fprintf(stderr, "Failed to add signals to agent event loop: %s\n", strerror(-r)); + return r; + } + + r = sd_bus_match_signal( + bus, + NULL, + service_name, + path, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + on_connection_state_changed, + is_online_cli); + if (r < 0) { + fprintf(stderr, "Failed to create callback for NodeConnectionStateChanged: %s\n", strerror(-r)); + return r; + } + r = sd_bus_match_signal( + bus, + NULL, + DBUS_SERVICE_NAME, + DBUS_SERVICE_PATH, + DBUS_SERVICE_IFACE, + "NameOwnerChanged", + on_name_owner_changed, + is_online_cli); + if (r < 0) { + fprintf(stderr, "Failed to create callback for NameAcquired: %s\n", strerror(-r)); + return r; + } + + is_online_cli->event = steal_pointer(&event); + return 0; +} + +static void start_monitoring( + sd_bus *bus, + const char *service_name, + const char *path, + ConnectionState *state, + ParsedParams *params) { + int r = 0; + IsOnlineCli is_online_cli = { + .state = state, + .params = params, + .event = NULL, + }; + + r = setup_event_loop(bus, service_name, path, &is_online_cli); + if (r < 0) { + return; + } + _cleanup_sd_event_ sd_event *cleanup = is_online_cli.event; + + if (is_online_cli.params->initial_wait > 0) { + r = setup_timer(&is_online_cli); + if (r < 0) { + return; + } + } + + r = sd_event_loop(is_online_cli.event); + if (r < 0) { + fprintf(stderr, "Failed to start event loop: %s\n", strerror(-r)); + return; + } +} + +static bool is_online(sd_bus *bus, const char *service_name, const char *path, const char *iface) { + _cleanup_sd_bus_error_ sd_bus_error prop_error = SD_BUS_ERROR_NULL; + _cleanup_sd_bus_message_ sd_bus_message *prop_reply = NULL; + + int r = sd_bus_get_property(bus, service_name, path, iface, "Status", &prop_error, &prop_reply, "s"); + if (r < 0) { + fprintf(stderr, "Failed to get property: %s\n", strerror(-r)); + return false; + } + + const char *status = NULL; + r = sd_bus_message_read(prop_reply, "s", &status); + if (r < 0) { + fprintf(stderr, "Failed to read property: %s\n", strerror(-r)); + return false; + } + + return (status != NULL && (streq(status, "online") || streq(status, "up"))); +} + +static bool is_service_available(sd_bus *bus, const char *service_name) { + int r = 0; + _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_sd_bus_message_ sd_bus_message *result = NULL; + + r = sd_bus_call_method( + bus, + DBUS_SERVICE_NAME, + DBUS_SERVICE_PATH, + DBUS_SERVICE_IFACE, + DBUS_METHOD_GETNAMEOWNER, + &error, + &result, + "s", + service_name); + + return r >= 0; +} + +static int get_options(Command *command, bool *ret_should_monitor, long *ret_initial_wait) { + const char *initial_wait = command_get_option(command, ARG_WAIT_SHORT); + long parsed_initial_wait = 0; + if (initial_wait != NULL && !streq(initial_wait, "") && + !parse_long(initial_wait, &parsed_initial_wait)) { + fprintf(stderr, "Failed to parse --%s=%s\n", ARG_WAIT, initial_wait); + return -EINVAL; + } + *ret_initial_wait = parsed_initial_wait; + *ret_should_monitor = command_flag_exists(command, ARG_MONITOR_SHORT); + + return 0; +} + +int method_is_online_agent(Command *command, UNUSED void *userdata) { + int r = 0; + bool should_monitor = false; + long initial_wait = 0; + r = get_options(command, &should_monitor, &initial_wait); + if (r < 0) { + return r; + } + + sd_bus *bus = (sd_bus *) userdata; + ConnectionState state = { + .has_name = false, + .is_connected = false, + }; + ParsedParams params = { + .initial_wait = initial_wait, + .should_monitor = should_monitor, + .service = BC_AGENT_DBUS_NAME, + }; + + state.has_name = is_service_available(bus, BC_AGENT_DBUS_NAME); + if (state.has_name) { + state.is_connected = is_online(bus, BC_AGENT_DBUS_NAME, BC_AGENT_OBJECT_PATH, AGENT_INTERFACE); + } + + if ((should_monitor && connection_state_is_online(&state)) || + (initial_wait > 0 && !connection_state_is_online(&state))) { + start_monitoring(bus, params.service, BC_AGENT_OBJECT_PATH, &state, ¶ms); + } + + if (!connection_state_is_online(&state)) { + fprintf(stderr, "BlueChi agent is offline\n"); + return -1; + } + return 0; +} + +int method_is_online_node(Command *command, UNUSED void *userdata) { + int r = 0; + const char *node_name = command->opargv[0]; + _cleanup_free_ char *node_path = NULL; + r = assemble_object_path_string(NODE_OBJECT_PATH_PREFIX, node_name, &node_path); + if (r < 0) { + fprintf(stderr, "Failed to assemble path for node '%s': %s\n", node_name, strerror(-r)); + return r; + } + + bool should_monitor = false; + long initial_wait = 0; + r = get_options(command, &should_monitor, &initial_wait); + if (r < 0) { + return r; + } + + sd_bus *bus = (sd_bus *) userdata; + ConnectionState state = { + .has_name = false, + .is_connected = false, + }; + ParsedParams params = { + .initial_wait = initial_wait, + .should_monitor = should_monitor, + .service = BC_DBUS_NAME, + }; + + state.has_name = is_service_available(bus, BC_DBUS_NAME); + if (state.has_name) { + state.is_connected = is_online(bus, BC_DBUS_NAME, node_path, NODE_INTERFACE); + } + + if ((should_monitor && connection_state_is_online(&state)) || + (params.initial_wait > 0 && !connection_state_is_online(&state))) { + start_monitoring(bus, params.service, node_path, &state, ¶ms); + } + + if (!connection_state_is_online(&state)) { + fprintf(stderr, "BlueChi node '%s' is offline\n", node_name); + return -1; + } + return 0; +} + +int method_is_online_system(Command *command, void *userdata) { + int r = 0; + bool should_monitor = false; + long initial_wait = 0; + r = get_options(command, &should_monitor, &initial_wait); + if (r < 0) { + return r; + } + + sd_bus *bus = (sd_bus *) userdata; + ConnectionState state = { + .has_name = false, + .is_connected = false, + }; + ParsedParams params = { + .initial_wait = initial_wait, + .should_monitor = should_monitor, + .service = BC_DBUS_NAME, + }; + + state.has_name = is_service_available(bus, BC_DBUS_NAME); + if (state.has_name) { + state.is_connected = is_online(bus, BC_DBUS_NAME, BC_OBJECT_PATH, CONTROLLER_INTERFACE); + } + + if ((should_monitor && connection_state_is_online(&state)) || + (params.initial_wait > 0 && !connection_state_is_online(&state))) { + start_monitoring(bus, params.service, BC_OBJECT_PATH, &state, ¶ms); + } + + if (!connection_state_is_online(&state)) { + fprintf(stderr, "BlueChi system either degraded or down\n"); + return -1; + } + return 0; +} diff --git a/src/is-online/is-online.h b/src/is-online/is-online.h new file mode 100644 index 0000000000..74b8beb558 --- /dev/null +++ b/src/is-online/is-online.h @@ -0,0 +1,12 @@ +/* + * Copyright Contributors to the Eclipse BlueChi project + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#pragma once + +#include "libbluechi/cli/command.h" + +int method_is_online_agent(Command *command, void *userdata); +int method_is_online_node(Command *command, void *userdata); +int method_is_online_system(Command *command, void *userdata); diff --git a/src/is-online/main.c b/src/is-online/main.c new file mode 100644 index 0000000000..26505f4876 --- /dev/null +++ b/src/is-online/main.c @@ -0,0 +1,121 @@ +/* + * Copyright Contributors to the Eclipse BlueChi project + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#include +#include +#include + +#include "libbluechi/cli/command.h" +#include "libbluechi/common/opt.h" + +#include "help.h" +#include "is-online.h" +#include "opt.h" + +int method_version(UNUSED Command *command, UNUSED void *userdata) { + printf("bluechi-is-online version %s\n", CONFIG_H_BC_VERSION); + return 0; +} + +const Method methods[] = { + { "help", 0, 0, OPT_NONE, method_help, usage }, + { "version", 0, 0, OPT_NONE, method_version, usage }, + { "agent", 0, 2, OPT_MONITOR | OPT_WAIT, method_is_online_agent, usage }, + { "node", 1, 3, OPT_MONITOR | OPT_WAIT, method_is_online_node, usage }, + { "system", 0, 2, OPT_MONITOR | OPT_WAIT, method_is_online_system, usage }, + { NULL, 0, 0, 0, NULL, NULL } +}; + +const OptionType option_types[] = { + { ARG_MONITOR_SHORT, ARG_MONITOR, OPT_MONITOR }, + { ARG_WAIT_SHORT, ARG_WAIT, OPT_WAIT }, + { 0, NULL, 0 } +}; + +#define GETOPT_OPTSTRING ARG_HELP_SHORT_S +const struct option getopt_options[] = { + { ARG_HELP, no_argument, 0, ARG_HELP_SHORT }, + { ARG_MONITOR, no_argument, 0, ARG_MONITOR_SHORT }, + { ARG_WAIT, required_argument, 0, ARG_WAIT_SHORT }, + { NULL, 0, 0, '\0' } +}; + +static int parse_cli_opts(int argc, char *argv[], Command *command) { + int opt = 0; + + while ((opt = getopt_long(argc, argv, GETOPT_OPTSTRING, getopt_options, NULL)) != -1) { + if (opt == ARG_HELP_SHORT) { + command->is_help = true; + } else if (opt == GETOPT_UNKNOWN_OPTION) { + // Unrecognized option, getopt_long() prints error + return -EINVAL; + } else { + const OptionType *option_type = get_option_type(option_types, opt); + assert(option_type); + // Recognized option, add it to the list + command_add_option(command, opt, optarg, option_type); + } + } + + if (optind < argc) { + command->op = argv[optind++]; + command->opargv = &argv[optind]; + command->opargc = argc - optind; + } else if (!command->is_help) { + fprintf(stderr, "No command given\n"); + return -EINVAL; + } + + return 0; +} + +int open_bus(sd_bus **bus) { + int r = 0; + +#ifdef USE_USER_API_BUS + r = sd_bus_open_user(bus); +#else + r = sd_bus_open_system(bus); +#endif + if (r < 0) { + fprintf(stderr, "Failed to connect to api bus: %s\n", strerror(-r)); + return r; + } + + return 0; +} + +int main(int argc, char *argv[]) { + int r = 0; + _cleanup_command_ Command *command = new_command(); + r = parse_cli_opts(argc, argv, command); + if (r < 0) { + usage(); + return EXIT_FAILURE; + } + if (command->op == NULL && command->is_help) { + usage(); + return EXIT_SUCCESS; + } + + command->method = methods_get_method(command->op, methods); + if (command->method == NULL) { + fprintf(stderr, "Method %s not found\n", command->op); + usage(); + return EXIT_FAILURE; + } + + _cleanup_sd_bus_ sd_bus *bus = NULL; + r = open_bus(&bus); + if (r < 0) { + return EXIT_FAILURE; + } + r = command_execute(command, bus); + if (r < 0) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/is-online/meson.build b/src/is-online/meson.build new file mode 100644 index 0000000000..a8764fb600 --- /dev/null +++ b/src/is-online/meson.build @@ -0,0 +1,27 @@ +# +# Copyright Contributors to the Eclipse BlueChi project +# +# SPDX-License-Identifier: LGPL-2.1-or-later + +is_online_src = [ + 'main.c', + 'help.c', + 'is-online.c', +] + +executable( + 'bluechi-is-online', + is_online_src, + dependencies: [ + systemd_dep, + ], + link_with: [ + bluechi_lib, + ], + c_args: common_cflags, + install: true, + include_directories: include_directories('..') +) + +# build test binaries +# subdir('test') diff --git a/src/is-online/opt.h b/src/is-online/opt.h new file mode 100644 index 0000000000..b39b8173bf --- /dev/null +++ b/src/is-online/opt.h @@ -0,0 +1,17 @@ +/* + * Copyright Contributors to the Eclipse BlueChi project + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +#pragma once + +#define OPT_NONE 0u +#define OPT_HELP 1u << 0u +#define OPT_MONITOR 1u << 1u +#define OPT_WAIT 1u << 2u + +#define ARG_MONITOR "monitor" +#define ARG_MONITOR_SHORT 1000 + +#define ARG_WAIT "wait" +#define ARG_WAIT_SHORT 1001