Merge facts into metrics and add systemd-report generate/upload (#41688)

This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-05-05 20:55:36 +02:00
committed by GitHub
16 changed files with 315 additions and 565 deletions
+103 -7
View File
@@ -1,6 +1,9 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<?xml version='1.0'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
<!ENTITY % entities SYSTEM "custom-entities.ent" >
%entities;
]>
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-report"
@@ -18,7 +21,7 @@
<refnamediv>
<refname>systemd-report</refname>
<refpurpose>Generate report of system facts and metrics</refpurpose>
<refpurpose>Generate report of system metrics</refpurpose>
</refnamediv>
<refsynopsisdiv>
@@ -33,7 +36,7 @@
<para><emphasis>Note: this command is experimental for now. While it is likely to become a regular
component of systemd, it might still change in behaviour and interface.</emphasis></para>
<para><command>systemd-report</command> requests facts and metrics from the system and writes them to
<para><command>systemd-report</command> requests metrics from the system and writes them to
standard output.</para>
</refsect1>
@@ -56,18 +59,43 @@
</varlistentry>
<varlistentry>
<term><command>describe-metrics</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<term><command>describe</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<listitem><para>Acquire a list of metric families from all local services providing them, and write
them to standard output. This returns primarily static information about metrics, their data types
and human readable description, without values.</para>
<para>Match expressions similar to those supported by <command>metrics</command> are supported for
<command>describe-metrics</command>, too.</para>
<para>Match expressions supported by <command>metrics</command> are supported by
<command>describe</command> too.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><command>generate</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<listitem><para>Acquire a list of metrics and build a JSON report.</para>
<para>Match expressions supported by <command>metrics</command> are supported here too.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>upload</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<listitem><para>This command can be used to send the report built by <command>generate</command>
to an external server. Two upload mechanisms are supported. If an <literal>http://</literal> or
<literal>https://</literal> URL is specified with <option>--url=</option>, an HTTP upload will be
performed to the specified location. Otherwise, any sockets under
<filename>/run/systemd/metrics-upload/</filename> will be used to call
<function>io.systemd.Report.Upload()</function>.</para>
<para>Match expressions supported by <command>metrics</command> are supported here too.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><command>list-sources</command></term>
@@ -101,6 +129,74 @@
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
<varlistentry>
<term><option>--url=<replaceable>URL</replaceable></option></term>
<listitem><para>Upload the collected report to the specified address instead of writing it to
standard output. <replaceable>URL</replaceable> must point to a server accepting POST requests with
a JSON-formatted report body.</para>
<para>Note: both <literal>http://</literal> and <literal>https://</literal> URLs are supported, but
<emphasis>connections over plain HTTP are made without encryption</emphasis>. Thus, this mode should
only be used in specific situations where integrity and confidentiality of the report is not required
or is ensured through some other means. Using <literal>https://</literal> is recommended.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--key=<replaceable>FILENAME</replaceable></option></term>
<listitem><para>Takes a path to a SSL key file in PEM format, used for client certificate
authentication when uploading. Can also be set to <literal>-</literal>, to disable client certificate
authentication. Defaults to
<filename>&CERTIFICATE_ROOT;/private/systemd-report.pem</filename>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--cert=<replaceable>FILENAME</replaceable></option></term>
<listitem><para>Takes a path to a SSL certificate file in PEM format, used for client certificate
authentication when uploading. Defaults to
<filename>&CERTIFICATE_ROOT;/certs/systemd-report.pem</filename>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--trust=<replaceable>FILENAME</replaceable>|<constant>all</constant></option></term>
<listitem><para>Takes a path to a SSL CA certificate file in PEM format used to verify the server
certificate, or the literal string <constant>all</constant> to disable certificate checking
entirely. Defaults to <filename>&CERTIFICATE_ROOT;/ca/trusted.pem</filename>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--network-timeout=<replaceable>SEC</replaceable></option></term>
<listitem><para>Timeout for the network upload operation. Takes a value in seconds (or in other
time units if suffixed with <literal>ms</literal>, <literal>min</literal>, <literal>h</literal>,
etc.); see
<citerefentry><refentrytitle>systemd.time</refentrytitle><manvolnum>7</manvolnum></citerefentry>
for details. Defaults to 30 seconds.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--extra-header=<replaceable>NAME</replaceable>: <replaceable>VALUE</replaceable></option></term>
<listitem><para>Inject an additional HTTP header into the upload request. May be specified multiple
times to add several headers. Passing an empty string clears any headers added by previous
<option>--extra-header=</option> uses.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
+10 -3
View File
@@ -4,12 +4,12 @@
#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-io.systemd.Metrics.h"
#include "varlink-util.h"
static int vl_server(void) {
@@ -20,9 +20,16 @@ static int vl_server(void) {
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);
r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Metrics);
if (r < 0)
return log_error_errno(r, "Failed to register Facts varlink interface: %m");
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
vs,
"io.systemd.Metrics.List", vl_method_list_metrics,
"io.systemd.Metrics.Describe", vl_method_describe_metrics);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(vs);
if (r < 0)
+43 -31
View File
@@ -8,21 +8,22 @@
#include "alloc-util.h"
#include "architecture.h"
#include "facts.h"
#include "hostname-setup.h"
#include "metrics.h"
#include "report-basic.h"
#include "virt.h"
static int architecture_generate(FactFamilyContext *context, void *userdata) {
static int architecture_generate(MetricFamilyContext *context, void *userdata) {
assert(context);
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
architecture_to_string(uname_architecture()));
architecture_to_string(uname_architecture()),
/* fields= */ NULL);
}
static int boot_id_generate(FactFamilyContext *context, void *userdata) {
static int boot_id_generate(MetricFamilyContext *context, void *userdata) {
sd_id128_t id;
int r;
@@ -32,13 +33,14 @@ static int boot_id_generate(FactFamilyContext *context, void *userdata) {
if (r < 0)
return r;
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
SD_ID128_TO_STRING(id));
SD_ID128_TO_STRING(id),
/* fields= */ NULL);
}
static int hostname_generate(FactFamilyContext *context, void *userdata) {
static int hostname_generate(MetricFamilyContext *context, void *userdata) {
_cleanup_free_ char *hostname = NULL;
int r;
@@ -48,26 +50,28 @@ static int hostname_generate(FactFamilyContext *context, void *userdata) {
if (r < 0)
return r;
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
hostname);
hostname,
/* fields= */ NULL);
}
static int kernel_version_generate(FactFamilyContext *context, void *userdata) {
static int kernel_version_generate(MetricFamilyContext *context, void *userdata) {
struct utsname u;
assert(context);
assert_se(uname(&u) >= 0);
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
u.release);
u.release,
/* fields= */ NULL);
}
static int machine_id_generate(FactFamilyContext *context, void *userdata) {
static int machine_id_generate(MetricFamilyContext *context, void *userdata) {
sd_id128_t id;
int r;
@@ -77,13 +81,14 @@ static int machine_id_generate(FactFamilyContext *context, void *userdata) {
if (r < 0)
return r;
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
SD_ID128_TO_STRING(id));
SD_ID128_TO_STRING(id),
/* fields= */ NULL);
}
static int virtualization_generate(FactFamilyContext *context, void *userdata) {
static int virtualization_generate(MetricFamilyContext *context, void *userdata) {
Virtualization v;
assert(context);
@@ -92,51 +97,58 @@ static int virtualization_generate(FactFamilyContext *context, void *userdata) {
if (v < 0)
return v;
return fact_build_send_string(
return metric_build_send_string(
context,
/* object= */ NULL,
virtualization_to_string(v));
virtualization_to_string(v),
/* fields= */ NULL);
}
static const FactFamily fact_family_table[] = {
/* Keep facts ordered alphabetically */
static const MetricFamily metric_family_table[] = {
/* Keep entries ordered alphabetically */
{
.name = FACT_IO_SYSTEMD_BASIC "Architecture",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "Architecture",
.description = "CPU architecture",
.type = METRIC_FAMILY_TYPE_STRING,
.generate = architecture_generate,
},
{
.name = FACT_IO_SYSTEMD_BASIC "BootID",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "BootID",
.description = "Current boot ID",
.type = METRIC_FAMILY_TYPE_STRING,
.generate = boot_id_generate,
},
{
.name = FACT_IO_SYSTEMD_BASIC "Hostname",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "Hostname",
.description = "System hostname",
.type = METRIC_FAMILY_TYPE_STRING,
.generate = hostname_generate,
},
{
.name = FACT_IO_SYSTEMD_BASIC "KernelVersion",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "KernelVersion",
.description = "Kernel version",
.type = METRIC_FAMILY_TYPE_STRING,
.generate = kernel_version_generate,
},
{
.name = FACT_IO_SYSTEMD_BASIC "MachineID",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "MachineID",
.description = "Machine ID",
.type = METRIC_FAMILY_TYPE_STRING,
.generate = machine_id_generate,
},
{
.name = FACT_IO_SYSTEMD_BASIC "Virtualization",
.name = METRIC_IO_SYSTEMD_BASIC_PREFIX "Virtualization",
.description = "Virtualization type",
.type = METRIC_FAMILY_TYPE_STRING,
.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_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_describe(metric_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);
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_list(metric_family_table, link, parameters, flags, userdata);
}
+3 -3
View File
@@ -3,7 +3,7 @@
#include "shared-forward.h"
#define FACT_IO_SYSTEMD_BASIC "io.systemd.Basic."
#define METRIC_IO_SYSTEMD_BASIC_PREFIX "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);
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+96 -23
View File
@@ -2,12 +2,15 @@
#include "sd-json.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "log.h"
#include "report.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "utf8.h"
#include "varlink-util.h"
#include "version.h"
#if HAVE_LIBCURL
@@ -49,6 +52,7 @@ static size_t output_callback(char *buf,
return nmemb;
}
#endif
static int build_json_report(Context *context, sd_json_variant **ret) {
/* Convert the variant array to a JSON report. */
@@ -59,26 +63,18 @@ static int build_json_report(Context *context, sd_json_variant **ret) {
usec_t ts = now(CLOCK_REALTIME);
int r;
const char *ident;
if (IN_SET(context->action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS))
ident = "metrics";
else if (IN_SET(context->action, ACTION_LIST_FACTS, ACTION_DESCRIBE_FACTS))
ident = "facts";
else
assert_not_reached();
r = sd_json_buildo(ret,
SD_JSON_BUILD_PAIR_STRING("mediaType", "application/vnd.io.systemd.report"),
SD_JSON_BUILD_PAIR("timestamp",
SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
SD_JSON_BUILD_PAIR(ident,
SD_JSON_BUILD_PAIR("metrics",
SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics)));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
return 0;
}
#endif
int upload_collected(Context *context) {
static int http_upload_collected(Context *context, sd_json_variant *report) {
#if HAVE_LIBCURL
_cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL;
char error[CURL_ERROR_SIZE] = {};
@@ -89,19 +85,11 @@ int upload_collected(Context *context) {
if (r < 0)
return r;
{
/* Convert our variant array to a JSON report.
* We won't need the JSON structure again, so free it quickly. */
/* Upload a JSON report in text form as a single JSON object, instead of a JSON-SEQ list. */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL;
r = build_json_report(context, &vl);
if (r < 0)
return r;
r = sd_json_variant_format(vl, /* flags= */ 0, &json);
if (r < 0)
return log_error_errno(r, "Failed to format JSON data: %m");
}
r = sd_json_variant_format(report, /* flags= */ 0, &json);
if (r < 0)
return log_error_errno(r, "Failed to format JSON data: %m");
r = curl_append_to_header(&header,
STRV_MAKE("Content-Type: application/json",
@@ -214,3 +202,88 @@ int upload_collected(Context *context) {
"Compiled without libcurl.");
#endif
}
static int execute_dir_reply(
sd_varlink *link,
sd_json_variant *reply,
const char *error_id,
sd_varlink_reply_flags_t flags,
void *userdata) {
assert(link);
Context *context = ASSERT_PTR(userdata);
int r;
if (error_id) {
r = sd_varlink_error_to_errno(error_id, reply);
RET_GATHER(context->upload_result, r);
log_error_errno(r, "Upload via Varlink failed: %s", error_id);
if (reply)
(void) sd_json_variant_dump(reply, arg_json_format_flags,
/* f= */ NULL, /* prefix= */ NULL);
return r;
}
printf("Upload via Varlink was successful; reply: ");
// TODO: once we know what we want to put in the reply, replace the JSON dump by
// some formatted output.
r = sd_json_variant_dump(reply, arg_json_format_flags, stderr, /* prefix= */ ">>> ");
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
return 0;
}
static int upload_collected(Context *context, sd_json_variant *report) {
int r;
if (arg_url)
return http_upload_collected(context, report);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
r = sd_json_buildo(&params,
SD_JSON_BUILD_PAIR_VARIANT("report", report));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
ssize_t jobs = varlink_execute_directory(
REPORT_UPLOAD_DIR,
"io.systemd.Report.Upload",
params,
/* more= */ false,
arg_network_timeout_usec,
execute_dir_reply,
/* userdata= */ context);
if (jobs < 0)
return log_error_errno(jobs, "Failed to execute upload via %s: %m", REPORT_UPLOAD_DIR);
if (jobs == 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOPKG),
"No upload mechanism found via %s.", REPORT_UPLOAD_DIR);
if (context->upload_result < 0)
/* The details were printed at error level by execute_dir_reply above. */
return log_debug_errno(context->upload_result, "Upload via %s failed: %m", REPORT_UPLOAD_DIR);
log_debug("Upload via %s finished successfully.", REPORT_UPLOAD_DIR);
return 0;
}
/* Make a structured report and either print it or upload it. */
int report_collected(Context *context) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
int r;
r = build_json_report(context, &report);
if (r < 0)
return r;
if (context->action == ACTION_UPLOAD)
return upload_collected(context, report);
/* Just print the report for now. */
assert(context->action == ACTION_GENERATE);
r = sd_json_variant_dump(report, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
return 0;
}
+29 -230
View File
@@ -20,7 +20,6 @@
#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"
@@ -28,15 +27,15 @@
#include "verbs.h"
#include "web-util.h"
#define METRICS_OR_FACTS_MAX 4096U
#define METRICS_OR_FACTS_LINKS_MAX 128U
#define METRICS_MAX 4096U
#define METRICS_LINKS_MAX 128U
#define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
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;
char *arg_cert = NULL;
@@ -84,15 +83,6 @@ 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;
@@ -249,7 +239,7 @@ static int on_query_reply(
goto finish;
}
if (context->n_metrics >= METRICS_OR_FACTS_MAX) {
if (context->n_metrics >= METRICS_MAX) {
context->n_skipped_metrics++;
goto finish;
}
@@ -290,6 +280,10 @@ static int call_collect(Context *context, const char *name, const char *path) {
if (r < 0)
return log_error_errno(r, "Unable to connect to %s: %m", path);
r = sd_varlink_set_description(vl, name);
if (r < 0)
return log_error_errno(r, "Failed to set varlink description: %m");
r = sd_varlink_set_relative_timeout(vl, TIMEOUT_USEC);
if (r < 0)
return log_error_errno(r, "Failed to set varlink timeout: %m");
@@ -302,7 +296,9 @@ static int call_collect(Context *context, const char *name, const char *path) {
if (r < 0)
return log_error_errno(r, "Failed to bind reply callback: %m");
const char *method = ASSERT_PTR(action_method_to_string(context->action));
const char *method = context->action == ACTION_DESCRIBE_METRICS ?
"io.systemd.Metrics.Describe" :
"io.systemd.Metrics.List"; /* This is the method for all other actions. */
r = sd_varlink_observe(vl, method, /* parameters= */ NULL);
if (r < 0)
@@ -436,107 +432,6 @@ static int output_collected_describe(Context *context, Table **ret) {
return 0;
}
static int facts_output_list(Context *context, Table **ret) {
int r;
assert(context);
assert(ret);
_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);
assert(ret);
_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;
@@ -555,13 +450,8 @@ static int output_collected(Context *context) {
return log_error_errno(r, "Failed to write JSON: %m");
}
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.");
}
if (context->n_metrics == 0 && arg_legend)
log_info("No metrics collected.");
return 0;
}
@@ -577,14 +467,6 @@ static int output_collected(Context *context) {
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:
assert_not_reached();
}
@@ -598,12 +480,10 @@ static int output_collected(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 %s available.\n", type);
printf("No metrics available.\n");
else
printf("\n%zu %s listed.\n", table_get_rows(table) - 1, type);
printf("\n%zu metrics listed.\n", table_get_rows(table) - 1);
}
return 0;
@@ -707,18 +587,24 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
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-metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
VERB_FULL(verb_metrics, "describe", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
"Describe available metrics");
VERB_FULL(verb_metrics, "generate", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_GENERATE,
"Build a report with metrics");
VERB_FULL(verb_metrics, "upload", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_UPLOAD,
"Upload a report with metrics");
static int verb_metrics(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_METRICS, ACTION_DESCRIBE_METRICS));
assert(IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS, ACTION_GENERATE, ACTION_UPLOAD));
/* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */
arg_json_format_flags |= SD_JSON_FORMAT_SEQ;
if (IN_SET(action, ACTION_LIST_METRICS, ACTION_DESCRIBE_METRICS))
/* Enable JSON-SEQ mode for the first two verbs, since we'll dump a large series of JSON
* 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)
@@ -746,7 +632,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_OR_FACTS_LINKS_MAX) {
if (set_size(context.link_infos) >= METRICS_LINKS_MAX) {
n_skipped_sources++;
break;
}
@@ -769,8 +655,8 @@ 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");
if (arg_url)
r = upload_collected(&context);
if (IN_SET(action, ACTION_GENERATE, ACTION_UPLOAD))
r = report_collected(&context);
else
r = output_collected(&context);
if (r < 0)
@@ -792,93 +678,6 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
return 0;
}
VERB_FULL(verb_facts, "facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_FACTS,
"Acquire list of facts and their values");
VERB_FULL(verb_facts, "describe-facts", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_FACTS,
"Describe available facts");
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");
if (arg_url)
r = upload_collected(&context);
else
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;
}
VERB_NOARG(verb_list_sources, "list-sources", "Show list of known metrics sources");
static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
@@ -955,7 +754,7 @@ static int help(void) {
(void) table_sync_column_widths(0, options, verbs);
printf("%s [OPTIONS...] COMMAND ...\n"
"\n%sAcquire metrics and facts from local sources.%s\n"
"\n%sAcquire metrics from local sources.%s\n"
"\n%sCommands:%s\n",
program_invocation_short_name,
ansi_highlight(),
+10 -6
View File
@@ -9,6 +9,9 @@
#define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem"
#define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem"
#define REPORT_UPLOAD_DIR "/run/systemd/metrics-upload"
extern sd_json_format_flags_t arg_json_format_flags;
extern char *arg_url, *arg_key, *arg_cert, *arg_trust;
extern char **arg_extra_headers;
extern usec_t arg_network_timeout_usec;
@@ -16,21 +19,22 @@ extern usec_t arg_network_timeout_usec;
typedef enum Action {
ACTION_LIST_METRICS,
ACTION_DESCRIBE_METRICS,
ACTION_LIST_FACTS,
ACTION_DESCRIBE_FACTS,
ACTION_GENERATE,
ACTION_UPLOAD,
_ACTION_MAX,
_ACTION_INVALID = -EINVAL,
} Action;
/* The structure for collected "metrics" or "facts". The fields
* are prefixed with just "metrics" for brevity. */
/* The structure for collected "metrics". */
typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
sd_json_variant **metrics; /* Collected metrics or facts for sorting */
sd_json_variant **metrics; /* Collected metrics for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
int upload_result;
struct iovec_wrapper upload_answer;
} Context;
int upload_collected(Context *context);
int report_collected(Context *context);
-151
View File
@@ -1,151 +0,0 @@
/* 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_VARIANT("value", 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);
}
-31
View File
@@ -1,31 +0,0 @@
/* 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);
-2
View File
@@ -80,7 +80,6 @@ shared_sources = files(
'exit-status.c',
'extension-util.c',
'factory-reset.c',
'facts.c',
'fdisk-util.c',
'fdset.c',
'fido2-util.c',
@@ -219,7 +218,6 @@ 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',
-37
View File
@@ -1,37 +0,0 @@
/* 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);
-6
View File
@@ -1,6 +0,0 @@
/* 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;
@@ -35,8 +35,8 @@ class Handler(BaseHTTPRequestHandler):
print(f"JSON: {s if len(s := str(data)) < 80 else s[:40] + '…' + s[-40:]}")
if "metrics" not in data and "facts" not in data:
self.send_error(400, "Missing 'metrics' or 'facts' field")
if "metrics" not in data:
self.send_error(400, "Missing 'metrics' field")
return
response = json.dumps({"status": "ok"}).encode()
+17 -31
View File
@@ -16,9 +16,9 @@ REPORT=/usr/lib/systemd/systemd-report
"$REPORT" metrics
"$REPORT" metrics -j
"$REPORT" metrics --no-legend
"$REPORT" describe-metrics
"$REPORT" describe-metrics -j
"$REPORT" describe-metrics --no-legend
"$REPORT" describe
"$REPORT" describe -j
"$REPORT" describe --no-legend
"$REPORT" list-sources
"$REPORT" list-sources -j
"$REPORT" list-sources --no-legend
@@ -26,9 +26,9 @@ REPORT=/usr/lib/systemd/systemd-report
"$REPORT" metrics io
"$REPORT" metrics io.systemd piff
"$REPORT" metrics piff
"$REPORT" describe-metrics io
"$REPORT" describe-metrics io.systemd piff
"$REPORT" describe-metrics piff
"$REPORT" describe io
"$REPORT" describe io.systemd piff
"$REPORT" describe piff
# test io.systemd.CGroup Metrics
systemctl start systemd-report-cgroup.socket
@@ -50,28 +50,12 @@ 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
# test io.systemd.Basic Metrics
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 {}
varlinkctl info /run/systemd/report/io.systemd.Basic
varlinkctl list-methods /run/systemd/report/io.systemd.Basic
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Metrics.List {}
varlinkctl --more call /run/systemd/report/io.systemd.Basic io.systemd.Metrics.Describe {}
# Test HTTP upload (plain http)
FAKE_SERVER=/usr/lib/systemd/tests/integration-tests/TEST-74-AUX-UTILS/TEST-74-AUX-UTILS.units/fake-report-server.py
@@ -87,8 +71,11 @@ trap at_exit EXIT
systemd-run -p Type=notify --unit=fake-report-server "$FAKE_SERVER"
systemctl status fake-report-server
"$REPORT" metrics --url=http://localhost:8089/
"$REPORT" facts --url=http://localhost:8089/
"$REPORT" generate io.systemd.Manager.UnitsTotal
"$REPORT" generate io.systemd.Manager.UnitsTotal | jq .
"$REPORT" upload --url=http://localhost:8089/
# Test HTTPS upload with generated TLS certificates
openssl req -x509 -newkey rsa:2048 -keyout "$CERTDIR/server.key" -out "$CERTDIR/server.crt" \
@@ -98,6 +85,5 @@ systemd-run -p Type=notify --unit=fake-report-server-tls \
"$FAKE_SERVER" --cert="$CERTDIR/server.crt" --key="$CERTDIR/server.key" --port=8090
systemctl status fake-report-server-tls
"$REPORT" metrics --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt"
"$REPORT" facts --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
"$REPORT" upload --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
--extra-header='Authorization: Bearer magic string'
+1 -1
View File
@@ -7,7 +7,7 @@
# 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
Description=Report System Basic Metrics Socket
DefaultDependencies=no
Before=sockets.target
+1 -1
View File
@@ -7,7 +7,7 @@
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=Report System Basic Facts
Description=Report System Basic Metrics
DefaultDependencies=no
Conflicts=shutdown.target