]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
report: turn systemd-report itself into a varlink service
authorLennart Poettering <lennart@amutable.com>
Mon, 15 Jun 2026 08:06:03 +0000 (10:06 +0200)
committerLennart Poettering <lennart@amutable.com>
Fri, 19 Jun 2026 03:23:18 +0000 (05:23 +0200)
Add a Varlink API for requesting a report.

presets/90-systemd.preset
src/libsystemd/sd-varlink/test-varlink-idl.c
src/report/report.c
src/report/report.h
src/shared/meson.build
src/shared/varlink-io.systemd.Report.c [new file with mode: 0644]
src/shared/varlink-io.systemd.Report.h [new file with mode: 0644]
units/meson.build
units/systemd-report.socket [new file with mode: 0644]
units/systemd-report@.service.in [new file with mode: 0644]

index 658fa1ce608f8dac35730e35f10b792cfe38620f..a00643f6841280050ec89dc7bedada46c060c6ce 100644 (file)
@@ -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
index 0eda928763656b40cb93c740c3f2c64a1ade7a69..e026d4b07ed3193710d9e6e19439877e3cd9829b 100644 (file)
@@ -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,
index 242cd97befa43da80efe2e81bd4e5f86011c0025..c3b014a008085f8271f63d820c65b65edafd323b 100644 (file)
@@ -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;
index 0b8873a4ee4134a5e40f6b6606f5361ea763ef54..177491d0d748c2611e1c3b2ef8830908ea1f25c8 100644 (file)
@@ -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;
index 0dd733100ab25b6ed1aa26f3835cc34879d10d8d..a3684ade1e20efafdfb4308cc2f61ebf2c33bccc 100644 (file)
@@ -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 (file)
index 0000000..38f2f78
--- /dev/null
@@ -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 (file)
index 0000000..8585613
--- /dev/null
@@ -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;
index ae2e2c8ccf87fce04887b1018affabaa2f36bc2f..11b73b9b1cc7b0828b982bf47ace908878b76b47 100644 (file)
@@ -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 (file)
index 0000000..9a569a7
--- /dev/null
@@ -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 (file)
index 0000000..41c4c5a
--- /dev/null
@@ -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