move implementation to internal/ directory (#828)
This commit is contained in:
committed by
GitHub
parent
b9b9ad10cf
commit
908807bd59
@@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/config"
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging"
|
||||
"github.com/zrepl/zrepl/internal/logger"
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/platformtest/tests"
|
||||
)
|
||||
|
||||
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() {
|
||||
|
||||
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.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.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
|
||||
flag.Parse()
|
||||
|
||||
if err := HarnessRun(args); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var exitWithErr = fmt.Errorf("exit with error")
|
||||
|
||||
type HarnessArgs struct {
|
||||
CreateArgs platformtest.ZpoolCreateArgs
|
||||
StopAndKeepPoolOnFail bool
|
||||
Run string
|
||||
}
|
||||
|
||||
type invocation struct {
|
||||
runFunc tests.Case
|
||||
idstring string
|
||||
result *testCaseResult
|
||||
children map[string]*invocation
|
||||
}
|
||||
|
||||
func newInvocation(runFunc tests.Case, id string) *invocation {
|
||||
return &invocation{
|
||||
runFunc: runFunc,
|
||||
idstring: id,
|
||||
children: make(map[string]*invocation),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *invocation) String() string {
|
||||
idsuffix := ""
|
||||
if i.idstring != "" {
|
||||
idsuffix = fmt.Sprintf(": %s", i.idstring)
|
||||
}
|
||||
return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
|
||||
}
|
||||
|
||||
func (i *invocation) RegisterChild(c *invocation) error {
|
||||
if c.idstring == "" {
|
||||
return fmt.Errorf("child must have id string")
|
||||
}
|
||||
if oc := i.children[c.idstring]; oc != nil {
|
||||
return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
|
||||
}
|
||||
i.children[c.idstring] = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func HarnessRun(args HarnessArgs) error {
|
||||
|
||||
runRE := regexp.MustCompile(args.Run)
|
||||
|
||||
outlets := logger.NewOutlets()
|
||||
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
|
||||
LoggingOutletCommon: config.LoggingOutletCommon{
|
||||
Level: "debug",
|
||||
Format: "human",
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
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))
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
testQueue := list.New()
|
||||
for _, c := range tests.Cases {
|
||||
if runRE.MatchString(c.String()) {
|
||||
testQueue.PushBack(newInvocation(c, ""))
|
||||
}
|
||||
}
|
||||
|
||||
completedTests := list.New()
|
||||
|
||||
for testQueue.Len() > 0 {
|
||||
inv := testQueue.Remove(testQueue.Front()).(*invocation)
|
||||
|
||||
bold.Printf("BEGIN TEST CASE %s\n", inv)
|
||||
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "create test pool"))
|
||||
}
|
||||
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
|
||||
stinv := newInvocation(stf, id)
|
||||
err := inv.RegisterChild(stinv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bold.Printf(" QUEUING SUBTEST %q\n", id)
|
||||
testQueue.PushFront(stinv)
|
||||
},
|
||||
}
|
||||
|
||||
res := runTestCase(ctx, ex, inv.runFunc)
|
||||
inv.result = res
|
||||
if res.failed {
|
||||
fmt.Printf("%+v\n", res.failedStack) // print with stack trace
|
||||
}
|
||||
|
||||
if res.failed && args.StopAndKeepPoolOnFail {
|
||||
boldRed.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
|
||||
return exitWithErr
|
||||
}
|
||||
|
||||
if err := pool.Destroy(ctx, ex); err != nil {
|
||||
panic(fmt.Sprintf("error destroying test pool: %s", err))
|
||||
}
|
||||
|
||||
completedTests.PushBack(inv)
|
||||
|
||||
if res.failed {
|
||||
boldRed.Printf("TEST FAILED\n")
|
||||
} else if res.skipped {
|
||||
bold.Printf("TEST SKIPPED\n")
|
||||
} else if res.succeeded {
|
||||
boldGreen.Printf("TEST PASSED\n")
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
var summary struct {
|
||||
succ, fail, skip []*invocation
|
||||
}
|
||||
for completedTests.Len() > 0 {
|
||||
inv := completedTests.Remove(completedTests.Front()).(*invocation)
|
||||
var bucket *[]*invocation
|
||||
if inv.result.failed {
|
||||
bucket = &summary.fail
|
||||
} else if inv.result.skipped {
|
||||
bucket = &summary.skip
|
||||
} else if inv.result.succeeded {
|
||||
bucket = &summary.succ
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
*bucket = append(*bucket, inv)
|
||||
}
|
||||
printBucket := func(bucketName string, c *color.Color, bucket []*invocation) {
|
||||
c.Printf("%s:", bucketName)
|
||||
if len(bucket) == 0 {
|
||||
fmt.Printf(" []\n")
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, inv := range bucket {
|
||||
fmt.Printf(" %s\n", inv)
|
||||
}
|
||||
}
|
||||
printBucket("PASSING TESTS", boldGreen, summary.succ)
|
||||
printBucket("SKIPPED TESTS", bold, summary.skip)
|
||||
printBucket("FAILED TESTS", boldRed, summary.fail)
|
||||
|
||||
if len(summary.fail) > 0 {
|
||||
return errors.New("at least one test failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testCaseResult struct {
|
||||
// oneof
|
||||
failed, skipped, succeeded bool
|
||||
|
||||
failedStack error // has stack inside, valid if failed=true
|
||||
}
|
||||
|
||||
func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {
|
||||
|
||||
// run case
|
||||
var panicked = false
|
||||
var panicValue interface{} = nil
|
||||
var panicStack error
|
||||
func() {
|
||||
defer func() {
|
||||
if item := recover(); item != nil {
|
||||
panicValue = item
|
||||
panicked = true
|
||||
panicStack = errors.Errorf("panic while running test: %v", panicValue)
|
||||
}
|
||||
}()
|
||||
c(ctx)
|
||||
}()
|
||||
|
||||
if panicked {
|
||||
switch panicValue {
|
||||
case platformtest.SkipNowSentinel:
|
||||
return &testCaseResult{skipped: true}
|
||||
case platformtest.FailNowSentinel:
|
||||
return &testCaseResult{failed: true, failedStack: panicStack}
|
||||
default:
|
||||
return &testCaseResult{failed: true, failedStack: panicStack}
|
||||
}
|
||||
} else {
|
||||
return &testCaseResult{succeeded: true}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/openSUSE/umoci/blob/v0.2.1/cmd/umoci/main_test.go
|
||||
//
|
||||
/* 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
|
||||
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) {
|
||||
fmt.Println("incoming args: ", os.Args)
|
||||
|
||||
var (
|
||||
args []string
|
||||
run bool
|
||||
startCaptureArgs bool
|
||||
)
|
||||
|
||||
for i, arg := range os.Args {
|
||||
switch {
|
||||
case arg == "__DEVEL--i-heard-you-like-tests":
|
||||
run = true
|
||||
startCaptureArgs = true
|
||||
case strings.HasPrefix(arg, "-test"):
|
||||
case strings.HasPrefix(arg, "__DEVEL"):
|
||||
case i == 0:
|
||||
args = append(args, arg)
|
||||
case startCaptureArgs:
|
||||
args = append(args, arg)
|
||||
}
|
||||
}
|
||||
os.Args = args
|
||||
fmt.Println("using args: ", os.Args)
|
||||
|
||||
if run {
|
||||
main()
|
||||
}
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/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[@]}"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/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[@]}"
|
||||
@@ -0,0 +1,46 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
context.Context
|
||||
RootDataset string
|
||||
// Use this callback from a top-level test case to queue the
|
||||
// execution of sub-tests after this test case is complete.
|
||||
//
|
||||
// Note that the testing harness executes the subtest
|
||||
// _after_ the current top-level test. Hence, the subtest
|
||||
// cannot use any ZFS state of the top-level test.
|
||||
QueueSubtest func(id string, stf func(*Context))
|
||||
}
|
||||
|
||||
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
|
||||
|
||||
var SkipNowSentinel = fmt.Errorf("platformtest: SkipNow called on context")
|
||||
|
||||
var _ assert.TestingT = (*Context)(nil)
|
||||
var _ require.TestingT = (*Context)(nil)
|
||||
|
||||
func (c *Context) Logf(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
GetLog(c).Info(msg)
|
||||
}
|
||||
|
||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||
GetLog(c).Printf(format, args...)
|
||||
c.FailNow()
|
||||
}
|
||||
|
||||
func (c *Context) FailNow() {
|
||||
panic(FailNowSentinel)
|
||||
}
|
||||
|
||||
func (c *Context) SkipNow() {
|
||||
panic(SkipNowSentinel)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/util/circlog"
|
||||
)
|
||||
|
||||
type ex struct {
|
||||
log Logger
|
||||
}
|
||||
|
||||
func NewEx(log Logger) Execer {
|
||||
return &ex{log}
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(true, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error {
|
||||
return e.runNoOutput(false, ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e *ex) runNoOutput(expectSuccess bool, ctx context.Context, cmd string, args ...string) error {
|
||||
log := e.log.WithField("command", fmt.Sprintf("%q %q", cmd, args))
|
||||
log.Debug("begin executing")
|
||||
defer log.Debug("done executing")
|
||||
ecmd := exec.CommandContext(ctx, cmd, args...)
|
||||
buf, _ := circlog.NewCircularLog(32 << 10)
|
||||
ecmd.Stdout, ecmd.Stderr = buf, buf
|
||||
err := ecmd.Run()
|
||||
log.WithField("output", buf.String()).Debug("command output")
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
if expectSuccess && err != nil {
|
||||
return fmt.Errorf("expecting no error, got error: %s\n%s", err, buf.String())
|
||||
} else if !expectSuccess && err == nil {
|
||||
return fmt.Errorf("expecting error, got no error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging"
|
||||
"github.com/zrepl/zrepl/internal/logger"
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
func GetLog(ctx context.Context) Logger {
|
||||
return logging.GetLogger(ctx, logging.SubsysPlatformtest)
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Execer interface {
|
||||
RunExpectSuccessNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
RunExpectFailureNoOutput(ctx context.Context, cmd string, args ...string) error
|
||||
}
|
||||
|
||||
type Stmt interface {
|
||||
Run(context context.Context, e Execer) error
|
||||
}
|
||||
|
||||
type Op string
|
||||
|
||||
const (
|
||||
Comment Op = "#"
|
||||
AssertExists Op = "!E"
|
||||
AssertNotExists Op = "!N"
|
||||
Add Op = "+"
|
||||
Del Op = "-"
|
||||
RunCmd Op = "R"
|
||||
DestroyRoot Op = "DESTROYROOT"
|
||||
CreateRoot Op = "CREATEROOT"
|
||||
)
|
||||
|
||||
type DestroyRootOp struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *DestroyRootOp) Run(ctx context.Context, e Execer) error {
|
||||
// early-exit if it doesn't exist
|
||||
if err := e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path); err != nil {
|
||||
GetLog(ctx).WithField("root_ds", o.Path).Info("assume root ds doesn't exist")
|
||||
return nil
|
||||
}
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", "-r", o.Path)
|
||||
}
|
||||
|
||||
type FSOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
Encrypted bool // only for Op=Add
|
||||
}
|
||||
|
||||
func (o *FSOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
opts := []string{"create"}
|
||||
if o.Encrypted {
|
||||
const passphraseFilePath = "/tmp/zreplplatformtest.encryption.passphrase"
|
||||
const passphrase = "foobar2342"
|
||||
err := os.WriteFile(passphraseFilePath, []byte(passphrase), 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
opts = append(opts,
|
||||
"-o", "encryption=on",
|
||||
"-o", "keylocation=file:///"+passphraseFilePath,
|
||||
"-o", "keyformat=passphrase",
|
||||
)
|
||||
}
|
||||
opts = append(opts, o.Path)
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", opts...)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type SnapOp struct {
|
||||
Op Op
|
||||
Path string
|
||||
}
|
||||
|
||||
func (o *SnapOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case AssertExists:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case AssertNotExists:
|
||||
return e.RunExpectFailureNoOutput(ctx, "zfs", "get", "-H", "name", o.Path)
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "snapshot", o.Path)
|
||||
case Del:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Path)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type BookmarkOp struct {
|
||||
Op Op
|
||||
Existing string
|
||||
Bookmark string
|
||||
}
|
||||
|
||||
func (o *BookmarkOp) Run(ctx context.Context, e Execer) error {
|
||||
switch o.Op {
|
||||
case Add:
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "bookmark", o.Existing, o.Bookmark)
|
||||
case Del:
|
||||
if o.Existing != "" {
|
||||
panic("existing must be empty for destroy, got " + o.Existing)
|
||||
}
|
||||
return e.RunExpectSuccessNoOutput(ctx, "zfs", "destroy", o.Bookmark)
|
||||
default:
|
||||
panic(o.Op)
|
||||
}
|
||||
}
|
||||
|
||||
type RunOp struct {
|
||||
RootDS string
|
||||
Script string
|
||||
}
|
||||
|
||||
func (o *RunOp) Run(ctx context.Context, e Execer) error {
|
||||
cmd := exec.CommandContext(ctx, "/usr/bin/env", "bash", "-c", o.Script)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOTDS=%s", o.RootDS))
|
||||
log := GetLog(ctx).WithField("script", o.Script)
|
||||
log.Info("start script")
|
||||
defer log.Info("script done")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if _, ok := err.(*exec.ExitError); err != nil && !ok {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("script output:\n%s", output)
|
||||
return err
|
||||
}
|
||||
|
||||
type LineError struct {
|
||||
Line string
|
||||
What string
|
||||
}
|
||||
|
||||
func (e LineError) Error() string {
|
||||
return fmt.Sprintf("%q: %s", e.Line, e.What)
|
||||
}
|
||||
|
||||
type RunKind int
|
||||
|
||||
const (
|
||||
PanicErr RunKind = 1 << iota
|
||||
RunAll
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, rk RunKind, rootds string, stmtsStr string) {
|
||||
stmt, err := parseSequence(rootds, stmtsStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
execer := NewEx(GetLog(ctx))
|
||||
for _, s := range stmt {
|
||||
err := s.Run(ctx, execer)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if rk == PanicErr {
|
||||
panic(err)
|
||||
} else if rk == RunAll {
|
||||
continue
|
||||
} else {
|
||||
panic(rk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNoSpace(r rune) bool {
|
||||
return !unicode.IsSpace(r)
|
||||
}
|
||||
|
||||
func splitQuotedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
begin := bytes.IndexFunc(data, isNoSpace)
|
||||
if begin == -1 {
|
||||
return len(data), nil, nil
|
||||
}
|
||||
if data[begin] == '"' {
|
||||
end := begin + 1
|
||||
for end < len(data) {
|
||||
endCandidate := bytes.Index(data[end:], []byte(`"`))
|
||||
if endCandidate == -1 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
end += endCandidate
|
||||
if data[end-1] != '\\' {
|
||||
// unescaped quote, end of this string
|
||||
// remove backslash-escapes
|
||||
withBackslash := data[begin+1 : end]
|
||||
withoutBackslash := bytes.Replace(withBackslash, []byte("\\\""), []byte("\""), -1)
|
||||
return end + 1, withoutBackslash, nil
|
||||
} else {
|
||||
// continue to next quote
|
||||
end += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endOffset := bytes.IndexFunc(data[begin:], unicode.IsSpace)
|
||||
var end int
|
||||
if endOffset == -1 {
|
||||
if !atEOF {
|
||||
return 0, nil, nil
|
||||
} else {
|
||||
end = len(data)
|
||||
}
|
||||
} else {
|
||||
end = begin + endOffset
|
||||
}
|
||||
return end, data[begin:end], nil
|
||||
}
|
||||
return 0, nil, fmt.Errorf("unexpected")
|
||||
}
|
||||
|
||||
func parseSequence(rootds, stmtsStr string) (stmts []Stmt, err error) {
|
||||
scan := bufio.NewScanner(strings.NewReader(stmtsStr))
|
||||
nextLine:
|
||||
for scan.Scan() {
|
||||
if len(bytes.TrimSpace(scan.Bytes())) == 0 {
|
||||
continue
|
||||
}
|
||||
comps := bufio.NewScanner(bytes.NewReader(scan.Bytes()))
|
||||
comps.Split(splitQuotedWords)
|
||||
|
||||
expectMoreTokens := func() error {
|
||||
if !comps.Scan() {
|
||||
return &LineError{scan.Text(), "unexpected EOL"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Op
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var op Op
|
||||
switch comps.Text() {
|
||||
|
||||
case string(RunCmd):
|
||||
script := strings.TrimPrefix(strings.TrimSpace(scan.Text()), string(RunCmd))
|
||||
stmts = append(stmts, &RunOp{RootDS: rootds, Script: script})
|
||||
continue nextLine
|
||||
|
||||
case string(DestroyRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
stmts = append(stmts, &DestroyRootOp{rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(CreateRoot):
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{Op: Add, Path: rootds})
|
||||
continue nextLine
|
||||
|
||||
case string(Add):
|
||||
op = Add
|
||||
case string(Del):
|
||||
op = Del
|
||||
case string(AssertExists):
|
||||
op = AssertExists
|
||||
case string(AssertNotExists):
|
||||
op = AssertNotExists
|
||||
case string(Comment):
|
||||
op = Comment
|
||||
continue
|
||||
default:
|
||||
return nil, &LineError{scan.Text(), fmt.Sprintf("invalid op %q", comps.Text())}
|
||||
}
|
||||
|
||||
// FS / SNAP / BOOKMARK
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.ContainsAny(comps.Text(), "@") {
|
||||
stmts = append(stmts, &SnapOp{Op: op, Path: fmt.Sprintf("%s/%s", rootds, comps.Text())})
|
||||
} else if strings.ContainsAny(comps.Text(), "#") {
|
||||
bookmark := fmt.Sprintf("%s/%s", rootds, comps.Text())
|
||||
if err := expectMoreTokens(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing := fmt.Sprintf("%s/%s", rootds, comps.Text())
|
||||
stmts = append(stmts, &BookmarkOp{Op: op, Existing: existing, Bookmark: bookmark})
|
||||
} else {
|
||||
// FS
|
||||
fs := comps.Text()
|
||||
var encrypted bool = false
|
||||
if op == Add {
|
||||
if comps.Scan() {
|
||||
t := comps.Text()
|
||||
switch t {
|
||||
case "encrypted":
|
||||
encrypted = true
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected token %q", t))
|
||||
}
|
||||
}
|
||||
}
|
||||
stmts = append(stmts, &FSOp{
|
||||
Op: op,
|
||||
Path: fmt.Sprintf("%s/%s", rootds, fs),
|
||||
Encrypted: encrypted,
|
||||
})
|
||||
}
|
||||
|
||||
if comps.Scan() {
|
||||
return nil, &LineError{scan.Text(), "unexpected tokens at EOL"}
|
||||
}
|
||||
}
|
||||
return stmts, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitQuotedWords(t *testing.T) {
|
||||
s := bufio.NewScanner(strings.NewReader(`
|
||||
foo "bar baz" blah "foo \"with single escape" "blah baz" "\"foo" "foo\""
|
||||
`))
|
||||
s.Split(splitQuotedWords)
|
||||
var words []string
|
||||
for s.Scan() {
|
||||
words = append(words, s.Text())
|
||||
}
|
||||
assert.Equal(
|
||||
t,
|
||||
[]string{
|
||||
"foo",
|
||||
"bar baz",
|
||||
"blah",
|
||||
"foo \"with single escape",
|
||||
"blah baz",
|
||||
"\"foo",
|
||||
"foo\"",
|
||||
},
|
||||
words)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package platformtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// export pool if it already exists (idempotence)
|
||||
if _, err := zfs.ZFSGetRawAnySource(ctx, args.PoolName, []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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, 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")
|
||||
}
|
||||
image.Close()
|
||||
|
||||
// create the pool
|
||||
err = e.RunExpectSuccessNoOutput(ctx, "zpool", "create", "-f",
|
||||
"-O", fmt.Sprintf("mountpoint=%s", args.Mountpoint),
|
||||
args.PoolName, args.ImagePath,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "zpool create")
|
||||
}
|
||||
|
||||
return &Zpool{args}, nil
|
||||
}
|
||||
|
||||
func (p *Zpool) Name() string { return p.args.PoolName }
|
||||
|
||||
func (p *Zpool) Destroy(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)
|
||||
}
|
||||
err := e.RunExpectSuccessNoOutput(ctx, "zpool", "export", p.args.PoolName)
|
||||
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", p.args.PoolName)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Remove(p.args.ImagePath); 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
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func BatchDestroy(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
+ "foo bar@2"
|
||||
+ "foo bar@3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
`)
|
||||
|
||||
reqs := []*zfs.DestroySnapOp{
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "3",
|
||||
},
|
||||
&zfs.DestroySnapOp{
|
||||
ErrOut: new(error),
|
||||
Filesystem: fmt.Sprintf("%s/foo bar", ctx.RootDataset),
|
||||
Name: "2",
|
||||
},
|
||||
}
|
||||
zfs.ZFSDestroyFilesystemVersions(ctx, reqs)
|
||||
if *reqs[0].ErrOut != nil {
|
||||
panic("expecting no error")
|
||||
}
|
||||
err := (*reqs[1].ErrOut).Error()
|
||||
if !strings.Contains(err, fmt.Sprintf("%s/foo bar@2", ctx.RootDataset)) {
|
||||
panic(fmt.Sprintf("expecting error about being unable to destroy @2: %T\n%s", err, err))
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
!N "foo bar@3"
|
||||
!E "foo bar@1"
|
||||
!E "foo bar@2"
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@2"
|
||||
- "foo bar@2"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
`)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type platformtestFuncDeclFinder struct {
|
||||
testFuncs []*ast.FuncDecl
|
||||
}
|
||||
|
||||
func isPlatformtestFunc(n *ast.FuncDecl) bool {
|
||||
if !n.Name.IsExported() {
|
||||
return false
|
||||
}
|
||||
if n.Recv != nil {
|
||||
return false
|
||||
}
|
||||
if n.Type.Results.NumFields() != 0 {
|
||||
return false
|
||||
}
|
||||
if n.Type.Params.NumFields() != 1 {
|
||||
return false
|
||||
}
|
||||
se, ok := n.Type.Params.List[0].Type.(*ast.StarExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
sel, ok := se.X.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
x, ok := sel.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if x.Name != "platformtest" || sel.Sel.Name != "Context" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *platformtestFuncDeclFinder) Visit(n2 ast.Node) ast.Visitor {
|
||||
switch n := n2.(type) {
|
||||
case *ast.File:
|
||||
return e
|
||||
case *ast.FuncDecl:
|
||||
if isPlatformtestFunc(n) {
|
||||
e.testFuncs = append(e.testFuncs, n)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// TODO safeguards that prevent us from deleting non-generated generated_cases.go
|
||||
os.Remove("generated_cases.go")
|
||||
// (no error handling to easily cover the case where the file doesn't exist)
|
||||
|
||||
pkgs, err := packages.Load(
|
||||
&packages.Config{
|
||||
Mode: packages.NeedFiles,
|
||||
Tests: false,
|
||||
},
|
||||
os.Args[1],
|
||||
)
|
||||
check(err)
|
||||
|
||||
if len(pkgs) != 1 {
|
||||
panic(pkgs)
|
||||
}
|
||||
|
||||
p := pkgs[0]
|
||||
|
||||
var tests []*ast.FuncDecl
|
||||
|
||||
for _, f := range p.GoFiles {
|
||||
s := token.NewFileSet()
|
||||
a, err := parser.ParseFile(s, f, nil, parser.AllErrors)
|
||||
check(err)
|
||||
finder := &platformtestFuncDeclFinder{}
|
||||
ast.Walk(finder, a)
|
||||
tests = append(tests, finder.testFuncs...)
|
||||
}
|
||||
|
||||
sort.Slice(tests, func(i, j int) bool {
|
||||
return strings.Compare(tests[i].Name.Name, tests[j].Name.Name) < 0
|
||||
})
|
||||
|
||||
{
|
||||
casesTemplate := `
|
||||
// Code generated by zrepl tooling; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
var Cases = []Case {
|
||||
{{- range . -}}
|
||||
{{ .Name }},
|
||||
{{ end -}}
|
||||
}
|
||||
|
||||
`
|
||||
t, err := template.New("CaseFunc").Parse(casesTemplate)
|
||||
check(err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = t.Execute(&buf, tests)
|
||||
check(err)
|
||||
|
||||
formatted, err := format.Source(buf.Bytes())
|
||||
check(err)
|
||||
|
||||
err = os.WriteFile("generated_cases.go", formatted, 0664)
|
||||
check(err)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Code generated by zrepl tooling; DO NOT EDIT.
|
||||
|
||||
package tests
|
||||
|
||||
var Cases = []Case{BatchDestroy,
|
||||
CreateReplicationCursor,
|
||||
GetNonexistent,
|
||||
HoldsWork,
|
||||
IdempotentBookmark,
|
||||
IdempotentDestroy,
|
||||
IdempotentHold,
|
||||
ListFilesystemVersionsFilesystemNotExist,
|
||||
ListFilesystemVersionsTypeFilteringAndPrefix,
|
||||
ListFilesystemVersionsUserrefs,
|
||||
ListFilesystemVersionsZeroExistIsNotAnError,
|
||||
ListFilesystemsNoFilter,
|
||||
ReceiveForceIntoEncryptedErr,
|
||||
ReceiveForceRollbackWorksUnencrypted,
|
||||
ReplicationFailingInitialParentProhibitsChildReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithCacheOnSecondReplication,
|
||||
ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication,
|
||||
ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist,
|
||||
ReplicationIncrementalHandlesFromVersionEqTentativeCursorCorrectly,
|
||||
ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed,
|
||||
ReplicationInitialAll,
|
||||
ReplicationInitialFail,
|
||||
ReplicationInitialMostRecent,
|
||||
ReplicationIsResumableFullSend__both_GuaranteeResumability,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication,
|
||||
ReplicationOfPlaceholderFilesystemsInChainedReplicationScenario,
|
||||
ReplicationPlaceholderEncryption__EncryptOnReceiverUseCase__WorksIfConfiguredWithInherit,
|
||||
ReplicationPlaceholderEncryption__UnspecifiedIsOkForClientIdentityPlaceholder,
|
||||
ReplicationPlaceholderEncryption__UnspecifiedLeadsToFailureAtRuntimeWhenCreatingPlaceholders,
|
||||
ReplicationPropertyReplicationWorks,
|
||||
ReplicationReceiverErrorWhileStillSending,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
||||
ResumableRecvAndTokenHandling,
|
||||
ResumeTokenParsing,
|
||||
SendArgsValidationEE_EncryptionAndRaw,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||
SendStreamCloseAfterBlockedOnPipeWrite,
|
||||
SendStreamCloseAfterEOFRead,
|
||||
SendStreamMultipleCloseAfterEOF,
|
||||
SendStreamMultipleCloseBeforeEOF,
|
||||
SendStreamNonEOFReadErrorHandling,
|
||||
UndestroyableSnapshotParsing,
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func GetNonexistent(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
`)
|
||||
|
||||
// test raw
|
||||
_, err := zfs.ZFSGetRawAnySource(ctx, fmt.Sprintf("%s/foo bar", ctx.RootDataset), []string{"name"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent filesystem
|
||||
nonexistent := fmt.Sprintf("%s/nonexistent filesystem", ctx.RootDataset)
|
||||
props, err := zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent snapshot
|
||||
nonexistent = fmt.Sprintf("%s/foo bar@non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok = err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// test nonexistent bookmark
|
||||
nonexistent = fmt.Sprintf("%s/foo bar#non existent", ctx.RootDataset)
|
||||
props, err = zfs.ZFSGetRawAnySource(ctx, nonexistent, []string{"name"})
|
||||
if err == nil {
|
||||
panic(props)
|
||||
}
|
||||
dsne, ok = err.(*zfs.DatasetDoesNotExist)
|
||||
if !ok {
|
||||
panic(err)
|
||||
} else if dsne.Path != nonexistent {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/filters"
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/util/limitio"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func sendArgVersion(ctx *platformtest.Context, fs, relName string) zfs.ZFSSendArgVersion {
|
||||
guid, err := zfs.ZFSGetGUID(ctx, fs, relName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return zfs.ZFSSendArgVersion{
|
||||
RelName: relName,
|
||||
GUID: guid,
|
||||
}
|
||||
}
|
||||
|
||||
func fsversion(ctx *platformtest.Context, fs, relname string) zfs.FilesystemVersion {
|
||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, fs+relname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func mustDatasetPath(fs string) *zfs.DatasetPath {
|
||||
p, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func mustSnapshot(ctx *platformtest.Context, snap string) {
|
||||
if err := zfs.EntityNamecheck(snap, zfs.EntityTypeSnapshot); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
comps := strings.Split(snap, "@")
|
||||
if len(comps) != 2 {
|
||||
panic(comps)
|
||||
}
|
||||
err := zfs.ZFSSnapshot(ctx, mustDatasetPath(comps[0]), comps[1], false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetFilesystemVersion(ctx *platformtest.Context, snapOrBookmark string) zfs.FilesystemVersion {
|
||||
v, err := zfs.ZFSGetFilesystemVersion(ctx, snapOrBookmark)
|
||||
check(err)
|
||||
return v
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var dummyDataRand = rand.New(rand.NewSource(99))
|
||||
|
||||
func writeDummyData(path string, numBytes int64) {
|
||||
r := io.LimitReader(dummyDataRand, numBytes)
|
||||
d, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||
check(err)
|
||||
defer d.Close()
|
||||
_, err = io.Copy(d, r)
|
||||
check(err)
|
||||
}
|
||||
|
||||
type dummySnapshotSituation struct {
|
||||
sendFS string
|
||||
dummyDataLen int64
|
||||
snapA *zfs.ZFSSendArgVersion
|
||||
snapB *zfs.ZFSSendArgVersion
|
||||
}
|
||||
|
||||
type resumeSituation struct {
|
||||
sendArgs zfs.ZFSSendArgsUnvalidated
|
||||
recvOpts zfs.RecvOptions
|
||||
sendErr, recvErr error
|
||||
recvErrDecoded *zfs.RecvFailedWithResumeTokenErr
|
||||
}
|
||||
|
||||
func makeDummyDataSnapshots(ctx *platformtest.Context, sendFS string) (situation dummySnapshotSituation) {
|
||||
|
||||
situation.sendFS = sendFS
|
||||
sendFSMount, err := zfs.ZFSGetMountpoint(ctx, sendFS)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sendFSMount.Mounted)
|
||||
|
||||
const dummyLen = int64(10 * (1 << 20))
|
||||
situation.dummyDataLen = dummyLen
|
||||
|
||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||
mustSnapshot(ctx, sendFS+"@a snapshot")
|
||||
snapA := sendArgVersion(ctx, sendFS, "@a snapshot")
|
||||
situation.snapA = &snapA
|
||||
|
||||
writeDummyData(path.Join(sendFSMount.Mountpoint, "dummy_data"), dummyLen)
|
||||
mustSnapshot(ctx, sendFS+"@b snapshot")
|
||||
snapB := sendArgVersion(ctx, sendFS, "@b snapshot")
|
||||
situation.snapB = &snapB
|
||||
|
||||
return situation
|
||||
}
|
||||
|
||||
func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation, recvFS string, sendArgs zfs.ZFSSendArgsUnvalidated, recvOptions zfs.RecvOptions) *resumeSituation {
|
||||
|
||||
situation := &resumeSituation{}
|
||||
|
||||
situation.sendArgs = sendArgs
|
||||
situation.recvOpts = recvOptions
|
||||
require.True(ctx, recvOptions.SavePartialRecvState, "this method would be pointless otherwise")
|
||||
require.Equal(ctx, sendArgs.FS, src.sendFS)
|
||||
sendArgsValidated, err := sendArgs.Validate(ctx)
|
||||
situation.sendErr = err
|
||||
if err != nil {
|
||||
return situation
|
||||
}
|
||||
|
||||
copier, err := zfs.ZFSSend(ctx, sendArgsValidated)
|
||||
situation.sendErr = err
|
||||
if err != nil {
|
||||
return situation
|
||||
}
|
||||
|
||||
limitedCopier := limitio.ReadCloser(copier, src.dummyDataLen/2)
|
||||
defer limitedCopier.Close()
|
||||
|
||||
require.NotNil(ctx, sendArgs.To)
|
||||
err = zfs.ZFSRecv(ctx, recvFS, sendArgs.To, limitedCopier, recvOptions)
|
||||
situation.recvErr = err
|
||||
ctx.Logf("zfs recv exit with %T %s", err, err)
|
||||
|
||||
require.NotNil(ctx, err)
|
||||
resumeErr, ok := err.(*zfs.RecvFailedWithResumeTokenErr)
|
||||
require.True(ctx, ok)
|
||||
situation.recvErrDecoded = resumeErr
|
||||
|
||||
return situation
|
||||
}
|
||||
|
||||
func versionRelnamesSorted(versions []zfs.FilesystemVersion) []string {
|
||||
var vstrs []string
|
||||
for _, v := range versions {
|
||||
vstrs = append(vstrs, v.RelName())
|
||||
}
|
||||
sort.Strings(vstrs)
|
||||
return vstrs
|
||||
}
|
||||
|
||||
func datasetToStringSortedTrimPrefix(prefix *zfs.DatasetPath, paths []*zfs.DatasetPath) []string {
|
||||
var pstrs []string
|
||||
for _, p := range paths {
|
||||
trimmed := p.Copy()
|
||||
trimmed.TrimPrefix(prefix)
|
||||
if trimmed.Length() == 0 {
|
||||
continue
|
||||
}
|
||||
pstrs = append(pstrs, trimmed.ToString())
|
||||
}
|
||||
sort.Strings(pstrs)
|
||||
return pstrs
|
||||
}
|
||||
|
||||
func mustAddToSFilter(ctx *platformtest.Context, f *filters.DatasetMapFilter, fs string) {
|
||||
err := f.Add(fs, "ok")
|
||||
require.NoError(ctx, err)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func HoldsWork(ctx *platformtest.Context) {
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@snap name"
|
||||
`)
|
||||
|
||||
fs := path.Join(ctx.RootDataset, "foo bar")
|
||||
|
||||
err := zfs.ZFSHold(ctx, fs, fsversion(ctx, fs, "@snap name"), "tag 1")
|
||||
require.NoError(ctx, err)
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, fsversion(ctx, fs, "@snap name"), "tag 2")
|
||||
require.NoError(ctx, err)
|
||||
|
||||
holds, err := zfs.ZFSHolds(ctx, fs, "snap name")
|
||||
require.NoError(ctx, err)
|
||||
require.Len(ctx, holds, 2)
|
||||
require.Contains(ctx, holds, "tag 1")
|
||||
require.Contains(ctx, holds, "tag 2")
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func IdempotentBookmark(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
+ "foo bar@another snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
|
||||
asnap := fsversion(ctx, fs, "@a snap")
|
||||
anotherSnap := fsversion(ctx, fs, "@another snap")
|
||||
|
||||
aBookmark, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// do it again, should be idempotent
|
||||
aBookmarkIdemp, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(ctx, aBookmark, aBookmarkIdemp)
|
||||
|
||||
// should fail for another snapshot
|
||||
_, err = zfs.ZFSBookmark(ctx, fs, anotherSnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, ok := err.(*zfs.BookmarkExists); !ok {
|
||||
panic(fmt.Sprintf("has type %T", err))
|
||||
}
|
||||
|
||||
// destroy the snapshot
|
||||
if err := zfs.ZFSDestroy(ctx, fmt.Sprintf("%s@a snap", fs)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// do it again, should fail with special error type
|
||||
_, err = zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
||||
panic(fmt.Sprintf("has type %T", err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func IdempotentDestroy(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
asnap := fsversion(ctx, fs, "@a snap")
|
||||
_, err := zfs.ZFSBookmark(ctx, fs, asnap, "a bookmark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description, path string
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{"snapshot", fmt.Sprintf("%s@a snap", fs)},
|
||||
{"bookmark", fmt.Sprintf("%s#a bookmark", fs)},
|
||||
{"filesystem", fs},
|
||||
}
|
||||
|
||||
for i := range cases {
|
||||
func() {
|
||||
c := cases[i]
|
||||
|
||||
log.Printf("SUBBEGIN testing idempotent destroy %q for path %q", c.description, c.path)
|
||||
|
||||
log.Println("destroy existing")
|
||||
err = zfs.ZFSDestroy(ctx, c.path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("destroy again, non-idempotently, must error")
|
||||
err = zfs.ZFSDestroy(ctx, c.path)
|
||||
if _, ok := err.(*zfs.DatasetDoesNotExist); !ok {
|
||||
panic(fmt.Sprintf("%T: %s", err, err))
|
||||
}
|
||||
log.Println("destroy again, idempotently, must not error")
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, c.path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Println("SUBEND")
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
// also test idempotent destroy for cases where the parent dataset does not exist
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar@nonexistent snapshot", ctx.RootDataset))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = zfs.ZFSDestroyIdempotent(ctx, fmt.Sprintf("%s/not foo bar#nonexistent bookmark", ctx.RootDataset))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func IdempotentHold(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1"
|
||||
`)
|
||||
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@1"
|
||||
- "foo bar@1"
|
||||
- "foo bar"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
v1 := fsversion(ctx, fs, "@1")
|
||||
|
||||
tag := "zrepl_platformtest"
|
||||
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = zfs.ZFSHold(ctx, fs, v1, tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@foo 1"
|
||||
+ "foo bar#foo 1" "foo bar@foo 1"
|
||||
+ "foo bar#bookfoo 1" "foo bar@foo 1"
|
||||
+ "foo bar@foo 2"
|
||||
+ "foo bar#foo 2" "foo bar@foo 2"
|
||||
+ "foo bar#bookfoo 2" "foo bar@foo 2"
|
||||
+ "foo bar@blup 1"
|
||||
+ "foo bar#blup 1" "foo bar@blup 1"
|
||||
+ "foo bar@ foo with leading whitespace"
|
||||
|
||||
# repeat the whole thing for a child dataset to make sure we disable recursion
|
||||
|
||||
+ "foo bar/child dataset"
|
||||
+ "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset#foo 1" "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset#bookfoo 1" "foo bar/child dataset@foo 1"
|
||||
+ "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset#foo 2" "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset#bookfoo 2" "foo bar/child dataset@foo 2"
|
||||
+ "foo bar/child dataset@blup 1"
|
||||
+ "foo bar/child dataset#blup 1" "foo bar/child dataset@blup 1"
|
||||
+ "foo bar/child dataset@ foo with leading whitespace"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
// no options := all types
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2",
|
||||
"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2",
|
||||
}, versionRelnamesSorted(vs))
|
||||
|
||||
// just snapshots
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Snapshots,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
// just bookmarks
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Bookmarks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
// just with prefix foo
|
||||
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||
ShortnamePrefix: "foo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"#foo 1", "#foo 2", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
||||
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.Empty(t, vs)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsFilesystemNotExist(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
`)
|
||||
|
||||
nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset)
|
||||
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{})
|
||||
require.Empty(t, vs)
|
||||
require.Error(t, err)
|
||||
t.Logf("err = %T\n%s", err, err)
|
||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, nonexistentFS, dsne.Path)
|
||||
}
|
||||
|
||||
func ListFilesystemVersionsUserrefs(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@snap 1"
|
||||
+ "foo bar#snap 1" "foo bar@snap 1"
|
||||
+ "foo bar@snap 2"
|
||||
+ "foo bar#snap 2" "foo bar@snap 2"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 2"
|
||||
+ "foo bar@snap 3"
|
||||
+ "foo bar#snap 3" "foo bar@snap 3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@snap 3"
|
||||
R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar@snap 3"
|
||||
+ "foo bar@snap 4"
|
||||
+ "foo bar#snap 4" "foo bar@snap 4"
|
||||
|
||||
|
||||
+ "foo bar/child datset"
|
||||
+ "foo bar/child datset@snap 1"
|
||||
+ "foo bar/child datset#snap 1" "foo bar/child datset@snap 1"
|
||||
+ "foo bar/child datset@snap 2"
|
||||
+ "foo bar/child datset#snap 2" "foo bar/child datset@snap 2"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 2"
|
||||
+ "foo bar/child datset@snap 3"
|
||||
+ "foo bar/child datset#snap 3" "foo bar/child datset@snap 3"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar/child datset@snap 3"
|
||||
R zfs hold zrepl_platformtest_second_hold "${ROOTDS}/foo bar/child datset@snap 3"
|
||||
+ "foo bar/child datset@snap 4"
|
||||
+ "foo bar/child datset#snap 4" "foo bar/child datset@snap 4"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||
|
||||
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
type expectation struct {
|
||||
relName string
|
||||
userrefs zfs.OptionUint64
|
||||
}
|
||||
|
||||
expect := []expectation{
|
||||
{"#snap 1", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 2", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 3", zfs.OptionUint64{Valid: false}},
|
||||
{"#snap 4", zfs.OptionUint64{Valid: false}},
|
||||
{"@snap 1", zfs.OptionUint64{Value: 0, Valid: true}},
|
||||
{"@snap 2", zfs.OptionUint64{Value: 1, Valid: true}},
|
||||
{"@snap 3", zfs.OptionUint64{Value: 2, Valid: true}},
|
||||
{"@snap 4", zfs.OptionUint64{Value: 0, Valid: true}},
|
||||
}
|
||||
|
||||
sort.Slice(vs, func(i, j int) bool {
|
||||
return strings.Compare(vs[i].RelName(), vs[j].RelName()) < 0
|
||||
})
|
||||
|
||||
var expectRelNames []string
|
||||
for _, e := range expect {
|
||||
expectRelNames = append(expectRelNames, e.relName)
|
||||
}
|
||||
|
||||
require.Equal(t, expectRelNames, versionRelnamesSorted(vs))
|
||||
|
||||
for i, e := range expect {
|
||||
require.Equal(t, e.relName, vs[i].RelName())
|
||||
require.Equal(t, e.userrefs, vs[i].UserRefs)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func ListFilesystemsNoFilter(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
R zfs create -V 10M "${ROOTDS}/bar baz"
|
||||
+ "foo bar"
|
||||
+ "foo bar/bar blup"
|
||||
+ "foo bar/blah"
|
||||
R zfs create -V 10M "${ROOTDS}/foo bar/blah/a volume"
|
||||
`)
|
||||
|
||||
fss, err := zfs.ZFSListMapping(t, zfs.NoFilter())
|
||||
require.NoError(t, err)
|
||||
var onlyTestPool []*zfs.DatasetPath
|
||||
for _, fs := range fss {
|
||||
if strings.HasPrefix(fs.ToString(), t.RootDataset) {
|
||||
onlyTestPool = append(onlyTestPool, fs)
|
||||
}
|
||||
}
|
||||
onlyTestPoolStr := datasetToStringSortedTrimPrefix(mustDatasetPath(t.RootDataset), onlyTestPool)
|
||||
require.Equal(t, []string{"bar baz", "foo bar", "foo bar/bar blup", "foo bar/blah", "foo bar/blah/a volume"}, onlyTestPoolStr)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func ReceiveForceIntoEncryptedErr(ctx *platformtest.Context) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
require.NoError(ctx, err, "encryption feature test failed")
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar" encrypted
|
||||
+ "sender" encrypted
|
||||
+ "sender@1"
|
||||
`)
|
||||
|
||||
rfs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
sfs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||
sfsSnap1 := sendArgVersion(ctx, sfs, "@1")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sfs,
|
||||
From: nil,
|
||||
To: &sfsSnap1,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: false},
|
||||
ResumeToken: "",
|
||||
},
|
||||
}.Validate(ctx)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
require.NoError(ctx, err)
|
||||
defer sendStream.Close()
|
||||
|
||||
recvOpts := zfs.RecvOptions{
|
||||
RollbackAndForceRecv: true,
|
||||
SavePartialRecvState: false,
|
||||
}
|
||||
err = zfs.ZFSRecv(ctx, rfs, &zfs.ZFSSendArgVersion{RelName: "@1", GUID: sfsSnap1.GUID}, sendStream, recvOpts)
|
||||
require.Error(ctx, err)
|
||||
re, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr)
|
||||
require.True(ctx, ok)
|
||||
require.Contains(ctx, re.Error(), "zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted on")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func ReceiveForceRollbackWorksUnencrypted(ctx *platformtest.Context) {
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@a snap"
|
||||
+ "foo bar@another snap"
|
||||
+ "foo bar@snap3"
|
||||
+ "sender"
|
||||
+ "sender@1"
|
||||
`)
|
||||
|
||||
rfs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||
sfs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||
sfsSnap1 := sendArgVersion(ctx, sfs, "@1")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sfs,
|
||||
From: nil,
|
||||
To: &sfsSnap1,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: false},
|
||||
ResumeToken: "",
|
||||
},
|
||||
}.Validate(ctx)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
require.NoError(ctx, err)
|
||||
defer sendStream.Close()
|
||||
|
||||
recvOpts := zfs.RecvOptions{
|
||||
RollbackAndForceRecv: true,
|
||||
SavePartialRecvState: false,
|
||||
}
|
||||
err = zfs.ZFSRecv(ctx, rfs, &zfs.ZFSSendArgVersion{RelName: "@1", GUID: sfsSnap1.GUID}, sendStream, recvOpts)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
// assert exists on receiver
|
||||
rfsSnap1 := fsversion(ctx, rfs, "@1")
|
||||
// assert it's the only one (rollback and force-recv should be blowing away the other filesystems)
|
||||
rfsVersions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{})
|
||||
require.NoError(ctx, err)
|
||||
assert.Len(ctx, rfsVersions, 1)
|
||||
assert.Equal(ctx, rfsVersions[0], rfsSnap1)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/endpoint"
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func CreateReplicationCursor(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@1 with space" "${ROOTDS}/foo bar#1 with space"
|
||||
+ "foo bar@2 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@2 with space" "${ROOTDS}/foo bar#2 with space"
|
||||
+ "foo bar@3 with space"
|
||||
R zfs bookmark "${ROOTDS}/foo bar@3 with space" "${ROOTDS}/foo bar#3 with space"
|
||||
- "foo bar@3 with space"
|
||||
`)
|
||||
|
||||
jobid := endpoint.MustMakeJobID("zreplplatformtest")
|
||||
|
||||
ds, err := zfs.NewDatasetPath(ctx.RootDataset + "/foo bar")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fs := ds.ToString()
|
||||
|
||||
checkCreateCursor := func(createErr error, c endpoint.Abstraction, references zfs.FilesystemVersion) {
|
||||
assert.NoError(ctx, createErr)
|
||||
expectName, err := endpoint.ReplicationCursorBookmarkName(fs, references.Guid, jobid)
|
||||
assert.NoError(ctx, err)
|
||||
require.Equal(ctx, expectName, c.GetFilesystemVersion().Name)
|
||||
}
|
||||
|
||||
snap := fsversion(ctx, fs, "@1 with space")
|
||||
book := fsversion(ctx, fs, "#1 with space")
|
||||
book3 := fsversion(ctx, fs, "#3 with space")
|
||||
|
||||
// create first cursor
|
||||
cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
checkCreateCursor(err, cursorOfSnap, snap)
|
||||
// check CreateReplicationCursor is idempotent (for snapshot target)
|
||||
cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid)
|
||||
checkCreateCursor(err, cursorOfSnap, snap)
|
||||
// check CreateReplicationCursor is idempotent (for bookmark target of snapshot)
|
||||
cursorOfBook, err := endpoint.CreateReplicationCursor(ctx, fs, book, jobid)
|
||||
checkCreateCursor(err, cursorOfBook, snap)
|
||||
// ... for target = non-cursor bookmark
|
||||
_, err = endpoint.CreateReplicationCursor(ctx, fs, book3, jobid)
|
||||
assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err)
|
||||
// ... for target = replication cursor bookmark to be created
|
||||
cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid)
|
||||
checkCreateCursor(err, cursorOfCursor, cursorOfSnap.GetFilesystemVersion())
|
||||
|
||||
snapProps, err := zfs.ZFSGetFilesystemVersion(ctx, snap.FullPath(fs))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bm, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs, jobid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if bm.CreateTXG != snapProps.CreateTXG {
|
||||
panic(fmt.Sprintf("createtxgs do not match: %v != %v", bm.CreateTXG, snapProps.CreateTXG))
|
||||
}
|
||||
if bm.Guid != snapProps.Guid {
|
||||
panic(fmt.Sprintf("guids do not match: %v != %v", bm.Guid, snapProps.Guid))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func ResumableRecvAndTokenHandling(ctx *platformtest.Context) {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er"
|
||||
`)
|
||||
|
||||
sendFS := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
recvFS := fmt.Sprintf("%s/recv er", ctx.RootDataset)
|
||||
|
||||
supported, err := zfs.ResumeRecvSupported(ctx, mustDatasetPath(sendFS))
|
||||
check(err)
|
||||
|
||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||
|
||||
s := makeResumeSituation(ctx, src, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: false},
|
||||
ResumeToken: "",
|
||||
},
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false, // doesnt' exist yet
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
if !supported {
|
||||
_, ok := s.recvErr.(*zfs.ErrRecvResumeNotSupported)
|
||||
require.True(ctx, ok)
|
||||
|
||||
// we know that support on sendFS implies support on recvFS
|
||||
// => assert that if we don't support resumed recv, the method returns ""
|
||||
tok, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
||||
check(err)
|
||||
require.Equal(ctx, "", tok)
|
||||
|
||||
return // nothing more to test for recv that doesn't support -s
|
||||
}
|
||||
|
||||
getTokenRaw, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(recvFS))
|
||||
check(err)
|
||||
|
||||
require.NotEmpty(ctx, getTokenRaw)
|
||||
decodedToken, err := zfs.ParseResumeToken(ctx, getTokenRaw)
|
||||
check(err)
|
||||
|
||||
require.True(ctx, decodedToken.HasToGUID)
|
||||
require.Equal(ctx, s.sendArgs.To.GUID, decodedToken.ToGUID)
|
||||
|
||||
recvErr := s.recvErr.(*zfs.RecvFailedWithResumeTokenErr)
|
||||
require.Equal(ctx, recvErr.ResumeTokenRaw, getTokenRaw)
|
||||
require.Equal(ctx, recvErr.ResumeTokenParsed, decodedToken)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
type resumeTokenTest struct {
|
||||
Msg string
|
||||
Token string
|
||||
ExpectToken *zfs.ResumeToken
|
||||
ExpectError error
|
||||
}
|
||||
|
||||
func (rtt *resumeTokenTest) Test(t *platformtest.Context) {
|
||||
|
||||
resumeSendSupported, err := zfs.ResumeSendSupported(t)
|
||||
if err != nil {
|
||||
t.Errorf("cannot determine whether resume supported: %T %s", err, err)
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := zfs.ParseResumeToken(t, rtt.Token)
|
||||
|
||||
// if decoding is not supported, don't bother with the expectations
|
||||
if !resumeSendSupported {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, zfs.ResumeTokenDecodingNotSupported, err)
|
||||
return
|
||||
}
|
||||
|
||||
if rtt.ExpectError != nil {
|
||||
require.EqualValues(t, rtt.ExpectError, err)
|
||||
return
|
||||
}
|
||||
if rtt.ExpectToken != nil {
|
||||
require.Nil(t, err)
|
||||
require.EqualValues(t, rtt.ExpectToken, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ResumeTokenParsing(ctx *platformtest.Context) {
|
||||
|
||||
// cases generated using resumeTokensGenerate.bash on ZoL 0.8.1
|
||||
cases := []resumeTokenTest{
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/full",
|
||||
Token: "1-b338b54f3-c0-789c636064000310a500c4ec50360710e72765a52697303030419460caa7a515a796806474e0f26c48f2499525a9c540ba42430fabfe92fcf4d2cc140686c88a76d578ae45530c90e439c1f27989b9a90c0c5545a905390539892569f945b940234bf48b8b921d12c1660200c61a1aba",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasToGUID: true,
|
||||
ToGUID: 0x94a20a5f25877859,
|
||||
ToName: "zreplplatformtest/src@a",
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/full_raw",
|
||||
Token: "1-e3f40c323-f8-789c636064000310a500c4ec50360710e72765a52697303030419460caa7a515a796806474e0f26c48f2499525a9c540da454f0fabfe92fcf4d2cc140686c88a76d578ae45530c90e439c1f27989b9a90c0c5545a905390539892569f945b940234bf48b8b921d12c1e6713320dc9f9c9f5b50945a5c9c9f0d119380ba07265f94580e936200004ff12141",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasToGUID: true,
|
||||
ToGUID: 0x94a20a5f25877859,
|
||||
ToName: "zreplplatformtest/src@a",
|
||||
HasCompressOK: true, CompressOK: true,
|
||||
HasRawOk: true, RawOK: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/inc",
|
||||
Token: "1-eadabb296-e8-789c636064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b518483ba8ea61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b09900d43e2191",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasFromGUID: true, FromGUID: 0x94a20a5f25877859,
|
||||
HasToGUID: true, ToGUID: 0xf784e1004f460f7a,
|
||||
ToName: "zreplplatformtest/src@b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "zreplplatformtest/dst/inc_raw",
|
||||
Token: "1-1164f8d409-120-789c636064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b51848f368eb61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b079dc0c087f26e7e71614a51617e76743c424a0ee81c9172596c3a41800dd2c2818",
|
||||
ExpectToken: &zfs.ResumeToken{
|
||||
HasFromGUID: true, FromGUID: 0x94a20a5f25877859,
|
||||
HasToGUID: true, ToGUID: 0xf784e1004f460f7a,
|
||||
ToName: "zreplplatformtest/src@b",
|
||||
HasCompressOK: true, CompressOK: true,
|
||||
HasRawOk: true, RawOK: true,
|
||||
},
|
||||
},
|
||||
|
||||
// manual test csaes
|
||||
{
|
||||
Msg: "corrupted",
|
||||
Token: "1-1164f8d409-120-badf00d064000310a501c49c50360710a715e5e7a69766a63040416445bb6a3cd7a2290a40363b92bafca4acd4e412060626a83a0cf9b4b4e2d412908c0e5c9e0d493ea9b224b51848f368eb61d55f920f714515bf9b3fc3c396ef0648f29c60f9bcc4dc54a07c516a414e414e62495a7e512ed0c812fde2a2648724b079dc0c087f26e7e71614a51617e76743c424a0ee81c9172596c3a41800dd2c2818",
|
||||
ExpectError: zfs.ResumeTokenCorruptError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
ctx.Logf("BEGIN SUBTEST: %s", test.Msg)
|
||||
test.Test(ctx)
|
||||
ctx.Logf("COMPLETE SUBTEST: %s", test.Msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -x
|
||||
|
||||
POOLNAME="zreplplatformtest"
|
||||
POOLIMG="$1"
|
||||
|
||||
if zpool status "$POOLNAME"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -e "$POOLIMG" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fallocate -l 500M "$POOLIMG"
|
||||
zpool create "$POOLNAME" "$POOLIMG"
|
||||
|
||||
SRC_DATASET="$POOLNAME/src"
|
||||
DST_ROOT="$POOLNAME/dst"
|
||||
|
||||
keylocation="$(mktemp /tmp/ZREPL_PLATFORMTEST_GENERATE_TOKENS_KEYFILE_XXX)"
|
||||
echo "foobar123" > "$keylocation"
|
||||
zfs create -o encryption=on -o keylocation="file:///$keylocation" -o keyformat=passphrase "$SRC_DATASET"
|
||||
rm "$keylocation"
|
||||
|
||||
SRC_MOUNT="$(zfs get -H -o value mountpoint "$SRC_DATASET")"
|
||||
test -d "$SRC_MOUNT"
|
||||
|
||||
A="$SRC_DATASET"@a
|
||||
B="$SRC_DATASET"@b
|
||||
dd if=/dev/urandom of="$SRC_MOUNT"/dummy_data bs=1M count=5
|
||||
zfs snapshot "$A"
|
||||
dd if=/dev/urandom of="$SRC_MOUNT"/dummy_data bs=1M count=5
|
||||
zfs snapshot "$B"
|
||||
|
||||
zfs create "$DST_ROOT"
|
||||
|
||||
cutoff="dd bs=1024 count=3K"
|
||||
|
||||
set +e
|
||||
zfs send "$A" | $cutoff | zfs recv -s "$DST_ROOT"/full
|
||||
|
||||
zfs send "$A" | zfs recv "$DST_ROOT"/inc
|
||||
zfs send -i "$A" "$B" | $cutoff | zfs recv -s "$DST_ROOT"/inc
|
||||
|
||||
zfs send -w "$A" | $cutoff | zfs recv -s "$DST_ROOT"/full_raw
|
||||
|
||||
zfs send -w "$A" | zfs recv "$DST_ROOT"/inc_raw
|
||||
zfs send -w -i "$A" "$B" | $cutoff | zfs recv -s "$DST_ROOT"/inc_raw
|
||||
set -e
|
||||
|
||||
TOKENS="$(zfs list -H -o name,receive_resume_token -r "$DST_ROOT")"
|
||||
|
||||
echo "$TOKENS" | awk -e '//{ if ($2 == "-") { } else { system("zfs send -nvt " + $2);} }'
|
||||
|
||||
zpool destroy "$POOLNAME"
|
||||
rm "$POOLIMG"
|
||||
@@ -0,0 +1,438 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/endpoint"
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/replication/logic"
|
||||
"github.com/zrepl/zrepl/internal/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/internal/replication/report"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true(ctx *platformtest.Context) {
|
||||
sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx, true)
|
||||
}
|
||||
|
||||
func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false(ctx *platformtest.Context) {
|
||||
sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx, false)
|
||||
}
|
||||
|
||||
func sendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden_impl(ctx *platformtest.Context, testForEncryptionSupported bool) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
check(err)
|
||||
if supported != testForEncryptionSupported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
noEncryptionCLISupport := !supported
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er"
|
||||
+ "send er@a snap"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
props := mustGetFilesystemVersion(ctx, fs+"@a snap")
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: fs,
|
||||
To: &zfs.ZFSSendArgVersion{
|
||||
RelName: "@a snap",
|
||||
GUID: props.Guid,
|
||||
},
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: true},
|
||||
ResumeToken: "",
|
||||
},
|
||||
}.Validate(ctx)
|
||||
|
||||
var stream *zfs.SendStream
|
||||
if err == nil {
|
||||
stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow
|
||||
if err == nil {
|
||||
defer stream.Close()
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
if noEncryptionCLISupport {
|
||||
require.Error(ctx, err)
|
||||
saverr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok, "%T", err)
|
||||
require.Equal(ctx, zfs.ZFSSendArgsEncryptedSendRequestedButFSUnencrypted, saverr.What)
|
||||
return
|
||||
}
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.True(ctx, validationErr.What == zfs.ZFSSendArgsEncryptedSendRequestedButFSUnencrypted)
|
||||
}
|
||||
|
||||
func SendArgsValidationResumeTokenEncryptionMismatchForbidden(ctx *platformtest.Context) {
|
||||
|
||||
supported, err := zfs.EncryptionCLISupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
supported, err = zfs.ResumeSendSupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er" encrypted
|
||||
`)
|
||||
|
||||
sendFS := fmt.Sprintf("%s/send er", ctx.RootDataset)
|
||||
unencRecvFS := fmt.Sprintf("%s/unenc recv", ctx.RootDataset)
|
||||
encRecvFS := fmt.Sprintf("%s/enc recv", ctx.RootDataset)
|
||||
|
||||
src := makeDummyDataSnapshots(ctx, sendFS)
|
||||
|
||||
unencS := makeResumeSituation(ctx, src, unencRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{Encrypted: &nodefault.Bool{B: false}}, // !
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
encS := makeResumeSituation(ctx, src, encRecvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS,
|
||||
To: src.snapA,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{Encrypted: &nodefault.Bool{B: true}}, // !
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
// threat model: use of a crafted resume token that requests an unencrypted send
|
||||
// but send args require encrypted send
|
||||
{
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = encS.sendArgs
|
||||
maliciousSend.ResumeToken = unencS.recvErrDecoded.ResumeTokenRaw
|
||||
|
||||
_, err := maliciousSend.Validate(ctx)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionNotSet)
|
||||
}
|
||||
|
||||
// threat model: use of a crafted resume token that requests an encrypted send
|
||||
// but send args require unencrypted send
|
||||
{
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = unencS.sendArgs
|
||||
maliciousSend.ResumeToken = encS.recvErrDecoded.ResumeTokenRaw
|
||||
|
||||
_, err := maliciousSend.Validate(ctx)
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchEncryptionSet)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest.Context) {
|
||||
supported, err := zfs.ResumeSendSupported(ctx)
|
||||
check(err)
|
||||
if !supported {
|
||||
ctx.SkipNow()
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "send er1"
|
||||
+ "send er2"
|
||||
`)
|
||||
|
||||
sendFS1 := fmt.Sprintf("%s/send er1", ctx.RootDataset)
|
||||
sendFS2 := fmt.Sprintf("%s/send er2", ctx.RootDataset)
|
||||
recvFS := fmt.Sprintf("%s/unenc recv", ctx.RootDataset)
|
||||
|
||||
src1 := makeDummyDataSnapshots(ctx, sendFS1)
|
||||
src2 := makeDummyDataSnapshots(ctx, sendFS2)
|
||||
|
||||
rs := makeResumeSituation(ctx, src1, recvFS, zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS1,
|
||||
To: src1.snapA,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{Encrypted: &nodefault.Bool{B: false}},
|
||||
}, zfs.RecvOptions{
|
||||
RollbackAndForceRecv: false,
|
||||
SavePartialRecvState: true,
|
||||
})
|
||||
|
||||
// threat model: forged resume token tries to steal a full send of snapA on fs2 by
|
||||
// presenting a resume token for full send of snapA on fs1
|
||||
var maliciousSend zfs.ZFSSendArgsUnvalidated = zfs.ZFSSendArgsUnvalidated{
|
||||
FS: sendFS2,
|
||||
To: &zfs.ZFSSendArgVersion{
|
||||
RelName: src2.snapA.RelName,
|
||||
GUID: src2.snapA.GUID,
|
||||
},
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: false},
|
||||
ResumeToken: rs.recvErrDecoded.ResumeTokenRaw,
|
||||
},
|
||||
}
|
||||
_, err = maliciousSend.Validate(ctx)
|
||||
require.Error(ctx, err)
|
||||
ctx.Logf("send err: %T %s", err, err)
|
||||
validationErr, ok := err.(*zfs.ZFSSendArgsValidationError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, validationErr.What, zfs.ZFSSendArgsResumeTokenMismatch)
|
||||
ctx.Logf("%s", validationErr)
|
||||
|
||||
mismatchError, ok := validationErr.Msg.(*zfs.ZFSSendArgsResumeTokenMismatchError)
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
|
||||
}
|
||||
|
||||
type sendArgsValidationEndToEndTestOutcome string
|
||||
|
||||
const (
|
||||
ValidationAccepts sendArgsValidationEndToEndTestOutcome = "accept"
|
||||
ValidationRejects sendArgsValidationEndToEndTestOutcome = "rejects"
|
||||
)
|
||||
|
||||
type sendArgsValidationEndToEndTest struct {
|
||||
encryptedSenderFilesystem bool
|
||||
senderConfigHook func(config *endpoint.SenderConfig)
|
||||
expectedOutcome sendArgsValidationEndToEndTestOutcome
|
||||
outcomeRejectsInspectError func(require.TestingT, *report.FilesystemReport, bool)
|
||||
inspectReceiverFSAfterSuccessfulCycle func(rfs string)
|
||||
}
|
||||
|
||||
func implSendArgsValidationEndToEndTest(ctx *platformtest.Context, setup sendArgsValidationEndToEndTest) {
|
||||
|
||||
senderEncrypted := ""
|
||||
if setup.encryptedSenderFilesystem {
|
||||
senderEncrypted = "encrypted"
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, fmt.Sprintf(`
|
||||
CREATEROOT
|
||||
+ "sender" %s
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`, senderEncrypted))
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sfsmp.Mounted)
|
||||
|
||||
// Two cycles. one initial replication, one incremental replication.
|
||||
// Within each cycle: interrupt replication at least once.
|
||||
// This exercises both the no-resume-token-present and the resume-token-present validation code paths.
|
||||
initial_then_incremental:
|
||||
for i := 0; i < 2; i++ {
|
||||
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 2*(1<<20))
|
||||
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
senderConfigHook: setup.senderConfigHook,
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
skipSendArgsValidation: false,
|
||||
}
|
||||
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
// PartialSender interrupts after 1MiB, and we wrote 2 MiB of data
|
||||
// => Give it 3 attempts to replicate. after that, we should have a stable outcome
|
||||
var lastReport *report.Report
|
||||
lastResumeToken := ""
|
||||
interrupt_current_step:
|
||||
for j := 0; j < 3; j++ {
|
||||
|
||||
lastReport = rep.Do(ctx)
|
||||
ctx.Logf("\nreport=%s", pretty.Sprint(lastReport))
|
||||
require.Len(ctx, lastReport.Attempts, 1)
|
||||
require.Len(ctx, lastReport.Attempts[0].Filesystems, 1)
|
||||
lastReportFS := lastReport.Attempts[0].Filesystems[0]
|
||||
|
||||
var rfsExists bool
|
||||
rfsResumeToken, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||
if err != nil {
|
||||
_, ok := err.(*zfs.DatasetDoesNotExist) // no shadow
|
||||
require.True(ctx, ok, "no other errors expected")
|
||||
rfsExists = false
|
||||
rfsResumeToken = ""
|
||||
} else {
|
||||
rfsExists = true
|
||||
}
|
||||
|
||||
if setup.expectedOutcome == ValidationRejects {
|
||||
|
||||
// When expecting rejection, it should manifest immediately, before sending anything.
|
||||
// This is tested in the j=0 iteration (for both initial and incremental repl (i=0, i=1)).
|
||||
// But we also want to assert correct behavior in case zrepl observes resume tokens.
|
||||
// Specifically, cases where the send parameters encoded in the token conflict with the
|
||||
// configured encryption policy.
|
||||
// Hence, for scenarios that are expected to reject, after we validated that they reject
|
||||
// for the non-resuming case (j==0), fabricate a resuming scenario by temporarily disabling
|
||||
// send args validation. After fabricating the scenario, proceed into j==1 to exercise
|
||||
// the resume token validation.
|
||||
if j == 0 {
|
||||
if i == 0 {
|
||||
require.False(ctx, rfsExists, "the sender should not have sent anything")
|
||||
} else {
|
||||
// we fabricate a scenario where rfsExists below, hence can't assert non-existence anymore
|
||||
}
|
||||
|
||||
ctx.Logf("skipping send args validation to test resuming case")
|
||||
|
||||
rep.skipSendArgsValidation = true
|
||||
setupResumeReport := rep.Do(ctx)
|
||||
ctx.Logf("setupResumeReport=%s", pretty.Sprint(setupResumeReport))
|
||||
rep.skipSendArgsValidation = false
|
||||
rt, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||
require.NoError(ctx, err)
|
||||
require.NotEmpty(ctx, rt, "we disabled send args validation, so the .Do above should have resulted in a resume token on rfs")
|
||||
lastResumeToken = rt
|
||||
continue interrupt_current_step // next iteration will test resume case with send args validation enabled
|
||||
} else { // j > 0
|
||||
require.Equal(ctx, lastResumeToken, rfsResumeToken, "we expect policy to refuse replication, no progress must happen")
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||
_, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(ctx, ok, "another check that no progress is happening")
|
||||
}
|
||||
|
||||
setup.outcomeRejectsInspectError(ctx, lastReportFS, j == 0)
|
||||
|
||||
// XXX: check rejection cases for incremental replication as well
|
||||
break initial_then_incremental
|
||||
|
||||
} else {
|
||||
require.Equal(ctx, ValidationAccepts, setup.expectedOutcome)
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||
_, notExist := err.(*zfs.DatasetDoesNotExist)
|
||||
if notExist {
|
||||
require.NotEmpty(ctx, rfsResumeToken)
|
||||
continue interrupt_current_step // next iteration will resume
|
||||
} else {
|
||||
require.NoError(ctx, err)
|
||||
// version exists
|
||||
|
||||
// make sure all the filesystem versions we created so far were replicated by the replication loop
|
||||
for j := 0; j <= i; j++ {
|
||||
_ = fsversion(ctx, rfs, fmt.Sprintf("@%d", j))
|
||||
}
|
||||
|
||||
setup.inspectReceiverFSAfterSuccessfulCycle(rfs)
|
||||
continue initial_then_incremental
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SendArgsValidationEE_EncryptionAndRaw(ctx *platformtest.Context) {
|
||||
type TC struct {
|
||||
// create sender filesystem with encryption enabled yes/no
|
||||
SFSEnc bool
|
||||
SndEnc bool // send flag
|
||||
SndRaw bool // send flag
|
||||
RFSEnc bool
|
||||
Outcome sendArgsValidationEndToEndTestOutcome
|
||||
RejectErrorNoResume string
|
||||
RejectErrorResume string
|
||||
}
|
||||
tcs := []TC{
|
||||
// Sender FS is unencrypted
|
||||
{SFSEnc: false, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts},
|
||||
{SFSEnc: false, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationAccepts}, // allow unencrypted raw sends (#503)
|
||||
{SFSEnc: false, SndEnc: true, SndRaw: false, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
},
|
||||
{SFSEnc: false, SndEnc: true, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
},
|
||||
// Sender FS is encrypted
|
||||
{SFSEnc: true, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts}, // passes because keys are loaded, thus can send plain.
|
||||
{SFSEnc: true, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `policy mandates raw\+unencrypted sends, but filesystem .* is encrypted`,
|
||||
RejectErrorResume: `resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only`,
|
||||
},
|
||||
{SFSEnc: true, SndEnc: true, SndRaw: false, RFSEnc: true, Outcome: ValidationAccepts},
|
||||
{SFSEnc: true, SndEnc: true, SndRaw: true, RFSEnc: true, Outcome: ValidationAccepts},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc := tc // closure would copy by ref otherwise
|
||||
ctx.QueueSubtest(fmt.Sprintf("%#v", tc), func(ctx *platformtest.Context) {
|
||||
implSendArgsValidationEndToEndTest(ctx, sendArgsValidationEndToEndTest{
|
||||
encryptedSenderFilesystem: tc.SFSEnc,
|
||||
senderConfigHook: func(c *endpoint.SenderConfig) {
|
||||
c.Encrypt = &nodefault.Bool{B: tc.SndEnc}
|
||||
c.SendRaw = tc.SndRaw
|
||||
},
|
||||
expectedOutcome: tc.Outcome,
|
||||
outcomeRejectsInspectError: func(ctx require.TestingT, fr *report.FilesystemReport, isResume bool) {
|
||||
// this callback is only called for ValidationRejects
|
||||
|
||||
// validation should be failing during dry send => planning stage
|
||||
// XXX mock out ZFS to ensure we never call a zfs send that would send data
|
||||
// if we're expecting validation to fail
|
||||
require.Equal(ctx, report.FilesystemPlanningErrored, fr.State)
|
||||
|
||||
if isResume {
|
||||
require.NotEmpty(ctx, tc.RejectErrorResume)
|
||||
require.Regexp(ctx, tc.RejectErrorResume, fr.PlanError)
|
||||
} else {
|
||||
require.NotEmpty(ctx, tc.RejectErrorNoResume)
|
||||
require.Regexp(ctx, tc.RejectErrorNoResume, fr.PlanError)
|
||||
}
|
||||
},
|
||||
inspectReceiverFSAfterSuccessfulCycle: func(rfs string) {
|
||||
enabled, err := zfs.ZFSGetEncryptionEnabled(ctx, rfs)
|
||||
require.NoError(ctx, err)
|
||||
require.Equal(ctx, tc.RFSEnc, enabled, "receiver filesystem encryption settings unexpected")
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func sendStreamTest(ctx *platformtest.Context) *zfs.SendStream {
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "sender"
|
||||
`)
|
||||
|
||||
fs := fmt.Sprintf("%s/sender", ctx.RootDataset)
|
||||
|
||||
fsmpo, err := zfs.ZFSGetMountpoint(ctx, fs)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
writeDummyData(path.Join(fsmpo.Mountpoint, "dummy.data"), 1<<26)
|
||||
mustSnapshot(ctx, fs+"@1")
|
||||
snap := fsversion(ctx, fs, "@1")
|
||||
snapSendArg := snap.ToSendArgVersion()
|
||||
|
||||
sendArgs, err := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: fs,
|
||||
From: nil,
|
||||
To: &snapSendArg,
|
||||
ZFSSendFlags: zfs.ZFSSendFlags{
|
||||
Encrypted: &nodefault.Bool{B: false},
|
||||
},
|
||||
}.Validate(ctx)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
return sendStream
|
||||
|
||||
}
|
||||
|
||||
func SendStreamCloseAfterBlockedOnPipeWrite(ctx *platformtest.Context) {
|
||||
|
||||
sendStream := sendStreamTest(ctx)
|
||||
|
||||
// let the pipe buffer fill and the zfs process block uninterruptibly
|
||||
|
||||
ctx.Logf("waiting for pipe write to block")
|
||||
time.Sleep(5 * time.Second) // XXX need a platform-neutral way to detect that the pipe is full and the writer is blocked
|
||||
|
||||
ctx.Logf("closing send stream")
|
||||
err := sendStream.Close() // this is what this test case is about
|
||||
ctx.Logf("close error: %T %s", err, err)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
exitErrZfsError := sendStream.TestOnly_ExitErr()
|
||||
require.Contains(ctx, exitErrZfsError.Error(), "signal")
|
||||
require.Error(ctx, exitErrZfsError)
|
||||
exitErr, ok := exitErrZfsError.WaitErr.(*exec.ExitError)
|
||||
require.True(ctx, ok)
|
||||
if exitErr.Exited() {
|
||||
// some ZFS impls (FreeBSD 12) behaves that way
|
||||
return
|
||||
}
|
||||
|
||||
// ProcessState is only available after exit
|
||||
// => use as proxy that the process was wait()ed upon and is gone
|
||||
ctx.Logf("%#v", exitErr.ProcessState)
|
||||
require.NotNil(ctx, exitErr.ProcessState)
|
||||
// and let's verify that the process got killed, so that we know it was the call to .Close() above
|
||||
waitStatus := exitErr.ProcessState.Sys().(syscall.WaitStatus)
|
||||
ctx.Logf("wait status: %#v", waitStatus)
|
||||
ctx.Logf("exit status: %v", waitStatus.ExitStatus())
|
||||
require.True(ctx, waitStatus.Signaled())
|
||||
switch waitStatus.Signal() {
|
||||
case unix.SIGKILL:
|
||||
fallthrough
|
||||
case unix.SIGPIPE:
|
||||
// ok
|
||||
|
||||
default:
|
||||
ctx.Errorf("%T %s\n%v", waitStatus.Signal(), waitStatus.Signal(), waitStatus.Signal())
|
||||
ctx.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func SendStreamCloseAfterEOFRead(ctx *platformtest.Context) {
|
||||
|
||||
sendStream := sendStreamTest(ctx)
|
||||
|
||||
_, err := io.Copy(io.Discard, sendStream)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
var buf [128]byte
|
||||
n, err := sendStream.Read(buf[:])
|
||||
require.Zero(ctx, n)
|
||||
require.Equal(ctx, io.EOF, err)
|
||||
|
||||
err = sendStream.Close()
|
||||
require.NoError(ctx, err)
|
||||
|
||||
n, err = sendStream.Read(buf[:])
|
||||
require.Zero(ctx, n)
|
||||
require.Equal(ctx, os.ErrClosed, err, "same read error should be returned")
|
||||
}
|
||||
|
||||
func SendStreamMultipleCloseAfterEOF(ctx *platformtest.Context) {
|
||||
|
||||
sendStream := sendStreamTest(ctx)
|
||||
|
||||
_, err := io.Copy(io.Discard, sendStream)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
var buf [128]byte
|
||||
n, err := sendStream.Read(buf[:])
|
||||
require.Zero(ctx, n)
|
||||
require.Equal(ctx, io.EOF, err)
|
||||
|
||||
err = sendStream.Close()
|
||||
require.NoError(ctx, err)
|
||||
|
||||
err = sendStream.Close()
|
||||
require.Equal(ctx, os.ErrClosed, err)
|
||||
}
|
||||
|
||||
func SendStreamMultipleCloseBeforeEOF(ctx *platformtest.Context) {
|
||||
|
||||
sendStream := sendStreamTest(ctx)
|
||||
|
||||
err := sendStream.Close()
|
||||
require.NoError(ctx, err)
|
||||
|
||||
err = sendStream.Close()
|
||||
require.Equal(ctx, os.ErrClosed, err)
|
||||
}
|
||||
|
||||
type failingReadCloser struct {
|
||||
err error
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = &failingReadCloser{}
|
||||
|
||||
func (c *failingReadCloser) Read(p []byte) (int, error) { return 0, c.err }
|
||||
func (c *failingReadCloser) Close() error { return c.err }
|
||||
|
||||
func SendStreamNonEOFReadErrorHandling(ctx *platformtest.Context) {
|
||||
|
||||
sendStream := sendStreamTest(ctx)
|
||||
|
||||
var buf [128]byte
|
||||
n, err := sendStream.Read(buf[:])
|
||||
require.Equal(ctx, len(buf), n)
|
||||
require.NoError(ctx, err)
|
||||
|
||||
var mockError = fmt.Errorf("taeghaefow4piesahwahjocu7ul5tiachaiLipheijae8ooZ8Pies8shohGee9feeTeirai5aiFeiyaecai4kiaLoh4azeih0tea")
|
||||
mock := &failingReadCloser{err: mockError}
|
||||
orig := sendStream.TestOnly_ReplaceStdoutReader(mock)
|
||||
|
||||
n, err = sendStream.Read(buf[:])
|
||||
require.Equal(ctx, 0, n)
|
||||
require.Equal(ctx, mockError, err)
|
||||
|
||||
if sendStream.TestOnly_ReplaceStdoutReader(orig) != mock {
|
||||
panic("incorrect test impl")
|
||||
}
|
||||
|
||||
err = sendStream.Close()
|
||||
require.NoError(ctx, err) // if we can't kill the child then this will be a flaky test, but let's assume we can kill the child
|
||||
|
||||
err = sendStream.Close()
|
||||
require.Equal(ctx, os.ErrClosed, err)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
)
|
||||
|
||||
type Case func(*platformtest.Context)
|
||||
|
||||
func (c Case) String() string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(c).Pointer()).Name()
|
||||
}
|
||||
|
||||
//go:generate go run ./gen github.com/zrepl/zrepl/platformtest/tests
|
||||
@@ -0,0 +1,37 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/platformtest"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func UndestroyableSnapshotParsing(t *platformtest.Context) {
|
||||
platformtest.Run(t, platformtest.PanicErr, t.RootDataset, `
|
||||
DESTROYROOT
|
||||
CREATEROOT
|
||||
+ "foo bar"
|
||||
+ "foo bar@1 2 3"
|
||||
+ "foo bar@4 5 6"
|
||||
+ "foo bar@7 8 9"
|
||||
R zfs hold zrepl_platformtest "${ROOTDS}/foo bar@4 5 6"
|
||||
`)
|
||||
|
||||
err := zfs.ZFSDestroy(t, fmt.Sprintf("%s/foo bar@1 2 3,4 5 6,7 8 9", t.RootDataset))
|
||||
if err == nil {
|
||||
panic("expecting destroy error due to hold")
|
||||
}
|
||||
if dse, ok := err.(*zfs.DestroySnapshotsError); !ok {
|
||||
panic(fmt.Sprintf("expecting *zfs.DestroySnapshotsError, got %T\n%v\n%s", err, err, err))
|
||||
} else {
|
||||
if dse.Filesystem != fmt.Sprintf("%s/foo bar", t.RootDataset) {
|
||||
panic(dse.Filesystem)
|
||||
}
|
||||
require.Equal(t, []string{"4 5 6"}, dse.Undestroyable)
|
||||
require.Equal(t, []string{"dataset is busy"}, dse.Reason)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user