mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 02:12:12 +00:00
fix: address scheduled stopped LXC review feedback
This commit is contained in:
@@ -217,6 +217,10 @@ class CommandDescriptionsTests(unittest.TestCase):
|
||||
"reasonStoppedContainer",
|
||||
english["vmLxc"]["scheduled"]["reasonStoppedContainer"],
|
||||
)
|
||||
messages["vmLxc"]["scheduled"].setdefault(
|
||||
"runSkippedStatus",
|
||||
english["vmLxc"]["scheduled"]["runSkippedStatus"],
|
||||
)
|
||||
for key in ("noShell", "enterFailed"):
|
||||
messages["lxcTerminal"]["errors"].setdefault(key, english["lxcTerminal"]["errors"][key])
|
||||
# The notification history panel texts await the bot as well.
|
||||
|
||||
@@ -2465,7 +2465,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
: scheduleLastRunStatus === "deferred"
|
||||
? t("vmLxc.scheduled.runDeferred")
|
||||
: scheduleLastRunStatus === "skipped"
|
||||
? t("vmLxc.scheduled.runSkipped")
|
||||
? t("vmLxc.scheduled.runSkippedStatus")
|
||||
: t("vmLxc.scheduled.runFailed")}
|
||||
</span></>
|
||||
)}
|
||||
|
||||
@@ -1288,6 +1288,7 @@
|
||||
"runFailed": "✗ failed",
|
||||
"runDeferred": "held for safety",
|
||||
"runSkipped": "nothing pending",
|
||||
"runSkippedStatus": "run skipped",
|
||||
"reasonStoppedContainer": "Container is stopped; scheduled update was skipped.",
|
||||
"releaseDelaySummary": "{days}-day release hold",
|
||||
"releaseDelayLabel": "Wait after a new app release",
|
||||
|
||||
@@ -1288,6 +1288,7 @@
|
||||
"runFailed": "✗ zlyhalo",
|
||||
"runDeferred": "odložené z bezpečnostných dôvodov",
|
||||
"runSkipped": "nič nečaká",
|
||||
"runSkippedStatus": "spustenie bolo preskočené",
|
||||
"reasonStoppedContainer": "Kontajner je vypnutý; plánovaná aktualizácia sa preskočila.",
|
||||
"releaseDelaySummary": "odklad {days} deň/dní",
|
||||
"releaseDelayLabel": "Počkať po novej verzii aplikácie",
|
||||
|
||||
@@ -22245,7 +22245,7 @@ def api_proxmenux_self_update_status():
|
||||
# so scheduled and manual applies cannot overlap.
|
||||
|
||||
_APPLY_UPDATES_SCRIPT = _LXC_APPLY_UPDATES_SCRIPT
|
||||
_SCHEDULED_STOPPED_EXIT = 8
|
||||
_SCHEDULED_STOPPED_EXIT = 9
|
||||
_DOCKER_ENGINE_INTEGRATED_COMMAND = (
|
||||
'python3 /usr/local/share/proxmenux/monitor-app/usr/bin/'
|
||||
'update_docker_engine.py --vmid "$VMID"'
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
# 6 post-update restart failed
|
||||
# 7 another ProxMenux update is already running for this CT
|
||||
# 8 started CT could not record its verification handoff identity
|
||||
# 9 scheduled run skipped because the CT was intentionally stopped
|
||||
#
|
||||
# The frontend surfaces exit code + duration in a follow-up POST to
|
||||
# /api/lxc-updates/<vmid>/applied so the notification event fires with
|
||||
@@ -166,7 +167,7 @@ STATE=$(pct status "$VMID" | awk '{print $2}')
|
||||
if [[ "$STATE" != "running" && "${EXECUTION_MODE:-}" == "scheduled" ]]; then
|
||||
[[ -n "${SCHEDULED_STOPPED_MARKER:-}" ]] && printf '%s\n' "$SCHEDULED_STOPPED_MARKER"
|
||||
echo "SKIPPED: CT $VMID is stopped; a scheduled update will not start it."
|
||||
exit 8
|
||||
exit 9
|
||||
fi
|
||||
STARTED_BY_PROXMENUX=0
|
||||
OBSERVATION_ATTEMPTED=0
|
||||
|
||||
@@ -164,7 +164,7 @@ run_case stopped_restore RUN_HELPER=0 UPDATE_COMMAND='update-custom' MOCK_INITIA
|
||||
|
||||
run_case scheduled_stopped RUN_HELPER=0 UPDATE_COMMAND='update-custom' \
|
||||
EXECUTION_MODE=scheduled SCHEDULED_STOPPED_MARKER='fixture-stopped-marker' MOCK_INITIAL_STATE=stopped
|
||||
[[ $CASE_RC -eq 8 ]] || fail "scheduled_stopped returned $CASE_RC"
|
||||
[[ $CASE_RC -eq 9 ]] || fail "scheduled_stopped returned $CASE_RC"
|
||||
[[ $(count 'pct start 101') -eq 0 ]] || fail "scheduled update started a stopped CT"
|
||||
[[ $(count 'pct exec') -eq 0 ]] || fail "scheduled update executed inside a stopped CT"
|
||||
[[ $(count CUSTOM_EXEC) -eq 0 ]] || fail "scheduled update ran a custom updater"
|
||||
|
||||
@@ -78,7 +78,7 @@ function walk(node) {
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
const name = node.name.getText(tree)
|
||||
if (wanted.has(name)) definitions.push(`const ${node.getText(tree)};`)
|
||||
if (["applySchedulePayload", "loadSchedule", "loadBulkUpdate"].includes(name))
|
||||
if (["LEGACY_STOPPED_SCHEDULE_REASONS", "scheduleReasonCodeFromPersisted", "applySchedulePayload", "loadSchedule", "loadBulkUpdate"].includes(name))
|
||||
preferenceDefinitions.push(`const ${node.getText(tree)};`)
|
||||
if (["osUpdateStale", "osUpdateStatusKnown", "hasOsUpdates"].includes(name))
|
||||
status.push(`const ${node.getText(tree)};`)
|
||||
@@ -325,11 +325,11 @@ context.selectedVMRef.current = { vmid: 100 }
|
||||
}
|
||||
for (const name of ["ScheduleEnabled", "ScheduleConfigured", "ScheduleCron", "SchedulePreset",
|
||||
"ScheduleTarget", "ScheduleTargets", "ScheduleReleaseDelayDays", "ApplyBackup", "ApplyBackupStorage",
|
||||
"ApplyRestart", "ScheduleLastRunAt", "ScheduleLastRunStatus", "ScheduleLastRunReason", "ScheduleLastRunLog",
|
||||
"ApplyRestart", "ScheduleLastRunAt", "ScheduleLastRunStatus", "ScheduleLastRunReason", "ScheduleLastRunReasonCode", "ScheduleLastRunLog",
|
||||
"ScheduleLastRunRebootRequired", "ScheduleLastRunRebootPackages", "ExternalCron", "ScheduleError",
|
||||
"ScheduleLoaded", "BulkError", "BulkLoaded", "BulkEditMode", "BulkConfigured", "BulkTargets", "BulkPersistedTargets"])
|
||||
preferenceContext[`set${name}`] = value => { preferenceState[name] = value }
|
||||
assert.equal(preferenceDefinitions.length, 3)
|
||||
assert.equal(preferenceDefinitions.length, 5)
|
||||
const preferences = compile(preferenceDefinitions.join("\n") + `
|
||||
exports.apply = applySchedulePayload; exports.schedule = loadSchedule; exports.bulk = loadBulkUpdate;
|
||||
`, preferenceContext)
|
||||
@@ -340,6 +340,12 @@ context.selectedVMRef.current = { vmid: 100 }
|
||||
assert.equal(preferenceState.ApplyRestart, false, "missing restart cannot inherit a reboot")
|
||||
preferences.apply({ backup: false, backup_storage: "", restart: false })
|
||||
assert.equal(preferenceState.ApplyBackupStorage, "")
|
||||
preferences.apply({ last_run_reason_code: "stopped_container", last_run_reason: "raw stopped reason" })
|
||||
assert.equal(preferenceState.ScheduleLastRunReasonCode, "stopped_container",
|
||||
"schedule payload publishes its structured stopped reason")
|
||||
preferences.apply({})
|
||||
assert.equal(preferenceState.ScheduleLastRunReasonCode, null,
|
||||
"a missing reason code cannot leak from the previously selected guest")
|
||||
preferenceContext.vmModalCacheRef.current.schedule.set(213, { backup: true, backup_storage: "local", restart: true })
|
||||
const previousSchedule = preferences.schedule(213)
|
||||
assert.equal(preferenceState.ApplyBackup, true, "use only the selected guest's own cache")
|
||||
|
||||
@@ -73,7 +73,9 @@ for (const locale of ['en', 'sk']) {
|
||||
const render = compile(context)
|
||||
const translated = flattenText(render(true))
|
||||
const expected = t('vmLxc.scheduled.reasonStoppedContainer')
|
||||
const skippedLabel = t('vmLxc.scheduled.runSkippedStatus')
|
||||
assert(translated.includes(expected), `${locale}: known reason code must use the locale catalog`)
|
||||
assert(translated.includes(skippedLabel), `${locale}: skipped run must not claim that no updates are pending`)
|
||||
assert(!translated.includes(context.scheduleLastRunReason), `${locale}: raw stopped reason must not leak into UI`)
|
||||
|
||||
context.scheduleLastRunReasonCode = 'unknown_reason'
|
||||
|
||||
@@ -135,7 +135,7 @@ raise SystemExit(h.main())
|
||||
"_json_list": lambda value: json.loads(value or '[]'),
|
||||
"_lxc_update_finalizations": {}, "_lxc_update_finalization_lock": threading.RLock(),
|
||||
"_LXC_UPDATE_FINALIZATION_TTL": 3600,
|
||||
"_SCHEDULED_STOPPED_EXIT": 8,
|
||||
"_SCHEDULED_STOPPED_EXIT": 9,
|
||||
"_fast_guest_status": lambda *_: self.state.read_text(),
|
||||
"_lxc_update_snapshot": lambda *_: {"ct_name": "fixture"},
|
||||
"_lxc_update_target_labels": lambda *_: ["OS"],
|
||||
|
||||
@@ -362,17 +362,18 @@ class ScheduledStoppedContainerTests(unittest.TestCase):
|
||||
|
||||
def test_stopped_marker_requires_the_scheduler_token_not_only_exit_code(self):
|
||||
marker = "PROXMENUX_SCHEDULED_STOPPED:fixture"
|
||||
reader = self._stopped_marker_reader({"_SCHEDULED_STOPPED_EXIT": 8, "os": os})
|
||||
self.assertTrue(reader(SimpleNamespace(returncode=8, stdout=marker, stderr=""), None, marker))
|
||||
self.assertFalse(reader(SimpleNamespace(returncode=8, stdout="", stderr=""), None, marker))
|
||||
reader = self._stopped_marker_reader({"_SCHEDULED_STOPPED_EXIT": 9, "os": os})
|
||||
self.assertTrue(reader(SimpleNamespace(returncode=9, stdout=marker, stderr=""), None, marker))
|
||||
self.assertFalse(reader(SimpleNamespace(returncode=9, stdout="", stderr=""), None, marker))
|
||||
self.assertFalse(reader(SimpleNamespace(returncode=8, stdout=marker, stderr=""), None, marker))
|
||||
self.assertFalse(reader(SimpleNamespace(returncode=4, stdout=marker, stderr=""), None, marker))
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as log:
|
||||
log.write("prefix\\n" + marker + "\\n")
|
||||
log.flush()
|
||||
self.assertTrue(reader(SimpleNamespace(returncode=8, stdout="", stderr=""), log.name, marker))
|
||||
self.assertTrue(reader(SimpleNamespace(returncode=9, stdout="", stderr=""), log.name, marker))
|
||||
|
||||
def test_runner_race_to_stopped_is_reported_as_skipped_without_reboot_probe(self):
|
||||
runner = MagicMock(return_value=SimpleNamespace(returncode=8, stdout="", stderr=""))
|
||||
runner = MagicMock(return_value=SimpleNamespace(returncode=9, stdout="", stderr=""))
|
||||
finalizer = MagicMock(return_value={"recorded": True})
|
||||
reboot_probe = MagicMock()
|
||||
stopped_marker = MagicMock(return_value=True)
|
||||
@@ -388,7 +389,7 @@ class ScheduledStoppedContainerTests(unittest.TestCase):
|
||||
"os": os,
|
||||
"json": json,
|
||||
"_APPLY_UPDATES_SCRIPT": "/bin/true",
|
||||
"_SCHEDULED_STOPPED_EXIT": 8,
|
||||
"_SCHEDULED_STOPPED_EXIT": 9,
|
||||
"_DOCKER_ENGINE_INTEGRATED_COMMAND": "integrated-docker",
|
||||
"_create_lxc_update_log": lambda _vmid, _run_id: (None, None),
|
||||
"_fast_guest_status": MagicMock(return_value="running"),
|
||||
@@ -420,6 +421,7 @@ class ScheduledStoppedContainerTests(unittest.TestCase):
|
||||
|
||||
stopped_marker.reset_mock()
|
||||
stopped_marker.return_value = False
|
||||
runner.return_value = SimpleNamespace(returncode=8, stdout="", stderr="")
|
||||
reboot_probe.return_value = (None, [], None)
|
||||
outcome = self._worker(scope)(101, {"target": "os"})
|
||||
self.assertEqual(outcome["status"], "failure")
|
||||
|
||||
Reference in New Issue
Block a user