Files
zrepl_patched/internal/platformtest/platformtest_zpool.go
Christian Schwarz b3f24861c4 platformtest: replace logmockzfs with multi-personality safety interposer
Replace the shell-script-based logmockzfs wrapper with a Go-native
multi-personality binary (busybox-style). The platformtest binary now
acts as a safety interposer for zfs/zpool when invoked via symlinks,
validating that all commands reference the hardcoded test pool before
delegating through sudo.

Key changes:
- New interposer.go: SetupInterposerPath creates a tmpdir with symlinks
  and replaces PATH; RunInterposer validates args and delegates via sudo
- Pool name, image path, and mountpoint are now hardcoded constants
- Remove ZpoolCreateArgs, Zpool struct, and CLI flags for pool config
- Remove logmockzfs/ shell scripts (logzfsenv, zfs wrapper)
- Simplify Makefile: no more logmockzfs invocation or root check
- Add -no-interposer flag for direct execution as root
- Safety: block -a (all) flags without pool reference, validate
  mountpoint paths against traversal, command-aware flag parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:56:38 +00:00

91 lines
2.4 KiB
Go

package platformtest
import (
"context"
"fmt"
"os"
"runtime"
"strings"
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/internal/zfs"
)
const (
PlatformTestPoolName = "zreplplatformtest"
PlatformTestImagePath = "/tmp/zreplplatformtest.pool.img"
PlatformTestMountpoint = "/tmp/zreplplatformtest.pool"
PlatformTestImageSize = 200 * (1 << 20) // 200 MiB
)
var ZpoolExportTimeout time.Duration = 500 * time.Millisecond
func CreateOrReplaceZpool(ctx context.Context, e Execer) error {
// export pool if it already exists (idempotence)
if _, err := zfs.ZFSGetRawAnySource(ctx, PlatformTestPoolName, []string{"name"}); err != nil {
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
// we'll create it shortly
} else {
return errors.Wrapf(err, "cannot determine whether test pool %q exists", PlatformTestPoolName)
}
} else {
// exists, export it, OpenFile will destroy it
if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", PlatformTestPoolName); err != nil {
return errors.Wrapf(err, "cannot destroy test pool %q", PlatformTestPoolName)
}
}
// idempotently (re)create the pool image
image, err := os.OpenFile(PlatformTestImagePath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return errors.Wrap(err, "create image file")
}
defer image.Close()
if err := image.Truncate(PlatformTestImageSize); err != nil {
return errors.Wrap(err, "create image: truncate")
}
image.Close()
// create the pool (zpool runs via sudo through the interposer,
// which creates the mountpoint and chmods it to 0777)
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-f",
"-O", fmt.Sprintf("mountpoint=%s", PlatformTestMountpoint),
PlatformTestPoolName, PlatformTestImagePath,
)
if err != nil {
return errors.Wrap(err, "zpool create")
}
return nil
}
func DestroyZpool(ctx context.Context, e Execer) error {
exportDeadline := time.Now().Add(ZpoolExportTimeout)
for {
if time.Now().After(exportDeadline) {
return errors.Errorf("could not zpool export (got 'pool is busy'): %s", PlatformTestPoolName)
}
err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", PlatformTestPoolName)
if err == nil {
break
}
if strings.Contains(err.Error(), "pool is busy") {
runtime.Gosched()
continue
}
if err != nil {
return errors.Wrapf(err, "export pool %q", PlatformTestPoolName)
}
}
if err := os.Remove(PlatformTestImagePath); err != nil {
return errors.Wrapf(err, "remove pool image")
}
return nil
}