"""资源预检的反例和部署策略回归;仅在批准的远程测试环境运行。""" import configparser import importlib.util import io import json from pathlib import Path import tempfile from types import SimpleNamespace import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] SPEC = importlib.util.spec_from_file_location('check_resources', ROOT / 'deploy/check_resources.py') RESOURCES = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(RESOURCES) MIB = RESOURCES.MIB def snapshot(): return { 'stable': True, 'go_memory_bytes': 256 * MIB, 'host_available_bytes': 1024 * MIB, 'host_total_bytes': 2048 * MIB, 'oom': {'oom': 0, 'oom_kill': 0}, 'parents': [], 'service': { 'MemoryHigh': str(384 * MIB), 'MemoryMax': str(512 * MIB), 'MemorySwapMax': '0', 'TasksMax': '256', 'OOMPolicy': 'stop', 'KillMode': 'control-group', 'Restart': 'on-failure', 'RestartUSec': '10s', 'StartLimitIntervalUSec': '5min', 'StartLimitBurst': '3', 'TimeoutStopUSec': '1min 30s', }, 'kernel': { 'memory.high': str(384 * MIB), 'memory.max': str(512 * MIB), 'memory.current': str(128 * MIB), 'memory.swap.max': '0', 'pids.max': '256', 'pids.current': '32', 'cpu.max': '100000 100000', }, } class ResourcePolicyTests(unittest.TestCase): def test_valid_budget_is_not_production_approval(self): result = RESOURCES.evaluate(snapshot(), 256 * MIB) self.assertTrue(result['resource_checks_passed']) self.assertFalse(result['production_approved']) self.assertEqual(result['remaining_growth_bytes'], 384 * MIB) self.assertEqual(result['failures'], []) def test_missing_and_unsafe_limits_fail_closed(self): cases = [ ('kernel', 'memory.high', 'max'), ('kernel', 'memory.max', 'max'), ('kernel', 'memory.high', str(512 * MIB)), ('kernel', 'memory.high', '0'), ('kernel', 'memory.current', str(384 * MIB)), ('kernel', 'memory.swap.max', 'max'), ('kernel', 'memory.swap.max', str(MIB)), ('kernel', 'pids.max', 'max'), ('kernel', 'pids.max', '0'), ('kernel', 'pids.current', '256'), ('kernel', 'cpu.max', 'max 100000'), ('service', 'MemoryMax', 'infinity'), ('service', 'MemoryHigh', str(256 * MIB)), ('service', 'MemorySwapMax', 'infinity'), ('service', 'TasksMax', '512'), ('service', 'OOMPolicy', 'continue'), ('service', 'KillMode', 'process'), ('service', 'TimeoutStopUSec', '60s'), ('service', 'TimeoutStopUSec', 'infinity'), ('service', 'Restart', 'always'), ('service', 'RestartUSec', '100ms'), ('service', 'RestartUSec', 'infinity'), ('service', 'StartLimitIntervalUSec', '0'), ('service', 'StartLimitIntervalUSec', '5s'), ('service', 'StartLimitBurst', '0'), ('service', 'StartLimitBurst', '6'), ('oom', 'oom', 1), ('oom', 'oom_kill', 1), ] for section, key, value in cases: with self.subTest(section=section, key=key, value=value): candidate = snapshot() candidate[section][key] = value report = RESOURCES.evaluate(candidate, 256 * MIB) self.assertFalse(report['resource_checks_passed']) self.assertTrue(report['failures']) def test_process_budget_and_restart_race_fail_closed(self): for key, values in { 'go_memory_bytes': [None, 0, 63 * MIB, 384 * MIB, 1024 * MIB], 'stable': [False], 'host_available_bytes': [128 * MIB, 512 * MIB], 'host_total_bytes': [512 * MIB], }.items(): for value in values: with self.subTest(key=key, value=value): candidate = snapshot() candidate[key] = value self.assertFalse(RESOURCES.evaluate(candidate, 256 * MIB)['resource_checks_passed']) self.assertFalse(RESOURCES.evaluate(snapshot(), 0)['resource_checks_passed']) def test_ancestor_limits_cannot_be_ignored(self): baseline = {'memory.high': 'max', 'memory.max': 'max', 'memory.current': str(256 * MIB), 'pids.max': 'max', 'pids.current': '64', 'cpu.max': 'max 100000'} cases = [('memory.high', str(768 * MIB)), ('memory.max', str(768 * MIB)), ('pids.max', '256'), ('cpu.max', '50000 100000')] for key, value in cases: with self.subTest(key=key): candidate = snapshot() limits = {**baseline, key: value} candidate['parents'] = [{'path': 'system.slice', 'limits': limits}] result = RESOURCES.evaluate(candidate, 256 * MIB) self.assertFalse(result['resource_checks_passed']) self.assertTrue(any('system.slice' in failure for failure in result['failures'])) candidate = snapshot() candidate['parents'] = [{'path': 'system.slice', 'limits': baseline}] self.assertTrue(RESOURCES.evaluate(candidate, 256 * MIB)['resource_checks_passed']) def test_environment_parser_never_returns_credentials(self): environment = b'SMAIL_SECRET=must-not-appear\0GOMEMLIMIT=256MiB\0TOKEN=also-secret\0' self.assertEqual(RESOURCES.go_memory_limit(environment), 256 * MIB) for value in (b'', b'off', b'256MB', b'1.5GiB', b'secret', b'-1', b' 256MiB', b'\xff'): with self.subTest(value=value): self.assertIsNone(RESOURCES.go_memory_limit(b'GOMEMLIMIT=' + value + b'\0')) self.assertIsNone(RESOURCES.go_memory_limit(environment + b'GOMEMLIMIT=128MiB\0')) self.assertIsNone(RESOURCES.go_memory_limit(b'TOKEN=secret\0')) self.assertEqual(RESOURCES.go_memory_limit(b'GOMEMLIMIT=1024\0'), 1024) self.assertEqual(RESOURCES.go_memory_limit(b'GOMEMLIMIT=1GiB\0'), 1024 * MIB) def test_duration_and_cpu_parsing(self): self.assertEqual(RESOURCES.duration_usec('5min'), 300000000) self.assertEqual(RESOURCES.duration_usec('1min 2.5s'), 62500000) self.assertEqual(RESOURCES.duration_usec('500ms'), 500000) for value in ('infinity', 'junk10s', '10s junk', '-1s', ''): self.assertIsNone(RESOURCES.duration_usec(value)) self.assertEqual(RESOURCES.cpu_quota('25000 100000'), 0.25) self.assertIsNone(RESOURCES.cpu_quota('max 100000')) for value in ('0 100000', '100000 0', '-1 100000', 'unknown'): with self.assertRaises(ValueError): RESOURCES.cpu_quota(value) def test_systemd_query_is_read_only_and_does_not_request_environment(self): output = '\n'.join(name + '=value' for name in RESOURCES.PROPERTIES) output += '\nEnvironment=TOKEN=must-not-appear' with patch.object(RESOURCES.subprocess, 'run', return_value=SimpleNamespace(stdout=output)) as run: result = RESOURCES.systemd_state() self.assertEqual(set(result), set(RESOURCES.PROPERTIES)) self.assertNotIn('must-not-appear', json.dumps(result)) command = run.call_args.args[0] self.assertEqual(command[:3], ['systemctl', 'show', 'smail.service']) self.assertNotIn('Environment', command[-1]) self.assertNotIn('ExecStart', command[-1]) self.assertNotIn('shell', run.call_args.kwargs) with patch.object(RESOURCES.subprocess, 'run', return_value=SimpleNamespace(stdout='MainPID=1')): with self.assertRaises(ValueError): RESOURCES.systemd_state() def test_collect_refuses_inactive_service_before_reading_process(self): with patch.object(RESOURCES, 'systemd_state', return_value={ 'MainPID': '0', 'ActiveState': 'inactive', 'SubState': 'dead', }): with self.assertRaises(ValueError): RESOURCES.collect() def collection_fixture(self): root = Path(tempfile.mkdtemp(prefix='smail-resource-')) base = root / 'sys/fs/cgroup' group = base / 'system.slice/smail.service' proc = root / 'proc/123' group.mkdir(parents=True) proc.mkdir(parents=True) for name, value in snapshot()['kernel'].items(): (group / name).write_text(value, encoding='ascii') (group / 'memory.events').write_text('oom 0\noom_kill 0\n', encoding='ascii') (proc / 'cgroup').write_text('0::/system.slice/smail.service\n', encoding='ascii') (proc / 'environ').write_bytes(b'GOMEMLIMIT=256MiB\0TOKEN=must-not-appear\0') (root / 'proc/meminfo').write_text('MemAvailable: 1048576 kB\nMemTotal: 2097152 kB\n', encoding='ascii') state = {**snapshot()['service'], 'MainPID': '123', 'NRestarts': '0', 'InvocationID': 'fixture', 'ActiveState': 'active', 'SubState': 'running', 'ControlGroup': '/system.slice/smail.service'} def locate(value): path = Path(value) if str(path).startswith('/proc') or str(path).startswith('/sys/fs/cgroup'): return root / str(path).lstrip('/') return path return root, state, locate def test_collect_reads_kernel_and_only_the_go_environment(self): root, state, locate = self.collection_fixture() with patch.object(RESOURCES, 'systemd_state', side_effect=[state, state.copy()]), \ patch.object(RESOURCES, 'Path', side_effect=locate): result = RESOURCES.collect() self.assertTrue(result['stable']) self.assertEqual(result['go_memory_bytes'], 256 * MIB) self.assertEqual(result['host_available_bytes'], 1024 * MIB) self.assertNotIn('must-not-appear', json.dumps(result)) self.assertEqual([parent['path'] for parent in result['parents']], ['system.slice', '.']) self.assertTrue(RESOURCES.evaluate(result, 256 * MIB)['resource_checks_passed']) def test_collect_detects_service_restart(self): root, state, locate = self.collection_fixture() changed = {**state, 'InvocationID': 'new-process'} with patch.object(RESOURCES, 'systemd_state', side_effect=[state, changed]), \ patch.object(RESOURCES, 'Path', side_effect=locate): result = RESOURCES.collect() self.assertFalse(result['stable']) self.assertFalse(RESOURCES.evaluate(result, 256 * MIB)['resource_checks_passed']) def test_collect_refuses_pid_in_another_group(self): root, state, locate = self.collection_fixture() (root / 'proc/123/cgroup').write_text('0::/system.slice/other.service\n', encoding='ascii') with patch.object(RESOURCES, 'systemd_state', return_value=state), \ patch.object(RESOURCES, 'Path', side_effect=locate): with self.assertRaises(ValueError): RESOURCES.collect() def test_cli_redacts_collection_errors_and_fails(self): output = io.StringIO() with patch('sys.argv', ['check_resources.py', '--reserve-mib', '256']), \ patch.object(RESOURCES.os, 'name', 'posix'), \ patch.object(RESOURCES.os, 'geteuid', return_value=0, create=True), \ patch.object(RESOURCES, 'collect', side_effect=OSError('TOKEN=must-not-appear')), \ patch('sys.stdout', output): status = RESOURCES.main() self.assertEqual(status, 1) self.assertNotIn('TOKEN', output.getvalue()) self.assertFalse(json.loads(output.getvalue())['production_approved']) def test_cli_reports_success_without_production_approval(self): output = io.StringIO() with patch('sys.argv', ['check_resources.py', '--reserve-mib', '256']), \ patch.object(RESOURCES.os, 'name', 'posix'), \ patch.object(RESOURCES.os, 'geteuid', return_value=0, create=True), \ patch.object(RESOURCES, 'collect', return_value=snapshot()), \ patch('sys.stdout', output): self.assertEqual(RESOURCES.main(), 0) self.assertTrue(json.loads(output.getvalue())['resource_checks_passed']) self.assertFalse(json.loads(output.getvalue())['production_approved']) def test_deployment_unit_has_bounded_resources_and_restart_policy(self): unit = configparser.ConfigParser(interpolation=None) unit.read(ROOT / 'deploy/systemd/smail.service', encoding='utf-8') values = unit['Service'] for key, value in {'MemoryHigh': '384M', 'MemoryMax': '512M', 'MemorySwapMax': '0', 'CPUQuota': '100%', 'TasksMax': '256', 'OOMPolicy': 'stop', 'KillMode': 'control-group', 'Restart': 'on-failure', 'RestartSec': '10s', 'TimeoutStopSec': '90s', 'MemoryAccounting': 'true', 'CPUAccounting': 'true', 'TasksAccounting': 'true'}.items(): self.assertEqual(values[key], value) self.assertEqual(unit['Unit']['StartLimitIntervalSec'], '300s') self.assertEqual(unit['Unit']['StartLimitBurst'], '3') def test_installer_preserves_existing_environment_budget(self): source = (ROOT / 'deploy/install-systemd.sh').read_text(encoding='utf-8') guard = source.index('if [ ! -e /etc/smail/smail.env ]; then') end = source.index('\nfi', guard) assignment = source.index("'GOMEMLIMIT=256MiB'") self.assertTrue(guard < assignment < end) self.assertEqual(source.count('GOMEMLIMIT='), 1) self.assertIn('chmod 0600 /etc/smail/smail.env', source) self.assertIn('cp -p -- /etc/smail/smail.env "$backup_dir/smail.env"', source) if __name__ == '__main__': unittest.main(verbosity=2)