Files
yatm/internal/executor/tape_encrypt_script_test.go
T
Samuel CuiandClaude Opus 5.5 47b525b747 Write, read and publish encrypted tar Media
Compose tar FORMAT/APPEND, Restore, Scan and Verify through complete Media sessions and the configured encrypt adapter. Publish only complete members at normal or capacity/EW completion, maintain position maps, apply Logical or Auto read order, record passive read learning observations in each Job and refit learned read parameters after drive release. LTFS keeps its adapters and gains the shared read order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:01:44 +08:00

179 lines
6.7 KiB
Go

package executor
import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestTapeEncryptScriptContract(t *testing.T) {
// Execute the shipped adapter with an exclusive tool directory, never host device tools.
bundled, err := filepath.Abs("../../scripts/encrypt")
require.NoError(t, err)
cases := []struct {
name string
restore bool
failures int
loadExit int
wantTries int
wantSleeps []string
wantExit int
}{
{name: "new key succeeds after retries", failures: 2, wantTries: 3, wantSleeps: []string{"5", "5", "5", "3"}},
{name: "restored key succeeds", restore: true, wantTries: 1, wantSleeps: []string{"5", "3"}},
{name: "exhausted retries stop Session", restore: true, failures: 60, wantTries: 60, wantSleeps: repeatEncryptSleep("5", 60), wantExit: 1},
{name: "load failure stops before stenc", loadExit: 23, wantExit: 23},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
// Keep the adapter's service working directory and relative helper contract intact.
service := t.TempDir()
t.Chdir(service)
state := t.TempDir()
toolDir := filepath.Join(state, "tools")
tapeDir := filepath.Join(state, "Tape artifacts")
require.NoError(t, os.Mkdir(toolDir, 0700))
require.NoError(t, os.Mkdir(tapeDir, 0700))
require.NoError(t, os.Symlink("/bin/bash", filepath.Join(toolDir, "bash")))
writeEncryptContractScript(t, filepath.Join(toolDir, "mkdir"), `
[ "$#" -eq 2 ] && [ "$1" = -p ] && [ "$2" = "$TAPE_DIR" ] && [ -d "$2" ] || exit 91
`)
writeEncryptContractScript(t, filepath.Join(toolDir, "tee"), `
[ "$#" -eq 2 ] && [ "$1" = -a ] && [ "$2" = "$TAPE_DIR/ltfs.log" ] || exit 92
while IFS= read -r line; do printf '%s\n' "$line" >> "$2"; done
`)
writeEncryptContractScript(t, filepath.Join(toolDir, "mt"), fmt.Sprintf(`
printf '%%s\n' "$@" > "$CONTRACT_STATE/mt"
exit %d
`, test.loadExit))
writeEncryptContractScript(t, filepath.Join(toolDir, "sleep"), `
printf '%s\n' "$@" >> "$CONTRACT_STATE/sleep"
`)
writeEncryptContractScript(t, filepath.Join(toolDir, "stenc"), fmt.Sprintf(`
count=0
if [ -f "$CONTRACT_STATE/count" ]; then read -r count < "$CONTRACT_STATE/count"; fi
count=$((count + 1))
printf '%%s\n' "$count" > "$CONTRACT_STATE/count"
printf '%%s\n' "$@" > "$CONTRACT_STATE/stenc-$count"
[ "$count" -gt %d ]
`, test.failures))
helper := `
printf '%s\n' "$DEVICE" "$KEY_FILE" "$TAPE_BARCODE" "$TAPE_NAME" "$TAPE_DIR" "$PWD" "$PATH" > "$CONTRACT_STATE/environment"
key=''
IFS= read -r key < "$KEY_FILE" || [ -n "$key" ]
printf '%s' "$key" > "$CONTRACT_STATE/key"
`
require.NoError(t, os.WriteFile(filepath.Join(service, "encrypt-helper"), []byte(helper), 0600))
// A test-only bootstrap supplies the exclusive PATH; the bundled adapter is unchanged.
adapter := filepath.Join(state, "encrypt")
writeEncryptContractScript(t, adapter, fmt.Sprintf(`
export PATH=%s
export CONTRACT_STATE=%s
. ./encrypt-helper
exec /bin/bash %s
`, encryptShellQuote(toolDir), encryptShellQuote(state), encryptShellQuote(bundled)))
exe := &Executor{paths: Paths{Work: t.TempDir()}, scripts: Scripts{Encrypt: adapter}}
logger := logrus.New()
logger.SetOutput(io.Discard)
backend := &mediaBackend{executor: exe, logger: logger}
// New and restored keys share the actual private temporary-file lifecycle.
var encoded, keyPath string
var recycle func()
if test.restore {
encoded = "v1:" + strings.Repeat("ab", 32)
keyPath, recycle, err = exe.RestoreKey(encoded)
} else {
encoded, keyPath, recycle, err = exe.NewKey()
}
require.NoError(t, err)
t.Cleanup(recycle)
require.True(t, strings.HasPrefix(encoded, "v1:"))
decoded, err := hex.DecodeString(strings.TrimPrefix(encoded, "v1:"))
require.NoError(t, err)
require.Len(t, decoded, 32)
keyInfo, err := os.Stat(keyPath)
require.NoError(t, err)
require.Equal(t, os.FileMode(0600), keyInfo.Mode().Perm())
keyBytes, err := os.ReadFile(keyPath)
require.NoError(t, err)
require.Equal(t, strings.TrimPrefix(encoded, "v1:"), string(keyBytes))
// Literal arguments include spaces and shell metacharacters to detect adapter splitting.
device, barcode, name := "/dev/stub Tape [0]", "AB1234", "Tape name * [one]"
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exe.MakeEncryptCmd(ctx, device, keyPath, barcode, name)
require.Equal(t, []string{adapter}, cmd.Args)
require.Empty(t, cmd.Dir)
err = backend.configureTape(ctx, device, keyPath, barcode, name, tapeDir)
if test.wantExit == 0 {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, "configure Tape encryption failed")
var exit *exec.ExitError
require.True(t, errors.As(err, &exit))
require.Equal(t, test.wantExit, exit.ExitCode())
}
// Assert observed inputs, retries and command boundaries rather than mocked return values.
cwd, err := os.Getwd()
require.NoError(t, err)
cwd, err = filepath.EvalSymlinks(cwd)
require.NoError(t, err)
require.Equal(t, []string{device, keyPath, barcode, name, tapeDir, cwd, toolDir}, encryptContractLines(t, filepath.Join(state, "environment")))
require.Equal(t, []string{"-f", device, "load"}, encryptContractLines(t, filepath.Join(state, "mt")))
require.Equal(t, test.wantSleeps, encryptContractLines(t, filepath.Join(state, "sleep")))
for attempt := 1; attempt <= test.wantTries; attempt++ {
require.Equal(t, []string{"-f", device, "-e", "on", "-k", keyPath, "-kd", barcode, "-a", "1", "--ckod"}, encryptContractLines(t, filepath.Join(state, fmt.Sprintf("stenc-%d", attempt))))
}
require.NoFileExists(t, filepath.Join(state, fmt.Sprintf("stenc-%d", test.wantTries+1)))
observedKey, err := os.ReadFile(filepath.Join(state, "key"))
require.NoError(t, err)
require.Equal(t, keyBytes, observedKey)
require.FileExists(t, filepath.Join(tapeDir, "ltfs.log"))
require.FileExists(t, keyPath)
recycle()
require.NoDirExists(t, filepath.Dir(keyPath))
})
}
}
func writeEncryptContractScript(t *testing.T, path, body string) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte("#!/bin/sh\nset -eu\n"+body), 0700))
}
func encryptShellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
func encryptContractLines(t *testing.T, path string) []string {
t.Helper()
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
require.NoError(t, err)
return strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
}
func repeatEncryptSleep(value string, count int) []string {
result := make([]string, count)
for i := range result {
result[i] = value
}
return result
}