Freeze lab-tested PostgreSQL plugin engine

Extract PostgreSQL, Podman runtime and systemd lifecycle adapters while preserving existing lifecycle and scoring behavior. Update RPM layout and add guarded operator-run lab harnesses.

Record successful manual stop/start, promotion, demotion and rewind-only rejoin with pinned installed hashes. The unfenced lab does not validate automatic failover, Pacemaker-managed transitions or divergent-data rewind.

Validation: 103 offline tests, original-function differential checks, checksum manifest and whitespace checks passed.
This commit is contained in:
Lyncoln
2026-09-13 01:57:04 -04:00
parent 488a1672d7
commit ec2077f979
41 changed files with 1925 additions and 331 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Shared test loader and narrowly reversible command-boundary substitutions."""
from pathlib import Path
import re
import shlex
ROOT = Path(__file__).resolve().parents[1]
MODULE_DIR = ROOT / 'refactored/managed-container.d'
LOAD = '''MC_PROMOTION_RESOURCE_INSTANCE="${OCF_RESOURCE_INSTANCE:-}"
: "${OCF_ROOT:=/usr/lib/ocf}"
. "$OCF_ROOT/lib/heartbeat/ocf-shellfuncs"
: "${OCF_RUNNING_PROMOTED:=${OCF_RUNNING_MASTER:-8}}"
MC_MODULE_DIR=''' + shlex.quote(str(MODULE_DIR)) + '''
. "$MC_MODULE_DIR/common.sh"
. "$MC_MODULE_DIR/container.sh"
. "$MC_MODULE_DIR/engine.sh"
mc_engine_load || exit $?
'''
def restore_boundaries(source):
"""Project only adapter calls back to their frozen original spelling.
This is not whitespace normalization or a general semantic equivalence
claim. All remaining function bytes must match the immutable reference.
"""
source = source.replace('mc_runtime_inspect', 'as_service_user podman inspect')
source = source.replace('mc_runtime_info', 'as_service_user podman info')
pattern = r'^([ \t]*)(.*?)mc_runtime_exec \\\n'
source = re.sub(pattern, lambda m: m[1] + m[2] + 'as_service_user \\\n' + m[1] + ' podman exec \\\n', source, flags=re.M)
pattern = r'^([ \t]*)(.*?)mc_lifecycle_(start|stop|cat|running) '
source = re.sub(pattern, lambda m: m[1] + m[2] + 'as_service_user \\\n' + m[1] + ' systemctl --user ' +
('is-active --quiet' if m[3] == 'running' else m[3]) + ' ', source, flags=re.M)
return source
+1 -1
View File
@@ -12,7 +12,7 @@ import unittest
from test_promotion_scores import Scores, LOAD, FIXTURE
ROOT = Path(__file__).resolve().parents[1]
HELPER = ROOT / 'refactored/managed-container.d/promotion_state.py'
HELPER = ROOT / 'refactored/managed-container.d/applications/postgresql/promotion_state.py'
spec = importlib.util.spec_from_file_location('promotion_state', HELPER)
policy = importlib.util.module_from_spec(spec)
spec.loader.exec_module(policy)
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Stop/start harness tests with mocked commands and temporary evidence only."""
import contextlib
import importlib.util
import io
import os
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('cycle', ROOT / 'scripts/test-elitedesk-cycle.py')
cycle = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cycle)
class Cycle(unittest.TestCase):
def scenario(self, failure=None):
with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack:
state = {'running': True, 'started': 'old'}
calls = []
def command(args, **kwargs):
action = args[-1]; calls.append(action)
self.assertIn(action, ['validate-all', 'monitor', 'stop', 'start', 'status'])
if action == failure:
return SimpleNamespace(returncode=1, stdout='', stderr='injected failure')
if action == 'stop': state['running'] = False
if action == 'start': state.update(running=True, started='new')
rc = 7 if action == 'monitor' and not state['running'] else 0
return SimpleNamespace(returncode=rc, stdout='', stderr='')
stack.enter_context(patch.object(cycle.os, 'geteuid', return_value=0))
stack.enter_context(patch.object(cycle.socket, 'gethostname', return_value='DAVID-ELITEDESK'))
stack.enter_context(patch.object(cycle.lab, 'BACKUP_ROOT', Path(directory)))
stack.enter_context(patch.object(cycle.lab, 'cluster', return_value={}))
stack.enter_context(patch.object(cycle.lab, 'observe', side_effect=lambda:
dict(system_id='same', started=state['started'])))
stack.enter_context(patch.object(cycle, 'verify_payload'))
stack.enter_context(patch.object(cycle, 'owner_command', side_effect=lambda *args:
'inactive' if args[0] == 'systemctl' else ''))
stack.enter_context(patch.object(cycle, 'score', side_effect=lambda:
'100' if state['running'] else '-INFINITY'))
stack.enter_context(patch.object(cycle.subprocess, 'run', side_effect=command))
old_mask = os.umask(0o022)
try:
with contextlib.redirect_stdout(io.StringIO()):
if failure:
with self.assertRaises(RuntimeError): cycle.main()
self.assertEqual(calls[-1], failure)
self.assertEqual(calls.count(failure), 1)
else:
cycle.main()
self.assertEqual(calls, ['validate-all','monitor','stop','monitor','start','monitor','status'])
finally:
os.umask(old_mask)
self.assertEqual(len(list(Path(directory).glob('*/results.json'))), 1)
def test_successful_cycle(self): self.scenario()
def test_stop_failure_halts_without_start(self): self.scenario('stop')
def test_start_failure_halts_without_retry(self): self.scenario('start')
if __name__ == '__main__':
unittest.main(verbosity=2)
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Exercise publication and rollback using temp files and mocked lab boundaries."""
import contextlib
import hashlib
import importlib.util
import io
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('installer', ROOT / 'scripts/test-elitedesk-plugin.py')
installer = importlib.util.module_from_spec(spec)
spec.loader.exec_module(installer)
class Installer(unittest.TestCase):
def scenario(self, fail_monitor=False):
with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack:
directory = Path(directory)
provider = directory / 'provider'; provider.mkdir()
agent = provider / 'managed-container'; agent.write_text('previous agent')
modules = provider / 'managed-container.d'; modules.mkdir()
old_module = modules / 'old.sh'; old_module.write_text('previous module')
baseline = directory / 'baseline'
baseline.write_text(''.join(hashlib.sha256(p.read_bytes()).hexdigest() + ' ' + str(p.relative_to(provider)) + '\n'
for p in [agent, old_module]))
backup = directory / 'backups'; backup.mkdir()
for name, value in dict(PROVIDER=provider, AGENT=agent, MODULES=modules,
BASELINE=baseline, BACKUP_ROOT=backup).items():
stack.enter_context(patch.object(installer, name, value))
stack.enter_context(patch.object(installer.os, 'geteuid', return_value=0))
stack.enter_context(patch.object(installer.socket, 'gethostname', return_value='DAVID-ELITEDESK'))
stack.enter_context(patch.object(installer.signal, 'signal'))
stack.enter_context(patch.object(installer, 'cluster', return_value={}))
stack.enter_context(patch.object(installer, 'observe', return_value={'unchanged': True}))
stack.enter_context(patch.dict(os.environ))
calls = []
def fake_run(args, **kwargs):
calls.append(args)
if args[0] == 'restorecon': return ''
if args[0] == 'crm_attribute': return '100'
require_action = args[1]
self.assertIn(require_action, ['meta-data', 'validate-all', 'monitor', 'status', 'start'])
if require_action == 'meta-data':
return '<resource-agent><parameters>' + '<parameter/>' * 16 + '</parameters></resource-agent>'
if fail_monitor and Path(args[0]) == agent and require_action == 'monitor':
raise RuntimeError('injected installed monitor failure')
return ''
stack.enter_context(patch.object(installer, 'run', side_effect=fake_run))
old_mask = os.umask(0o022)
try:
with contextlib.redirect_stdout(io.StringIO()):
if fail_monitor:
with self.assertRaisesRegex(RuntimeError, 'injected'):
installer.main()
self.assertEqual(agent.read_text(), 'previous agent')
self.assertEqual(old_module.read_text(), 'previous module')
else:
installer.main()
self.assertEqual(agent.read_bytes(), (ROOT / 'refactored/managed-container').read_bytes())
self.assertTrue((modules / 'applications/postgresql/plugin.sh').is_file())
self.assertFalse(old_module.exists())
finally:
os.umask(old_mask)
self.assertEqual(len(list(backup.glob('*/results.json'))), 1)
def test_success_only_runs_basic_actions(self):
self.scenario()
def test_failed_installed_monitor_restores_agent_and_modules(self):
self.scenario(fail_monitor=True)
if __name__ == '__main__':
unittest.main(verbosity=2)
+55
View File
@@ -0,0 +1,55 @@
"""Exercise the safety gates without running cluster or PostgreSQL commands."""
import importlib.util
from pathlib import Path
import unittest
spec = importlib.util.spec_from_file_location('roles', Path(__file__).resolve().parents[1] / 'scripts/test-lab-roles.py')
roles = importlib.util.module_from_spec(spec)
spec.loader.exec_module(roles)
def db(recovery, source=None, **changes):
return dict(dict(system_id='same', recovery=recovery, paused=False,
receiver='streaming' if source else None,
sender=roles.NODES.get(source)), **changes)
class Gates(unittest.TestCase):
def test_demote_requires_streaming_successor(self):
roles.guard('demote', 'DAVID-DV6', db(False), db(True, 'DAVID-DV6'))
for peer in [db(False), db(True), db(True, 'DAVID-DV6', paused=True)]:
with self.assertRaises(RuntimeError):
roles.guard('demote', 'DAVID-DV6', db(False), peer)
def test_promote_refuses_live_peer_primary(self):
with self.assertRaises(RuntimeError):
roles.guard('promote', 'DAVID-ELITEDESK', db(True, 'DAVID-DV6'), db(False))
def test_promote_requires_demoted_peer_streaming_back(self):
roles.guard('promote', 'DAVID-ELITEDESK', db(True), db(True, 'DAVID-ELITEDESK'))
with self.assertRaises(RuntimeError):
roles.guard('promote', 'DAVID-ELITEDESK', db(True), db(True))
def test_identity_mismatch_refused(self):
with self.assertRaises(RuntimeError):
roles.guard('demote', 'DAVID-DV6', db(False), db(True, 'DAVID-DV6', system_id='other'))
def test_rejoin_only_healthy_elitedesk_standby(self):
roles.guard('rejoin', 'DAVID-ELITEDESK', db(True, 'DAVID-DV6'), db(False))
for node, local, peer in [('DAVID-DV6', db(True, 'DAVID-ELITEDESK'), db(False)),
('DAVID-ELITEDESK', db(False), db(True)),
('DAVID-ELITEDESK', db(True, 'DAVID-DV6'), db(True))]:
with self.assertRaises(RuntimeError):
roles.guard('rejoin', node, local, peer)
def test_pinned_versions_match_lab_snapshots(self):
root = Path(__file__).resolve().parents[1]
for node, name in [('DAVID-DV6', 'lab-installed-before-plugin.sha256'),
('DAVID-ELITEDESK', 'lab-installed-plugin.sha256')]:
expected = {p.removeprefix(str(roles.PROVIDER) + '/'): h for h, p in
(line.split(' ', 1) for line in (root / 'docs' / name).read_text().splitlines())}
self.assertEqual(roles.PINNED[node], expected)
if __name__ == '__main__':
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Plugin contract tests: no real runtime, systemd, or cluster operations."""
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import unittest
import xml.etree.ElementTree as ET
from plugin_test_support import LOAD, ROOT
AGENT = ROOT / 'refactored/managed-container'
class Engine(unittest.TestCase):
def run_shell(self, code, rc=0, env=None):
result = subprocess.run(['bash', '-c', LOAD + '\n' + code], text=True,
capture_output=True, timeout=10, env=env)
self.assertEqual(result.returncode, rc, result.stderr)
return result
def trace(self, command, arguments, expected, rc=0):
args = ' '.join(shlex.quote(x) for x in arguments)
fixture = '''
as_service_user() {
python3 -c 'import json,sys; print(json.dumps(sys.argv[1:]))' "$@"
return ''' + str(rc) + '''
}
'''
result = self.run_shell(fixture + command + ' ' + args, rc)
self.assertEqual(json.loads(result.stdout), expected)
def test_exec_preserves_argument_boundaries(self):
args = ['--user', 'postgres', '--env', 'VALUE=a b\n$(touch /UNEXPECTED)',
'container', 'sh', '-c', 'printf "%s" "$1"', 'sh', "quotes'\" $HOME ;"]
self.trace('mc_runtime_exec', args, ['podman', 'exec', *args])
def test_exec_error_is_not_systemd_success_exit_status(self):
self.trace('mc_runtime_exec', ['container', 'false'], ['podman', 'exec', 'container', 'false'], 143)
def test_bounded_exec_preserves_timeout_location_and_status(self):
self.trace('mc_runtime_exec_bounded', ['3s', '--user', 'postgres', 'container', 'psql'],
['timeout', '--kill-after=1s', '3s', 'podman', 'exec', '--user', 'postgres', 'container', 'psql'], 124)
def test_inspect_and_info(self):
self.trace('mc_runtime_inspect', ['--format', '{{.State.Running}}', 'container'],
['podman', 'inspect', '--format', '{{.State.Running}}', 'container'])
self.trace('mc_runtime_info', [], ['podman', 'info'], 125)
def test_lifecycle_uses_systemd_only(self):
for action, args in [('start', ['start']), ('stop', ['stop']),
('running', ['is-active', '--quiet']), ('cat', ['cat'])]:
self.trace('mc_lifecycle_' + action, ['postgres.service'],
['systemctl', '--user', *args, 'postgres.service'])
self.trace('mc_lifecycle_stop', ['postgres.service'],
['systemctl', '--user', 'stop', 'postgres.service'], 1)
def test_application_dispatch_and_return_codes(self):
for action in ['start', 'stop', 'promote', 'demote', 'rejoin']:
result = self.run_shell('managed_' + action + '() { echo ' + action + '; return 6; }; '
'mc_application_action ' + action, 6)
self.assertEqual(result.stdout.strip(), action)
self.run_shell('mc_application_action invalid', 3)
self.run_shell('managed_monitor() { return 8; }; mc_application_monitor', 8)
self.run_shell('managed_notify() { return 1; }; mc_application_notify', 1)
def test_unsupported_or_path_like_selection_rejected_before_commands(self):
for key, value in [('application', 'mysql'), ('application', '../postgresql'),
('container_runtime', 'docker'), ('lifecycle_manager', 'runtime'),
('application', '$(touch /UNEXPECTED)')]:
env = dict(os.environ, **{'OCF_RESKEY_' + key: value})
result = subprocess.run([str(AGENT), 'monitor'], env=env, text=True, capture_output=True)
self.assertEqual(result.returncode, 6)
self.assertIn('Supported plugin combination', result.stderr)
def test_missing_and_incompatible_plugins(self):
for kind in ['missing', 'version', 'hook']:
with tempfile.TemporaryDirectory(prefix='mc-plugin-loader-') as directory:
target = Path(directory) / 'refactored'
shutil.copytree(AGENT.parent, target, ignore=shutil.ignore_patterns('__pycache__'))
plugin = target / 'managed-container.d/runtimes/podman.sh'
if kind == 'missing':
plugin.unlink()
elif kind == 'version':
plugin.write_text(plugin.read_text().replace('MC_RUNTIME_API=1', 'MC_RUNTIME_API=2'))
else:
plugin.write_text(plugin.read_text().replace('mc_runtime_exec()', 'missing_hook()'))
result = subprocess.run([str(target / 'managed-container'), 'meta-data'],
text=True, capture_output=True, cwd='/tmp')
self.assertEqual(result.returncode, 6, result.stderr)
self.assertEqual(result.stdout, '')
def test_metadata_without_instance_and_with_explicit_defaults(self):
env = {k: v for k, v in os.environ.items() if not k.startswith('OCF_')}
baseline = subprocess.check_output([str(AGENT), 'meta-data'], env=env, cwd='/tmp', text=True)
tree = ET.fromstring(baseline)
params = {p.get('name'): p.find('content').get('default') for p in tree.findall('./parameters/parameter')}
self.assertEqual(len(params), 16)
for key, value in [('application', 'postgresql'), ('container_runtime', 'podman'), ('lifecycle_manager', 'systemd')]:
self.assertEqual(params[key], value)
env['OCF_RESKEY_' + key] = value
self.assertEqual(subprocess.check_output([str(AGENT), 'meta-data'], env=env, text=True), baseline)
def test_metadata_failure_is_not_hidden(self):
self.run_shell('mc_application_metadata() { return 1; }; mc_engine_metadata', 1)
def test_application_cannot_call_runtime_or_systemd_directly(self):
# All execution crosses an explicit adapter; command dependency names
# remain part of PostgreSQL's compatibility validation in this revision.
for name in ['postgres', 'auth', 'replication', 'notify', 'promote', 'demote', 'rejoin']:
text = (AGENT.parent / 'managed-container.d/applications/postgresql' / (name + '.sh')).read_text()
self.assertNotIn('as_service_user', text, name)
self.assertNotIn('systemctl --user', text, name)
self.assertNotIn('podman exec', text, name)
if __name__ == '__main__':
unittest.main(verbosity=2)
+2 -4
View File
@@ -12,9 +12,7 @@ import unittest
ROOT = Path(__file__).resolve().parents[1]
AGENT = ROOT / 'refactored/managed-container'
MODULES = ('common', 'container', 'postgres', 'auth', 'replication', 'notify', 'promote', 'demote', 'rejoin')
LOAD = 'MC_PROMOTION_RESOURCE_INSTANCE="${OCF_RESOURCE_INSTANCE:-}"\n: "${OCF_ROOT:=/usr/lib/ocf}"\n. "$OCF_ROOT/lib/heartbeat/ocf-shellfuncs"\n: "${OCF_RUNNING_PROMOTED:=${OCF_RUNNING_MASTER:-8}}"\n'
LOAD += '\n'.join('. ' + shlex.quote(str(AGENT.parent / 'managed-container.d' / (m + '.sh'))) for m in MODULES)
from plugin_test_support import LOAD
CRM = '''#!/usr/bin/env python3
import json, os, sys
@@ -282,7 +280,7 @@ class Scores(unittest.TestCase):
for action in ('monitor', 'status'):
code = 'mc_promotion_monitor() { echo scored-monitor; return 8; }; set -- ' + action + '\ncase "$1" in' + dispatch
self.assertEqual(self.run_shell(code, 8).stdout.strip(), 'scored-monitor')
for action, handler in [('meta-data', 'mc_promotion_metadata'), ('validate-all', 'mc_promotion_validate'), ('notify', 'managed_notify')]:
for action, handler in [('meta-data', 'mc_engine_metadata'), ('validate-all', 'mc_promotion_validate'), ('notify', 'mc_application_notify')]:
code = handler + '() { echo plain; return 0; }; set -- ' + action + '\ncase "$1" in' + dispatch
self.assertEqual(self.run_shell(code).stdout.strip(), 'plain')
self.assertFalse(self.state()['calls'])
+9 -4
View File
@@ -18,11 +18,11 @@ assert output('rpm', '-q', '--qf', '%{ARCH}', PACKAGE) == 'noarch'
assert output('rpm', '-q', '--scripts', PACKAGE) == ''
subprocess.run(['rpm', '-V', PACKAGE], check=True)
expected = {AGENT: ROOT / 'refactored/managed-container'}
for path in (ROOT / 'refactored/managed-container.d').glob('*'):
if not path.is_file():
for path in (ROOT / 'refactored/managed-container.d').rglob('*'):
if not path.is_file() or path.suffix not in ('.sh', '.py'):
continue
expected[PROVIDER / 'managed-container.d' / path.name] = path
assert len(expected) == 11
expected[PROVIDER / path.relative_to(ROOT / 'refactored')] = path
assert len(expected) == 15
for path, source in expected.items():
assert path.read_bytes() == source.read_bytes(), path
info = path.stat()
@@ -31,6 +31,11 @@ for path, source in expected.items():
owned = set(output('rpm', '-ql', PACKAGE).splitlines())
allowed = {str(path) for path in expected} | {str(PROVIDER), str(PROVIDER / 'managed-container.d')}
for path in expected:
allowed.update(str(parent) for parent in path.parents if parent == PROVIDER or PROVIDER in parent.parents)
for directory in allowed - {str(path) for path in expected}:
info = Path(directory).stat()
assert (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) == (0, 0, 0o755), directory
unexpected = {path for path in owned - allowed if not path.startswith('/usr/share/doc/' + PACKAGE)}
assert not unexpected, unexpected
+1 -1
View File
@@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory(prefix='mc-score-sql-') as directory:
query_file = Path(directory) / 'query.sql'
env = dict(os.environ, QUERY_FILE=str(query_file))
extract = '. ' + shlex.quote(str(ROOT / 'refactored/managed-container.d/postgres.sh')) + '''
extract = '. ' + shlex.quote(str(ROOT / 'refactored/managed-container.d/runtimes/podman.sh')) + '\n. ' + shlex.quote(str(ROOT / 'refactored/managed-container.d/applications/postgresql/postgres.sh')) + '''
as_service_user() { printf '%s' "${@: -1}" > "$QUERY_FILE"; echo t; }
mc_pg_promotion_eligible
'''
+1 -1
View File
@@ -12,7 +12,7 @@ from test_disconnected_state import CONNECTED, DISCONNECTED
assert os.geteuid() == 0 and os.environ.get('MC_DISPOSABLE_STATE_TEST') == '1', (
'Run only in a disposable container with MC_DISPOSABLE_STATE_TEST=1')
helper = (Path(sys.argv[1]) if len(sys.argv) > 1 else
Path(__file__).resolve().parents[1] / 'refactored/managed-container.d/promotion_state.py')
Path(__file__).resolve().parents[1] / 'refactored/managed-container.d/applications/postgresql/promotion_state.py')
resource = 'mc-disposable-' + uuid.uuid4().hex
args = ['python3', str(helper), resource, 'test-owner', 'test.service', 'test-container', '300']
+11 -13
View File
@@ -6,11 +6,12 @@ import re
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from plugin_test_support import LOAD, restore_boundaries
ROOT = Path(__file__).resolve().parents[1]
REFERENCE = ROOT / 'reference/managed-container'
AGENT = ROOT / 'refactored/managed-container'
MODULES = sorted((AGENT.parent / 'managed-container.d').glob('*.sh'))
MODULES = sorted((AGENT.parent / 'managed-container.d').rglob('*.sh'))
SOURCE = REFERENCE.read_text()
PATTERN = r'^([a-z_]+)\(\)\n\{\n.*?^\}'
@@ -43,21 +44,16 @@ print('PASS both reference hashes')
current_functions = functions('\n'.join(p.read_text() for p in MODULES))
for name, definition in functions(SOURCE).items():
assert current_functions[name] == definition, name
assert set(current_functions) - set(functions(SOURCE)) == {
'mc_promotion_score_set', 'mc_promotion_score_from_monitor',
'mc_promotion_monitor', 'mc_promotion_action', 'mc_promotion_validate', 'mc_pg_promotion_eligible',
'mc_promotion_metadata', 'mc_pg_standby_observation', 'mc_pg_standby_score',
}
assert restore_boundaries(current_functions[name]) == definition, name
assert len(functions(SOURCE)) == 29
common = (AGENT.parent / 'managed-container.d/common.sh').read_text()
common = (AGENT.parent / 'managed-container.d/applications/postgresql/plugin.sh').read_text()
defaults = SOURCE[SOURCE.index('OCF_RESKEY_service_user_default='):SOURCE.index('\n###############################################################################\n# Metadata')].strip()
assert defaults in common
print('PASS all 29 original function bodies and defaults/globals identical; nine scoring helpers added')
print('PASS all 29 original function bodies identical after exact adapter-call reversal; defaults unchanged')
for path in [AGENT, *MODULES]:
subprocess.run(['bash', '-n', str(path)], check=True)
print('PASS syntax for entrypoint and all nine modules')
print('PASS syntax for entrypoint and all modules')
metadata = []
for path in (REFERENCE, AGENT):
@@ -69,6 +65,10 @@ parameters = extended.find('parameters')
added = parameters.find("parameter[@name='disconnected_max_age']")
assert added is not None and added.find('content').get('default') == '300'
parameters.remove(added)
for name, default in [('application', 'postgresql'), ('container_runtime', 'podman'), ('lifecycle_manager', 'systemd')]:
added = parameters.find("parameter[@name='" + name + "']")
assert added is not None and added.find('content').get('default') == default
parameters.remove(added)
def normalize(element):
return (element.tag, element.attrib, (element.text or '').strip(),
[normalize(child) for child in element])
@@ -80,9 +80,7 @@ print('PASS original metadata preserved plus freshness parameter and XML parsing
# Sourcing production modules in a fresh shell tests their real loading order.
# No dispatcher executes. All operational boundaries are overridden below.
modular_definitions = SOURCE[:SOURCE.index('\n###############################################################################')]
for name in ('common', 'container', 'postgres', 'auth', 'replication', 'notify', 'promote', 'demote', 'rejoin'):
modular_definitions += '\n. ' + "'" + str(AGENT.parent / 'managed-container.d' / (name + '.sh')).replace("'", "'\\''") + "'"
modular_definitions = LOAD
base = '''
ocf_log() { printf 'log %s\\n' "$*"; }
ocf_exit_reason() { printf 'reason %s\\n' "$*" >&2; }