latest
This commit is contained in:
parent
b63bab0582
commit
2e8bbfee34
23
Makefile
23
Makefile
@ -1,16 +1,21 @@
|
||||
# Infer VERSION from the most recent tag; if none, use short commit hash.
|
||||
VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || \
|
||||
git rev-parse --short HEAD)
|
||||
# -------- version detection -------------------------------------------------
|
||||
# Use latest annotated/lightweight tag; if none, fall back to short SHA.
|
||||
VERSION ?= $(shell git describe --tags --always --dirty=-dirty 2>/dev/null)
|
||||
|
||||
GOLDFLAGS += -s -w \
|
||||
-X 'git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=$(VERSION)'
|
||||
# -------- linker flags -------------------------------------------------------
|
||||
# No inner quotes around -X value — go tool handles the spaces correctly.
|
||||
LDFLAGS = -s -w -X "git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=$(VERSION)"
|
||||
|
||||
all: build
|
||||
# -------- generic go build/install ------------------------------------------
|
||||
PKG_CMD = ./cmd/sysinfo
|
||||
|
||||
build:
|
||||
GOFLAGS=-ldflags="$(GOLDFLAGS)" go build ./cmd/sysinfo
|
||||
go build -ldflags '$(LDFLAGS)' $(PKG_CMD)
|
||||
|
||||
install:
|
||||
GOFLAGS=-ldflags="$(GOLDFLAGS)" go install ./cmd/sysinfo
|
||||
go install -ldflags '$(LDFLAGS)' $(PKG_CMD)
|
||||
|
||||
.PHONY: all build install
|
||||
clean:
|
||||
rm -f sysinfo
|
||||
|
||||
.PHONY: build install clean
|
||||
|
21
internal/sysinfo/collector.go
Normal file
21
internal/sysinfo/collector.go
Normal file
@ -0,0 +1,21 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context is shared with every Collector.
|
||||
type Context struct {
|
||||
Now time.Time
|
||||
Logf func(string, ...any)
|
||||
Run func(string, ...string) ([]byte, error)
|
||||
SafeRun func(string, ...string) string
|
||||
SafeRead func(string) string
|
||||
}
|
||||
|
||||
// Collector produces one JSON fragment.
|
||||
type Collector interface {
|
||||
Key() string
|
||||
Collect(*Context) (json.RawMessage, error)
|
||||
}
|
67
internal/sysinfo/collector_block.go
Normal file
67
internal/sysinfo/collector_block.go
Normal file
@ -0,0 +1,67 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BlockCollector struct{}
|
||||
|
||||
func (BlockCollector) Key() string { return "blockdevs" }
|
||||
|
||||
func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
rePU := regexp.MustCompile(`PARTUUID="([^"]+)"`)
|
||||
all := map[string]any{}
|
||||
|
||||
links, _ := filepath.Glob("/dev/disk/by-id/*")
|
||||
seen := map[string]bool{}
|
||||
for _, link := range links {
|
||||
base := filepath.Base(link)
|
||||
target, _ := filepath.EvalSymlinks(link)
|
||||
dev := filepath.Base(target)
|
||||
if seen[dev] {
|
||||
continue
|
||||
}
|
||||
seen[dev] = true
|
||||
|
||||
bd := map[string]any{
|
||||
"timestamp": c.Now.Format(time.RFC3339),
|
||||
"smartctl": c.SafeRun("smartctl", "-a", "/dev/"+dev),
|
||||
"blkid": c.SafeRun("blkid", "-p", "/dev/"+dev),
|
||||
}
|
||||
sfd, _ := c.runCmd("sfdisk", "-J", "/dev/"+dev)
|
||||
bd["sfdisk"] = json.RawMessage(sfd)
|
||||
lsb, _ := c.runCmd("lsblk", "-J", "/dev/"+dev)
|
||||
bd["lsblk"] = json.RawMessage(lsb)
|
||||
|
||||
parts := map[string]any{}
|
||||
var ls struct {
|
||||
Blockdevices []struct {
|
||||
Children []struct{ Name string `json:"name"` } `json:"children"`
|
||||
} `json:"blockdevices"`
|
||||
}
|
||||
_ = json.Unmarshal(lsb, &ls)
|
||||
for _, d := range ls.Blockdevices {
|
||||
for _, ch := range d.Children {
|
||||
partdev := "/dev/" + ch.Name
|
||||
blk := c.SafeRun("blkid", "-p", partdev)
|
||||
m := rePU.FindStringSubmatch(blk)
|
||||
if len(m) != 2 {
|
||||
continue
|
||||
}
|
||||
uuid := strings.ToLower(m[1])
|
||||
pinfo := map[string]any{
|
||||
"blkid": blk,
|
||||
}
|
||||
plsb, _ := c.runCmd("lsblk", "-J", partdev)
|
||||
pinfo["lsblk"] = json.RawMessage(plsb)
|
||||
parts[uuid] = pinfo
|
||||
}
|
||||
}
|
||||
bd["partitions"] = parts
|
||||
all[base] = bd
|
||||
}
|
||||
return json.Marshal(all)
|
||||
}
|
49
internal/sysinfo/collector_network.go
Normal file
49
internal/sysinfo/collector_network.go
Normal file
@ -0,0 +1,49 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NetworkCollector struct{}
|
||||
|
||||
func (NetworkCollector) Key() string { return "network" }
|
||||
|
||||
func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
def, _ := c.defaultIfaceSet()
|
||||
sections := map[string]any{}
|
||||
|
||||
nets, _ := filepath.Glob("/sys/class/net/*")
|
||||
for _, n := range nets {
|
||||
if strings.HasSuffix(n, "/lo") {
|
||||
continue
|
||||
}
|
||||
iface := filepath.Base(n)
|
||||
mac := strings.ToLower(strings.ReplaceAll(
|
||||
c.SafeRead(filepath.Join(n, "address")), ":", ""))
|
||||
if mac == "" {
|
||||
continue
|
||||
}
|
||||
entry := map[string]any{
|
||||
"iface": iface,
|
||||
"timestamp": c.Now.Format(time.RFC3339),
|
||||
"link": c.SafeRun("ip", "-details", "link", "show", iface),
|
||||
"ethtool": c.SafeRun("ethtool", iface),
|
||||
}
|
||||
ipJSON, _ := c.runCmd("ip", "-j", "address", "show", iface)
|
||||
entry["ip_addr"] = json.RawMessage(ipJSON)
|
||||
|
||||
stats, _ := c.readNetStats(iface)
|
||||
entry["statistics"] = json.RawMessage(stats)
|
||||
|
||||
if def[iface] {
|
||||
if out := c.SafeRun("curl", "--interface", iface, "-s",
|
||||
"https://ipinfo.io"); json.Valid([]byte(out)) {
|
||||
entry["ipinfo"] = json.RawMessage(out)
|
||||
}
|
||||
}
|
||||
sections[mac] = entry
|
||||
}
|
||||
return json.Marshal(sections)
|
||||
}
|
12
internal/sysinfo/collector_packages.go
Normal file
12
internal/sysinfo/collector_packages.go
Normal file
@ -0,0 +1,12 @@
|
||||
package sysinfo
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type PackagesCollector struct{}
|
||||
|
||||
func (PackagesCollector) Key() string { return "packages" }
|
||||
func (PackagesCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
out := c.SafeRun("dpkg-query", "-W",
|
||||
"-f=${Package} ${Version}\\n")
|
||||
return json.Marshal(map[string]string{"dpkg": out})
|
||||
}
|
12
internal/sysinfo/collector_sensors.go
Normal file
12
internal/sysinfo/collector_sensors.go
Normal file
@ -0,0 +1,12 @@
|
||||
package sysinfo
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type SensorsCollector struct{}
|
||||
|
||||
func (SensorsCollector) Key() string { return "sensors" }
|
||||
func (SensorsCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"output": c.SafeRun("sensors"),
|
||||
})
|
||||
}
|
27
internal/sysinfo/collector_system.go
Normal file
27
internal/sysinfo/collector_system.go
Normal file
@ -0,0 +1,27 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SystemCollector struct{}
|
||||
|
||||
func (SystemCollector) Key() string { return "system" }
|
||||
|
||||
func (SystemCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
data := map[string]string{
|
||||
"timestamp": c.Now.Format(time.RFC3339),
|
||||
"uname": c.SafeRun("uname", "-a"),
|
||||
"lsb_release": c.SafeRun("lsb_release", "-a"),
|
||||
"cpuinfo": c.SafeRead("/proc/cpuinfo"),
|
||||
"meminfo": c.SafeRead("/proc/meminfo"),
|
||||
"distro": "ubuntu",
|
||||
}
|
||||
serial := strings.TrimSpace(
|
||||
c.SafeRun("dmidecode", "-s", "system-serial-number"))
|
||||
if serial != "" {
|
||||
data["id"] = serial
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
28
internal/sysinfo/collector_zfs.go
Normal file
28
internal/sysinfo/collector_zfs.go
Normal file
@ -0,0 +1,28 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ZFSCollector struct{}
|
||||
|
||||
func (ZFSCollector) Key() string { return "zfs" }
|
||||
|
||||
func (ZFSCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
if c.SafeRun("which", "zpool") == "" {
|
||||
return json.Marshal(nil) // no zfs installed
|
||||
}
|
||||
pools := strings.Fields(
|
||||
c.SafeRun("zpool", "list", "-H", "-o", "name"))
|
||||
section := map[string]any{}
|
||||
for _, p := range pools {
|
||||
section[p] = map[string]string{
|
||||
"timestamp": c.Now.Format(time.RFC3339),
|
||||
"status": c.SafeRun("zpool", "status", "-v", p),
|
||||
"get": c.SafeRun("zpool", "get", "-H", "all", p),
|
||||
"zfs_list": c.SafeRun("zfs", "list", "-Hp", "-r", p),
|
||||
}
|
||||
}
|
||||
return json.Marshal(section)
|
||||
}
|
@ -1,12 +1,21 @@
|
||||
package sysinfo
|
||||
|
||||
// Snapshot is the top-level JSON object.
|
||||
import "encoding/json"
|
||||
|
||||
// Snapshot stores timestamp + raw section blobs.
|
||||
type Snapshot struct {
|
||||
SnapshotTime string `json:"snapshot_time"`
|
||||
System *SystemData `json:"system"`
|
||||
Sensors *SensorsData `json:"sensors,omitempty"`
|
||||
Blockdevs map[string]*BlockDevice `json:"blockdevs,omitempty"`
|
||||
Network map[string]*NetIface `json:"network,omitempty"`
|
||||
ZFS map[string]*ZPool `json:"zfs,omitempty"`
|
||||
Packages *PackagesData `json:"packages,omitempty"`
|
||||
Timestamp string `json:"snapshot_time"`
|
||||
Sections map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// MarshalJSON injects raw sections into root object.
|
||||
func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||
obj := map[string]json.RawMessage{
|
||||
"snapshot_time": json.RawMessage(
|
||||
[]byte(`"` + s.Timestamp + `"`)),
|
||||
}
|
||||
for k, v := range s.Sections {
|
||||
obj[k] = v
|
||||
}
|
||||
return json.Marshal(obj)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user