Refactor bee CLI and LiveCD integration
This commit is contained in:
94
audit/internal/platform/export.go
Normal file
94
audit/internal/platform/export.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *System) ListRemovableTargets() ([]RemovableTarget, error) {
|
||||
raw, err := exec.Command("lsblk", "-P", "-o", "NAME,TYPE,PKNAME,RM,FSTYPE,MOUNTPOINT,SIZE,LABEL,MODEL").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []RemovableTarget
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
fields := parseLSBLKPairs(line)
|
||||
deviceType := fields["TYPE"]
|
||||
if deviceType == "rom" || deviceType == "loop" {
|
||||
continue
|
||||
}
|
||||
|
||||
removable := fields["RM"] == "1"
|
||||
if !removable {
|
||||
if parent := fields["PKNAME"]; parent != "" {
|
||||
if data, err := os.ReadFile(filepath.Join("/sys/class/block", parent, "removable")); err == nil {
|
||||
removable = strings.TrimSpace(string(data)) == "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
if !removable || fields["FSTYPE"] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, RemovableTarget{
|
||||
Device: "/dev/" + fields["NAME"],
|
||||
FSType: fields["FSTYPE"],
|
||||
Size: fields["SIZE"],
|
||||
Label: fields["LABEL"],
|
||||
Model: fields["MODEL"],
|
||||
Mountpoint: fields["MOUNTPOINT"],
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Device < out[j].Device })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *System) ExportFileToTarget(src string, target RemovableTarget) (string, error) {
|
||||
if src == "" || target.Device == "" {
|
||||
return "", fmt.Errorf("source and target are required")
|
||||
}
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mountpoint := strings.TrimSpace(target.Mountpoint)
|
||||
mountedHere := false
|
||||
if mountpoint == "" {
|
||||
mountpoint = filepath.Join("/tmp", "bee-export-"+filepath.Base(target.Device))
|
||||
if err := os.MkdirAll(mountpoint, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if raw, err := exec.Command("mount", target.Device, mountpoint).CombinedOutput(); err != nil {
|
||||
_ = os.Remove(mountpoint)
|
||||
return string(raw), err
|
||||
}
|
||||
mountedHere = true
|
||||
}
|
||||
|
||||
filename := filepath.Base(src)
|
||||
dst := filepath.Join(mountpoint, filename)
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = exec.Command("sync").Run()
|
||||
|
||||
if mountedHere {
|
||||
_ = exec.Command("umount", mountpoint).Run()
|
||||
_ = os.Remove(mountpoint)
|
||||
}
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
156
audit/internal/platform/network.go
Normal file
156
audit/internal/platform/network.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *System) ListInterfaces() ([]InterfaceInfo, error) {
|
||||
names, err := listInterfaceNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]InterfaceInfo, 0, len(names))
|
||||
for _, name := range names {
|
||||
state := "unknown"
|
||||
if raw, err := exec.Command("ip", "-o", "link", "show", name).Output(); err == nil {
|
||||
fields := strings.Fields(string(raw))
|
||||
if len(fields) >= 9 {
|
||||
state = fields[8]
|
||||
}
|
||||
}
|
||||
|
||||
var ipv4 []string
|
||||
if raw, err := exec.Command("ip", "-o", "-4", "addr", "show", "dev", name).Output(); err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 4 {
|
||||
ipv4 = append(ipv4, fields[3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, InterfaceInfo{Name: name, State: state, IPv4: ipv4})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *System) DefaultRoute() string {
|
||||
raw, err := exec.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fields := strings.Fields(string(raw))
|
||||
for i := 0; i < len(fields)-1; i++ {
|
||||
if fields[i] == "via" {
|
||||
return fields[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *System) DHCPOne(iface string) (string, error) {
|
||||
var out bytes.Buffer
|
||||
if err := exec.Command("ip", "link", "set", iface, "up").Run(); err != nil {
|
||||
fmt.Fprintf(&out, "WARN: ip link set up failed: %v\n", err)
|
||||
}
|
||||
if raw, err := exec.Command("dhclient", "-r", iface).CombinedOutput(); err == nil {
|
||||
out.Write(raw)
|
||||
} else if len(raw) > 0 {
|
||||
out.Write(raw)
|
||||
}
|
||||
raw, err := exec.Command("dhclient", "-4", "-v", iface).CombinedOutput()
|
||||
out.Write(raw)
|
||||
if err != nil {
|
||||
return out.String(), err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func (s *System) DHCPAll() (string, error) {
|
||||
ifaces, err := listInterfaceNames()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out strings.Builder
|
||||
for _, iface := range ifaces {
|
||||
fmt.Fprintf(&out, "[%s]\n", iface)
|
||||
log, err := s.DHCPOne(iface)
|
||||
out.WriteString(log)
|
||||
if err != nil {
|
||||
fmt.Fprintf(&out, "ERROR: %v\n", err)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func (s *System) SetStaticIPv4(cfg StaticIPv4Config) (string, error) {
|
||||
if cfg.Interface == "" || cfg.Address == "" || cfg.Prefix == "" {
|
||||
return "", fmt.Errorf("interface, address, and prefix are required")
|
||||
}
|
||||
|
||||
dns := cfg.DNS
|
||||
if len(dns) == 0 {
|
||||
dns = []string{"77.88.8.8", "77.88.8.1", "1.1.1.1", "8.8.8.8"}
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
_ = exec.Command("ip", "link", "set", cfg.Interface, "up").Run()
|
||||
_ = exec.Command("ip", "addr", "flush", "dev", cfg.Interface).Run()
|
||||
if raw, err := exec.Command("ip", "addr", "add", cfg.Address+"/"+cfg.Prefix, "dev", cfg.Interface).CombinedOutput(); err != nil {
|
||||
return string(raw), err
|
||||
}
|
||||
out.WriteString("address configured\n")
|
||||
if cfg.Gateway != "" {
|
||||
_ = exec.Command("ip", "route", "del", "default").Run()
|
||||
if raw, err := exec.Command("ip", "route", "add", "default", "via", cfg.Gateway, "dev", cfg.Interface).CombinedOutput(); err != nil {
|
||||
return out.String() + string(raw), err
|
||||
}
|
||||
out.WriteString("default route configured\n")
|
||||
}
|
||||
|
||||
var resolv strings.Builder
|
||||
for _, dnsServer := range dns {
|
||||
dnsServer = strings.TrimSpace(dnsServer)
|
||||
if dnsServer == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&resolv, "nameserver %s\n", dnsServer)
|
||||
}
|
||||
if err := os.WriteFile("/etc/resolv.conf", []byte(resolv.String()), 0644); err != nil {
|
||||
return out.String(), err
|
||||
}
|
||||
out.WriteString("dns configured\n")
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func listInterfaceNames() ([]string, error) {
|
||||
raw, err := exec.Command("ip", "-o", "link", "show").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||||
fields := strings.SplitN(line, ": ", 3)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
name := fields[1]
|
||||
if name == "lo" || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "virbr") ||
|
||||
strings.HasPrefix(name, "veth") || strings.HasPrefix(name, "tun") ||
|
||||
strings.HasPrefix(name, "tap") || strings.HasPrefix(name, "br-") ||
|
||||
strings.HasPrefix(name, "bond") || strings.HasPrefix(name, "dummy") {
|
||||
continue
|
||||
}
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
43
audit/internal/platform/parse.go
Normal file
43
audit/internal/platform/parse.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package platform
|
||||
|
||||
import "strings"
|
||||
|
||||
func parseLSBLKPairs(line string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, part := range splitQuotedFields(line) {
|
||||
idx := strings.Index(part, "=")
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
key := part[:idx]
|
||||
value := strings.Trim(part[idx+1:], `"`)
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func splitQuotedFields(s string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
inQuotes := false
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
inQuotes = !inQuotes
|
||||
cur.WriteRune(r)
|
||||
case ' ':
|
||||
if inQuotes {
|
||||
cur.WriteRune(r)
|
||||
} else if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
101
audit/internal/platform/sat.go
Normal file
101
audit/internal/platform/sat.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *System) RunNvidiaAcceptancePack(baseDir string) (string, error) {
|
||||
if baseDir == "" {
|
||||
baseDir = "/var/log/bee-sat"
|
||||
}
|
||||
ts := time.Now().UTC().Format("20060102-150405")
|
||||
runDir := filepath.Join(baseDir, "gpu-nvidia-"+ts)
|
||||
if err := os.MkdirAll(runDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type job struct {
|
||||
name string
|
||||
cmd []string
|
||||
}
|
||||
jobs := []job{
|
||||
{name: "01-nvidia-smi-q.log", cmd: []string{"nvidia-smi", "-q"}},
|
||||
{name: "02-dmidecode-baseboard.log", cmd: []string{"dmidecode", "-t", "baseboard"}},
|
||||
{name: "03-dmidecode-system.log", cmd: []string{"dmidecode", "-t", "system"}},
|
||||
{name: "04-nvidia-bug-report.log", cmd: []string{"nvidia-bug-report.sh", "--output", filepath.Join(runDir, "nvidia-bug-report.log")}},
|
||||
}
|
||||
|
||||
var summary strings.Builder
|
||||
fmt.Fprintf(&summary, "run_at_utc=%s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
for _, job := range jobs {
|
||||
out, err := exec.Command(job.cmd[0], job.cmd[1:]...).CombinedOutput()
|
||||
if writeErr := os.WriteFile(filepath.Join(runDir, job.name), out, 0644); writeErr != nil {
|
||||
return "", writeErr
|
||||
}
|
||||
rc := 0
|
||||
if err != nil {
|
||||
rc = 1
|
||||
}
|
||||
fmt.Fprintf(&summary, "%s_rc=%d\n", strings.TrimSuffix(strings.TrimPrefix(job.name, "0"), ".log"), rc)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(runDir, "summary.txt"), []byte(summary.String()), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
archive := filepath.Join(baseDir, "gpu-nvidia-"+ts+".tar.gz")
|
||||
if err := createTarGz(archive, runDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return archive, nil
|
||||
}
|
||||
|
||||
func createTarGz(dst, srcDir string) error {
|
||||
file, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gz := gzip.NewWriter(file)
|
||||
defer gz.Close()
|
||||
|
||||
tw := tar.NewWriter(gz)
|
||||
defer tw.Close()
|
||||
|
||||
base := filepath.Dir(srcDir)
|
||||
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = rel
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(tw, file)
|
||||
return err
|
||||
})
|
||||
}
|
||||
54
audit/internal/platform/services.go
Normal file
54
audit/internal/platform/services.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *System) ListBeeServices() ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, pattern := range []string{"/etc/systemd/system/bee-*.service", "/lib/systemd/system/bee-*.service"} {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, match := range matches {
|
||||
name := strings.TrimSuffix(filepath.Base(match), ".service")
|
||||
if !seen[name] {
|
||||
seen[name] = true
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *System) ServiceState(name string) string {
|
||||
raw, err := exec.Command("systemctl", "is-active", name).CombinedOutput()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
raw, err = exec.Command("systemctl", "show", name, "--property=ActiveState", "--value").CombinedOutput()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
state := strings.TrimSpace(string(raw))
|
||||
if state == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (s *System) ServiceDo(name string, action ServiceAction) (string, error) {
|
||||
raw, err := exec.Command("systemctl", string(action), name).CombinedOutput()
|
||||
return string(raw), err
|
||||
}
|
||||
|
||||
func (s *System) ServiceStatus(name string) (string, error) {
|
||||
raw, err := exec.Command("systemctl", "status", name, "--no-pager").CombinedOutput()
|
||||
return string(raw), err
|
||||
}
|
||||
49
audit/internal/platform/system_test.go
Normal file
49
audit/internal/platform/system_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package platform
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplitQuotedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
line := `NAME="sdb1" TYPE="part" LABEL="BEE EXPORT" MODEL="USB DISK 3.0"`
|
||||
got := splitQuotedFields(line)
|
||||
want := []string{
|
||||
`NAME="sdb1"`,
|
||||
`TYPE="part"`,
|
||||
`LABEL="BEE EXPORT"`,
|
||||
`MODEL="USB DISK 3.0"`,
|
||||
}
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("len(got)=%d len(want)=%d; got=%q", len(got), len(want), got)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("got[%d]=%q want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLSBLKPairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
line := `NAME="sdb1" TYPE="part" PKNAME="sdb" RM="1" FSTYPE="vfat" MOUNTPOINT="" SIZE="57.3G" LABEL="BEE EXPORT" MODEL="USB DISK 3.0"`
|
||||
got := parseLSBLKPairs(line)
|
||||
|
||||
checks := map[string]string{
|
||||
"NAME": "sdb1",
|
||||
"TYPE": "part",
|
||||
"PKNAME": "sdb",
|
||||
"RM": "1",
|
||||
"FSTYPE": "vfat",
|
||||
"MOUNTPOINT": "",
|
||||
"SIZE": "57.3G",
|
||||
"LABEL": "BEE EXPORT",
|
||||
"MODEL": "USB DISK 3.0",
|
||||
}
|
||||
for key, want := range checks {
|
||||
if got[key] != want {
|
||||
t.Fatalf("got[%s]=%q want %q", key, got[key], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
29
audit/internal/platform/tools.go
Normal file
29
audit/internal/platform/tools.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *System) TailFile(path string, lines int) string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("read %s: %v", path, err)
|
||||
}
|
||||
all := strings.Split(strings.TrimRight(string(raw), "\n"), "\n")
|
||||
if lines <= 0 || len(all) <= lines {
|
||||
return string(raw)
|
||||
}
|
||||
return strings.Join(all[len(all)-lines:], "\n")
|
||||
}
|
||||
|
||||
func (s *System) CheckTools(names []string) []ToolStatus {
|
||||
out := make([]ToolStatus, 0, len(names))
|
||||
for _, name := range names {
|
||||
path, err := exec.LookPath(name)
|
||||
out = append(out, ToolStatus{Name: name, Path: path, OK: err == nil})
|
||||
}
|
||||
return out
|
||||
}
|
||||
44
audit/internal/platform/types.go
Normal file
44
audit/internal/platform/types.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package platform
|
||||
|
||||
type System struct{}
|
||||
|
||||
type InterfaceInfo struct {
|
||||
Name string
|
||||
State string
|
||||
IPv4 []string
|
||||
}
|
||||
|
||||
type ServiceAction string
|
||||
|
||||
const (
|
||||
ServiceStart ServiceAction = "start"
|
||||
ServiceStop ServiceAction = "stop"
|
||||
ServiceRestart ServiceAction = "restart"
|
||||
)
|
||||
|
||||
type StaticIPv4Config struct {
|
||||
Interface string
|
||||
Address string
|
||||
Prefix string
|
||||
Gateway string
|
||||
DNS []string
|
||||
}
|
||||
|
||||
type RemovableTarget struct {
|
||||
Device string
|
||||
FSType string
|
||||
Size string
|
||||
Label string
|
||||
Model string
|
||||
Mountpoint string
|
||||
}
|
||||
|
||||
type ToolStatus struct {
|
||||
Name string
|
||||
Path string
|
||||
OK bool
|
||||
}
|
||||
|
||||
func New() *System {
|
||||
return &System{}
|
||||
}
|
||||
Reference in New Issue
Block a user