mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 02:12:12 +00:00
Add selective settings menu for OCI modifications
This commit is contained in:
@@ -32,3 +32,11 @@ jobs:
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
run: python3 -m unittest discover -s oci/tests -p 'test_registry_*.py' -v
|
||||
- name: Run OCI modification menu regressions
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
run: |
|
||||
python3 -m unittest discover -s oci/tests -p test_modify_menu.py -v
|
||||
python3 -m unittest discover -s oci/tests -p test_recreation.py -v
|
||||
python3 -m unittest discover -s oci/tests -p test_shared_directories.py -v
|
||||
python3 -m unittest discover -s oci/tests -p 'test_wizard*.py' -v
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Kann die wiederverwendete Festplatte nicht überprüfen:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Das Bildarchiv kann nicht in {storage} geschrieben werden. Überprüfen Sie den Speicherzugriff und den Bereitstellungsstatus.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Fähigkeiten können nicht gehalten werden und alle gleichzeitig fallen gelassen werden",
|
||||
"Catalog options": "Katalogoptionen",
|
||||
"Category": "Kategorie",
|
||||
"Caution: Maximum mode generates more heat.": "Achtung: Im Maximalmodus entsteht mehr Wärme.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Ceph-Prüfung durch Benutzerflag übersprungen (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Wählen Sie einen Zeitplan (systemd-Kalender-Ereignis-Syntax):",
|
||||
"Choose a script to execute:": "Wählen Sie ein auszuführendes Skript aus:",
|
||||
"Choose a server:": "Wählen Sie einen Server:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Wähle eine Einstellung. Änderungen werden erst nach der abschließenden Prüfung angewendet.",
|
||||
"Choose a share to mount:": "Wählen Sie eine Freigabe zum Mounten aus:",
|
||||
"Choose a specific supported GPU or USB node": "Wählen Sie einen bestimmten unterstützten GPU- oder USB-Knoten aus",
|
||||
"Choose action for this controller/NVMe:": "Wählen Sie eine Aktion für diesen Controller/NVMe:",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Rpcbind-Dienst und Socket deaktivieren und stoppen ...",
|
||||
"Disabling non-free firmware warnings...": "Warnungen zu nicht kostenloser Firmware werden deaktiviert...",
|
||||
"Disabling portmapper/rpcbind for security...": "Portmapper/rpcbind wird aus Sicherheitsgründen deaktiviert...",
|
||||
"Discard changes": "Änderungen verwerfen",
|
||||
"Discard unapplied changes?": "Noch nicht angewendete Änderungen verwerfen?",
|
||||
"Discover targets on portal:": "Entdecken Sie Ziele auf dem Portal:",
|
||||
"Discovering iSCSI targets...": "iSCSI-Ziele werden ermittelt...",
|
||||
"Discovery successful": "Entdeckung erfolgreich",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Kein Passwort angegeben.",
|
||||
"No paths available": "Keine Pfade verfügbar",
|
||||
"No paths selected. Select at least one path.": "Keine Pfade ausgewählt. Wählen Sie mindestens einen Pfad aus.",
|
||||
"No pending changes": "Keine ausstehenden Änderungen",
|
||||
"No pending paths to schedule for reboot.": "Keine ausstehenden Pfade zum Planen eines Neustarts.",
|
||||
"No permanent NFS mounts configured.": "Keine permanenten NFS-Mounts konfiguriert.",
|
||||
"No permanent Samba mounts configured.": "Keine permanenten Samba-Mounts konfiguriert.",
|
||||
@@ -4402,6 +4407,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM entfernt (Marionettenartefakte müssen möglicherweise manuell bereinigt werden)",
|
||||
"OVH server detected": "OVH-Server erkannt",
|
||||
"OVH server detection and RTM installation process completed": "OVH-Servererkennung und RTM-Installationsprozess abgeschlossen",
|
||||
"Object detector": "Objekterkennung",
|
||||
"Object detector for Frigate": "Objektdetektor für Frigate",
|
||||
"Observer is not responding on port 4357": "Beobachter reagiert nicht auf Port 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian ist eine Notiz-App, mit der Sie Ihre Notizen auf Ihrem Gerät erstellen, verknüpfen und organisieren können, mit Hunderten von Plugins und Themes, um Ihren Workflow anzupassen. Sie können Ihre Notizen auch online veröffentlichen, offline darauf zugreifen und sie sicher mit einer End-to-End-Verschlüsselung synchronisieren.",
|
||||
@@ -5328,6 +5334,7 @@
|
||||
"Reverting ZFS ARC tuning...": "ZFS ARC-Tuning wird zurückgesetzt...",
|
||||
"Reverting pigz wrapper...": "Pigz-Wrapper wird zurückgesetzt...",
|
||||
"Reverting vzdump speed tuning...": "vzdump-Geschwindigkeitsoptimierung wird zurückgesetzt...",
|
||||
"Review and apply": "Prüfen und anwenden",
|
||||
"Review external OCI changes": "Überprüfen Sie externe OCI-Änderungen",
|
||||
"Review package upgrade": "Paketaktualisierung prüfen",
|
||||
"Review passthrough config files": "Überprüfen Sie die Passthrough-Konfigurationsdateien",
|
||||
@@ -5787,6 +5794,7 @@
|
||||
"ShareCoveX panel": "ShareCoveX-Panel",
|
||||
"Shared Directory Ready:": "Bereit für das freigegebene Verzeichnis:",
|
||||
"Shared Group": "Geteilte Gruppe",
|
||||
"Shared directories": "Freigegebene Verzeichnisse",
|
||||
"Shared directory created:": "Gemeinsames Verzeichnis erstellt:",
|
||||
"Shared directory for consume and export": "Gemeinsames Verzeichnis für Konsum und Export",
|
||||
"Shared directory for copy/sync operations": "Gemeinsames Verzeichnis für Copy/Sync operations",
|
||||
@@ -6997,6 +7005,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Das Root-Dateisystem konnte nicht identifiziert werden. Abbruch.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "System-Pool-Festplatten ZFS können nicht aufgelöst werden. Abbruch.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Die Topologie der Systemfestplatte kann nicht aufgelöst werden. Abbruch.",
|
||||
"Unapplied changes": "Noch nicht angewendete Änderungen",
|
||||
"Understand the security implications of privileged containers": "Verstehen Sie die Sicherheitsauswirkungen privilegierter Container",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Unerwartetes Proxmox-Inventar; Erholung blockiert",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Unerwartetes Formatierungsverzeichnis im neuen Valkey-Volume",
|
||||
|
||||
@@ -748,6 +748,7 @@
|
||||
"Cannot verify the reused disk:": "No se puede verificar el disco reutilizado:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "No se puede escribir el archivo de la imagen en {storage}. Comprueba el acceso al almacenamiento y que esté montado.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "No se pueden mantener las capacidades y todas se disminuyen al mismo tiempo",
|
||||
"Catalog options": "Opciones del catálogo",
|
||||
"Category": "Categoría",
|
||||
"Caution: Maximum mode generates more heat.": "Precaución: el modo máximo genera más calor.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Comprobación de Ceph omitida por el indicador del usuario (--ignore-ceph-check)",
|
||||
@@ -882,6 +883,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Elija una programación (sintaxis de eventos de calendario de systemd):",
|
||||
"Choose a script to execute:": "Elige un script para ejecutar:",
|
||||
"Choose a server:": "Elige un servidor:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Elige qué configurar. Los cambios solo se aplican tras la revisión final.",
|
||||
"Choose a share to mount:": "Elige un recurso compartido para montar:",
|
||||
"Choose a specific supported GPU or USB node": "Elija un nodo concreto de GPU o USB compatible",
|
||||
"Choose action for this controller/NVMe:": "Elige la acción para este controlador/NVMe:",
|
||||
@@ -1764,6 +1766,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Deshabilitar y detener el servicio y el socket rpcbind...",
|
||||
"Disabling non-free firmware warnings...": "Deshabilitando las advertencias de firmware no libre...",
|
||||
"Disabling portmapper/rpcbind for security...": "Deshabilitando portmapper/rpcbind por seguridad...",
|
||||
"Discard changes": "Descartar cambios",
|
||||
"Discard unapplied changes?": "¿Descartar los cambios sin aplicar?",
|
||||
"Discover targets on portal:": "Detectar destinos en el portal:",
|
||||
"Discovering iSCSI targets...": "Descubriendo destinos iSCSI...",
|
||||
"Discovery successful": "Detección correcta",
|
||||
@@ -4256,6 +4260,7 @@
|
||||
"No password provided.": "No se proporcionó contraseña.",
|
||||
"No paths available": "No hay rutas disponibles",
|
||||
"No paths selected. Select at least one path.": "No hay rutas seleccionadas. Seleccione al menos una ruta.",
|
||||
"No pending changes": "No hay cambios pendientes",
|
||||
"No pending paths to schedule for reboot.": "No hay rutas pendientes para programar el reinicio.",
|
||||
"No permanent NFS mounts configured.": "No se han configurado montajes NFS permanentes.",
|
||||
"No permanent Samba mounts configured.": "No hay montajes Samba permanentes configurados.",
|
||||
@@ -4394,6 +4399,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM eliminado (los artefactos de las marionetas pueden necesitar una limpieza manual)",
|
||||
"OVH server detected": "Servidor OVH detectado",
|
||||
"OVH server detection and RTM installation process completed": "Finalizado el proceso de detección del servidor OVH e instalación de RTM",
|
||||
"Object detector": "Detector de objetos",
|
||||
"Object detector for Frigate": "Detector de objetos para Frigate",
|
||||
"Observer is not responding on port 4357": "El observador no responde en el puerto 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian es una aplicación de notas que te permite crear, enlazar y organizar tus notas en tu dispositivo, con cientos de plugins y temas para personalizar tu flujo de trabajo. También puedes publicar tus notas en línea, acceder a ellas sin conexión y sincronizarlas de forma segura con cifrado de extremo a extremo.",
|
||||
@@ -5312,6 +5318,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Revirtiendo el ajuste ZFS ARC...",
|
||||
"Reverting pigz wrapper...": "Revertir la envoltura de pigz...",
|
||||
"Reverting vzdump speed tuning...": "Revertir el ajuste de velocidad de vzdump...",
|
||||
"Review and apply": "Revisar y aplicar",
|
||||
"Review external OCI changes": "Revisar cambios externos de OCI",
|
||||
"Review package upgrade": "Revisar la actualización de paquetes",
|
||||
"Review passthrough config files": "Revisar los archivos de configuración de passthrough",
|
||||
@@ -5771,6 +5778,7 @@
|
||||
"ShareCoveX panel": "Panel de ShareCoveX",
|
||||
"Shared Directory Ready:": "Directorio compartido listo:",
|
||||
"Shared Group": "Grupo compartido",
|
||||
"Shared directories": "Carpetas compartidas",
|
||||
"Shared directory created:": "Directorio compartido creado:",
|
||||
"Shared directory for consume and export": "Directorio compartido para consumo y exportación",
|
||||
"Shared directory for copy/sync operations": "Directorio compartido para operaciones de copia/sincronización",
|
||||
@@ -6972,6 +6980,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "No se pudo identificar el sistema de archivos raíz. Se cancela.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "No se pudieron resolver los discos del pool ZFS del sistema. Se cancela.",
|
||||
"Unable to resolve system disk topology. Aborting.": "No se pudo resolver la topología de discos del sistema. Se cancela.",
|
||||
"Unapplied changes": "Cambios sin aplicar",
|
||||
"Understand the security implications of privileged containers": "Comprender las implicaciones de seguridad de los contenedores privilegiados",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Inventario Proxmox inesperado; recuperación bloqueada",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Directorio de formato inesperado en el nuevo volumen Valkey",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Impossible de vérifier le disque réutilisé :",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Impossible d'écrire l'archive d'images sur {storage}. Vérifiez l'accès au stockage et l'état du montage.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Les capacités ne peuvent être conservées et toutes abandonnées en même temps",
|
||||
"Catalog options": "Options du catalogue",
|
||||
"Category": "Catégorie",
|
||||
"Caution: Maximum mode generates more heat.": "Attention : le mode maximum génère plus de chaleur.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Vérification Ceph ignorée par l'indicateur utilisateur (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Choisissez un calendrier (syntaxe d'événement de calendrier systemd) :",
|
||||
"Choose a script to execute:": "Choisissez un script à exécuter :",
|
||||
"Choose a server:": "Choisissez un serveur :",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Choisissez un réglage. Les modifications ne sont appliquées qu’après la vérification finale.",
|
||||
"Choose a share to mount:": "Choisissez un partage à monter :",
|
||||
"Choose a specific supported GPU or USB node": "Choisissez un GPU ou un nœud USB pris en charge spécifique",
|
||||
"Choose action for this controller/NVMe:": "Choisissez une action pour ce contrôleur/NVMe :",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Désactivation et arrêt du service et du socket rpcbind...",
|
||||
"Disabling non-free firmware warnings...": "Désactivation des avertissements du firmware non libre...",
|
||||
"Disabling portmapper/rpcbind for security...": "Désactivation de portmapper/rpcbind pour la sécurité...",
|
||||
"Discard changes": "Abandonner les modifications",
|
||||
"Discard unapplied changes?": "Abandonner les modifications non appliquées ?",
|
||||
"Discover targets on portal:": "Découvrez les cibles sur le portail :",
|
||||
"Discovering iSCSI targets...": "Découverte des cibles iSCSI...",
|
||||
"Discovery successful": "Découverte réussie",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Aucun mot de passe fourni.",
|
||||
"No paths available": "Aucun chemin disponible",
|
||||
"No paths selected. Select at least one path.": "Aucun chemin sélectionné. Sélectionnez au moins un chemin.",
|
||||
"No pending changes": "Aucune modification en attente",
|
||||
"No pending paths to schedule for reboot.": "Aucun chemin en attente pour planifier le redémarrage.",
|
||||
"No permanent NFS mounts configured.": "Aucun montage NFS permanent configuré.",
|
||||
"No permanent Samba mounts configured.": "Aucun montage Samba permanent configuré.",
|
||||
@@ -4402,6 +4407,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM supprimé (les artefacts de marionnettes peuvent nécessiter un nettoyage manuel)",
|
||||
"OVH server detected": "Serveur OVH détecté",
|
||||
"OVH server detection and RTM installation process completed": "Processus de détection du serveur OVH et d'installation de RTM terminé",
|
||||
"Object detector": "Détecteur d’objets",
|
||||
"Object detector for Frigate": "Détecteur d'objets pour Frigate",
|
||||
"Observer is not responding on port 4357": "L'observateur ne répond pas au port 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian est une application de prise de notes qui vous permet de créer, de lier et d'organiser vos notes sur votre appareil, avec des centaines de plugins et de thèmes pour personnaliser votre workflow. Vous pouvez également publier vos notes en ligne, les accéder hors ligne et les synchroniser en toute sécurité avec le chiffrement de bout en bout.",
|
||||
@@ -5328,6 +5334,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Annulation du réglage ZFS ARC...",
|
||||
"Reverting pigz wrapper...": "Rétablissement du wrapper pigz...",
|
||||
"Reverting vzdump speed tuning...": "Annulation du réglage de la vitesse de vzdump...",
|
||||
"Review and apply": "Vérifier et appliquer",
|
||||
"Review external OCI changes": "Examiner les modifications externes de OCI",
|
||||
"Review package upgrade": "Examiner la mise à niveau des paquets",
|
||||
"Review passthrough config files": "Examiner les fichiers de configuration relais",
|
||||
@@ -5787,6 +5794,7 @@
|
||||
"ShareCoveX panel": "Panneau ShareCoveX",
|
||||
"Shared Directory Ready:": "Répertoire partagé prêt :",
|
||||
"Shared Group": "Groupe partagé",
|
||||
"Shared directories": "Dossiers partagés",
|
||||
"Shared directory created:": "Répertoire partagé créé & #160;:",
|
||||
"Shared directory for consume and export": "Répertoire partagé pour la consommation et l'exportation",
|
||||
"Shared directory for copy/sync operations": "Répertoire partagé pour copier/sync operations",
|
||||
@@ -6996,6 +7004,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Impossible d'identifier le système de fichiers racine. Avorter.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "Impossible de résoudre les disques du pool système ZFS.Avorter.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Impossible de résoudre la topologie du disque système. Avorter.",
|
||||
"Unapplied changes": "Modifications non appliquées",
|
||||
"Understand the security implications of privileged containers": "Comprendre les implications de sécurité des conteneurs privilégiés",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "stocks imprévus Proxmox; récupération bloquée",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Répertoire de formatage inattendu dans le nouveau volume Valkey",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Impossibile verificare il disco riutilizzato:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Impossibile scrivere l'archivio immagini su {storage}. Controlla l'accesso allo spazio di archiviazione e lo stato di montaggio.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Non è possibile conservare alcune capability e rimuoverle tutte contemporaneamente",
|
||||
"Catalog options": "Opzioni del catalogo",
|
||||
"Category": "Categoria",
|
||||
"Caution: Maximum mode generates more heat.": "Attenzione: la modalità massima genera più calore.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Controllo Ceph saltato dal flag utente (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Scegli una pianificazione (sintassi calendar-event di systemd):",
|
||||
"Choose a script to execute:": "Scegli uno script da eseguire:",
|
||||
"Choose a server:": "Scegli un server:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Scegli cosa configurare. Le modifiche vengono applicate solo dopo la verifica finale.",
|
||||
"Choose a share to mount:": "Scegli una condivisione da montare:",
|
||||
"Choose a specific supported GPU or USB node": "Scegli uno specifico nodo GPU o USB supportato",
|
||||
"Choose action for this controller/NVMe:": "Scegli l'azione per questo controller/NVMe:",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Disabilitazione e arresto del servizio e del socket rpcbind...",
|
||||
"Disabling non-free firmware warnings...": "Disabilitazione degli avvisi sul firmware non libero...",
|
||||
"Disabling portmapper/rpcbind for security...": "Disabilitazione di portmapper/rpcbind per sicurezza...",
|
||||
"Discard changes": "Scarta le modifiche",
|
||||
"Discard unapplied changes?": "Scartare le modifiche non applicate?",
|
||||
"Discover targets on portal:": "Scopri target sul portale:",
|
||||
"Discovering iSCSI targets...": "Individuazione dei target iSCSI...",
|
||||
"Discovery successful": "Scoperta riuscita",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Nessuna password fornita.",
|
||||
"No paths available": "Nessun percorso disponibile",
|
||||
"No paths selected. Select at least one path.": "Nessun percorso selezionato. Seleziona almeno un percorso.",
|
||||
"No pending changes": "Nessuna modifica in sospeso",
|
||||
"No pending paths to schedule for reboot.": "Nessun percorso in sospeso da pianificare per il riavvio.",
|
||||
"No permanent NFS mounts configured.": "Nessun montaggio NFS permanente configurato.",
|
||||
"No permanent Samba mounts configured.": "Nessun montaggio Samba permanente configurato.",
|
||||
@@ -4402,6 +4407,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM rimosso (gli artefatti di Puppet potrebbero richiedere una pulizia manuale)",
|
||||
"OVH server detected": "Rilevato server OVH",
|
||||
"OVH server detection and RTM installation process completed": "Processo di rilevamento dei server OVH e installazione RTM completato",
|
||||
"Object detector": "Rilevatore di oggetti",
|
||||
"Object detector for Frigate": "Rilevatore di oggetti per Frigate",
|
||||
"Observer is not responding on port 4357": "Observer non risponde sulla porta 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian è un’app per creare, collegare e organizzare le note sul tuo dispositivo, con centinaia di plugin e temi per personalizzare il flusso di lavoro. Puoi pubblicare le note online, consultarle offline e sincronizzarle in modo sicuro con crittografia end-to-end.",
|
||||
@@ -5328,6 +5334,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Rimozione delle ottimizzazioni di ZFS ARC…",
|
||||
"Reverting pigz wrapper...": "Rimozione del wrapper pigz e ripristino della configurazione precedente...",
|
||||
"Reverting vzdump speed tuning...": "Rimozione ottimizzazioni velocità vzdump…",
|
||||
"Review and apply": "Verifica e applica",
|
||||
"Review external OCI changes": "Esamina le modifiche OCI esterne",
|
||||
"Review package upgrade": "Esamina l'aggiornamento dei pacchetti",
|
||||
"Review passthrough config files": "Esamina i file di configurazione passthrough",
|
||||
@@ -5787,6 +5794,7 @@
|
||||
"ShareCoveX panel": "Pannello ShareCoveX",
|
||||
"Shared Directory Ready:": "Directory condivisa pronta:",
|
||||
"Shared Group": "Gruppo condiviso",
|
||||
"Shared directories": "Cartelle condivise",
|
||||
"Shared directory created:": "Directory condivisa creata:",
|
||||
"Shared directory for consume and export": "Directory condivisa per consume ed export",
|
||||
"Shared directory for copy/sync operations": "Directory condivisa per copia e sincronizzazione",
|
||||
@@ -6997,6 +7005,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Impossibile identificare il filesystem root. Interruzione.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "Impossibile individuare i dischi del pool ZFS di sistema. Interruzione.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Impossibile determinare la topologia del disco di sistema. Interruzione.",
|
||||
"Unapplied changes": "Modifiche non applicate",
|
||||
"Understand the security implications of privileged containers": "Comprendere le implicazioni sulla sicurezza dei container privilegiati",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Inventario Proxmox inatteso; ripristino bloccato",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Directory di formattazione imprevista nel nuovo volume Valkey",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Não foi possível verificar o disco reutilizado:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Não é possível gravar o arquivo de imagem em {storage}. Verifique o acesso ao armazenamento e o status de montagem.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Capacidades não podem ser mantidas e todas retiradas ao mesmo tempo",
|
||||
"Catalog options": "Opções do catálogo",
|
||||
"Category": "Categoria",
|
||||
"Caution: Maximum mode generates more heat.": "Cuidado: O modo máximo gera mais calor.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Verificação do Ceph ignorada pelo sinalizador do usuário (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Escolha uma programação (sintaxe de evento de calendário do systemd):",
|
||||
"Choose a script to execute:": "Escolha um script para executar:",
|
||||
"Choose a server:": "Escolha um servidor:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Escolha o que configurar. As alterações só são aplicadas após a revisão final.",
|
||||
"Choose a share to mount:": "Escolha um compartilhamento para montar:",
|
||||
"Choose a specific supported GPU or USB node": "Escolha um GPU ou nó USB específico compatível",
|
||||
"Choose action for this controller/NVMe:": "Escolha a ação para este controlador/NVMe:",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Desativando e parando o serviço e soquete rpcbind...",
|
||||
"Disabling non-free firmware warnings...": "Desativando avisos de firmware não-livre...",
|
||||
"Disabling portmapper/rpcbind for security...": "Desativando portmapper/rpcbind por segurança...",
|
||||
"Discard changes": "Descartar alterações",
|
||||
"Discard unapplied changes?": "Descartar as alterações por aplicar?",
|
||||
"Discover targets on portal:": "Descubra alvos no portal:",
|
||||
"Discovering iSCSI targets...": "Descobrindo alvos iSCSI...",
|
||||
"Discovery successful": "Descoberta bem-sucedida",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Nenhuma senha fornecida.",
|
||||
"No paths available": "Nenhum caminho disponível",
|
||||
"No paths selected. Select at least one path.": "Nenhum caminho selecionado. Selecione pelo menos um caminho.",
|
||||
"No pending changes": "Sem alterações pendentes",
|
||||
"No pending paths to schedule for reboot.": "Não há caminhos pendentes para agendar a reinicialização.",
|
||||
"No permanent NFS mounts configured.": "Nenhuma montagem NFS permanente configurada.",
|
||||
"No permanent Samba mounts configured.": "Nenhuma montagem permanente do Samba configurada.",
|
||||
@@ -4402,6 +4407,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM removido (artefatos de marionetes podem precisar de limpeza manual)",
|
||||
"OVH server detected": "Servidor OVH detectado",
|
||||
"OVH server detection and RTM installation process completed": "Processo de detecção do servidor OVH e instalação RTM concluído",
|
||||
"Object detector": "Detetor de objetos",
|
||||
"Object detector for Frigate": "Detector de objetos para Frigate",
|
||||
"Observer is not responding on port 4357": "Observador não está respondendo na porta 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian é um aplicativo de anotações que permite criar, vincular e organizar suas notas em seu dispositivo, com centenas de plugins e temas para personalizar seu fluxo de trabalho. Você também pode publicar suas notas online, acessá-las offline e sincronizá-las com segurança com criptografia de ponta a ponta.",
|
||||
@@ -5328,6 +5334,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Revertendo ajuste do ZFS ARC...",
|
||||
"Reverting pigz wrapper...": "Revertendo o wrapper pigz...",
|
||||
"Reverting vzdump speed tuning...": "Revertendo o ajuste de velocidade do vzdump...",
|
||||
"Review and apply": "Rever e aplicar",
|
||||
"Review external OCI changes": "revisar alterações externas do OCI",
|
||||
"Review package upgrade": "Rever a atualização de pacotes",
|
||||
"Review passthrough config files": "Revise os arquivos de configuração de passagem",
|
||||
@@ -5787,6 +5794,7 @@
|
||||
"ShareCoveX panel": "Painel ShareCoveX",
|
||||
"Shared Directory Ready:": "Diretório compartilhado pronto:",
|
||||
"Shared Group": "Grupo Compartilhado",
|
||||
"Shared directories": "Diretórios partilhados",
|
||||
"Shared directory created:": "Directório partilhado criado:",
|
||||
"Shared directory for consume and export": "Diretório compartilhado para consumir e exportar",
|
||||
"Shared directory for copy/sync operations": "Diretório compartilhado para copiar/sincronizar operations",
|
||||
@@ -6997,6 +7005,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Não foi possível identificar o sistema de arquivos raiz. Abortando.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "Não foi possível resolver os discos do pool ZFS do sistema. Abortando.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Não é possível resolver a topologia do disco do sistema. Abortando.",
|
||||
"Unapplied changes": "Alterações por aplicar",
|
||||
"Understand the security implications of privileged containers": "Entenda as implicações de segurança de contêineres privilegiados",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Inesperado inventário Proxmox; recuperação bloqueada",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Directório de formatação inesperado no novo volume Valkey",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Nie je možné overiť znovu použitý disk:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Nie je možné zapísať archív obrázkov do {storage}. Skontrolujte prístup k úložisku a stav pripojenia.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Nie je možné udržať si schopnosti a zároveň ich znížiť.",
|
||||
"Catalog options": "Možnosti katalógu",
|
||||
"Category": "Kategória",
|
||||
"Caution: Maximum mode generates more heat.": "Pozor: maximálny režim vytvára viac tepla.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Kontrola Ceph preskočená používateľskou voľbou (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Vyberte plán (syntax systémového kalendára a udalosti):",
|
||||
"Choose a script to execute:": "Vyberte skript, ktorý sa má spustiť:",
|
||||
"Choose a server:": "Vyberte server:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Vyberte nastavenie. Zmeny sa použijú až po záverečnej kontrole.",
|
||||
"Choose a share to mount:": "Vyberte zdieľanie na pripojenie:",
|
||||
"Choose a specific supported GPU or USB node": "Vyberte konkrétny podporovaný uzol GPU alebo USB",
|
||||
"Choose action for this controller/NVMe:": "Vyberte akciu pre tento Controller/NVMe:",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Zakázanie a zastavenie služby rpcbind a soketu...",
|
||||
"Disabling non-free firmware warnings...": "Vypínam upozornenia na non-free firmware...",
|
||||
"Disabling portmapper/rpcbind for security...": "Z bezpečnostných dôvodov vypínam portmapper/rpcbind...",
|
||||
"Discard changes": "Zahodiť zmeny",
|
||||
"Discard unapplied changes?": "Zahodiť nepoužité zmeny?",
|
||||
"Discover targets on portal:": "Vyhľadať targety na portáli:",
|
||||
"Discovering iSCSI targets...": "Vyhľadávam iSCSI targety...",
|
||||
"Discovery successful": "Vyhľadanie bolo úspešné",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Nebolo zadané heslo.",
|
||||
"No paths available": "Nie sú dostupné žiadne cesty",
|
||||
"No paths selected. Select at least one path.": "Nie sú vybrané žiadne cesty. Vyberte aspoň jednu cestu.",
|
||||
"No pending changes": "Žiadne čakajúce zmeny",
|
||||
"No pending paths to schedule for reboot.": "Nie sú žiadne čakajúce cesty, ktoré by sa mali naplánovať na reštart.",
|
||||
"No permanent NFS mounts configured.": "Nie sú nastavené žiadne trvalé NFS pripojenia.",
|
||||
"No permanent Samba mounts configured.": "Nie sú nastavené žiadne trvalé Samba pripojenia.",
|
||||
@@ -4404,6 +4409,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM bol odstránený (Puppet súbory môže byť potrebné vyčistiť ručne)",
|
||||
"OVH server detected": "Zistený OVH server",
|
||||
"OVH server detection and RTM installation process completed": "Kontrola OVH servera a inštalácia RTM sú dokončené",
|
||||
"Object detector": "Detektor objektov",
|
||||
"Object detector for Frigate": "Detektor objektov pre Frigate",
|
||||
"Observer is not responding on port 4357": "Pozorovateľ nereaguje na prístav 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian je note-prijímajúce aplikácie, ktoré vám umožní vytvoriť, odkaz, a usporiadať poznámky na vašom zariadení, so stovkami pluginov a tém prispôsobiť svoje pracovné postupy. Môžete tiež zverejniť svoje poznámky on-line, prístup je offline, a synchronizovať ich bezpečne s end-to-end šifrovanie.",
|
||||
@@ -5330,6 +5336,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Vraciam späť ladenie ZFS ARC...",
|
||||
"Reverting pigz wrapper...": "Vraciam späť wrapper pigz...",
|
||||
"Reverting vzdump speed tuning...": "Vraciam späť ladenie rýchlosti vzdump...",
|
||||
"Review and apply": "Skontrolovať a použiť",
|
||||
"Review external OCI changes": "Skontrolujte externé zmeny OCI",
|
||||
"Review package upgrade": "Skontrolovať aktualizáciu balíkov",
|
||||
"Review passthrough config files": "Skontrolovať konfiguračné súbory passthrough",
|
||||
@@ -5789,6 +5796,7 @@
|
||||
"ShareCoveX panel": "Panel ShareCoveX",
|
||||
"Shared Directory Ready:": "Zdieľaný priečinok je pripravený:",
|
||||
"Shared Group": "Zdieľaná skupina",
|
||||
"Shared directories": "Zdieľané priečinky",
|
||||
"Shared directory created:": "Vytvorený zdieľaný adresár:",
|
||||
"Shared directory for consume and export": "Zdieľaný adresár pre spotrebu a export",
|
||||
"Shared directory for copy/sync operations": "Zdieľaný adresár pre kopírovanie/sync operations",
|
||||
@@ -6998,6 +7006,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Nepodarilo sa identifikovať koreňový súborový systém. Prerušuje sa.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "Nepodarilo sa vyriešiť systémové disky ZFS fondu. Prerušuje sa.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Nepodarilo sa vyriešiť topológiu systémového disku. Prerušuje sa.",
|
||||
"Unapplied changes": "Nepoužité zmeny",
|
||||
"Understand the security implications of privileged containers": "Uistite sa, že rozumiete bezpečnostným dôsledkom privilegovaných kontajnerov",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Neočakávaný inventár Proxmox; zablokovaná obnova",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Neočakávaný formátovanie adresára v novom hlasitosti Valkey",
|
||||
|
||||
@@ -749,6 +749,7 @@
|
||||
"Cannot verify the reused disk:": "Kan inte verifiera den återanvända disken:",
|
||||
"Cannot write the image archive to {storage}. Check storage access and mount status.": "Kan inte skriva bildarkivet till {storage}. Kontrollera lagringsåtkomst och monteringsstatus.",
|
||||
"Capabilities cannot be kept and all dropped at the same time": "Förmågor kan inte hållas och alla tappas samtidigt",
|
||||
"Catalog options": "Katalogalternativ",
|
||||
"Category": "Kategori",
|
||||
"Caution: Maximum mode generates more heat.": "Varning: Maximalt läge genererar mer värme.",
|
||||
"Ceph check skipped by user flag (--ignore-ceph-check)": "Ceph-kontroll hoppas över av användarflaggan (--ignore-ceph-check)",
|
||||
@@ -883,6 +884,7 @@
|
||||
"Choose a schedule (systemd calendar-event syntax):": "Välj ett schema (syntax för systemd kalenderhändelser):",
|
||||
"Choose a script to execute:": "Välj ett skript att köra:",
|
||||
"Choose a server:": "Välj en server:",
|
||||
"Choose a setting. Changes are applied only after the final review.": "Välj en inställning. Ändringarna tillämpas först efter den slutliga granskningen.",
|
||||
"Choose a share to mount:": "Välj en resurs att montera:",
|
||||
"Choose a specific supported GPU or USB node": "Välj en specifik GPU- eller USB-nod som stöds",
|
||||
"Choose action for this controller/NVMe:": "Välj åtgärd för denna kontroller/NVMe:",
|
||||
@@ -1766,6 +1768,8 @@
|
||||
"Disabling and stopping rpcbind service and socket...": "Inaktivera och stoppa rpcbind-tjänsten och -socket...",
|
||||
"Disabling non-free firmware warnings...": "Inaktiverar icke-fri firmware-varningar...",
|
||||
"Disabling portmapper/rpcbind for security...": "Inaktiverar portmapper/rpcbind för säkerhet...",
|
||||
"Discard changes": "Kasta ändringar",
|
||||
"Discard unapplied changes?": "Kasta ändringar som inte har tillämpats?",
|
||||
"Discover targets on portal:": "Upptäck mål på portalen:",
|
||||
"Discovering iSCSI targets...": "Upptäcker iSCSI-mål...",
|
||||
"Discovery successful": "Upptäckten lyckades",
|
||||
@@ -4261,6 +4265,7 @@
|
||||
"No password provided.": "Inget lösenord angett.",
|
||||
"No paths available": "Inga vägar tillgängliga",
|
||||
"No paths selected. Select at least one path.": "Inga sökvägar har valts. Välj minst en väg.",
|
||||
"No pending changes": "Inga väntande ändringar",
|
||||
"No pending paths to schedule for reboot.": "Inga väntande sökvägar att schemalägga för omstart.",
|
||||
"No permanent NFS mounts configured.": "Inga permanenta NFS-monteringar har konfigurerats.",
|
||||
"No permanent Samba mounts configured.": "Inga permanenta Samba-fästen har konfigurerats.",
|
||||
@@ -4402,6 +4407,7 @@
|
||||
"OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM har tagits bort (Puppet artefakter kan behöva manuell rengöring)",
|
||||
"OVH server detected": "OVH-server upptäckt",
|
||||
"OVH server detection and RTM installation process completed": "OVH-serverdetektering och RTM-installation har slutförts",
|
||||
"Object detector": "Objektdetektor",
|
||||
"Object detector for Frigate": "Objektdetektor för Frigate",
|
||||
"Observer is not responding on port 4357": "Observer svarar inte på port 4357",
|
||||
"Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian är en noterande app som låter dig skapa, länka och organisera dina anteckningar på din enhet, med hundratals plugins och teman för att anpassa ditt arbetsflöde. Du kan också publicera dina anteckningar online, komma åt dem offline och synkronisera dem säkert med end-to-end-kryptering.",
|
||||
@@ -5328,6 +5334,7 @@
|
||||
"Reverting ZFS ARC tuning...": "Återställer ZFS ARC-inställning...",
|
||||
"Reverting pigz wrapper...": "Återställer pigz-omslag...",
|
||||
"Reverting vzdump speed tuning...": "Återställer vzdump-hastighetsinställning...",
|
||||
"Review and apply": "Granska och tillämpa",
|
||||
"Review external OCI changes": "Granska externa OCI ändringar",
|
||||
"Review package upgrade": "Granska paketuppgraderingen",
|
||||
"Review passthrough config files": "Granska passthrough-konfigurationsfiler",
|
||||
@@ -5787,6 +5794,7 @@
|
||||
"ShareCoveX panel": "ShareCoveX panel",
|
||||
"Shared Directory Ready:": "Redo för delad katalog:",
|
||||
"Shared Group": "Delad grupp",
|
||||
"Shared directories": "Delade kataloger",
|
||||
"Shared directory created:": "Delad katalog skapad:",
|
||||
"Shared directory for consume and export": "Delad katalog för konsumering och export",
|
||||
"Shared directory for copy/sync operations": "Delad katalog för kopia/synkronisera operations",
|
||||
@@ -6997,6 +7005,7 @@
|
||||
"Unable to identify the root filesystem. Aborting.": "Det går inte att identifiera rotfilsystemet. Avbryter.",
|
||||
"Unable to resolve system ZFS pool disks. Aborting.": "Det går inte att lösa system ZFS pooldiskar. Avbryter.",
|
||||
"Unable to resolve system disk topology. Aborting.": "Det gick inte att lösa systemdisktopologin. Avbryter.",
|
||||
"Unapplied changes": "Ändringar som inte har tillämpats",
|
||||
"Understand the security implications of privileged containers": "Förstå säkerhetskonsekvenserna av privilegierade containrar",
|
||||
"Unexpected Proxmox inventory; recovery blocked": "Oväntad Proxmox inventering; återhämtning blockerad",
|
||||
"Unexpected formatting directory in the new Valkey volume": "Oväntad formateringskatalog i den nya Valkey-volymen",
|
||||
|
||||
@@ -423,25 +423,8 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()):
|
||||
proposal = None
|
||||
if action == 'modify':
|
||||
from .recreation import edit_recreation
|
||||
from .cli import _deployment_summary_text
|
||||
from .ui import BacktrackUI, RestartWizard
|
||||
wizard = BacktrackUI(ui)
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
proposal = edit_recreation(record, wizard)
|
||||
approved = wizard.review(_deployment_summary_text(proposal['candidate']['template'],
|
||||
proposal['candidate']['deployment']), translate('Modify OCI'),
|
||||
question=translate('Apply these changes?'), default=True)
|
||||
break
|
||||
except RestartWizard:
|
||||
wizard.restart()
|
||||
except ValueError as error:
|
||||
if not wizard.retry_last(error):
|
||||
raise
|
||||
finally:
|
||||
wizard.close()
|
||||
if not approved:
|
||||
proposal = edit_recreation(record, ui)
|
||||
if proposal is None:
|
||||
return False
|
||||
elif not ui.review(translate('If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If the update fails after the container was changed, the backup is restored. An update that stops halfway is recovered from the OCI management menu.'),
|
||||
translate('Update OCI'), question=translate('Update now?'), default=True):
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
from .i18n import translate
|
||||
from .ui import UserCancelled
|
||||
from .ui import BacktrackUI, RestartWizard, UserCancelled
|
||||
|
||||
|
||||
def positive_integer(ui, prompt, value, minimum=1):
|
||||
@@ -30,13 +30,13 @@ def edit_network(deployment, ui):
|
||||
network.update(bridge=bridge, ipv4=ipv4, gateway=gateway)
|
||||
|
||||
|
||||
def edit_acceleration(candidate, ui):
|
||||
def edit_acceleration(candidate, ui, confirm=True):
|
||||
from .installer import configure_acceleration
|
||||
deployment = candidate['deployment']
|
||||
template = candidate.get('template', {})
|
||||
installer = template.get('proxmox', {}).get('installer_profile', {})
|
||||
hardware = installer.get('hardware_acceleration')
|
||||
if not hardware or not ui.confirm(translate('Change GPU acceleration?'), False):
|
||||
if not hardware or (confirm and not ui.confirm(translate('Change GPU acceleration?'), False)):
|
||||
return
|
||||
profiles = hardware.get('profiles', [])
|
||||
reference = template.get('container_contract', {}).get('image', {}).get('reference', '')
|
||||
@@ -104,44 +104,142 @@ def edit_environment(deployment, ui):
|
||||
environment.append(item)
|
||||
|
||||
|
||||
def edit_recreation(record, ui):
|
||||
candidate = copy.deepcopy(record)
|
||||
refresh_template(candidate, ui)
|
||||
def modification_options(candidate):
|
||||
"""Only offer supported settings; never put environment values in the menu."""
|
||||
deployment = candidate['deployment']
|
||||
resources = deployment['resources']
|
||||
resources['cores'] = positive_integer(ui, translate('Cores'), resources['cores'])
|
||||
resources['memory_mb'] = positive_integer(ui, translate('RAM in MiB'), resources['memory_mb'], 128)
|
||||
profile = candidate.get('template', {}).get('proxmox', {}).get('installer_profile', {})
|
||||
network = deployment.get('network', {})
|
||||
address = network.get('ipv4', 'dhcp')
|
||||
|
||||
def row(label, value):
|
||||
return f"{label:<26} {value}"
|
||||
|
||||
options = [
|
||||
('cores', row(translate('CPU cores'), resources['cores'])),
|
||||
('memory', row(translate('RAM in MiB'), resources['memory_mb'])),
|
||||
('network', row(translate('Network'),
|
||||
f"{network.get('bridge', '-')} · {'DHCP' if address == 'dhcp' else address}")),
|
||||
('paths', row(translate('Extra paths'), len(deployment.get('mounts', [])))),
|
||||
]
|
||||
if profile.get('shared_directories'):
|
||||
options.append(('shares', translate('Shared directories')))
|
||||
if profile.get('hardware_acceleration'):
|
||||
hardware = profile['hardware_acceleration']
|
||||
selected = next((p for p in hardware.get('profiles', [])
|
||||
if p['id'] == deployment.get('hardware_profile')), {})
|
||||
options.append(('acceleration', row(translate('Acceleration'),
|
||||
translate(selected.get('label') or deployment.get('hardware_profile') or 'CPU'))))
|
||||
if profile.get('object_detector'):
|
||||
options.append(('detector', translate('Object detector')))
|
||||
options += [
|
||||
('devices', row(translate('Devices'), len(deployment.get('devices', [])))),
|
||||
('environment', row(translate('Variables'), len(deployment.get('environment', [])))),
|
||||
('catalog', translate('Catalog options')),
|
||||
('apply', translate('Review and apply')),
|
||||
('discard', translate('Discard changes')),
|
||||
]
|
||||
return options
|
||||
|
||||
|
||||
def edit_setting(candidate, ui, setting):
|
||||
deployment = candidate['deployment']
|
||||
template = candidate.get('template', {})
|
||||
if template.get('proxmox', {}).get('installer_profile', {}).get('shared_directories'):
|
||||
profile = template.get('proxmox', {}).get('installer_profile', {})
|
||||
if setting == 'cores':
|
||||
resources = deployment['resources']
|
||||
resources['cores'] = positive_integer(ui, translate('Cores'), resources['cores'])
|
||||
elif setting == 'memory':
|
||||
resources = deployment['resources']
|
||||
resources['memory_mb'] = positive_integer(ui, translate('RAM in MiB'), resources['memory_mb'], 128)
|
||||
elif setting == 'network':
|
||||
edit_network(deployment, ui)
|
||||
elif setting == 'shares':
|
||||
from .installer import ask_shared_directories
|
||||
deployment['mounts'] += ask_shared_directories(
|
||||
ui, template, deployment['mounts'], deployment['rootfs']['storage'], True, optional=True,
|
||||
unprivileged=deployment.get('security', {}).get('unprivileged', True))
|
||||
from .custom_mounts import ask_custom_mounts
|
||||
deployment['mounts'] = ask_custom_mounts(
|
||||
ui, deployment['mounts'], deployment['rootfs']['storage'])
|
||||
if ui.confirm(translate('Change the access network?'), False):
|
||||
edit_network(deployment, ui)
|
||||
edit_acceleration(candidate, ui)
|
||||
from .installer import configure_detector
|
||||
installer_profile = candidate.get('template', {}).get('proxmox', {}).get('installer_profile', {})
|
||||
deployment['devices'], notes = configure_detector(installer_profile, deployment.get('devices', []), ui)
|
||||
if notes:
|
||||
deployment['completion_notes'] = notes
|
||||
from .extra_devices import ask_extra_devices
|
||||
reference = candidate.get('template', {}).get('container_contract', {}).get('image', {}).get('reference', '')
|
||||
repository = reference.split('@')[0].rsplit(':', 1)[0]
|
||||
deployment['devices'] = ask_extra_devices(
|
||||
ui, deployment.get('devices', []), deployment.get('security', {}).get('unprivileged', True),
|
||||
allow_coral=repository in ('ghcr.io/blakeblackshear/frigate', 'codeproject/ai-server', 'docker.io/codeproject/ai-server'))
|
||||
from .extra_devices import device_permissions
|
||||
deployment['device_permissions'] = device_permissions(
|
||||
reference, deployment['devices'], deployment.get('device_permissions'))
|
||||
edit_environment(deployment, ui)
|
||||
proposal = {'operation': 'recreate', 'candidate': candidate}
|
||||
if record.get('observed', {}).get('config_sha256'):
|
||||
proposal['base_config_sha256'] = record['observed']['config_sha256']
|
||||
return proposal
|
||||
elif setting == 'paths':
|
||||
from .custom_mounts import ask_custom_mounts
|
||||
deployment['mounts'] = ask_custom_mounts(ui, deployment['mounts'], deployment['rootfs']['storage'])
|
||||
elif setting == 'acceleration':
|
||||
edit_acceleration(candidate, ui, confirm=False)
|
||||
elif setting == 'detector':
|
||||
from .installer import configure_detector
|
||||
deployment['devices'], notes = configure_detector(profile, deployment.get('devices', []), ui)
|
||||
if notes:
|
||||
deployment['completion_notes'] = notes
|
||||
elif setting == 'devices':
|
||||
from .extra_devices import ask_extra_devices
|
||||
reference = template.get('container_contract', {}).get('image', {}).get('reference', '')
|
||||
repository = reference.split('@')[0].rsplit(':', 1)[0]
|
||||
deployment['devices'] = ask_extra_devices(
|
||||
ui, deployment.get('devices', []), deployment.get('security', {}).get('unprivileged', True),
|
||||
allow_coral=repository in ('ghcr.io/blakeblackshear/frigate', 'codeproject/ai-server', 'docker.io/codeproject/ai-server'))
|
||||
elif setting == 'environment':
|
||||
edit_environment(deployment, ui)
|
||||
elif setting == 'catalog':
|
||||
refresh_template(candidate, ui)
|
||||
else:
|
||||
raise ValueError(setting)
|
||||
if setting in ('acceleration', 'detector', 'devices'):
|
||||
from .extra_devices import device_permissions
|
||||
reference = candidate.get('template', {}).get('container_contract', {}).get('image', {}).get('reference', '')
|
||||
permissions = device_permissions(reference, deployment.get('devices', []), deployment.get('device_permissions'))
|
||||
if permissions is not None or 'device_permissions' in deployment:
|
||||
deployment['device_permissions'] = permissions
|
||||
|
||||
|
||||
def edit_section(candidate, ui, setting):
|
||||
"""A cancelled or invalid section cannot leave a half-edited draft."""
|
||||
wizard = BacktrackUI(ui)
|
||||
try:
|
||||
while True:
|
||||
edited = copy.deepcopy(candidate)
|
||||
try:
|
||||
edit_setting(edited, wizard, setting)
|
||||
return edited
|
||||
except RestartWizard:
|
||||
wizard.restart()
|
||||
except UserCancelled:
|
||||
return candidate
|
||||
except ValueError as error:
|
||||
if not wizard.retry_last(error):
|
||||
ui.message(str(error), translate('Modify OCI'))
|
||||
return candidate
|
||||
finally:
|
||||
wizard.close()
|
||||
|
||||
|
||||
def edit_recreation(record, ui):
|
||||
"""Keep a draft across independently editable settings; apply it once."""
|
||||
from .cli import _deployment_summary_text
|
||||
candidate = copy.deepcopy(record)
|
||||
selected = 'cores'
|
||||
while True:
|
||||
pending = candidate != record
|
||||
status = translate('Unapplied changes') if pending else translate('No pending changes')
|
||||
text = (f"CT {record.get('vmid', '-')}: {candidate['deployment'].get('hostname', '')}\n"
|
||||
f"{status}\n\n"
|
||||
+ translate('Choose a setting. Changes are applied only after the final review.'))
|
||||
selected = ui.choose(text, modification_options(candidate), selected,
|
||||
title=translate('Modify OCI'), show_tags=False)
|
||||
if selected is None or selected == 'discard':
|
||||
if not pending or ui.confirm(translate('Discard unapplied changes?'), False):
|
||||
return None
|
||||
selected = 'apply'
|
||||
elif selected == 'apply':
|
||||
if not pending:
|
||||
ui.message(translate('Nothing was changed.'), translate('Modify OCI'))
|
||||
return None
|
||||
if ui.review(_deployment_summary_text(candidate['template'], candidate['deployment']),
|
||||
translate('Modify OCI'), question=translate('Apply these changes?'), default=True):
|
||||
proposal = {'operation': 'recreate', 'candidate': candidate}
|
||||
if record.get('observed', {}).get('config_sha256'):
|
||||
proposal['base_config_sha256'] = record['observed']['config_sha256']
|
||||
return proposal
|
||||
else:
|
||||
candidate = edit_section(candidate, ui, selected)
|
||||
|
||||
|
||||
def refresh_template(candidate, ui):
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Selective OCI edits stay in a draft until reviewed, then use one transaction."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
sys.path.insert(0, str(ROOT / 'remote'))
|
||||
|
||||
from proxmenux_oci import management, recreation
|
||||
from proxmenux_oci.ui import BackRequested, UserCancelled
|
||||
|
||||
|
||||
def record():
|
||||
return {'vmid': 115, 'installation_id': 'test-installation', 'status': 'installed',
|
||||
'observed': {'config_sha256': 'native-config-fingerprint'},
|
||||
'template': {'id': 'example-app', 'catalog_ui': {'title': 'Example'},
|
||||
'container_contract': {'image': {'reference': 'example/app:latest'}},
|
||||
'proxmox': {'installer_profile': {}}},
|
||||
'deployment': {'vmid': 115, 'hostname': 'example', 'resources': {'cores': 2, 'memory_mb': 512},
|
||||
'network': {'bridge': 'vmbr0', 'ipv4': 'dhcp', 'gateway': None},
|
||||
'rootfs': {'storage': 'local-zfs', 'size_gb': 8}, 'mounts': [
|
||||
{'container_path': '/data', 'source': 'local-zfs',
|
||||
'type': 'managed-volume', 'size_gb': 8, 'backup': True}],
|
||||
'devices': [], 'security': {'unprivileged': True},
|
||||
'environment': [{'name': 'PASSWORD', 'value': 'private-test-secret',
|
||||
'sensitive': True}]}}
|
||||
|
||||
|
||||
class MenuUI:
|
||||
def __init__(self, choices, answers=(), reviews=(True,), confirms=()):
|
||||
self.choices, self.answers = iter(choices), iter(answers)
|
||||
self.reviews, self.confirms = iter(reviews), iter(confirms)
|
||||
self.events = []
|
||||
self.back_enabled = False
|
||||
|
||||
def choose(self, text, options, default=None, **kwargs):
|
||||
self.events.append(('menu', text, dict(options)))
|
||||
value = next(self.choices)
|
||||
if value is not None:
|
||||
assert value in dict(options), value
|
||||
return value
|
||||
|
||||
def ask(self, text, default=None, **kwargs):
|
||||
self.events.append(('ask', text, default))
|
||||
value = next(self.answers)
|
||||
if isinstance(value, Exception):
|
||||
raise value
|
||||
return value
|
||||
|
||||
def confirm(self, text, default=False):
|
||||
self.events.append(('confirm', text))
|
||||
return next(self.confirms)
|
||||
|
||||
def review(self, text, *args, **kwargs):
|
||||
self.events.append(('review', text))
|
||||
return next(self.reviews)
|
||||
|
||||
def message(self, text, *args):
|
||||
self.events.append(('message', text))
|
||||
|
||||
|
||||
@patch('proxmenux_oci.i18n.language', return_value='en')
|
||||
class ModifyMenuTests(unittest.TestCase):
|
||||
def test_ram_only_does_not_ask_about_cpu_network_paths_devices_or_catalog(self, _language):
|
||||
original = record()
|
||||
saved = copy.deepcopy(original)
|
||||
ui = MenuUI(['memory', 'apply'], ['768'])
|
||||
with patch.object(recreation, 'refresh_template') as refresh:
|
||||
result = recreation.edit_recreation(original, ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 2, 'memory_mb': 768})
|
||||
self.assertEqual([e[1] for e in ui.events if e[0] == 'ask'], ['RAM in MiB'])
|
||||
self.assertEqual(result['base_config_sha256'], 'native-config-fingerprint')
|
||||
self.assertEqual(original, saved)
|
||||
refresh.assert_not_called()
|
||||
self.assertFalse(ui.back_enabled)
|
||||
|
||||
def test_several_sections_and_revisits_keep_the_current_draft(self, _language):
|
||||
ui = MenuUI(['memory', 'cores', 'memory', 'apply'], ['768', '3', '1024'])
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 1024})
|
||||
self.assertEqual([e[2] for e in ui.events if e[0] == 'ask'], ['512', '2', '768'])
|
||||
self.assertEqual(sum(e[0] == 'review' for e in ui.events), 1)
|
||||
|
||||
def test_declining_review_returns_to_menu_without_losing_edits(self, _language):
|
||||
ui = MenuUI(['cores', 'apply', 'memory', 'apply'], ['3', '768'], reviews=[False, True])
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 768})
|
||||
self.assertEqual(sum(e[0] == 'review' for e in ui.events), 2)
|
||||
|
||||
def test_unchanged_apply_does_not_propose_recreating_the_guest(self, _language):
|
||||
ui = MenuUI(['memory', 'apply'], ['512'])
|
||||
self.assertIsNone(recreation.edit_recreation(record(), ui))
|
||||
self.assertFalse(any(e[0] == 'review' for e in ui.events))
|
||||
self.assertIn(('message', 'Nothing was changed.'), ui.events)
|
||||
|
||||
def test_discard_leaves_the_saved_record_untouched(self, _language):
|
||||
original = record()
|
||||
saved = copy.deepcopy(original)
|
||||
ui = MenuUI(['memory', 'discard'], ['768'], confirms=[True])
|
||||
self.assertIsNone(recreation.edit_recreation(original, ui))
|
||||
self.assertEqual(original, saved)
|
||||
self.assertFalse(any(e[0] == 'review' for e in ui.events))
|
||||
|
||||
def test_declining_discard_preserves_the_draft(self, _language):
|
||||
ui = MenuUI(['memory', None, 'apply'], ['768'], confirms=[False])
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources']['memory_mb'], 768)
|
||||
|
||||
def test_cancel_without_changes_asks_nothing_else(self, _language):
|
||||
ui = MenuUI([None])
|
||||
self.assertIsNone(recreation.edit_recreation(record(), ui))
|
||||
self.assertEqual(len(ui.events), 1)
|
||||
|
||||
def test_invalid_ram_is_retried_without_restarting_other_sections(self, _language):
|
||||
ui = MenuUI(['cores', 'memory', 'apply'], ['3', 'invalid', '64', '768'])
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 768})
|
||||
self.assertEqual(sum(e[0] == 'message' for e in ui.events), 2)
|
||||
self.assertEqual(sum(e[:2] == ('ask', 'Cores') for e in ui.events), 1)
|
||||
|
||||
def test_back_from_first_field_returns_to_menu_and_keeps_other_edits(self, _language):
|
||||
ui = MenuUI(['cores', 'memory', 'apply'], ['3', BackRequested()])
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 512})
|
||||
self.assertFalse(ui.back_enabled)
|
||||
|
||||
def test_cancelling_a_partly_edited_section_rolls_back_only_that_section(self, _language):
|
||||
real_edit = recreation.edit_setting
|
||||
def edit(candidate, ui, setting):
|
||||
if setting == 'environment':
|
||||
candidate['deployment']['environment'][0]['value'] = 'partial-value'
|
||||
raise UserCancelled()
|
||||
real_edit(candidate, ui, setting)
|
||||
ui = MenuUI(['cores', 'environment', 'apply'], ['3'])
|
||||
with patch.object(recreation, 'edit_setting', side_effect=edit):
|
||||
result = recreation.edit_recreation(record(), ui)
|
||||
self.assertEqual(result['candidate']['deployment']['resources']['cores'], 3)
|
||||
self.assertEqual(result['candidate']['deployment']['environment'], record()['deployment']['environment'])
|
||||
|
||||
def test_menu_and_review_do_not_expose_a_saved_secret(self, _language):
|
||||
ui = MenuUI(['cores', 'apply'], ['3'])
|
||||
recreation.edit_recreation(record(), ui)
|
||||
self.assertNotIn('private-test-secret', json.dumps(ui.events))
|
||||
|
||||
def test_optional_categories_follow_the_application_profile(self, _language):
|
||||
candidate = record()
|
||||
options = dict(recreation.modification_options(candidate))
|
||||
self.assertTrue({'cores', 'memory', 'network', 'paths', 'devices', 'environment', 'catalog', 'apply'} <= options.keys())
|
||||
self.assertFalse({'shares', 'acceleration', 'detector'} & options.keys())
|
||||
candidate['template']['proxmox']['installer_profile'] = {
|
||||
'shared_directories': {'base_path': '/shares'}, 'object_detector': {'devices': []},
|
||||
'hardware_acceleration': {'profiles': [{'id': 'intel', 'label': 'Intel GPU'}]}}
|
||||
candidate['deployment']['hardware_profile'] = 'intel'
|
||||
options = dict(recreation.modification_options(candidate))
|
||||
self.assertTrue({'shares', 'acceleration', 'detector'} <= options.keys())
|
||||
self.assertIn('Intel GPU', options['acceleration'])
|
||||
|
||||
|
||||
class ModifyDispatchTests(unittest.TestCase):
|
||||
def call_manager(self, proposal, lifecycle):
|
||||
import oci_instances
|
||||
import oci_instance_reconcile
|
||||
saved = record()
|
||||
row = {'vmid': 115, 'reason': 'matched', 'stack': False, 'pending': False, 'status': 'installed'}
|
||||
with patch.object(management, 'carry_records', return_value={}), \
|
||||
patch.object(management, 'check_selected', return_value=row), \
|
||||
patch.object(management, '_work_backup_ready', return_value=True), \
|
||||
patch.object(oci_instances, 'read', return_value=saved), \
|
||||
patch.object(oci_instances, 'command', return_value='hostname: example'), \
|
||||
patch.object(oci_instance_reconcile, 'propose', return_value=None), \
|
||||
patch.object(recreation, 'edit_recreation', return_value=proposal), \
|
||||
patch.object(management, '_run_lifecycle', side_effect=lifecycle):
|
||||
return management.manage_instance(ROOT, Mock(), row, action='modify')
|
||||
|
||||
def test_cancel_does_not_start_a_lifecycle_transaction(self):
|
||||
lifecycle = Mock()
|
||||
self.assertFalse(self.call_manager(None, lifecycle))
|
||||
lifecycle.assert_not_called()
|
||||
|
||||
def test_apply_sends_one_private_proposal_to_the_existing_backend(self):
|
||||
proposal = {'operation': 'recreate', 'candidate': record(), 'base_config_sha256': 'fingerprint'}
|
||||
seen = []
|
||||
def lifecycle(command, title):
|
||||
path = Path(command[command.index('--proposal') + 1])
|
||||
seen.append(json.loads(path.read_text()))
|
||||
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600)
|
||||
self.assertNotIn('private-test-secret', ' '.join(command))
|
||||
self.assertIn('oci_update_current.py', command[1])
|
||||
self.assertIn('--acknowledge-external-data', command)
|
||||
return True
|
||||
self.assertTrue(self.call_manager(proposal, lifecycle))
|
||||
self.assertEqual(seen, [proposal])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -43,15 +43,18 @@ class ScriptedUI:
|
||||
def password(self, text, required=True):
|
||||
return self._answer(text, "")
|
||||
|
||||
def choose(self, text, options, default=None):
|
||||
def choose(self, text, options, default=None, **_kwargs):
|
||||
return self._answer(text, default)
|
||||
|
||||
def confirm(self, text, default=False):
|
||||
return self._answer(text, default)
|
||||
|
||||
def message(self, text):
|
||||
def message(self, text, *_args):
|
||||
pass
|
||||
|
||||
def review(self, *_args, **_kwargs):
|
||||
return True
|
||||
|
||||
|
||||
@patch("proxmenux_oci.i18n.language", return_value="en")
|
||||
@patch("proxmenux_oci.installer.host.storages", side_effect=storages)
|
||||
@@ -223,14 +226,15 @@ class SharedDirectoriesTests(unittest.TestCase):
|
||||
patch("proxmenux_oci.recreation.edit_acceleration"), \
|
||||
patch("proxmenux_oci.recreation.edit_environment"):
|
||||
ui = ScriptedUI({"Add a directory to share": [True], NAME: ["photos"],
|
||||
"Choose a setting. Changes are applied only after the final review.": ["shares", "apply"],
|
||||
"Where to store /shares/photos": ["host-bind"],
|
||||
"Host path for /shares/photos": ["/tank/photos"]})
|
||||
proposal = edit_recreation(record, ui)
|
||||
mounts = [m["container_path"] for m in proposal["candidate"]["deployment"]["mounts"]]
|
||||
self.assertEqual(mounts, ["/config", "/shares/media", "/shares/photos"])
|
||||
untouched = edit_recreation(record, ScriptedUI())
|
||||
self.assertEqual([m["container_path"] for m in untouched["candidate"]["deployment"]["mounts"]],
|
||||
["/config", "/shares/media"])
|
||||
untouched = edit_recreation(record, ScriptedUI({
|
||||
"Choose a setting. Changes are applied only after the final review.": ["discard"]}))
|
||||
self.assertIsNone(untouched)
|
||||
self.assertEqual([m["container_path"] for m in plan["mounts"]], ["/config", "/shares/media"])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user