1
0
mirror of https://github.com/systemd/systemd synced 2026-03-01 18:54:47 +01:00

Compare commits

..

No commits in common. "c871afd311349cec99a75a39e719f5a476bb8b20" and "be9dd0da4053fafaebbbc19bb703621dae49cac2" have entirely different histories.

18 changed files with 231 additions and 923 deletions

11
TODO
View File

@ -134,12 +134,17 @@ Features:
* oci: support "data" in any OCI descriptor, not just manifest config. * oci: support "data" in any OCI descriptor, not just manifest config.
* report: * report:
- plug "facts" into systemd-report too, i.e. stuff that is more static, such as hostnames, ssh keys and so on. - should the list of metrics use JSON-SEQ? or maybe be wrapped in a json
- pass filtering hints to services, so that they can also be applied server-side, not just client side array (the latter might be necessary, once we sign the combination)
- metrics from pid1: suppress metrics form units that are inactive and have nothing to report - metrics from pid1: suppress metrics form units that are inactive and have nothing to report
- how to plug facts into this? i.e. hostname, ssh keys, and so on
- switch to daan's suggested hierarchy?
- enforce naming rules: a backend can only report metrics with the prefix of its service name
- use that to optimize sorted report generation
- figure out report vs. metrics
- systemd-report: add prefix matching of metrics
- add "hint-suppress-zero" flag (which suppresses all metrics which are zero) - add "hint-suppress-zero" flag (which suppresses all metrics which are zero)
- add "hint-object" parameter (which only queries info about certain object) - add "hint-object" parameter (which only queries info about certain object)
- make systemd-report a varlink service
* implement a varlink registry service, similar to the one of the reference * implement a varlink registry service, similar to the one of the reference
implementation, backed by /run/varlink/registry/. Then, also implement implementation, backed by /run/varlink/registry/. Then, also implement

View File

@ -37,47 +37,6 @@
standard output.</para> standard output.</para>
</refsect1> </refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>metrics</command> <arg choice="opt" rep="repeat">MATCH</arg></term>
<listitem><para>Acquire a list of metrics values from all local services providing them, and write
them to standard output. Optionally takes one or more match expressions for filtering the metrics to
show. The expression either may be a literal metric family name to search for, or a prefix of one
(which will be matched only at dot boundaries). If multiple matches are specified as multiple
paramaters, any metric matching <emphasis>any</emphasis> of the specified matches are shown.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><command>describe-metrics</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>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><command>list-sources</command></term>
<listitem><para>Show list of known metrics sources.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1> <refsect1>
<title>Options</title> <title>Options</title>
@ -95,10 +54,7 @@
</listitem> </listitem>
</varlistentry> </varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="json" /> <xi:include href="standard-options.xml" xpointer="json" />
<xi:include href="standard-options.xml" xpointer="j" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" /> <xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" /> <xi:include href="standard-options.xml" xpointer="version" />
</variablelist> </variablelist>

View File

@ -295,12 +295,6 @@ int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange
assert(mode < _UID_RANGE_USERNS_MODE_MAX); assert(mode < _UID_RANGE_USERNS_MODE_MAX);
assert(ret); assert(ret);
r = is_our_namespace(userns_fd, NAMESPACE_USER);
if (r < 0)
return r;
if (r > 0)
return uid_range_load_userns(/* path= */ NULL, mode, ret);
r = userns_enter_and_pin(userns_fd, &pidref); r = userns_enter_and_pin(userns_fd, &pidref);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -101,10 +101,7 @@ static OciLayerState* oci_layer_state_free(OciLayerState *st) {
pidref_done_sigkill_wait(&st->tar_pid); pidref_done_sigkill_wait(&st->tar_pid);
if (st->temp_path) { if (st->temp_path) {
import_remove_tree( import_remove_tree(st->temp_path, st->pull ? &st->pull->userns_fd : NULL, st->pull->flags);
st->temp_path,
st->pull ? &st->pull->userns_fd : NULL,
st->pull ? st->pull->flags : 0);
free(st->temp_path); free(st->temp_path);
} }
free(st->final_path); free(st->final_path);

View File

@ -743,105 +743,3 @@ int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_jso
return 0; return 0;
} }
int json_variant_compare(sd_json_variant *a, sd_json_variant *b) {
int r;
r = CMP(!!a, !!b);
if (r != 0)
return r;
if (sd_json_variant_equal(a, b))
return 0;
if (sd_json_variant_is_null(a))
return -1;
if (sd_json_variant_is_null(b))
return 1;
if (sd_json_variant_is_string(a) &&
sd_json_variant_is_string(b))
return strcmp(sd_json_variant_string(a), sd_json_variant_string(b));
if (sd_json_variant_is_integer(a) &&
sd_json_variant_is_integer(b))
return CMP(sd_json_variant_integer(a), sd_json_variant_integer(b));
if (sd_json_variant_is_unsigned(a) &&
sd_json_variant_is_unsigned(b))
return CMP(sd_json_variant_unsigned(a), sd_json_variant_unsigned(b));
/* We cannot necessarily compare 64bit signed with unsigned, hence we go via sign checking instead */
if (sd_json_variant_is_number(a) && sd_json_variant_is_number(b)) {
if (sd_json_variant_is_negative(a) &&
!sd_json_variant_is_negative(b))
return -1;
if (!sd_json_variant_is_negative(a) &&
sd_json_variant_is_negative(b))
return 1;
}
if (sd_json_variant_is_real(a) &&
sd_json_variant_is_real(b))
return CMP(sd_json_variant_real(a), sd_json_variant_real(b));
if (sd_json_variant_is_boolean(a) &&
sd_json_variant_is_boolean(b))
return CMP(sd_json_variant_boolean(a), sd_json_variant_boolean(b));
if (sd_json_variant_is_array(a) &&
sd_json_variant_is_array(b)) {
size_t n = sd_json_variant_elements(a),
m = sd_json_variant_elements(b);
for (size_t i = 0; i < n || i < m; i++) {
if (i >= n)
return -1;
if (i >= m)
return 1;
r = json_variant_compare(
sd_json_variant_by_index(a, i),
sd_json_variant_by_index(b, i));
if (r != 0)
return r;
}
return 0;
}
if (sd_json_variant_is_object(a) &&
sd_json_variant_is_object(b)) {
const char *k, *lowest = NULL;
sd_json_variant *v;
int result = 0;
JSON_VARIANT_OBJECT_FOREACH(k, v, a) {
if (lowest && strcmp(k, lowest) >= 0)
continue;
r = json_variant_compare(v, sd_json_variant_by_key(b, k));
if (r != 0) {
lowest = k;
result = r;
}
}
JSON_VARIANT_OBJECT_FOREACH(k, v, b) {
if (lowest && strcmp(k, lowest) >= 0)
continue;
r = json_variant_compare(v, sd_json_variant_by_key(a, k));
if (r != 0) {
lowest = k;
result = -r;
}
}
return result;
}
return CMP(sd_json_variant_type(a), sd_json_variant_type(b));
}

View File

