move implementation to internal/ directory (#828)
This commit is contained in:
committed by
GitHub
parent
b9b9ad10cf
commit
908807bd59
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user