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