mirror of
https://github.com/zrepl/zrepl.git
synced 2024-11-27 19:04:28 +01:00
326 lines
7.6 KiB
Go
326 lines
7.6 KiB
Go
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
|
|
}
|