report: add basic upload functionality
This commit is contained in:
@@ -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 + {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user