From: Lennart Poettering Date: Mon, 15 Jun 2026 08:06:03 +0000 (+0200) Subject: report: turn systemd-report itself into a varlink service X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7683c3281ce11c1fce2e6b1000f5efef6c931ff7;p=thirdparty%2Fsystemd.git report: turn systemd-report itself into a varlink service Add a Varlink API for requesting a report. --- diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index 658fa1ce608..a00643f6841 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -35,6 +35,7 @@ enable systemd-report-basic.socket enable systemd-report-cgroup.socket enable systemd-report-files.socket enable systemd-report-sign-plain.socket +enable systemd-report.socket enable systemd-resolved.service enable systemd-sysext.service enable systemd-timesyncd.service diff --git a/src/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c index 0eda9287636..e026d4b07ed 100644 --- a/src/libsystemd/sd-varlink/test-varlink-idl.c +++ b/src/libsystemd/sd-varlink/test-varlink-idl.c @@ -43,6 +43,7 @@ #include "varlink-io.systemd.PCRExtend.h" #include "varlink-io.systemd.PCRLock.h" #include "varlink-io.systemd.Repart.h" +#include "varlink-io.systemd.Report.h" #include "varlink-io.systemd.Report.Signer.h" #include "varlink-io.systemd.Report.Uploader.h" #include "varlink-io.systemd.Resolve.h" @@ -218,6 +219,7 @@ TEST(parse_format) { &vl_interface_io_systemd_PCRExtend, &vl_interface_io_systemd_PCRLock, &vl_interface_io_systemd_Repart, + &vl_interface_io_systemd_Report, &vl_interface_io_systemd_Report_Signer, &vl_interface_io_systemd_Report_Uploader, &vl_interface_io_systemd_Resolve, diff --git a/src/report/report.c b/src/report/report.c index 242cd97befa..c3b014a0080 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -17,6 +17,7 @@ #include "recurse-dir.h" #include "report.h" #include "report-generate.h" +#include "report-sign.h" #include "report-upload.h" #include "runtime-scope.h" #include "set.h" @@ -25,6 +26,8 @@ #include "strv.h" #include "time-util.h" #include "varlink-idl-util.h" +#include "varlink-io.systemd.Report.h" +#include "varlink-util.h" #include "verbs.h" #include "web-util.h" @@ -35,7 +38,6 @@ static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM; -static char **arg_matches = NULL; sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO; char *arg_url = NULL; char *arg_key = NULL; @@ -45,7 +47,6 @@ char **arg_extra_headers = NULL; usec_t arg_network_timeout_usec = TIMEOUT_USEC; bool arg_sign = false; -STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_url, freep); STATIC_DESTRUCTOR_REGISTER(arg_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_cert, freep); @@ -73,6 +74,7 @@ static void context_done(Context *context) { context->event = sd_event_unref(context->event); context->link_infos = set_free(context->link_infos); + context->matches = strv_free(context->matches); sd_json_variant_unref_many(context->metrics, context->n_metrics); context->metrics = NULL; context->n_metrics = 0; @@ -193,13 +195,13 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) { /* Check it against any specified matches */ bool matches; - if (strv_isempty(arg_matches)) + if (strv_isempty(li->context->matches)) matches = true; else { matches = false; /* Allow exact matches or prefix matches */ - STRV_FOREACH(i, arg_matches) + STRV_FOREACH(i, li->context->matches) if (streq(metric_name, *i) || metric_startswith_prefix(metric_name, *i)) { matches = true; @@ -493,28 +495,33 @@ static int output_collected(Context *context) { return 0; } -static int parse_metrics_matches(char **matches) { +static int parse_metrics_matches(char **input, char ***ret) { int r; - STRV_FOREACH(i, matches) { + assert(ret); + + _cleanup_strv_free_ char **matches = NULL; + STRV_FOREACH(i, input) { r = metrics_name_valid(*i); if (r < 0) return log_error_errno(r, "Failed to determine if '%s' is a valid metric name: %m", *i); if (!r && !varlink_idl_interface_name_is_valid(*i)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Match is not a valid family name or prefix: %s", *i); - if (strv_extend(&arg_matches, *i) < 0) + if (strv_extend(&matches, *i) < 0) return log_oom(); } - strv_sort_uniq(arg_matches); + strv_sort_uniq(matches); + + *ret = TAKE_PTR(matches); return 0; } -static bool test_service_matches(const char *service) { +static bool test_service_matches(const char *service, char **matches) { assert(service); - if (strv_isempty(arg_matches)) + if (strv_isempty(matches)) return true; /* Only contact services whose name is either a prefix of any of the specified metrics families, or @@ -527,7 +534,7 @@ static bool test_service_matches(const char *service) { * it should also be fine to specify a full metric name, and then go directly to the relevant services, and ask for matching metrics. */ - STRV_FOREACH(i, arg_matches) { + STRV_FOREACH(i, matches) { if (streq(service, *i)) return true; @@ -539,7 +546,7 @@ static bool test_service_matches(const char *service) { return false; } -static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { +static int readdir_sources(char **matches, char **ret_directory, DirectoryEntries **ret) { int r; assert(ret_directory); @@ -575,7 +582,7 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { if (!varlink_idl_interface_name_is_valid(d->d_name)) continue; - if (!test_service_matches(d->d_name)) + if (!test_service_matches(d->d_name, matches)) continue; de->entries[m++] = *i; @@ -589,6 +596,51 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) { return m > 0; } +static int context_collect_metrics(Context *context) { + int r; + + /* Contacts all known metrics sources, issues the appropriate Varlink call on each and runs the + * event loop until all replies came in. Expects the caller to have set up context->event + * beforehand. The collected metrics end up in context->metrics. */ + + assert(context); + assert(context->event); + + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_free_ char *sources_path = NULL; + r = readdir_sources(context->matches, &sources_path, &de); + if (r < 0) + return r; + if (r == 0) + return 0; + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *d = *i; + + if (set_size(context->link_infos) >= METRICS_LINKS_MAX) { + context->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); + } + + context->n_contacted_sources = set_size(context->link_infos); + + if (context->n_contacted_sources == 0) + return 0; + + r = sd_event_loop(context->event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + return 1; +} + VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_METRICS, "Acquire list of metrics and their values"); VERB_FULL(verb_metrics, "describe", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS, @@ -610,55 +662,29 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) * objects. In the report format, we return a single JSON object, so don't do this. */ 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); + r = parse_metrics_matches(argv + 1, &context.matches); 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_LINKS_MAX) { - n_skipped_sources++; - break; - } - _cleanup_free_ char *p = path_join(sources_path, d->d_name); - if (!p) - return log_oom(); + r = sd_event_default(&context.event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); - (void) call_collect(&context, d->d_name, p); - } - } + 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"); - if (set_isempty(context.link_infos)) { + r = context_collect_metrics(&context); + if (r < 0) + return r; + if (r == 0) { if (arg_legend) log_info("No metrics 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"); - switch (action) { case ACTION_LIST_METRICS: @@ -681,10 +707,10 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata) return r; } - if (n_skipped_sources > 0) + if (context.n_skipped_sources > 0) return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN), - "Too many metrics sources, only %u sources contacted, %zu sources skipped.", - set_size(context.link_infos), n_skipped_sources); + "Too many metrics sources, only %zu sources contacted, %zu sources skipped.", + context.n_contacted_sources, context.n_skipped_sources); if (context.n_invalid_metrics > 0) return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN), "%zu metrics are not valid.", @@ -706,7 +732,7 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user _cleanup_free_ char *sources_path = NULL; _cleanup_free_ DirectoryEntries *de = NULL; - r = readdir_sources(&sources_path, &de); + r = readdir_sources(/* matches= */ NULL, &sources_path, &de); if (r < 0) return r; if (r > 0) @@ -752,6 +778,110 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user return 0; } +static int vl_method_generate_internal( + sd_varlink *link, + sd_json_variant *parameters, + bool sign) { + + int r; + + assert(link); + assert(parameters); + + _cleanup_strv_free_ char **input_matches = NULL; + + static const sd_json_dispatch_field dispatch_table[] = { + { "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, 0, 0 }, + {} + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &input_matches); + if (r != 0) + return r; + + _cleanup_(context_done) Context context = { + .action = ACTION_GENERATE, + }; + + r = parse_metrics_matches(input_matches, &context.matches); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "matches"); + + r = sd_event_new(&context.event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = context_collect_metrics(&context); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL; + r = context_build_report(&context, &report); + if (r < 0) + return r; + + if (sign) { + /* Use compact JSON formatting (no pretty/color/seq flags), matching the on-the-wire format + * used for uploads. context_sign_report() adds the JSON-SEQ record separators itself. */ + _cleanup_free_ char *s = NULL; + r = context_sign_report_as_string(&context, report, /* format_flags= */ 0, &s); + if (r < 0) + return r; + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR_BASE64("reportData", s, strlen(s))); + } + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR_VARIANT("report", report)); +} + +static int vl_method_generate( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + return vl_method_generate_internal(link, parameters, /* sign= */ false); +} + +static int vl_method_generate_signed( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + return vl_method_generate_internal(link, parameters, /* sign= */ true); +} + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL; + int r; + + r = varlink_server_new(&vs, SD_VARLINK_SERVER_MYSELF_ONLY|SD_VARLINK_SERVER_ROOT_ONLY, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Report); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + vs, + "io.systemd.Report.Generate", vl_method_generate, + "io.systemd.Report.GenerateSigned", vl_method_generate_signed); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %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) { int r; @@ -896,6 +1026,14 @@ static int run(int argc, char *argv[]) { log_setup(); + /* If invoked as a socket-activated Varlink service (Accept=yes), act as the io.systemd.Report + * server instead of running the command line interface. */ + 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 vl_server(); + r = parse_argv(argc, argv, &args); if (r <= 0) return r; diff --git a/src/report/report.h b/src/report/report.h index 0b8873a4ee4..177491d0d74 100644 --- a/src/report/report.h +++ b/src/report/report.h @@ -29,8 +29,9 @@ typedef struct Context { Action action; sd_event *event; Set *link_infos; + char **matches; /* Metric families to include, or NULL/empty for all */ sd_json_variant **metrics; /* Collected metrics for sorting */ - size_t n_metrics, n_skipped_metrics, n_invalid_metrics; + size_t n_metrics, n_skipped_metrics, n_invalid_metrics, n_contacted_sources, n_skipped_sources; int upload_result; struct iovec_wrapper upload_answer; diff --git a/src/shared/meson.build b/src/shared/meson.build index 0dd733100ab..a3684ade1e2 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -241,6 +241,7 @@ shared_sources = files( 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.PCRLock.c', 'varlink-io.systemd.Repart.c', + 'varlink-io.systemd.Report.c', 'varlink-io.systemd.Report.Signer.c', 'varlink-io.systemd.Report.Uploader.c', 'varlink-io.systemd.Resolve.c', diff --git a/src/shared/varlink-io.systemd.Report.c b/src/shared/varlink-io.systemd.Report.c new file mode 100644 index 00000000000..38f2f780bd7 --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.c @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Report.h" + +static SD_VARLINK_DEFINE_METHOD( + Generate, + SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."), + SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The generated report as a JSON object. This mode does not sign the report, hence a precise binary formatting of the JSON data is not relevant."), + SD_VARLINK_DEFINE_OUTPUT(report, SD_VARLINK_OBJECT, 0)); + +static SD_VARLINK_DEFINE_METHOD( + GenerateSigned, + SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."), + SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The generated, signed report in Base64. A precise binary formatting of the JSON data is important to authenticate the signature. This data contains a JSON-SEQ compliant stream of objects, the first being the report, the following ones signature objects."), + SD_VARLINK_DEFINE_OUTPUT(reportData, SD_VARLINK_STRING, 0)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_Report, + "io.systemd.Report", + SD_VARLINK_INTERFACE_COMMENT("Frontend API for generating system reports. This interface is implemented by systemd-report, which aggregates the metrics exposed by the io.systemd.Metrics services linked into /run/systemd/report/."), + SD_VARLINK_SYMBOL_COMMENT("Generate a report and return it as a JSON object."), + &vl_method_Generate, + SD_VARLINK_SYMBOL_COMMENT("Generate a signed report and return it Base64 encoded."), + &vl_method_GenerateSigned); diff --git a/src/shared/varlink-io.systemd.Report.h b/src/shared/varlink-io.systemd.Report.h new file mode 100644 index 00000000000..85856134285 --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.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_Report; diff --git a/units/meson.build b/units/meson.build index ae2e2c8ccf8..11b73b9b1cc 100644 --- a/units/meson.build +++ b/units/meson.build @@ -633,6 +633,8 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { 'file' : 'systemd-report.socket' }, + { 'file' : 'systemd-report@.service.in' }, { 'file' : 'systemd-report-basic.socket' }, { 'file' : 'systemd-report-basic@.service.in' }, { diff --git a/units/systemd-report.socket b/units/systemd-report.socket new file mode 100644 index 00000000000..9a569a78341 --- /dev/null +++ b/units/systemd-report.socket @@ -0,0 +1,24 @@ +# 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=System Report Socket +Documentation=man:systemd-report(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/io.systemd.Report +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes +MaxConnectionsPerSource=16 +RemoveOnStop=yes + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-report@.service.in b/units/systemd-report@.service.in new file mode 100644 index 00000000000..41c4c5a7005 --- /dev/null +++ b/units/systemd-report@.service.in @@ -0,0 +1,18 @@ +# 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=System Report +Documentation=man:systemd-report(1) + +DefaultDependencies=no +Conflicts=shutdown.target +Before=shutdown.target + +[Service] +ExecStart={{LIBEXECDIR}}/systemd-report