report: turn systemd-report itself into a varlink service
Add a Varlink API for requesting a report.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+187
-49
@@ -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,21 +662,14 @@ 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");
|
||||
@@ -633,32 +678,13 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
|
||||
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();
|
||||
|
||||
(void) call_collect(&context, d->d_name, p);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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' },
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user