latest
This commit is contained in:
parent
2e8bbfee34
commit
ce2e3dcf46
@ -1,37 +1,35 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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
|
||||
Short: "capture system snapshot",
|
||||
Version: Version,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: a.run,
|
||||
}
|
||||
|
||||
root.Flags().BoolVarP(&a.force, "force", "f", false,
|
||||
"overwrite /etc/sysinfo if it exists")
|
||||
"overwrite /etc/sysinfo")
|
||||
root.Flags().BoolVar(&a.jsonOut, "json", false,
|
||||
"emit JSON snapshot to stdout")
|
||||
root.Flags().BoolVarP(&a.verbose, "verbose", "v", false,
|
||||
@ -40,9 +38,9 @@ func NewApp() (*App, error) {
|
||||
|
||||
root.AddCommand(&cobra.Command{
|
||||
Use: "schema",
|
||||
Short: "print the JSON schema",
|
||||
Short: "print JSON schema",
|
||||
Run: func(*cobra.Command, []string) {
|
||||
fmt.Fprintln(os.Stdout, JSONSchema)
|
||||
fmt.Println(JSONSchema)
|
||||
},
|
||||
})
|
||||
|
||||
@ -58,21 +56,19 @@ func (a *App) logf(f string, v ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (a *App) run(*cobra.Command, []string) error {
|
||||
if err := ensureUbuntu(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pm := newPackageManager(a.logf)
|
||||
|
||||
ctx := &Context{
|
||||
Now: time.Now().UTC(),
|
||||
Logf: a.logf,
|
||||
Run: a.runCmd,
|
||||
SafeRun: a.safeRun,
|
||||
SafeRead: a.safeRead,
|
||||
Run: runCmd,
|
||||
SafeRun: safeRun,
|
||||
SafeRead: safeRead,
|
||||
}
|
||||
|
||||
snap := &Snapshot{
|
||||
@ -86,17 +82,38 @@ func (a *App) run(_ *cobra.Command, _ []string) error {
|
||||
PackagesCollector{}, ZFSCollector{},
|
||||
}
|
||||
|
||||
for _, c := range collectors {
|
||||
raw, err := c.Collect(ctx)
|
||||
if err != nil {
|
||||
ctx.Logf("%s: %v", c.Key(), err)
|
||||
for _, col := range collectors {
|
||||
if err := col.EnsurePrerequisites(pm, ctx); err != nil {
|
||||
ctx.Logf("%s: prereq error: %v", col.SectionKey(), err)
|
||||
continue
|
||||
}
|
||||
snap.Sections[c.Key()] = raw
|
||||
if !col.IsSupported(pm, ctx) {
|
||||
ctx.Logf("%s: not supported on this host", col.SectionKey())
|
||||
continue
|
||||
}
|
||||
raw, err := col.CollectData(ctx)
|
||||
if err != nil {
|
||||
ctx.Logf("%s: %v", col.SectionKey(), err)
|
||||
continue
|
||||
}
|
||||
snap.Sections[col.SectionKey()] = raw
|
||||
}
|
||||
|
||||
if a.jsonOut {
|
||||
return a.emitJSON(os.Stdout, snap)
|
||||
return emitJSON(os.Stdout, snap)
|
||||
}
|
||||
return a.writeHierarchy(snap)
|
||||
|
||||
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
|
||||
}
|
||||
f, err := os.Create(filepath.Join(rootDir, snapshotJSON))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return emitJSON(f, snap)
|
||||
}
|
||||
|
@ -1,85 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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
|
||||
}
|
@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context is shared with every Collector.
|
||||
// Context carries helpers every Collector may need.
|
||||
type Context struct {
|
||||
Now time.Time
|
||||
Logf func(string, ...any)
|
||||
@ -14,8 +14,18 @@ type Context struct {
|
||||
SafeRead func(string) string
|
||||
}
|
||||
|
||||
// Collector produces one JSON fragment.
|
||||
// Collector gathers one snapshot section.
|
||||
type Collector interface {
|
||||
Key() string
|
||||
Collect(*Context) (json.RawMessage, error)
|
||||
// SectionKey becomes the JSON field name (e.g. "system", "zfs").
|
||||
SectionKey() string
|
||||
|
||||
// EnsurePrerequisites installs/verifies required tools via the
|
||||
// provided PackageManager. Must be idempotent.
|
||||
EnsurePrerequisites(pm PackageManager, ctx *Context) error
|
||||
|
||||
// IsSupported decides if CollectData should run on this machine.
|
||||
IsSupported(pm PackageManager, ctx *Context) bool
|
||||
|
||||
// CollectData returns the JSON blob for this section.
|
||||
CollectData(ctx *Context) (json.RawMessage, error)
|
||||
}
|
||||
|
@ -5,13 +5,25 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BlockCollector struct{}
|
||||
|
||||
func (BlockCollector) Key() string { return "blockdevs" }
|
||||
func (BlockCollector) SectionKey() string { return "blockdevs" }
|
||||
|
||||
func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
func (BlockCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||
if pm.Distro() == "ubuntu" {
|
||||
return pm.InstallPackages("smartmontools", "util-linux", "blkid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (BlockCollector) IsSupported(pm PackageManager, _ *Context) bool {
|
||||
return pm.ExecExists("lsblk")
|
||||
}
|
||||
|
||||
func (BlockCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
rePU := regexp.MustCompile(`PARTUUID="([^"]+)"`)
|
||||
all := map[string]any{}
|
||||
|
||||
@ -31,9 +43,9 @@ func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
"smartctl": c.SafeRun("smartctl", "-a", "/dev/"+dev),
|
||||
"blkid": c.SafeRun("blkid", "-p", "/dev/"+dev),
|
||||
}
|
||||
sfd, _ := c.runCmd("sfdisk", "-J", "/dev/"+dev)
|
||||
sfd, _ := c.Run("sfdisk", "-J", "/dev/"+dev)
|
||||
bd["sfdisk"] = json.RawMessage(sfd)
|
||||
lsb, _ := c.runCmd("lsblk", "-J", "/dev/"+dev)
|
||||
lsb, _ := c.Run("lsblk", "-J", "/dev/"+dev)
|
||||
bd["lsblk"] = json.RawMessage(lsb)
|
||||
|
||||
parts := map[string]any{}
|
||||
@ -45,17 +57,15 @@ func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
_ = json.Unmarshal(lsb, &ls)
|
||||
for _, d := range ls.Blockdevices {
|
||||
for _, ch := range d.Children {
|
||||
partdev := "/dev/" + ch.Name
|
||||
blk := c.SafeRun("blkid", "-p", partdev)
|
||||
part := "/dev/" + ch.Name
|
||||
blk := c.SafeRun("blkid", "-p", part)
|
||||
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 := map[string]any{"blkid": blk}
|
||||
plsb, _ := c.Run("lsblk", "-J", part)
|
||||
pinfo["lsblk"] = json.RawMessage(plsb)
|
||||
parts[uuid] = pinfo
|
||||
}
|
||||
|
@ -4,14 +4,34 @@ import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NetworkCollector struct{}
|
||||
|
||||
func (NetworkCollector) Key() string { return "network" }
|
||||
func (NetworkCollector) SectionKey() string { return "network" }
|
||||
|
||||
func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
def, _ := c.defaultIfaceSet()
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* prerequisite / support checks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func (NetworkCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||
if pm.Distro() == "ubuntu" {
|
||||
return pm.InstallPackages("iproute2", "curl", "ethtool")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NetworkCollector) IsSupported(pm PackageManager, _ *Context) bool {
|
||||
return pm.ExecExists("ip")
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* collection */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func (NetworkCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
def, _ := defaultIfaceSet(c)
|
||||
sections := map[string]any{}
|
||||
|
||||
nets, _ := filepath.Glob("/sys/class/net/*")
|
||||
@ -25,16 +45,18 @@ func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
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)
|
||||
|
||||
ipJSON, _ := c.Run("ip", "-j", "address", "show", iface)
|
||||
entry["ip_addr"] = json.RawMessage(ipJSON)
|
||||
|
||||
stats, _ := c.readNetStats(iface)
|
||||
stats, _ := readNetStats(iface)
|
||||
entry["statistics"] = json.RawMessage(stats)
|
||||
|
||||
if def[iface] {
|
||||
@ -47,3 +69,21 @@ func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
}
|
||||
return json.Marshal(sections)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func defaultIfaceSet(c *Context) (map[string]bool, error) {
|
||||
out, err := c.Run("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
|
||||
}
|
||||
|
@ -4,9 +4,17 @@ 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")
|
||||
func (PackagesCollector) SectionKey() string { return "packages" }
|
||||
|
||||
func (PackagesCollector) EnsurePrerequisites(_ PackageManager, _ *Context) error {
|
||||
return nil // core tool
|
||||
}
|
||||
|
||||
func (PackagesCollector) IsSupported(pm PackageManager, _ *Context) bool {
|
||||
return pm.ExecExists("dpkg-query")
|
||||
}
|
||||
|
||||
func (PackagesCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
out := c.SafeRun("dpkg-query", "-W", "-f=${Package} ${Version}\\n")
|
||||
return json.Marshal(map[string]string{"dpkg": out})
|
||||
}
|
||||
|
@ -4,8 +4,20 @@ import "encoding/json"
|
||||
|
||||
type SensorsCollector struct{}
|
||||
|
||||
func (SensorsCollector) Key() string { return "sensors" }
|
||||
func (SensorsCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
func (SensorsCollector) SectionKey() string { return "sensors" }
|
||||
|
||||
func (SensorsCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||
if pm.Distro() == "ubuntu" {
|
||||
return pm.InstallPackages("lm-sensors")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (SensorsCollector) IsSupported(pm PackageManager, _ *Context) bool {
|
||||
return pm.ExecExists("sensors")
|
||||
}
|
||||
|
||||
func (SensorsCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"output": c.SafeRun("sensors"),
|
||||
})
|
||||
|
@ -3,20 +3,32 @@ package sysinfo
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemCollector struct{}
|
||||
|
||||
func (SystemCollector) Key() string { return "system" }
|
||||
func (SystemCollector) SectionKey() string { return "system" }
|
||||
|
||||
func (SystemCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
func (SystemCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||
if pm.Distro() == "ubuntu" {
|
||||
return pm.InstallPackages("dmidecode")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (SystemCollector) IsSupported(_ PackageManager, _ *Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (SystemCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
data := map[string]string{
|
||||
"timestamp": c.Now.Format(time.RFC3339),
|
||||
"distro": "ubuntu",
|
||||
"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"))
|
||||
|
@ -3,16 +3,25 @@ package sysinfo
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ZFSCollector struct{}
|
||||
|
||||
func (ZFSCollector) Key() string { return "zfs" }
|
||||
func (ZFSCollector) SectionKey() string { return "zfs" }
|
||||
|
||||
func (ZFSCollector) Collect(c *Context) (json.RawMessage, error) {
|
||||
if c.SafeRun("which", "zpool") == "" {
|
||||
return json.Marshal(nil) // no zfs installed
|
||||
func (ZFSCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||
if pm.Distro() == "ubuntu" {
|
||||
return pm.InstallPackages("zfsutils-linux")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ZFSCollector) IsSupported(pm PackageManager, _ *Context) bool {
|
||||
return pm.ExecExists("zpool")
|
||||
}
|
||||
|
||||
func (ZFSCollector) CollectData(c *Context) (json.RawMessage, error) {
|
||||
pools := strings.Fields(
|
||||
c.SafeRun("zpool", "list", "-H", "-o", "name"))
|
||||
section := map[string]any{}
|
||||
|
@ -1,29 +0,0 @@
|
||||
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
|
||||
}
|
@ -15,15 +15,14 @@ import (
|
||||
const (
|
||||
rootDir = "/etc/sysinfo"
|
||||
snapshotJSON = "snapshot.json"
|
||||
debianFrontEnv = "DEBIAN_FRONTEND=noninteractive"
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* platform check */
|
||||
/* platform check (Ubuntu) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func (a *App) ensureUbuntu() error {
|
||||
out, err := a.runCmd("lsb_release", "-is")
|
||||
func ensureUbuntu() error {
|
||||
out, err := runCmd("lsb_release", "-is")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported OS: lsb_release not found")
|
||||
}
|
||||
@ -35,10 +34,10 @@ func (a *App) ensureUbuntu() error {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* command helpers */
|
||||
/* shell helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func (a *App) runCmd(name string, args ...string) ([]byte, error) {
|
||||
func runCmd(name string, args ...string) ([]byte, error) {
|
||||
cmd := exec.Command(name, args...)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &buf, &buf
|
||||
@ -46,38 +45,25 @@ func (a *App) runCmd(name string, args ...string) ([]byte, error) {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func (a *App) safeRun(name string, args ...string) string {
|
||||
out, _ := a.runCmd(name, args...)
|
||||
func safeRun(name string, args ...string) string {
|
||||
out, _ := 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 {
|
||||
func safeRead(path string) string {
|
||||
b, _ := ioutil.ReadFile(path)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* JSON emitter */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
func (a *App) emitJSON(f *os.File, s *Snapshot) error {
|
||||
func emitJSON(f *os.File, v any) error {
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(s)
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* network helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ---------- generic helpers reused in collectors ------------------ */
|
||||
|
||||
func (a *App) readNetStats(iface string) (json.RawMessage, error) {
|
||||
func readNetStats(iface string) (json.RawMessage, error) {
|
||||
dir := filepath.Join("/sys/class/net", iface, "statistics")
|
||||
ent, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
@ -89,82 +75,5 @@ func (a *App) readNetStats(iface string) (json.RawMessage, error) {
|
||||
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)
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
@ -1,135 +0,0 @@
|
||||
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
|
||||
}
|
41
internal/sysinfo/packagemanager.go
Normal file
41
internal/sysinfo/packagemanager.go
Normal file
@ -0,0 +1,41 @@
|
||||
package sysinfo
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// PackageManager is an abstraction over distro package tooling.
|
||||
type PackageManager interface {
|
||||
Distro() string
|
||||
InstallPackages(pkgs ...string) error
|
||||
ExecExists(bin string) bool
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* aptManager (Ubuntu / Debian) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
type aptManager struct {
|
||||
logf func(string, ...any)
|
||||
}
|
||||
|
||||
func newPackageManager(logf func(string, ...any)) PackageManager {
|
||||
return &aptManager{logf: logf}
|
||||
}
|
||||
|
||||
func (a *aptManager) Distro() string { return "ubuntu" }
|
||||
|
||||
func (a *aptManager) ExecExists(bin string) bool {
|
||||
_, err := exec.LookPath(bin)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (a *aptManager) InstallPackages(pkgs ...string) error {
|
||||
args := append([]string{"-y", "install"}, pkgs...)
|
||||
cmd := exec.Command("apt-get", args...)
|
||||
cmd.Env = append(cmd.Env, "DEBIAN_FRONTEND=noninteractive")
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
a.logf("apt install %v", pkgs)
|
||||
return cmd.Run()
|
||||
}
|
Loading…
Reference in New Issue
Block a user