Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

140 lines
4.2 KiB
Go

package hostcollect
import (
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/itworx/pulse/internal/host"
)
// pseudoFilesystems never describe usable capacity; reporting them would fill the
// filesystem list with kernel bookkeeping and push real volumes past the limit.
var pseudoFilesystems = map[string]struct{}{
"autofs": {}, "bpf": {}, "binfmt_misc": {}, "cgroup": {}, "cgroup2": {},
"configfs": {}, "debugfs": {}, "devpts": {}, "devtmpfs": {}, "efivarfs": {},
"fuse.gvfsd-fuse": {}, "fusectl": {}, "hugetlbfs": {}, "mqueue": {}, "nsfs": {},
"overlay": {}, "proc": {}, "pstore": {}, "ramfs": {}, "rpc_pipefs": {},
"securityfs": {}, "selinuxfs": {}, "squashfs": {}, "sysfs": {}, "tmpfs": {},
"tracefs": {},
}
type mountEntry struct {
device string
mount string
fsType string
}
// parseMounts reads /proc/mounts. Fields are space separated and the kernel escapes
// space, tab, newline and backslash inside the device and mount point as octal
// sequences, so an Unraid share called "/mnt/user/Media Backup" arrives as
// "/mnt/user/Media\040Backup" and must be unescaped before it can be used as a path.
func parseMounts(data []byte) []mountEntry {
all := lines(data)
entries := make([]mountEntry, 0, len(all))
for _, line := range all {
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
entries = append(entries, mountEntry{
device: unescapeMountField(fields[0]),
mount: unescapeMountField(fields[1]),
fsType: fields[2],
})
}
return entries
}
func unescapeMountField(value string) string {
if !strings.Contains(value, `\`) {
return value
}
var builder strings.Builder
builder.Grow(len(value))
for index := 0; index < len(value); index++ {
if value[index] == '\\' && index+3 < len(value) {
if decoded, err := strconv.ParseUint(value[index+1:index+4], 8, 8); err == nil {
builder.WriteByte(byte(decoded))
index += 3
continue
}
}
builder.WriteByte(value[index])
}
return builder.String()
}
// filesystems reports capacity and inode usage per real mount point.
//
// It is disabled unless FilesystemRoot is configured. Inside a container the host's
// /proc/mounts lists host paths that do not exist in the container's mount namespace;
// calling statfs on the same string would either fail or — worse, for "/" — silently
// measure the container's own overlay and report it as the host's root filesystem.
// Mounting a host root read-only and pointing FilesystemRoot at it is an explicit,
// auditable decision rather than an accident of naming.
func (c *Collector) filesystems(warnings *warningSet) []host.RawFilesystem {
if c.options.FilesystemRoot == "" {
warnings.add(WarningFilesystemDisabled)
return nil
}
data, err := readLimited(c.procPath("mounts"), maxFileBytes)
if err != nil {
warnings.add(WarningMountsUnavailable)
return nil
}
seen := make(map[string]struct{})
results := make([]host.RawFilesystem, 0, 16)
failures := 0
for _, entry := range parseMounts(data) {
if _, pseudo := pseudoFilesystems[entry.fsType]; pseudo {
continue
}
if entry.mount == "" || len(entry.mount) > 512 {
continue
}
if _, duplicate := seen[entry.mount]; duplicate {
continue
}
seen[entry.mount] = struct{}{}
usage, statErr := c.options.StatFS(filepath.Join(c.options.FilesystemRoot, entry.mount))
if statErr != nil {
failures++
continue
}
if usage.CapacityBytes == 0 {
continue
}
if usage.UsedBytes > usage.CapacityBytes {
usage.UsedBytes = usage.CapacityBytes
}
item := host.RawFilesystem{
Mount: entry.mount,
Filesystem: truncate(entry.fsType, 64),
CapacityBytes: usage.CapacityBytes,
UsedBytes: usage.UsedBytes,
}
if usage.InodesTotal > 0 {
used := usage.InodesUsed
if used > usage.InodesTotal {
used = usage.InodesTotal
}
item.Inodes = &host.RawInodes{Total: usage.InodesTotal, Used: used}
}
results = append(results, item)
}
if failures > 0 {
warnings.add(WarningFilesystemPartial)
}
sort.Slice(results, func(i, j int) bool { return results[i].Mount < results[j].Mount })
if len(results) > c.options.HostLimits.MaxFilesystems {
results = results[:c.options.HostLimits.MaxFilesystems]
warnings.add(WarningFilesystemTruncated)
}
if len(results) == 0 {
return nil
}
return results
}