Public source validation / validate (push) Failing after 3m8s
140 lines
4.2 KiB
Go
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
|
|
}
|