From: Lennart Poettering Date: Sat, 30 May 2026 09:02:47 +0000 (+0200) Subject: imds: expose instance metadata as an io.systemd.Metrics provider X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=55db8dc12b75fe43b7edcfdd35910b3151e26838;p=thirdparty%2Fsystemd.git imds: expose instance metadata as an io.systemd.Metrics provider When systemd-imds is invoked as a Varlink service (via the new systemd-imds-metrics.socket), it now acts as an io.systemd.Metrics provider for systemd-report. It connects to systemd-imdsd over the existing io.systemd.InstanceMetadata interface to acquire the real data and re-exposes the detected cloud vendor plus the well-known hostname, region, zone and public IPv4/IPv6 fields as metrics in the io.systemd.InstanceMetadata.* namespace. The metrics logic lives entirely on the client side (imds-tool-metrics.c); systemd-imdsd is unchanged. Each metric is acquired on demand with a blocking call to the daemon, benefiting from its local cache. Fields that are unset or unsupported by the vendor are simply omitted. The metrics socket is statically enabled into sockets.target.wants/. --- diff --git a/man/systemd-imds.xml b/man/systemd-imds.xml index 3980c7560c3..b8b314ccbd6 100644 --- a/man/systemd-imds.xml +++ b/man/systemd-imds.xml @@ -69,6 +69,17 @@ fields are imported into the local credential store, where they are used to configure and parameterize the system. For details see below. + + In addition, when invoked as a Varlink service (i.e. via socket activation through + systemd-imds-metrics.socket), systemd-imds acts as an + io.systemd.Metrics provider for + systemd-report1. It + exposes the detected cloud vendor and the well-known hostname, + region, zone, ipv4-public and + ipv6-public fields as metrics in the io.systemd.InstanceMetadata. + namespace. The data is acquired on demand from + systemd-imdsd@.service8, + benefiting from its local cache. @@ -167,6 +178,7 @@ systemd1 systemd-imdsd@.service8 systemd-imds-generator8 + systemd-report1 systemd.system-credentials7 diff --git a/src/imds/imds-generator.c b/src/imds/imds-generator.c index 854877d40ed..3f22ca30310 100644 --- a/src/imds/imds-generator.c +++ b/src/imds/imds-generator.c @@ -189,6 +189,11 @@ static int run(const char *dest, const char *dest_early, const char *dest_late) return log_error_errno(r, "Failed to hook in systemd-imds-import.service: %m"); } + /* Enable IMDS metrics in case IMDS is supported */ + r = generator_add_symlink(dest_early, SPECIAL_SOCKETS_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-imds-metrics.socket"); + if (r < 0) + return log_error_errno(r, "Failed to hook in systemd-imds-metrics.socket: %m"); + return 0; } diff --git a/src/imds/imds-tool-metrics.c b/src/imds/imds-tool-metrics.c new file mode 100644 index 00000000000..27eabe92bc1 --- /dev/null +++ b/src/imds/imds-tool-metrics.c @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" +#include "sd-varlink.h" + +#include "imds-tool.h" +#include "imds-tool-metrics.h" +#include "imds-util.h" +#include "log.h" +#include "metrics.h" +#include "string-util.h" +#include "varlink-io.systemd.Metrics.h" +#include "varlink-util.h" + +#define METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "io.systemd.InstanceMetadata." + +static int metric_vendor_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + sd_varlink *imds = userdata; /* The live systemd-imdsd connection, or NULL if unavailable */ + _cleanup_free_ char *vendor = NULL; + int r; + + assert(mf && mf->name); + assert(vl); + + if (!imds) + return 0; + + r = acquire_imds_vendor(imds, &vendor); + if (r <= 0) + return 0; /* On error or absence simply omit the metric */ + + return metric_build_send_string(mf, vl, /* object= */ NULL, vendor, /* fields= */ NULL); +} + +static int metric_well_known_build_json( + const MetricFamily *mf, + sd_varlink *vl, + void *userdata, + ImdsWellKnown wk) { + + sd_varlink *imds = userdata; + _cleanup_free_ char *value = NULL; + int r; + + assert(mf && mf->name); + assert(vl); + + if (!imds) + return 0; + + r = acquire_imds_key_as_string(imds, wk, /* key= */ NULL, &value); + if (r <= 0 || isempty(value)) + return 0; /* Field not supported/set, or error: omit the metric */ + + return metric_build_send_string(mf, vl, /* object= */ NULL, value, /* fields= */ NULL); +} + +static int metric_hostname_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + return metric_well_known_build_json(mf, vl, userdata, IMDS_HOSTNAME); +} + +static int metric_ipv4_public_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + return metric_well_known_build_json(mf, vl, userdata, IMDS_IPV4_PUBLIC); +} + +static int metric_ipv6_public_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + return metric_well_known_build_json(mf, vl, userdata, IMDS_IPV6_PUBLIC); +} + +static int metric_region_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + return metric_well_known_build_json(mf, vl, userdata, IMDS_REGION); +} + +static int metric_zone_build_json(const MetricFamily *mf, sd_varlink *vl, void *userdata) { + return metric_well_known_build_json(mf, vl, userdata, IMDS_ZONE); +} + +/* Keep metrics ordered alphabetically */ +static const MetricFamily imds_metric_family_table[] = { + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "Hostname", + .description = "Instance hostname reported by IMDS", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_hostname_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "IPv4Public", + .description = "Public IPv4 address reported by IMDS", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_ipv4_public_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "IPv6Public", + .description = "Public IPv6 address reported by IMDS", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_ipv6_public_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "Region", + .description = "Cloud region reported by IMDS", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_region_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "Vendor", + .description = "Detected cloud vendor", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_vendor_build_json, + }, + { + .name = METRIC_IO_SYSTEMD_INSTANCE_METADATA_PREFIX "Zone", + .description = "Cloud availability zone reported by IMDS", + .type = METRIC_FAMILY_TYPE_STRING, + .generate = metric_zone_build_json, + }, + {} +}; + +static int vl_method_metrics_describe(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return metrics_method_describe(imds_metric_family_table, link, parameters, flags, userdata); +} + +static int vl_method_metrics_list(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + int r; + + /* Acquire a single connection to systemd-imdsd, shared by all metric generators (so they benefit + * from the daemon's token/data cache). If the daemon is unreachable we still serve an empty list + * rather than failing the whole request. */ + _cleanup_(sd_varlink_unrefp) sd_varlink *imds = NULL; + r = connect_imdsd(&imds); + if (r < 0) + log_debug_errno(r, "Failed to connect to systemd-imdsd, serving empty metrics list: %m"); + + return metrics_method_list(imds_metric_family_table, link, parameters, flags, imds); +} + +int imds_metrics_run(void) { + int r; + + /* Invocation as a Varlink metrics provider (io.systemd.Metrics), typically socket-activated via + * /run/systemd/report/io.systemd.InstanceMetadata. */ + + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *server = NULL; + r = varlink_server_new(&server, /* flags= */ 0, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(server, &vl_interface_io_systemd_Metrics); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method_many( + server, + "io.systemd.Metrics.List", vl_method_metrics_list, + "io.systemd.Metrics.Describe", vl_method_metrics_describe); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink methods: %m"); + + r = sd_varlink_server_loop_auto(server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} diff --git a/src/imds/imds-tool-metrics.h b/src/imds/imds-tool-metrics.h new file mode 100644 index 00000000000..db0e55226e0 --- /dev/null +++ b/src/imds/imds-tool-metrics.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +/* Runs systemd-imds as an io.systemd.Metrics provider (socket-activated). */ +int imds_metrics_run(void); diff --git a/src/imds/imds-tool.c b/src/imds/imds-tool.c index 6b58c61d7e8..bc0a6b1103b 100644 --- a/src/imds/imds-tool.c +++ b/src/imds/imds-tool.c @@ -18,6 +18,8 @@ #include "format-util.h" #include "fs-util.h" #include "hexdecoct.h" +#include "imds-tool.h" +#include "imds-tool-metrics.h" #include "imds-util.h" #include "in-addr-util.h" #include "io-util.h" @@ -232,7 +234,7 @@ static int acquire_imds_key( return 1; } -static int acquire_imds_key_as_string( +int acquire_imds_key_as_string( sd_varlink *link, ImdsWellKnown wk, const char *key, @@ -292,14 +294,11 @@ static int acquire_imds_key_as_ip_address( return 1; } -static int action_summary(sd_varlink *link) { +int acquire_imds_vendor(sd_varlink *link, char **ret) { int r; assert(link); - - _cleanup_(table_unrefp) Table *table = table_new_vertical(); - if (!table) - return log_oom(); + assert(ret); const char *error_id = NULL; sd_json_variant *reply = NULL; @@ -311,17 +310,48 @@ static int action_summary(sd_varlink *link) { &error_id); if (r < 0) return log_error_errno(r, "Failed to issue io.systemd.InstanceMetadata.GetVendorInfo(): %m"); - if (error_id) + if (error_id) { + if (streq(error_id, "io.systemd.InstanceMetadata.NotSupported")) { + *ret = NULL; + return 0; + } + return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to issue io.systemd.InstanceMetadata.GetVendorInfo(): %s", error_id); + } const char *vendor = NULL; static const sd_json_dispatch_field dispatch_table[] = { - { "vendor", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 }, + { "vendor", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 }, {} }; r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &vendor); if (r < 0) return r; + if (isempty(vendor)) { + *ret = NULL; + return 0; + } + + if (strdup_to_full(ret, vendor) < 0) + return log_oom(); + + return 1; +} + +static int action_summary(sd_varlink *link) { + int r; + + assert(link); + + _cleanup_(table_unrefp) Table *table = table_new_vertical(); + if (!table) + return log_oom(); + + _cleanup_free_ char *vendor = NULL; + r = acquire_imds_vendor(link, &vendor); + if (r < 0) + return r; + if (vendor) { r = table_add_many(table, TABLE_FIELD, "Vendor", @@ -820,14 +850,10 @@ static int action_import(sd_varlink *link) { return RET_GATHER(ret, import_credentials(data.iov_base)); } -static int run(int argc, char* argv[]) { +int connect_imdsd(sd_varlink **ret) { int r; - log_setup(); - - r = parse_argv(argc, argv); - if (r <= 0) - return r; + assert(ret); _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.InstanceMetadata"); @@ -850,6 +876,33 @@ static int run(int argc, char* argv[]) { return log_error_errno(r, "Failed to connect to imdsd service: %m"); } + *ret = TAKE_PTR(link); + return 0; +} + +static int run(int argc, char* argv[]) { + int r; + + log_setup(); + + /* When invoked as a Varlink service (socket activation) we act as an io.systemd.Metrics provider + * and run a server loop. The metric generators connect to systemd-imdsd on demand, so we never open + * a one-shot connection here. */ + 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 imds_metrics_run(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = connect_imdsd(&link); + if (r < 0) + return r; + switch (arg_action) { case ACTION_SUMMARY: diff --git a/src/imds/imds-tool.h b/src/imds/imds-tool.h new file mode 100644 index 00000000000..bfe26015c34 --- /dev/null +++ b/src/imds/imds-tool.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "imds-util.h" +#include "shared-forward.h" + +/* Helpers shared between imds-tool.c and imds-tool-metrics.c for talking to systemd-imdsd. */ + +int connect_imdsd(sd_varlink **ret); +int acquire_imds_key_as_string(sd_varlink *link, ImdsWellKnown wk, const char *key, char **ret); +int acquire_imds_vendor(sd_varlink *link, char **ret); diff --git a/src/imds/meson.build b/src/imds/meson.build index 9295c8cf620..a23735d1002 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -14,7 +14,9 @@ executables += [ libexec_template + { 'name' : 'systemd-imds', 'public' : true, - 'sources' : files('imds-tool.c'), + 'sources' : files( + 'imds-tool.c', + 'imds-tool-metrics.c'), 'objects' : ['systemd-imdsd'], }, generator_template + { diff --git a/units/meson.build b/units/meson.build index 47c48bcabcc..34051d63c32 100644 --- a/units/meson.build +++ b/units/meson.build @@ -413,6 +413,14 @@ units = [ 'file' : 'systemd-imds-import.service.in', 'conditions' : ['ENABLE_IMDS'], }, + { + 'file' : 'systemd-imds-metrics.socket', + 'conditions' : ['ENABLE_IMDS'], + }, + { + 'file' : 'systemd-imds-metrics@.service.in', + 'conditions' : ['ENABLE_IMDS'], + }, { 'file' : 'systemd-importd.service.in', 'conditions' : ['ENABLE_IMPORTD'], diff --git a/units/systemd-imds-metrics.socket b/units/systemd-imds-metrics.socket new file mode 100644 index 00000000000..f71a31e5b9a --- /dev/null +++ b/units/systemd-imds-metrics.socket @@ -0,0 +1,22 @@ +# 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=Cloud Instance Metadata Metrics +Documentation=man:systemd-imds(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/report/io.systemd.InstanceMetadata +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 +RemoveOnStop=yes diff --git a/units/systemd-imds-metrics@.service.in b/units/systemd-imds-metrics@.service.in new file mode 100644 index 00000000000..f8da319cd47 --- /dev/null +++ b/units/systemd-imds-metrics@.service.in @@ -0,0 +1,25 @@ +# 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=Cloud Instance Metadata Metrics +Documentation=man:systemd-imds(1) +DefaultDependencies=no +Conflicts=shutdown.target initrd-switch-root.target +Before=shutdown.target initrd-switch-root.target +After=systemd-imdsd.socket + +[Service] +# Runs systemd-imds as an io.systemd.Metrics provider. It connects to +# systemd-imdsd to acquire the actual data. Runs as root so the daemon's Polkit +# check on io.systemd.InstanceMetadata.Get() passes. +ExecStart={{LIBEXECDIR}}/systemd-imds +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes