latest
This commit is contained in:
parent
2e8bbfee34
commit
ce2e3dcf46
@ -1,37 +1,35 @@
|
|||||||
package sysinfo
|
package sysinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App holds CLI state.
|
|
||||||
type App struct {
|
type App struct {
|
||||||
force bool
|
force bool
|
||||||
jsonOut bool
|
jsonOut bool
|
||||||
verbose bool
|
verbose bool
|
||||||
aptUpdated bool
|
|
||||||
cmd *cobra.Command
|
cmd *cobra.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp configures root command.
|
|
||||||
func NewApp() (*App, error) {
|
func NewApp() (*App, error) {
|
||||||
a := &App{}
|
a := &App{}
|
||||||
root := &cobra.Command{
|
root := &cobra.Command{
|
||||||
Use: "sysinfo",
|
Use: "sysinfo",
|
||||||
Short: "capture block-device / system snapshot",
|
Short: "capture system snapshot",
|
||||||
Version: Version, // <-- --version flag
|
Version: Version,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: a.run,
|
RunE: a.run,
|
||||||
}
|
}
|
||||||
|
|
||||||
root.Flags().BoolVarP(&a.force, "force", "f", false,
|
root.Flags().BoolVarP(&a.force, "force", "f", false,
|
||||||
"overwrite /etc/sysinfo if it exists")
|
"overwrite /etc/sysinfo")
|
||||||
root.Flags().BoolVar(&a.jsonOut, "json", false,
|
root.Flags().BoolVar(&a.jsonOut, "json", false,
|
||||||
"emit JSON snapshot to stdout")
|
"emit JSON snapshot to stdout")
|
||||||
root.Flags().BoolVarP(&a.verbose, "verbose", "v", false,
|
root.Flags().BoolVarP(&a.verbose, "verbose", "v", false,
|
||||||
@ -40,9 +38,9 @@ func NewApp() (*App, error) {
|
|||||||
|
|
||||||
root.AddCommand(&cobra.Command{
|
root.AddCommand(&cobra.Command{
|
||||||
Use: "schema",
|
Use: "schema",
|
||||||
Short: "print the JSON schema",
|
Short: "print JSON schema",
|
||||||
Run: func(*cobra.Command, []string) {
|
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 {
|
||||||
func (a *App) run(_ *cobra.Command, _ []string) error {
|
if err := ensureUbuntu(); err != nil {
|
||||||
if err := a.ensureUbuntu(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := a.ensureDeps(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pm := newPackageManager(a.logf)
|
||||||
|
|
||||||
ctx := &Context{
|
ctx := &Context{
|
||||||
Now: time.Now().UTC(),
|
Now: time.Now().UTC(),
|
||||||
Logf: a.logf,
|
Logf: a.logf,
|
||||||
Run: a.runCmd,
|
Run: runCmd,
|
||||||
SafeRun: a.safeRun,
|
SafeRun: safeRun,
|
||||||
SafeRead: a.safeRead,
|
SafeRead: safeRead,
|
||||||
}
|
}
|
||||||
|
|
||||||
snap := &Snapshot{
|
snap := &Snapshot{
|
||||||
@ -86,17 +82,38 @@ func (a *App) run(_ *cobra.Command, _ []string) error {
|
|||||||
PackagesCollector{}, ZFSCollector{},
|
PackagesCollector{}, ZFSCollector{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range collectors {
|
for _, col := range collectors {
|
||||||
raw, err := c.Collect(ctx)
|
if err := col.EnsurePrerequisites(pm, ctx); err != nil {
|
||||||
if err != nil {
|
ctx.Logf("%s: prereq error: %v", col.SectionKey(), err)
|
||||||
ctx.Logf("%s: %v", c.Key(), err)
|
|
||||||
continue
|
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 {
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is shared with every Collector.
|
// Context carries helpers every Collector may need.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Now time.Time
|
Now time.Time
|
||||||
Logf func(string, ...any)
|
Logf func(string, ...any)
|
||||||
@ -14,8 +14,18 @@ type Context struct {
|
|||||||
SafeRead func(string) string
|
SafeRead func(string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collector produces one JSON fragment.
|
// Collector gathers one snapshot section.
|
||||||
type Collector interface {
|
type Collector interface {
|
||||||
Key() string
|
// SectionKey becomes the JSON field name (e.g. "system", "zfs").
|
||||||
Collect(*Context) (json.RawMessage, error)
|
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"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BlockCollector struct{}
|
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="([^"]+)"`)
|
rePU := regexp.MustCompile(`PARTUUID="([^"]+)"`)
|
||||||
all := map[string]any{}
|
all := map[string]any{}
|
||||||
|
|
||||||
@ -31,9 +43,9 @@ func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
|||||||
"smartctl": c.SafeRun("smartctl", "-a", "/dev/"+dev),
|
"smartctl": c.SafeRun("smartctl", "-a", "/dev/"+dev),
|
||||||
"blkid": c.SafeRun("blkid", "-p", "/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)
|
bd["sfdisk"] = json.RawMessage(sfd)
|
||||||
lsb, _ := c.runCmd("lsblk", "-J", "/dev/"+dev)
|
lsb, _ := c.Run("lsblk", "-J", "/dev/"+dev)
|
||||||
bd["lsblk"] = json.RawMessage(lsb)
|
bd["lsblk"] = json.RawMessage(lsb)
|
||||||
|
|
||||||
parts := map[string]any{}
|
parts := map[string]any{}
|
||||||
@ -45,17 +57,15 @@ func (BlockCollector) Collect(c *Context) (json.RawMessage, error) {
|
|||||||
_ = json.Unmarshal(lsb, &ls)
|
_ = json.Unmarshal(lsb, &ls)
|
||||||
for _, d := range ls.Blockdevices {
|
for _, d := range ls.Blockdevices {
|
||||||
for _, ch := range d.Children {
|
for _, ch := range d.Children {
|
||||||
partdev := "/dev/" + ch.Name
|
part := "/dev/" + ch.Name
|
||||||
blk := c.SafeRun("blkid", "-p", partdev)
|
blk := c.SafeRun("blkid", "-p", part)
|
||||||
m := rePU.FindStringSubmatch(blk)
|
m := rePU.FindStringSubmatch(blk)
|
||||||
if len(m) != 2 {
|
if len(m) != 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
uuid := strings.ToLower(m[1])
|
uuid := strings.ToLower(m[1])
|
||||||
pinfo := map[string]any{
|
pinfo := map[string]any{"blkid": blk}
|
||||||
"blkid": blk,
|
plsb, _ := c.Run("lsblk", "-J", part)
|
||||||
}
|
|
||||||
plsb, _ := c.runCmd("lsblk", "-J", partdev)
|
|
||||||
pinfo["lsblk"] = json.RawMessage(plsb)
|
pinfo["lsblk"] = json.RawMessage(plsb)
|
||||||
parts[uuid] = pinfo
|
parts[uuid] = pinfo
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,34 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetworkCollector struct{}
|
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{}
|
sections := map[string]any{}
|
||||||
|
|
||||||
nets, _ := filepath.Glob("/sys/class/net/*")
|
nets, _ := filepath.Glob("/sys/class/net/*")
|
||||||
@ -25,16 +45,18 @@ func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
|||||||
if mac == "" {
|
if mac == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := map[string]any{
|
entry := map[string]any{
|
||||||
"iface": iface,
|
"iface": iface,
|
||||||
"timestamp": c.Now.Format(time.RFC3339),
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
"link": c.SafeRun("ip", "-details", "link", "show", iface),
|
"link": c.SafeRun("ip", "-details", "link", "show", iface),
|
||||||
"ethtool": c.SafeRun("ethtool", 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)
|
entry["ip_addr"] = json.RawMessage(ipJSON)
|
||||||
|
|
||||||
stats, _ := c.readNetStats(iface)
|
stats, _ := readNetStats(iface)
|
||||||
entry["statistics"] = json.RawMessage(stats)
|
entry["statistics"] = json.RawMessage(stats)
|
||||||
|
|
||||||
if def[iface] {
|
if def[iface] {
|
||||||
@ -47,3 +69,21 @@ func (NetworkCollector) Collect(c *Context) (json.RawMessage, error) {
|
|||||||
}
|
}
|
||||||
return json.Marshal(sections)
|
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{}
|
type PackagesCollector struct{}
|
||||||
|
|
||||||
func (PackagesCollector) Key() string { return "packages" }
|
func (PackagesCollector) SectionKey() string { return "packages" }
|
||||||
func (PackagesCollector) Collect(c *Context) (json.RawMessage, error) {
|
|
||||||
out := c.SafeRun("dpkg-query", "-W",
|
func (PackagesCollector) EnsurePrerequisites(_ PackageManager, _ *Context) error {
|
||||||
"-f=${Package} ${Version}\\n")
|
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})
|
return json.Marshal(map[string]string{"dpkg": out})
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,20 @@ import "encoding/json"
|
|||||||
|
|
||||||
type SensorsCollector struct{}
|
type SensorsCollector struct{}
|
||||||
|
|
||||||
func (SensorsCollector) Key() string { return "sensors" }
|
func (SensorsCollector) SectionKey() string { return "sensors" }
|
||||||
func (SensorsCollector) Collect(c *Context) (json.RawMessage, error) {
|
|
||||||
|
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{
|
return json.Marshal(map[string]string{
|
||||||
"output": c.SafeRun("sensors"),
|
"output": c.SafeRun("sensors"),
|
||||||
})
|
})
|
||||||
|
@ -3,20 +3,32 @@ package sysinfo
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemCollector struct{}
|
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{
|
data := map[string]string{
|
||||||
"timestamp": c.Now.Format(time.RFC3339),
|
"timestamp": c.Now.Format(time.RFC3339),
|
||||||
|
"distro": "ubuntu",
|
||||||
"uname": c.SafeRun("uname", "-a"),
|
"uname": c.SafeRun("uname", "-a"),
|
||||||
"lsb_release": c.SafeRun("lsb_release", "-a"),
|
"lsb_release": c.SafeRun("lsb_release", "-a"),
|
||||||
"cpuinfo": c.SafeRead("/proc/cpuinfo"),
|
"cpuinfo": c.SafeRead("/proc/cpuinfo"),
|
||||||
"meminfo": c.SafeRead("/proc/meminfo"),
|
"meminfo": c.SafeRead("/proc/meminfo"),
|
||||||
"distro": "ubuntu",
|
|
||||||
}
|
}
|
||||||
serial := strings.TrimSpace(
|
serial := strings.TrimSpace(
|
||||||
c.SafeRun("dmidecode", "-s", "system-serial-number"))
|
c.SafeRun("dmidecode", "-s", "system-serial-number"))
|
||||||
|
@ -3,16 +3,25 @@ package sysinfo
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ZFSCollector struct{}
|
type ZFSCollector struct{}
|
||||||
|
|
||||||
func (ZFSCollector) Key() string { return "zfs" }
|
func (ZFSCollector) SectionKey() string { return "zfs" }
|
||||||
|
|
||||||
func (ZFSCollector) Collect(c *Context) (json.RawMessage, error) {
|
func (ZFSCollector) EnsurePrerequisites(pm PackageManager, _ *Context) error {
|
||||||
if c.SafeRun("which", "zpool") == "" {
|
if pm.Distro() == "ubuntu" {
|
||||||
return json.Marshal(nil) // no zfs installed
|
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(
|
pools := strings.Fields(
|
||||||
c.SafeRun("zpool", "list", "-H", "-o", "name"))
|
c.SafeRun("zpool", "list", "-H", "-o", "name"))
|
||||||
section := map[string]any{}
|
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 (
|
const (
|
||||||
rootDir = "/etc/sysinfo"
|
rootDir = "/etc/sysinfo"
|
||||||
snapshotJSON = "snapshot.json"
|
snapshotJSON = "snapshot.json"
|
||||||
debianFrontEnv = "DEBIAN_FRONTEND=noninteractive"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* platform check */
|
/* platform check (Ubuntu) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
func (a *App) ensureUbuntu() error {
|
func ensureUbuntu() error {
|
||||||
out, err := a.runCmd("lsb_release", "-is")
|
out, err := runCmd("lsb_release", "-is")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unsupported OS: lsb_release not found")
|
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...)
|
cmd := exec.Command(name, args...)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
cmd.Stdout, cmd.Stderr = &buf, &buf
|
cmd.Stdout, cmd.Stderr = &buf, &buf
|
||||||
@ -46,38 +45,25 @@ func (a *App) runCmd(name string, args ...string) ([]byte, error) {
|
|||||||
return buf.Bytes(), err
|
return buf.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) safeRun(name string, args ...string) string {
|
func safeRun(name string, args ...string) string {
|
||||||
out, _ := a.runCmd(name, args...)
|
out, _ := runCmd(name, args...)
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) runJSON(dst *json.RawMessage,
|
func safeRead(path string) string {
|
||||||
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)
|
b, _ := ioutil.ReadFile(path)
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
func emitJSON(f *os.File, v any) error {
|
||||||
/* JSON emitter */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
func (a *App) emitJSON(f *os.File, s *Snapshot) error {
|
|
||||||
enc := json.NewEncoder(f)
|
enc := json.NewEncoder(f)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
return enc.Encode(s)
|
return enc.Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ---------- generic helpers reused in collectors ------------------ */
|
||||||
/* network helpers */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
func (a *App) readNetStats(iface string) (json.RawMessage, error) {
|
func readNetStats(iface string) (json.RawMessage, error) {
|
||||||
dir := filepath.Join("/sys/class/net", iface, "statistics")
|
dir := filepath.Join("/sys/class/net", iface, "statistics")
|
||||||
ent, err := ioutil.ReadDir(dir)
|
ent, err := ioutil.ReadDir(dir)
|
||||||
if err != nil {
|
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)
|
n, _ := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
|
||||||
m[f.Name()] = n
|
m[f.Name()] = n
|
||||||
}
|
}
|
||||||
j, _ := json.Marshal(m)
|
return 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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