fix(oci): preserve Monitor stack editor action

This commit is contained in:
VAIO73
2026-10-06 18:22:42 +02:00
parent cff386053b
commit ebb25e5a70
4 changed files with 71 additions and 4 deletions
@@ -0,0 +1,62 @@
"""Keep the Monitor OCI editor path distinct from coordinated stack recreation."""
import ast
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest import TestCase
from unittest.mock import Mock, patch
ROOT = Path(__file__).resolve().parents[3]
MENU = ROOT / 'oci/src/proxmenux_oci/management.py'
CLI = ROOT / 'oci/src/proxmenux_oci/cli.py'
MONITOR = ROOT / 'AppImage/components/virtual-machines.tsx'
def extracted_stack_manager(scope):
source = MENU.read_text(encoding='utf-8').replace(
' from .stack_recreation import modify_stack\n', '')
node = next(item for item in ast.parse(source).body
if isinstance(item, ast.FunctionDef) and item.name == '_manage_stack')
exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])),
str(MENU), 'exec'), scope)
return scope['_manage_stack']
class MonitorModifyContractTests(TestCase):
def test_monitor_sends_modify_for_a_stack_and_recreate_for_a_single_instance(self):
source = MONITOR.read_text(encoding='utf-8')
self.assertIn('action: "update" | "modify" | "recreate" | "recover"', source)
self.assertIn('action: ociInstance.stack ? "modify" : "recreate"', source)
def test_cli_accepts_the_monitor_modify_action(self):
self.assertIn('choices=("update", "modify", "recreate")', CLI.read_text(encoding='utf-8'))
def test_modify_action_opens_the_stack_editor_without_running_lifecycle(self):
primary = {'stack': {'members': [{'vmid': 101}]}}
instances = ModuleType('oci_instances')
instances.ROOT = Path('/inert')
instances.read = lambda *_: primary
modify_stack = Mock(return_value=True)
run_lifecycle = Mock()
manager = extracted_stack_manager({
'sys': SimpleNamespace(path=[]),
'Path': Path,
'modify_stack': modify_stack,
'_run_lifecycle': run_lifecycle,
})
ui = SimpleNamespace()
with patch.dict(sys.modules, {
'oci_instances': instances,
'oci_stack_replay': ModuleType('oci_stack_replay'),
}):
result = manager(Path('/inert'), ui, {'vmid': 101}, action='modify')
self.assertTrue(result)
modify_stack.assert_called_once_with(Path('/inert'), ui, primary, run_lifecycle)
run_lifecycle.assert_not_called()
if __name__ == '__main__':
import unittest
unittest.main()
+5 -2
View File
@@ -954,7 +954,7 @@ export function VirtualMachines() {
// A container restored from a backup carries the mark of its installation
// and has no record on this host until it is recovered from the menu.
const [ociRestored, setOciRestored] = useState(false)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" | "recover" } | null>(null)
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "modify" | "recreate" | "recover" } | null>(null)
// Firewall log state — fetched only when the operator opens that tab
// so a CT/VM without firewall use doesn't pay the pvesh cost on every
@@ -5341,7 +5341,10 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
<Button
size="sm"
className={neutralBtnCls}
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recreate" })}
onClick={() => setOciAction({
vmid: selectedVM.vmid,
action: ociInstance.stack ? "modify" : "recreate",
})}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.recreate")}
+2 -2
View File
@@ -595,9 +595,9 @@ def build_parser() -> argparse.ArgumentParser:
rclone_parser.add_argument("--host", default="auto")
rclone_parser.add_argument("--dry-run", action="store_true")
subparsers.add_parser("recover", help="Register again the OCI applications restored from a backup")
manage_parser = subparsers.add_parser("manage", help="Update or recreate one installed OCI instance")
manage_parser = subparsers.add_parser("manage", help="Update, modify or recreate one installed OCI instance")
manage_parser.add_argument("vmid", type=int)
manage_parser.add_argument("--action", choices=("update", "recreate"), required=True)
manage_parser.add_argument("--action", choices=("update", "modify", "recreate"), required=True)
manage_parser.add_argument("--keep-backup", metavar="STORAGE",
help="Keep the backup taken before the update in this Proxmox storage")
manage_parser.add_argument("--unattended", action="store_true",
+2
View File
@@ -285,6 +285,8 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
return False
if row['stack']:
return _manage_stack(project, ui, row, action, lifecycle_args)
if action == 'modify':
return False
if not row['pending']:
if row['status'] != 'installed' or row['reason'] != 'matched':
ui.message(translate('The instance identity or status must be reviewed before updating.'), translate('OCI management'))