report: add metrics source that reports current disk space data

This commit is contained in:
Lennart Poettering
2026-09-25 16:43:13 +02:00
committed by Lennart Poettering
parent 9336b9ce23
commit c25226b820
15 changed files with 781 additions and 0 deletions
+4
View File
@@ -1209,6 +1209,10 @@ manpages = [
'ENABLE_RANDOMSEED'],
['systemd-remount-fs.service', '8', ['systemd-remount-fs'], ''],
['systemd-repart', '8', ['systemd-repart.service'], 'ENABLE_REPART'],
['systemd-report-diskspace@.service',
'8',
['systemd-report-diskspace', 'systemd-report-diskspace.socket'],
'HAVE_LIBMOUNT'],
['systemd-report-files@.service',
'8',
['systemd-report-files', 'systemd-report-files.socket'],
+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0"?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-report-diskspace_.service" xmlns:xi="http://www.w3.org/2001/XInclude"
conditional='HAVE_LIBMOUNT'>
<refentryinfo>
<title>systemd-report-diskspace@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-report-diskspace@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-report-diskspace@.service</refname>
<refname>systemd-report-diskspace.socket</refname>
<refname>systemd-report-diskspace</refname>
<refpurpose>Report size and free disk space of mounted file systems as system report metrics</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-report-diskspace@.service</filename></para>
<para><filename>systemd-report-diskspace.socket</filename></para>
<para><filename>/usr/lib/systemd/systemd-report-diskspace</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-report-diskspace@.service</command> is a metrics source for
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It
reports the size of the mounted file systems and how much disk space is still available on them, by
implementing the <constant>io.systemd.Metrics</constant> Varlink interface on a socket linked into the
<filename>/run/systemd/report/</filename> directory.</para>
<para>All mount points of the system are enumerated. Mount points of file systems that are not backed by
a block device (i.e. pseudo file systems such as <literal>proc</literal>, <literal>sysfs</literal> or
<literal>tmpfs</literal>, network file systems, <literal>overlay</literal>, …) are skipped, as are
read-only mount points. For each remaining mount point two gauge metrics are reported, with the mount
point path as metric object:</para>
<variablelist>
<varlistentry>
<term><literal>io.systemd.DiskSpace.SizeBytes</literal></term>
<listitem><para>The total size of the file system, in bytes. This matches the
<literal>1K-blocks</literal> column of
<citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry>
(scaled to bytes).</para></listitem>
</varlistentry>
<varlistentry>
<term><literal>io.systemd.DiskSpace.FreeBytes</literal></term>
<listitem><para>The disk space still available to unprivileged users on the file system, in
bytes. This matches the <literal>Avail</literal> column of
<citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
i.e. space reserved for the superuser (as many file systems do) is not counted as
free.</para></listitem>
</varlistentry>
</variablelist>
<para>The following metric fields are set on both metrics:</para>
<variablelist>
<varlistentry>
<term><varname>fileSystemType</varname></term>
<listitem><para>The file system type, for example <literal>ext4</literal> or
<literal>btrfs</literal>. Always set.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>source</varname></term>
<listitem><para>The mount source, typically the device node the file system is mounted from, for
example <literal>/dev/sda1</literal>. Omitted if unknown.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>originatingSource</varname></term>
<listitem><para>If the block device the file system is mounted from is a stacked one (for example a
LUKS or other device mapper volume), the device node of the block device it ultimately originates
from, for example <literal>/dev/sda2</literal> for a file system mounted from
<literal>/dev/mapper/root</literal>. Omitted if the source is not a stacked block device, or its
origin cannot be determined. Note that stacked devices backed by multiple block devices (e.g. RAID)
are not covered by this.</para></listitem>
</varlistentry>
</variablelist>
<para>A file system that is mounted multiple times (for example via bind mounts, or as multiple
subvolumes of the same volume) is reported only once, under the shortest of its mount point paths (and
among equally long ones, the alphabetically first). Mount points that are shadowed by another mount on
top of them, or that the service cannot access, are not considered.</para>
</refsect1>
<!-- Note: we do not document the command line switches here. The systemd-report-diskspace binary itself
should not be invoked by users, but only as a service. Use systemd-report(1) to acquire metrics. -->
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry project='man-pages'><refentrytitle>statfs</refentrytitle><manvolnum>2</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -35,6 +35,7 @@ enable systemd-nsresourced.socket
enable systemd-pstore.service
enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-diskspace.socket
enable systemd-report-files.socket
enable systemd-report-hwmon.socket
enable systemd-report-sign-plain.socket
+9
View File
@@ -29,6 +29,15 @@ executables += [
'report-cgroup-server.c',
),
},
libexec_template + {
'name' : 'systemd-report-diskspace',
'sources' : files(
'report-diskspace.c',
'report-diskspace-server.c',
),
'dependencies' : libmount_cflags,
'conditions' : ['HAVE_LIBMOUNT'],
},
libexec_template + {
'name' : 'systemd-report-files',
'sources' : files(
+95
View File
@@ -0,0 +1,95 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "sd-varlink.h"
#include "build.h"
#include "dlopen-note.h"
#include "log.h"
#include "main-func.h"
#include "report-diskspace.h"
#include "varlink-io.systemd.Metrics.h"
#include "varlink-util.h"
#include "verbs.h"
COMMAND(
"systemd-report-diskspace\0",
"Report disk space metrics.",
);
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_INHERIT_USERDATA, /* 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_Metrics);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
vs,
"io.systemd.Metrics.List", vl_method_list_metrics,
"io.systemd.Metrics.Describe", vl_method_describe_metrics);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(vs);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
int r;
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return command_print_help();
OPTION_COMMON_VERSION:
return version();
OPTION_COMMON_INTROSPECT_CLI:
return introspect_cli(SD_JSON_FORMAT_OFF);
}
if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"This program takes no arguments.");
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 log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"This program can only run as a Varlink service.");
return 1;
}
static int run(int argc, char *argv[]) {
int r;
LIBMOUNT_NOTE(required);
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return vl_server();
}
DEFINE_MAIN_FUNCTION(run);
+366
View File
@@ -0,0 +1,366 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <sys/statvfs.h>
#include "sd-device.h"
#include "sd-json.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "blockdev-util.h"
#include "device-util.h"
#include "devnum-util.h"
#include "fd-util.h"
#include "hash-funcs.h"
#include "hashmap.h"
#include "json-util.h"
#include "libmount-util.h"
#include "log.h"
#include "metrics.h"
#include "mountpoint-util.h"
#include "path-util.h"
#include "report-diskspace.h"
#include "sort-util.h"
#include "stat-util.h"
/* Reports the size of and the disk space still available on all mounted, writable, block device backed file
* systems, i.e. roughly what df(1) shows. Pseudo file systems (procfs, sysfs, tmpfs, …), network file
* systems and other file systems not backed by a block device are skipped, as are read-only mounts: their
* free space is not going to change, and typically is not meaningful anyway (squashfs, erofs, DDIs, …).
*
* A file system (i.e. a superblock, identified by its device number) that is mounted in multiple places
* (bind mounts, btrfs subvolumes, …) is reported only once, under its shortest mount path. */
typedef struct FileSystem {
dev_t devno; /* the hashmap key: device number of the superblock */
struct libmnt_fs *fs; /* the mount we report the file system under, owned by the mount table */
int fd; /* O_PATH fd to the mount point */
uint64_t free_bytes;
uint64_t size_bytes;
} FileSystem;
static FileSystem* file_system_free(FileSystem *f) {
if (!f)
return NULL;
safe_close(f->fd);
return mfree(f);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(FileSystem*, file_system_free);
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
file_system_hash_ops,
dev_t, devt_hash_func, devt_compare_func,
FileSystem, file_system_free);
static const char* file_system_path(const FileSystem *f) {
assert(f);
assert(f->fs);
return ASSERT_PTR(sym_mnt_fs_get_target(f->fs));
}
/* Among the mount points of the same file system we prefer the shortest path, and among equally short ones
* the alphabetically earlier. */
static bool path_is_preferred(const char *a, const char *b) {
assert(a);
assert(b);
int ret = CMP(strlen(a), strlen(b));
if (ret != 0)
return ret < 0;
return path_compare(a, b) < 0;
}
static int file_system_compare(FileSystem *const *a, FileSystem *const *b) {
assert(a);
assert(b);
return path_compare(file_system_path(*a), file_system_path(*b));
}
static int mount_collect(Hashmap **file_systems, struct libmnt_fs *fs) {
int r;
assert(file_systems);
assert(fs);
const char *target = sym_mnt_fs_get_target(fs);
if (!target)
return 0;
const char *fstype = sym_mnt_fs_get_fstype(fs);
if (!fstype)
return 0;
if (!fstype_is_blockdev_backed(fstype)) {
log_debug("Mount '%s' of type %s is not backed by a block device, skipping.", target, fstype);
return 0;
}
/* This covers both file systems that are read-only as a whole, and read-only bind mounts of
* otherwise writable file systems, since libmount searches both the superblock and the per-mount
* options here. */
r = sym_mnt_fs_get_option(fs, "ro", /* value= */ NULL, /* valsz= */ NULL);
if (r < 0)
return log_debug_errno(r, "Failed to check whether '%s' is mounted read-only: %m", target);
if (r == 0) {
log_debug("Mount '%s' is read-only, skipping.", target);
return 0;
}
dev_t devno = sym_mnt_fs_get_devno(fs);
if (devno == 0) {
log_debug("Mount '%s' has no device number, skipping.", target);
return 0;
}
FileSystem *existing = hashmap_get(*file_systems, &devno);
if (existing && !path_is_preferred(target, file_system_path(existing))) {
log_debug("File system mounted on '%s' is already known as '%s', skipping.",
target, file_system_path(existing));
return 0;
}
/* Mount points below a directory we lack access to (or that have been unmounted since the mount
* table was read) are simply not considered. */
_cleanup_close_ int fd = open(target, O_PATH|O_CLOEXEC);
if (fd < 0) {
log_debug_errno(errno, "Failed to open mount point '%s', ignoring: %m", target);
return 0;
}
/* Make sure the path still refers to the mount we are looking at, and not to something that was
* mounted on top of it since, shadowing it. Also catches mount points that are hidden entirely. */
int mnt_id;
r = path_get_mnt_id_at(fd, /* path= */ NULL, &mnt_id);
if (r < 0) {
log_debug_errno(r, "Failed to get mount ID of '%s', ignoring: %m", target);
return 0;
}
if (mnt_id != sym_mnt_fs_get_id(fs)) {
log_debug("Mount '%s' is shadowed by another mount, skipping.", target);
return 0;
}
struct statfs sfs;
r = xstatfsat(fd, /* path= */ NULL, &sfs);
if (r < 0) {
log_debug_errno(r, "Failed to statfs() '%s', ignoring: %m", target);
return 0;
}
/* Already excluded above based on the mount table, but let's check again on the authoritative data,
* in case things changed in the meantime. */
if (FLAGS_SET(sfs.f_flags, ST_RDONLY)) {
log_debug("Mount '%s' is read-only, skipping.", target);
return 0;
}
if (sfs.f_blocks == 0) {
log_debug("Mount '%s' has zero size, skipping.", target);
return 0;
}
/* Report the space available to unprivileged users as free, matching what df(1) shows as "Avail".
* Space reserved for the superuser (typically 5% on ext4) is hence not counted as free. */
uint64_t size_bytes, free_bytes;
if (!MUL_SAFE(&free_bytes, (uint64_t) sfs.f_frsize, (uint64_t) sfs.f_bavail) ||
!MUL_SAFE(&size_bytes, (uint64_t) sfs.f_frsize, (uint64_t) sfs.f_blocks)) {
log_debug("Sizes of '%s' overflow, skipping.", target);
return 0;
}
_cleanup_(file_system_freep) FileSystem *f = new(FileSystem, 1);
if (!f)
return log_oom();
*f = (FileSystem) {
.devno = devno,
.fs = fs,
.fd = TAKE_FD(fd),
.free_bytes = free_bytes,
.size_bytes = size_bytes,
};
/* Replace the entry we already have for this file system, if any, by the better one */
file_system_free(hashmap_remove(*file_systems, &f->devno));
if (hashmap_ensure_put(file_systems, &file_system_hash_ops, &f->devno, f) < 0)
return log_oom();
TAKE_PTR(f);
return 0;
}
/* If the file system's backing block device is a stacked one (LUKS-style DM, …), returns the device node of
* the block device it originates from, chased down through all layers. Returns NULL (and success) if the
* backing device is not a stacked block device, or is not a block device at all. */
static int file_system_get_originating_source(const FileSystem *f, char **ret) {
int r;
assert(f);
assert(ret);
/* NB: We do not use f->devno here, but acquire the devno again from the fd. That's because on btrfs
* we want the devnum of the backing device node, and not of the anonymous superblock */
dev_t devno;
r = get_block_device_fd(f->fd, &devno);
if (r < 0) {
return log_debug_errno(r, "Failed to get backing block device of '%s', ignoring: %m", file_system_path(f));
*ret = NULL;
return 0;
}
if (devno == 0) {
log_debug("Path '%s' has no backing block device, ignoring.", file_system_path(f));
*ret = NULL;
return 0;
}
/* The mount's device number is the backing block device's (except for file systems that use
* anonymous device numbers, such as btrfs, which we hence cannot trace this way). */
_cleanup_(sd_device_unrefp) sd_device *dev = NULL;
r = sd_device_new_from_devnum(&dev, 'b', devno);
if (r < 0) {
log_debug_errno(r, "Failed to get block device " DEVNUM_FORMAT_STR " backing '%s', ignoring: %m",
DEVNUM_FORMAT_VAL(devno), file_system_path(f));
*ret = NULL;
return 0;
}
_cleanup_(sd_device_unrefp) sd_device *origin = NULL;
r = block_device_get_originating(dev, &origin, /* recursive= */ true);
if (r < 0) {
if (r != -ENOENT) /* -ENOENT means: not stacked */
log_device_debug_errno(dev, r, "Failed to determine originating block device, ignoring: %m");
*ret = NULL;
return 0;
}
const char *node;
r = sd_device_get_devname(origin, &node);
if (r < 0) {
log_device_debug_errno(origin, r, "Failed to get device node of originating block device, ignoring: %m");
*ret = NULL;
return 0;
}
return strdup_to(ret, node);
}
static int file_system_send(
const MetricFamily mf[static 2],
sd_varlink *link,
const FileSystem *f) {
int r;
assert(mf && mf[0].name && mf[1].name);
assert(link);
assert(f);
_cleanup_free_ char *originating_source = NULL;
r = file_system_get_originating_source(f, &originating_source);
if (r < 0)
return log_error_errno(r, "Failed to determine originating source: %m");
_cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL;
r = sd_json_buildo(
&fields,
JSON_BUILD_PAIR_STRING_NON_EMPTY("source", sym_mnt_fs_get_source(f->fs)),
JSON_BUILD_PAIR_STRING_NON_EMPTY("originatingSource", originating_source),
SD_JSON_BUILD_PAIR_STRING("fileSystemType", sym_mnt_fs_get_fstype(f->fs)));
if (r < 0)
return log_error_errno(r, "Failed to build metric fields: %m");
r = metric_build_send_unsigned(mf + 0, link, file_system_path(f), f->free_bytes, fields);
if (r < 0)
return r;
return metric_build_send_unsigned(mf + 1, link, file_system_path(f), f->size_bytes, fields);
}
static int diskspace_generate(
const MetricFamily mf[static 2],
sd_varlink *link,
void *userdata) {
int r;
assert(mf);
assert(link);
_cleanup_(mnt_free_tablep) struct libmnt_table *table = NULL;
_cleanup_(mnt_free_iterp) struct libmnt_iter *iter = NULL;
r = libmount_parse_mountinfo(/* source= */ NULL, &table, &iter);
if (r < 0)
return log_error_errno(r, "Failed to parse /proc/self/mountinfo: %m");
/* First pass: collect all file systems we care about, keyed by superblock */
_cleanup_hashmap_free_ Hashmap *file_systems = NULL;
for (;;) {
struct libmnt_fs *fs;
r = sym_mnt_table_next_fs(table, iter, &fs);
if (r == 1)
break;
if (r < 0)
return log_error_errno(r, "Failed to get next entry from /proc/self/mountinfo: %m");
r = mount_collect(&file_systems, fs);
if (r < 0)
return r;
}
/* Second pass: report them, sorted by mount path for stable output */
_cleanup_free_ FileSystem **sorted = NULL;
size_t n = 0;
FileSystem *f;
HASHMAP_FOREACH(f, file_systems) {
if (!GREEDY_REALLOC(sorted, n + 1))
return log_oom();
sorted[n++] = f;
}
typesafe_qsort(sorted, n, file_system_compare);
FOREACH_ARRAY(i, sorted, n) {
r = file_system_send(mf, link, *i);
if (r < 0)
return r;
}
return 0;
}
static const MetricFamily diskspace_metric_family_table[] = {
/* Keep metrics ordered alphabetically. Both are generated by the same function, attached to the
* first of them. */
{
"io.systemd.DiskSpace.FreeBytes",
"Per file system metric: disk space available to unprivileged users in bytes, on writable "
"block device backed file systems (object=mount point, fileSystemType=file system type, "
"source=backing device, originatingSource=device the backing device is stacked on, if any)",
METRIC_FAMILY_TYPE_GAUGE,
.generate = diskspace_generate,
},
{
"io.systemd.DiskSpace.SizeBytes",
"Per file system metric: file system size in bytes, on writable block device backed file "
"systems (object=mount point, fileSystemType=file system type, source=backing device, "
"originatingSource=device the backing device is stacked on, if any)",
METRIC_FAMILY_TYPE_GAUGE,
},
{}
};
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_describe(diskspace_metric_family_table, link, parameters, flags, userdata);
}
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_list(diskspace_metric_family_table, link, parameters, flags, userdata);
}
+7
View File
@@ -0,0 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "forward.h"
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+2
View File
@@ -12,6 +12,7 @@
DLSYM_PROTOTYPE(mnt_free_iter) = NULL;
DLSYM_PROTOTYPE(mnt_free_table) = NULL;
DLSYM_PROTOTYPE(mnt_fs_get_devno) = NULL;
DLSYM_PROTOTYPE(mnt_fs_get_fs_options) = NULL;
DLSYM_PROTOTYPE(mnt_fs_get_fstype) = NULL;
DLSYM_PROTOTYPE(mnt_fs_get_id) = NULL;
@@ -154,6 +155,7 @@ int dlopen_libmount(int log_level) {
log_level,
DLSYM_ARG(mnt_free_iter),
DLSYM_ARG(mnt_free_table),
DLSYM_ARG(mnt_fs_get_devno),
DLSYM_ARG(mnt_fs_get_fs_options),
DLSYM_ARG(mnt_fs_get_fstype),
DLSYM_ARG(mnt_fs_get_id),
+1
View File
@@ -16,6 +16,7 @@
extern DLSYM_PROTOTYPE(mnt_free_iter);
extern DLSYM_PROTOTYPE(mnt_free_table);
extern DLSYM_PROTOTYPE(mnt_fs_get_devno);
extern DLSYM_PROTOTYPE(mnt_fs_get_fs_options);
extern DLSYM_PROTOTYPE(mnt_fs_get_fstype);
extern DLSYM_PROTOTYPE(mnt_fs_get_id);
@@ -109,6 +109,7 @@ INTROSPECTABLE=(
systemd-report
systemd-report-basic
systemd-report-cgroup
systemd-report-diskspace
systemd-report-files
systemd-report-hwmon
systemd-report-sign-plain
+99
View File
@@ -93,6 +93,105 @@ fi
"$REPORT" describe io.systemd.HWMon
"$REPORT" metrics io.systemd.HWMon
# test io.systemd.DiskSpace Metrics
# Mount a scratch ext4 file system and check that it is reported with plausible numbers, that a tmpfs next to
# it is not, that additional bind mounts of it do not result in additional rows but the shortest writable
# mount path wins regardless of mount order, that it disappears from the report once remounted read-only,
# and that a device mapper device stacked on top of it is reported with the loop device as originating
# source.
diskspace_mount_tests() {
# Loop devices and device mapper are not available in containers
if systemd-detect-virt --quiet --container; then
echo "Skipping io.systemd.DiskSpace mount tests in container."
return
fi
DISKSPACE_WORK="$(mktemp -d /var/tmp/report-diskspace.XXXXXX)"
diskspace_cleanup() {
set +e
umount "$DISKSPACE_WORK/tmpfs" "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/dm"
dmsetup remove report-diskspace
systemd-dissect --detach "$DISKSPACE_WORK/scratch.raw"
rm -rf "$DISKSPACE_WORK"
set -e
}
trap diskspace_cleanup EXIT
mkdir "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/dm" "$DISKSPACE_WORK/tmpfs"
truncate -s 64M "$DISKSPACE_WORK/scratch.raw"
mkfs.ext4 -q "$DISKSPACE_WORK/scratch.raw"
DISKSPACE_LOOP="$(systemd-dissect --attach --loop-ref=report-diskspace "$DISKSPACE_WORK/scratch.raw")"
mount "$DISKSPACE_LOOP" "$DISKSPACE_WORK/ext4"
# "a" sorts before "b" and is just as short, but is read-only, hence "b" must be picked
mount --bind -o ro "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/a"
mount --bind "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/b"
# "longer" is writable too, but has a longer path, hence must not win even though it is mounted last
mount --bind "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/longer"
mount -t tmpfs tmpfs "$DISKSPACE_WORK/tmpfs"
dd if=/dev/urandom of="$DISKSPACE_WORK/ext4/fill" bs=1M count=8
sync -f "$DISKSPACE_WORK/ext4"
diskspace_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
# Exactly one row per metric for the scratch file system, reported under the shortest writable mount path
[[ "$(echo "$diskspace_metrics" | jq -r --arg s "$DISKSPACE_LOOP" 'select(.name == "io.systemd.DiskSpace.SizeBytes" and .fields.source == $s) | .object')" == "$DISKSPACE_WORK/b" ]]
[[ "$(echo "$diskspace_metrics" | jq -r --arg s "$DISKSPACE_LOOP" 'select(.name == "io.systemd.DiskSpace.FreeBytes" and .fields.source == $s) | .object')" == "$DISKSPACE_WORK/b" ]]
diskspace_size="$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.name == "io.systemd.DiskSpace.SizeBytes" and .object == $o) | .value')"
diskspace_free="$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.name == "io.systemd.DiskSpace.FreeBytes" and .object == $o) | .value')"
# The file system cannot be larger than the image, and at least the 8M we wrote must be missing from the
# free space.
[[ "$diskspace_size" -gt 0 ]]
[[ "$diskspace_size" -le $((64 * 1024 * 1024)) ]]
[[ "$diskspace_free" -gt 0 ]]
[[ $((diskspace_free + 8 * 1024 * 1024)) -le "$diskspace_size" ]]
# df(1) derives the same two numbers from statfs(), hence they must match exactly.
read -r diskspace_df_size diskspace_df_avail < <(df -B1 --output=size,avail "$DISKSPACE_WORK/ext4" | sed 1d)
[[ "$diskspace_size" == "$diskspace_df_size" ]]
[[ "$diskspace_free" == "$diskspace_df_avail" ]]
# Both rows must carry the file system type as field, and no originating source, as a loop device is not
# stacked on anything
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.object == $o) | .fields.fileSystemType' | sort -u)" == "ext4" ]]
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.object == $o) | .fields | has("originatingSource") | tostring' | sort -u)" == "false" ]]
# The tmpfs may not be reported
(! echo "$diskspace_metrics" | jq -r '.object' | grep -Fx "$DISKSPACE_WORK/tmpfs" >/dev/null)
# Once remounted read-only the file system must no longer be reported, regardless of the mount path
mount -o remount,ro "$DISKSPACE_WORK/ext4"
diskspace_metrics="$(varlinkctl call --more --graceful=io.systemd.Metrics.NoSuchMetric /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
(! echo "$diskspace_metrics" | jq -r '.fields.source' | grep -Fx "$DISKSPACE_LOOP" >/dev/null)
# Now stack a (linear) device mapper device on top of the loop device, and mount the file system through
# that: the DM device must be reported as source, and the loop device it sits on as originating source. The
# loop device must be unmounted first, as DM needs exclusive access to it.
umount "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/ext4"
dmsetup create report-diskspace --table "0 $(blockdev --getsz "$DISKSPACE_LOOP") linear $DISKSPACE_LOOP 0"
udevadm wait --timeout=30 --settle /dev/mapper/report-diskspace
mount /dev/mapper/report-diskspace "$DISKSPACE_WORK/dm"
diskspace_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/dm" 'select(.object == $o) | .fields.source' | sort -u)" == "/dev/mapper/report-diskspace" ]]
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/dm" 'select(.object == $o) | .fields.originatingSource' | sort -u)" == "$DISKSPACE_LOOP" ]]
diskspace_cleanup
trap - EXIT
}
# The socket and service are only built if libmount is available
if ! systemctl cat systemd-report-diskspace.socket &>/dev/null; then
echo "systemd-report-diskspace.socket is not installed, skipping io.systemd.DiskSpace tests."
else
systemctl start systemd-report-diskspace.socket
varlinkctl info /run/systemd/report/io.systemd.DiskSpace
varlinkctl list-methods /run/systemd/report/io.systemd.DiskSpace
# varlinkctl emits a JSON-SEQ stream for --more; strip the record separators so that plain jq can consume it
diskspace_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.Describe {} | tr -d '\036')"
echo "$diskspace_describe" | jq -r 'select(.name == "io.systemd.DiskSpace.FreeBytes") | .type' | grep -wx gauge >/dev/null
echo "$diskspace_describe" | jq -r 'select(.name == "io.systemd.DiskSpace.SizeBytes") | .type' | grep -wx gauge >/dev/null
diskspace_mount_tests
"$REPORT" describe io.systemd.DiskSpace
"$REPORT" metrics io.systemd.DiskSpace
fi
# test io.systemd.Basic Metrics
# ensure the socket is running, as some distros don't enable it by default
systemctl start systemd-report-basic.socket
+5
View File
@@ -588,3 +588,8 @@ systemd.network.xml ./refsect1[title="[ModemManager] Section Options"]/variablel
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="chip"]
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="sensor"]
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="label"]
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[1]/varlistentry[term="io.systemd.DiskSpace.SizeBytes"]
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[1]/varlistentry[term="io.systemd.DiskSpace.FreeBytes"]
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="source"]
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="originatingSource"]
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="fileSystemType"]
+8
View File
@@ -794,6 +794,14 @@ units = [
},
{ 'file' : 'systemd-report-cgroup.socket' },
{ 'file' : 'systemd-report-cgroup@.service.in' },
{
'file' : 'systemd-report-diskspace.socket',
'conditions' : ['HAVE_LIBMOUNT'],
},
{
'file' : 'systemd-report-diskspace@.service.in',
'conditions' : ['HAVE_LIBMOUNT'],
},
{ 'file' : 'systemd-report-files.socket' },
{ 'file' : 'systemd-report-files@.service.in' },
{ 'file' : 'systemd-report-hwmon.socket' },
+29
View File
@@ -0,0 +1,29 @@
# 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=Disk Space Report Varlink Socket
Documentation=man:systemd-report-diskspace@.service(8)
DefaultDependencies=no
Before=sockets.target shutdown.target
Conflicts=shutdown.target
[Socket]
ListenStream=/run/systemd/report/io.systemd.DiskSpace
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
XAttrEntryPoint=user.varlink=entrypoint
XAttrListen=user.varlink=listen
XAttrAccept=user.varlink=server
[Install]
WantedBy=sockets.target
@@ -0,0 +1,43 @@
# 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=Disk Space Report Service
Documentation=man:systemd-report-diskspace@.service(8)
DefaultDependencies=no
Conflicts=shutdown.target
Before=shutdown.target
[Service]
# Note that DynamicUser=, ProtectSystem=, ProtectHome= and PrivateTmp= are deliberately not used here: they
# make the host's file systems appear read-only, or shadow them with private mounts, in the service's mount
# namespace, and hence would hide from this service exactly what it is supposed to report on.
CapabilityBoundingSet=
DeviceAllow=
IPAddressDeny=any
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateIPC=yes
PrivateNetwork=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RuntimeMaxSec=1min
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
ExecStart={{LIBEXECDIR}}/systemd-report-diskspace