diff --git a/lang/de.json b/lang/de.json index 47c46f05..ca5dcb81 100644 --- a/lang/de.json +++ b/lang/de.json @@ -9014,5 +9014,6 @@ "⚠ Disk data will NOT be erased.": "⚠ Festplattendaten werden NICHT gelöscht.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Die Festplatte wird ausgehängt und aus /etc/fstab entfernt.", "⚠ The /etc/fstab entry will be removed.": "⚠ Der /etc/fstab-Eintrag wird entfernt.", - "⚠ The disk will be unmounted.": "⚠ Die Festplatte wird ausgehängt." + "⚠ The disk will be unmounted.": "⚠ Die Festplatte wird ausgehängt.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Dieser Container wird von der Hochverfügbarkeit verwaltet. Entfernen Sie ct:{vmid} aus HA (Rechenzentrum → HA oder ha-manager remove ct:{vmid}), wiederholen Sie den Vorgang und fügen Sie ihn mit denselben Einstellungen wieder zu HA hinzu." } diff --git a/lang/es.json b/lang/es.json index 5db49b84..4cbafc83 100644 --- a/lang/es.json +++ b/lang/es.json @@ -8984,5 +8984,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á.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Este contenedor está gestionado por alta disponibilidad. Elimina ct:{vmid} de HA (Centro de datos → HA, o ha-manager remove ct:{vmid}), repite la operación y vuelve a añadirlo a HA con la misma configuración." } diff --git a/lang/fr.json b/lang/fr.json index 13059a44..e487d6fb 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -9013,5 +9013,6 @@ "⚠ Disk data will NOT be erased.": "⚠ Les données du disque ne seront PAS effacées.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Le disque sera démonté et supprimé de /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ L'entrée /etc/fstab sera supprimée.", - "⚠ The disk will be unmounted.": "⚠ Le disque sera démonté." + "⚠ The disk will be unmounted.": "⚠ Le disque sera démonté.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Ce conteneur est géré par la haute disponibilité. Retirez ct:{vmid} de HA (Centre de données → HA, ou ha-manager remove ct:{vmid}), relancez l'opération et rajoutez-le à HA avec les mêmes paramètres." } diff --git a/lang/it.json b/lang/it.json index 4bca27a3..9869d014 100644 --- a/lang/it.json +++ b/lang/it.json @@ -9014,5 +9014,6 @@ "⚠ Disk data will NOT be erased.": "⚠ I dati del disco NON verranno cancellati.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Il disco verrà smontato e rimosso da /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ La voce /etc/fstab verrà rimossa.", - "⚠ The disk will be unmounted.": "⚠ Il disco verrà smontato." + "⚠ The disk will be unmounted.": "⚠ Il disco verrà smontato.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Questo container è gestito dall'alta disponibilità. Rimuovi ct:{vmid} da HA (Datacenter → HA, oppure ha-manager remove ct:{vmid}), ripeti l'operazione e aggiungilo di nuovo a HA con le stesse impostazioni." } diff --git a/lang/pt.json b/lang/pt.json index a22b83a3..f0b3ef01 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -9014,5 +9014,6 @@ "⚠ Disk data will NOT be erased.": "⚠ Os dados do disco NÃO serão apagados.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ O disco será desmontado e removido de /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ A entrada /etc/fstab será removida.", - "⚠ The disk will be unmounted.": "⚠ O disco será desmontado." + "⚠ The disk will be unmounted.": "⚠ O disco será desmontado.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Este contentor é gerido pela alta disponibilidade. Remova ct:{vmid} da HA (Datacenter → HA, ou ha-manager remove ct:{vmid}), repita a operação e volte a adicioná-lo à HA com as mesmas definições." } diff --git a/lang/sk.json b/lang/sk.json index 8924c537..0408c8be 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -9014,5 +9014,6 @@ "⚠ Disk data will NOT be erased.": "⚠ Dáta na disku sa NEVYMAŽÚ.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disk sa odpojí a odstráni z /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ Záznam v /etc/fstab bude odstránený.", - "⚠ The disk will be unmounted.": "⚠ Disk bude odpojený." + "⚠ The disk will be unmounted.": "⚠ Disk bude odpojený.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Tento kontajner spravuje vysoká dostupnosť. Odstráňte ct:{vmid} z HA (Dátové centrum → HA alebo ha-manager remove ct:{vmid}), zopakujte operáciu a potom ho znova pridajte do HA s rovnakými nastaveniami." } diff --git a/lang/sv.json b/lang/sv.json index 1cd27e28..84ad8b70 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -9012,5 +9012,8 @@ "⚠ Disk data will NOT be erased.": "⚠ Diskdata kommer INTE att raderas.", "⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disken kommer att avmonteras och tas bort från /etc/fstab.", "⚠ The /etc/fstab entry will be removed.": "⚠ /etc/fstab-posten kommer att tas bort.", - "⚠ The disk will be unmounted.": "⚠ Disken kommer att avmonteras." + "⚠ The disk will be unmounted.": "⚠ Disken kommer att avmonteras.", + "This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.": "Den här containern hanteras av hög tillgänglighet. Ta bort ct:{vmid} från HA (Datacenter → HA, eller ha-manager remove ct:{vmid}), upprepa åtgärden och lägg sedan tillbaka den i HA med samma inställningar.", + "Krusader Web UI": "Krusaders webbgränssnitt", + "Z-Wave JS UI admin login": "Administratörsinloggning för Z-Wave JS UI" } diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index 5fc59340..0e07d7c5 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -621,6 +621,17 @@ def without_public_routes(config, vmid): return b'\n'.join(line for line in config.splitlines() if not public_routes.own(line, vmid)) + b'\n' +def refuse_high_availability(vmid): + """Recreating a container that HA manages would race the HA manager, which + restarts or relocates it in the middle of the replacement.""" + resources = json.loads(run('pvesh', 'get', '/cluster/ha/resources', '--output-format', 'json')) + if any(r.get('sid') == f'ct:{vmid}' for r in resources): + raise ValueError(translate( + 'This container is managed by high availability. Remove ct:{vmid} from HA (Datacenter → HA, ' + 'or ha-manager remove ct:{vmid}), repeat the operation and add it back to HA with the same settings.' + ).replace('{vmid}', str(vmid))) + + def preflight(record, candidate, config, coordinated=None, in_place=False, baseline=True): deployment = record['deployment'] desired = candidate['deployment'] @@ -1202,9 +1213,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i runtime_settings.file_binds(before)) original_gpu = gpu_devices.planned(record['deployment']) desired_gpu = gpu_devices.planned(candidate['deployment']) - resources = json.loads(run('pvesh', 'get', '/cluster/ha/resources', '--output-format', 'json')) - if any(r.get('sid') == f'ct:{vmid}' for r in resources): - raise ValueError(translate('High availability resources are not supported by this profile')) + refuse_high_availability(vmid) archive = archive.resolve(strict=True) check_archive(archive) image = image_from_archive(str(archive)) diff --git a/oci/remote/oci_update_current.py b/oci/remote/oci_update_current.py index c9ddc8e3..b0278615 100644 --- a/oci/remote/oci_update_current.py +++ b/oci/remote/oci_update_current.py @@ -233,6 +233,8 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non # the route before changing anything; actual validation/API failures # must not silently switch to a destructive fallback. transaction.preflight(record, desired, config) + # Refused before the image is downloaded, not after. + transaction.refuse_high_availability(vmid) msg_ok(translate('Container checked')) current = record['observed']['image'] if operation == 'update' else None check = lambda: work_backup.locate( diff --git a/oci/tests/test_native_modify.py b/oci/tests/test_native_modify.py index 48b89886..e108a557 100644 --- a/oci/tests/test_native_modify.py +++ b/oci/tests/test_native_modify.py @@ -151,6 +151,7 @@ class NativeApplyTests(unittest.TestCase): self.commands.append(args) if args[:2]==('pct','status'):return b'status: running' if self.state['running'] else b'status: stopped' if args[:2]==('pct','start'):self.state['running']=True + if args[:3]==('pvesh','get','/cluster/ha/resources'):return b'[]' return b'' def stop(vmid):self.state['running']=False def setter(vmid,identity,expected,values,delete=()): diff --git a/oci/tests/test_update_high_availability.py b/oci/tests/test_update_high_availability.py new file mode 100644 index 00000000..356dfa6b --- /dev/null +++ b/oci/tests/test_update_high_availability.py @@ -0,0 +1,49 @@ +"""A container that HA manages is refused before its new image is downloaded.""" +import contextlib +import json +from pathlib import Path +import sys +import unittest +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'remote')) +import oci_instance_transaction as transaction +import oci_update_current as update + + +def ha_resources(*vmids): + answer = json.dumps([{'sid': f'ct:{vmid}', 'state': 'started'} for vmid in vmids]).encode() + return patch.object(transaction, 'run', return_value=answer) + + +class HighAvailabilityTests(unittest.TestCase): + def test_the_refusal_names_the_resource_and_how_to_proceed(self): + with ha_resources(101), self.assertRaises(ValueError) as refused: + transaction.refuse_high_availability(101) + self.assertIn('ha-manager remove ct:101', str(refused.exception)) + + def test_a_container_outside_ha_is_not_refused(self): + with ha_resources(200): + transaction.refuse_high_availability(101) + + def test_an_update_stops_before_downloading_the_image(self): + record = {'status': 'installed', 'observed': {'image': {}}} + resolve = Mock() + with contextlib.ExitStack() as stack: + stack.enter_context(patch.object(update.instances, 'locked', return_value=contextlib.nullcontext())) + stack.enter_context(patch.object(update.instances, 'read', return_value=record)) + stack.enter_context(patch.object(update.instances, 'command', return_value=b'')) + stack.enter_context(patch.object(update.transaction, 'candidate_contract', return_value={})) + stack.enter_context(patch.object(update.transaction, 'external_changes', return_value={})) + stack.enter_context(patch.object(update.transaction, 'preflight')) + stack.enter_context(patch.object(update, 'resolve_archive', resolve)) + stack.enter_context(patch.object(update, 'msg_info')) + stack.enter_context(patch.object(update, 'msg_ok')) + stack.enter_context(ha_resources(101)) + with self.assertRaises(ValueError): + update.update(101) + resolve.assert_not_called() + + +if __name__ == '__main__': + unittest.main()