Integrate reviewed NAS qualification fixture repair

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-13 04:57:48 +01:00
5 changed files with 75 additions and 12 deletions
+8 -1
View File
@@ -220,7 +220,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
brew install bash gnu-tar
brew install bash gnu-tar gawk
linux_bash="$(brew --prefix bash)/bin/bash"
test -x "$linux_bash"
"$linux_bash" -c '(( BASH_VERSINFO[0] >= 5 ))'
@@ -237,6 +237,13 @@ jobs:
test -x "$gnu_tar"
"$gnu_tar" --version | grep -q 'GNU tar'
ln -s "$gnu_tar" "$fixture_bin/tar"
# The NAS boot filter rejects embedded NUL/control bytes with octal
# ranges. Use the same binary-aware awk semantics as its Linux jobs,
# not the native macOS parser, only within these server fixtures.
gnu_awk="$(brew --prefix gawk)/bin/gawk"
test -x "$gnu_awk"
"$gnu_awk" --version | grep -q 'GNU Awk'
ln -s "$gnu_awk" "$fixture_bin/awk"
printf 'PULSE_INSTALLER_FIXTURE_BIN=%s\n' "$fixture_bin" >> "$GITHUB_ENV"
- name: Test native Unix installer contracts
+35 -2
View File
@@ -44,6 +44,20 @@ def definitions(source, names=FUNCTIONS):
return "\n".join(result)
def generated_collector_is_running(listing, runtime):
"""Match the owned executable with either Unix comm representation.
Darwin reports an executable path where Linux reports its basename. Keep
the argument path exact so another same-named process is not our collector.
"""
for line in listing.splitlines():
fields = line.split(maxsplit=1)
if (len(fields) == 2 and Path(fields[0]).name == "pulse-agent"
and fields[1].startswith(str(runtime) + " ")):
return True
return False
MODEL = r'''#!/usr/bin/python3
import json, os, pathlib, sys
root = pathlib.Path(os.environ['FIXTURE'])
@@ -392,7 +406,9 @@ remove_safe_profile_state_dir() { rm -rf -- "$SAFE_PROFILE_STATE_DIR"; }
import time
for platform in ('Unraid', 'QNAP'):
with self.subTest(platform=platform), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
# Darwin's temporary-directory alias must not become a
# different executable identity in its process observation.
root = Path(directory).resolve()
for name in ('state', 'bin', 'tools', 'flash', 'logs'): (root / name).mkdir(mode=0o700)
runtime = root / 'bin/pulse-agent'; stored = root / 'state/stored-agent'
stored.write_bytes(Path('/bin/sleep').read_bytes()); stored.chmod(0o700)
@@ -438,7 +454,7 @@ remove_safe_profile_state_dir() { rm -rf -- "$SAFE_PROFILE_STATE_DIR"; }
deadline = time.monotonic() + 8
while time.monotonic() < deadline:
listing = subprocess.run(['ps', '-ax', '-o', 'comm=', '-o', 'args='], capture_output=True, text=True, check=True).stdout
if any(line.strip().startswith('pulse-agent ') and str(runtime) in line for line in listing.splitlines()): break
if generated_collector_is_running(listing, runtime): break
self.assertIsNone(process.poll(), output.read_text()); time.sleep(.05)
else: self.fail('private generated collector did not start')
result = subprocess.run(['bash', str(control)], env=env, capture_output=True, text=True, timeout=12)
@@ -451,6 +467,23 @@ remove_safe_profile_state_dir() { rm -rf -- "$SAFE_PROFILE_STATE_DIR"; }
except ProcessLookupError: pass
process.wait(timeout=3)
def test_generated_collector_observation_keeps_exact_identity_on_both_unix_formats(self):
runtime = Path('/synthetic/owned/bin/pulse-agent')
for comm in ('pulse-agent', str(runtime)):
with self.subTest(comm=comm):
self.assertTrue(generated_collector_is_running(f'{comm} {runtime} 30\n', runtime))
for listing in (
f'other {runtime} 30\n',
'pulse-agent /synthetic/foreign/bin/pulse-agent 30\n',
f'pulse-agent {runtime}-prod 30\n',
f'pulse-agent /bin/sh echo {runtime} 30\n',
f'pulse-agent {runtime}\n',
'pulse-agent\n',
'',
):
with self.subTest(listing=listing):
self.assertFalse(generated_collector_is_running(listing, runtime))
def test_caller_has_no_blanket_nas_or_legacy_tree_deletion(self):
caller = self.source.split('# --- Uninstall Logic ---\n', 1)[1].split('# --- Validation ---\n', 1)[0]
self.assertNotIn("sed -i '/pulse-agent/d'", caller)
@@ -18,6 +18,7 @@ ROOT = Path(__file__).resolve().parents[2]
SOURCE = Path(os.environ.get("PULSE_AGENT_TRUENAS_SOURCE", ROOT / "scripts/install.sh"))
FUNCTIONS = (
"portable_path_uid", "portable_path_mode", "trusted_lifecycle_regular_file",
"nas_agent_removal_platform",
"read_agent_removal_unit_state", "confirm_agent_unit_quiescent",
"quiesce_agent_units_for_removal", "teardown_systemd_agent_service",
"read_legacy_agent_removal_processes", "legacy_agent_processes_absent",
@@ -372,6 +373,10 @@ remove_safe_profile_state_dir() { rm -rf -- "$SAFE_PROFILE_STATE_DIR"; }
result = self.exercise(bootstrap='missing', tasks='absent', appliance=False)
self.assertEqual(result[0].returncode, 0, result[0].stderr)
self.assertFalse(any(e.startswith('task:') for e in result[2]))
# The complete caller also checks the unrelated NAS platform. Extract
# that real function, with its existing vendor paths redirected, rather
# than letting an undefined command masquerade as a negative scope read.
self.assertNotIn('command not found', result[0].stdout + result[0].stderr)
def test_process_read_exposes_only_roles_and_real_timeout_child_is_stopped(self):
result = self.exercise(fault='live-bootstrap', flow='process')
+2 -1
View File
@@ -8574,13 +8574,14 @@ func TestInstallSHNASBootOwnersQuiesceBeforeRecoveryErasure(t *testing.T) {
"test_recovered_ordinary_linux_state_is_not_qnap_platform_authority",
"test_live_unraid_boot_shell_is_unknown_not_a_signal_target",
"test_actual_generated_watchdogs_and_children_become_quiescent",
"test_generated_collector_observation_keeps_exact_identity_on_both_unix_formats",
"test_caller_has_no_blanket_nas_or_legacy_tree_deletion",
} {
if !strings.Contains(string(output), name+" (__main__.AgentNASTeardown."+name+") ... ok") {
t.Fatalf("NAS removal control %s did not pass:\n%s", name, output)
}
}
if !strings.Contains(string(output), "\nRan 18 tests in ") || !strings.Contains(string(output), "\nOK\n") {
if !strings.Contains(string(output), "\nRan 19 tests in ") || !strings.Contains(string(output), "\nOK\n") {
t.Fatalf("incomplete NAS removal verdict:\n%s", output)
}
t.Logf("%s", output)
@@ -52,7 +52,7 @@ func nativeInstallerFixtureSteps(t *testing.T) (nativeGuidanceStep, nativeGuidan
func TestNativeInstallerFixtureToolsAreRealAndStepLocal(t *testing.T) {
supply, installer := nativeInstallerFixtureSteps(t)
for _, scenario := range []string{"real tools", "install failure", "missing bash", "legacy bash", "missing stat", "non-GNU chmod", "missing mv", "non-GNU mv", "missing tar", "non-GNU tar"} {
for _, scenario := range []string{"real tools", "install failure", "missing bash", "legacy bash", "missing stat", "non-GNU chmod", "missing mv", "non-GNU mv", "missing tar", "non-GNU tar", "missing awk", "non-GNU awk"} {
t.Run(scenario, func(t *testing.T) {
root := t.TempDir()
bin, prefix, tarPrefix, runnerTemp := filepath.Join(root, "bin"), filepath.Join(root, "brew with spaces"), filepath.Join(root, "gnu tar with spaces"), filepath.Join(root, "runner temp")
@@ -64,16 +64,17 @@ func TestNativeInstallerFixtureToolsAreRealAndStepLocal(t *testing.T) {
brew := `#!/bin/sh
printf '%s\n' "$*" >> "$BREW_CALLS"
case "$*" in
'install bash gnu-tar') exit "${BREW_EXIT:-0}" ;;
'install bash gnu-tar gawk') exit "${BREW_EXIT:-0}" ;;
'--prefix bash'|'--prefix coreutils') printf '%s\n' "$TOOLS_PREFIX" ;;
'--prefix gnu-tar') printf '%s\n' "$TAR_PREFIX" ;;
'--prefix gawk') printf '%s\n' "$TOOLS_PREFIX" ;;
*) exit 64 ;;
esac
`
if err := os.WriteFile(filepath.Join(bin, "brew"), []byte(brew), 0700); err != nil {
t.Fatal(err)
}
tools := []string{"bash", "stat", "chmod", "chown", "mv", "tar"}
tools := []string{"bash", "stat", "chmod", "chown", "mv", "tar", "awk"}
installed := make(map[string]string)
for _, name := range tools {
leaf, toolPrefix := name, prefix
@@ -97,7 +98,11 @@ esac
t.Fatal(err)
}
} else {
realTool, err := exec.LookPath(name)
realName := name
if name == "awk" {
realName = "gawk"
}
realTool, err := exec.LookPath(realName)
if err != nil {
t.Fatal(err)
}
@@ -109,7 +114,7 @@ esac
// Native macOS defaults must not be the fallback for Linux-only
// archive extraction or atomic replacement. The supplied real GNU
// tools below must win even when the native names are unsuitable.
for _, name := range []string{"mv", "tar"} {
for _, name := range []string{"mv", "tar", "awk"} {
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nprintf 'native BSD utility reached\\n' >&2\nexit 64\n"), 0700); err != nil {
t.Fatal(err)
}
@@ -156,14 +161,14 @@ esac
}
entries, err := os.ReadDir(fixtureBin)
if err != nil || len(entries) != len(tools) {
t.Fatal("fixture supply must select only Bash/stat/chmod/chown/mv/tar")
t.Fatal("fixture supply must select only Bash/stat/chmod/chown/mv/tar/awk")
}
for _, name := range tools {
if target, err := os.Readlink(filepath.Join(fixtureBin, name)); err != nil || target != installed[name] {
t.Fatalf("%s did not bind the verified installed executable", name)
}
}
if data, err := os.ReadFile(calls); err != nil || string(data) != "install bash gnu-tar\n--prefix bash\n--prefix coreutils\n--prefix gnu-tar\n" {
if data, err := os.ReadFile(calls); err != nil || string(data) != "install bash gnu-tar gawk\n--prefix bash\n--prefix coreutils\n--prefix gnu-tar\n--prefix gawk\n" {
t.Fatal("fixture supply changed its fixed package/resource selection")
}
// Exercise the actual workflow step's local PATH boundary, replacing
@@ -172,7 +177,7 @@ esac
set -eu
test "$*" = 'scripts/installtests/run_unix_contracts.py' || exit 91
test "$(command -v bash)" = "$PULSE_INSTALLER_FIXTURE_BIN/bash" || exit 92
for tool in stat chmod chown mv tar; do
for tool in stat chmod chown mv tar awk; do
test "$(command -v "$tool")" = "$PULSE_INSTALLER_FIXTURE_BIN/$tool" || exit 93
done
# Exercise the actual Linux-only flags with real executables and file bytes.
@@ -186,6 +191,18 @@ cp "$PROBE_ROOT/extracted/source" "$PROBE_ROOT/staged"
mv -T "$PROBE_ROOT/staged" "$PROBE_ROOT/published"
cmp "$PROBE_ROOT/source" "$PROBE_ROOT/published"
test ! -e "$PROBE_ROOT/staged"
# Exercise the NAS filter's actual octal byte range, not a --version double.
# Safe text must pass; NUL and other disallowed controls must fail. Neither
# false-positive empty matching nor dropping binary bytes is acceptable.
printf 'safe boot text\n' > "$PROBE_ROOT/boot-safe"
printf 'unsafe\000boot\n' > "$PROBE_ROOT/boot-nul"
printf 'unsafe\001boot\n' > "$PROBE_ROOT/boot-control"
awk 'BEGIN { bad = 0 } /[\000-\010\013-\037\177]/ { bad = 1 } END { exit bad }' "$PROBE_ROOT/boot-safe"
for file in boot-nul boot-control; do
if awk 'BEGIN { bad = 0 } /[\000-\010\013-\037\177]/ { bad = 1 } END { exit bad }' "$PROBE_ROOT/$file"; then
exit 94
fi
done
exit "${SUITE_EXIT:-0}"
`
if err := os.WriteFile(filepath.Join(bin, "python3"), []byte(probe), 0700); err != nil {