@ -273,5 +273,3 @@ int json_variant_new_fd_info(sd_json_variant **ret, int fd);
char *json_underscorify(char *p); char *json_underscorify(char *p);
char *json_dashify(char *p); char *json_dashify(char *p);
int json_variant_compare(sd_json_variant *a, sd_json_variant *b);

View File

@ -68,7 +68,7 @@ static inline struct mount *real_mount(struct vfsmount *mnt) {
return container_of(mnt, struct mount, mnt); return container_of(mnt, struct mount, mnt);
} }
static int validate_mount(struct vfsmount *v) { static int validate_inode_on_mount(struct inode *inode, struct vfsmount *v) {
struct user_namespace *mount_userns, *task_userns, *p; struct user_namespace *mount_userns, *task_userns, *p;
unsigned task_userns_inode; unsigned task_userns_inode;
struct task_struct *task; struct task_struct *task;
@ -124,9 +124,10 @@ static int validate_path(const struct path *path, int ret) {
if (ret != 0) /* propagate earlier error */ if (ret != 0) /* propagate earlier error */
return ret; return ret;
inode = path->dentry->d_inode;
v = path->mnt; v = path->mnt;
return validate_mount(v); return validate_inode_on_mount(inode, v);
} }
SEC("lsm/path_chown") SEC("lsm/path_chown")

View File

@ -8,136 +8,180 @@
#include "errno-util.h" #include "errno-util.h"
#include "fd-util.h" #include "fd-util.h"
#include "log.h"
#include "main-func.h"
#include "namespace-util.h" #include "namespace-util.h"
#include "pidref.h" #include "pidref.h"
#include "process-util.h" #include "process-util.h"
#include "rm-rf.h" #include "rm-rf.h"
#include "tests.h"
#include "tmpfile-util.h" #include "tmpfile-util.h"
#include "userns-restrict.h" #include "userns-restrict.h"
static int make_tmpfs_fsmount(void) { static int make_tmpfs_fsmount(void) {
_cleanup_close_ int fsfd = -EBADF, mntfd = -EBADF; _cleanup_close_ int fsfd = -EBADF, mntfd = -EBADF;
fsfd = ASSERT_OK_ERRNO(fsopen("tmpfs", FSOPEN_CLOEXEC)); fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC);
ASSERT_OK_ERRNO(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)); assert_se(fsfd >= 0);
assert_se(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) >= 0);
mntfd = ASSERT_OK_ERRNO(fsmount(fsfd, FSMOUNT_CLOEXEC, 0)); mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, 0);
assert_se(mntfd >= 0);
return TAKE_FD(mntfd); return TAKE_FD(mntfd);
} }
static struct userns_restrict_bpf *bpf_obj = NULL; static void test_works_reg(int parent_fd, const char *fname) {
STATIC_DESTRUCTOR_REGISTER(bpf_obj, userns_restrict_bpf_freep); _cleanup_close_ int fd = -EBADF;
static int intro(void) { fd = openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666);
int r; assert_se(fd >= 0);
r = userns_restrict_install(/* pin= */ false, &bpf_obj);
if (ERRNO_IS_NOT_SUPPORTED(r))
return log_tests_skipped("LSM-BPF logic not supported");
if (ERRNO_IS_PRIVILEGE(r))
return log_tests_skipped("Lacking privileges");
ASSERT_OK(r);
return 0;
} }
TEST(userns_restrict) { static void test_fails_reg(int parent_fd, const char *fname) {
errno = 0;
assert_se(openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666) < 0);
assert_se(errno == EPERM);
}
static void test_works_dir(int parent_fd, const char *fname) {
assert_se(mkdirat(parent_fd, fname, 0666) >= 0);
}
static void test_fails_dir(int parent_fd, const char *fname) {
errno = 0;
assert_se(mkdirat(parent_fd, fname, 0666) < 0);
assert_se(errno == EPERM);
}
static int run(int argc, char *argv[]) {
_cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL;
_cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF; _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF;
_cleanup_(rm_rf_physical_and_freep) char *t = NULL; _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
int r; int r;
ASSERT_OK(mkdtemp_malloc(NULL, &t)); log_set_max_level(LOG_DEBUG);
log_setup();
host_fd1 = ASSERT_OK_ERRNO(open(t, O_DIRECTORY|O_CLOEXEC)); r = userns_restrict_install(/* pin= */ false, &obj);
host_tmpfs = ASSERT_OK(make_tmpfs_fsmount()); if (ERRNO_IS_NOT_SUPPORTED(r)) {
userns_fd = ASSERT_OK(userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true)); log_notice("Skipping test, LSM-BPF logic not supported.");
return EXIT_TEST_SKIP;
}
if (ERRNO_IS_PRIVILEGE(r)) {
log_notice("Skipping test, lacking privileges.");
return EXIT_TEST_SKIP;
}
if (r < 0)
return r;
ASSERT_OK(userns_restrict_put_by_fd( assert_se(mkdtemp_malloc(NULL, &t) >= 0);
bpf_obj,
host_fd1 = open(t, O_DIRECTORY|O_CLOEXEC);
assert_se(host_fd1 >= 0);
host_tmpfs = make_tmpfs_fsmount();
assert_se(host_tmpfs >= 0);
userns_fd = userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true);
if (userns_fd < 0)
return log_error_errno(userns_fd, "Failed to make user namespace: %m");
r = userns_restrict_put_by_fd(
obj,
userns_fd, userns_fd,
/* replace= */ true, /* replace= */ true,
/* mount_fds= */ NULL, /* mount_fds= */ NULL,
/* n_mount_fds= */ 0)); /* n_mount_fds= */ 0);
if (r < 0)
return log_error_errno(r, "Failed to restrict user namespace: %m");
afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC)); afd = eventfd(0, EFD_CLOEXEC);
bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC)); bfd = eventfd(0, EFD_CLOEXEC);
r = ASSERT_OK(pidref_safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pidref)); assert_se(afd >= 0 && bfd >= 0);
r = pidref_safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pidref);
assert_se(r >= 0);
if (r == 0) { if (r == 0) {
_cleanup_close_ int private_tmpfs = -EBADF; _cleanup_close_ int private_tmpfs = -EBADF;
ASSERT_OK_ERRNO(setns(userns_fd, CLONE_NEWUSER)); assert_se(setns(userns_fd, CLONE_NEWUSER) >= 0);
ASSERT_OK_ERRNO(unshare(CLONE_NEWNS)); assert_se(unshare(CLONE_NEWNS) >= 0);
/* Allocate tmpfs locally */ /* Allocate tmpfs locally */
private_tmpfs = make_tmpfs_fsmount(); private_tmpfs = make_tmpfs_fsmount();
/* These two host mounts should be inaccessible */ /* These two host mounts should be inaccessible */
ASSERT_ERROR_ERRNO(openat(host_fd1, "test", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM); test_fails_reg(host_fd1, "test");
ASSERT_ERROR_ERRNO(openat(host_tmpfs, "xxx", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM); test_fails_reg(host_tmpfs, "xxx");
ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "test2", 0666), EPERM); test_fails_dir(host_fd1, "test2");
ASSERT_ERROR_ERRNO(mkdirat(host_tmpfs, "xxx2", 0666), EPERM); test_fails_dir(host_tmpfs, "xxx2");
/* But this mount created locally should be fine */ /* But this mount created locally should be fine */
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "yyy", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(private_tmpfs, "yyy");
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "yyy2", 0666)); test_works_dir(private_tmpfs, "yyy2");
/* Let's sync with the parent, so that it allowlists more stuff for us */ /* Let's sync with the parent, so that it allowlists more stuff for us */
ASSERT_OK_ERRNO(eventfd_write(afd, 1)); assert_se(eventfd_write(afd, 1) >= 0);
uint64_t x; uint64_t x;
ASSERT_OK_ERRNO(eventfd_read(bfd, &x)); assert_se(eventfd_read(bfd, &x) >= 0);
/* And now we should also have access to the host tmpfs */ /* And now we should also have access to the host tmpfs */
safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "zzz", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(host_tmpfs, "zzz");
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "aaa", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(private_tmpfs, "aaa");
ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "zzz2", 0666)); test_works_dir(host_tmpfs, "zzz2");
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "aaa2", 0666)); test_works_dir(private_tmpfs, "aaa2");
/* But this one should still fail */ /* But this one should still fail */
ASSERT_ERROR_ERRNO(openat(host_fd1, "bbb", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM); test_fails_reg(host_fd1, "bbb");
ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "bbb2", 0666), EPERM); test_fails_dir(host_fd1, "bbb2");
/* Sync again, to get more stuff allowlisted */ /* Sync again, to get more stuff allowlisted */
ASSERT_OK_ERRNO(eventfd_write(afd, 1)); assert_se(eventfd_write(afd, 1) >= 0);
ASSERT_OK_ERRNO(eventfd_read(bfd, &x)); assert_se(eventfd_read(bfd, &x) >= 0);
/* Everything should now be allowed */ /* Everything should now be allowed */
safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "ccc", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(host_tmpfs, "ccc");
safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(host_fd1, "ddd");
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "eee", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(private_tmpfs, "eee");
ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "ccc2", 0666)); test_works_dir(host_tmpfs, "ccc2");
safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd2", O_RDWR|O_CREAT|O_CLOEXEC, 0666))); test_works_reg(host_fd1, "ddd2");
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "eee2", 0666)); test_works_dir(private_tmpfs, "eee2");
_exit(EXIT_SUCCESS); _exit(EXIT_SUCCESS);
} }
uint64_t x; uint64_t x;
ASSERT_OK_ERRNO(eventfd_read(afd, &x)); assert_se(eventfd_read(afd, &x) >= 0);
ASSERT_OK(userns_restrict_put_by_fd( r = userns_restrict_put_by_fd(
bpf_obj, obj,
userns_fd, userns_fd,
/* replace= */ false, /* replace= */ false,
&host_tmpfs, &host_tmpfs,
1)); 1);
if (r < 0)
return log_error_errno(r, "Failed to loosen user namespace: %m");
ASSERT_OK_ERRNO(eventfd_write(bfd, 1)); assert_se(eventfd_write(bfd, 1) >= 0);
ASSERT_OK_ERRNO(eventfd_read(afd, &x));
ASSERT_OK(userns_restrict_put_by_fd( assert_se(eventfd_read(afd, &x) >= 0);
bpf_obj,
r = userns_restrict_put_by_fd(
obj,
userns_fd, userns_fd,
/* replace= */ false, /* replace= */ false,
&host_fd1, &host_fd1,
1)); 1);
if (r < 0)
return log_error_errno(r, "Failed to loosen user namespace: %m");
ASSERT_OK_ERRNO(eventfd_write(bfd, 1)); assert_se(eventfd_write(bfd, 1) >= 0);
ASSERT_OK(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG)); assert_se(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG) >= 0);
return 0;
} }
DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); DEFINE_MAIN_FUNCTION(run);

