fix: guard OCI scheduled updates after stop

This commit is contained in:
VAIO73
2026-10-16 18:27:33 +00:00
parent 690ace1e57
commit 9e6978cf10
2 changed files with 47 additions and 0 deletions
+9
View File
@@ -22772,6 +22772,15 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
if requested_targets == ['oci_image']:
# A container installed by OCI manager Apps is updated by replacing
# its image, through the same flow as the OCI menu, unattended.
# OCI updates have their own runner rather than apply_updates.sh.
# Check once more immediately before handing control to it: a user
# may have intentionally stopped the CT after the scheduler's first
# observation, and the OCI transaction can start a replacement CT
# while validating its new image.
if _fast_guest_status(vmid, 'lxc') != 'running':
reason_code = 'stopped_container'
reasons.append('container was stopped before scheduled update could begin')
return finish('skipped', 'oci_image', [])
command = ['bash', '/usr/local/share/proxmenux/scripts/oci/oci_manager_apps.sh',
'manage', str(vmid), '--action', 'update', '--unattended']
storage = (sched.get('backup_storage') or '').strip()
+38
View File
@@ -1,6 +1,7 @@
import ast
import json
import os
import re
import sys
import subprocess
import tempfile
@@ -429,6 +430,43 @@ class ScheduledStoppedContainerTests(unittest.TestCase):
stopped_marker.assert_called_once()
reboot_probe.assert_called_once()
def test_oci_race_to_stopped_skips_before_the_oci_engine_runs(self):
runner = MagicMock(return_value=SimpleNamespace(returncode=0, stdout="", stderr=""))
finalizer = MagicMock(return_value={"recorded": True})
status = MagicMock(side_effect=["running", "stopped"])
scope = {
"time": SimpleNamespace(monotonic=MagicMock(return_value=10.0)),
"uuid": SimpleNamespace(uuid4=lambda: SimpleNamespace(hex="c" * 32)),
"datetime": __import__("datetime").datetime,
"subprocess": SimpleNamespace(
run=runner,
STDOUT=object(),
DEVNULL=object(),
TimeoutExpired=subprocess.TimeoutExpired,
),
"os": os,
"re": re,
"_create_lxc_update_log": lambda _vmid, _run_id: (None, None),
"_fast_guest_status": status,
"_normalise_schedule_targets": lambda _sched: ["oci_image"],
"_lxc_update_snapshot": lambda _vmid: {"ct_name": "fixture"},
"_lxc_update_target_labels": lambda *_: ["OCI image"],
"_append_lxc_update_log": MagicMock(),
"_prune_lxc_update_logs": MagicMock(),
"_finalize_lxc_update": finalizer,
}
outcome = self._worker(scope)(101, {"targets": ["oci_image"]})
self.assertEqual(outcome["status"], "skipped")
self.assertEqual(outcome["target"], "oci_image")
self.assertEqual(outcome["executed_targets"], [])
self.assertEqual(outcome["reason_code"], "stopped_container")
self.assertIn("before scheduled update could begin", outcome["reason"])
self.assertEqual(status.call_count, 2)
runner.assert_not_called()
self.assertEqual(finalizer.call_args.kwargs["status"], "skipped")
class DockerStackNotificationTests(unittest.TestCase):
def _docker_app(self):