diff --git a/src/report/meson.build b/src/report/meson.build index 2813f9d033..26d1bbfdc3 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -6,4 +6,13 @@ executables += [ 'public' : true, 'sources' : files('report.c'), }, + + libexec_template + { + 'name' : 'systemd-report-basic', + 'public' : true, + 'sources' : files( + 'report-basic-server.c', + 'report-basic.c', + ), + }, ] diff --git a/src/report/report-basic-server.c b/src/report/report-basic-server.c new file mode 100644 index 0000000000..32ec9b0356 --- /dev/null +++ b/src/report/report-basic-server.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink.h" + +#include "ansi-color.h" +#include "build.h" +#include "facts.h" +#include "format-table.h" +#include "log.h" +#include "main-func.h" +#include "options.h" +#include "report-basic.h" +#include "varlink-util.h" + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL; + int r; + + r = varlink_server_new(&vs, /* flags= */ 0, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = facts_add_to_varlink_server(vs, vl_method_list_facts, vl_method_describe_facts); + if (r < 0) + return log_error_errno(r, "Failed to register Facts varlink interface: %m"); + + r = sd_varlink_server_loop_auto(vs); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} + +static int help(void) { + _cleanup_(table_unrefp) Table *options = NULL; + int r; + + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + printf("%s [OPTIONS...]\n" + "\n%sGenerate a report describing the current system%s\n" + "\n%sOptions:%s\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + ansi_underline(), + ansi_normal()); + table_print(options, stdout); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + int r; + + assert(argc >= 0); + assert(argv); + + OptionParser state = { argc, argv }; + + FOREACH_OPTION(&state, c, /* ret_a= */ NULL, /* on_error= */ return c) + switch (c) { + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + } + + if (state.optind < argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This program takes no arguments."); + + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This program can only run as a Varlink service."); + return 1; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return vl_server(); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/report/report-basic.c b/src/report/report-basic.c new file mode 100644 index 0000000000..381262dfd4 --- /dev/null +++ b/src/report/report-basic.c @@ -0,0 +1,142 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-id128.h" +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "architecture.h" +#include "facts.h" +#include "hostname-setup.h" +#include "report-basic.h" +#include "virt.h" + +static int architecture_generate(FactFamilyContext *context, void *userdata) { + assert(context); + + return fact_build_send_string( + context, + /* object= */ NULL, + architecture_to_string(uname_architecture())); +} + +static int boot_id_generate(FactFamilyContext *context, void *userdata) { + sd_id128_t id; + int r; + + assert(context); + + r = sd_id128_get_boot(&id); + if (r < 0) + return r; + + return fact_build_send_string( + context, + /* object= */ NULL, + SD_ID128_TO_STRING(id)); +} + +static int hostname_generate(FactFamilyContext *context, void *userdata) { + _cleanup_free_ char *hostname = NULL; + int r; + + assert(context); + + r = gethostname_full(GET_HOSTNAME_ALLOW_LOCALHOST | GET_HOSTNAME_FALLBACK_DEFAULT, &hostname); + if (r < 0) + return r; + + return fact_build_send_string( + context, + /* object= */ NULL, + hostname); +} + +static int kernel_version_generate(FactFamilyContext *context, void *userdata) { + struct utsname u; + + assert(context); + + assert_se(uname(&u) >= 0); + + return fact_build_send_string( + context, + /* object= */ NULL, + u.release); +} + +static int machine_id_generate(FactFamilyContext *context, void *userdata) { + sd_id128_t id; + int r; + + assert(context); + + r = sd_id128_get_machine(&id); + if (r < 0) + return r; + + return fact_build_send_string( + context, + /* object= */ NULL, + SD_ID128_TO_STRING(id)); +} + +static int virtualization_generate(FactFamilyContext *context, void *userdata) { + Virtualization v; + + assert(context); + + v = detect_virtualization(); + if (v < 0) + return v; + + return fact_build_send_string( + context, + /* object= */ NULL, + virtualization_to_string(v)); +} + +static const FactFamily fact_family_table[] = { + /* Keep facts ordered alphabetically */ + { + .name = FACT_IO_SYSTEMD_BASIC "Architecture", + .description = "CPU architecture", + .generate = architecture_generate, + }, + { + .name = FACT_IO_SYSTEMD_BASIC "BootID", + .description = "Current boot ID", + .generate = boot_id_generate, + }, + { + .name = FACT_IO_SYSTEMD_BASIC "Hostname", + .description = "System hostname", + .generate = hostname_generate, + }, + { + .name = FACT_IO_SYSTEMD_BASIC "KernelVersion", + .description = "Kernel version", + .generate = kernel_version_generate, + }, + { + .name = FACT_IO_SYSTEMD_BASIC "MachineID", + .description = "Machine ID", + .generate = machine_id_generate, + }, + { + .name = FACT_IO_SYSTEMD_BASIC "Virtualization", + .description = "Virtualization type", + .generate = virtualization_generate, + }, + {} +}; + +int vl_method_describe_facts(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return facts_method_describe(fact_family_table, link, parameters, flags, userdata); +} + +int vl_method_list_facts(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return facts_method_list(fact_family_table, link, parameters, flags, userdata); +} diff --git a/src/report/report-basic.h b/src/report/report-basic.h new file mode 100644 index 0000000000..b24613edb6 --- /dev/null +++ b/src/report/report-basic.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +#define FACT_IO_SYSTEMD_BASIC "io.systemd.Basic." + +int vl_method_list_facts(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int vl_method_describe_facts(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/src/report/report.c b/src/report/report.c index f93b3076c6..108b24e470 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -20,14 +20,15 @@ #include "runtime-scope.h" #include "set.h" #include "sort-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" #include "varlink-idl-util.h" #include "verbs.h" -#define METRICS_MAX 1024U -#define METRICS_LINKS_MAX 128U +#define METRICS_OR_FACTS_MAX 1024U +#define METRICS_OR_FACTS_LINKS_MAX 128U #define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */ static PagerFlags arg_pager_flags = 0; @@ -39,17 +40,21 @@ static char **arg_matches = NULL; STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep); typedef enum Action { - ACTION_LIST, - ACTION_DESCRIBE, + ACTION_LIST_METRICS, + ACTION_DESCRIBE_METRICS, + ACTION_LIST_FACTS, + ACTION_DESCRIBE_FACTS, _ACTION_MAX, _ACTION_INVALID = -EINVAL, } Action; +/* The structure for collected "metrics" or "facts". The fields + * are prefixed with just "metrics" for brevity. */ typedef struct Context { Action action; sd_event *event; Set *link_infos; - sd_json_variant **metrics; /* Collected metrics for sorting */ + sd_json_variant **metrics; /* Collected metrics or facts for sorting */ size_t n_metrics, n_skipped_metrics, n_invalid_metrics; } Context; @@ -83,6 +88,15 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( void, trivial_hash_func, trivial_compare_func, LinkInfo, link_info_free); +static const char* const action_method_table[] = { + [ACTION_LIST_METRICS] = "io.systemd.Metrics.List", + [ACTION_DESCRIBE_METRICS] = "io.systemd.Metrics.Describe", + [ACTION_LIST_FACTS] = "io.systemd.Facts.List", + [ACTION_DESCRIBE_FACTS] = "io.systemd.Facts.Describe", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(action_method, Action); + static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) { const char *name_a, *name_b, *object_a, *object_b; sd_json_variant *fields_a, *fields_b; @@ -213,7 +227,7 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) { return VERDICT_MATCH; } -static int metrics_on_query_reply( +static int on_query_reply( sd_varlink *link, sd_json_variant *parameters, const char *error_id, @@ -226,7 +240,10 @@ static int metrics_on_query_reply( Context *context = ASSERT_PTR(li->context); if (error_id) { - if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) + if (STR_IN_SET(error_id, SD_VARLINK_ERROR_METHOD_NOT_FOUND, + SD_VARLINK_ERROR_METHOD_NOT_IMPLEMENTED)) + log_debug("Ignoring Varlink endpoint '%s': %s", li->name, error_id); + else if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) log_warning("Varlink connection to '%s' disconnected prematurely, ignoring.", li->name); else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT)) log_warning("Varlink connection to '%s' timed out, ignoring.", li->name); @@ -236,7 +253,7 @@ static int metrics_on_query_reply( goto finish; } - if (context->n_metrics >= METRICS_MAX) { + if (context->n_metrics >= METRICS_OR_FACTS_MAX) { context->n_skipped_metrics++; goto finish; } @@ -266,7 +283,7 @@ finish: return 0; } -static int metrics_call(Context *context, const char *name, const char *path) { +static int call_collect(Context *context, const char *name, const char *path) { _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r; @@ -285,14 +302,13 @@ static int metrics_call(Context *context, const char *name, const char *path) { if (r < 0) return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); - r = sd_varlink_bind_reply(vl, metrics_on_query_reply); + r = sd_varlink_bind_reply(vl, on_query_reply); if (r < 0) return log_error_errno(r, "Failed to bind reply callback: %m"); - const char *method = context->action == ACTION_LIST ? "io.systemd.Metrics.List" : "io.systemd.Metrics.Describe"; - r = sd_varlink_observe(vl, - method, - /* parameters= */ NULL); + const char *method = ASSERT_PTR(action_method_to_string(context->action)); + + r = sd_varlink_observe(vl, method, /* parameters= */ NULL); if (r < 0) return log_error_errno(r, "Failed to issue %s() call: %m", method); @@ -305,7 +321,6 @@ static int metrics_call(Context *context, const char *name, const char *path) { .link = sd_varlink_ref(vl), .name = strdup(name), }; - if (!li->name) return log_oom(); @@ -318,7 +333,7 @@ static int metrics_call(Context *context, const char *name, const char *path) { return 0; } -static int metrics_output_list(Context *context, Table **ret) { +static int output_collected_list(Context *context, Table **ret) { int r; assert(context); @@ -372,7 +387,7 @@ static int metrics_output_list(Context *context, Table **ret) { return 0; } -static int metrics_output_describe(Context *context, Table **ret) { +static int output_collected_describe(Context *context, Table **ret) { int r; assert(context); @@ -423,7 +438,106 @@ static int metrics_output_describe(Context *context, Table **ret) { return 0; } -static int metrics_output(Context *context) { +static int facts_output_list(Context *context, Table **ret) { + int r; + + assert(context); + + _cleanup_(table_unrefp) Table *table = table_new("family", "object", "value"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_sort(table, (size_t) 0, (size_t) 1, (size_t) 2); + + FOREACH_ARRAY(m, context->metrics, context->n_metrics) { + struct { + const char *name; + const char *object; + sd_json_variant *value; + } d = {}; + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY }, + { "object", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, object), 0 }, + { "value", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, value), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d); + if (r < 0) { + _cleanup_free_ char *t = NULL; + int k = sd_json_variant_format(*m, /* flags= */ 0, &t); + if (k < 0) + return log_error_errno(k, "Failed to format JSON: %m"); + + log_warning_errno(r, "Cannot parse fact, skipping: %s", t); + continue; + } + + r = table_add_many( + table, + TABLE_STRING, d.name, + TABLE_STRING, d.object, + TABLE_JSON, d.value, + TABLE_SET_WEIGHT, 50U); + if (r < 0) + return table_log_add_error(r); + } + + *ret = TAKE_PTR(table); + return 0; +} + +static int facts_output_describe(Context *context, Table **ret) { + int r; + + assert(context); + + _cleanup_(table_unrefp) Table *table = table_new("family", "description"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_sort(table, (size_t) 0, (size_t) 1); + + FOREACH_ARRAY(m, context->metrics, context->n_metrics) { + struct { + const char *name; + const char *description; + } d = {}; + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, name), SD_JSON_MANDATORY }, + { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, description), 0 }, + {} + }; + + r = sd_json_dispatch(*m, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &d); + if (r < 0) { + _cleanup_free_ char *t = NULL; + int k = sd_json_variant_format(*m, /* flags= */ 0, &t); + if (k < 0) + return log_error_errno(k, "Failed to format JSON: %m"); + + log_warning_errno(r, "Cannot parse fact description, skipping: %s", t); + continue; + } + + r = table_add_many( + table, + TABLE_STRING, d.name, + TABLE_STRING, d.description, + TABLE_SET_WEIGHT, 50U); + if (r < 0) + return table_log_add_error(r); + } + + *ret = TAKE_PTR(table); + return 0; +} + +static int output_collected(Context *context) { int r; assert(context); @@ -441,8 +555,12 @@ static int metrics_output(Context *context) { return log_error_errno(r, "Failed to write JSON: %m"); } - if (context->n_metrics == 0 && arg_legend) - log_info("No metrics collected."); + if (context->n_metrics == 0 && arg_legend) { + if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS)) + log_info("No facts collected."); + else + log_info("No metrics collected."); + } return 0; } @@ -450,12 +568,20 @@ static int metrics_output(Context *context) { _cleanup_(table_unrefp) Table *table = NULL; switch(context->action) { - case ACTION_LIST: - r = metrics_output_list(context, &table); + case ACTION_LIST_METRICS: + r = output_collected_list(context, &table); break; - case ACTION_DESCRIBE: - r = metrics_output_describe(context, &table); + case ACTION_DESCRIBE_METRICS: + r = output_collected_describe(context, &table); + break; + + case ACTION_LIST_FACTS: + r = facts_output_list(context, &table); + break; + + case ACTION_DESCRIBE_FACTS: + r = facts_output_describe(context, &table); break; default: @@ -471,10 +597,12 @@ static int metrics_output(Context *context) { } if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { + const char *type = IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS) ? "facts" : "metrics"; + if (table_isempty(table)) - printf("No metrics available.\n"); + printf("No %s available.\n", type); else - printf("\n%zu metrics listed.\n", table_get_rows(table) - 1); + printf("\n%zu %s listed.\n", table_get_rows(table) - 1, type); } return 0; @@ -582,7 +710,7 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) assert(argc >= 1); assert(argv); - assert(IN_SET(action, ACTION_LIST, ACTION_DESCRIBE)); + assert(IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS)); /* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */ arg_json_format_flags |= SD_JSON_FORMAT_SEQ; @@ -613,7 +741,7 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) FOREACH_ARRAY(i, de->entries, de->n_entries) { struct dirent *d = *i; - if (set_size(context.link_infos) >= METRICS_LINKS_MAX) { + if (set_size(context.link_infos) >= METRICS_OR_FACTS_LINKS_MAX) { n_skipped_sources++; break; } @@ -622,7 +750,7 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) if (!p) return log_oom(); - (void) metrics_call(&context, d->d_name, p); + (void) call_collect(&context, d->d_name, p); } } @@ -636,7 +764,7 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) if (r < 0) return log_error_errno(r, "Failed to run event loop: %m"); - r = metrics_output(&context); + r = output_collected(&context); if (r < 0) return r; } @@ -656,6 +784,86 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) return 0; } +static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) { + Action action = data; + int r; + + assert(argc >= 1); + assert(argv); + assert(IN_SET(action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS)); + + /* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */ + arg_json_format_flags |= SD_JSON_FORMAT_SEQ; + + r = parse_metrics_matches(argv + 1); + if (r < 0) + return r; + + _cleanup_(context_done) Context context = { + .action = action, + }; + size_t n_skipped_sources = 0; + + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_free_ char *sources_path = NULL; + r = readdir_sources(&sources_path, &de); + if (r < 0) + return r; + if (r > 0) { + r = sd_event_default(&context.event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_event_set_signal_exit(context.event, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m"); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *d = *i; + + if (set_size(context.link_infos) >= METRICS_OR_FACTS_LINKS_MAX) { + n_skipped_sources++; + break; + } + + _cleanup_free_ char *p = path_join(sources_path, d->d_name); + if (!p) + return log_oom(); + + (void) call_collect(&context, d->d_name, p); + } + } + + if (set_isempty(context.link_infos)) { + if (arg_legend) + log_info("No facts sources found."); + } else { + assert(context.event); + + r = sd_event_loop(context.event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + r = output_collected(&context); + if (r < 0) + return r; + } + + if (n_skipped_sources > 0) + return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Too many facts sources, only %u sources contacted, %zu sources skipped.", + set_size(context.link_infos), n_skipped_sources); + if (context.n_invalid_metrics > 0) + return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN), + "%zu facts are not valid.", + context.n_invalid_metrics); + if (context.n_skipped_metrics > 0) + return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Too many facts, only %zu facts collected, %zu facts skipped.", + context.n_metrics, context.n_skipped_metrics); + return 0; +} + static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *userdata) { int r; @@ -720,11 +928,14 @@ static int help(void) { return log_oom(); printf("%1$s [OPTIONS...] COMMAND ...\n" - "\n%5$sAcquire metrics from local sources.%6$s\n" + "\n%5$sAcquire metrics and facts from local sources.%6$s\n" "\n%3$sCommands:%4$s\n" " metrics [MATCH...] Acquire list of metrics and their values\n" " describe-metrics [MATCH...]\n" " Describe available metrics\n" + " facts [MATCH...] Acquire list of facts and their values\n" + " describe-facts [MATCH...]\n" + " Describe available facts\n" " list-sources Show list of known metrics sources\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" @@ -825,10 +1036,12 @@ static int parse_argv(int argc, char *argv[]) { static int report_main(int argc, char *argv[]) { static const Verb verbs[] = { - { "help", VERB_ANY, 1, 0, verb_help }, - { "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_LIST }, - { "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_DESCRIBE }, - { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, + { "help", VERB_ANY, 1, 0, verb_help }, + { "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_LIST_METRICS }, + { "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics, ACTION_DESCRIBE_METRICS }, + { "facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_LIST_FACTS }, + { "describe-facts", VERB_ANY, VERB_ANY, 0, verb_facts, ACTION_DESCRIBE_FACTS }, + { "list-sources", VERB_ANY, 1, 0, verb_list_sources }, {} }; diff --git a/src/shared/facts.c b/src/shared/facts.c new file mode 100644 index 0000000000..7554126e2b --- /dev/null +++ b/src/shared/facts.c @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink.h" + +#include "facts.h" +#include "json-util.h" +#include "log.h" +#include "varlink-io.systemd.Facts.h" + +int facts_add_to_varlink_server( + sd_varlink_server *server, + sd_varlink_method_t vl_method_list_cb, + sd_varlink_method_t vl_method_describe_cb) { + + int r; + + assert(server); + assert(vl_method_list_cb); + assert(vl_method_describe_cb); + + r = sd_varlink_server_add_interface(server, &vl_interface_io_systemd_Facts); + if (r < 0) + return log_debug_errno(r, "Failed to add varlink facts interface to varlink server: %m"); + + r = sd_varlink_server_bind_method_many( + server, + "io.systemd.Facts.List", vl_method_list_cb, + "io.systemd.Facts.Describe", vl_method_describe_cb); + if (r < 0) + return log_debug_errno(r, "Failed to register varlink facts methods: %m"); + + return 0; +} + +static int fact_family_build_json(const FactFamily *ff, sd_json_variant **ret) { + assert(ff); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("name", ff->name), + SD_JSON_BUILD_PAIR_STRING("description", ff->description)); +} + +int facts_method_describe( + const FactFamily fact_family_table[], + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(fact_family_table); + assert(link); + assert(parameters); + assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)); + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + r = sd_varlink_set_sentinel(link, "io.systemd.Facts.NoSuchFact"); + if (r < 0) + return r; + + for (const FactFamily *ff = fact_family_table; ff && ff->name; ff++) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + r = fact_family_build_json(ff, &v); + if (r < 0) + return log_debug_errno(r, "Failed to describe fact family '%s': %m", ff->name); + + r = sd_varlink_reply(link, v); + if (r < 0) + return log_debug_errno(r, "Failed to send varlink reply: %m"); + } + + return 0; +} + +int facts_method_list( + const FactFamily fact_family_table[], + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(fact_family_table); + assert(link); + assert(parameters); + assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)); + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + r = sd_varlink_set_sentinel(link, "io.systemd.Facts.NoSuchFact"); + if (r < 0) + return r; + + FactFamilyContext ctx = { .link = link }; + for (const FactFamily *ff = fact_family_table; ff && ff->name; ff++) { + assert(ff->generate); + + ctx.fact_family = ff; + r = ff->generate(&ctx, userdata); + if (r < 0) + return log_debug_errno( + r, "Failed to list facts for fact family '%s': %m", ff->name); + } + + return 0; +} + +static int fact_build_send(FactFamilyContext *context, const char *object, sd_json_variant *value) { + assert(context); + assert(value); + assert(context->link); + assert(context->fact_family); + + return sd_varlink_replybo(context->link, + SD_JSON_BUILD_PAIR_STRING("name", context->fact_family->name), + JSON_BUILD_PAIR_STRING_NON_EMPTY("object", object), + SD_JSON_BUILD_PAIR("value", SD_JSON_BUILD_VARIANT(value))); +} + +int fact_build_send_string(FactFamilyContext *context, const char *object, const char *value) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + assert(value); + + r = sd_json_variant_new_string(&v, value); + if (r < 0) + return log_debug_errno(r, "Failed to allocate JSON string: %m"); + + return fact_build_send(context, object, v); +} + +int fact_build_send_unsigned(FactFamilyContext *context, const char *object, uint64_t value) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r; + + r = sd_json_variant_new_unsigned(&v, value); + if (r < 0) + return log_debug_errno(r, "Failed to allocate JSON unsigned: %m"); + + return fact_build_send(context, object, v); +} diff --git a/src/shared/facts.h b/src/shared/facts.h new file mode 100644 index 0000000000..8a8a94cd91 --- /dev/null +++ b/src/shared/facts.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +typedef struct FactFamily FactFamily; + +typedef struct FactFamilyContext { + const FactFamily *fact_family; + sd_varlink *link; +} FactFamilyContext; + +typedef int (*fact_family_generate_func_t)(FactFamilyContext *ffc, void *userdata); + +typedef struct FactFamily { + const char *name; + const char *description; + fact_family_generate_func_t generate; +} FactFamily; + +/* Add io.systemd.Facts interface + methods to an existing varlink server */ +int facts_add_to_varlink_server( + sd_varlink_server *server, + sd_varlink_method_t vl_method_list_cb, + sd_varlink_method_t vl_method_describe_cb); + +int facts_method_describe(const FactFamily fact_family_table[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int facts_method_list(const FactFamily fact_family_table[], sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); + +int fact_build_send_string(FactFamilyContext *context, const char *object, const char *value); +int fact_build_send_unsigned(FactFamilyContext *context, const char *object, uint64_t value); diff --git a/src/shared/meson.build b/src/shared/meson.build index cdbe763d01..e2c1501adb 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -77,6 +77,7 @@ shared_sources = files( 'exit-status.c', 'extension-util.c', 'factory-reset.c', + 'facts.c', 'fdset.c', 'fido2-util.c', 'find-esp.c', @@ -205,6 +206,7 @@ shared_sources = files( 'varlink-io.systemd.BootControl.c', 'varlink-io.systemd.Credentials.c', 'varlink-io.systemd.FactoryReset.c', + 'varlink-io.systemd.Facts.c', 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Import.c', 'varlink-io.systemd.InstanceMetadata.c', diff --git a/src/shared/options.c b/src/shared/options.c index c5118f3342..853df0d38d 100644 --- a/src/shared/options.c +++ b/src/shared/options.c @@ -72,8 +72,6 @@ int option_parse( const Option **ret_option, const char **ret_arg) { - assert(ret_arg); - /* Check and initialize */ if (state->optind == 0) { if (state->argc < 1 || strv_isempty(state->argv)) @@ -226,7 +224,13 @@ int option_parse( if (ret_option) /* Return the matched Option structure to allow the caller to "know" what was matched */ *ret_option = option; - *ret_arg = optval; + + if (ret_arg) + *ret_arg = optval; + else + /* It's fine to omit ret_arg, but only if no options return a value. */ + assert(!optval); + return option->id; } diff --git a/src/shared/options.h b/src/shared/options.h index 7980b69448..afa17d9e30 100644 --- a/src/shared/options.h +++ b/src/shared/options.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "memory-util.h" #include "shared-forward.h" typedef enum OptionFlags { diff --git a/src/shared/varlink-io.systemd.Facts.c b/src/shared/varlink-io.systemd.Facts.c new file mode 100644 index 0000000000..dad1271c72 --- /dev/null +++ b/src/shared/varlink-io.systemd.Facts.c @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-varlink-idl.h" + +#include "varlink-io.systemd.Facts.h" + +static SD_VARLINK_DEFINE_ERROR(NoSuchFact); + +static SD_VARLINK_DEFINE_METHOD_FULL( + List, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Fact family name, e.g. io.systemd.Basic.Hostname"), + SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0), + /* This is currently an unused placeholder. Add examples when we have them. */ + SD_VARLINK_FIELD_COMMENT("Fact object name"), + SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Fact value"), + SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + Describe, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Fact family name, e.g. io.systemd.Basic.Hostname"), + SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Fact family description"), + SD_VARLINK_DEFINE_OUTPUT(description, SD_VARLINK_STRING, 0)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_Facts, + "io.systemd.Facts", + SD_VARLINK_INTERFACE_COMMENT("Facts APIs"), + SD_VARLINK_SYMBOL_COMMENT("Method to get a list of facts and their values"), + &vl_method_List, + SD_VARLINK_SYMBOL_COMMENT("Method to get the fact families"), + &vl_method_Describe, + SD_VARLINK_SYMBOL_COMMENT("No such fact found"), + &vl_error_NoSuchFact); diff --git a/src/shared/varlink-io.systemd.Facts.h b/src/shared/varlink-io.systemd.Facts.h new file mode 100644 index 0000000000..ce07de32fb --- /dev/null +++ b/src/shared/varlink-io.systemd.Facts.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_Facts; diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 876c5b2cad..0b9006e059 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -35,3 +35,26 @@ varlinkctl info /run/systemd/report/io.systemd.Network varlinkctl list-methods /run/systemd/report/io.systemd.Network varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.List {} varlinkctl --more call /run/systemd/report/io.systemd.Network io.systemd.Metrics.Describe {} + +# Make sure the service for "system facts" is enabled +systemctl start systemd-report-basic.socket + +# Test facts verbs +"$REPORT" facts +"$REPORT" facts -j +"$REPORT" facts --no-legend +"$REPORT" describe-facts +"$REPORT" describe-facts -j +"$REPORT" describe-facts --no-legend + +# Test facts with match filters +"$REPORT" facts io +"$REPORT" facts io.systemd piff +"$REPORT" facts piff +"$REPORT" describe-facts io +"$REPORT" describe-facts io.systemd piff +"$REPORT" describe-facts piff + +# Test facts via direct Varlink call on existing socket +varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.List {} +varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Facts.Describe {} diff --git a/units/meson.build b/units/meson.build index 774c02c0ac..02c2db074c 100644 --- a/units/meson.build +++ b/units/meson.build @@ -624,6 +624,8 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { 'file' : 'systemd-report-basic.socket' }, + { 'file' : 'systemd-report-basic@.service.in' }, { 'file' : 'systemd-tpm2-clear.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], diff --git a/units/systemd-report-basic.socket b/units/systemd-report-basic.socket new file mode 100644 index 0000000000..bfa4ea7256 --- /dev/null +++ b/units/systemd-report-basic.socket @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +[Unit] +Description=Report System Basic Facts Socket +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/report/io.systemd.Basic +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 +RemoveOnStop=yes + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-report-basic@.service.in b/units/systemd-report-basic@.service.in new file mode 100644 index 0000000000..043324b5c3 --- /dev/null +++ b/units/systemd-report-basic@.service.in @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +[Unit] +Description=Report System Basic Facts + +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +CapabilityBoundingSet= +DeviceAllow= +DynamicUser=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +PrivateDevices=yes +PrivateIPC=yes +PrivateNetwork=yes +PrivateTmp=disconnected +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service + +ExecStart={{LIBEXECDIR}}/systemd-report-basic