mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Apply OCI runtime settings to existing containers with recoverable edits
This commit is contained in:
@@ -31,7 +31,9 @@ jobs:
|
||||
- name: Run OCI image reuse and resource edit regressions
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
run: python3 -m unittest discover -s oci/tests -p test_modify_image_reuse.py -v
|
||||
run: |
|
||||
python3 -m unittest discover -s oci/tests -p test_modify_image_reuse.py -v
|
||||
python3 -m unittest discover -s oci/tests -p test_native_modify.py -v
|
||||
- name: Run offline registry account tests
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Anwenden von Controller/NVMe-Passthrough auf die VM",
|
||||
"Applying balanced memory optimization settings...": "Ausgewogene Einstellungen zur Speicheroptimierung werden angewendet...",
|
||||
"Applying changes safely...": "Änderungen sicher anwenden...",
|
||||
"Applying container settings without rebuilding the container...": "Containereinstellungen werden ohne Neuerstellung des Containers angewendet...",
|
||||
"Applying custom restore": "Benutzerdefinierte Wiederherstellung anwenden",
|
||||
"Applying default VM configuration": "Anwenden der Standard-VM-Konfiguration",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Anwenden von Hostberechtigungen für nicht privilegierte LXC-Bind-Mounts ...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Wert für ungültig ALLOWED HOSTs",
|
||||
"Invalid Home Assistant OS check timeout": "Invalid Home Assistant OS Check Timeout",
|
||||
"Invalid ID": "Ungültige ID",
|
||||
"Invalid IPv4 address": "Ungültige IPv4-Adresse",
|
||||
"Invalid IPv4 gateway": "Ungültiges IPv4-Gateway",
|
||||
"Invalid Intel render path": "Ungültiger Intel-Renderpfad",
|
||||
"Invalid Jellyfin path:": "Ungültige Jellyfin-Strecke:",
|
||||
"Invalid MAC address:": "Ungültige MAC-Adresse:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "E-Mail des ungültigen Administrators",
|
||||
"Invalid administrator user name": "Ungültiger Administrator Benutzername",
|
||||
"Invalid base VMID": "Ungültige Basis VMID",
|
||||
"Invalid bridge": "Ungültige Bridge",
|
||||
"Invalid check package:": "Ungültiges Kontrollpaket:",
|
||||
"Invalid configuration path:": "Ungültiger Konfigurationspfad:",
|
||||
"Invalid consume/export volumes": "Ungültige Verbrauchs-/Ausfuhrmengen",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Name der ungültigen Fernbedienung",
|
||||
"Invalid remote path": "Ungültiger Fernpfad",
|
||||
"Invalid repair version:": "Ungültige Reparaturversion:",
|
||||
"Invalid resource value": "Ungültiger Ressourcenwert",
|
||||
"Invalid resources": "Ungültige Mittel",
|
||||
"Invalid restored volume path": "Ungültiger wiederhergestellter Volumenpfad",
|
||||
"Invalid running state": "Invaliditätsstatus",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Ungültige Stapelmitglieder",
|
||||
"Invalid stack name": "Name des ungültigen Stacks",
|
||||
"Invalid stack operation": "Ungültiger Stapel operation",
|
||||
"Invalid storage": "Ungültiger Speicher",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Ungültige Speicher-ID. Verwenden Sie nur Buchstaben, Zahlen, Bindestriche und Unterstriche.",
|
||||
"Invalid storage name": "Ungültiger Speichername",
|
||||
"Invalid suite application": "Anwendung ungültiger Suiten",
|
||||
@@ -4888,6 +4894,7 @@
|
||||
"Proxmox VE configuration completed.": "Proxmox VE-Konfiguration abgeschlossen.",
|
||||
"Proxmox VE safe update completed": "Sicheres Proxmox VE-Update abgeschlossen",
|
||||
"Proxmox auth logger service created and started": "Proxmox-Auth-Logger-Dienst erstellt und gestartet",
|
||||
"Proxmox could not apply the container settings": "Proxmox konnte die Containereinstellungen nicht anwenden",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Das Proxmox-Enterprise-Repository fehlt möglicherweise oder ist nicht zugänglich. Ich versuche, auf kein Abonnement umzustellen...",
|
||||
"Proxmox filter configured": "Proxmox-Filter konfiguriert",
|
||||
"Proxmox iSCSI Storage Status:": "Proxmox iSCSI-Speicherstatus:",
|
||||
@@ -6327,6 +6334,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "Der Build konnte vorher nicht überprüft werden;ohne diese Prüfung weitermachen.",
|
||||
"The cached image does not match the current digest": "Das zwischengespeicherte Bild entspricht nicht dem aktuellen Digest",
|
||||
"The cached image is damaged; it will be downloaded again.": "Das zwischengespeicherte Bild ist beschädigt; es wird erneut heruntergeladen.",
|
||||
"The changes need the application installer": "Die Änderungen erfordern das Installationsprogramm der Anwendung",
|
||||
"The character device does not exist:": "Das Zeichengerät existiert nicht:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Der Befehl fragt nach einem Passwort, das nicht wiederholt wird. Nach dem Erstellen der Benutzer fragt die Weboberfläche nach einem Benutzernamen und einem Passwort.",
|
||||
"The command does not name an image": "Der Befehl benennt kein Bild",
|
||||
@@ -6446,6 +6454,7 @@
|
||||
"The host bind source does not exist:": "Die Host-Bind-Quelle existiert nicht:",
|
||||
"The host bind source is not a regular file or directory:": "Die Host-Bind-Quelle ist keine reguläre Datei oder ein Verzeichnis:",
|
||||
"The host directory changed before it was mounted": "Das Host-Verzeichnis wurde geändert, bevor es gemountet wurde",
|
||||
"The host directory does not exist": "Das Verzeichnis auf dem Host ist nicht vorhanden",
|
||||
"The host directory may not be accessible from an unprivileged container.": "Auf das Hostverzeichnis kann von einem unprivilegierten Container aus möglicherweise nicht zugegriffen werden.",
|
||||
"The host has no IPv4 address on the selected bridge": "Der Host hat keine IPv4-Adresse auf der ausgewählten Brücke",
|
||||
"The host monitor does not see the real host memory": "Der Host-Monitor sieht den echten Host-Speicher nicht",
|
||||
@@ -6683,6 +6692,7 @@
|
||||
"The selected VM": "Die ausgewählte VM",
|
||||
"The selected VM is running.": "Die ausgewählte VM läuft.",
|
||||
"The selected base folder does not exist and could not be created:": "Der ausgewählte Basisordner existiert nicht und konnte nicht erstellt werden:",
|
||||
"The selected bridge does not exist": "Die ausgewählte Bridge ist nicht vorhanden",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "Die ausgewählte Bridge verfügt über kein IPv4-Subnetz für die Host-Monitor-Firewall",
|
||||
"The selected configuration needs to start the LXC during the installation": "Die ausgewählte Konfiguration muss den LXC während der Installation starten",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Der ausgewählte Container ist nicht privilegiert. Für den direkten Geräte-Passthrough ist ein privilegierter Container erforderlich.",
|
||||
@@ -6705,6 +6715,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "Der Dienst erstellt ein eigenes Image; nur ein veröffentlichtes Image kann installiert werden",
|
||||
"The service declares no image:": "Der Dienst erklärt kein Bild:",
|
||||
"The setting has no final value:": "Die Einstellung hat keinen Endwert:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Die Einstellungen konnten nicht vollständig zurückgesetzt werden. Prüfe den Container und die gespeicherte Änderung, bevor du fortfährst.",
|
||||
"The share already exists in smb.conf:": "Die Freigabe existiert bereits in smb.conf:",
|
||||
"The shared destination is not a directory": "Das gemeinsame Ziel ist kein Verzeichnis",
|
||||
"The shared directory points to a protected host path": "Das Shared Directory zeigt auf einen geschützten Hostpfad",
|
||||
@@ -6779,6 +6790,7 @@
|
||||
"These interface configurations will be removed": "Diese Schnittstellenkonfigurationen werden entfernt",
|
||||
"These manual changes cannot be adopted safely:": "Diese manuellen Änderungen können nicht sicher übernommen werden:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Diese Pfade werden nicht live wiederhergestellt und zur manuellen Wiederherstellung extrahiert.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Diese Einstellungen erfordern eine anwendungsspezifische Anpassung. Das Image wurde nicht heruntergeladen und der Container nicht neu erstellt.",
|
||||
"These values are asked during the installation:": "Diese Werte werden während der Installation abgefragt:",
|
||||
"They are described in the image documentation:": "Sie sind in der Dokumentation des Images beschrieben:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Sie werden zu oci-compose hinzugefügt, bevor Sie fortfahren. Nicht unterstützte oder geänderte Ressourcen werden nicht importiert.",
|
||||
@@ -7114,6 +7126,7 @@
|
||||
"Unsupported mount type": "Typ der nicht unterstützten Halterung",
|
||||
"Unsupported mount type:": "Typ der nicht unterstützten Halterung:",
|
||||
"Unsupported native device type:": "Nicht unterstützter nativer Gerätetyp:",
|
||||
"Unsupported network settings": "Nicht unterstützte Netzwerkeinstellungen",
|
||||
"Unsupported operation": "Nicht unterstützte operation",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Nicht unterstützte oder unbekannte Proxmox-Version; es wurde kein Repository geändert.",
|
||||
"Unsupported output format:": "Nicht unterstütztes Ausgabeformat:",
|
||||
|
||||
@@ -445,6 +445,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Aplicando el passthrough de controlador/NVMe a la VM",
|
||||
"Applying balanced memory optimization settings...": "Aplicando configuraciones equilibradas de optimización de memoria...",
|
||||
"Applying changes safely...": "Aplicando cambios de forma segura...",
|
||||
"Applying container settings without rebuilding the container...": "Aplicando los ajustes sin recrear el contenedor...",
|
||||
"Applying custom restore": "Aplicar restauración personalizada",
|
||||
"Applying default VM configuration": "Aplicar la configuración predeterminada de VM",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Aplicando permisos de host para montajes de enlace LXC sin privilegios...",
|
||||
@@ -3211,6 +3212,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Valor inválido ALLOWED_HOSTS",
|
||||
"Invalid Home Assistant OS check timeout": "Inválido Home Assistant OS check timeout",
|
||||
"Invalid ID": "ID no válida",
|
||||
"Invalid IPv4 address": "Dirección IPv4 no válida",
|
||||
"Invalid IPv4 gateway": "Puerta de enlace IPv4 no válida",
|
||||
"Invalid Intel render path": "Ruta de renderización Intel no válida",
|
||||
"Invalid Jellyfin path:": "Camino inválido Jellyfin:",
|
||||
"Invalid MAC address:": "Dirección MAC inválida:",
|
||||
@@ -3243,6 +3246,7 @@
|
||||
"Invalid administrator email": "Correo electrónico del administrador inválido",
|
||||
"Invalid administrator user name": "Nombre del usuario del administrador inválido",
|
||||
"Invalid base VMID": "Base inválida VMID",
|
||||
"Invalid bridge": "Puente no válido",
|
||||
"Invalid check package:": "Paquete de comprobación inválido:",
|
||||
"Invalid configuration path:": "Vía de configuración inválida:",
|
||||
"Invalid consume/export volumes": "Volumen de consumo/exportación inválidos",
|
||||
@@ -3306,6 +3310,7 @@
|
||||
"Invalid remote name": "Nombre remoto inválido",
|
||||
"Invalid remote path": "Camino remoto inválido",
|
||||
"Invalid repair version:": "Versión de reparación inválida:",
|
||||
"Invalid resource value": "Valor de recursos no válido",
|
||||
"Invalid resources": "Recursos inválidos",
|
||||
"Invalid restored volume path": "Camino de volumen restaurado inválido",
|
||||
"Invalid running state": "Estado de funcionamiento inválido",
|
||||
@@ -3324,6 +3329,7 @@
|
||||
"Invalid stack members": "Miembros de pila no válidos",
|
||||
"Invalid stack name": "Nombre de pila no válido",
|
||||
"Invalid stack operation": "Operación de pila no válida",
|
||||
"Invalid storage": "Almacenamiento no válido",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de almacenamiento no válido. Utilice únicamente letras, números, guiones y guiones bajos.",
|
||||
"Invalid storage name": "Nombre de almacenamiento no válido",
|
||||
"Invalid suite application": "Aplicación de suite inválida",
|
||||
@@ -4879,6 +4885,7 @@
|
||||
"Proxmox VE configuration completed.": "Configuración de Proxmox VE completada.",
|
||||
"Proxmox VE safe update completed": "Actualización segura de Proxmox VE completada",
|
||||
"Proxmox auth logger service created and started": "Servicio de registro de autenticación Proxmox creado e iniciado",
|
||||
"Proxmox could not apply the container settings": "Proxmox no ha podido aplicar los ajustes del contenedor",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Es posible que falte el repositorio empresarial de Proxmox o que no se pueda acceder a él. Intentando cambiar a sin suscripción...",
|
||||
"Proxmox filter configured": "Filtro Proxmox configurado",
|
||||
"Proxmox iSCSI Storage Status:": "Estado de almacenamiento Proxmox iSCSI:",
|
||||
@@ -6310,6 +6317,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "La compilación no se pudo comprobar de antemano; se continúa sin esa comprobación.",
|
||||
"The cached image does not match the current digest": "La imagen en caché no coincide con el digest actual",
|
||||
"The cached image is damaged; it will be downloaded again.": "La imagen en caché está dañada; se descargará de nuevo.",
|
||||
"The changes need the application installer": "Los cambios requieren el instalador de la aplicación",
|
||||
"The character device does not exist:": "El dispositivo de carácter no existe:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "El comando pide una contraseña que no se muestra al escribirla. Tras crear los usuarios, la interfaz web pide un nombre de usuario y una contraseña.",
|
||||
"The command does not name an image": "El comando no nombra ninguna imagen",
|
||||
@@ -6428,6 +6436,7 @@
|
||||
"The host bind source does not exist:": "La fuente de host bind no existe:",
|
||||
"The host bind source is not a regular file or directory:": "La fuente host bind no es un archivo o directorio regular:",
|
||||
"The host directory changed before it was mounted": "El directorio host cambió antes de montarlo",
|
||||
"The host directory does not exist": "El directorio del host no existe",
|
||||
"The host directory may not be accessible from an unprivileged container.": "Es posible que no se pueda acceder al directorio del host desde un contenedor sin privilegios.",
|
||||
"The host has no IPv4 address on the selected bridge": "El host no tiene dirección IPv4 en el puente seleccionado",
|
||||
"The host monitor does not see the real host memory": "El monitor host no ve la memoria real del host",
|
||||
@@ -6659,6 +6668,7 @@
|
||||
"The selected VM": "La máquina virtual seleccionada",
|
||||
"The selected VM is running.": "La VM seleccionada se está ejecutando.",
|
||||
"The selected base folder does not exist and could not be created:": "La carpeta base seleccionada no existe y no se pudo crear:",
|
||||
"The selected bridge does not exist": "El puente seleccionado no existe",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "El bridge seleccionado no tiene subred IPv4 para el cortafuegos del monitor del host",
|
||||
"The selected configuration needs to start the LXC during the installation": "La configuración seleccionada debe iniciar el LXC durante la instalación",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "El contenedor seleccionado no tiene privilegios. Se requiere un contenedor privilegiado para el passthrough directo de dispositivos.",
|
||||
@@ -6681,6 +6691,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "El servicio construye su propia imagen; solo se puede instalar una imagen publicada",
|
||||
"The service declares no image:": "El servicio no declara imagen:",
|
||||
"The setting has no final value:": "El ajuste no tiene valor final:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "No se han podido revertir todos los ajustes. Revisa el contenedor y la modificación guardada antes de continuar.",
|
||||
"The share already exists in smb.conf:": "El recurso compartido ya existe en smb.conf:",
|
||||
"The shared destination is not a directory": "El destino compartido no es un directorio",
|
||||
"The shared directory points to a protected host path": "El directorio compartido apunta a una ruta de host protegida",
|
||||
@@ -6754,6 +6765,7 @@
|
||||
"These interface configurations will be removed": "Estas configuraciones de interfaz serán eliminadas",
|
||||
"These manual changes cannot be adopted safely:": "Estos cambios manuales no se pueden incorporar con seguridad:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Estas rutas no se restaurarán en vivo y se extraerán para su recuperación manual.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Estos ajustes requieren una adaptación específica de la aplicación. No se ha descargado la imagen ni se ha recreado el contenedor.",
|
||||
"These values are asked during the installation:": "Estos valores se preguntan durante la instalación:",
|
||||
"They are described in the image documentation:": "Están descritos en la documentación de la imagen:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Se añadirán al oci-compose antes de continuar. No se importarán recursos incompatibles o modificados.",
|
||||
@@ -7089,6 +7101,7 @@
|
||||
"Unsupported mount type": "Tipo de montaje no compatible",
|
||||
"Unsupported mount type:": "Tipo de montaje no compatible:",
|
||||
"Unsupported native device type:": "Tipo de dispositivo nativo sin soporte:",
|
||||
"Unsupported network settings": "Ajustes de red no compatibles",
|
||||
"Unsupported operation": "Operación sin soporte",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Versión de Proxmox no compatible o desconocida; no se ha modificado ningún repositorio.",
|
||||
"Unsupported output format:": "Formato de salida no admitido:",
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Application du relais Controller/NVMe à la VM",
|
||||
"Applying balanced memory optimization settings...": "Application des paramètres d'optimisation de la mémoire équilibrée...",
|
||||
"Applying changes safely...": "Appliquer les modifications en toute sécurité...",
|
||||
"Applying container settings without rebuilding the container...": "Application des paramètres sans recréer le conteneur...",
|
||||
"Applying custom restore": "Application d'une restauration personnalisée",
|
||||
"Applying default VM configuration": "Application de la configuration de VM par défaut",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Application des autorisations d'hôte pour les montages de liaison LXC non privilégiés...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Valeur HÔTES INDÉSIRABLES",
|
||||
"Invalid Home Assistant OS check timeout": "Non valide Home Assistant OS check timeout",
|
||||
"Invalid ID": "Pièce d'identité invalide",
|
||||
"Invalid IPv4 address": "Adresse IPv4 non valide",
|
||||
"Invalid IPv4 gateway": "Passerelle IPv4 non valide",
|
||||
"Invalid Intel render path": "Chemin de rendu Intel incorrect",
|
||||
"Invalid Jellyfin path:": "Chemin Jellyfin non valide:",
|
||||
"Invalid MAC address:": "Adresse MAC non valide:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "Courriel administrateur non valide",
|
||||
"Invalid administrator user name": "Nom d'utilisateur non valide",
|
||||
"Invalid base VMID": "VMID de base non valide",
|
||||
"Invalid bridge": "Pont réseau non valide",
|
||||
"Invalid check package:": "Dossier de contrôle non valide:",
|
||||
"Invalid configuration path:": "Chemin de configuration non valide & #160;:",
|
||||
"Invalid consume/export volumes": "Volumes de consommation/d'exportation non valides",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Nom distant non valide",
|
||||
"Invalid remote path": "Voie distante non valide",
|
||||
"Invalid repair version:": "Version de réparation non valide:",
|
||||
"Invalid resource value": "Valeur de ressource non valide",
|
||||
"Invalid resources": "Ressources non valides",
|
||||
"Invalid restored volume path": "Voie de volume restaurée non valide",
|
||||
"Invalid running state": "État de fonctionnement non valide",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Membres de pile non valides",
|
||||
"Invalid stack name": "Nom de la pile non valide",
|
||||
"Invalid stack operation": "Pile non valide operation",
|
||||
"Invalid storage": "Stockage non valide",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de stockage invalide. Utilisez uniquement des lettres, des chiffres, des traits d'union et des traits de soulignement.",
|
||||
"Invalid storage name": "Nom de stockage invalide",
|
||||
"Invalid suite application": "Demande de suite non valide",
|
||||
@@ -4888,6 +4894,7 @@
|
||||
"Proxmox VE configuration completed.": "Configuration de Proxmox VE terminée.",
|
||||
"Proxmox VE safe update completed": "Mise à jour sécurisée de Proxmox VE terminée",
|
||||
"Proxmox auth logger service created and started": "Service d'enregistrement d'authentification Proxmox créé et démarré",
|
||||
"Proxmox could not apply the container settings": "Proxmox n’a pas pu appliquer les paramètres du conteneur",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Le référentiel d'entreprise Proxmox peut être manquant ou inaccessible. J'essaie de passer au sans abonnement...",
|
||||
"Proxmox filter configured": "Filtre Proxmox configuré",
|
||||
"Proxmox iSCSI Storage Status:": "État du stockage Proxmox iSCSI :",
|
||||
@@ -6326,6 +6333,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "La build n'a pas pu être vérifiée au préalable ;continuer sans cette vérification.",
|
||||
"The cached image does not match the current digest": "L'image mise en cache ne correspond pas au digest actuel",
|
||||
"The cached image is damaged; it will be downloaded again.": "L'image mise en cache est endommagée ; elle sera à nouveau téléchargée.",
|
||||
"The changes need the application installer": "Ces modifications nécessitent le programme d’installation de l’application",
|
||||
"The character device does not exist:": "Le dispositif de caractère n'existe pas :",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "La commande demande un mot de passe qui n'est pas repris. Après la création des utilisateurs, l'interface web demande un nom d'utilisateur et un mot de passe.",
|
||||
"The command does not name an image": "La commande ne nomme pas une image",
|
||||
@@ -6445,6 +6453,7 @@
|
||||
"The host bind source does not exist:": "La source de liaison hôte n'existe pas:",
|
||||
"The host bind source is not a regular file or directory:": "La source de liaison hôte n'est pas un fichier ou un répertoire régulier :",
|
||||
"The host directory changed before it was mounted": "Le répertoire hôte a changé avant son montage",
|
||||
"The host directory does not exist": "Le répertoire de l’hôte n’existe pas",
|
||||
"The host directory may not be accessible from an unprivileged container.": "Le répertoire hôte peut ne pas être accessible à partir d'un conteneur non privilégié.",
|
||||
"The host has no IPv4 address on the selected bridge": "L'hôte n'a pas d'adresse IPv4 sur le pont sélectionné",
|
||||
"The host monitor does not see the real host memory": "Le moniteur hôte ne voit pas la mémoire de l'hôte réel",
|
||||
@@ -6682,6 +6691,7 @@
|
||||
"The selected VM": "La VM sélectionnée",
|
||||
"The selected VM is running.": "La VM sélectionnée est en cours d'exécution.",
|
||||
"The selected base folder does not exist and could not be created:": "Le dossier de base sélectionné n'existe pas et n'a pas pu être créé :",
|
||||
"The selected bridge does not exist": "Le pont réseau sélectionné n’existe pas",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "Le pont sélectionné n'a pas de sous-réseau IPv4 pour le pare-feu du moniteur hôte",
|
||||
"The selected configuration needs to start the LXC during the installation": "La configuration sélectionnée doit démarrer le LXC pendant l'installation",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Le conteneur sélectionné n'est pas privilégié. Un conteneur privilégié est requis pour le relais direct du périphérique.",
|
||||
@@ -6704,6 +6714,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "Le service construit sa propre image; seule une image publiée peut être installée",
|
||||
"The service declares no image:": "Le service ne déclare aucune image:",
|
||||
"The setting has no final value:": "Le réglage n'a pas de valeur finale:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Les paramètres n’ont pas pu être entièrement rétablis. Vérifiez le conteneur et la modification enregistrée avant de continuer.",
|
||||
"The share already exists in smb.conf:": "Le partage existe déjà dans smb.conf :",
|
||||
"The shared destination is not a directory": "La destination partagée n'est pas un répertoire",
|
||||
"The shared directory points to a protected host path": "Le répertoire partagé indique un chemin d'hôte protégé",
|
||||
@@ -6778,6 +6789,7 @@
|
||||
"These interface configurations will be removed": "Ces configurations d'interface seront supprimées",
|
||||
"These manual changes cannot be adopted safely:": "Ces modifications manuelles ne peuvent pas être adoptées en toute sécurité :",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Ces chemins ne seront pas restaurés en direct et seront extraits pour une récupération manuelle.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Ces paramètres nécessitent une adaptation propre à l’application. L’image n’a pas été téléchargée et le conteneur n’a pas été recréé.",
|
||||
"These values are asked during the installation:": "Ces valeurs sont demandées lors de l'installation:",
|
||||
"They are described in the image documentation:": "Ils sont décrits dans la documentation de l'image :",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Ils seront ajoutés à oci-compose avant de continuer. Les ressources non prises en charge ou modifiées ne sont pas importées.",
|
||||
@@ -7113,6 +7125,7 @@
|
||||
"Unsupported mount type": "Type de montage non supporté",
|
||||
"Unsupported mount type:": "Type de montage non pris en charge:",
|
||||
"Unsupported native device type:": "Type de périphérique natif non pris en charge:",
|
||||
"Unsupported network settings": "Paramètres réseau non pris en charge",
|
||||
"Unsupported operation": "operation non prise en charge",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Version Proxmox non prise en charge ou inconnue ; aucun référentiel n'a été modifié.",
|
||||
"Unsupported output format:": "Format de sortie non pris en charge :",
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Applicazione del passthrough Controller/NVMe alla VM",
|
||||
"Applying balanced memory optimization settings...": "Applicazione delle impostazioni bilanciate di ottimizzazione della memoria...",
|
||||
"Applying changes safely...": "Applicazione sicura delle modifiche...",
|
||||
"Applying container settings without rebuilding the container...": "Applicazione delle impostazioni senza ricreare il container...",
|
||||
"Applying custom restore": "Applicazione del ripristino personalizzato",
|
||||
"Applying default VM configuration": "Applicazione della configurazione predefinita della VM",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Applicazione delle autorizzazioni sull’host per i bind mount dei container LXC non privilegiati...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Valore ALLOWED_HOSTS non valido",
|
||||
"Invalid Home Assistant OS check timeout": "Timeout controllo Home Assistant OS non valido",
|
||||
"Invalid ID": "ID non valido",
|
||||
"Invalid IPv4 address": "Indirizzo IPv4 non valido",
|
||||
"Invalid IPv4 gateway": "Gateway IPv4 non valido",
|
||||
"Invalid Intel render path": "Percorso di rendering Intel non valido",
|
||||
"Invalid Jellyfin path:": "Percorso Jellyfin non valido:",
|
||||
"Invalid MAC address:": "Indirizzo MAC non valido:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "Email amministratore non valida",
|
||||
"Invalid administrator user name": "Nome utente amministratore non valido",
|
||||
"Invalid base VMID": "VMID di base non valido",
|
||||
"Invalid bridge": "Bridge non valido",
|
||||
"Invalid check package:": "Pacchetto controllo non valido:",
|
||||
"Invalid configuration path:": "Percorso configurazione non valido:",
|
||||
"Invalid consume/export volumes": "Volumi di acquisizione/esportazione non validi",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Nome del remote non valido",
|
||||
"Invalid remote path": "Percorso remoto non valido",
|
||||
"Invalid repair version:": "Versione di riparazione non valida:",
|
||||
"Invalid resource value": "Valore della risorsa non valido",
|
||||
"Invalid resources": "Risorse non valide",
|
||||
"Invalid restored volume path": "Percorso volume ripristinato non valido",
|
||||
"Invalid running state": "Stato di esecuzione non valido",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Membri stack non validi",
|
||||
"Invalid stack name": "Nome stack non valido",
|
||||
"Invalid stack operation": "Operazione stack non valida",
|
||||
"Invalid storage": "Archiviazione non valida",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID storage non valido. Usa solo lettere, numeri, trattini e trattini bassi.",
|
||||
"Invalid storage name": "Nome storage non valido",
|
||||
"Invalid suite application": "Applicazione suite non valida",
|
||||
@@ -4888,6 +4894,7 @@
|
||||
"Proxmox VE configuration completed.": "Configurazione Proxmox VE completata.",
|
||||
"Proxmox VE safe update completed": "Aggiornamento sicuro di Proxmox VE completato",
|
||||
"Proxmox auth logger service created and started": "Servizio log autenticazione Proxmox creato e avviato",
|
||||
"Proxmox could not apply the container settings": "Proxmox non è riuscito ad applicare le impostazioni del container",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Il repository enterprise Proxmox potrebbe essere mancante o inaccessibile. Tentativo di passare al repository no-subscription...",
|
||||
"Proxmox filter configured": "Filtro Proxmox configurato",
|
||||
"Proxmox iSCSI Storage Status:": "Stato storage iSCSI Proxmox:",
|
||||
@@ -6327,6 +6334,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "Non è stato possibile verificare la build in anticipo; si prosegue senza tale verifica.",
|
||||
"The cached image does not match the current digest": "L'immagine nella cache non corrisponde al digest corrente",
|
||||
"The cached image is damaged; it will be downloaded again.": "L'immagine nella cache è danneggiata; verrà scaricata nuovamente.",
|
||||
"The changes need the application installer": "Le modifiche richiedono il programma di installazione dell’applicazione",
|
||||
"The character device does not exist:": "Il dispositivo a caratteri non esiste:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Il comando richiede una password non visualizzata durante la digitazione. Dopo la creazione degli utenti, l’interfaccia web richiede nome utente e password.",
|
||||
"The command does not name an image": "Il comando non specifica un’immagine",
|
||||
@@ -6446,6 +6454,7 @@
|
||||
"The host bind source does not exist:": "La sorgente del bind mount sull’host non esiste:",
|
||||
"The host bind source is not a regular file or directory:": "La sorgente del bind mount sull'host non è un file regolare né una directory:",
|
||||
"The host directory changed before it was mounted": "La directory dell'host è cambiata prima del montaggio",
|
||||
"The host directory does not exist": "La directory dell’host non esiste",
|
||||
"The host directory may not be accessible from an unprivileged container.": "La directory dell’host potrebbe non essere accessibile da un container non privilegiato.",
|
||||
"The host has no IPv4 address on the selected bridge": "L'host non ha un indirizzo IPv4 sul bridge selezionato",
|
||||
"The host monitor does not see the real host memory": "Il monitoraggio dell'host non rileva la memoria reale dell'host",
|
||||
@@ -6683,6 +6692,7 @@
|
||||
"The selected VM": "La VM selezionata",
|
||||
"The selected VM is running.": "La VM selezionata è in esecuzione.",
|
||||
"The selected base folder does not exist and could not be created:": "La cartella di base selezionata non esiste e non è stato possibile crearla:",
|
||||
"The selected bridge does not exist": "Il bridge selezionato non esiste",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "Il bridge selezionato non ha una sottorete IPv4 per il firewall del monitoraggio dell’host",
|
||||
"The selected configuration needs to start the LXC during the installation": "La configurazione selezionata richiede l'avvio del container LXC durante l'installazione",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Il container selezionato non è privilegiato. È richiesto un container privilegiato per il passthrough diretto del dispositivo.",
|
||||
@@ -6705,6 +6715,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "Il servizio crea la propria immagine; si possono installare solo immagini pubblicate",
|
||||
"The service declares no image:": "Il servizio non dichiara alcuna immagine:",
|
||||
"The setting has no final value:": "L'impostazione non ha un valore finale:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Non è stato possibile ripristinare tutte le impostazioni. Controlla il container e la modifica salvata prima di continuare.",
|
||||
"The share already exists in smb.conf:": "La condivisione esiste già in smb.conf:",
|
||||
"The shared destination is not a directory": "La destinazione condivisa non è una directory",
|
||||
"The shared directory points to a protected host path": "La directory condivisa punta a un percorso protetto dell'host",
|
||||
@@ -6779,6 +6790,7 @@
|
||||
"These interface configurations will be removed": "Queste configurazioni dell'interfaccia verranno rimosse",
|
||||
"These manual changes cannot be adopted safely:": "Queste modifiche manuali non possono essere acquisite in modo sicuro:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Questi percorsi non verranno ripristinati sul sistema in esecuzione e verranno estratti per il ripristino manuale.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Queste impostazioni richiedono un adattamento specifico per l’applicazione. L’immagine non è stata scaricata e il container non è stato ricreato.",
|
||||
"These values are asked during the installation:": "Questi valori verranno richiesti durante l’installazione:",
|
||||
"They are described in the image documentation:": "Sono descritti nella documentazione dell'immagine:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Se confermi e la verifica finale riesce, queste risorse verranno registrate in oci-compose prima di proseguire. Le risorse non supportate o modificate non vengono acquisite.",
|
||||
@@ -7114,6 +7126,7 @@
|
||||
"Unsupported mount type": "Tipo mount non supportato",
|
||||
"Unsupported mount type:": "Tipo mount non supportato:",
|
||||
"Unsupported native device type:": "Tipo dispositivo nativo non supportato:",
|
||||
"Unsupported network settings": "Impostazioni di rete non supportate",
|
||||
"Unsupported operation": "Operazione non supportata",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Versione Proxmox non supportata o sconosciuta; nessun repository è stato modificato.",
|
||||
"Unsupported output format:": "Formato output non supportato:",
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Aplicando passagem de controlador/NVMe à VM",
|
||||
"Applying balanced memory optimization settings...": "Aplicando configurações de otimização de memória balanceadas...",
|
||||
"Applying changes safely...": "Aplicando alterações com segurança...",
|
||||
"Applying container settings without rebuilding the container...": "A aplicar as definições sem recriar o contentor...",
|
||||
"Applying custom restore": "Aplicando restauração personalizada",
|
||||
"Applying default VM configuration": "Aplicando configuração de VM padrão",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Aplicando permissões de host para montagens de ligação LXC sem privilégios...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Valor Allowed hosts inválido",
|
||||
"Invalid Home Assistant OS check timeout": "Tempo de verificação do sistema operacional Home Assistant inválido",
|
||||
"Invalid ID": "ID inválido",
|
||||
"Invalid IPv4 address": "Endereço IPv4 inválido",
|
||||
"Invalid IPv4 gateway": "Gateway IPv4 inválido",
|
||||
"Invalid Intel render path": "Caminho de renderização Intel inválido",
|
||||
"Invalid Jellyfin path:": "Caminho Jellyfin inválido:",
|
||||
"Invalid MAC address:": "Endereço MAC inválido:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "E- mail de administrador inválido",
|
||||
"Invalid administrator user name": "Nome de usuário do administrador inválido",
|
||||
"Invalid base VMID": "VMID base inválida",
|
||||
"Invalid bridge": "Bridge inválida",
|
||||
"Invalid check package:": "Pacote de verificação inválido:",
|
||||
"Invalid configuration path:": "Caminho de configuração inválido:",
|
||||
"Invalid consume/export volumes": "Volumes de consumo/exportação inválidos",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Nome remoto inválido",
|
||||
"Invalid remote path": "Caminho remoto inválido",
|
||||
"Invalid repair version:": "Versão de reparação inválida:",
|
||||
"Invalid resource value": "Valor de recurso inválido",
|
||||
"Invalid resources": "Recursos inválidos",
|
||||
"Invalid restored volume path": "Caminho de volume restaurado inválido",
|
||||
"Invalid running state": "Estado de execução inválido",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Membros inválidos da pilha",
|
||||
"Invalid stack name": "Nome da pilha inválido",
|
||||
"Invalid stack operation": "Pilha inválida operation",
|
||||
"Invalid storage": "Armazenamento inválido",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de armazenamento inválido. Use apenas letras, números, hífens e sublinhados.",
|
||||
"Invalid storage name": "Nome de armazenamento inválido",
|
||||
"Invalid suite application": "Aplicativo de suite inválido",
|
||||
@@ -4888,6 +4894,7 @@
|
||||
"Proxmox VE configuration completed.": "Configuração do Proxmox VE concluída.",
|
||||
"Proxmox VE safe update completed": "Atualização segura do Proxmox VE concluída",
|
||||
"Proxmox auth logger service created and started": "Serviço de registrador de autenticação Proxmox criado e iniciado",
|
||||
"Proxmox could not apply the container settings": "O Proxmox não conseguiu aplicar as definições do contentor",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "O repositório corporativo Proxmox pode estar ausente ou inacessível. Tentando mudar para sem assinatura...",
|
||||
"Proxmox filter configured": "Filtro Proxmox configurado",
|
||||
"Proxmox iSCSI Storage Status:": "Status de armazenamento iSCSI do Proxmox:",
|
||||
@@ -6327,6 +6334,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "A compilação não pôde ser verificada antecipadamente;continuando sem essa verificação.",
|
||||
"The cached image does not match the current digest": "A imagem em cache não corresponde à digest atual",
|
||||
"The cached image is damaged; it will be downloaded again.": "A imagem em cache está danificada; será transferida novamente.",
|
||||
"The changes need the application installer": "As alterações requerem o instalador da aplicação",
|
||||
"The character device does not exist:": "O dispositivo de caracteres não existe:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "O comando pede uma senha que não seja ecoada. Depois de criar os usuários, a interface web pede um nome de usuário e uma senha.",
|
||||
"The command does not name an image": "O comando não nomeia uma imagem",
|
||||
@@ -6446,6 +6454,7 @@
|
||||
"The host bind source does not exist:": "O código de ligação da máquina não existe:",
|
||||
"The host bind source is not a regular file or directory:": "O código de ligação da máquina não é um ficheiro ou directório regular:",
|
||||
"The host directory changed before it was mounted": "A pasta da máquina mudou antes de ser montada",
|
||||
"The host directory does not exist": "O diretório do anfitrião não existe",
|
||||
"The host directory may not be accessible from an unprivileged container.": "O diretório host pode não estar acessível a partir de um contêiner sem privilégios.",
|
||||
"The host has no IPv4 address on the selected bridge": "A máquina não tem endereço IPv4 na ponte seleccionada",
|
||||
"The host monitor does not see the real host memory": "O monitor da máquina não vê a memória verdadeira da máquina",
|
||||
@@ -6683,6 +6692,7 @@
|
||||
"The selected VM": "A VM selecionada",
|
||||
"The selected VM is running.": "A VM selecionada está em execução.",
|
||||
"The selected base folder does not exist and could not be created:": "A pasta base selecionada não existe e não pôde ser criada:",
|
||||
"The selected bridge does not exist": "A bridge selecionada não existe",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "A ponte selecionada não possui sub-rede IPv4 para o firewall do monitor host",
|
||||
"The selected configuration needs to start the LXC during the installation": "A configuração selecionada precisa iniciar o LXC durante a instalação",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "O contêiner selecionado não tem privilégios. Um contêiner privilegiado é necessário para passagem direta do dispositivo.",
|
||||
@@ -6705,6 +6715,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "O serviço constrói sua própria imagem; somente uma imagem publicada pode ser instalada",
|
||||
"The service declares no image:": "O serviço não declara imagem:",
|
||||
"The setting has no final value:": "A configuração não tem valor final:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Não foi possível repor todas as definições. Reveja o contentor e a alteração guardada antes de continuar.",
|
||||
"The share already exists in smb.conf:": "O compartilhamento já existe em smb.conf:",
|
||||
"The shared destination is not a directory": "O destino compartilhado não é um diretório",
|
||||
"The shared directory points to a protected host path": "A pasta partilhada aponta para uma localização protegida da máquina",
|
||||
@@ -6779,6 +6790,7 @@
|
||||
"These interface configurations will be removed": "Essas configurações de interface serão removidas",
|
||||
"These manual changes cannot be adopted safely:": "Estas alterações manuais não podem ser adotadas com segurança:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Esses caminhos não serão restaurados ao vivo e serão extraídos para recuperação manual.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Estas definições requerem uma adaptação específica da aplicação. A imagem não foi transferida e o contentor não foi recriado.",
|
||||
"These values are asked during the installation:": "Estes valores são questionados durante a instalação:",
|
||||
"They are described in the image documentation:": "Estão descritos na documentação da imagem:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Eles serão adicionados ao oci-compose antes de continuar. Recursos não suportados ou alterados não são importados.",
|
||||
@@ -7114,6 +7126,7 @@
|
||||
"Unsupported mount type": "Tipo de montagem não suportado",
|
||||
"Unsupported mount type:": "Tipo de montagem não suportado:",
|
||||
"Unsupported native device type:": "Tipo de dispositivo nativo não suportado:",
|
||||
"Unsupported network settings": "Definições de rede não suportadas",
|
||||
"Unsupported operation": "operation não suportado",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Versão Proxmox não suportada ou desconhecida; nenhum repositório foi alterado.",
|
||||
"Unsupported output format:": "Formato de saída não suportado:",
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Používam priame priradenie Controller/NVMe do VM",
|
||||
"Applying balanced memory optimization settings...": "Používam vyvážené nastavenia optimalizácie pamäte...",
|
||||
"Applying changes safely...": "Bezpečne používam zmeny...",
|
||||
"Applying container settings without rebuilding the container...": "Aplikujú sa nastavenia bez opätovného vytvorenia kontajnera...",
|
||||
"Applying custom restore": "Používam vlastnú obnovu",
|
||||
"Applying default VM configuration": "Používam predvolené nastavenie VM",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Nastavujem oprávnenia hosta pre LXC bind mounty v neprivilegovaných kontajneroch...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Neplatná hodnota UDELENÁ HOSTS",
|
||||
"Invalid Home Assistant OS check timeout": "Neplatný čas odhlásenia Home Assistant OS",
|
||||
"Invalid ID": "Neplatné ID",
|
||||
"Invalid IPv4 address": "Neplatná adresa IPv4",
|
||||
"Invalid IPv4 gateway": "Neplatná brána IPv4",
|
||||
"Invalid Intel render path": "Neplatná cesta k zobrazeniu informácií",
|
||||
"Invalid Jellyfin path:": "Neplatná cesta Jellyfin:",
|
||||
"Invalid MAC address:": "Neplatná adresa MAC:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "Neplatný administrátorský mail",
|
||||
"Invalid administrator user name": "Neplatné meno správcu",
|
||||
"Invalid base VMID": "Neplatná základňa VMID",
|
||||
"Invalid bridge": "Neplatný sieťový most",
|
||||
"Invalid check package:": "Neplatný kontrolný balík:",
|
||||
"Invalid configuration path:": "Neplatná konfiguračná dráha:",
|
||||
"Invalid consume/export volumes": "Neplatné objemy spotreby/vývozu",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Neplatné vzdialené meno",
|
||||
"Invalid remote path": "Neplatná vzdialená cesta",
|
||||
"Invalid repair version:": "Neplatná verzia opravy:",
|
||||
"Invalid resource value": "Neplatná hodnota prostriedkov",
|
||||
"Invalid resources": "Neplatné zdroje",
|
||||
"Invalid restored volume path": "Neplatná cesta obnoveného objemu",
|
||||
"Invalid running state": "Neplatný jazdný stav",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Neplatní členovia stohu",
|
||||
"Invalid stack name": "Neplatný názov stohu",
|
||||
"Invalid stack operation": "Neplatný zásobník operation",
|
||||
"Invalid storage": "Neplatné úložisko",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Neplatné ID úložiska. Použite iba písmená, čísla, pomlčky a podčiarkovníky.",
|
||||
"Invalid storage name": "Neplatný názov úložiska",
|
||||
"Invalid suite application": "Neplatná aplikácia",
|
||||
@@ -4890,6 +4896,7 @@
|
||||
"Proxmox VE configuration completed.": "Nastavenie Proxmox VE je dokončené.",
|
||||
"Proxmox VE safe update completed": "Bezpečná aktualizácia Proxmox VE je dokončená",
|
||||
"Proxmox auth logger service created and started": "Služba na logovanie Proxmox prihlásení bola vytvorená a spustená",
|
||||
"Proxmox could not apply the container settings": "Proxmox nedokázal použiť nastavenia kontajnera",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Proxmox enterprise repozitár môže chýbať alebo byť nedostupný. Skúšam prepnúť na no-subscription...",
|
||||
"Proxmox filter configured": "Proxmox filter nastavený",
|
||||
"Proxmox iSCSI Storage Status:": "Stav iSCSI úložiska v Proxmoxe:",
|
||||
@@ -6328,6 +6335,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "Zostavenie nebolo možné vopred skontrolovať;pokračovanie bez tejto kontroly.",
|
||||
"The cached image does not match the current digest": "Comment",
|
||||
"The cached image is damaged; it will be downloaded again.": "Comment",
|
||||
"The changes need the application installer": "Zmeny vyžadujú inštalačný program aplikácie",
|
||||
"The character device does not exist:": "Znakové zariadenie neexistuje:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Príkaz vyžaduje heslo, ktoré nie je ozvene. Po vytvorení užívateľov, webové rozhranie žiada o užívateľské meno a heslo.",
|
||||
"The command does not name an image": "Príkaz nepomenuje obrázok",
|
||||
@@ -6447,6 +6455,7 @@
|
||||
"The host bind source does not exist:": "Zdroj hostiteľskej väzby neexistuje:",
|
||||
"The host bind source is not a regular file or directory:": "Zdroj odkazu hostiteľa nie je bežný súbor alebo adresár:",
|
||||
"The host directory changed before it was mounted": "Adresár hostiteľa sa zmenil predtým, ako bol pripojený",
|
||||
"The host directory does not exist": "Adresár na hostiteľovi neexistuje",
|
||||
"The host directory may not be accessible from an unprivileged container.": "Priečinok na hostovi nemusí byť dostupný z neprivilegovaného kontajnera.",
|
||||
"The host has no IPv4 address on the selected bridge": "Hostiteľ nemá IPv4 adresu na vybranom moste",
|
||||
"The host monitor does not see the real host memory": "Monitor hostiteľa nevidí skutočnú pamäť hostiteľa",
|
||||
@@ -6684,6 +6693,7 @@
|
||||
"The selected VM": "Vybraná VM",
|
||||
"The selected VM is running.": "Vybraná VM beží.",
|
||||
"The selected base folder does not exist and could not be created:": "Vybraný základný priečinok neexistuje a nepodarilo sa ho vytvoriť:",
|
||||
"The selected bridge does not exist": "Vybraný sieťový most neexistuje",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "Vybraný bridge nemá IPv4 podsieť pre firewall monitora hostiteľa",
|
||||
"The selected configuration needs to start the LXC during the installation": "Vybraná konfigurácia musí spustiť LXC počas inštalácie",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Vybraný kontajner je neprivilegovaný. Na priame priradenie zariadenia je potrebný privilegovaný kontajner.",
|
||||
@@ -6706,6 +6716,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "Služba buduje svoj vlastný obraz; môže byť nainštalovaný iba zverejnený obrázok",
|
||||
"The service declares no image:": "Služba neprehlasuje žiadny obrázok:",
|
||||
"The setting has no final value:": "Nastavenie nemá žiadnu konečnú hodnotu:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Nepodarilo sa vrátiť všetky nastavenia. Pred pokračovaním skontrolujte kontajner a uloženú zmenu.",
|
||||
"The share already exists in smb.conf:": "Zdieľanie už existuje v smb.conf:",
|
||||
"The shared destination is not a directory": "Zdieľaná destinácia nie je adresár",
|
||||
"The shared directory points to a protected host path": "Zdieľaný adresár ukazuje na chránenú cestu hostiteľa",
|
||||
@@ -6780,6 +6791,7 @@
|
||||
"These interface configurations will be removed": "Tieto nastavenia rozhraní sa odstránia",
|
||||
"These manual changes cannot be adopted safely:": "Tieto manuálne zmeny nie je možné bezpečne prijať:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Tieto cesty sa neobnovia za behu a rozbalia sa na ručnú obnovu.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Tieto nastavenia vyžadujú úpravu špecifickú pre aplikáciu. Obraz sa nestiahol a kontajner sa nevytvoril znova.",
|
||||
"These values are asked during the installation:": "Tieto hodnoty sa požadujú počas inštalácie:",
|
||||
"They are described in the image documentation:": "Ich popis nájdete v dokumentácii image:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Pred pokračovaním budú pridané do oci-compose. Nepodporované alebo zmenené zdroje sa neimportujú.",
|
||||
@@ -7115,6 +7127,7 @@
|
||||
"Unsupported mount type": "Nepodporovaný typ pripojenia",
|
||||
"Unsupported mount type:": "Nepodporovaný typ pripojenia:",
|
||||
"Unsupported native device type:": "Nepodporovaný natívne typ zariadenia:",
|
||||
"Unsupported network settings": "Nepodporované sieťové nastavenia",
|
||||
"Unsupported operation": "Nepodporovaná operation",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Nepodporovaná alebo neznáma verzia Proxmox; žiadne úložisko sa nezmenilo.",
|
||||
"Unsupported output format:": "Nepodporovaný výstupný formát:",
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
"Applying Controller/NVMe passthrough to VM": "Tillämpar styrenhets-/NVMe-passthrough på VM",
|
||||
"Applying balanced memory optimization settings...": "Tillämpar inställningar för balanserad minnesoptimering...",
|
||||
"Applying changes safely...": "Tillämpar ändringar på ett säkert sätt...",
|
||||
"Applying container settings without rebuilding the container...": "Tillämpar inställningarna utan att återskapa containern...",
|
||||
"Applying custom restore": "Använder anpassad återställning",
|
||||
"Applying default VM configuration": "Tillämpar standard VM-konfiguration",
|
||||
"Applying host permissions for unprivileged LXC bind-mounts...": "Tillämpar värdbehörigheter för oprivilegierade LXC bind-mounts...",
|
||||
@@ -3214,6 +3215,8 @@
|
||||
"Invalid ALLOWED_HOSTS value": "Invalid ALLOWED_HOSTS värde",
|
||||
"Invalid Home Assistant OS check timeout": "Ogiltig Home Assistant OS check timeout",
|
||||
"Invalid ID": "Ogiltigt ID",
|
||||
"Invalid IPv4 address": "Ogiltig IPv4-adress",
|
||||
"Invalid IPv4 gateway": "Ogiltig IPv4-gateway",
|
||||
"Invalid Intel render path": "Invalid Intel render bana",
|
||||
"Invalid Jellyfin path:": "Invalid Jellyfin väg:",
|
||||
"Invalid MAC address:": "Ogiltig MAC-adress:",
|
||||
@@ -3246,6 +3249,7 @@
|
||||
"Invalid administrator email": "Invalid administratör e-post",
|
||||
"Invalid administrator user name": "Invalid administratör användarnamn",
|
||||
"Invalid base VMID": "Ogiltig bas VMID",
|
||||
"Invalid bridge": "Ogiltig brygga",
|
||||
"Invalid check package:": "Ogiltigt checkpaket:",
|
||||
"Invalid configuration path:": "Ogiltig konfigurationsväg:",
|
||||
"Invalid consume/export volumes": "Ogiltiga konsum/exportvolymer",
|
||||
@@ -3309,6 +3313,7 @@
|
||||
"Invalid remote name": "Ogiltigt fjärrnamn",
|
||||
"Invalid remote path": "Invalid fjärrväg",
|
||||
"Invalid repair version:": "Ogiltig reparationsversion:",
|
||||
"Invalid resource value": "Ogiltigt resursvärde",
|
||||
"Invalid resources": "Ogiltiga resurser",
|
||||
"Invalid restored volume path": "Invalid återställd volymväg",
|
||||
"Invalid running state": "Ogiltig körstatus",
|
||||
@@ -3327,6 +3332,7 @@
|
||||
"Invalid stack members": "Ogiltiga stackmedlemmar",
|
||||
"Invalid stack name": "Invalid stack namn",
|
||||
"Invalid stack operation": "Ogiltig stack operation",
|
||||
"Invalid storage": "Ogiltig lagring",
|
||||
"Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Ogiltigt lagrings-ID. Använd endast bokstäver, siffror, bindestreck och understreck.",
|
||||
"Invalid storage name": "Ogiltigt lagringsnamn",
|
||||
"Invalid suite application": "Invalid suite applikation",
|
||||
@@ -4888,6 +4894,7 @@
|
||||
"Proxmox VE configuration completed.": "Proxmox VE-konfigurationen slutförd.",
|
||||
"Proxmox VE safe update completed": "Proxmox VE säker uppdatering slutförd",
|
||||
"Proxmox auth logger service created and started": "Proxmox authlogger-tjänst skapad och startade",
|
||||
"Proxmox could not apply the container settings": "Proxmox kunde inte tillämpa containerns inställningar",
|
||||
"Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Proxmox företagsrepo kan saknas eller är otillgänglig. Försöker byta till ingen prenumeration...",
|
||||
"Proxmox filter configured": "Proxmox-filter konfigurerat",
|
||||
"Proxmox iSCSI Storage Status:": "Proxmox iSCSI-lagringsstatus:",
|
||||
@@ -6327,6 +6334,7 @@
|
||||
"The build could not be checked beforehand; continuing without that check.": "Bygget kunde inte kontrolleras i förväg;fortsätter utan den kontrollen.",
|
||||
"The cached image does not match the current digest": "Den cachade bilden matchar inte den nuvarande smältningen",
|
||||
"The cached image is damaged; it will be downloaded again.": "Den cachade bilden är skadad; den kommer att laddas ner igen.",
|
||||
"The changes need the application installer": "Ändringarna kräver programmets installationsverktyg",
|
||||
"The character device does not exist:": "Karaktärenheten existerar inte:",
|
||||
"The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Kommandot begär ett lösenord som inte echoed. Efter att ha skapat användarna ber webbgränssnittet om ett användarnamn och ett lösenord.",
|
||||
"The command does not name an image": "Kommandot heter inte en bild",
|
||||
@@ -6446,6 +6454,7 @@
|
||||
"The host bind source does not exist:": "Värdbindningskällan finns inte:",
|
||||
"The host bind source is not a regular file or directory:": "Värdbindningskällan är inte en vanlig fil eller katalog:",
|
||||
"The host directory changed before it was mounted": "Värdkatalogen ändrades innan den monterades",
|
||||
"The host directory does not exist": "Katalogen på värden finns inte",
|
||||
"The host directory may not be accessible from an unprivileged container.": "Värdkatalogen kanske inte är tillgänglig från en oprivilegierad behållare.",
|
||||
"The host has no IPv4 address on the selected bridge": "Värden har ingen IPv4-adress på den valda bron",
|
||||
"The host monitor does not see the real host memory": "Värdskärmen ser inte det verkliga värdminnet",
|
||||
@@ -6683,6 +6692,7 @@
|
||||
"The selected VM": "Den valda virtuella datorn",
|
||||
"The selected VM is running.": "Den valda virtuella datorn körs.",
|
||||
"The selected base folder does not exist and could not be created:": "Den valda basmappen finns inte och kunde inte skapas:",
|
||||
"The selected bridge does not exist": "Den valda bryggan finns inte",
|
||||
"The selected bridge has no IPv4 subnet for the host-monitor firewall": "Den valda bryggan har inget IPv4-undernät för värd-monitorns brandvägg",
|
||||
"The selected configuration needs to start the LXC during the installation": "Den valda konfigurationen måste starta LXC under installationen",
|
||||
"The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Den valda behållaren är oprivilegierad. En privilegierad behållare krävs för direkt enhetsgenomföring.",
|
||||
@@ -6705,6 +6715,7 @@
|
||||
"The service builds its own image; only a published image can be installed": "Tjänsten bygger sin egen bild; endast en publicerad bild kan installeras.",
|
||||
"The service declares no image:": "Tjänsten förklarar ingen bild:",
|
||||
"The setting has no final value:": "Inställningen har inget slutvärde:",
|
||||
"The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Det gick inte att återställa alla inställningar. Kontrollera containern och den sparade ändringen innan du fortsätter.",
|
||||
"The share already exists in smb.conf:": "Delningen finns redan i smb.conf:",
|
||||
"The shared destination is not a directory": "Den delade destinationen är inte en katalog",
|
||||
"The shared directory points to a protected host path": "Den delade katalogen pekar på en skyddad värdväg",
|
||||
@@ -6779,6 +6790,7 @@
|
||||
"These interface configurations will be removed": "Dessa gränssnittskonfigurationer kommer att tas bort",
|
||||
"These manual changes cannot be adopted safely:": "Dessa manuella ändringar kan inte användas på ett säkert sätt:",
|
||||
"These paths will not be restored live and will be extracted for manual recovery.": "Dessa sökvägar kommer inte att återställas live och kommer att extraheras för manuell återställning.",
|
||||
"These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Dessa inställningar kräver en anpassning för just det här programmet. Avbildningen hämtades inte och containern återskapades inte.",
|
||||
"These values are asked during the installation:": "Dessa värden tillfrågas under installationen:",
|
||||
"They are described in the image documentation:": "De beskrivs i bilddokumentationen:",
|
||||
"They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "De kommer att läggas till oci-compose innan du fortsätter. Resurser som inte stöds eller ändras importeras inte.",
|
||||
@@ -7114,6 +7126,7 @@
|
||||
"Unsupported mount type": "Unsupported Mount Typ",
|
||||
"Unsupported mount type:": "Unsupported Mount Typ:",
|
||||
"Unsupported native device type:": "Inbyggd enhetstyp som inte stöds:",
|
||||
"Unsupported network settings": "Nätverksinställningarna stöds inte",
|
||||
"Unsupported operation": "Unsupported operation",
|
||||
"Unsupported or unknown Proxmox version; no repository changed.": "Ostödd eller okänd version av Proxmox; inget arkiv har ändrats.",
|
||||
"Unsupported output format:": "Utdataformat som inte stöds:",
|
||||
|
||||
+16
-1
@@ -142,7 +142,7 @@ An application made of several images is one entry of the catalog and one instal
|
||||
| Operation | What changes | What is kept |
|
||||
|---|---|---|
|
||||
| **Update** | The rootfs is replaced by the image the saved tag publishes today | Record, container disks, host directories, network and devices |
|
||||
| **Modify** | An editor opens with the current resources, network, paths and GPU; the container is rebuilt with the changes | The data of container disks and host directories |
|
||||
| **Modify** | Choose a setting, review it, then apply native LXC changes to the existing container; runtime settings may require a restart | Installed image, rootfs, persistent data and unrelated Proxmox settings |
|
||||
| **Recreate** (multi-container) | Every container is rebuilt from the image it was installed with | Data, network and devices |
|
||||
| **Watchdog** | Whether the application is started again when it stops on its own | Everything; only the choice is saved |
|
||||
| **Remove** | The LXC, or every container of the application, its disks and its record | Host directories, with their content |
|
||||
@@ -159,6 +159,21 @@ Settings changed in Proxmox VE after the installation, such as memory, cores or
|
||||
|
||||
A rolling tag is not an unattended update: the digest changes only through an explicit update.
|
||||
|
||||
For standalone applications, CPU and RAM changes apply live. Network settings,
|
||||
environment overrides, directory mounts, additional managed volumes and supported
|
||||
native devices use a controlled stop/start when needed. Modify does not query the
|
||||
registry or download an image for these edits, even if the cached archive has
|
||||
already been cleaned. Removing a managed mount detaches its disk without deleting
|
||||
its data. An optional retained backup does not force recreation.
|
||||
|
||||
The native path checks the saved configuration and installation identity, keeps a
|
||||
private before/after journal, and restores the original settings if applying them
|
||||
or checking the restarted service fails. It refuses to overwrite concurrent edits.
|
||||
Application-specific installer adaptations (such as changing an NVIDIA runtime or
|
||||
post-start configuration) are not silently converted into a full recreation:
|
||||
unsupported same-image changes stop with an explanation before modifying the CT.
|
||||
Image changes and coordinated stack operations retain their separate update flow.
|
||||
|
||||
## From a Compose file or a docker run command to an oci-compose
|
||||
|
||||
The converter reads a Compose file with one service. A `docker run` command and an image reference alone are first written as a Compose file and follow the same path, so the three produce the same kind of oci-compose.
|
||||
|
||||
@@ -453,7 +453,7 @@ def external_changes(record, config, adopt=True):
|
||||
return values
|
||||
|
||||
|
||||
def preflight(record, candidate, config, coordinated=None):
|
||||
def preflight(record, candidate, config, coordinated=None, in_place=False, baseline=True):
|
||||
deployment = record['deployment']
|
||||
desired = candidate['deployment']
|
||||
if coordinated and (record.get('native_stack_intent') or deployment.get('rootfs_adaptation_replay_required')):
|
||||
@@ -462,7 +462,7 @@ def preflight(record, candidate, config, coordinated=None):
|
||||
raise ValueError(translate('Stack members are updated together with their stack'))
|
||||
if instances.identity(config) != record['installation_id']:
|
||||
raise ValueError(translate('The container identity changed; the container is not replaced'))
|
||||
for key, value in external_changes(record, config, adopt=not coordinated).items():
|
||||
for key, value in (external_changes(record, config, adopt=not coordinated) if baseline else {}).items():
|
||||
section, name = ADOPTABLE[key]
|
||||
recorded = deployment.get(section, {}) if section else deployment
|
||||
target = desired.setdefault(section, {}) if section else desired
|
||||
@@ -491,7 +491,8 @@ def preflight(record, candidate, config, coordinated=None):
|
||||
runtime_keys.add('lxc.mount.entry')
|
||||
runtime_settings.check(config, deployment, record['vmid'])
|
||||
coordinated_keys = {'hookscript', *KEPT_AS_IS} if coordinated else set(KEPT_AS_IS)
|
||||
if '[' in config.decode() or keys - BASIC - runtime_keys - coordinated_keys - {k for k in keys if re.fullmatch(r'(mp|dev)[0-9]+', k)}:
|
||||
preserved_keys = {'nameserver', 'searchdomain', 'timezone', 'protection'} if in_place else set()
|
||||
if '[' in config.decode() or keys - BASIC - runtime_keys - coordinated_keys - preserved_keys - {k for k in keys if re.fullmatch(r'(mp|dev)[0-9]+', k) or (in_place and re.fullmatch(r'unused[0-9]+', k))}:
|
||||
raise ValueError(translate('The container has advanced Proxmox settings outside the supported profile'))
|
||||
unprivileged = deployment.get('security', {}).get('unprivileged', True) is not False
|
||||
if (desired.get('security', {}).get('unprivileged', True) is not False) != unprivileged \
|
||||
@@ -532,7 +533,8 @@ def preflight(record, candidate, config, coordinated=None):
|
||||
raise ValueError(translate('Changing the rootfs or its storage requires a separate migration'))
|
||||
actual = mounts(config)
|
||||
gpu_devices.check(config, deployment)
|
||||
gpu_devices.verify_baseline(record['observed'].get('gpu_devices', {}), deployment)
|
||||
if baseline:
|
||||
gpu_devices.verify_baseline(record['observed'].get('gpu_devices', {}), deployment)
|
||||
recorded_files = {m['source']: bool(m.get('read_only')) for m in deployment.get('mounts', []) if is_file_bind(m, files)}
|
||||
if files != recorded_files:
|
||||
raise ValueError(translate('A container mount has a source, backup or permission different from the saved record'))
|
||||
@@ -1273,6 +1275,9 @@ def complete_recovery(root, journal, state):
|
||||
|
||||
def recover(root, journal):
|
||||
state = json.loads(journal.read_text())
|
||||
if state.get('kind') == 'native-modify':
|
||||
import oci_native_modify
|
||||
return oci_native_modify.recover(root, journal)
|
||||
vmid = state['vmid']
|
||||
show = not state.get('coordinated')
|
||||
if show:
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Edit an installed application's LXC configuration, retaining its rootfs/data.
|
||||
|
||||
Runtime changes use a controlled stop/start, not pct destroy/create. Values,
|
||||
including secrets and NUL-separated environment, reach PVE over stdin only.
|
||||
"""
|
||||
import copy
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import oci_instances as instances
|
||||
import oci_instance_transaction as transaction
|
||||
import oci_resource_modify as resources
|
||||
import oci_gpu_devices as devices
|
||||
import oci_host_mounts as host_mounts
|
||||
from oci_installation_state import parse_config, sha
|
||||
from oci_ui import translate, msg_info, msg_ok, msg_warn
|
||||
|
||||
PERL_UPDATE = r'''
|
||||
use strict; use warnings; use JSON::PP;
|
||||
use PVE::RPCEnvironment; use PVE::INotify; use PVE::API2::LXC::Config;
|
||||
PVE::RPCEnvironment->setup_default_cli_env();
|
||||
my $p = decode_json(do { local $/; <STDIN> });
|
||||
$p->{node} = PVE::INotify::nodename();
|
||||
PVE::API2::LXC::Config->update_vm($p);
|
||||
'''
|
||||
|
||||
|
||||
def environment(config):
|
||||
result = {}
|
||||
entries = []
|
||||
for line in config.decode().splitlines():
|
||||
if line.startswith('lxc.environment.runtime: '):
|
||||
entries.append(line.split(': ', 1)[1])
|
||||
elif line.startswith('env: '):
|
||||
# pct config exposes PVE's public env property, whereas older
|
||||
# observations and native files contain repeated runtime lines.
|
||||
entries.extend(line.split(': ', 1)[1].split('\0'))
|
||||
for entry in entries:
|
||||
name, sep, value = entry.partition('=')
|
||||
if not sep or name in result:
|
||||
raise ValueError(translate('Duplicate variable in the contract; review it before editing'))
|
||||
result[name] = value
|
||||
return result
|
||||
|
||||
|
||||
def environment_values(items):
|
||||
result = {}
|
||||
for item in items:
|
||||
name, value = item['name'], str(item['value'])
|
||||
if (not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name) or name in result
|
||||
or any(ord(c) < 32 or ord(c) == 127 for c in value)):
|
||||
raise ValueError(translate('The value contains an unsupported character'))
|
||||
result[name] = value
|
||||
return result
|
||||
|
||||
|
||||
def changes(record, candidate, config):
|
||||
"""Only unchanged recipes and native settings; adapters keep their installer."""
|
||||
old = transaction.with_default_healthcheck(copy.deepcopy(record))
|
||||
candidate = transaction.with_default_healthcheck(copy.deepcopy(candidate))
|
||||
if (old['template'] != candidate['template'] or old['deployment'].get('host_monitor')
|
||||
or any(k in record for k in ('stack', 'stack_member', 'native_stack_intent'))
|
||||
or old['deployment'].get('stack_managed')):
|
||||
return None
|
||||
before, after = old['deployment'], candidate['deployment']
|
||||
changed = {k for k in before.keys() | after.keys() if before.get(k) != after.get(k)}
|
||||
allowed = {'resources', 'network', 'environment', 'devices', 'mounts', 'hardware_profile'}
|
||||
if not changed or changed - allowed:
|
||||
return None
|
||||
resource_keys = {k for k in before.get('resources', {}).keys() | after.get('resources', {}).keys()
|
||||
if before.get('resources', {}).get(k) != after.get('resources', {}).get(k)}
|
||||
if resource_keys - {'cores', 'memory_mb', 'swap_mb', 'cpu_units'}:
|
||||
return None
|
||||
if 'devices' in changed:
|
||||
if any(d.get('kind', 'character-device') not in ('character-device', 'block-device')
|
||||
or d.get('append_host_device_gid_to_environment')
|
||||
for plan in (before, after) for d in plan.get('devices', [])):
|
||||
return None
|
||||
if before.get('device_permissions') or after.get('device_permissions'):
|
||||
return None
|
||||
if 'mounts' in changed:
|
||||
if any(transaction.is_file_bind(m) for plan in (before, after) for m in plan.get('mounts', [])):
|
||||
return None
|
||||
return changed
|
||||
|
||||
|
||||
def plan(record, candidate, config):
|
||||
changed = changes(record, candidate, config)
|
||||
if not changed:
|
||||
return None
|
||||
transaction.preflight(record, candidate, config, in_place=True)
|
||||
old, new = record['deployment'], candidate['deployment']
|
||||
cfg = parse_config(config)
|
||||
values, delete = {}, []
|
||||
if 'resources' in changed:
|
||||
for key, native, minimum in (('cores', 'cpulimit' if 'cpulimit' in cfg and 'cores' not in cfg else 'cores', 1),
|
||||
('memory_mb', 'memory', 128), ('swap_mb', 'swap', 0), ('cpu_units', 'cpuunits', 1)):
|
||||
value = new['resources'].get(key)
|
||||
if old['resources'].get(key) == value:
|
||||
continue
|
||||
if value is None and key == 'cpu_units':
|
||||
if native in cfg: delete.append(native)
|
||||
elif isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
||||
raise ValueError(translate('Invalid resource value'))
|
||||
else:
|
||||
values[native] = str(value)
|
||||
if 'network' in changed:
|
||||
network = new['network']
|
||||
if set(network) - {'bridge', 'ipv4', 'gateway', 'firewall', 'host_managed', 'mac_address'}:
|
||||
raise ValueError(translate('Unsupported network settings'))
|
||||
if not re.fullmatch(r'[A-Za-z0-9_.:-]+', network['bridge']):
|
||||
raise ValueError(translate('Invalid bridge'))
|
||||
if not Path('/sys/class/net', network['bridge'], 'bridge').is_dir():
|
||||
raise ValueError(translate('The selected bridge does not exist'))
|
||||
address = network['ipv4']
|
||||
if address not in ('dhcp', 'manual'):
|
||||
if ipaddress.ip_interface(address).version != 4:
|
||||
raise ValueError(translate('Invalid IPv4 address'))
|
||||
gateway = network.get('gateway')
|
||||
if gateway and ipaddress.ip_address(gateway).version != 4:
|
||||
raise ValueError(translate('Invalid IPv4 gateway'))
|
||||
fields = dict(p.split('=', 1) for p in cfg['net0'].split(',') if '=' in p)
|
||||
fields.update(bridge=network['bridge'], ip=address)
|
||||
if gateway: fields['gw'] = gateway
|
||||
else: fields.pop('gw', None)
|
||||
for key, native in (('firewall', 'firewall'), ('host_managed', 'host-managed')):
|
||||
if old['network'].get(key) != network.get(key):
|
||||
fields[native] = '1' if network.get(key) else '0'
|
||||
if old['network'].get('mac_address') != network.get('mac_address'):
|
||||
raise ValueError(translate('The MAC address of the container cannot be kept'))
|
||||
values['net0'] = ','.join(k+'='+v for k, v in fields.items())
|
||||
if 'environment' in changed:
|
||||
env = environment(config)
|
||||
previous = environment_values(old.get('environment', []))
|
||||
wanted = environment_values(new.get('environment', []))
|
||||
defaults = dict(item.split('=', 1) for item in record['observed']['image'].get('defaults', {}).get('Env', []) if '=' in item)
|
||||
for name in previous.keys() - wanted.keys():
|
||||
if name in defaults: env[name] = defaults[name]
|
||||
else: env.pop(name, None)
|
||||
env.update(wanted)
|
||||
if env: values['env'] = '\0'.join(k+'='+v for k, v in env.items())
|
||||
else: delete.append('env')
|
||||
if 'devices' in changed:
|
||||
inventory = devices.planned(new)
|
||||
existing = {v['path']: k for k, v in ((k, dict(p.split('=', 1) for p in v.split(',')))
|
||||
for k, v in cfg.items() if re.fullmatch(r'dev[0-9]+', k))}
|
||||
used = set(existing.values())
|
||||
for path, key in existing.items():
|
||||
if path not in inventory: delete.append(key)
|
||||
for device in new.get('devices', []):
|
||||
path = device['host_path']
|
||||
key = existing.get(path)
|
||||
if not key:
|
||||
key = next('dev'+str(n) for n in range(256) if 'dev'+str(n) not in used)
|
||||
used.add(key)
|
||||
info = inventory[path]
|
||||
mode = info['mode'] if device.get('mode') == 'preserve-host' else int(device.get('mode', '0660'), 8)
|
||||
values[key] = (f"path={path},mode={mode:04o},uid={int(device.get('uid', 0))},"
|
||||
f"gid={info['gid'] if device['gid_strategy'] == 'host-device-gid' else 0},"
|
||||
f"deny-write={1 if device.get('deny_write') else 0}")
|
||||
if 'mounts' in changed:
|
||||
actual = transaction.mounts(config)
|
||||
used = {m['key'] for m in actual.values()}
|
||||
wanted = {m['container_path']: m for m in new.get('mounts', [])}
|
||||
for target, mount in actual.items():
|
||||
if target not in wanted: delete.append(mount['key'])
|
||||
for target, mount in wanted.items():
|
||||
if target in actual:
|
||||
original = next(m for m in old['mounts'] if m['container_path'] == target)
|
||||
if mount == original: continue
|
||||
fields = dict(p.split('=', 1) for p in actual[target]['value'].split(',')[1:] if '=' in p)
|
||||
fields['ro'] = '1' if mount.get('read_only') else '0'
|
||||
values[actual[target]['key']] = actual[target]['volume']+','+','.join(k+'='+v for k, v in fields.items())
|
||||
continue
|
||||
key = next('mp'+str(n) for n in range(256) if 'mp'+str(n) not in used)
|
||||
used.add(key)
|
||||
source = mount['source']
|
||||
if mount['type'] == 'managed-volume':
|
||||
if not re.fullmatch(r'[A-Za-z0-9_-]+', source):
|
||||
raise ValueError(translate('Invalid storage'))
|
||||
source += ':'+str(mount['size_gb'])
|
||||
else:
|
||||
source = host_mounts.valid_path(source)
|
||||
if not Path(source).is_dir():
|
||||
if not mount.get('create_if_missing'):
|
||||
raise ValueError(translate('The host directory does not exist'))
|
||||
values[key] = (f"{source},mp={target},backup={1 if mount['backup'] else 0},"
|
||||
f"ro={1 if mount.get('read_only') else 0}")
|
||||
return {'values': values, 'delete': delete, 'restart': bool(changed - {'resources'}),
|
||||
'changed': sorted(changed)}
|
||||
|
||||
|
||||
def verify_values(config, operation):
|
||||
"""PVE may canonicalize property strings; compare their actual fields."""
|
||||
cfg = parse_config(config)
|
||||
for key, value in operation['values'].items():
|
||||
if key == 'env':
|
||||
if environment(config) != dict(item.split('=', 1) for item in value.split('\0')):
|
||||
raise ValueError(translate('Proxmox could not apply the container settings'))
|
||||
elif re.fullmatch(r'(mp|dev|net)[0-9]+', key):
|
||||
desired = dict(p.split('=', 1) for p in value.split(',') if '=' in p)
|
||||
actual = dict(p.split('=', 1) for p in cfg.get(key, '').split(',') if '=' in p)
|
||||
defaults = {'uid': '0', 'gid': '0', 'deny-write': '0', 'ro': '0', 'backup': '0', 'mode': '0660', 'firewall': '0'}
|
||||
def matches(k, v):
|
||||
observed = actual.get(k, defaults.get(k))
|
||||
if k == 'mode': return observed is not None and int(observed, 8) == int(v, 8)
|
||||
return observed == v
|
||||
if any(not matches(k, v) for k, v in desired.items()):
|
||||
raise ValueError(translate('Proxmox could not apply the container settings'))
|
||||
elif cfg.get(key) != value:
|
||||
raise ValueError(translate('Proxmox could not apply the container settings'))
|
||||
if any(key in cfg or (key == 'env' and environment(config)) for key in operation['delete']):
|
||||
raise ValueError(translate('Proxmox could not apply the container settings'))
|
||||
|
||||
|
||||
def set_config(vmid, installation_id, expected, values, delete=()):
|
||||
raw = (resources.CONFIG_DIR / f'{vmid}.conf').read_bytes()
|
||||
if instances.command('pct', 'config', str(vmid)) != expected or instances.identity(expected) != installation_id:
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
payload = dict(values, vmid=vmid, digest=hashlib.sha1(raw).hexdigest())
|
||||
if delete: payload['delete'] = ','.join(delete)
|
||||
transaction.log('Applying native settings: '+', '.join(sorted(set(values) | set(delete))))
|
||||
process = subprocess.run(['perl', '-e', PERL_UPDATE], input=json.dumps(payload).encode(),
|
||||
capture_output=True, timeout=120)
|
||||
# API errors may include secret values. Only keep the exit status.
|
||||
if process.returncode:
|
||||
raise RuntimeError(translate('Proxmox could not apply the container settings'))
|
||||
|
||||
|
||||
def untouched(before, after, keys):
|
||||
keys = set(keys)
|
||||
if 'env' in keys: keys.add('lxc.environment.runtime')
|
||||
def keep(config):
|
||||
return b''.join(line for line in config.splitlines(keepends=True)
|
||||
if line.split(b':', 1)[0].decode() not in keys
|
||||
and not re.match(rb'unused[0-9]+:', line))
|
||||
return keep(before) == keep(after)
|
||||
|
||||
|
||||
def restore_settings(root, record, before, now, operation):
|
||||
vmid = record['vmid']
|
||||
keys = set(operation['values']) | set(operation['delete'])
|
||||
if operation['restart']: transaction.stop(vmid)
|
||||
original = parse_config(before)
|
||||
restore = {k: original[k] for k in keys if k in original and k != 'env'}
|
||||
remove = [k for k in keys if k not in original and k != 'env' and k in parse_config(now)]
|
||||
if 'env' in keys:
|
||||
env = environment(before)
|
||||
if env: restore['env'] = '\0'.join(k+'='+v for k, v in env.items())
|
||||
elif environment(now): remove.append('env')
|
||||
if restore or remove:
|
||||
set_config(vmid, record['installation_id'], now, restore, remove)
|
||||
restored_config = instances.command('pct', 'config', str(vmid))
|
||||
verify_values(restored_config, {'values': restore, 'delete': remove})
|
||||
if not untouched(before, restored_config, keys):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
restored = copy.deepcopy(record)
|
||||
obs = record['observed']
|
||||
restored['observed'] = instances.observe(vmid, record['installation_id'],
|
||||
obs['archive_path'], obs['resolved_registry_digest'], obs['image'])
|
||||
if restored['observed']['config_sha256'] != sha(restored_config):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
return restored
|
||||
|
||||
|
||||
def recover(root, journal):
|
||||
"""Recover through the normal Manage interrupted-operation menu."""
|
||||
state = json.loads(journal.read_text())
|
||||
record, operation = state['record'], state['operation_plan']
|
||||
vmid = record['vmid']
|
||||
current = instances.read(root, vmid)
|
||||
if (state['kind'] != 'native-modify' or state['phase'] in ('committed', 'rolled-back')
|
||||
or current.get('pending_transaction') != str(journal)
|
||||
or current.get('transaction_id') != state['id']
|
||||
or current['installation_id'] != record['installation_id']):
|
||||
raise ValueError(translate('The record does not belong to this operation'))
|
||||
before = state['config'].encode()
|
||||
now = instances.command('pct', 'config', str(vmid))
|
||||
keys = set(operation['values']) | set(operation['delete'])
|
||||
if instances.identity(now) != record['installation_id'] or not untouched(before, now, keys):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
if state.get('after_config'):
|
||||
if now not in (before, state['after_config'].encode()):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
elif now != before:
|
||||
# A crash during the PVE API call is recoverable only when the full
|
||||
# planned edit is present. Refuse unknown partial/concurrent changes.
|
||||
verify_values(now, operation)
|
||||
host_mounts.verify_sources(state.get('original_host_sources', {}))
|
||||
tx_record = restore_settings(root, record, before, now, operation) if now != before else copy.deepcopy(record)
|
||||
if state['was_running']:
|
||||
if transaction.run('pct', 'status', str(vmid)).strip() != b'status: running':
|
||||
transaction.run('pct', 'start', str(vmid))
|
||||
transaction.healthcheck(vmid, record['template'])
|
||||
instances.write(instances.location(root, vmid), tx_record)
|
||||
state['phase'] = 'rolled-back'
|
||||
instances.write(journal, state)
|
||||
msg_ok(translate('Previous installation restored'))
|
||||
|
||||
|
||||
def apply(root, record, candidate, config, acknowledge_external_data=False):
|
||||
operation = plan(record, candidate, config)
|
||||
if not operation:
|
||||
raise ValueError(translate('The changes need the application installer'))
|
||||
original_sources, desired_sources = transaction.freeze_host_sources(
|
||||
record, candidate, acknowledge_external_data)
|
||||
vmid = record['vmid']
|
||||
folder = instances.location(root, vmid).parent / 'modifications' / uuid.uuid4().hex
|
||||
running = transaction.run('pct', 'status', str(vmid)).strip() == b'status: running'
|
||||
journal = {'kind': 'native-modify', 'id': folder.name, 'vmid': vmid, 'operation': 'recreate',
|
||||
'record': record, 'config': config.decode(), 'operation_plan': operation,
|
||||
'phase': 'prepared', 'was_running': running, 'original_host_sources': original_sources}
|
||||
instances.write(folder / 'before.json', journal)
|
||||
transaction.open_log(folder)
|
||||
transaction._run['journal'] = folder / 'before.json'
|
||||
pending = copy.deepcopy(record)
|
||||
pending.update(status='updating', transaction_id=folder.name, pending_transaction=str(folder / 'before.json'))
|
||||
instances.write(instances.location(root, vmid), pending)
|
||||
updated_config = None
|
||||
changed = False
|
||||
try:
|
||||
if operation['restart'] and running:
|
||||
transaction.stop(vmid)
|
||||
# Do not accept a concurrent edit merely because the service stopped.
|
||||
if instances.command('pct', 'config', str(vmid)) != config:
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
host_mounts.verify_sources(desired_sources)
|
||||
for mount in candidate['deployment'].get('mounts', []):
|
||||
if mount['type'] == 'host-bind' and mount.get('create_if_missing'):
|
||||
Path(mount['source']).mkdir(parents=True, exist_ok=True)
|
||||
host_mounts.verify_sources(original_sources)
|
||||
# Capture newly-created directories before installing their mounts.
|
||||
pinned = {p: host_mounts.validate_source(p) for p in desired_sources}
|
||||
for path, previous in desired_sources.items():
|
||||
if previous['exists'] and not host_mounts.same_source(previous, pinned[path]):
|
||||
raise ValueError(translate('A shared source does not match its recorded identity'))
|
||||
host_mounts.verify_sources(pinned)
|
||||
msg_info(translate('Applying container settings without rebuilding the container...'))
|
||||
if operation['values'] or operation['delete']:
|
||||
changed = True # API updates may partially succeed before an error.
|
||||
set_config(vmid, record['installation_id'], config, operation['values'], operation['delete'])
|
||||
updated_config = instances.command('pct', 'config', str(vmid))
|
||||
verify_values(updated_config, operation)
|
||||
keys = set(operation['values']) | set(operation['delete'])
|
||||
if not untouched(config, updated_config, keys):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
transaction.preflight(candidate, candidate, updated_config, in_place=True, baseline=False)
|
||||
journal.update(phase='configured', after_config=updated_config.decode())
|
||||
instances.write(folder / 'before.json', journal)
|
||||
if operation['restart'] and running:
|
||||
transaction.run('pct', 'start', str(vmid))
|
||||
transaction.healthcheck(vmid, candidate['template'])
|
||||
observed = record['observed']
|
||||
fresh = instances.observe(vmid, record['installation_id'], observed['archive_path'],
|
||||
observed['resolved_registry_digest'], observed['image'])
|
||||
host_mounts.verify_observation(pinned, fresh)
|
||||
if fresh['config_sha256'] != sha(updated_config):
|
||||
raise ValueError(translate('The instance changed while it was being edited; open Modify again'))
|
||||
result = copy.deepcopy(record)
|
||||
result.update(deployment=copy.deepcopy(candidate['deployment']), observed=fresh)
|
||||
instances.write(instances.location(root, vmid), result)
|
||||
journal['phase'] = 'committed'
|
||||
instances.write(folder / 'before.json', journal)
|
||||
except BaseException:
|
||||
try:
|
||||
now = instances.command('pct', 'config', str(vmid))
|
||||
keys = set(operation['values']) | set(operation['delete'])
|
||||
if changed:
|
||||
if not untouched(config, now, keys) or (updated_config is not None and now != updated_config):
|
||||
raise ValueError('Concurrent edit; rollback refused')
|
||||
restored = restore_settings(root, record, config, now, operation)
|
||||
else:
|
||||
restored = copy.deepcopy(record)
|
||||
if operation['restart'] and running:
|
||||
if transaction.run('pct', 'status', str(vmid)).strip() != b'status: running':
|
||||
transaction.run('pct', 'start', str(vmid))
|
||||
transaction.healthcheck(vmid, record['template'])
|
||||
instances.write(instances.location(root, vmid), restored)
|
||||
journal['phase'] = 'rolled-back'
|
||||
instances.write(folder / 'before.json', journal)
|
||||
except Exception as error:
|
||||
transaction.log('native rollback failed: '+type(error).__name__)
|
||||
msg_warn(translate('The settings could not be fully rolled back. Review the container and its saved modification before continuing.'))
|
||||
raise
|
||||
try:
|
||||
import oci_carried_record
|
||||
oci_carried_record.carry(root, vmid, mount_stopped=False)
|
||||
except (OSError, ValueError, RuntimeError):
|
||||
transaction.log('carried record will be refreshed on the next management entry')
|
||||
msg_ok(translate('Changes applied. Data kept.'))
|
||||
return folder
|
||||
@@ -180,6 +180,16 @@ def application_name(record, vmid):
|
||||
return f'CT {vmid}'
|
||||
|
||||
|
||||
def backup_native_settings(vmid, storage):
|
||||
"""An optional retained backup does not require rebuilding the guest."""
|
||||
if storage:
|
||||
import oci_keep_backup
|
||||
oci_keep_backup.validate(storage)
|
||||
transaction.run('vzdump', str(vmid), '--storage', storage, '--mode', 'snapshot',
|
||||
'--compress', 'zstd', '--notes-template',
|
||||
'ProxMenux OCI: before modifying container settings')
|
||||
|
||||
|
||||
def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=None):
|
||||
operation = 'recreate' if proposal is not None else 'update'
|
||||
msg_info(translate('Checking the container before the update...') if operation == 'update'
|
||||
@@ -192,7 +202,7 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non
|
||||
desired = transaction.candidate_contract(record, operation, proposal)
|
||||
changes = transaction.external_changes(record, config)
|
||||
resource_changes = None
|
||||
if operation == 'recreate' and not keep_backup and not changes:
|
||||
if operation == 'recreate' and not changes:
|
||||
import oci_resource_modify
|
||||
resource_changes = oci_resource_modify.changes(record, desired, config)
|
||||
# CPU/RAM edits retain the installed image and all unrelated native
|
||||
@@ -202,7 +212,22 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non
|
||||
msg_ok(translate('Container checked'))
|
||||
import oci_operation_notice
|
||||
with oci_operation_notice.operation([vmid], 'modify', application_name(record, vmid)):
|
||||
backup_native_settings(vmid, keep_backup)
|
||||
return oci_resource_modify.apply(instances.ROOT, record, desired, config, resource_changes)
|
||||
if operation == 'recreate' and not changes:
|
||||
import oci_native_modify
|
||||
if oci_native_modify.changes(record, desired, config):
|
||||
import oci_operation_notice
|
||||
with oci_operation_notice.operation([vmid], 'modify', application_name(record, vmid)):
|
||||
# Validate the native plan before an optional full backup.
|
||||
oci_native_modify.plan(record, desired, config)
|
||||
backup_native_settings(vmid, keep_backup)
|
||||
return oci_native_modify.apply(instances.ROOT, record, desired, config,
|
||||
acknowledge_external_data=acknowledge_external_data)
|
||||
if (operation == 'recreate'
|
||||
and record['template']['container_contract']['image']['reference']
|
||||
== desired['template']['container_contract']['image']['reference']):
|
||||
raise ValueError(translate('These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.'))
|
||||
transaction.preflight(record, desired, config)
|
||||
msg_ok(translate('Container checked'))
|
||||
current = record['observed']['image'] if operation == 'update' else None
|
||||
|
||||
@@ -1588,7 +1588,7 @@ def _run_remote_install(
|
||||
shutil.copy2(project_root / 'remote' / 'haos_healthcheck.py', temporary / 'haos_healthcheck.py')
|
||||
shutil.copy2(project_root / 'remote' / 'oci_installation_state.py', temporary / 'oci_installation_state.py')
|
||||
shutil.copy2(project_root / 'remote' / 'oci_instances.py', temporary / 'oci_instances.py')
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_resource_modify.py', 'oci_native_modify.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
shutil.copy2(project_root / 'remote' / helper, temporary / helper)
|
||||
if deployment_kind == 'generic-multi-lxc-stack':
|
||||
shutil.copy2(project_root / 'remote' / 'install_generic_stack.py', temporary / 'install_generic_stack.py')
|
||||
@@ -1625,7 +1625,7 @@ def _run_remote_install(
|
||||
archive.add(project_root / 'remote' / 'haos_healthcheck.py', arcname='haos_healthcheck.py')
|
||||
archive.add(project_root / 'remote' / 'oci_installation_state.py', arcname='oci_installation_state.py')
|
||||
archive.add(project_root / 'remote' / 'oci_instances.py', arcname='oci_instances.py')
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_resource_modify.py', 'oci_native_modify.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
archive.add(project_root / 'remote' / helper, arcname=helper)
|
||||
if deployment_kind == 'generic-multi-lxc-stack':
|
||||
archive.add(project_root / 'remote' / 'install_generic_stack.py', arcname='install_generic_stack.py')
|
||||
|
||||
@@ -287,6 +287,7 @@ class UpdateRoutingTests(unittest.TestCase):
|
||||
candidate['deployment']['resources']['memory_mb'] = 768
|
||||
if not resource_edit:
|
||||
candidate['deployment']['environment'][0]['value'] = 'changed'
|
||||
candidate['template']['container_contract']['image']['reference'] = 'example/app:other'
|
||||
proposal = {'operation': 'recreate', 'candidate': candidate}
|
||||
import contextlib, oci_operation_notice
|
||||
with patch.object(instances, 'locked', return_value=contextlib.nullcontext()), \
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Native runtime edits keep rootfs, defaults and secrets; failures roll back."""
|
||||
import contextlib
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'remote'))
|
||||
import oci_native_modify as native
|
||||
import oci_instances as instances
|
||||
import oci_update_current as update
|
||||
import oci_operation_notice
|
||||
from test_modify_image_reuse import record, CONFIG
|
||||
|
||||
|
||||
class NativePlanTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.record = record()
|
||||
self.record['observed']['image']['defaults'] = {'Env': ['A=image-default']}
|
||||
self.record['deployment']['environment'] = [{'name': 'A', 'value': 'one'}]
|
||||
self.config = CONFIG+b'net0: name=eth0,bridge=vmbr0,ip=dhcp,hwaddr=AA:BB:CC:DD:EE:FF,tag=20,ip6=auto\n'
|
||||
self.candidate = copy.deepcopy(self.record)
|
||||
p = patch.object(native.transaction, 'preflight')
|
||||
self.preflight = p.start(); self.addCleanup(p.stop)
|
||||
|
||||
def test_environment_preserves_image_defaults_and_other_runtime_values(self):
|
||||
self.candidate['deployment']['environment'] = [{'name': 'SECRET', 'value': "a' $x \" ; = value", 'sensitive': True}]
|
||||
result = native.plan(self.record, self.candidate, self.config)
|
||||
env = dict(x.split('=', 1) for x in result['values']['env'].split('\0'))
|
||||
self.assertEqual(env, {'A': 'image-default', 'B': 'two', 'SECRET': "a' $x \" ; = value"})
|
||||
self.assertTrue(result['restart'])
|
||||
|
||||
def test_remove_environment_without_default_restores_image_environment(self):
|
||||
self.record['observed']['image']['defaults'] = {}
|
||||
self.candidate['deployment']['environment'] = []
|
||||
self.assertEqual(native.plan(self.record, self.candidate, self.config)['values']['env'], 'B=two')
|
||||
|
||||
def test_public_pct_environment_and_native_lines_have_the_same_values(self):
|
||||
raw=b'lxc.environment.runtime: A=one\nlxc.environment.runtime: B=two\n'
|
||||
public=b'env: A=one\0B=two\n'
|
||||
self.assertEqual(native.environment(raw),native.environment(public))
|
||||
self.assertEqual(native.environment(public),{'A':'one','B':'two'})
|
||||
|
||||
def test_duplicate_invalid_and_control_character_values_are_refused(self):
|
||||
for items in ([{'name':'X','value':'a'},{'name':'X','value':'b'}],
|
||||
[{'name':'X','value':'a\nb'}],[{'name':'BAD NAME','value':'a'}]):
|
||||
self.candidate['deployment']['environment'] = items
|
||||
with self.assertRaises(ValueError): native.plan(self.record, self.candidate, self.config)
|
||||
|
||||
def test_network_preserves_mac_vlan_ipv6_and_interface_identity(self):
|
||||
self.candidate['deployment']['network'].update(ipv4='192.0.2.2/24', gateway='192.0.2.1')
|
||||
with patch.object(Path, 'is_dir', return_value=True):
|
||||
result = native.plan(self.record, self.candidate, self.config)
|
||||
fields = dict(x.split('=',1) for x in result['values']['net0'].split(','))
|
||||
self.assertEqual(fields['hwaddr'], 'AA:BB:CC:DD:EE:FF')
|
||||
self.assertEqual(fields['tag'], '20'); self.assertEqual(fields['ip6'], 'auto')
|
||||
self.assertEqual(fields['ip'], '192.0.2.2/24'); self.assertEqual(fields['gw'], '192.0.2.1')
|
||||
|
||||
def test_invalid_network_is_rejected_before_stopping(self):
|
||||
for value in ('nonsense', '2001:db8::1/64'):
|
||||
self.candidate['deployment']['network']['ipv4'] = value
|
||||
with patch.object(Path, 'is_dir', return_value=True), self.assertRaises(ValueError):
|
||||
native.plan(self.record,self.candidate,self.config)
|
||||
|
||||
def test_new_managed_disk_uses_native_allocation_and_existing_disk_is_untouched(self):
|
||||
self.candidate['deployment']['mounts'] = [{'type':'managed-volume','container_path':'/extra',
|
||||
'source':'local-zfs','size_gb':1,'backup':True,'read_only':False}]
|
||||
result = native.plan(self.record,self.candidate,self.config)
|
||||
self.assertEqual(result['values'], {'mp0':'local-zfs:1,mp=/extra,backup=1,ro=0'})
|
||||
self.assertNotIn('rootfs', result['values'])
|
||||
|
||||
def test_removing_mount_detaches_it_without_freeing_its_volume(self):
|
||||
mount={'type':'managed-volume','container_path':'/extra','source':'local-zfs','size_gb':1,'backup':True}
|
||||
self.record['deployment']['mounts']=[mount]
|
||||
result=native.plan(self.record,self.candidate,self.config+b'mp4: local-zfs:subvol-115-disk-1,mp=/extra,backup=1,size=1G\n')
|
||||
self.assertEqual(result['delete'],['mp4'])
|
||||
|
||||
def test_image_or_installer_adapter_changes_do_not_take_native_path(self):
|
||||
for key in ('image', 'adapter'):
|
||||
candidate=copy.deepcopy(self.record)
|
||||
if key=='image':candidate['template']['container_contract']['image']['reference']='new/image'
|
||||
else:candidate['deployment']['post_start_configurations']=[{'id':'rootfs-adapter'}]
|
||||
self.assertIsNone(native.changes(self.record,candidate,self.config))
|
||||
|
||||
def test_secrets_use_stdin_never_arguments_or_logs(self):
|
||||
secret="super-secret with 'quotes' and $shell"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory);(root/'115.conf').write_bytes(self.config)
|
||||
with patch.object(native.resources,'CONFIG_DIR',root), \
|
||||
patch.object(instances,'command',return_value=self.config), \
|
||||
patch.object(native.transaction,'log') as log, \
|
||||
patch.object(native.subprocess,'run',return_value=Mock(returncode=0)) as run:
|
||||
native.set_config(115,self.record['installation_id'],self.config,{'env':'SECRET='+secret+'\0B=two'})
|
||||
self.assertNotIn(secret,str(run.call_args.args))
|
||||
self.assertNotIn(secret,str(log.call_args_list))
|
||||
self.assertIn(secret,json.loads(run.call_args.kwargs['input'])['env'])
|
||||
|
||||
def test_native_api_failure_does_not_echo_secret_stderr(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory);(root/'115.conf').write_bytes(self.config)
|
||||
with patch.object(native.resources,'CONFIG_DIR',root),patch.object(instances,'command',return_value=self.config), \
|
||||
patch.object(native.subprocess,'run',return_value=Mock(returncode=1,stderr=b'secret')):
|
||||
with self.assertRaisesRegex(RuntimeError,'Proxmox could not apply') as error:
|
||||
native.set_config(115,self.record['installation_id'],self.config,{'env':'SECRET=secret'})
|
||||
self.assertNotIn('secret',str(error.exception))
|
||||
|
||||
def test_verifies_all_environment_entries_and_native_values(self):
|
||||
native.verify_values(CONFIG,{'values':{'env':'A=one\0B=two','cores':'2'},'delete':[]})
|
||||
for values in ({'env':'A=wrong\0B=two'},{'cores':'3'}):
|
||||
with self.assertRaises(ValueError):native.verify_values(CONFIG,{'values':values,'delete':[]})
|
||||
with self.assertRaises(ValueError):native.verify_values(CONFIG,{'values':{},'delete':['env']})
|
||||
|
||||
def test_native_property_canonicalization_keeps_modes_and_default_flags(self):
|
||||
cfg=CONFIG+b'dev0: path=/dev/fuse,mode=660\n'
|
||||
native.verify_values(cfg,{'values':{'dev0':'path=/dev/fuse,mode=0660,uid=0,gid=0,deny-write=0'},'delete':[]})
|
||||
|
||||
|
||||
class NativeApplyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
|
||||
self.root=Path(self.temp.name)
|
||||
self.record=record();self.candidate=copy.deepcopy(self.record)
|
||||
self.candidate['deployment']['environment']=[{'name':'A','value':'changed'}]
|
||||
self.config=CONFIG
|
||||
self.state={'config':CONFIG,'running':True}
|
||||
self.commands=[]
|
||||
def run(*args):
|
||||
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
|
||||
return b''
|
||||
def stop(vmid):self.state['running']=False
|
||||
def setter(vmid,identity,expected,values,delete=()):
|
||||
self.assertEqual(expected,self.state['config'])
|
||||
text=self.state['config'].decode()
|
||||
if 'env' in values or 'env' in delete:
|
||||
public='env: ' in text
|
||||
text=''.join(x for x in text.splitlines(keepends=True) if not x.startswith(('lxc.environment.runtime: ','env: ')))
|
||||
if public and values.get('env'):text+='env: '+values['env']+'\n'
|
||||
else:
|
||||
for item in values.get('env','').split('\0'):
|
||||
if item:text+='lxc.environment.runtime: '+item+'\n'
|
||||
self.state['config']=text.encode()
|
||||
def observe(*args):
|
||||
observed=copy.deepcopy(self.record['observed'])
|
||||
observed.update(config=self.state['config'].decode(),config_sha256=native.sha(self.state['config']))
|
||||
return observed
|
||||
patches=[patch.object(native,'plan',return_value={'values':{'env':'A=changed\0B=two'},'delete':[],'restart':True,'changed':['environment']}),
|
||||
patch.object(native,'set_config',side_effect=setter),patch.object(instances,'command',side_effect=lambda *a:self.state['config']),
|
||||
patch.object(native.transaction,'run',side_effect=run),patch.object(native.transaction,'stop',side_effect=stop),
|
||||
patch.object(native.transaction,'healthcheck'),patch.object(native.transaction,'preflight'),
|
||||
patch.object(native.transaction,'freeze_host_sources',return_value=({},{})),
|
||||
patch.object(native.transaction,'open_log'),patch.object(native.transaction,'log'),
|
||||
patch.object(instances,'observe',side_effect=observe),patch('oci_carried_record.carry')]
|
||||
for p in patches:p.start();self.addCleanup(p.stop)
|
||||
instances.write(instances.location(self.root,115),self.record)
|
||||
|
||||
def apply(self):return native.apply(self.root,self.record,self.candidate,self.config)
|
||||
|
||||
def test_runtime_edit_restarts_existing_guest_and_keeps_rootfs_image_and_data(self):
|
||||
folder=self.apply()
|
||||
saved=instances.read(self.root,115)
|
||||
self.assertEqual(saved['observed']['image'],self.record['observed']['image'])
|
||||
self.assertIn(b'rootfs: local-zfs:subvol-115-disk-0',self.state['config'])
|
||||
self.assertEqual(saved['deployment'],self.candidate['deployment'])
|
||||
self.assertTrue(self.state['running'])
|
||||
self.assertEqual(json.loads((folder/'before.json').read_text())['phase'],'committed')
|
||||
self.assertFalse(any(c[:2] in (('pct','create'),('pct','destroy')) for c in self.commands))
|
||||
|
||||
def test_failed_healthcheck_rolls_back_and_starts_original_service(self):
|
||||
with patch.object(native.transaction,'healthcheck',side_effect=[RuntimeError('not healthy'),None]):
|
||||
with self.assertRaisesRegex(RuntimeError,'not healthy'):self.apply()
|
||||
self.assertEqual(self.state['config'],CONFIG)
|
||||
self.assertTrue(self.state['running'])
|
||||
self.assertEqual(instances.read(self.root,115)['deployment'],self.record['deployment'])
|
||||
|
||||
def test_health_failure_restores_public_nul_separated_environment(self):
|
||||
self.config=CONFIG.replace(b'lxc.environment.runtime: A=one\nlxc.environment.runtime: B=two\n',b'env: A=one\0B=two\n')
|
||||
self.state['config']=self.config
|
||||
self.record['observed'].update(config=self.config.decode(),config_sha256=native.sha(self.config))
|
||||
instances.write(instances.location(self.root,115),self.record)
|
||||
with patch.object(native.transaction,'healthcheck',side_effect=[RuntimeError('not healthy'),None]):
|
||||
with self.assertRaises(RuntimeError):self.apply()
|
||||
self.assertEqual(native.environment(self.state['config']),{'A':'one','B':'two'})
|
||||
self.assertTrue(self.state['running'])
|
||||
|
||||
def test_stopped_guest_stays_stopped(self):
|
||||
self.state['running']=False
|
||||
self.apply()
|
||||
self.assertFalse(self.state['running'])
|
||||
self.assertNotIn(('pct','start','115'),self.commands)
|
||||
|
||||
def test_interrupted_edit_is_recovered_through_transaction_dispatch(self):
|
||||
folder=self.apply()
|
||||
path=folder/'before.json';state=json.loads(path.read_text());state['phase']='configured'
|
||||
instances.write(path,state)
|
||||
pending=copy.deepcopy(self.record)
|
||||
pending.update(status='updating',transaction_id=folder.name,pending_transaction=str(path))
|
||||
instances.write(instances.location(self.root,115),pending)
|
||||
native.transaction.recover(self.root,path)
|
||||
self.assertEqual(self.state['config'],CONFIG)
|
||||
saved=instances.read(self.root,115)
|
||||
self.assertEqual(saved['status'],'installed');self.assertNotIn('pending_transaction',saved)
|
||||
self.assertEqual(json.loads(path.read_text())['phase'],'rolled-back')
|
||||
|
||||
def test_recovery_refuses_a_concurrent_environment_edit(self):
|
||||
folder=self.apply();path=folder/'before.json';state=json.loads(path.read_text());state['phase']='configured'
|
||||
instances.write(path,state)
|
||||
pending=copy.deepcopy(self.record)
|
||||
pending.update(status='updating',transaction_id=folder.name,pending_transaction=str(path))
|
||||
instances.write(instances.location(self.root,115),pending)
|
||||
self.state['config']=self.state['config'].replace(b'A=changed',b'A=concurrent')
|
||||
with self.assertRaisesRegex(ValueError,'changed'):native.transaction.recover(self.root,path)
|
||||
self.assertIn(b'A=concurrent',self.state['config'])
|
||||
|
||||
def test_concurrent_change_before_write_is_not_overwritten(self):
|
||||
def stop(vmid):
|
||||
self.state['running']=False
|
||||
self.state['config']+=b'timezone: host\n'
|
||||
with patch.object(native.transaction,'stop',side_effect=stop):
|
||||
with self.assertRaisesRegex(ValueError,'changed'):self.apply()
|
||||
self.assertIn(b'timezone: host',self.state['config'])
|
||||
self.assertTrue(self.state['running'])
|
||||
|
||||
def test_router_does_not_resolve_download_or_recreate_for_environment_edit(self):
|
||||
with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \
|
||||
patch.object(native.transaction,'external_changes',return_value={}), \
|
||||
patch.object(update,'resolve_archive',side_effect=AssertionError('registry path')), \
|
||||
patch.object(native.transaction,'apply',side_effect=AssertionError('recreation')), \
|
||||
patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()):
|
||||
update.update(115,proposal={'operation':'recreate','candidate':self.candidate})
|
||||
self.assertEqual(instances.read(self.root,115)['deployment']['environment'],self.candidate['deployment']['environment'])
|
||||
|
||||
def test_unsupported_same_image_adaptation_never_silently_recreates(self):
|
||||
self.candidate['deployment']['post_start_configurations']=[{'id':'unsupported-adapter'}]
|
||||
with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \
|
||||
patch.object(native.transaction,'external_changes',return_value={}), \
|
||||
patch.object(update,'resolve_archive',side_effect=AssertionError('registry path')), \
|
||||
patch.object(native.transaction,'apply',side_effect=AssertionError('recreation')):
|
||||
with self.assertRaisesRegex(ValueError,'application-specific adaptation'):
|
||||
update.update(115,proposal={'operation':'recreate','candidate':self.candidate})
|
||||
|
||||
def test_optional_backup_does_not_force_recreation(self):
|
||||
with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \
|
||||
patch.object(native.transaction,'external_changes',return_value={}), \
|
||||
patch.object(update,'resolve_archive',side_effect=AssertionError('registry path')), \
|
||||
patch.object(update,'backup_native_settings') as backup, \
|
||||
patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()):
|
||||
update.update(115,proposal={'operation':'recreate','candidate':self.candidate},keep_backup='test-storage')
|
||||
backup.assert_called_once_with(115,'test-storage')
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user