Fix the script terminal, Arr suite updates and OCI app tracking

This commit is contained in:
MacRimi
2026-09-26 16:57:35 +02:00
parent 93862abdb0
commit dd4bcfa867
14 changed files with 220 additions and 39 deletions
+5 -1
View File
@@ -1773,7 +1773,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
<SelectItem value="binary">{t("vmLxc.appEditor.binaryVersionHint")}</SelectItem>
<SelectItem value="file">{t("vmLxc.appEditor.methodFile")}</SelectItem>
<SelectItem value="python_dist">{t("vmLxc.appEditor.methodPython")}</SelectItem>
<SelectItem value="docker_label" disabled>{t("vmLxc.appEditor.methodDockerLabel")}</SelectItem>
{/* Chosen by detection for a Docker workload; kept only so an
application already using it shows its method. */}
{method === "docker_label" && (
<SelectItem value="docker_label">{t("vmLxc.appEditor.methodDockerLabel")}</SelectItem>
)}
<SelectItem value="docker_exec">{t("vmLxc.appEditor.methodDockerExec")}</SelectItem>
<SelectItem value="command">{t("vmLxc.appEditor.methodCommand")}</SelectItem>
<SelectItem value="manual">{t("vmLxc.appEditor.methodManual")}</SelectItem>
@@ -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 (
<>
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose() }}>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="max-w-7xl p-0 flex flex-col gap-0 overflow-hidden"
style={{
+10
View File
@@ -1427,6 +1427,16 @@ export function VirtualMachines() {
setOciInstance(null)
})
// An operation that is still running when the modal opens clears on its
// own: the record is read again until it is published.
useEffect(() => {
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
+11 -11
View File
@@ -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/<tu-aplicación>/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 <servicio> -p ExecStart",
"whichHint": "cual <aplicación>",
"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",
+8
View File
@@ -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)))
+11 -8
View File
@@ -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
+140 -5
View File
@@ -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(
+5 -3
View File
@@ -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:",
+14
View File
@@ -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
+2 -1
View File
@@ -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:
+10 -5
View File
@@ -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:
-3
View File
@@ -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
@@ -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 (<code>net1</code>) and the start order (<code>startup</code>) 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."
]
}
@@ -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 (<code>net1</code>) y el orden de arranque (<code>startup</code>) 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."
]
}