package main import ( "fmt" "math" "strconv" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // Tools is the module's tools over one machine. Every one only reads. func Tools(m *Machine) []stdio.Tool { window := map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 2, at most 10)"} return []stdio.Tool{ { Name: "disk_load", Description: "How loaded this machine's storage is, sampled over a few seconds: the share of CPU time spent waiting on I/O (iowait), " + "I/O pressure stall (PSI), and for each disk how busy it was (%), read and write throughput (MiB/s), IOPS, the average wait " + "per request (ms) and the queue depth, with the filesystems it holds; then the filesystems' space and the processes " + "and the units moving the most bytes (from their cgroups), with their container. Each finding is said in a line under `said`. Replaces iostat, vmstat, " + "iotop and df. (r)", Input: map[string]any{ "seconds": window, "processes": map[string]any{"type": "integer", "description": "how many of the busiest units and processes to name (default 5, 0 for none, at most 50)"}, "all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"}, }, Run: func(args map[string]any) (any, error) { w, err := seconds(args) if err != nil { return nil, err } n, err := count(args, "processes", 5, 0, 50) if err != nil { return nil, err } return m.LoadOver(w, n, flag(args, "all"), true) }, }, { Name: "disk_top", Description: "What moved the most bytes to and from storage over a few seconds: by=unit (the default) each systemd unit " + "and container from its cgroup's io.stat, with MiB/s and IOPS, complete whoever runs the tools; by=container only the " + "containers, by name; by=process each process from /proc//io, which misses another account's unless the tool " + "runner is root. Replaces iotop. (r)", Input: map[string]any{ "seconds": window, "limit": map[string]any{"type": "integer", "description": "how many (default 10, at most 50)"}, "by": map[string]any{"type": "string", "enum": []string{"process", "unit", "container"}, "description": "what to rank (default unit)"}, }, Run: func(args map[string]any) (any, error) { w, err := seconds(args) if err != nil { return nil, err } limit, err := count(args, "limit", 10, 1, 50) if err != nil { return nil, err } by := text(args, "by") switch by { case "": by = "unit" case "process", "unit", "container": default: return nil, fmt.Errorf("by is process, unit or container, not %q", by) } out := map[string]any{"window_s": w.Seconds(), "by": by} if by != "process" { counted := m.bottomDevices() a := m.unitIO(counted) m.Wait(w) out["top"] = m.topUnits(a, m.unitIO(counted), w, by, limit) return out, nil } a, _ := m.processIO() m.Wait(w) b, unreadable := m.processIO() out["top"] = m.topProcesses(a, b, w, limit) if unreadable > 0 { out["processes_unreadable"] = unreadable out["note"] = unreadableNote(unreadable) } return out, nil }, }, { Name: "cpu_top", Description: "What used the most CPU over a few seconds, from each unit's cgroup cpu.stat, which needs no root: the machine's " + "CPU in use (as % of all CPUs and as CPUs busy, from /proc/stat), then by=unit (the default) each systemd unit and " + "container ranked by CPU, busiest first, with the busy time no unit accounts for; by=container only the containers, " + "by name. Replaces top, systemd-cgtop. (r)", Input: map[string]any{ "seconds": map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 3, at most 10)"}, "limit": map[string]any{"type": "integer", "description": "how many units or containers to name (default 15, at most 50)"}, "by": map[string]any{"type": "string", "enum": []string{"unit", "container"}, "description": "what to rank (default unit)"}, }, Run: func(args map[string]any) (any, error) { n, err := count(args, "seconds", 3, 1, 10) if err != nil { return nil, err } w := time.Duration(n) * time.Second limit, err := count(args, "limit", 15, 1, 50) if err != nil { return nil, err } by := text(args, "by") switch by { case "": by = "unit" case "unit", "container": default: return nil, fmt.Errorf("by is unit or container, not %q", by) } ac, err := parseCPU(m.read("/proc/stat")) if err != nil { return nil, err } a := m.unitCPU() m.Wait(w) bc, err := parseCPU(m.read("/proc/stat")) if err != nil { return nil, err } b := m.unitCPU() machine := machineCPUOver(ac, bc) top, unaccounted := m.topCPU(a, b, w, by, limit, machine.raw) return map[string]any{ "window_s": w.Seconds(), "by": by, "cpu": machine, "top": top, "unaccounted_pct": unaccounted, }, nil }, }, { Name: "disk_filesystems", Description: "The space of every filesystem that stores data (not proc, tmpfs or overlay layers): size, used and free in GiB, " + "used % as df counts it, and inodes used %. A filesystem mounted at several places is said once. Replaces df. (r)", Run: func(map[string]any) (any, error) { fss := m.Filesystems() said := []string{} for _, fs := range fss { switch { case fs.Unanswered != "": said = append(said, fmt.Sprintf("%s: not read (%s)", fs.Mount, fs.Unanswered)) default: line := fmt.Sprintf("%s (%s, %s): %.1f %% used, %.1f GiB free of %.1f GiB", fs.Mount, fs.Type, fs.Source, fs.UsedPct, fs.AvailGiB, fs.SizeGiB) if fs.UsedPct >= fullLine || fs.InodesUsed >= fullLine { line += " — NEARLY FULL" } said = append(said, line) } } return map[string]any{"said": said, "filesystems": fss}, nil }, }, { Name: "disk_devices", Description: "Each disk as it is, without sampling: model, size (GiB), whether it spins, its I/O scheduler and queue limit, what " + "it is built on (for LVM, encryption, RAID), and since the machine started: GiB read and written, the share of the time it " + "was busy and the average wait per read and per write (ms). Replaces lsblk and cat /proc/diskstats. (r)", Input: map[string]any{ "all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"}, }, Run: func(args map[string]any) (any, error) { ds, up := m.DevicesNow(flag(args, "all")) return map[string]any{"up_hours": up, "devices": ds}, nil }, }, } } func seconds(args map[string]any) (time.Duration, error) { n, err := count(args, "seconds", 2, 1, 10) return time.Duration(n) * time.Second, err } func text(args map[string]any, key string) string { s, _ := args[key].(string) return strings.TrimSpace(s) } func flag(args map[string]any, key string) bool { switch v := args[key].(type) { case bool: return v case string: return v == "true" } return false } // count is a whole-number argument, defaulted, refused below least, held at most. func count(args map[string]any, key string, fallback, least, most int) (int, error) { v, given := args[key] if !given || v == nil { return fallback, nil } var n int switch x := v.(type) { case float64: if x != math.Trunc(x) { return 0, fmt.Errorf("%s must be a whole number, not %v", key, x) } n = int(x) case string: i, err := strconv.Atoi(strings.TrimSpace(x)) if err != nil { return 0, fmt.Errorf("%s must be a whole number, not %q", key, x) } n = i default: return 0, fmt.Errorf("%s must be a whole number", key) } if n < least { return 0, fmt.Errorf("%s must be at least %d", key, least) } return min(n, most), nil }