report: add metrics source that reports current disk space data
This commit is contained in:
committed by
Lennart Poettering
parent
9336b9ce23
commit
c25226b820
@@ -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'],
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user