initial
This commit is contained in:
commit
b63bab0582
16
Makefile
Normal file
16
Makefile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
GOLDFLAGS += -s -w \
|
||||||
|
-X 'git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=$(VERSION)'
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
GOFLAGS=-ldflags="$(GOLDFLAGS)" go build ./cmd/sysinfo
|
||||||
|
|
||||||
|
install:
|
||||||
|
GOFLAGS=-ldflags="$(GOLDFLAGS)" go install ./cmd/sysinfo
|
||||||
|
|
||||||
|
.PHONY: all build install
|
20
cmd/sysinfo/main.go
Normal file
20
cmd/sysinfo/main.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/sysinfo/internal/sysinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app, err := sysinfo.NewApp()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := app.Execute(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
23
go.mod
Normal file
23
go.mod
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module git.eeqj.de/sneak/sysinfo
|
||||||
|
|
||||||
|
go 1.23.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.12.0 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
42
go.sum
Normal file
42
go.sum
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
102
internal/sysinfo/app.go
Normal file
102
internal/sysinfo/app.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App holds CLI state.
|
||||||
|
type App struct {
|
||||||
|
force bool
|
||||||
|
jsonOut bool
|
||||||
|
verbose bool
|
||||||
|
aptUpdated bool
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp configures root command.
|
||||||
|
func NewApp() (*App, error) {
|
||||||
|
a := &App{}
|
||||||
|
root := &cobra.Command{
|
||||||
|
Use: "sysinfo",
|
||||||
|
Short: "capture block-device / system snapshot",
|
||||||
|
Version: Version, // <-- --version flag
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: a.run,
|
||||||
|
}
|
||||||
|
|
||||||
|
root.Flags().BoolVarP(&a.force, "force", "f", false,
|
||||||
|
"overwrite /etc/sysinfo if it exists")
|
||||||
|
root.Flags().BoolVar(&a.jsonOut, "json", false,
|
||||||
|
"emit JSON snapshot to stdout")
|
||||||
|
root.Flags().BoolVarP(&a.verbose, "verbose", "v", false,
|
||||||
|
"verbose progress")
|
||||||
|
_ = viper.BindPFlags(root.Flags())
|
||||||
|
|
||||||
|
root.AddCommand(&cobra.Command{
|
||||||
|
Use: "schema",
|
||||||
|
Short: "print the JSON schema",
|
||||||
|
Run: func(*cobra.Command, []string) {
|
||||||
|
fmt.Fprintln(os.Stdout, JSONSchema)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
a.cmd = root
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Execute() error { return a.cmd.Execute() }
|
||||||
|
|
||||||
|
func (a *App) logf(f string, v ...any) {
|
||||||
|
if a.verbose {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, f+"\n", v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes the snapshot workflow.
|
||||||
|
func (a *App) run(_ *cobra.Command, _ []string) error {
|
||||||
|
if err := a.ensureUbuntu(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.ensureDeps(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &Context{
|
||||||
|
Now: time.Now().UTC(),
|
||||||
|
Logf: a.logf,
|
||||||
|
Run: a.runCmd,
|
||||||
|
SafeRun: a.safeRun,
|
||||||
|
SafeRead: a.safeRead,
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := &Snapshot{
|
||||||
|
Timestamp: ctx.Now.Format(time.RFC3339),
|
||||||
|
Sections: map[string]json.RawMessage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
collectors := []Collector{
|
||||||
|
SystemCollector{}, BlockCollector{},
|
||||||
|
NetworkCollector{}, SensorsCollector{},
|
||||||
|
PackagesCollector{}, ZFSCollector{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range collectors {
|
||||||
|
raw, err := c.Collect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logf("%s: %v", c.Key(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snap.Sections[c.Key()] = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.jsonOut {
|
||||||
|
return a.emitJSON(os.Stdout, snap)
|
||||||
|
}
|
||||||
|
return a.writeHierarchy(snap)
|
||||||
|
}
|
85
internal/sysinfo/collect_block.go
Normal file
85
internal/sysinfo/collect_block.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlockDevice + Partition live with this collector.
|
||||||
|
|
||||||
|
type BlockDevice struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Smartctl string `json:"smartctl"`
|
||||||
|
LuksDump string `json:"luksDump,omitempty"`
|
||||||
|
Sfdisk json.RawMessage `json:"sfdisk"`
|
||||||
|
Blkid string `json:"blkid"`
|
||||||
|
Lsblk json.RawMessage `json:"lsblk"`
|
||||||
|
Partitions map[string]*Partition `json:"partitions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Partition struct {
|
||||||
|
Blkid string `json:"blkid"`
|
||||||
|
Lsblk json.RawMessage `json:"lsblk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectBlock(s *Snapshot, now string) error {
|
||||||
|
links, _ := filepath.Glob("/dev/disk/by-id/*")
|
||||||
|
rePU := regexp.MustCompile(`PARTUUID="([^"]+)"`)
|
||||||
|
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 := &BlockDevice{
|
||||||
|
Timestamp: now,
|
||||||
|
Partitions: map[string]*Partition{},
|
||||||
|
}
|
||||||
|
devPath := filepath.Join("/dev", dev)
|
||||||
|
|
||||||
|
bd.Smartctl = a.safeRun("smartctl", "-a", devPath)
|
||||||
|
if out := a.safeRun("cryptsetup", "luksDump", devPath); out != "" {
|
||||||
|
bd.LuksDump = out
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(
|
||||||
|
a.runJSON(&bd.Sfdisk, "sfdisk", "-J", devPath), &bd.Sfdisk)
|
||||||
|
bd.Blkid = a.safeRun("blkid", "-p", devPath)
|
||||||
|
_ = json.Unmarshal(
|
||||||
|
a.runJSON(&bd.Lsblk, "lsblk", "-J", devPath), &bd.Lsblk)
|
||||||
|
|
||||||
|
// enumerate partitions
|
||||||
|
var ls struct {
|
||||||
|
Blockdevices []struct {
|
||||||
|
Children []struct{ Name string `json:"name"` } `json:"children"`
|
||||||
|
} `json:"blockdevices"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(bd.Lsblk, &ls)
|
||||||
|
for _, d := range ls.Blockdevices {
|
||||||
|
for _, c := range d.Children {
|
||||||
|
part := filepath.Join("/dev", c.Name)
|
||||||
|
blk := a.safeRun("blkid", "-p", part)
|
||||||
|
m := rePU.FindStringSubmatch(blk)
|
||||||
|
if len(m) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uuid := strings.ToLower(m[1])
|
||||||
|
p := &Partition{Blkid: blk}
|
||||||
|
_ = json.Unmarshal(
|
||||||
|
a.runJSON(&p.Lsblk, "lsblk", "-J", part), &p.Lsblk)
|
||||||
|
bd.Partitions[uuid] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Blockdevs == nil {
|
||||||
|
s.Blockdevs = map[string]*BlockDevice{}
|
||||||
|
}
|
||||||
|
s.Blockdevs[base] = bd
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
57
internal/sysinfo/collect_network.go
Normal file
57
internal/sysinfo/collect_network.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetIface lives here.
|
||||||
|
|
||||||
|
type NetIface struct {
|
||||||
|
Iface string `json:"iface"`
|
||||||
|
IPAddr json.RawMessage `json:"ip_addr"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Ethtool string `json:"ethtool,omitempty"`
|
||||||
|
Stats json.RawMessage `json:"statistics"`
|
||||||
|
IPInfo json.RawMessage `json:"ipinfo,omitempty"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectNetwork(s *Snapshot, now string) error {
|
||||||
|
defMap, _ := a.defaultIfaceSet()
|
||||||
|
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(
|
||||||
|
a.safeRead(filepath.Join(n, "address")), ":", ""))
|
||||||
|
if mac == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nif := &NetIface{
|
||||||
|
Iface: iface,
|
||||||
|
Timestamp: now,
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(
|
||||||
|
a.runJSON(&nif.IPAddr, "ip", "-j", "address", "show", iface),
|
||||||
|
&nif.IPAddr)
|
||||||
|
nif.Link = a.safeRun("ip", "-details", "link", "show", iface)
|
||||||
|
nif.Ethtool = a.safeRun("ethtool", iface)
|
||||||
|
nif.Stats, _ = a.readNetStats(iface)
|
||||||
|
if defMap[iface] {
|
||||||
|
if o := a.safeRun("curl", "--interface", iface, "-s",
|
||||||
|
"https://ipinfo.io"); json.Valid([]byte(o)) {
|
||||||
|
nif.IPInfo = json.RawMessage(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Network == nil {
|
||||||
|
s.Network = map[string]*NetIface{}
|
||||||
|
}
|
||||||
|
s.Network[mac] = nif
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
14
internal/sysinfo/collect_packages.go
Normal file
14
internal/sysinfo/collect_packages.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
// PackagesData lives with this collector.
|
||||||
|
|
||||||
|
type PackagesData struct {
|
||||||
|
Dpkg string `json:"dpkg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectPackages(s *Snapshot, _ string) error {
|
||||||
|
out := a.safeRun("dpkg-query", "-W",
|
||||||
|
"-f=${Package} ${Version}\\n")
|
||||||
|
s.Packages = &PackagesData{Dpkg: out}
|
||||||
|
return nil
|
||||||
|
}
|
13
internal/sysinfo/collect_sensors.go
Normal file
13
internal/sysinfo/collect_sensors.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
// SensorsData lives here.
|
||||||
|
|
||||||
|
type SensorsData struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectSensors(s *Snapshot, _ string) error {
|
||||||
|
out := a.safeRun("sensors")
|
||||||
|
s.Sensors = &SensorsData{Output: out}
|
||||||
|
return nil
|
||||||
|
}
|
38
internal/sysinfo/collect_system.go
Normal file
38
internal/sysinfo/collect_system.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// SystemData lives with this collector.
|
||||||
|
|
||||||
|
type SystemData struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Distro string `json:"distro"`
|
||||||
|
LSBRel string `json:"lsb_release"`
|
||||||
|
Uname string `json:"uname"`
|
||||||
|
CPUInfo string `json:"cpuinfo"`
|
||||||
|
MemInfo string `json:"meminfo"`
|
||||||
|
Dmidecode string `json:"dmidecode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectSystem(s *Snapshot, now string) error {
|
||||||
|
sys := &SystemData{
|
||||||
|
Timestamp: now,
|
||||||
|
LSBRel: a.safeRun("lsb_release", "-a"),
|
||||||
|
Uname: a.safeRun("uname", "-a"),
|
||||||
|
CPUInfo: a.safeRead("/proc/cpuinfo"),
|
||||||
|
MemInfo: a.safeRead("/proc/meminfo"),
|
||||||
|
Dmidecode: a.safeRun("dmidecode"),
|
||||||
|
Distro: "ubuntu",
|
||||||
|
}
|
||||||
|
|
||||||
|
if id := strings.TrimSpace(
|
||||||
|
a.safeRun("dmidecode", "-s", "system-serial-number")); id != "" {
|
||||||
|
sys.ID = id
|
||||||
|
} else {
|
||||||
|
sys.ID = a.primaryMAC()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.System = sys
|
||||||
|
return nil
|
||||||
|
}
|
32
internal/sysinfo/collect_zfs.go
Normal file
32
internal/sysinfo/collect_zfs.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ZPool represents details for one ZFS pool.
|
||||||
|
type ZPool struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Get string `json:"get"`
|
||||||
|
ZfsList string `json:"zfs_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) collectZFS(s *Snapshot, now string) error {
|
||||||
|
if a.safeRun("which", "zpool") == "" {
|
||||||
|
return nil // ZFS not installed
|
||||||
|
}
|
||||||
|
pools := strings.Fields(a.safeRun("zpool", "list", "-H", "-o", "name"))
|
||||||
|
if len(pools) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.ZFS == nil {
|
||||||
|
s.ZFS = map[string]*ZPool{}
|
||||||
|
}
|
||||||
|
for _, p := range pools {
|
||||||
|
z := &ZPool{Timestamp: now}
|
||||||
|
z.Status = a.safeRun("zpool", "status", "-v", p)
|
||||||
|
z.Get = a.safeRun("zpool", "get", "-H", "all", p)
|
||||||
|
z.ZfsList = a.safeRun("zfs", "list", "-Hp", "-r", p)
|
||||||
|
s.ZFS[p] = z
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
29
internal/sysinfo/deps.go
Normal file
29
internal/sysinfo/deps.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
func (a *App) ensureDeps() error {
|
||||||
|
req := map[string]string{
|
||||||
|
"smartctl": "smartmontools",
|
||||||
|
"cryptsetup":"cryptsetup",
|
||||||
|
"sfdisk": "util-linux",
|
||||||
|
"lsblk": "util-linux",
|
||||||
|
"blkid": "util-linux",
|
||||||
|
"ip": "iproute2",
|
||||||
|
"ethtool": "ethtool",
|
||||||
|
"dmidecode": "dmidecode",
|
||||||
|
"sensors": "lm-sensors",
|
||||||
|
"curl": "curl",
|
||||||
|
"jc": "jc",
|
||||||
|
"dpkg": "dpkg",
|
||||||
|
}
|
||||||
|
for bin, pkg := range req {
|
||||||
|
if _, err := exec.LookPath(bin); err != nil {
|
||||||
|
a.logf("apt install %s", pkg)
|
||||||
|
if err := a.aptInstall(pkg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
170
internal/sysinfo/helpers.go
Normal file
170
internal/sysinfo/helpers.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rootDir = "/etc/sysinfo"
|
||||||
|
snapshotJSON = "snapshot.json"
|
||||||
|
debianFrontEnv = "DEBIAN_FRONTEND=noninteractive"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* platform check */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) ensureUbuntu() error {
|
||||||
|
out, err := a.runCmd("lsb_release", "-is")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unsupported OS: lsb_release not found")
|
||||||
|
}
|
||||||
|
if strings.ToLower(strings.TrimSpace(string(out))) != "ubuntu" {
|
||||||
|
return fmt.Errorf("unsupported OS: %s",
|
||||||
|
strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* command helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) runCmd(name string, args ...string) ([]byte, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
cmd.Stdout, cmd.Stderr = &buf, &buf
|
||||||
|
err := cmd.Run()
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) safeRun(name string, args ...string) string {
|
||||||
|
out, _ := a.runCmd(name, args...)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) runJSON(dst *json.RawMessage,
|
||||||
|
name string, args ...string) []byte {
|
||||||
|
out, _ := a.runCmd(name, args...)
|
||||||
|
*dst = json.RawMessage(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) safeRead(path string) string {
|
||||||
|
b, _ := ioutil.ReadFile(path)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* JSON emitter */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) emitJSON(f *os.File, s *Snapshot) error {
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* network helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) readNetStats(iface string) (json.RawMessage, error) {
|
||||||
|
dir := filepath.Join("/sys/class/net", iface, "statistics")
|
||||||
|
ent, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := map[string]int64{}
|
||||||
|
for _, f := range ent {
|
||||||
|
b, _ := ioutil.ReadFile(filepath.Join(dir, f.Name()))
|
||||||
|
n, _ := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
|
||||||
|
m[f.Name()] = n
|
||||||
|
}
|
||||||
|
j, _ := json.Marshal(m)
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) defaultIfaceSet() (map[string]bool, error) {
|
||||||
|
out, err := a.runCmd("ip", "-j", "route", "show", "default")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var routes []struct{ Dev string `json:"dev"` }
|
||||||
|
_ = json.Unmarshal(out, &routes)
|
||||||
|
m := map[string]bool{}
|
||||||
|
for _, r := range routes {
|
||||||
|
m[r.Dev] = true
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) primaryMAC() string {
|
||||||
|
ifs, _ := ioutil.ReadDir("/sys/class/net")
|
||||||
|
for _, f := range ifs {
|
||||||
|
if f.Name() == "lo" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mac := a.safeRead(filepath.Join(
|
||||||
|
"/sys/class/net", f.Name(), "address"))
|
||||||
|
mac = strings.ToLower(strings.ReplaceAll(
|
||||||
|
strings.TrimSpace(mac), ":", ""))
|
||||||
|
if mac != "" {
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* apt helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) aptInstall(pkg string) error {
|
||||||
|
if !a.aptUpdated {
|
||||||
|
if err := a.runApt("apt-get", "update"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.aptUpdated = true
|
||||||
|
}
|
||||||
|
return a.runApt("apt-get", "-y", "install", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) runApt(name string, args ...string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
cmd.Env = append(os.Environ(), debianFrontEnv)
|
||||||
|
if a.verbose {
|
||||||
|
cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr
|
||||||
|
}
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* file helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) mustWriteText(dir, name, content string) {
|
||||||
|
if err := ioutil.WriteFile(
|
||||||
|
filepath.Join(dir, name),
|
||||||
|
[]byte(content), 0644); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) mustWriteJSON(dir, name string, raw json.RawMessage) {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(
|
||||||
|
filepath.Join(dir, name), raw, 0644); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
135
internal/sysinfo/output.go
Normal file
135
internal/sysinfo/output.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeHierarchy mirrors the Snapshot into /etc/sysinfo.
|
||||||
|
func (a *App) writeHierarchy(s *Snapshot) error {
|
||||||
|
if _, err := os.Stat(rootDir); err == nil && !a.force {
|
||||||
|
return fmt.Errorf("%s exists; use --force", rootDir)
|
||||||
|
}
|
||||||
|
_ = os.RemoveAll(rootDir)
|
||||||
|
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(rootDir, "snapshot_time.txt", s.SnapshotTime)
|
||||||
|
|
||||||
|
if err := a.writeSystem(
|
||||||
|
filepath.Join(rootDir, "system"), s.System); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Sensors != nil {
|
||||||
|
sDir := filepath.Join(rootDir, "sensors")
|
||||||
|
if err := os.MkdirAll(sDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(sDir, "sensors.txt", s.Sensors.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, bd := range s.Blockdevs {
|
||||||
|
if err := a.writeBlockDevice(
|
||||||
|
filepath.Join(rootDir, "blockdevs", id), bd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for mac, n := range s.Network {
|
||||||
|
if err := a.writeNetIface(
|
||||||
|
filepath.Join(rootDir, "network", mac), n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pool, z := range s.ZFS {
|
||||||
|
zDir := filepath.Join(rootDir, "zfs", "pools", pool)
|
||||||
|
if err := os.MkdirAll(zDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(zDir, "timestamp.txt", z.Timestamp)
|
||||||
|
a.mustWriteText(zDir, "status.txt", z.Status)
|
||||||
|
a.mustWriteText(zDir, "get.txt", z.Get)
|
||||||
|
a.mustWriteText(zDir, "zfs_list.txt", z.ZfsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Packages != nil {
|
||||||
|
pDir := filepath.Join(rootDir, "packages")
|
||||||
|
if err := os.MkdirAll(pDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(pDir, "dpkg_list.txt", s.Packages.Dpkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(filepath.Join(rootDir, snapshotJSON))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return a.emitJSON(f, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* helpers for leaf sections */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
func (a *App) writeSystem(dir string, sys *SystemData) error {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(dir, "timestamp.txt", sys.Timestamp)
|
||||||
|
a.mustWriteText(dir, "id.txt", sys.ID)
|
||||||
|
a.mustWriteText(dir, "distro.txt", sys.Distro)
|
||||||
|
a.mustWriteText(dir, "lsb_release.txt", sys.LSBRel)
|
||||||
|
a.mustWriteText(dir, "uname.txt", sys.Uname)
|
||||||
|
a.mustWriteText(dir, "cpuinfo.txt", sys.CPUInfo)
|
||||||
|
a.mustWriteText(dir, "meminfo.txt", sys.MemInfo)
|
||||||
|
if sys.Dmidecode != "" {
|
||||||
|
a.mustWriteText(dir, "dmidecode.txt", sys.Dmidecode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) writeBlockDevice(dir string, bd *BlockDevice) error {
|
||||||
|
if err := os.MkdirAll(
|
||||||
|
filepath.Join(dir, "partitions"), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(dir, "timestamp.txt", bd.Timestamp)
|
||||||
|
a.mustWriteText(dir, "smartctl.txt", bd.Smartctl)
|
||||||
|
if bd.LuksDump != "" {
|
||||||
|
a.mustWriteText(dir, "luksDump.txt", bd.LuksDump)
|
||||||
|
}
|
||||||
|
a.mustWriteJSON(dir, "sfdisk.json", bd.Sfdisk)
|
||||||
|
a.mustWriteText(dir, "blkid.txt", bd.Blkid)
|
||||||
|
a.mustWriteJSON(dir, "lsblk.json", bd.Lsblk)
|
||||||
|
for uuid, p := range bd.Partitions {
|
||||||
|
pDir := filepath.Join(dir, "partitions", uuid)
|
||||||
|
if err := os.MkdirAll(pDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(pDir, "blkid.txt", p.Blkid)
|
||||||
|
a.mustWriteJSON(pDir, "lsblk.json", p.Lsblk)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) writeNetIface(dir string, n *NetIface) error {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mustWriteText(dir, "timestamp.txt", n.Timestamp)
|
||||||
|
a.mustWriteText(dir, "iface.txt", n.Iface)
|
||||||
|
a.mustWriteJSON(dir, "ip_addr.json", n.IPAddr)
|
||||||
|
a.mustWriteText(dir, "link.txt", n.Link)
|
||||||
|
if n.Ethtool != "" {
|
||||||
|
a.mustWriteText(dir, "ethtool.txt", n.Ethtool)
|
||||||
|
}
|
||||||
|
a.mustWriteJSON(dir, "statistics.json", n.Stats)
|
||||||
|
if len(n.IPInfo) > 0 {
|
||||||
|
a.mustWriteJSON(dir, "ipinfo.json", n.IPInfo)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
118
internal/sysinfo/schema.go
Normal file
118
internal/sysinfo/schema.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
// JSONSchema – draft-2020-12. Updated to include “packages” and a
|
||||||
|
// “distro” field inside the system block so multiple Linux distros are
|
||||||
|
// representable even though only Ubuntu is collected today.
|
||||||
|
const JSONSchema = `
|
||||||
|
{
|
||||||
|
"$schema":"https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title":"Sysinfo snapshot",
|
||||||
|
"type":"object",
|
||||||
|
"required":["snapshot_time","system"],
|
||||||
|
"properties":{
|
||||||
|
"snapshot_time":{"type":"string","format":"date-time"},
|
||||||
|
"system":{"$ref":"#/$defs/system"},
|
||||||
|
"sensors":{"$ref":"#/$defs/sensors"},
|
||||||
|
"blockdevs":{
|
||||||
|
"type":"object",
|
||||||
|
"patternProperties":{"^[A-Za-z0-9._-]+$":{"$ref":"#/$defs/block"}},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"network":{
|
||||||
|
"type":"object",
|
||||||
|
"patternProperties":{"^[0-9a-f]{12}$":{"$ref":"#/$defs/iface"}},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"zfs":{
|
||||||
|
"type":"object",
|
||||||
|
"patternProperties":{"^[A-Za-z0-9._-]+$":{"$ref":"#/$defs/zpool"}},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"packages":{"$ref":"#/$defs/packages"}
|
||||||
|
},
|
||||||
|
"$defs":{
|
||||||
|
"system":{
|
||||||
|
"type":"object",
|
||||||
|
"required":["timestamp","id","distro","uname"],
|
||||||
|
"properties":{
|
||||||
|
"timestamp":{"type":"string","format":"date-time"},
|
||||||
|
"id":{"type":"string"},
|
||||||
|
"distro":{"type":"string"},
|
||||||
|
"lsb_release":{"type":"string"},
|
||||||
|
"uname":{"type":"string"},
|
||||||
|
"cpuinfo":{"type":"string"},
|
||||||
|
"meminfo":{"type":"string"},
|
||||||
|
"dmidecode":{"type":"string"}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"sensors":{
|
||||||
|
"type":"object",
|
||||||
|
"properties":{"output":{"type":"string"}},
|
||||||
|
"required":["output"],
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"block":{
|
||||||
|
"type":"object",
|
||||||
|
"required":["timestamp","smartctl","sfdisk","blkid","lsblk",
|
||||||
|
"partitions"],
|
||||||
|
"properties":{
|
||||||
|
"timestamp":{"type":"string","format":"date-time"},
|
||||||
|
"smartctl":{"type":"string"},
|
||||||
|
"luksDump":{"type":"string"},
|
||||||
|
"sfdisk":{"type":"object"},
|
||||||
|
"blkid":{"type":"string"},
|
||||||
|
"lsblk":{"type":"object"},
|
||||||
|
"partitions":{
|
||||||
|
"type":"object",
|
||||||
|
"patternProperties":{
|
||||||
|
"^[A-Fa-f0-9-]+$":{"$ref":"#/$defs/partition"}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"partition":{
|
||||||
|
"type":"object",
|
||||||
|
"required":["blkid","lsblk"],
|
||||||
|
"properties":{
|
||||||
|
"blkid":{"type":"string"},
|
||||||
|
"lsblk":{"type":"object"}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"iface":{
|
||||||
|
"type":"object",
|
||||||
|
"required":["iface","ip_addr","link","statistics","timestamp"],
|
||||||
|
"properties":{
|
||||||
|
"iface":{"type":"string"},
|
||||||
|
"ip_addr":{"type":"object"},
|
||||||
|
"link":{"type":"string"},
|
||||||
|
"ethtool":{"type":"string"},
|
||||||
|
"statistics":{"type":"object"},
|
||||||
|
"ipinfo":{"type":"object"},
|
||||||
|
"timestamp":{"type":"string","format":"date-time"}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"zpool":{
|
||||||
|
"type":"object",
|
||||||
|
"required":["timestamp","status","get","zfs_list"],
|
||||||
|
"properties":{
|
||||||
|
"timestamp":{"type":"string","format":"date-time"},
|
||||||
|
"status":{"type":"string"},
|
||||||
|
"get":{"type":"string"},
|
||||||
|
"zfs_list":{"type":"string"}
|
||||||
|
},
|
||||||
|
"additionalProperties":false
|
||||||
|
},
|
||||||
|
"packages":{
|
||||||
|
"type":"object",
|
||||||
|
"properties":{"dpkg":{"type":"string"}},
|
||||||
|
"required":["dpkg"],
|
||||||
|
"additionalProperties":false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
12
internal/sysinfo/snapshot.go
Normal file
12
internal/sysinfo/snapshot.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
// Snapshot is the top-level JSON object.
|
||||||
|
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"`
|
||||||
|
}
|
5
internal/sysinfo/version.go
Normal file
5
internal/sysinfo/version.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package sysinfo
|
||||||
|
|
||||||
|
// Version is overridden at build time via:
|
||||||
|
// go build -ldflags "-X git.eeqj.de/sneak/sysinfo/internal/sysinfo.Version=1.2.3"
|
||||||
|
var Version = "dev"
|
Loading…
Reference in New Issue
Block a user