From 815d98ab9f20543546b87b6d59f9035dc06cb1a8 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Fri, 9 Oct 2026 09:37:51 +0200 Subject: [PATCH] feat(oci): the menu shows the saved records being brought up to date. --- lang/es.json | 3 ++- oci/src/proxmenux_oci/cli.py | 6 +++-- oci/src/proxmenux_oci/management.py | 15 +++++++++--- oci/tests/test_carried_record.py | 38 +++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/lang/es.json b/lang/es.json index f5f09301..eb72a5db 100644 --- a/lang/es.json +++ b/lang/es.json @@ -7919,5 +7919,6 @@ "⚠ 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.", "⚠ 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..." } diff --git a/oci/src/proxmenux_oci/cli.py b/oci/src/proxmenux_oci/cli.py index 5ce74def..dfe01b9b 100644 --- a/oci/src/proxmenux_oci/cli.py +++ b/oci/src/proxmenux_oci/cli.py @@ -439,7 +439,7 @@ def install_template(ui, template: dict[str, Any], identifier: str, mode: str) - return None if result: 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} _, removed = images.offer_removal(ui, sorted(vmids)) _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") if result: 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"{translate('Read/write')}: {result['read_write_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)})") elif selection == "manage": from .management import interactive_management + console.show_logo() + console.msg_title(translate("Manage installed OCI applications")) interactive_management(PROJECT_ROOT, ui) elif selection == "custom": from .custom import explore diff --git a/oci/src/proxmenux_oci/management.py b/oci/src/proxmenux_oci/management.py index e19ab09c..bfe21fab 100644 --- a/oci/src/proxmenux_oci/management.py +++ b/oci/src/proxmenux_oci/management.py @@ -125,22 +125,29 @@ def _run_lifecycle(command, title): console.msg_title(title) environment = dict(os.environ, OCI_SPINNER='1' if sys.stdout.isatty() else '0') completed = subprocess.run(command, env=environment, check=False) - carry_records(images.PROJECT_ROOT) + carry_records(images.PROJECT_ROOT, shown=True) if sys.stdin.isatty(): console.wait_for_enter(translate('Press Enter to return to the menu...')) 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 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')) + if shown: + console.msg_info(translate('Updating the copy of the record...')) try: import oci_carried_record return oci_carried_record.sync(oci_carried_record.instances.ROOT, vmids, mount_stopped, verify) except (ImportError, OSError, ValueError, RuntimeError, subprocess.SubprocessError): return {} + finally: + if shown: + console.stop_spinner() def recover_automatically(project): @@ -245,7 +252,7 @@ def _interactive_management(project, ui): _clean_orphans(project) recover_automatically(project) offer_recovery(project, ui) - carry_records(project, mount_stopped=False) + carry_records(project, mount_stopped=False, shown=True) rows = saved_inventory(project) if not rows: ui.message(translate('No registered OCI containers are available for selection on this host.'), translate('OCI management')) diff --git a/oci/tests/test_carried_record.py b/oci/tests/test_carried_record.py index f398fdd5..54e5ded9 100644 --- a/oci/tests/test_carried_record.py +++ b/oci/tests/test_carried_record.py @@ -77,5 +77,43 @@ class CarriedRecordTests(unittest.TestCase): 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__": unittest.main()