Files
mesh-catalog/modules/disk-load/cmd/disk-load/filesystems.go
T
jochen 6fdf5293eb
mesh/merge-gate pass: builds new: modules/disk-load, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Add disk-load: no tool said a machine's iowait, disk load or who does the I/O (hq issue 312)
2026-10-08 10:47:41 +02:00

133 lines
4.2 KiB
Go

package main
import (
"path/filepath"
"strings"
)
// storedOn are the filesystem types that hold data on a device or a server. Everything else in the
// mount table (proc, sysfs, cgroup, tmpfs, overlay layers, squashfs images…) is the kernel's or a copy.
var storedOn = map[string]bool{
"ext2": true, "ext3": true, "ext4": true, "xfs": true, "btrfs": true, "zfs": true, "f2fs": true,
"bcachefs": true, "vfat": true, "exfat": true, "ntfs": true, "ntfs3": true, "fuseblk": true,
"nfs": true, "nfs4": true, "cifs": true, "smb3": true,
}
// mount is one line of the mount table.
type mount struct{ source, target, fstype string }
// unescape undoes the mount table's octal escapes (a space is \040).
func unescape(s string) string {
if !strings.Contains(s, `\`) {
return s
}
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+3 < len(s) {
o := s[i+1 : i+4]
if len(o) == 3 && o[0] >= '0' && o[0] <= '3' && o[1] >= '0' && o[1] <= '7' && o[2] >= '0' && o[2] <= '7' {
b.WriteByte((o[0]-'0')<<6 | (o[1]-'0')<<3 | (o[2] - '0'))
i += 3
continue
}
}
b.WriteByte(s[i])
}
return b.String()
}
// mounts is this process's mount table, only the filesystems that store data.
func (m *Machine) mounts() []mount {
var out []mount
for _, line := range strings.Split(m.read("/proc/self/mounts"), "\n") {
f := strings.Fields(line)
if len(f) < 3 || !storedOn[f[2]] {
continue
}
out = append(out, mount{source: unescape(f[0]), target: unescape(f[1]), fstype: f[2]})
}
return out
}
// Filesystem is one filesystem's space, once however many places it is mounted at.
type Filesystem struct {
Mount string `json:"mount"`
AlsoAt []string `json:"also_at,omitempty"`
Source string `json:"source"`
Device string `json:"device,omitempty"`
Type string `json:"type"`
SizeGiB float64 `json:"size_gib"`
UsedGiB float64 `json:"used_gib"`
AvailGiB float64 `json:"available_gib"`
UsedPct float64 `json:"used_pct"`
InodesUsed float64 `json:"inodes_used_pct,omitempty"`
Unanswered string `json:"error,omitempty"`
}
// blockName is the kernel's name for the device a mount's source names: /dev/nvme0n1p3 is nvme0n1p3,
// /dev/mapper/root the dm-N whose name is root; "" for a source that is no local device.
func (m *Machine) blockName(source string, mappers map[string]string) string {
if !strings.HasPrefix(source, "/dev/") {
return ""
}
if name, ok := strings.CutPrefix(source, "/dev/mapper/"); ok {
return mappers[name]
}
// /dev/disk/by-uuid/… and the like are links the system made; reading where one points creates nothing.
if m.Root == "/" {
if real, err := filepath.EvalSymlinks(source); err == nil {
source = real
}
}
return filepath.Base(source)
}
// mappers is each device-mapper name with the dm-N it is.
func (m *Machine) mappers() map[string]string {
out := map[string]string{}
for _, dev := range m.names("/sys/block") {
if name := m.mapperName(dev); name != "" {
out[name] = dev
}
}
return out
}
// Filesystems is the space of every filesystem that stores data. A filesystem mounted at several
// places (a btrfs subvolume, a bind mount) is said once, at its first mount, with the others beside it.
func (m *Machine) Filesystems() []Filesystem {
mappers := m.mappers()
var out []Filesystem
seen := map[string]int{}
for _, mt := range m.mounts() {
key := mt.source
if i, ok := seen[key]; ok {
out[i].AlsoAt = append(out[i].AlsoAt, mt.target)
continue
}
fs := Filesystem{Mount: mt.target, Source: mt.source, Type: mt.fstype, Device: m.blockName(mt.source, mappers)}
st, err := m.Statfs(m.path(mt.target))
if err != nil {
fs.Unanswered = err.Error()
} else if st.Size > 0 {
used := st.Size - st.Free
fs.SizeGiB = round1(float64(st.Size) / gib)
fs.UsedGiB = round1(float64(used) / gib)
fs.AvailGiB = round1(float64(st.Avail) / gib)
// As df counts it: used against what an ordinary user could have, so the reserve shows as full.
if used+st.Avail > 0 {
fs.UsedPct = round1(float64(used) / float64(used+st.Avail) * 100)
}
if st.Files > 0 {
fs.InodesUsed = round1(float64(st.Files-st.FilesFree) / float64(st.Files) * 100)
}
}
seen[key] = len(out)
out = append(out, fs)
}
if out == nil {
out = []Filesystem{}
}
return out
}