b3f24861c4
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>
91 lines
2.4 KiB
Go
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
|
|
}
|