still working
This commit is contained in:
138
internal/infra/zfs/zfs.go
Normal file
138
internal/infra/zfs/zfs.go
Normal 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
|
||||
}
|
||||
58
internal/infra/zfs/zfs_test.go
Normal file
58
internal/infra/zfs/zfs_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user