mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Integrate reviewed removal fixture and release metadata controls
Change-source: pulse-maintainer
This commit is contained in:
@@ -11,6 +11,7 @@ from pathlib import Path
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
@@ -113,6 +114,32 @@ save(); sys.exit(0)
|
||||
'''
|
||||
|
||||
|
||||
LINK_MODEL = r'''
|
||||
import os, pathlib, sys
|
||||
args = sys.argv[1:]
|
||||
if len(args) == 4 and args[:2] == ['-T', '--']:
|
||||
no_target_directory, source, target = True, args[2], args[3]
|
||||
elif len(args) == 3 and args[0] == '--':
|
||||
no_target_directory, source, target = False, args[1], args[2]
|
||||
else:
|
||||
sys.exit(98)
|
||||
root = pathlib.Path(os.environ['FIXTURE']).resolve()
|
||||
for name in (source, target):
|
||||
path = pathlib.Path(name)
|
||||
if not path.is_absolute() or '..' in path.parts or not path.parent.resolve().is_relative_to(root):
|
||||
sys.exit(97)
|
||||
# Exercise the real exclusive link operation, not a simulated successful exit.
|
||||
# DSM's ln -T must not follow a raced target directory. The old flagless form
|
||||
# deliberately retains directory-following semantics for adverse source checks.
|
||||
if not no_target_directory and os.path.isdir(target):
|
||||
target = os.path.join(target, os.path.basename(source))
|
||||
try:
|
||||
os.link(source, target, follow_symlinks=False)
|
||||
except OSError:
|
||||
sys.exit(1)
|
||||
'''
|
||||
|
||||
|
||||
class AgentUpstartTeardown(unittest.TestCase):
|
||||
source = SOURCE.read_text()
|
||||
|
||||
@@ -123,6 +150,12 @@ class AgentUpstartTeardown(unittest.TestCase):
|
||||
tools = root / "tools"; tools.mkdir()
|
||||
for name in ("ps", "initctl"):
|
||||
p = tools / name; p.write_text(MODEL); p.chmod(0o700)
|
||||
link = tools / "fixture-link.py"; link.write_text(LINK_MODEL)
|
||||
# Native macOS ln lacks -T. No host ln implementation may determine
|
||||
# whether these closed DSM semantics or restoration controls pass.
|
||||
external_link = tools / "ln"
|
||||
external_link.write_text("#!/bin/sh\nprintf 'unexpected external ln\\n' >&2\nexit 96\n")
|
||||
external_link.chmod(0o700)
|
||||
if fault == "missing-initctl": (tools / "initctl").unlink()
|
||||
init = root / "init"; init.mkdir()
|
||||
config = init / "pulse-agent.conf"
|
||||
@@ -160,7 +193,7 @@ class AgentUpstartTeardown(unittest.TestCase):
|
||||
values = dict(FIXTURE=root, EVENTS=root / "events", STATE_DIR=root / "state",
|
||||
PRIVILEGED_HELPER_STATE_DIR=root / "helper", SAFE_PROFILE_STATE_DIR=root / "profile",
|
||||
PRIVILEGE_SUDOERS_FILE=root / "absent-sudoers", PRIVILEGE_HELPER_DIR=root / "absent-helper",
|
||||
REPLY=reply, FAULT=fault)
|
||||
REPLY=reply, FAULT=fault, FIXTURE_PYTHON=sys.executable, FIXTURE_LINK=link)
|
||||
setup = "\n".join(k + "=" + shlex.quote(str(v)) for k, v in values.items()) + "\n"
|
||||
shell = r'''
|
||||
set -euo pipefail
|
||||
@@ -213,8 +246,9 @@ ln() {
|
||||
race_restore_config "$target"
|
||||
fi
|
||||
printf 'file:link\n' >> "$EVENTS"
|
||||
# Preserve the actual external command exit for the production handler.
|
||||
if builtin command ln "$@"; then return 0; else return $?; fi
|
||||
# A portable private link model preserves the actual syscall exit, including
|
||||
# raced files/directories. Never fall back to success or weaken production -T.
|
||||
if "$FIXTURE_PYTHON" "$FIXTURE_LINK" "$@"; then return 0; else return $?; fi
|
||||
}
|
||||
mv() {
|
||||
local arg
|
||||
@@ -273,6 +307,40 @@ remove_safe_profile_state_dir() { builtin command rm -r -- "$SAFE_PROFILE_STATE_
|
||||
self.assertTrue(added <= {"init/pulse-agent.override"}, added)
|
||||
if added: self.assertEqual(after["init/pulse-agent.override"][2], b"manual\n")
|
||||
|
||||
def test_link_fixture_preserves_exclusive_no_target_directory_semantics(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "source"; source.write_bytes(b"original recovery\n")
|
||||
target = root / "target"
|
||||
env = dict(os.environ, FIXTURE=str(root))
|
||||
def link(*args):
|
||||
return subprocess.run([sys.executable, "-c", LINK_MODEL, *map(str, args)],
|
||||
env=env, capture_output=True, timeout=5).returncode
|
||||
self.assertEqual(link('-T', '--', source, target), 0)
|
||||
self.assertEqual(target.stat().st_ino, source.stat().st_ino)
|
||||
target.unlink(); target.write_bytes(b"concurrent recovery\n")
|
||||
self.assertEqual(link('-T', '--', source, target), 1)
|
||||
self.assertEqual(target.read_bytes(), b"concurrent recovery\n")
|
||||
target.unlink(); target.mkdir()
|
||||
self.assertEqual(link('-T', '--', source, target), 1)
|
||||
self.assertEqual(list(target.iterdir()), [])
|
||||
self.assertEqual(link('--', source, target), 0)
|
||||
self.assertEqual((target / source.name).stat().st_ino, source.stat().st_ino)
|
||||
self.assertEqual(link('-f', '--', source, target), 98)
|
||||
self.assertEqual(link('-T', '--', source, root / '..' / 'foreign'), 97)
|
||||
|
||||
def test_link_fixture_exposes_the_old_directory_restore_race(self):
|
||||
# The portable model must not launder the unsafe old argv into -T.
|
||||
self.source = self.source.replace('ln -T --', 'ln --')
|
||||
r, events, before, after, _ = self.exercise(fault="concurrent-restore-directory")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("remote:remove", events)
|
||||
self.assertNotIn("state:remove", events)
|
||||
nested = [n for n in after if n.startswith('init/pulse-agent.conf/')]
|
||||
self.assertEqual(len(nested), 1)
|
||||
self.assertEqual(after[nested[0]], before['init/pulse-agent.conf'])
|
||||
self.assertFalse(any(n.startswith('init/pulse-agent.conf.removal.') for n in after))
|
||||
|
||||
def test_refused_manager_precedes_remote_and_state_erasure(self):
|
||||
for fault in ("refused-stop", "refused-reload", "ignored-stop", "ignored-manual"):
|
||||
with self.subTest(fault=fault):
|
||||
|
||||
@@ -1320,6 +1320,12 @@ func TestCurrentPrereleasePacketTracksInstallMetadata(t *testing.T) {
|
||||
)
|
||||
assertFileDoesNotContain(t, releaseNotesPath, "## Fixes")
|
||||
comparisonSummary := "This changelog describes the changes since `v" + comparisonVersion + "`"
|
||||
if version == "6.6.0-beta.2" {
|
||||
// This packet's recorded list is cumulative from stable, not beta.1.
|
||||
// Keep its introduction bound to that actual list and rollback base.
|
||||
comparisonSummary = "This cumulative changelog describes the changes included in `v" + version + "` since stable `v" + previous + "`"
|
||||
assertFileContainsAllNormalized(t, changelogPath, "## Changes since v"+previous)
|
||||
}
|
||||
if version == "6.4.0-rc.10" {
|
||||
comparisonSummary = "The `v6.4.0-rc.9` release staged an immutable draft, tag, and exact-version artifacts but did not activate publicly."
|
||||
}
|
||||
|
||||
@@ -8478,6 +8478,8 @@ func TestInstallSHUpstartTeardownRequiresObservedQuiescence(t *testing.T) {
|
||||
t.Fatalf("DSM Upstart retirement and production EXIT cleanup controls: %v\n%s", err, output)
|
||||
}
|
||||
for _, name := range []string{
|
||||
"test_link_fixture_preserves_exclusive_no_target_directory_semantics",
|
||||
"test_link_fixture_exposes_the_old_directory_restore_race",
|
||||
"test_refused_manager_precedes_remote_and_state_erasure",
|
||||
"test_failed_empty_partial_diagnostic_status_cannot_authorise_removal",
|
||||
"test_effective_disabled_config_is_required_after_override_reload",
|
||||
@@ -8498,7 +8500,7 @@ func TestInstallSHUpstartTeardownRequiresObservedQuiescence(t *testing.T) {
|
||||
t.Fatalf("DSM Upstart control %s did not pass:\n%s", name, output)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(output), "\nRan 15 tests in ") || !strings.Contains(string(output), "\nOK\n") {
|
||||
if !strings.Contains(string(output), "\nRan 17 tests in ") || !strings.Contains(string(output), "\nOK\n") {
|
||||
t.Fatalf("incomplete DSM Upstart verdict:\n%s", output)
|
||||
}
|
||||
t.Logf("%s", output)
|
||||
|
||||
@@ -49,12 +49,34 @@ class AgentUninstallDocsTest(unittest.TestCase):
|
||||
for operation in ('remove_agent_state_dir "$STATE_DIR"',
|
||||
"teardown_action_runner_service", "teardown_privileged_helper_service",
|
||||
"remove_privileged_helper_state_dir", "remove_safe_profile_state_dir",
|
||||
'rm -f /var/log/pulse-agent.log', 'GO_SCRIPT="/boot/config/go"',
|
||||
"remove_qnap_autorun_block"):
|
||||
'rm -f /var/log/pulse-agent.log'):
|
||||
self.assertIn(operation, self.removal)
|
||||
self.assert_nas_boot_retirement_before_erasure(self.removal)
|
||||
self.assertIn('Remove-Item $StateDir -Recurse -Force',
|
||||
(ROOT / "scripts/install.ps1").read_text())
|
||||
|
||||
def assert_nas_boot_retirement_before_erasure(self, removal):
|
||||
# Removal no longer shares installation's best-effort boot-file edits.
|
||||
# Bind the existing boot-entry warning to the checked NAS lifecycle.
|
||||
quiesce = 'quiesce_nas_agent_for_removal "$uninstall_nas_platform"'
|
||||
retire = 'teardown_nas_agent_boot_owner "$uninstall_nas_platform"'
|
||||
for call in (quiesce, retire):
|
||||
self.assertIn(call + ' ||\n fail ', removal)
|
||||
self.assertLess(removal.index(quiesce), removal.index('uninstall_collector_registration'))
|
||||
self.assertLess(removal.index(retire), removal.index('remove_agent_state_dir "$STATE_DIR"'))
|
||||
for old_edit in ('GO_SCRIPT="/boot/config/go"', 'remove_qnap_autorun_block'):
|
||||
self.assertNotIn(old_edit, removal)
|
||||
|
||||
def test_nas_boot_guidance_rejects_missing_late_or_ignored_retirement(self):
|
||||
call = 'teardown_nas_agent_boot_owner "$uninstall_nas_platform"'
|
||||
late = self.removal.replace(call, ':').replace('remove_agent_state_dir "$STATE_DIR"',
|
||||
'remove_agent_state_dir "$STATE_DIR"; ' + call + ' ||\n fail "late" 1')
|
||||
for changed in (self.removal.replace(call, ':'), late,
|
||||
self.removal.replace(call + ' ||', call + ' || true #')):
|
||||
with self.subTest(mutation=changed):
|
||||
with self.assertRaises(AssertionError):
|
||||
self.assert_nas_boot_retirement_before_erasure(changed)
|
||||
|
||||
def test_saved_state_trust_and_explicit_path_authority_are_not_bypassed(self):
|
||||
for fact in ("same trusted Pulse instance", "on the affected agent host",
|
||||
"top-level `install.sh`", "Uninstallation needs no new token",
|
||||
|
||||
Reference in New Issue
Block a user