diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx index df70ff34..bdbd8b20 100644 --- a/AppImage/components/lxc-app-panel.tsx +++ b/AppImage/components/lxc-app-panel.tsx @@ -1773,7 +1773,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop {t("vmLxc.appEditor.binaryVersionHint")} {t("vmLxc.appEditor.methodFile")} {t("vmLxc.appEditor.methodPython")} - {t("vmLxc.appEditor.methodDockerLabel")} + {/* Chosen by detection for a Docker workload; kept only so an + application already using it shows its method. */} + {method === "docker_label" && ( + {t("vmLxc.appEditor.methodDockerLabel")} + )} {t("vmLxc.appEditor.methodDockerExec")} {t("vmLxc.appEditor.methodCommand")} {t("vmLxc.appEditor.methodManual")} diff --git a/AppImage/components/script-terminal-modal.tsx b/AppImage/components/script-terminal-modal.tsx index b4a7f308..605ed58a 100644 --- a/AppImage/components/script-terminal-modal.tsx +++ b/AppImage/components/script-terminal-modal.tsx @@ -649,7 +649,7 @@ const initMessage = { return } - if (value === "cancel") { + if (value === "cancel" || value === "") { setCurrentInteraction(null) setInteractionInput("") handleCloseModal() @@ -755,7 +755,7 @@ const initMessage = { return ( <> - { if (!open) onClose() }}> + { + if (!ociInstance?.pending || !selectedVM) return + const vmid = selectedVM.vmid + const timer = setInterval(() => fetchOciInstance(vmid), 15000) + return () => clearInterval(timer) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ociInstance?.pending, selectedVM?.vmid]) + const fetchMountPoints = async (vmid: number) => { // Two fetches in parallel: // 1) STATIC — configured mp entries + PVE classification. Backed diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json index e322b164..137626f0 100644 --- a/AppImage/messages/es/common.json +++ b/AppImage/messages/es/common.json @@ -98,7 +98,7 @@ "used": "Usado", "cached": "En caché", "total": "Total", - "free": "Gratis", + "free": "Libre", "activeVmLxc": "VM y LXC activos", "runningCount": "{count} en ejecución", "vmsCount": "{count} VM", @@ -176,7 +176,7 @@ "zfsPools": "Grupos ZFS", "local": "Local", "remote": "Remoto", - "free": "Gratis", + "free": "Libre", "used": "Usado", "total": "Total", "available": "Disponible", @@ -1133,7 +1133,7 @@ "description": "Configure las opciones de copia de seguridad para este {type}", "storage": "Almacenamiento", "selectStorage": "Seleccionar almacenamiento", - "free": "gratis", + "free": "libre", "mode": "Modo", "notification": "Notificación", "useGlobalSettings": "Usar configuración global", @@ -1504,14 +1504,14 @@ "optionalSuffix": "(opcional)", "methodNone": "Ninguno (solo enlace)", "methodDpkg": "Paquete dpkg (Debian/Ubuntu)", - "methodApk": "paquete apk (alpino)", + "methodApk": "Paquete apk (Alpine)", "methodBinary": "Binario (ruta absoluta o nombre simple)", - "methodFile": "archivo + expresión regular", + "methodFile": "Archivo + expresión regular", "methodDockerLabel": "docker inspect (etiqueta OCI)", "methodDockerExec": "docker exec (binario en contenedor)", - "methodPython": "distribución de Python (importlib.metadata)", - "methodCommand": "comando (avanzado - argv)", - "methodManual": "manual (escribo la versión)", + "methodPython": "Distribución de Python (importlib.metadata)", + "methodCommand": "Comando (avanzado: argv)", + "methodManual": "Manual (escribo la versión)", "packageIdentifierLabel": "Identificador de paquete", "binaryPathLabel": "Ruta del binario", "binaryPathPlaceholder": "por ejemplo, /opt//binary", @@ -1589,7 +1589,7 @@ "detectMethodDockerLabel": "etiqueta acoplable", "detectMethodDockerExec": "ejecutivo de ventana acoplable", "detectMethodCommand": "dominio", - "binaryVersionHint": "binario --versión", + "binaryVersionHint": "Binario --version", "systemctlHint": "systemctl show -p ExecStart", "whichHint": "cual ", "apkInfoHint": "\" : \"información de la aplicación | grep", @@ -3684,7 +3684,7 @@ "memory": "Memoria", "total": "Total", "used": "Usado", - "free": "Gratis", + "free": "Libre", "memoryUtilization": "Utilización de la memoria", "connection": "Conexión", "formFactor": "Factor de forma", @@ -4148,7 +4148,7 @@ "expressionLabel": "Expresión", "filesystemShort": "fs", "fingerprintOptional": "Huella digital (opcional)", - "free": "Gratis", + "free": "Libre", "groupNameInPbs": "Nombre del grupo en PBS", "jobId": "ID de tarea", "jobIdLabel": "ID de tarea", diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 32ef8e5d..efba26fe 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -2015,6 +2015,12 @@ def _refresh_started_guest(vmid: int, vm_type: str) -> None: lxc_apps.invalidate_docker_inventory(vmid) lxc_apps.get_docker_inventory(vmid, force=True) + if vm_type == 'lxc': + try: + import lxc_apps + lxc_apps.ensure_oci_registration(vmid) + except Exception as exc: + print(f'[ProxMenux] lifecycle refresh lxc {vmid}: OCI registration: {exc}', flush=True) handlers = [ (f'/api/vms/{vmid}', get_vm_config), (f'/api/vms/{vmid}/backups', api_vm_backups), @@ -12318,6 +12324,7 @@ def _vm_modal_prewarmer_pass(): ] if vm_type == 'lxc': import lxc_apps + lxc_apps.ensure_oci_registration(vmid) lxc_apps.load_sidecar(vmid) endpoints.extend([ (_vm_schedule_cache, _VM_SCHEDULE_TTL, api_vm_apps_schedule, @@ -13552,6 +13559,7 @@ def api_lxc_updates_detection_set(): def api_vm_apps_get(vmid): try: import lxc_apps + lxc_apps.ensure_oci_registration(vmid) sidecar = lxc_apps.load_sidecar(vmid) payload = sidecar if sidecar else {'vmid': vmid, 'apps': []} lxc_apps.annotate_delegated_apps(payload.get('apps') or [], _get_lxc_docker_inventory_map().get(str(vmid))) diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index 002b12bf..41c49721 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -647,9 +647,19 @@ def script_websocket(ws, session_id): break continue + # Keystrokes arrive as raw text. A lone number or word such as + # "4" or "true" is valid JSON too, so only an object is a + # control message. try: msg = json.loads(data) - + except (json.JSONDecodeError, TypeError): + msg = None + if not isinstance(msg, dict): + try: + os.write(master_fd, data.encode('utf-8')) + except OSError: + break + else: if msg.get('type') == 'interaction_response': interaction_id = msg.get('id') value = msg.get('value') @@ -677,13 +687,6 @@ def script_websocket(ws, session_id): rows = int(msg.get('rows', 30)) set_winsize(master_fd, rows, cols) continue - - except json.JSONDecodeError: - # Raw text input, send to script - try: - os.write(master_fd, data.encode('utf-8')) - except OSError as e: - break if script_process.poll() is not None: # The output worker owns the final PTY drain and emits both diff --git a/AppImage/scripts/lxc_apps.py b/AppImage/scripts/lxc_apps.py index 124b8fcb..849a68a1 100644 --- a/AppImage/scripts/lxc_apps.py +++ b/AppImage/scripts/lxc_apps.py @@ -1358,6 +1358,8 @@ def detect_installed_version(vmid, config: dict) -> tuple[Optional[str], Optiona if method == "oci_image": result = _oci_image_versions(vmid, with_latest=False) + if result.get("busy"): + return None, "an OCI operation on this container is running" if result.get("error"): return None, result["error"] # An image that states no application version is still an image with @@ -3372,7 +3374,7 @@ def _find_app(sidecar: dict, app_id: str) -> Optional[dict]: return None -def add_app(vmid, payload: dict) -> tuple[bool, Any]: +def add_app(vmid, payload: dict, notify: bool = True) -> tuple[bool, Any]: ok, cfg = validate_config(payload) if not ok: return False, cfg @@ -3393,7 +3395,7 @@ def add_app(vmid, payload: dict) -> tuple[bool, Any]: if not _write_sidecar(vmid, sidecar): return False, "could not persist sidecar (permission?)" # Kick a first check so the UI shows real numbers immediately - check_app(vmid, new_id, force=True) + check_app(vmid, new_id, force=True, notify=notify) return True, _read_sidecar(vmid) @@ -3461,6 +3463,9 @@ def delete_app(vmid, app_id: str) -> bool: sidecar = _read_sidecar(vmid) if not sidecar: return True + if any(a.get("id") == app_id and a.get("installed_via") == "oci_image" + for a in sidecar.get("apps") or []): + _dismiss_oci_registration(vmid) before = len(sidecar.get("apps") or []) sidecar["apps"] = [a for a in sidecar.get("apps") or [] if a.get("id") != app_id] sidecar["updated_at"] = _now_iso() @@ -3478,6 +3483,9 @@ def delete_app(vmid, app_id: str) -> bool: def delete_all(vmid) -> bool: + sidecar = _read_sidecar(vmid) or {} + if any(a.get("installed_via") == "oci_image" for a in sidecar.get("apps") or []): + _dismiss_oci_registration(vmid) try: os.unlink(_sidecar_path(vmid)) return True @@ -4325,6 +4333,9 @@ def check_app( if app.get("installed_via") == "oci_image": result = _oci_image_versions(vmid, known=state) + if result.get("busy"): + _recheck_after_oci_operation(vmid, app_id) + return sidecar app["state"] = { "installed_version": result.get("installed_version"), "latest_version": result.get("latest_version"), @@ -4341,7 +4352,9 @@ def check_app( } sidecar["updated_at"] = _now_iso() _write_sidecar(vmid, sidecar) - if notify and app["state"]["update_available"] and app["state"]["latest_version"]: + # The payload names an image by its build date and digest when it + # states no version, so it decides whether there is anything to send. + if notify and app["state"]["update_available"]: _fire_update_notification(vmid, app) return sidecar @@ -5359,6 +5372,124 @@ def _oci_name_from_image(reference: str | None) -> str: return " ".join(word.capitalize() for word in re.split(r"[-_.]+", basename) if word) +# Registrations of OCI applications the user removed, by VMID and the +# installation they belonged to, so they are not registered again. +_OCI_DISMISSED_FILE = f"{_APPS_DIR}/.oci-dismissed.json" + + +def _read_oci_record(vmid) -> Optional[dict]: + try: + with open(f"{_OCI_INSTANCE_ROOT}/{int(vmid)}/oci-compose.json", encoding="utf-8") as handle: + record = json.load(handle) + except (OSError, ValueError, TypeError): + return None + return record if isinstance(record, dict) else None + + +def _oci_dismissed() -> dict: + try: + with open(_OCI_DISMISSED_FILE, encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, ValueError): + return {} + return data if isinstance(data, dict) else {} + + +def _dismiss_oci_registration(vmid) -> None: + record = _read_oci_record(vmid) + if not record or not record.get("installation_id"): + return + data = _oci_dismissed() + data[str(int(vmid))] = record["installation_id"] + _ensure_dir() + tmp = f"{_OCI_DISMISSED_FILE}.tmp.{os.getpid()}" + try: + with open(tmp, "w", encoding="utf-8") as handle: + json.dump(data, handle, indent=2, sort_keys=True) + os.chmod(tmp, 0o600) + os.replace(tmp, _OCI_DISMISSED_FILE) + except OSError as exc: + print(f"[ProxMenux] lxc_apps: could not save the OCI dismissal: {exc}") + + +def ensure_oci_registration(vmid) -> bool: + """Register the application ProxMenux installed from an OCI image the + first time the Monitor sees its container, with version tracking by the + image. The auxiliary members of a stack are not registered, nor is a + container that already has applications, nor one whose registration the + user removed.""" + record = _read_oci_record(vmid) + if not record or record.get("status") != "installed": + return False + member = record.get("stack_member") if isinstance(record.get("stack_member"), dict) else {} + if member.get("primary_vmid") not in (None, record.get("vmid")): + return False + if _oci_dismissed().get(str(int(vmid))) == record.get("installation_id"): + return False + with _cache_lock: + sidecar = _read_sidecar(vmid) + if sidecar and sidecar.get("apps"): + return False + meta = _oci_instance_meta(vmid) + if not meta or not meta.get("name") or not _NAME_RE.match(str(meta["name"])): + return False + port = meta.get("endpoint_port") or next(iter(meta.get("ports") or []), None) + payload = { + "name": meta["name"], + "installed_via": "oci_image", + "helper_slug": meta.get("template_id") or "", + "logo_url": meta.get("logo") or "", + "update_method": "none", + "ports": [{ + "port": port, + "scheme": meta.get("endpoint_scheme") or "http", + "web_path": meta.get("endpoint_path") or "/", + "category": meta.get("category_label") or meta.get("category") or "", + "description": "", + }] if isinstance(port, int) else [], + } + # Registrations made at startup would each announce their update on their + # own; the scheduled sweep sends pending updates together instead. + ok, result = add_app(vmid, payload, notify=False) + if not ok: + print(f"[ProxMenux] lxc_apps: automatic OCI registration of CT {vmid} failed: {result}") + return ok + + +def _oci_operation_running(vmid) -> bool: + """Whether an update or recreation of this container is still running; + its record is only published again when the operation commits.""" + try: + with open(f"{_OCI_INSTANCE_ROOT}/{int(vmid)}/oci-compose.json", encoding="utf-8") as handle: + record = json.load(handle) + except (OSError, ValueError, TypeError): + return False + return isinstance(record, dict) and record.get("status") == "updating" + + +_oci_rechecks: set = set() + + +def _recheck_after_oci_operation(vmid, app_id: str) -> None: + """Check the application again once the running operation has finished.""" + key = (int(vmid), app_id) + if key in _oci_rechecks: + return + _oci_rechecks.add(key) + + def wait_and_check(): + try: + for _ in range(60): + time.sleep(15) + if not _oci_operation_running(vmid): + check_app(vmid, app_id, force=True) + return + finally: + _oci_rechecks.discard(key) + + threading.Thread(target=wait_and_check, name=f"oci-recheck-{vmid}", daemon=True).start() + + def _oci_instance_meta(vmid) -> Optional[dict]: """What ProxMenux itself recorded when it installed this container. @@ -5374,7 +5505,9 @@ def _oci_instance_meta(vmid) -> Optional[dict]: record = json.load(handle) except (OSError, ValueError, TypeError): return None - if not isinstance(record, dict) or record.get("status") not in ("installed", "assembling"): + # While an update or recreation runs, the record is the copy taken before + # it: still the right identity for the container, not yet its new image. + if not isinstance(record, dict) or record.get("status") not in ("installed", "assembling", "updating"): return None template = record.get("template") or {} contract = template.get("container_contract") or {} @@ -5516,6 +5649,8 @@ def _oci_image_versions(vmid, known: Optional[dict] = None, with_latest: bool = The installed version is read by digest, which never changes, so a previous answer for the same digest is reused instead of asked again. """ + if _oci_operation_running(vmid): + return {"busy": True} meta = _oci_instance_meta(vmid) if not meta or not meta.get("image_reference") or not meta.get("installed_digest"): return {"error": "no OCI installation record for this container"} @@ -5922,7 +6057,7 @@ def get_suggestions(vmid, force: bool = False) -> dict: # decided by the image, which always has a build date and a digest, # so it applies even to an image that states no application version. versions = _oci_image_versions(vmid, with_latest=False) - if not versions.get("error"): + if not versions.get("error") and not versions.get("busy"): tracking = { "installed_via": "oci_image", "detected_version": versions.get("installed_version") or _oci_image_label( diff --git a/lang/es.json b/lang/es.json index 2971bd7a..8f81f5b7 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1,4 +1,6 @@ { + "Unsupported kept setting:": "Ajuste conservado no admitido:", + "Could not restore the container setting:": "No se pudo restaurar el ajuste del contenedor:", "# Alternative if you prefer screen": "# Alternativa si prefieres la pantalla", "# Recommended: avoids disconnection during upgrade": "# Recomendado: evita la desconexión durante la actualización", "(Checked entries will be removed. Uncheck to keep in VM.)": "(Las entradas marcadas se eliminarán. Desmarque para mantener en VM).", @@ -2323,7 +2325,7 @@ "Free public Proxmox repository enabled": "Repositorio público gratuito de Proxmox habilitado", "Free space OK:": "Espacio libre Aceptar:", "Free up disk space": "Liberar espacio en disco", - "Free:": "Gratis:", + "Free:": "Libre:", "FreeCAD is a general-purpose parametric 3D computer-aided design (CAD) modeler and a building information modeling (BIM) software application with finite element method (FEM) support.": "FreeCAD es un modelador de diseño paramétrico 3D (CAD) de uso general y una aplicación de modelado de información de construcción (BIM) con soporte de elementos finitos (FEM).", "French": "Francés", "Freshrss is a free, self-hostable aggregator for rss feeds.": "Freshrss es un agregador libre y auto-hostable para las fuentes de rss.", @@ -3939,7 +3941,7 @@ "No folders found in /mnt. Please create a new folder.": "No se encontraron carpetas en /mnt. Por favor cree una nueva carpeta.", "No folders found inside /mnt in the CT.": "No se encontraron carpetas dentro de /mnt en el CT.", "No format-safe disks are available.": "No hay discos con formato seguro disponibles.", - "No free ProxMenux private /24 network is available": "No gratuito ProxMenux red privada /24 está disponible", + "No free ProxMenux private /24 network is available": "No hay ninguna red privada /24 de ProxMenux libre", "No gasket DKMS registrations remain.": "No quedan registros de gasket en DKMS.", "No group creation required — uses world-writable sticky bit permissions.": "No se requiere creación de grupos: utiliza permisos de bits adhesivos de escritura mundial.", "No host VFIO reconfiguration expected": "No se espera reconfiguración del VFIO del host", @@ -6237,7 +6239,7 @@ "There is no temporary container of this operation to keep the current disks": "No hay contenedor temporal de esta operación para mantener los discos actuales", "There is no verified backup; a modified container is not touched": "No hay backup verificado; un contenedor modificado no se toca", "These Proxmox resources were added outside ProxMenux:": "Se añadieron estos recursos en Proxmox fuera de ProxMenux:", - "These VMIDs are not free:": "Estos VMID no son gratuitos:", + "These VMIDs are not free:": "Estos VMID no están libres:", "These are the changes that will be made": "Estos son los cambios que se harán", "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:", diff --git a/oci/remote/install_oci.sh b/oci/remote/install_oci.sh index 02086f64..74699784 100755 --- a/oci/remote/install_oci.sh +++ b/oci/remote/install_oci.sh @@ -757,6 +757,19 @@ apply_extra_hosts() { (( failed == 0 )) || die "$(translate "Could not apply the Compose extra hosts")" } +# Settings an update gives back to the rebuilt container before it starts: +# the LAN leg of a suite or stack member and its start order. +apply_kept_settings() { + local key value + while IFS=$'\t' read -r key value; do + [[ -n $key ]] || continue + [[ $key == net1 || $key == startup ]] \ + || die "$(translate "Unsupported kept setting:") $key" + oci_quiet pct set "$VMID" "--$key" "$value" \ + || die "$(translate "Could not restore the container setting:") $key" + done < <(jq -r '.kept_proxmox_settings // {} | to_entries[] | [.key, .value] | @tsv' "$DEPLOYMENT_FILE") +} + apply_runtime_user() { local user_spec=$1 rootfs passwd_file group_file user_part group_part uid gid failed=0 rootfs="/var/lib/lxc/${VMID}/rootfs" @@ -1653,6 +1666,7 @@ apply_extra_hosts apply_installer_profile apply_rlimits apply_host_monitor +apply_kept_settings while IFS= read -r REPAIR_ENCODED; do [[ -n $REPAIR_ENCODED ]] || continue diff --git a/oci/remote/oci_instance_reconcile.py b/oci/remote/oci_instance_reconcile.py index b6567bb3..30b90458 100644 --- a/oci/remote/oci_instance_reconcile.py +++ b/oci/remote/oci_instance_reconcile.py @@ -102,7 +102,8 @@ def propose(record, config): if 'description' in changed: changed.remove('description') new_keys = [key for key in changed if key not in before and re.fullmatch(r'(mp|dev)[0-9]+', key)] - unsupported = [key for key in changed if key not in new_keys and key not in transaction.ADOPTABLE] + unsupported = [key for key in changed if key not in new_keys and key not in transaction.ADOPTABLE + and key not in transaction.KEPT_AS_IS] if unsupported: raise ValueError(f"{translate('These manual changes cannot be adopted safely:')} {', '.join(unsupported)}") if not new_keys: diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index e3aa6b3d..6c9a49a5 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -357,6 +357,9 @@ def candidate_contract(record, operation, proposal=None): # Resource settings edited in Proxmox that the new container keeps. +# Proxmox settings the rebuilt container gets back exactly as they are: the +# LAN leg ProxMenux adds to a suite or stack member, and the start order. +KEPT_AS_IS = ('net1', 'startup') ADOPTABLE = {'memory': ('resources', 'memory_mb'), 'swap': ('resources', 'swap_mb'), 'cores': ('resources', 'cores'), 'cpulimit': ('resources', 'cores'), 'cpuunits': ('resources', 'cpu_units'), 'onboot': (None, 'onboot')} @@ -384,7 +387,7 @@ def external_changes(record, config, adopt=True): elif key in ('memory', 'swap', 'cpuunits', cores_key) and value and re.fullmatch(r'[0-9]+', value): if int(value) > 0 or key == 'swap': values[key] = int(value) - refused = [key for key in changed if key not in values] + refused = [key for key in changed if key not in values and key not in KEPT_AS_IS] if refused: raise ValueError(f"{translate('The container was changed outside ProxMenux and an update would discard those changes:')} " f"{', '.join(refused)}") @@ -421,14 +424,13 @@ def preflight(record, candidate, config, coordinated=None): if deployment.get('security', {}).get('sysctls'): runtime_keys.add('lxc.include') runtime_settings.check(config, deployment, record['vmid']) - coordinated_keys = {'net1', 'startup', 'hookscript'} if coordinated else set() + 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)}: raise ValueError(translate('The container has advanced Proxmox settings outside the supported profile')) for plan in (deployment, desired): security = plan.get('security', {}) if (security.get('unprivileged') is not True or security.get('options') or plan.get('host_monitor') - or plan.get('extra_hosts') or plan.get('resources', {}).get('rlimits')): raise ValueError(translate('Updates are not available yet in this beta for applications that use ' 'a privileged container or advanced LXC settings')) @@ -900,6 +902,11 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i 'external_data_acknowledged': acknowledge_external_data, 'original_gpu_devices': original_gpu, 'desired_gpu_devices': desired_gpu, 'was_running': run('pct', 'status', str(vmid)).strip() == b'status: running'} + state['kept_config'] = {key: cfg[key] for key in KEPT_AS_IS if key in cfg} + if not coordinated and state['kept_config']: + # Applied by the installer before the new container starts, so the + # application has its LAN leg from the first second. + state['runtime_deployment']['kept_proxmox_settings'] = state['kept_config'] if coordinated: state['coordinated'] = coordinated state['preserved_stack_config'] = {key: cfg[key] for key in ('net1', 'startup', 'hookscript') if key in cfg} @@ -919,8 +926,6 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i gpu_devices.verify(desired_gpu) if show: msg_ok(translate('Update prepared') if update else translate('Recreation prepared')) - if original_sources or desired_sources: - msg_warn(translate('Host directories are not included in the backup and are not reverted by a recovery.')) msg_info(translate('Stopping the container...')) stop(vmid) if show: diff --git a/oci/remote/oci_stack_native.py b/oci/remote/oci_stack_native.py index ce77cc38..bf7c5c3b 100644 --- a/oci/remote/oci_stack_native.py +++ b/oci/remote/oci_stack_native.py @@ -672,9 +672,6 @@ def run(vmid, recover=False, acknowledge_external_data=False, keep_backup=None): msg_ok(f"{translate('Backup created in')} {keep_backup}") else: adapter.keep_backup = keep_backup - if any(mount['type'] == 'host-bind' for member in plan['members'] - for mount in member.get('deployment', {}).get('mounts', [])): - msg_warn(translate('Host directories are not included in the backups and are not reverted by a recovery.')) result = stack_tx.execute(journal, adapter, plan) msg_ok(translate('Stack update completed. Data kept.')) return result diff --git a/web/messages/en/docs/oci-manager/lifecycle.json b/web/messages/en/docs/oci-manager/lifecycle.json index 403c4324..4fc04142 100644 --- a/web/messages/en/docs/oci-manager/lifecycle.json +++ b/web/messages/en/docs/oci-manager/lifecycle.json @@ -95,6 +95,7 @@ "When the registry still serves the installed digest, nothing is downloaded and the instance is not touched.", "The new image is verified layer by layer before the CT stops. A download that arrives damaged is fetched a second time; an image already cached that fails the check is downloaded again.", "Settings changed in Proxmox VE after the installation (memory, swap, cores, CPU limit, CPU priority, start with the node) are kept and carried into the new contract.", + "The LAN interface that the Arr suite and multi-container applications add (net1) and the start order (startup) are given back to the new container as they are.", "Any other difference between the CT and its contract stops the update before the CT is stopped." ] } diff --git a/web/messages/es/docs/oci-manager/lifecycle.json b/web/messages/es/docs/oci-manager/lifecycle.json index a5527744..47c521f6 100644 --- a/web/messages/es/docs/oci-manager/lifecycle.json +++ b/web/messages/es/docs/oci-manager/lifecycle.json @@ -95,6 +95,7 @@ "Cuando el registro sigue sirviendo el digest instalado, no se descarga nada y la instancia no se toca.", "La imagen nueva se verifica capa a capa antes de detener el CT. Una descarga que llega dañada se repite una vez; una imagen ya almacenada que no supera la comprobación se descarga de nuevo.", "Los ajustes cambiados en Proxmox VE después de la instalación (memoria, swap, núcleos, límite de CPU, prioridad de CPU, arranque con el nodo) se conservan y pasan al contrato nuevo.", + "La interfaz de la LAN que añaden la suite Arr y las aplicaciones multicontenedor (net1) y el orden de arranque (startup) se devuelven tal cual al contenedor nuevo.", "Cualquier otra diferencia entre el CT y su contrato detiene la actualización antes de detener el CT." ] }