This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user