View File

@ -1,7 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
executables += [ executables += [
libexec_template + { executable_template + {
'name' : 'systemd-report', 'name' : 'systemd-report',
'public' : true, 'public' : true,
'sources' : files('report.c'), 'sources' : files('report.c'),

View File

@ -8,45 +8,27 @@
#include "alloc-util.h" #include "alloc-util.h"
#include "ansi-color.h" #include "ansi-color.h"
#include "build.h" #include "build.h"
#include "chase.h"
#include "dirent-util.h" #include "dirent-util.h"
#include "format-table.h" #include "fd-util.h"
#include "log.h" #include "log.h"
#include "main-func.h" #include "main-func.h"
#include "parse-argument.h" #include "parse-argument.h"
#include "path-lookup.h" #include "path-lookup.h"
#include "pretty-print.h" #include "pretty-print.h"
#include "recurse-dir.h"
#include "runtime-scope.h" #include "runtime-scope.h"
#include "set.h" #include "set.h"
#include "sort-util.h" #include "sort-util.h"
#include "string-util.h" #include "string-util.h"
#include "strv.h"
#include "time-util.h" #include "time-util.h"
#include "varlink-idl-util.h"
#include "verbs.h"
#define METRICS_MAX 1024U #define METRICS_MAX 1024U
#define METRICS_LINKS_MAX 128U #define METRICS_LINKS_MAX 128U
#define TIMEOUT_USEC (30 * USEC_PER_SEC) /* 30 seconds */ #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 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 sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
static char **arg_matches = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
typedef enum Action {
ACTION_LIST,
ACTION_DESCRIBE,
_ACTION_MAX,
_ACTION_INVALID = -EINVAL,
} Action;
typedef struct Context { typedef struct Context {
Action action;
sd_event *event; sd_event *event;
Set *link_infos; Set *link_infos;
sd_json_variant **metrics; /* Collected metrics for sorting */ sd_json_variant **metrics; /* Collected metrics for sorting */
@ -56,7 +38,7 @@ typedef struct Context {
typedef struct LinkInfo { typedef struct LinkInfo {
Context *context; Context *context;
sd_varlink *link; sd_varlink *link;
char *name; char *metric_prefix;
} LinkInfo; } LinkInfo;
static LinkInfo* link_info_free(LinkInfo *li) { static LinkInfo* link_info_free(LinkInfo *li) {
@ -64,7 +46,7 @@ static LinkInfo* link_info_free(LinkInfo *li) {
return NULL; return NULL;
sd_varlink_close_unref(li->link); sd_varlink_close_unref(li->link);
free(li->name); free(li->metric_prefix);
return mfree(li); return mfree(li);
} }
@ -79,12 +61,12 @@ static void context_done(Context *context) {
DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free); DEFINE_TRIVIAL_CLEANUP_FUNC(LinkInfo*, link_info_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
link_info_hash_ops, link_info_hash_ops,
void, void,
trivial_hash_func, trivial_hash_func,
trivial_compare_func, trivial_compare_func,
LinkInfo, LinkInfo,
link_info_free); link_info_free);
static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) { static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b) {
const char *name_a, *name_b, *object_a, *object_b; const char *name_a, *name_b, *object_a, *object_b;
@ -117,47 +99,15 @@ static int metric_compare(sd_json_variant *const *a, sd_json_variant *const *b)
return strcmp_ptr(fields_str_a, fields_str_b); return strcmp_ptr(fields_str_a, fields_str_b);
} }
static int metrics_name_valid(const char *metric_name) { static inline bool metric_startswith_prefix(const char *metric_name, const char *prefix) {
/* Validates a metrics family name. Since the prefix shall match the Varlink service name, we'll
* enforce Varlink interface naming rules on it. Given how close we are to Varlink let's also enforce
* rules on metrics names similar to those of Varlink field names. */
const char *e = strrchr(metric_name, '.');
if (!e)
return false;
_cleanup_free_ char *j = strndup(metric_name, e - metric_name);
if (!j)
return -ENOMEM;
if (!varlink_idl_interface_name_is_valid(j))
return false;
if (!varlink_idl_field_name_is_valid(e+1))
return false;
return true;
}
static bool metric_startswith_prefix(const char *metric_name, const char *prefix) {
if (isempty(metric_name) || isempty(prefix)) if (isempty(metric_name) || isempty(prefix))
return false; return false;
/* NB: this checks for a *true* prefix, i.e. insists on the dot separator after the prefix. Or in
* other words, "foo" is not going to be considered a prefix of "foo", but of "foo.bar" it will. */
const char *m = startswith(metric_name, prefix); const char *m = startswith(metric_name, prefix);
return !isempty(m) && m[0] == '.'; return !isempty(m) && m[0] == '.';
} }
typedef enum { static bool metrics_validate_one(LinkInfo *li, sd_json_variant *metric) {
VERDICT_INVALID,
VERDICT_MATCH,
VERDICT_MISMATCH,
} Verdict;
static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) {
int r; int r;
assert(li); assert(li);
@ -172,48 +122,10 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) {
r = sd_json_dispatch(metric, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &metric_name); r = sd_json_dispatch(metric, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &metric_name);
if (r < 0) { if (r < 0) {
log_debug_errno(r, "Failed to get metric name, assuming name is not valid: %m"); log_debug_errno(r, "Failed to get metric name, assuming name is not valid: %m");
return VERDICT_INVALID; return false;
} }
/* Validate metric name is generally valid */ return metric_startswith_prefix(metric_name, li->metric_prefix);
r = metrics_name_valid(metric_name);
if (r < 0) {
log_debug_errno(r, "Failed to determine if '%s' is a valid metric name: %m", metric_name);
return VERDICT_INVALID;
}
if (!r) {
log_debug("Metric name '%s' is not valid, skipping.", metric_name);
return VERDICT_INVALID;
}
/* Validate metric name matches the Varlink service it was found on */
if (!metric_startswith_prefix(metric_name, li->name)) {
log_debug("Metric name '%s' does not match service name '%s', skipping.", metric_name, li->name);
return VERDICT_INVALID;
}
/* Check it against any specified matches */
bool matches;
if (strv_isempty(arg_matches))
matches = true;
else {
matches = false;
/* Allow exact matches or prefix matches */
STRV_FOREACH(i, arg_matches)
if (streq(metric_name, *i) ||
metric_startswith_prefix(metric_name, *i)) {
matches = true;
break;
}
}
if (!matches) {
log_debug("Metric '%s' does not match search, ignoring.", metric_name);
return VERDICT_MISMATCH;
}
return VERDICT_MATCH;
} }
static int metrics_on_query_reply( static int metrics_on_query_reply(
@ -230,11 +142,11 @@ static int metrics_on_query_reply(
if (error_id) { if (error_id) {
if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED))
log_warning("Varlink connection to '%s' disconnected prematurely, ignoring.", li->name); log_info("Varlink disconnected");
else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT)) else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT))
log_warning("Varlink connection to '%s' timed out, ignoring.", li->name); log_info("Varlink timed out");
else else
log_warning("Varlink error from '%s', ignoring: %s", li->name, error_id); log_error("Varlink error: %s", error_id);
goto finish; goto finish;
} }
@ -244,13 +156,10 @@ static int metrics_on_query_reply(
goto finish; goto finish;
} }
Verdict v = metrics_verdict(li, parameters); if (!metrics_validate_one(li, parameters)) {
if (v == VERDICT_INVALID) {
context->n_invalid_metrics++; context->n_invalid_metrics++;
goto finish; goto finish;
} }
if (v == VERDICT_MISMATCH)
goto finish;
/* Collect metrics for later sorting */ /* Collect metrics for later sorting */
if (!GREEDY_REALLOC(context->metrics, context->n_metrics + 1)) if (!GREEDY_REALLOC(context->metrics, context->n_metrics + 1))
@ -269,7 +178,7 @@ finish:
return 0; return 0;
} }
static int metrics_call(Context *context, const char *name, const char *path) { static int metrics_call(Context *context, const char *path) {
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
int r; int r;
@ -292,12 +201,9 @@ static int metrics_call(Context *context, const char *name, const char *path) {
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to bind reply callback: %m"); return log_error_errno(r, "Failed to bind reply callback: %m");
const char *method = context->action == ACTION_LIST ? "io.systemd.Metrics.List" : "io.systemd.Metrics.Describe"; r = sd_varlink_observe(vl, "io.systemd.Metrics.List", /* parameters= */ NULL);
r = sd_varlink_observe(vl,
method,
/* parameters= */ NULL);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to issue %s() call: %m", method); return log_error_errno(r, "Failed to issue io.systemd.Metrics.List call: %m");
_cleanup_(link_info_freep) LinkInfo *li = new(LinkInfo, 1); _cleanup_(link_info_freep) LinkInfo *li = new(LinkInfo, 1);
if (!li) if (!li)
@ -306,346 +212,95 @@ static int metrics_call(Context *context, const char *name, const char *path) {
*li = (LinkInfo) { *li = (LinkInfo) {
.context = context, .context = context,
.link = sd_varlink_ref(vl), .link = sd_varlink_ref(vl),
.name = strdup(name),
}; };
if (!li->name) r = path_extract_filename(path, &li->metric_prefix);
return log_oom(); if (r < 0)
return log_error_errno(r, "Failed to extract metric name from path %s: %m", path);
if (set_ensure_put(&context->link_infos, &link_info_hash_ops, li) < 0)
return log_oom();
(void) sd_varlink_set_userdata(vl, li); (void) sd_varlink_set_userdata(vl, li);
if (set_ensure_put(&context->link_infos, &link_info_hash_ops, li) < 0)
return log_oom();
TAKE_PTR(li); TAKE_PTR(li);
return 0; return 0;
} }
static int metrics_output_list(Context *context, Table **ret) { static int metrics_output_sorted(Context *context) {
int r;
assert(context);
_cleanup_(table_unrefp) Table *table = table_new("family", "object", "fields", "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, (size_t) 3);
FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
struct {
const char *name;
const char *object;
sd_json_variant *fields, *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 },
{ "fields", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_variant_noref, voffsetof(d, fields), 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 metric, skipping: %s", t);
continue;
}
r = table_add_many(
table,
TABLE_STRING, d.name,
TABLE_STRING, d.object,
TABLE_JSON, d.fields,
TABLE_SET_WEIGHT, 50U,
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 metrics_output_describe(Context *context, Table **ret) {
int r;
assert(context);
_cleanup_(table_unrefp) Table *table = table_new("family", "type", "description");
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 *type;
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 },
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(d, type), 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 metric description, skipping: %s", t);
continue;
}
r = table_add_many(
table,
TABLE_STRING, d.name,
TABLE_STRING, d.type,
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 metrics_output(Context *context) {
int r; int r;
assert(context); assert(context);
typesafe_qsort(context->metrics, context->n_metrics, metric_compare); typesafe_qsort(context->metrics, context->n_metrics, metric_compare);
if (sd_json_format_enabled(arg_json_format_flags)) { FOREACH_ARRAY(m, context->metrics, context->n_metrics) {
FOREACH_ARRAY(m, context->metrics, context->n_metrics) { r = sd_json_variant_dump(
r = sd_json_variant_dump( *m,
*m, arg_json_format_flags | SD_JSON_FORMAT_FLUSH,
arg_json_format_flags | SD_JSON_FORMAT_FLUSH, stdout,
stdout, /* prefix= */ NULL);
/* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to write JSON: %m");
}
if (context->n_metrics == 0 && arg_legend)
log_info("No metrics collected.");
return 0;
}
_cleanup_(table_unrefp) Table *table = NULL;
switch(context->action) {
case ACTION_LIST:
r = metrics_output_list(context, &table);
break;
case ACTION_DESCRIBE:
r = metrics_output_describe(context, &table);
break;
default:
assert_not_reached();
}
if (r < 0)
return r;
if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) {
r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0) if (r < 0)
return r; return log_error_errno(r, "Failed to write JSON: %m");
} }
if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { if (context->n_metrics == 0)
if (table_isempty(table)) log_info("No metrics collected.");
printf("No metrics available.\n");
else
printf("\n%zu metrics listed.\n", table_get_rows(table) - 1);
}
return 0; return 0;
} }
static int parse_metrics_matches(char **matches) { static int metrics_query(void) {
int r; int r;
STRV_FOREACH(i, matches) { _cleanup_free_ char *metrics_path = NULL;
r = metrics_name_valid(*i); r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &metrics_path);
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)
return log_oom();
}
strv_sort_uniq(arg_matches);
return 0;
}
static bool test_service_matches(const char *service) {
assert(service);
if (strv_isempty(arg_matches))
return true;
/* Only contact services whose name is either a prefix of any of the specified metrics families, or
* if the specified metric families are a prefix of the service.
*
* Example: if user specifies "foo" we want to match sockets "foo" and "foo.bar".
* if user specifies "foo.waldo" we want to match sockets "foo" and "foo.waldo" as well as "foo.waldo.quux".
*
* in other words: it should be fine to specify any prefix of a service name to get all metrics from all matching services.
* 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) {
if (streq(service, *i))
return true;
if (metric_startswith_prefix(*i, service) ||
metric_startswith_prefix(service, *i))
return true;
}
return false;
}
static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
int r;
assert(ret_directory);
assert(ret);
_cleanup_free_ char *sources_path = NULL;
r = runtime_directory_generic(arg_runtime_scope, "systemd/report", &sources_path);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to determine sources directory path: %m"); return log_error_errno(r, "Failed to determine metrics directory path: %m");
log_debug("Looking for metrics in '%s'.", sources_path); log_debug("Looking for reports in %s/", metrics_path);
size_t m = 0; _cleanup_(context_done) Context context = {};
_cleanup_free_ DirectoryEntries *de = NULL; r = sd_event_default(&context.event);
r = readdir_all_at(
AT_FDCWD,
sources_path,
RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE,
&de);
if (r == -ENOENT)
*ret = NULL;
else if (r < 0)
return log_error_errno(r, "Failed to enumerate '%s': %m", sources_path);
else {
/* Filter out non-sockets/non-symlinks and badly named entries */
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
if (!IN_SET(d->d_type, DT_SOCK, DT_LNK))
continue;
if (!varlink_idl_interface_name_is_valid(d->d_name))
continue;
if (!test_service_matches(d->d_name))
continue;
de->entries[m++] = *i;
}
de->n_entries = m;
*ret = TAKE_PTR(de);
}
*ret_directory = TAKE_PTR(sources_path);
return m > 0;
}
static int verb_metrics(int argc, char *argv[], void *userdata) {
Action action;
int r;
assert(argc >= 1);
assert(argv);
/* Enable JSON-SEQ mode here, since we'll dump a large series of JSON objects */
arg_json_format_flags |= SD_JSON_FORMAT_SEQ;
if (streq_ptr(argv[0], "metrics"))
action = ACTION_LIST;
else {
assert(streq_ptr(argv[0], "describe-metrics"));
action = ACTION_DESCRIBE;
}
r = parse_metrics_matches(argv + 1);
if (r < 0) if (r < 0)
return r; 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");
_cleanup_(context_done) Context context = {
.action = action,
};
size_t n_skipped_sources = 0; size_t n_skipped_sources = 0;
_cleanup_closedir_ DIR *d = opendir(metrics_path);
if (!d) {
if (errno != ENOENT)
return log_error_errno(errno, "Failed to open metrics directory %s: %m", metrics_path);
} else
FOREACH_DIRENT(de, d,
return log_warning_errno(errno, "Failed to read %s: %m", metrics_path)) {
_cleanup_free_ DirectoryEntries *de = NULL; if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN))
_cleanup_free_ char *sources_path = NULL; continue;
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_LINKS_MAX) { if (set_size(context.link_infos) >= METRICS_LINKS_MAX) {
n_skipped_sources++; n_skipped_sources++;
break; break;
} }
_cleanup_free_ char *p = path_join(sources_path, d->d_name); _cleanup_free_ char *p = path_join(metrics_path, de->d_name);
if (!p) if (!p)
return log_oom(); return log_oom();
(void) metrics_call(&context, d->d_name, p); (void) metrics_call(&context, p);
} }
}
if (set_isempty(context.link_infos)) {
if (arg_legend)
log_info("No metrics sources found.");
} else {
assert(context.event);
if (set_isempty(context.link_infos))
log_info("No metrics sources found.");
else {
r = sd_event_loop(context.event); r = sd_event_loop(context.event);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m"); return log_error_errno(r, "Failed to run event loop: %m");
r = metrics_output(&context); r = metrics_output_sorted(&context);
if (r < 0) if (r < 0)
return r; return r;
} }
@ -665,62 +320,7 @@ static int verb_metrics(int argc, char *argv[], void *userdata) {
return 0; return 0;
} }
static int verb_list_sources(int argc, char *argv[], void *userdata) { static int help(void) {
int r;
_cleanup_(table_unrefp) Table *table = table_new("source", "address");
if (!table)
return log_oom();
_cleanup_free_ char *sources_path = NULL;
_cleanup_free_ DirectoryEntries *de = NULL;
r = readdir_sources(&sources_path, &de);
if (r < 0)
return r;
if (r > 0)
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
_cleanup_free_ char *k = path_join(sources_path, d->d_name);
if (!k)
return log_oom();
_cleanup_free_ char *resolved = NULL;
r = chase(k, /* root= */ NULL, CHASE_MUST_BE_SOCKET, &resolved, /* ret_fd= */ NULL);
if (r < 0) {
log_warning_errno(r, "Failed to resolve '%s', skipping: %m", k);
continue;
}
_cleanup_free_ char *j = strjoin("unix:", resolved);
if (!j)
return log_oom();
r = table_add_many(
table,
TABLE_STRING, d->d_name,
TABLE_STRING, j);
if (r < 0)
return table_log_add_error(r);
}
if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) {
r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
if (r < 0)
return r;
}
if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) {
if (table_isempty(table))
printf("No sources available.\n");
else
printf("\n%zu sources listed.\n", table_get_rows(table) - 1);
}
return 0;
}
static int verb_help(int argc, char *argv[], void *userdata) {
_cleanup_free_ char *link = NULL; _cleanup_free_ char *link = NULL;
int r; int r;
@ -728,31 +328,19 @@ static int verb_help(int argc, char *argv[], void *userdata) {
if (r < 0) if (r < 0)
return log_oom(); return log_oom();
printf("%1$s [OPTIONS...] COMMAND ...\n" printf("%s [OPTIONS...] \n\n"
"\n%5$sAcquire metrics from local sources.%6$s\n" "%sPrint metrics for all system components.%s\n\n"
"\n%3$sCommands:%4$s\n"
" metrics [MATCH...] Acquire list of metrics and their values\n"
" describe-metrics [MATCH...]\n"
" Describe available metrics\n"
" list-sources Show list of known metrics sources\n"
"\n%3$sOptions:%4$s\n"
" -h --help Show this help\n" " -h --help Show this help\n"
" --version Show package version\n" " --version Show package version\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
" --user Connect to user service manager\n" " --user Connect to user service manager\n"
" --system Connect to system service manager (default)\n" " --system Connect to system service manager (default)\n"
" --json=pretty|short\n" " --json=pretty|short\n"
" Configure JSON output\n" " Configure JSON output\n"
" -j Equivalent to --json=pretty (on TTY) or --json=short\n" "\nSee the %s for details.\n",
" (otherwise)\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name, program_invocation_short_name,
link,
ansi_underline(),
ansi_normal(),
ansi_highlight(), ansi_highlight(),
ansi_normal()); ansi_normal(),
link);
return 0; return 0;
} }
@ -760,21 +348,17 @@ static int verb_help(int argc, char *argv[], void *userdata) {
static int parse_argv(int argc, char *argv[]) { static int parse_argv(int argc, char *argv[]) {
enum { enum {
ARG_VERSION = 0x100, ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_USER, ARG_USER,
ARG_SYSTEM, ARG_SYSTEM,
ARG_JSON, ARG_JSON,
}; };
static const struct option options[] = { static const struct option options[] = {
{ "help", no_argument, NULL, 'h' }, { "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION }, { "version", no_argument, NULL, ARG_VERSION },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "user", no_argument, NULL, ARG_USER },
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND }, { "system", no_argument, NULL, ARG_SYSTEM },
{ "user", no_argument, NULL, ARG_USER }, { "json", required_argument, NULL, ARG_JSON },
{ "system", no_argument, NULL, ARG_SYSTEM },
{ "json", required_argument, NULL, ARG_JSON },
{} {}
}; };
@ -783,22 +367,14 @@ static int parse_argv(int argc, char *argv[]) {
assert(argc >= 0); assert(argc >= 0);
assert(argv); assert(argv);
while ((c = getopt_long(argc, argv, "hj", options, NULL)) >= 0) while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
switch (c) { switch (c) {
case 'h': case 'h':
return verb_help(/* argc= */ 0, /* argv= */ NULL, /* userdata= */ NULL); return help();
case ARG_VERSION: case ARG_VERSION:
return version(); return version();
case ARG_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case ARG_NO_LEGEND:
arg_legend = false;
break;
case ARG_USER: case ARG_USER:
arg_runtime_scope = RUNTIME_SCOPE_USER; arg_runtime_scope = RUNTIME_SCOPE_USER;
break; break;
@ -814,10 +390,6 @@ static int parse_argv(int argc, char *argv[]) {
break; break;
case 'j':
arg_json_format_flags = SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
break;
case '?': case '?':
return -EINVAL; return -EINVAL;
@ -825,22 +397,15 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached(); assert_not_reached();
} }
if (optind < argc)
return log_error_errno(
SYNTHETIC_ERRNO(EINVAL),
"%s takes no arguments.",
program_invocation_short_name);
return 1; return 1;
} }
static int report_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, 1, 0, verb_help },
{ "metrics", VERB_ANY, VERB_ANY, 0, verb_metrics },
{ "describe-metrics", VERB_ANY, VERB_ANY, 0, verb_metrics },
{ "list-sources", VERB_ANY, 1, 0, verb_list_sources },
{}
};
return dispatch_verb(argc, argv, verbs, NULL);
}
static int run(int argc, char *argv[]) { static int run(int argc, char *argv[]) {
int r; int r;
@ -850,7 +415,7 @@ static int run(int argc, char *argv[]) {
if (r <= 0) if (r <= 0)
return r; return r;
return report_main(argc, argv); return metrics_query();
} }
DEFINE_MAIN_FUNCTION(run); DEFINE_MAIN_FUNCTION(run);

View File

@ -14,7 +14,6 @@
#include "glyph-util.h" #include "glyph-util.h"
#include "gunicode.h" #include "gunicode.h"
#include "in-addr-util.h" #include "in-addr-util.h"
#include "json-util.h"
#include "memory-util.h" #include "memory-util.h"
#include "memstream-util.h" #include "memstream-util.h"
#include "pager.h" #include "pager.h"
@ -111,7 +110,6 @@ typedef struct TableData {
pid_t pid; pid_t pid;
mode_t mode; mode_t mode;
dev_t devnum; dev_t devnum;
sd_json_variant *json;
/* … add more here as we start supporting more cell data types … */ /* … add more here as we start supporting more cell data types … */
}; };
} TableData; } TableData;
@ -251,9 +249,6 @@ static TableData *table_data_free(TableData *d) {
if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED)) if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED))
strv_free(d->strv); strv_free(d->strv);
if (d->type == TABLE_JSON)
sd_json_variant_unref(d->json);
return mfree(d); return mfree(d);
} }
@ -368,9 +363,6 @@ static size_t table_data_size(TableDataType type, const void *data) {
case TABLE_DEVNUM: case TABLE_DEVNUM:
return sizeof(dev_t); return sizeof(dev_t);
case TABLE_JSON:
return sizeof(sd_json_variant*);
default: default:
assert_not_reached(); assert_not_reached();
} }
@ -453,22 +445,12 @@ static TableData *table_data_new(
d->ellipsize_percent = ellipsize_percent; d->ellipsize_percent = ellipsize_percent;
d->uppercase = uppercase; d->uppercase = uppercase;
switch (type) { if (IN_SET(type, TABLE_STRV, TABLE_STRV_WRAPPED)) {
case TABLE_STRV:
case TABLE_STRV_WRAPPED:
d->strv = strv_copy(data); d->strv = strv_copy(data);
if (!d->strv) if (!d->strv)
return NULL; return NULL;
break; } else
case TABLE_JSON:
d->json = sd_json_variant_ref((sd_json_variant*) data);
break;
default:
memcpy_safe(d->data, data, data_size); memcpy_safe(d->data, data, data_size);
}
return TAKE_PTR(d); return TAKE_PTR(d);
} }
@ -1114,10 +1096,6 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) {
data = &buffer.devnum; data = &buffer.devnum;
break; break;
case TABLE_JSON:
data = va_arg(ap, sd_json_variant*);
break;
case TABLE_SET_MINIMUM_WIDTH: { case TABLE_SET_MINIMUM_WIDTH: {
size_t w = va_arg(ap, size_t); size_t w = va_arg(ap, size_t);
@ -1528,9 +1506,6 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t
return CMP(minor(a->devnum), minor(b->devnum)); return CMP(minor(a->devnum), minor(b->devnum));
case TABLE_JSON:
return json_variant_compare(a->json, b->json);
default: default:
; ;
} }
@ -2096,18 +2071,6 @@ static const char *table_data_format(
break; break;
case TABLE_JSON: {
if (!d->json)
return table_ersatz_string(t);
char *p;
if (sd_json_variant_format(d->json, /* flags= */ 0, &p) < 0)
return NULL;
d->formatted = p;
break;
}
default: default:
assert_not_reached(); assert_not_reached();
} }
@ -2321,9 +2284,6 @@ static bool table_data_isempty(const TableData *d) {
if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED)) if (IN_SET(d->type, TABLE_STRV, TABLE_STRV_WRAPPED))
return strv_isempty(d->strv); return strv_isempty(d->strv);
if (d->type == TABLE_JSON)
return sd_json_variant_is_null(d->json);
/* Note that an empty string we do not consider empty here! */ /* Note that an empty string we do not consider empty here! */
return false; return false;
} }
@ -3031,15 +2991,6 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) {
SD_JSON_BUILD_UNSIGNED(major(d->devnum)), SD_JSON_BUILD_UNSIGNED(major(d->devnum)),
SD_JSON_BUILD_UNSIGNED(minor(d->devnum)))); SD_JSON_BUILD_UNSIGNED(minor(d->devnum))));
case TABLE_JSON:
if (!d->json)
return sd_json_variant_new_null(ret);
if (ret)
*ret = sd_json_variant_ref(d->json);
return 0;
case TABLE_STRING_WITH_ANSI: { case TABLE_STRING_WITH_ANSI: {
_cleanup_free_ char *s = strdup(d->string); _cleanup_free_ char *s = strdup(d->string);
if (!s) if (!s)

View File

@ -58,7 +58,6 @@ typedef enum TableDataType {
TABLE_MODE, /* as in UNIX file mode (mode_t), in typical octal output */ TABLE_MODE, /* as in UNIX file mode (mode_t), in typical octal output */
TABLE_MODE_INODE_TYPE, /* also mode_t, but displays only the inode type as string */ TABLE_MODE_INODE_TYPE, /* also mode_t, but displays only the inode type as string */
TABLE_DEVNUM, /* a dev_t, displayed in the usual major:minor way */ TABLE_DEVNUM, /* a dev_t, displayed in the usual major:minor way */
TABLE_JSON, /* an sd_json_variant, for output formatted into text */
_TABLE_DATA_TYPE_MAX, _TABLE_DATA_TYPE_MAX,
/* The following are not really data types, but commands for table_add_cell_many() to make changes to /* The following are not really data types, but commands for table_add_cell_many() to make changes to

View File

@ -56,7 +56,7 @@ static void mstack_done(MStack *mstack) {
safe_close(mstack->usr_mount_fd); safe_close(mstack->usr_mount_fd);
} }
MStack* mstack_free(MStack *mstack) { MStack *mstack_free(MStack *mstack) {
if (!mstack) if (!mstack)
return NULL; return NULL;
@ -501,7 +501,7 @@ static bool mount_is_ro(MStackMount *m, MStackFlags flags) {
IN_SET(m->mount_type, MSTACK_LAYER, MSTACK_ROBIND); IN_SET(m->mount_type, MSTACK_LAYER, MSTACK_ROBIND);
} }
static const char* mount_name(MStackMount *m) { static const char *mount_name(MStackMount *m) {
assert(m); assert(m);
/* Returns some vaguely useful identifier for this layer, for showing in debug output */ /* Returns some vaguely useful identifier for this layer, for showing in debug output */
@ -1120,10 +1120,11 @@ int mstack_apply(
return mstack_bind_mounts(&mstack, where, /* where_fd= */ -EBADF, flags, ret_root_fd); return mstack_bind_mounts(&mstack, where, /* where_fd= */ -EBADF, flags, ret_root_fd);
} }
int mstack_load(const char *dir, int dir_fd, MStack **ret) { int mstack_load(const char *dir,
int r; int dir_fd,
MStack **ret) {
assert(ret); int r;
/* Well-known errors: /* Well-known errors:
* *
@ -1131,7 +1132,7 @@ int mstack_load(const char *dir, int dir_fd, MStack **ret) {
* -EBADMSG Bad file suffix, inode type for layer, or unrecognized entry * -EBADMSG Bad file suffix, inode type for layer, or unrecognized entry
*/ */
_cleanup_(mstack_freep) MStack *mstack = new(MStack, 1); MStack *mstack = new(MStack, 1);
if (!mstack) if (!mstack)
return -ENOMEM; return -ENOMEM;
@ -1141,7 +1142,9 @@ int mstack_load(const char *dir, int dir_fd, MStack **ret) {
if (r < 0) if (r < 0)
return r; return r;
*ret = TAKE_PTR(mstack); if (ret)
*ret = TAKE_PTR(mstack);
return 0; return 0;
} }

View File

@ -47,7 +47,7 @@ typedef struct MStack {
.usr_mount_fd = -EBADF, \ .usr_mount_fd = -EBADF, \
} }
MStack* mstack_free(MStack *mstack); MStack *mstack_free(MStack *mstack);
DEFINE_TRIVIAL_CLEANUP_FUNC(MStack*, mstack_free); DEFINE_TRIVIAL_CLEANUP_FUNC(MStack*, mstack_free);
int mstack_load(const char *dir, int dir_fd, MStack **ret); int mstack_load(const char *dir, int dir_fd, MStack **ret);
@ -56,17 +56,7 @@ int mstack_make_mounts(MStack *mstack, const char *temp_mount_dir, MStackFlags f
int mstack_bind_mounts(MStack *mstack, const char *where, int where_fd, MStackFlags flags, int *ret_root_fd); int mstack_bind_mounts(MStack *mstack, const char *where, int where_fd, MStackFlags flags, int *ret_root_fd);
/* The four calls above in one */ /* The four calls above in one */
int mstack_apply( int mstack_apply(const char *dir, int dir_fd, const char *where, const char *temp_mount_dir, sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags, int *ret_root_fd);
const char *dir,
int dir_fd,
const char *where,
const char *temp_mount_dir,
sd_varlink *mountfsd_link,
int userns_fd,
const ImagePolicy *image_policy,
const ImageFilter *image_filter,
MStackFlags flags,
int *ret_root_fd);
int mstack_is_read_only(MStack *mstack); int mstack_is_read_only(MStack *mstack);
int mstack_is_foreign_uid_owned(MStack *mstack); int mstack_is_foreign_uid_owned(MStack *mstack);

View File

@ -18,15 +18,15 @@ static SD_VARLINK_DEFINE_ERROR(NoSuchMetric);
static SD_VARLINK_DEFINE_METHOD_FULL( static SD_VARLINK_DEFINE_METHOD_FULL(
List, List,
SD_VARLINK_REQUIRES_MORE, SD_VARLINK_REQUIRES_MORE,
SD_VARLINK_FIELD_COMMENT("Metric family name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"), SD_VARLINK_FIELD_COMMENT("Metric name, e.g. io.systemd.Manager.unitsByTypeTotal or io.systemd.Manager.unitActiveState"),
SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0), SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
/* metric value has various types depending on MetricFamilyType and actual data double/int/uint */ /* metric value has various types depending on MetricFamilyType and actual data double/int/uint */
SD_VARLINK_FIELD_COMMENT("Metric value"),
SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0),
SD_VARLINK_FIELD_COMMENT("Metric object name can be unit name, process name, etc, e.g. dev-hvc0.device"), SD_VARLINK_FIELD_COMMENT("Metric object name can be unit name, process name, etc, e.g. dev-hvc0.device"),
SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(object, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Metric fields are values to differentiate between different metrics in the same metric family"), SD_VARLINK_FIELD_COMMENT("Metric fields are values to differentiate between different metrics in the same metric family"),
SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(fields, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE));
SD_VARLINK_FIELD_COMMENT("Metric value"),
SD_VARLINK_DEFINE_OUTPUT(value, SD_VARLINK_ANY, 0));
static SD_VARLINK_DEFINE_METHOD_FULL( static SD_VARLINK_DEFINE_METHOD_FULL(
Describe, Describe,

View File

@ -1523,69 +1523,4 @@ TEST(access_mode) {
&mm), ERANGE); &mm), ERANGE);
} }
static void test_json_variant_compare_one(const char *a, const char *b, int expected) {
int r;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *aa = NULL;
if (!isempty(a))
ASSERT_OK(sd_json_parse(a, /* flags= */ 0, &aa, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
_cleanup_(sd_json_variant_unrefp) sd_json_variant *bb = NULL;
if (!isempty(b))
ASSERT_OK(sd_json_parse(b, /* flags= */ 0, &bb, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
r = json_variant_compare(aa, bb);
log_debug("%s vs %s → %i (expected %i)", a, b, r, expected);
if (expected < 0)
ASSERT_LT(r, 0);
else if (expected > 0)
ASSERT_GT(r, 0);
else
ASSERT_EQ(r, 0);
r = json_variant_compare(bb, aa);
if (expected < 0)
ASSERT_GT(r, 0);
else if (expected > 0)
ASSERT_LT(r, 0);
else
ASSERT_EQ(r, 0);
}
TEST(json_variant_compare) {
test_json_variant_compare_one("null", "\"a\"", -1);
test_json_variant_compare_one(NULL, "\"a\"", -1);
test_json_variant_compare_one("0", "1", -1);
test_json_variant_compare_one("1", "0", 1);
test_json_variant_compare_one("0", "0", 0);
test_json_variant_compare_one("1", "1", 0);
test_json_variant_compare_one("1", "null", 1);
test_json_variant_compare_one("null", "1", -1);
test_json_variant_compare_one("null", "null", 0);
test_json_variant_compare_one("false", "true", -1);
test_json_variant_compare_one("true", "false", 1);
test_json_variant_compare_one("true", "true", 0);
test_json_variant_compare_one("false", "false", 0);
test_json_variant_compare_one("\"a\"", "\"b\"", -1);
test_json_variant_compare_one("\"b\"", "\"a\"", 1);
test_json_variant_compare_one("18446744073709551615", "0", 1);
test_json_variant_compare_one("0", "18446744073709551615", -1);
test_json_variant_compare_one("18446744073709551615", "18446744073709551615", 0);
test_json_variant_compare_one("-9223372036854775808", "18446744073709551615", -1);
test_json_variant_compare_one("18446744073709551615", "-9223372036854775808", 1);
test_json_variant_compare_one("1.1", "3.4", -1);
test_json_variant_compare_one("1", "3.4", -1);
test_json_variant_compare_one("[1,2]", "[1,2]", 0);
test_json_variant_compare_one("[1,2]", "[2,1]", -1);
test_json_variant_compare_one("[1,2]", "[1,2,3]", -1);
test_json_variant_compare_one("{}", "{\"a\":\"b\"}", -1);
test_json_variant_compare_one("{\"a\":\"b\"}", "{\"a\":\"b\"}", 0);
test_json_variant_compare_one("{\"a\":\"b\"}", "{\"b\":\"c\"}", 1);
test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"b\":\"c\",\"a\":\"b\"}", 0);
test_json_variant_compare_one("{\"a\":\"b\",\"b\":\"c\"}", "{\"a\":\"b\"}", 1);
}
DEFINE_TEST_MAIN(LOG_DEBUG); DEFINE_TEST_MAIN(LOG_DEBUG);

View File

@ -1,37 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# Unset $PAGER so we don't have to use --no-pager everywhere
export PAGER=
REPORT=/usr/lib/systemd/systemd-report
"$REPORT" --help
"$REPORT" help
"$REPORT" --version
"$REPORT" --json=help
"$REPORT" metrics
"$REPORT" metrics -j
"$REPORT" metrics --no-legend
"$REPORT" describe-metrics
"$REPORT" describe-metrics -j
"$REPORT" describe-metrics --no-legend
"$REPORT" list-sources
"$REPORT" list-sources -j
"$REPORT" list-sources --no-legend
"$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
# test io.systemd.Network Metrics
varlinkctl info /run/systemd/report/io.systemd.Network
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 {}

View File

@ -242,3 +242,12 @@ systemd-run --wait --pipe --user --machine testuser@ \
# test io.systemd.Unit in user manager # test io.systemd.Unit in user manager
systemd-run --wait --pipe --user --machine testuser@ \ systemd-run --wait --pipe --user --machine testuser@ \
varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}' varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
# test report
systemd-report
# test io.systemd.Network Metrics
varlinkctl info /run/systemd/report/io.systemd.Network
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 {}