Files
storage-appliance/internal/infra/zfs/zfs.go
2025-12-13 15:31:52 +00:00

139 lines
3.8 KiB
Go

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
}