diff --git a/Makefile b/Makefile index 2bf3506..3f1d95c 100644 --- a/Makefile +++ b/Makefile @@ -273,7 +273,7 @@ cover-platform-bin: $(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \ -c -o "$(COVER_PLATFORM_BIN_PATH)" \ -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \ - ./platformtest/harness + ./internal/platformtest/harness cover-platform: # do not track dependency on cover-platform-bin to allow build of binary outside of test VM export _TEST_PLATFORM_CMD="$(COVER_PLATFORM_BIN_PATH) \ @@ -289,25 +289,13 @@ test-platform: export _TEST_PLATFORM_CMD="\"$(TEST_PLATFORM_BIN_PATH)\""; \ $(MAKE) _test-or-cover-platform-impl -ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest -ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img -ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool -ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log -# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool ZREPL_PLATFORMTEST_ARGS := _test-or-cover-platform-impl: $(ARTIFACTDIR) ifndef _TEST_PLATFORM_CMD $(error _TEST_PLATFORM_CMD is undefined, caller 'cover-platform' or 'test-platform' should have defined it) endif - rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)" rm -f "$(ARTIFACTDIR)/platformtest.cover" - internal/platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \ - $(_TEST_PLATFORM_CMD) \ - -poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \ - -imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \ - -mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \ - $(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \ - $(ZREPL_PLATFORMTEST_ARGS) + $(_TEST_PLATFORM_CMD) $(ZREPL_PLATFORMTEST_ARGS) cover-merge: $(ARTIFACTDIR) $(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover @@ -315,7 +303,6 @@ cover-html: cover-merge $(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html" cover-full: - test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2 $(MAKE) test-go COVER=1 $(MAKE) cover-platform-bin $(MAKE) cover-platform diff --git a/docs/usage.rst b/docs/usage.rst index 6867ba2..7228c14 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -101,24 +101,20 @@ Along with the main ``zrepl`` binary, we release the ``platformtest`` binaries. The zrepl platform tests are an integration test suite that is complementary to the pure Go unit tests. Any test that needs to interact with ZFS is a platform test. -The platform need to run as root. +The platform tests require ``sudo`` ``NOPASSWD``-access to ``zfs`` and ``zpool``. For each test, we create a fresh dummy zpool backed by a file-based vdev. -The file path, and a root mountpoint for the dummy zpool, must be specified on the command line: - -:: - - mkdir -p /tmp/zreplplatformtest - ./platformtest \ - -poolname 'zreplplatformtest' \ # <- name must contain zreplplatformtest - -imagepath /tmp/zreplplatformtest.img \ # <- zrepl will create the file - -mountpoint /tmp/zreplplatformtest # <- must exist +The pool name, image path, and mountpoint are hardcoded in the binary. +The ``platformtest`` binary is a multi-personality binary (similar to busybox). +It acts as a safety interposer for ``zfs`` and ``zpool`` commands, ensuring all operations +reference the hard-coded test pool name ``zreplplatformtest``. It creates symlinks to itself in a temporary +directory and replaces ``PATH`` so that all ``zfs``/``zpool`` invocations go through the interposer. .. WARNING:: - ``platformtest`` will unconditionally overwrite the file at `imagepath` - and unconditionally ``zpool destroy $poolname``. - So, don't use a production poolname, and consider running the test in a VM. + ``platformtest`` will unconditionally ``zpool destroy`` the test zpool + and unconditionally delete the image file that backs it. + Consider running the test in a VM. It'll be a lot faster as well because the underlying operations, ``zfs list`` in particular, will be faster. diff --git a/internal/platformtest/harness/harness.go b/internal/platformtest/harness/harness.go index c89e4d5..22de8d2 100644 --- a/internal/platformtest/harness/harness.go +++ b/internal/platformtest/harness/harness.go @@ -26,18 +26,23 @@ var bold = color.New(color.Bold) var boldRed = color.New(color.Bold, color.FgHiRed) var boldGreen = color.New(color.Bold, color.FgHiGreen) -const DefaultPoolImageSize = 200 * (1 << 20) - func main() { + // Multi-personality binary: check argv[0] to determine mode. + // When invoked via a symlink named "zfs" or "zpool", act as + // a safety interposer that validates commands and delegates + // to the real binary via sudo. + switch filepath.Base(os.Args[0]) { + case "zfs": + os.Exit(platformtest.RunInterposer("zfs")) + case "zpool": + os.Exit(platformtest.RunInterposer("zpool")) + } + // Test harness mode var args HarnessArgs - - flag.StringVar(&args.CreateArgs.PoolName, "poolname", "", "") - flag.StringVar(&args.CreateArgs.ImagePath, "imagepath", "", "") - flag.Int64Var(&args.CreateArgs.ImageSize, "imagesize", DefaultPoolImageSize, "") - flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "") + flag.BoolVar(&args.NoInterposer, "no-interposer", false, "skip safety interposer; symlink zfs/zpool directly to real binaries (requires running as root)") flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed") - flag.StringVar(&args.Run, "run", "", "") + flag.StringVar(&args.Run, "run", "", "regex to filter test cases") flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "") flag.Parse() @@ -49,7 +54,7 @@ func main() { var exitWithErr = fmt.Errorf("exit with error") type HarnessArgs struct { - CreateArgs platformtest.ZpoolCreateArgs + NoInterposer bool StopAndKeepPoolOnFail bool Run string } @@ -90,6 +95,14 @@ func (i *invocation) RegisterChild(c *invocation) error { func HarnessRun(args HarnessArgs) error { + // Set up interposer: create symlinks and replace PATH. + // In direct mode, zfs/zpool symlink to real binaries (no sudo, no validation). + _, cleanup, err := platformtest.SetupInterposerPath("", args.NoInterposer) + if err != nil { + return errors.Wrap(err, "set up interposer PATH") + } + defer cleanup() + runRE := regexp.MustCompile(args.Run) outlets := logger.NewOutlets() @@ -104,11 +117,6 @@ func HarnessRun(args HarnessArgs) error { } outlets.Add(outlet, level) logger := logger.NewLogger(outlets, 1*time.Second) - - if err := args.CreateArgs.Validate(); err != nil { - logger.Error(err.Error()) - panic(err) - } ctx := context.Background() defer trace.WithTaskFromStackUpdateCtx(&ctx)() ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger)) @@ -128,14 +136,13 @@ func HarnessRun(args HarnessArgs) error { bold.Printf("BEGIN TEST CASE %s\n", inv) - pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs) - if err != nil { + if err := platformtest.CreateOrReplaceZpool(ctx, ex); err != nil { panic(errors.Wrap(err, "create test pool")) } ctx := &platformtest.Context{ Context: ctx, - RootDataset: filepath.Join(pool.Name(), "rootds"), + RootDataset: filepath.Join(platformtest.PlatformTestPoolName, "rootds"), QueueSubtest: func(id string, stf func(*platformtest.Context)) { stinv := newInvocation(stf, id) err := inv.RegisterChild(stinv) @@ -158,7 +165,7 @@ func HarnessRun(args HarnessArgs) error { return exitWithErr } - if err := pool.Destroy(ctx, ex); err != nil { + if err := platformtest.DestroyZpool(ctx, ex); err != nil { panic(fmt.Sprintf("error destroying test pool: %s", err)) } diff --git a/internal/platformtest/harness/harness_test.go b/internal/platformtest/harness/harness_test.go index 3589610..d18d785 100644 --- a/internal/platformtest/harness/harness_test.go +++ b/internal/platformtest/harness/harness_test.go @@ -3,8 +3,11 @@ package main import ( "fmt" "os" + "path/filepath" "strings" "testing" + + "github.com/zrepl/zrepl/internal/platformtest" ) // Idea taken from @@ -12,16 +15,22 @@ import ( // /* How to generate coverage: go test -c -covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... - sudo ../logmockzfs/logzfsenv /tmp/zrepl_platform_test.log /usr/bin/zfs \ - ./harness.test -test.coverprofile=/tmp/harness.out \ - -test.v __DEVEL--i-heard-you-like-tests \ - -imagepath /tmp/testpool.img -poolname zreplplatformtest + ./harness.test -test.coverprofile=/tmp/harness.out \ + -test.v __DEVEL--i-heard-you-like-tests go tool cover -html=/tmp/harness.out -o /tmp/harness.html */ // Merge with existing coverage reports using gocovmerge: // https://github.com/wadey/gocovmerge func TestMain(t *testing.T) { + // Multi-personality: when invoked as zfs/zpool symlink, run interposer + switch filepath.Base(os.Args[0]) { + case "zfs": + os.Exit(platformtest.RunInterposer("zfs")) + case "zpool": + os.Exit(platformtest.RunInterposer("zpool")) + } + fmt.Println("incoming args: ", os.Args) var ( diff --git a/internal/platformtest/interposer.go b/internal/platformtest/interposer.go new file mode 100644 index 0000000..dadf73c --- /dev/null +++ b/internal/platformtest/interposer.go @@ -0,0 +1,281 @@ +package platformtest + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// SetupInterposerPath creates a temp directory with zfs/zpool symlinks +// and replaces PATH so that all zfs/zpool invocations go through it. +// +// When direct is false (normal mode), zfs/zpool symlink to the current +// executable (interposer mode: validates commands and delegates via sudo). +// Additional .real symlinks point to the actual binaries. +// +// When direct is true, zfs/zpool symlink directly to the real binaries +// (no interposer, no sudo). Use this when running as root. +// +// Returns the directory path and a cleanup function. +func SetupInterposerPath(explicitDir string, direct bool) (string, func(), error) { + // Resolve real binary paths while original PATH is still intact. + // In direct mode we only need zfs/zpool and bash; in interposer + // mode we also need sudo. + lookupNames := []string{"zfs", "zpool", "bash"} + if !direct { + lookupNames = append(lookupNames, "sudo") + } + realBinaries := make(map[string]string) + for _, name := range lookupNames { + p, err := exec.LookPath(name) + if err != nil { + return "", nil, fmt.Errorf("find real %s binary: %w", name, err) + } + realBinaries[name] = p + } + + var dir string + var cleanup func() + + if explicitDir != "" { + dir = explicitDir + cleanup = func() {} // caller manages lifecycle + } else { + var err error + dir, err = os.MkdirTemp("", "zrepl-platformtest-*") + if err != nil { + return "", nil, fmt.Errorf("create interposer dir: %w", err) + } + cleanup = func() { os.RemoveAll(dir) } + } + + if direct { + // Direct mode: symlink zfs/zpool to real binaries (no interposer). + for _, name := range []string{"zfs", "zpool", "bash"} { + link := filepath.Join(dir, name) + os.Remove(link) + if err := os.Symlink(realBinaries[name], link); err != nil { + cleanup() + return "", nil, fmt.Errorf("create symlink %s: %w", name, err) + } + } + } else { + // Interposer mode: symlink zfs/zpool to self, .real to actual binaries. + self, err := os.Executable() + if err != nil { + cleanup() + return "", nil, fmt.Errorf("get executable path: %w", err) + } + + for _, name := range []string{"zfs", "zpool"} { + link := filepath.Join(dir, name) + os.Remove(link) + if err := os.Symlink(self, link); err != nil { + cleanup() + return "", nil, fmt.Errorf("create symlink %s: %w", name, err) + } + realLink := filepath.Join(dir, name+".real") + os.Remove(realLink) + if err := os.Symlink(realBinaries[name], realLink); err != nil { + cleanup() + return "", nil, fmt.Errorf("create symlink %s.real: %w", name, err) + } + } + + // Symlink other required binaries so they remain accessible + // after PATH replacement. + for _, name := range []string{"sudo", "bash"} { + link := filepath.Join(dir, name) + os.Remove(link) + if err := os.Symlink(realBinaries[name], link); err != nil { + cleanup() + return "", nil, fmt.Errorf("create symlink %s: %w", name, err) + } + } + } + + // Replace PATH entirely so ALL zfs/zpool calls go through this directory. + os.Setenv("PATH", dir) + + return dir, cleanup, nil +} + +// RunInterposer handles the zfs/zpool interposer mode. +// It validates that the command references the test pool, +// then executes the real binary via sudo using the .real +// symlink placed by SetupInterposerPath. +func RunInterposer(command string) int { + args := os.Args[1:] + + if err := validatePoolName(command, args, PlatformTestPoolName); err != nil { + fmt.Fprintf(os.Stderr, "SAFETY: %v\n", err) + return 1 + } + + // The .real symlink in PATH points to the actual binary. + // sudo follows the symlink, so no explicit resolution needed. + realBinary := filepath.Join(os.Getenv("PATH"), command+".real") + sudoArgs := append([]string{realBinary}, args...) + cmd := exec.Command("sudo", sudoArgs...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + + // After a successful create, chmod the mountpoint so the + // unprivileged test user can operate on it. + if err == nil && isCreateOperation(args) { + if mountpoint := extractCreateMountpoint(command, args); mountpoint != "" { + chmodMountpoint(mountpoint) + } + } + + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return exitErr.ExitCode() + } + fmt.Fprintf(os.Stderr, "interposer: exec error: %v\n", err) + return 1 + } + return 0 +} + +func validatePoolName(command string, args []string, poolName string) error { + // Allow subcommands that don't reference a specific dataset, + // used for feature detection (e.g., "zfs send" with no dataset, + // "zfs receive" with no dataset, "zpool get"). + if len(args) == 0 { + return nil + } + + // Some subcommands are inherently safe / don't target a pool. + // Allow them unconditionally. The real safety net is that we + // only have sudo for zfs/zpool, not for anything destructive + // outside the test pool. + // + // Check if any non-flag argument references the pool name. + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + continue + } + if arg == poolName || strings.HasPrefix(arg, poolName+"/") || strings.HasPrefix(arg, poolName+"@") || strings.HasPrefix(arg, poolName+"#") { + return nil + } + } + + // Block commands with -a (all) flag when no pool name was found. + // These operate on ALL pools/filesystems (e.g., zfs unmount -a, + // zpool export -a) and must not be allowed without a pool reference. + for _, arg := range args { + if !strings.HasPrefix(arg, "-") { + continue + } + // -a standalone or in combined flags (e.g., -fa, -ra) + if arg == "-a" || (len(arg) > 2 && arg[0] == '-' && arg[1] != '-' && + strings.ContainsRune(arg[1:], 'a')) { + return fmt.Errorf("%s command rejected: -a flag not allowed without explicit pool reference", command) + } + } + + // Special case: bare subcommand with only flags (feature detection). + // Count non-flag args. If there's only one (the subcommand itself), + // allow it. + // + // Note: -s is intentionally NOT in the skip list. It is boolean in + // some subcommands (e.g., zfs recv -s) and argument-taking in others + // (e.g., zfs list -s property). Not skipping its "value" is safe: + // commands with the pool name are already handled by the loop above, + // and for commands without a pool name, over-counting non-flag args + // causes a safe rejection rather than a dangerous acceptance. + nonFlagArgs := 0 + skipNext := false + for _, arg := range args { + if skipNext { + skipNext = false + continue + } + // Flags that take a value argument: -o, -O always; -t for zfs only. + // -t is arg-taking in zfs (list -t type, send -t token) but + // boolean in zpool (import -t = temporary, events -t = timestamps). + argTakingChars := "oO" + if command == "zfs" { + argTakingChars = "oOt" + } + if arg == "-o" || arg == "-O" || (command == "zfs" && arg == "-t") || + (len(arg) > 2 && arg[0] == '-' && arg[1] != '-' && + strings.ContainsAny(arg, argTakingChars)) { + skipNext = true + continue + } + if strings.HasPrefix(arg, "-") { + continue + } + nonFlagArgs++ + } + // If only the subcommand is present (e.g., "send", "receive", "get"), + // this is likely feature detection. + if nonFlagArgs <= 1 { + return nil + } + + return fmt.Errorf("%s command rejected: args %v do not reference pool %q", command, args, poolName) +} + +func isCreateOperation(args []string) bool { + return len(args) > 0 && args[0] == "create" +} + +func extractCreateMountpoint(command string, args []string) string { + switch command { + case "zpool": + // zpool create ... -O mountpoint= ... + for i, arg := range args { + if arg == "-O" && i+1 < len(args) { + if strings.HasPrefix(args[i+1], "mountpoint=") { + return strings.TrimPrefix(args[i+1], "mountpoint=") + } + } + } + case "zfs": + // zfs create [-p] [-o ...] + // Dataset is the last non-flag argument. Its mountpoint is + // derived from the pool mountpoint + dataset path suffix. + dataset := "" + skipNext := false + for _, arg := range args[1:] { // skip "create" + if skipNext { + skipNext = false + continue + } + if arg == "-o" { + skipNext = true + continue + } + if strings.HasPrefix(arg, "-") { + continue + } + dataset = arg + } + if dataset != "" && strings.HasPrefix(dataset, PlatformTestPoolName+"/") { + suffix := strings.TrimPrefix(dataset, PlatformTestPoolName) + return PlatformTestMountpoint + suffix + } + } + return "" +} + +func chmodMountpoint(mountpoint string) { + // Validate: only chmod paths under the test mountpoint to prevent + // path traversal (e.g., dataset "pool/../../etc" -> mountpoint "/etc"). + cleaned := filepath.Clean(mountpoint) + if cleaned != PlatformTestMountpoint && !strings.HasPrefix(cleaned, PlatformTestMountpoint+"/") { + fmt.Fprintf(os.Stderr, "interposer: refusing to chmod %q (not under %s)\n", + mountpoint, PlatformTestMountpoint) + return + } + // Best-effort: make mountpoint accessible to unprivileged user. + exec.Command("sudo", "chmod", "0777", cleaned).Run() //nolint:errcheck +} diff --git a/internal/platformtest/logmockzfs/logzfsenv b/internal/platformtest/logmockzfs/logzfsenv deleted file mode 100755 index 6bb0829..0000000 --- a/internal/platformtest/logmockzfs/logzfsenv +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -ue -export ZREPL_MOCK_ZFS_COMMAND_LOG="$1" -shift -export ZREPL_MOCK_ZFS_PATH="$1" -shift -dirname="$(dirname "${BASH_SOURCE[0]}")" -# If we invoke this script from the top zrepl source tree, like so: -# ./platformtest/logmockzfs/logzfsenv ... -# then dirname is relative, i.e., ./platformtest/logmockzfs. -# If we put that relative dir in PATH, then Go >= 1.19 will refuse to -# exec the `./platformtest/logmockzfs/zfs` wrapper script if it finds -# it via PATH lookup. For Example: -# cmd := exec.Command("zfs") -# err := cmd.Run() -# will fail with an error that errors.Is(err, exec.ErrDot), message: -# cannot run executable found relative to current directory -# The solution is to use an abspath. -# Learn more at https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory -# and https://go-review.googlesource.com/c/go/+/381374 -absdirname="$(readlink -e "$dirname")" -export PATH="$absdirname":"$PATH" -args=("$@") -exec "${args[@]}" diff --git a/internal/platformtest/logmockzfs/zfs b/internal/platformtest/logmockzfs/zfs deleted file mode 100755 index 699e84c..0000000 --- a/internal/platformtest/logmockzfs/zfs +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -eu -args=("$@") - -if [ -n "$ZREPL_MOCK_ZFS_COMMAND_LOG" ]; then - ( - flock -x 200 - jq --compact-output -n --argjson args "$(printf '%s\0' "${args[@]}" | jq -Rsc 'split("\u0000")')" '{date: now|strflocaltime("%Y-%m-%dT%H:%M:%S"), command: $args}' >> "$ZREPL_MOCK_ZFS_COMMAND_LOG" - ) 200>"$ZREPL_MOCK_ZFS_COMMAND_LOG".lock -fi - -exec "$ZREPL_MOCK_ZFS_PATH" "${args[@]}" diff --git a/internal/platformtest/platformtest_zpool.go b/internal/platformtest/platformtest_zpool.go index 74a0f9f..f66e884 100644 --- a/internal/platformtest/platformtest_zpool.go +++ b/internal/platformtest/platformtest_zpool.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "runtime" "strings" "time" @@ -14,97 +13,63 @@ import ( "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 -type Zpool struct { - args ZpoolCreateArgs -} - -type ZpoolCreateArgs struct { - PoolName string - ImagePath string - ImageSize int64 - Mountpoint string -} - -func (a ZpoolCreateArgs) Validate() error { - if !filepath.IsAbs(a.ImagePath) { - return errors.Errorf("ImagePath must be absolute, got %q", a.ImagePath) - } - const minImageSize = 1024 - if a.ImageSize < minImageSize { - return errors.Errorf("ImageSize must be > %v, got %v", minImageSize, a.ImageSize) - } - if a.Mountpoint == "" || a.Mountpoint[0] != '/' { - return errors.Errorf("Mountpoint must be an absolute path to a directory") - } - if a.PoolName == "" { - return errors.Errorf("PoolName must not be empty") - } - return nil -} - -func CreateOrReplaceZpool(ctx context.Context, e Execer, args ZpoolCreateArgs) (*Zpool, error) { - if err := args.Validate(); err != nil { - return nil, errors.Wrap(err, "zpool create args validation error") - } - +func CreateOrReplaceZpool(ctx context.Context, e Execer) error { // export pool if it already exists (idempotence) - if _, err := zfs.ZFSGetRawAnySource(ctx, args.PoolName, []string{"name"}); err != nil { + if _, err := zfs.ZFSGetRawAnySource(ctx, PlatformTestPoolName, []string{"name"}); err != nil { if _, ok := err.(*zfs.DatasetDoesNotExist); ok { // we'll create it shortly } else { - return nil, errors.Wrapf(err, "cannot determine whether test pool %q exists", args.PoolName) + 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", args.PoolName); err != nil { - return nil, errors.Wrapf(err, "cannot destroy test pool %q", args.PoolName) + if err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", PlatformTestPoolName); err != nil { + return errors.Wrapf(err, "cannot destroy test pool %q", PlatformTestPoolName) } } - // clear the mountpoint dir - if err := os.RemoveAll(args.Mountpoint); err != nil { - return nil, errors.Wrapf(err, "remove mountpoint dir %q", args.Mountpoint) - } - if err := os.Mkdir(args.Mountpoint, 0700); err != nil { - return nil, errors.Wrapf(err, "create mountpoint dir %q", args.Mountpoint) - } - // idempotently (re)create the pool image - image, err := os.OpenFile(args.ImagePath, os.O_CREATE|os.O_RDWR, 0600) + image, err := os.OpenFile(PlatformTestImagePath, os.O_CREATE|os.O_RDWR, 0600) if err != nil { - return nil, errors.Wrap(err, "create image file") + return errors.Wrap(err, "create image file") } defer image.Close() - if err := image.Truncate(args.ImageSize); err != nil { - return nil, errors.Wrap(err, "create image: truncate") + if err := image.Truncate(PlatformTestImageSize); err != nil { + return errors.Wrap(err, "create image: truncate") } image.Close() - // create the pool + // 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", args.Mountpoint), - args.PoolName, args.ImagePath, + "-O", fmt.Sprintf("mountpoint=%s", PlatformTestMountpoint), + PlatformTestPoolName, PlatformTestImagePath, ) if err != nil { - return nil, errors.Wrap(err, "zpool create") + return errors.Wrap(err, "zpool create") } - return &Zpool{args}, nil + return nil } -func (p *Zpool) Name() string { return p.args.PoolName } - -func (p *Zpool) Destroy(ctx context.Context, e Execer) error { +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", p.args.PoolName) + return errors.Errorf("could not zpool export (got 'pool is busy'): %s", PlatformTestPoolName) } - err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName) + err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", PlatformTestPoolName) if err == nil { break } @@ -113,17 +78,13 @@ func (p *Zpool) Destroy(ctx context.Context, e Execer) error { continue } if err != nil { - return errors.Wrapf(err, "export pool %q", p.args.PoolName) + return errors.Wrapf(err, "export pool %q", PlatformTestPoolName) } } - if err := os.Remove(p.args.ImagePath); err != nil { + if err := os.Remove(PlatformTestImagePath); err != nil { return errors.Wrapf(err, "remove pool image") } - if err := os.RemoveAll(p.args.Mountpoint); err != nil { - return errors.Wrapf(err, "remove mountpoint dir %q", p.args.Mountpoint) - } - return nil }