report: add basic upload functionality

This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-04-16 21:12:20 +02:00
parent c861496cd3
commit 5bbbe210a4
4 changed files with 302 additions and 24 deletions
+6 -1
View File
@@ -4,7 +4,12 @@ executables += [
libexec_template + {
'name' : 'systemd-report',
'public' : true,
'sources' : files('report.c'),
'sources' : files(
'report.c',
'report-upload.c',
),
'link_with' : [libcurlutil_static, libshared],
'dependencies' : [libcurl],
},
libexec_template + {
+200
View File
@@ -0,0 +1,200 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "log.h"
#include "report.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "utf8.h"
#include "version.h"
#if HAVE_LIBCURL
#include "curl-util.h"
#include <curl/easy.h> /* Sadly this fails if ordered first. */
static size_t output_callback(char *buf,
size_t size,
size_t nmemb,
void *userp) {
Context *context = ASSERT_PTR(userp);
int r;
assert(size == 1); /* The docs say that this is always true. */
log_debug("Got an answer from the server (%zu bytes)", nmemb);
if (nmemb != 0) {
if (memchr(buf, 0, nmemb)) {
log_warning("Server answer contains an embedded NUL, refusing.");
return 0;
}
r = iovw_append(&context->upload_answer, buf, nmemb);
if (r < 0) {
log_warning("Failed to store server answer (%zu bytes): out of memory", nmemb);
return 0; /* Returning < nmemb signals failure */
}
}
return nmemb;
}
static int build_json_report(Context *context, sd_json_variant **ret) {
/* Convert the variant array to a JSON report. */
assert(context);
assert(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("timestamp",
SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
SD_JSON_BUILD_PAIR(ident,
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) {
#if HAVE_LIBCURL
_cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL;
char error[CURL_ERROR_SIZE] = {};
_cleanup_free_ char *json = NULL;
int r;
{
/* Convert our variant array to a JSON report.
* We won't need the JSON structure again, so free it quickly. */
_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 = curl_append_to_header(&header,
STRV_MAKE("Content-Type: application/json",
"Accept: application/json"));
if (r < 0)
return log_error_errno(r, "Failed to create curl header: %m");
_cleanup_(curl_easy_cleanupp) CURL *curl = curl_easy_init();
if (!curl)
return log_error_errno(SYNTHETIC_ERRNO(ENOSR),
"Call to curl_easy_init failed.");
/* If configured, set a timeout for the curl operation. */
if (arg_network_timeout_usec != USEC_INFINITY &&
!easy_setopt(curl, LOG_ERR, CURLOPT_TIMEOUT,
(long) DIV_ROUND_UP(arg_network_timeout_usec, USEC_PER_SEC)))
return -EXFULL;
/* Tell it to POST to the URL */
if (!easy_setopt(curl, LOG_ERR, CURLOPT_POST, 1L))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_ERRORBUFFER, error))
return -EXFULL;
/* Where to write to */
if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEFUNCTION, output_callback))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_WRITEDATA, context))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_HTTPHEADER, header))
return -EXFULL;
if (DEBUG_LOGGING)
/* enable verbose for easier tracing */
(void) easy_setopt(curl, LOG_WARNING, CURLOPT_VERBOSE, 1L);
(void) easy_setopt(curl, LOG_WARNING,
CURLOPT_USERAGENT, "systemd-report " GIT_VERSION);
if (!streq_ptr(arg_key, "-") && (arg_key || startswith(arg_url, "https://"))) {
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLKEY, arg_key ?: REPORT_PRIV_KEY_FILE))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSLCERT, arg_cert ?: REPORT_CERT_FILE))
return -EXFULL;
}
if (STRPTR_IN_SET(arg_trust, "-", "all")) {
log_info("Server certificate verification disabled.");
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYPEER, 0L))
return -EUCLEAN;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_SSL_VERIFYHOST, 0L))
return -EUCLEAN;
} else if (arg_trust || startswith(arg_url, "https://")) {
if (!easy_setopt(curl, LOG_ERR, CURLOPT_CAINFO, arg_trust ?: REPORT_TRUST_FILE))
return -EXFULL;
}
if (startswith(arg_url, "https://"))
(void) easy_setopt(curl, LOG_WARNING, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
/* Upload to this place */
if (!easy_setopt(curl, LOG_ERR, CURLOPT_URL, arg_url))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_POSTFIELDS, json))
return -EXFULL;
CURLcode code = curl_easy_perform(curl);
if (code != CURLE_OK)
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Upload to %s failed: %s", arg_url,
empty_to_null(&error[0]) ?: curl_easy_strerror(code));
long status;
code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
if (code != CURLE_OK)
return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Failed to retrieve response code: %s",
curl_easy_strerror(code));
_cleanup_free_ char *ans = iovw_to_cstring(&context->upload_answer);
if (!ans)
return log_oom();
if (!utf8_is_valid(ans))
return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Upload to %s failed with code %ld and an invalid UTF-8 answer.",
arg_url, status);
if (status >= 300)
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Upload to %s failed with code %ld: %s",
arg_url, status, strna(ans));
if (status < 200)
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Upload to %s finished with unexpected code %ld: %s",
arg_url, status, strna(ans));
log_info("Upload to %s finished successfully with code %ld: %s",
arg_url, status, strna(ans));
return 0;
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Compiled without libcurl.");
#endif
}
+61 -23
View File
@@ -16,6 +16,7 @@
#include "path-lookup.h"
#include "pretty-print.h"
#include "recurse-dir.h"
#include "report.h"
#include "runtime-scope.h"
#include "set.h"
#include "sort-util.h"
@@ -35,27 +36,17 @@ 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;
char *arg_url = NULL;
char *arg_key = NULL;
char *arg_cert = NULL;
char *arg_trust = NULL;
usec_t arg_network_timeout_usec = TIMEOUT_USEC;
STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
typedef enum Action {
ACTION_LIST_METRICS,
ACTION_DESCRIBE_METRICS,
ACTION_LIST_FACTS,
ACTION_DESCRIBE_FACTS,
_ACTION_MAX,
_ACTION_INVALID = -EINVAL,
} Action;
/* The structure for collected "metrics" or "facts". The fields
* are prefixed with just "metrics" for brevity. */
typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
sd_json_variant **metrics; /* Collected metrics or facts for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
} Context;
STATIC_DESTRUCTOR_REGISTER(arg_url, freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_cert, freep);
STATIC_DESTRUCTOR_REGISTER(arg_trust, freep);
typedef struct LinkInfo {
Context *context;
@@ -76,9 +67,12 @@ static void context_done(Context *context) {
if (!context)
return;
set_free(context->link_infos);
context->event = sd_event_unref(context->event);
context->link_infos = set_free(context->link_infos);
sd_json_variant_unref_many(context->metrics, context->n_metrics);
sd_event_unref(context->event);
context->metrics = NULL;
context->n_metrics = 0;
iovw_done_free(&context->upload_answer);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free);
@@ -569,6 +563,7 @@ static int output_collected(Context *context) {
}
_cleanup_(table_unrefp) Table *table = NULL;
switch(context->action) {
case ACTION_LIST_METRICS:
@@ -771,7 +766,10 @@ 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");
r = output_collected(&context);
if (arg_url)
r = upload_collected(&context);
else
r = output_collected(&context);
if (r < 0)
return r;
}
@@ -855,7 +853,10 @@ static int verb_facts(int argc, char *argv[], uintptr_t data, void *userdata) {
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
r = output_collected(&context);
if (arg_url)
r = upload_collected(&context);
else
r = output_collected(&context);
if (r < 0)
return r;
}
@@ -1017,8 +1018,45 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OPTION_COMMON_LOWERCASE_J:
arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
break;
OPTION_LONG("url", "URL",
"Upload to this address"):
r = free_and_strdup_warn(&arg_url, arg);
if (r < 0)
return r;
break;
OPTION_LONG("key", "FILENAME",
"Specify key in PEM format (default: \"" REPORT_PRIV_KEY_FILE "\")"):
r = free_and_strdup_warn(&arg_key, arg);
if (r < 0)
return r;
break;
OPTION_LONG("cert", "FILENAME",
"Specify certificate in PEM format (default: \"" REPORT_CERT_FILE "\")"):
r = free_and_strdup_warn(&arg_cert, arg);
if (r < 0)
return r;
break;
OPTION_LONG("trust", "FILENAME|all",
"Specify CA certificate or disable checking (default: \"" REPORT_TRUST_FILE "\")"):
r = free_and_strdup_warn(&arg_trust, arg);
if (r < 0)
return r;
break;
OPTION_LONG("network-timeout", "SEC", "Specify timeout for network upload operation"):
r = parse_sec(arg, &arg_network_timeout_usec);
if (r < 0)
return log_error_errno(r, "Failed to parse --network-timeout value: %s", arg);
break;
}
if ((arg_url || arg_key || arg_cert || arg_trust) && !HAVE_LIBCURL)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without libcurl.");
*ret_args = option_parser_get_args(&state);
return 1;
}
+35
View File
@@ -0,0 +1,35 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
#include "iovec-wrapper.h"
#define REPORT_PRIV_KEY_FILE CERTIFICATE_ROOT "/private/systemd-report.pem"
#define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem"
#define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem"
extern char *arg_url, *arg_key, *arg_cert, *arg_trust;
extern usec_t arg_network_timeout_usec;
typedef enum Action {
ACTION_LIST_METRICS,
ACTION_DESCRIBE_METRICS,
ACTION_LIST_FACTS,
ACTION_DESCRIBE_FACTS,
_ACTION_MAX,
_ACTION_INVALID = -EINVAL,
} Action;
/* The structure for collected "metrics" or "facts". The fields
* are prefixed with just "metrics" for brevity. */
typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
sd_json_variant **metrics; /* Collected metrics or facts for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
struct iovec_wrapper upload_answer;
} Context;
int upload_collected(Context *context);