still working

This commit is contained in:
Dev
2025-12-13 15:31:52 +00:00
parent dda7abedb7
commit d69e01bbaf
18 changed files with 795 additions and 1 deletions

138
internal/infra/zfs/zfs.go Normal file
View File

@@ -0,0 +1,138 @@
package zfs
import (
"context"
"fmt"
"strings"
"time"
"github.com/example/storage-appliance/internal/domain"
"github.com/example/storage-appliance/internal/infra/osexec"
)
type Adapter struct {
Runner osexec.Runner
}
func NewAdapter(runner osexec.Runner) *Adapter { return &Adapter{Runner: runner} }
func (a *Adapter) ListPools(ctx context.Context) ([]domain.Pool, error) {
out, errOut, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zpool", "list", "-H", "-o", "name,health,size")
_ = errOut
if err != nil {
return nil, err
}
if code != 0 {
return nil, fmt.Errorf("zpool list returned %d: %s", code, out)
}
var pools []domain.Pool
lines := strings.Split(strings.TrimSpace(out), "\n")
for _, l := range lines {
parts := strings.Split(l, "\t")
if len(parts) >= 3 {
pools = append(pools, domain.Pool{Name: parts[0], Health: parts[1], Capacity: parts[2]})
}
}
return pools, nil
}
func (a *Adapter) GetPoolStatus(ctx context.Context, pool string) (domain.PoolHealth, error) {
out, _, _, err := osexec.ExecWithRunner(a.Runner, ctx, "zpool", "status", pool)
if err != nil {
return domain.PoolHealth{}, err
}
// heuristic: find HEALTH: lines or scan lines
status := "UNKNOWN"
detail := ""
for _, line := range strings.Split(out, "\n") {
if strings.Contains(line, "state:") || strings.Contains(line, "health:") {
detail = detail + line + "\n"
if strings.Contains(line, "ONLINE") || strings.Contains(line, "ONLINE") {
status = "ONLINE"
}
if strings.Contains(line, "DEGRADED") {
status = "DEGRADED"
}
}
}
return domain.PoolHealth{Pool: pool, Status: status, Detail: detail}, nil
}
func (a *Adapter) CreatePoolSync(ctx context.Context, name string, vdevs []string) error {
args := append([]string{"create", name}, vdevs...)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zpool", args...)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("zpool create failed: %s", stderr)
}
return nil
}
func (a *Adapter) ListDatasets(ctx context.Context, pool string) ([]domain.Dataset, error) {
out, _, _, err := osexec.ExecWithRunner(a.Runner, ctx, "zfs", "list", "-H", "-o", "name,type", "-r", pool)
if err != nil {
return nil, err
}
var res []domain.Dataset
for _, l := range strings.Split(strings.TrimSpace(out), "\n") {
parts := strings.Split(l, "\t")
if len(parts) >= 2 {
res = append(res, domain.Dataset{Name: parts[0], Pool: pool, Type: parts[1]})
}
}
return res, nil
}
func (a *Adapter) CreateDataset(ctx context.Context, name string, props map[string]string) error {
args := []string{"create", name}
for k, v := range props {
args = append(args, "-o", fmt.Sprintf("%s=%s", k, v))
}
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zfs", args...)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("zfs create failed: %s", stderr)
}
return nil
}
func (a *Adapter) Snapshot(ctx context.Context, dataset, snapName string) error {
name := fmt.Sprintf("%s@%s", dataset, snapName)
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zfs", "snapshot", name)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("zfs snapshot failed: %s", stderr)
}
return nil
}
func (a *Adapter) ScrubStart(ctx context.Context, pool string) error {
_, stderr, code, err := osexec.ExecWithRunner(a.Runner, ctx, "zpool", "scrub", pool)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("zpool scrub failed: %s", stderr)
}
return nil
}
func (a *Adapter) ScrubStatus(ctx context.Context, pool string) (string, error) {
out, _, _, err := osexec.ExecWithRunner(a.Runner, ctx, "zpool", "status", pool)
if err != nil {
return "", err
}
// Find scan line
for _, line := range strings.Split(out, "\n") {
if strings.Contains(line, "scan: ") {
return strings.TrimSpace(line), nil
}
}
return "no-scan-status", nil
}

View File

@@ -0,0 +1,58 @@
package zfs
import (
"context"
"testing"
"time"
"github.com/example/storage-appliance/internal/infra/osexec"
)
func TestListPoolsParse(t *testing.T) {
out := "tank\tONLINE\t1.00T\n"
fr := &osexec.FakeRunner{Stdout: out, ExitCode: 0}
a := NewAdapter(fr)
ctx := context.Background()
pools, err := a.ListPools(ctx)
if err != nil {
t.Fatalf("ListPools failed: %v", err)
}
if len(pools) != 1 || pools[0].Name != "tank" || pools[0].Health != "ONLINE" {
t.Fatalf("unexpected pools result: %+v", pools)
}
}
func TestListDatasetsParse(t *testing.T) {
out := "tank\tdataset\ntank/ds\tdataset\n"
fr := &osexec.FakeRunner{Stdout: out, ExitCode: 0}
a := NewAdapter(fr)
pool := "tank"
ds, err := a.ListDatasets(context.Background(), pool)
if err != nil {
t.Fatalf("ListDatasets failed: %v", err)
}
if len(ds) != 2 {
t.Fatalf("expected 2 datasets, got %d", len(ds))
}
}
func TestScrubStatusParse(t *testing.T) {
out := " scan: scrub repaired 0 in 0h0m with 0 errors on Tue Jul 1 12:34:56 2025\n"
fr := &osexec.FakeRunner{Stdout: out, ExitCode: 0}
a := NewAdapter(fr)
s, err := a.ScrubStatus(context.Background(), "tank")
if err != nil {
t.Fatalf("ScrubStatus failed: %v", err)
}
if s == "" {
t.Fatalf("expected scan line, got empty")
}
}
func TestCreatePoolSync(t *testing.T) {
fr := &osexec.FakeRunner{Stdout: "", ExitCode: 0}
a := NewAdapter(fr)
if err := a.CreatePoolSync(context.Background(), "tank", []string{"/dev/sda"}); err != nil {
t.Fatalf("CreatePoolSync failed: %v", err)
}
}