feat(oci): the menu shows the saved records being brought up to date.

This commit is contained in:
MacRimi
2026-10-09 09:37:51 +02:00
parent 5a892db7c9
commit 815d98ab9f
4 changed files with 55 additions and 7 deletions
+2 -1
View File
@@ -7919,5 +7919,6 @@
"⚠ Disk data will NOT be erased.": "⚠ Los datos del disco NO se borrarán.", "⚠ Disk data will NOT be erased.": "⚠ Los datos del disco NO se borrarán.",
"⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ El disco se desmontará y se eliminará de /etc/fstab.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ El disco se desmontará y se eliminará de /etc/fstab.",
"⚠ The /etc/fstab entry will be removed.": "⚠ Se eliminará la entrada /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ Se eliminará la entrada /etc/fstab.",
"⚠ The disk will be unmounted.": "⚠ El disco se desmontará." "⚠ The disk will be unmounted.": "⚠ El disco se desmontará.",
"Updating the copy of the record...": "Actualizando la copia del registro..."
} }
+4 -2
View File
@@ -439,7 +439,7 @@ def install_template(ui, template: dict[str, Any], identifier: str, mode: str) -
return None return None
if result: if result:
from .management import carry_records from .management import carry_records
carry_records(PROJECT_ROOT) carry_records(PROJECT_ROOT, shown=True)
vmids = {int(v) for v in [result.get("vmid"), *(result.get("stack_vmids") or {}).values()] if v} vmids = {int(v) for v in [result.get("vmid"), *(result.get("stack_vmids") or {}).values()] if v}
_, removed = images.offer_removal(ui, sorted(vmids)) _, removed = images.offer_removal(ui, sorted(vmids))
_print_installation_summary(result, removed) _print_installation_summary(result, removed)
@@ -461,7 +461,7 @@ def _rclone_mount(catalog: Catalog, ui) -> None:
result = run_remote_rclone_mount(PROJECT_ROOT, template, deployment, "auto") result = run_remote_rclone_mount(PROJECT_ROOT, template, deployment, "auto")
if result: if result:
from .management import carry_records from .management import carry_records
carry_records(PROJECT_ROOT) carry_records(PROJECT_ROOT, shown=True)
console.msg_ok(f"Remote: {result['remote']}:") console.msg_ok(f"Remote: {result['remote']}:")
console.msg_ok(f"{translate('Read/write')}: {result['read_write_path']}") console.msg_ok(f"{translate('Read/write')}: {result['read_write_path']}")
console.msg_ok(f"{translate('Read-only')}: {result['read_only_path']}") console.msg_ok(f"{translate('Read-only')}: {result['read_only_path']}")
@@ -562,6 +562,8 @@ def interactive(catalog: Catalog) -> int:
f"{translate('All applications')} ({len(applications)})") f"{translate('All applications')} ({len(applications)})")
elif selection == "manage": elif selection == "manage":
from .management import interactive_management from .management import interactive_management
console.show_logo()
console.msg_title(translate("Manage installed OCI applications"))
interactive_management(PROJECT_ROOT, ui) interactive_management(PROJECT_ROOT, ui)
elif selection == "custom": elif selection == "custom":
from .custom import explore from .custom import explore
+11 -4
View File
@@ -125,22 +125,29 @@ def _run_lifecycle(command, title):
console.msg_title(title) console.msg_title(title)
environment = dict(os.environ, OCI_SPINNER='1' if sys.stdout.isatty() else '0') environment = dict(os.environ, OCI_SPINNER='1' if sys.stdout.isatty() else '0')
completed = subprocess.run(command, env=environment, check=False) completed = subprocess.run(command, env=environment, check=False)
carry_records(images.PROJECT_ROOT) carry_records(images.PROJECT_ROOT, shown=True)
if sys.stdin.isatty(): if sys.stdin.isatty():
console.wait_for_enter(translate('Press Enter to return to the menu...')) console.wait_for_enter(translate('Press Enter to return to the menu...'))
return completed.returncode == 0 return completed.returncode == 0
def carry_records(project, mount_stopped=True, vmids=None, verify=False): def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=False):
"""Leave inside each container the copy of its record that a restore on """Leave inside each container the copy of its record that a restore on
another host needs. It is refreshed after every operation. Returns what another host needs. It is refreshed after every operation. Returns what
was found for each container.""" was found for each container. Every installed application is looked at,
which takes a while with many of them: `shown` says so on the screen."""
from . import console
sys.path.insert(0, str(project / 'remote')) sys.path.insert(0, str(project / 'remote'))
if shown:
console.msg_info(translate('Updating the copy of the record...'))
try: try:
import oci_carried_record import oci_carried_record
return oci_carried_record.sync(oci_carried_record.instances.ROOT, vmids, mount_stopped, verify) return oci_carried_record.sync(oci_carried_record.instances.ROOT, vmids, mount_stopped, verify)
except (ImportError, OSError, ValueError, RuntimeError, subprocess.SubprocessError): except (ImportError, OSError, ValueError, RuntimeError, subprocess.SubprocessError):
return {} return {}
finally:
if shown:
console.stop_spinner()
def recover_automatically(project): def recover_automatically(project):
@@ -245,7 +252,7 @@ def _interactive_management(project, ui):
_clean_orphans(project) _clean_orphans(project)
recover_automatically(project) recover_automatically(project)
offer_recovery(project, ui) offer_recovery(project, ui)
carry_records(project, mount_stopped=False) carry_records(project, mount_stopped=False, shown=True)
rows = saved_inventory(project) rows = saved_inventory(project)
if not rows: if not rows:
ui.message(translate('No registered OCI containers are available for selection on this host.'), translate('OCI management')) ui.message(translate('No registered OCI containers are available for selection on this host.'), translate('OCI management'))
+38
View File
@@ -77,5 +77,43 @@ class CarriedRecordTests(unittest.TestCase):
self.assertEqual(carried.mapped_root(custom), (200000, 300000)) self.assertEqual(carried.mapped_root(custom), (200000, 300000))
class ShownSyncTests(unittest.TestCase):
"""Every installed application is looked at after an operation, which
takes a while with many of them: the screen says what is being done."""
def setUp(self):
sys.path.insert(0, str(ROOT / "src"))
from proxmenux_oci import console, management
self.console, self.management = console, management
def run_sync(self, outcome, **options):
from unittest.mock import patch
events = []
with patch.object(self.console, "msg_info", side_effect=lambda text: events.append(("info", text))), \
patch.object(self.console, "stop_spinner", side_effect=lambda *a, **k: events.append(("stop",))), \
patch.object(carried, "sync", side_effect=outcome) as sync:
result = self.management.carry_records(ROOT, **options)
return result, events, sync
def test_the_sync_is_announced_and_the_notice_is_cleared(self):
result, events, sync = self.run_sync(lambda *args: {120: "current"}, shown=True)
self.assertEqual(result, {120: "current"})
self.assertEqual([event[0] for event in events], ["info", "stop"])
self.assertEqual(events[0][1], "Updating the copy of the record...")
sync.assert_called_once()
def test_the_notice_is_cleared_when_the_sync_fails(self):
result, events, _sync = self.run_sync(OSError("registry"), shown=True)
self.assertEqual(result, {})
self.assertEqual([event[0] for event in events], ["info", "stop"])
def test_nothing_is_shown_unless_asked(self):
_result, events, sync = self.run_sync(lambda *args: {}, vmids=[120], verify=True)
self.assertEqual(events, [])
self.assertEqual(sync.call_args.args[1:], ([120], True, True))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()