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:
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
'''
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user