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 }