mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-08 22:46:41 +00:00
feat(oci): recover applications after a Proxmox reinstall, cluster records and AMD GPU profiles
The installation record of an OCI application travels with its container: a copy inside the container and another in /etc/pve, written together with the one kept on the host. A container restored on a newly installed Proxmox, restored with another ID or moved to another node of a cluster is recognised and registered again, with its private network, hookscript, Rclone mount, host firewall rule and NVIDIA runtime. The Monitor offers the same recovery from the Updates tab. AMD GPUs are offered by generation. A GPU the ROCm image supports takes the profile as it is; one of a supported family (Radeon 680M, 780M) is an experimental option that asks for confirmation and is never proposed; an older one is not offered. The GPU is checked with a real inference before the installation accepts it. Recreate changes what runs recognition in an installed Immich, between the CPU and a GPU of the host. Updates: - A failed update that is restored and checked removes its temporary container and the disks of the failed attempt. - Every container volume is part of the backups, so Jellyfin, Plex and Hugo update with their default installation. - An image published with a Docker-format manifest is recognised by its layers and build time and updates. - The Proxmox notes of a multi-container application link to its LAN address.
This commit is contained in:
@@ -51,6 +51,7 @@ interface OciInstanceInfo {
|
||||
members: number[]
|
||||
host_directories: boolean
|
||||
pending: boolean
|
||||
restored?: boolean
|
||||
}
|
||||
|
||||
interface LxcUpdateCheck {
|
||||
@@ -950,7 +951,10 @@ export function VirtualMachines() {
|
||||
// Set when the open container was installed by OCI manager Apps: its
|
||||
// Updates tab replaces the image instead of updating packages.
|
||||
const [ociInstance, setOciInstance] = useState<OciInstanceInfo | null>(null)
|
||||
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" } | null>(null)
|
||||
// A container restored from a backup carries the mark of its installation
|
||||
// and has no record on this host until it is recovered from the menu.
|
||||
const [ociRestored, setOciRestored] = useState(false)
|
||||
const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" | "recover" } | null>(null)
|
||||
|
||||
// Firewall log state — fetched only when the operator opens that tab
|
||||
// so a CT/VM without firewall use doesn't pay the pvesh cost on every
|
||||
@@ -1260,6 +1264,7 @@ export function VirtualMachines() {
|
||||
setFirewallEnabled(true)
|
||||
setConsoleLogAvailable(false)
|
||||
setOciInstance(null)
|
||||
setOciRestored(false)
|
||||
|
||||
// Prime UI from last-known payloads so a reopened guest never
|
||||
// flashes "Loading…" — the backend and this cache both revalidate
|
||||
@@ -1429,10 +1434,12 @@ export function VirtualMachines() {
|
||||
.then((r) => {
|
||||
setConsoleLogAvailable(!!r?.console_log)
|
||||
setOciInstance(r?.oci_instance ? r : null)
|
||||
setOciRestored(!!r?.restored)
|
||||
})
|
||||
.catch(() => {
|
||||
setConsoleLogAvailable(false)
|
||||
setOciInstance(null)
|
||||
setOciRestored(false)
|
||||
})
|
||||
|
||||
// An operation that is still running when the modal opens clears on its
|
||||
@@ -5550,7 +5557,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
{/* Branch 1 — OCI-image container not installed by
|
||||
OCI manager Apps */}
|
||||
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
|
||||
selectedVM.update_check?.is_oci_lxc && (
|
||||
(ociRestored || selectedVM.update_check?.is_oci_lxc) && (
|
||||
<Card className="border border-border bg-card/50">
|
||||
<CardContent className="p-4 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -5558,12 +5565,24 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
<Container className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{t("vmLxc.updates.ociTitle")}
|
||||
{t(ociRestored ? "vmLxc.updates.ociRestoredTitle" : "vmLxc.updates.ociTitle")}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t("vmLxc.updates.ociBody")}
|
||||
{t(ociRestored ? "vmLxc.updates.ociRestoredBody" : "vmLxc.updates.ociBody")}
|
||||
</p>
|
||||
{ociRestored && (
|
||||
<div className="mt-4 pt-4 border-t border-border/50 flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="border border-input bg-background text-foreground/80 hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recover" })}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-1.5" />
|
||||
{t("vmLxc.ociUpdates.recover")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -5582,7 +5601,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
Individual actions stay with their section. The
|
||||
optional reusable bulk action is configured in its
|
||||
own card immediately before Options. */}
|
||||
{!selectedVM.update_check?.managed_oci_app && !ociInstance &&
|
||||
{!selectedVM.update_check?.managed_oci_app && !ociInstance && !ociRestored &&
|
||||
!selectedVM.update_check?.is_oci_lxc && (() => {
|
||||
const uc = selectedVM.update_check
|
||||
const osUpdateStatusKnown = !!uc && !uc.error
|
||||
@@ -7838,7 +7857,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
}}
|
||||
scriptPath="/usr/local/share/proxmenux/scripts/oci/manage_instance.sh"
|
||||
scriptName="oci_manage_instance"
|
||||
title={ociAction.action === "update" ? t("vmLxc.ociUpdates.terminalTitleUpdate") : t("vmLxc.ociUpdates.terminalTitleRecreate")}
|
||||
title={ociAction.action === "update" ? t("vmLxc.ociUpdates.terminalTitleUpdate")
|
||||
: ociAction.action === "recover" ? t("vmLxc.ociUpdates.terminalTitleRecover")
|
||||
: t("vmLxc.ociUpdates.terminalTitleRecreate")}
|
||||
description={t("vmLxc.ociUpdates.terminalDescription")}
|
||||
params={{
|
||||
VMID: String(ociAction.vmid),
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Wiederherstellen",
|
||||
"terminalTitleUpdate": "OCI-Anwendung aktualisieren",
|
||||
"terminalTitleRecreate": "OCI-Anwendung neu erstellen",
|
||||
"terminalTitleRecover": "Wiederhergestellte OCI-Anwendung registrieren",
|
||||
"terminalDescription": "Derselbe Ablauf wie OCI manager Apps → Installierte OCI-Anwendungen verwalten.",
|
||||
"keepBackup": "Das vor dem Update erstellte Backup behalten",
|
||||
"noKeepBackup": "Das vor dem Update erstellte Backup wird nicht behalten",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Noch keine Update-Informationen – prüfen Sie unter Sicherheit → Secure Gateway.",
|
||||
"ociTitle": "OCI-Image-Container",
|
||||
"ociBody": "Dieser Container wurde aus einem OCI-Image (Docker) erstellt. Die Updateverwaltung für OCI-Container wird mit der kommenden OCI-Installationsfunktion geliefert – Updates erstellen den Container anhand eines neueren Image-Tags neu, anstatt Pakete darin zu patchen.",
|
||||
"ociRestoredTitle": "Wiederhergestellte OCI-Anwendung",
|
||||
"ociRestoredBody": "Dieser Container wurde aus einem Backup wiederhergestellt und dieser Host hat keinen Eintrag zu seiner Anwendung, daher kann er von hier aus noch nicht aktualisiert werden. Wiederherstellen registriert ihn erneut auf diesem Host, genauso wie OCI manager Apps → Installierte OCI-Anwendungen verwalten.",
|
||||
"dockerImagesTitle": "Docker-Images",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Images",
|
||||
|
||||
@@ -1225,6 +1225,7 @@
|
||||
"recover": "Recover",
|
||||
"terminalTitleUpdate": "Update OCI application",
|
||||
"terminalTitleRecreate": "Recreate OCI application",
|
||||
"terminalTitleRecover": "Recover restored OCI application",
|
||||
"terminalDescription": "The same flow as OCI manager Apps → Manage installed OCI applications.",
|
||||
"keepBackup": "Keep the backup taken before updating",
|
||||
"noKeepBackup": "The backup taken before updating is not kept",
|
||||
@@ -1436,6 +1437,8 @@
|
||||
"noManagedUpdateInfo": "No update information yet — check from Security → Secure Gateway.",
|
||||
"ociTitle": "OCI image container",
|
||||
"ociBody": "This container was created from an OCI image outside OCI manager Apps. It is updated by replacing its image, which ProxMenux does not manage for it.",
|
||||
"ociRestoredTitle": "Restored OCI application",
|
||||
"ociRestoredBody": "This container was restored from a backup and this host has no record of its application, so it cannot be updated from here yet. Recover registers it again on this host, the same as OCI manager Apps → Manage installed OCI applications.",
|
||||
"dockerImagesTitle": "Docker images",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Images",
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Recuperar",
|
||||
"terminalTitleUpdate": "Actualizar aplicación OCI",
|
||||
"terminalTitleRecreate": "Recrear aplicación OCI",
|
||||
"terminalTitleRecover": "Recuperar aplicación OCI restaurada",
|
||||
"terminalDescription": "El mismo recorrido que OCI manager Apps → Gestionar aplicaciones OCI instaladas.",
|
||||
"keepBackup": "Conservar el backup realizado antes de actualizar",
|
||||
"noKeepBackup": "No se conserva el backup realizado antes de actualizar",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Aún no hay información de actualización: verifique desde Seguridad → Secure Gateway.",
|
||||
"ociTitle": "Contenedor de imágenes OCI",
|
||||
"ociBody": "Este contenedor se creó desde una imagen OCI fuera de OCI manager Apps. Se actualiza sustituyendo su imagen, algo que ProxMenux no gestiona en este caso.",
|
||||
"ociRestoredTitle": "Aplicación OCI restaurada",
|
||||
"ociRestoredBody": "Este contenedor se restauró desde un backup y este host no tiene el registro de su aplicación, así que todavía no se puede actualizar desde aquí. Recuperar lo registra de nuevo en este host, igual que OCI manager Apps → Gestionar aplicaciones OCI instaladas.",
|
||||
"dockerImagesTitle": "Imágenes Docker",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Imágenes",
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Récupérer",
|
||||
"terminalTitleUpdate": "Mettre à jour l'application OCI",
|
||||
"terminalTitleRecreate": "Recréer l'application OCI",
|
||||
"terminalTitleRecover": "Récupérer l'application OCI restaurée",
|
||||
"terminalDescription": "Le même parcours que OCI manager Apps → Gérer les applications OCI installées.",
|
||||
"keepBackup": "Conserver la sauvegarde prise avant la mise à jour",
|
||||
"noKeepBackup": "La sauvegarde prise avant la mise à jour n'est pas conservée",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Aucune information de mise à jour pour l'instant - vérifiez depuis Sécurité → Secure Gateway.",
|
||||
"ociTitle": "Conteneur d'images OCI",
|
||||
"ociBody": "Ce conteneur a été créé à partir d'une image OCI (Docker). La gestion des mises à jour pour les conteneurs OCI est fournie avec la prochaine fonctionnalité d'installation OCI : les mises à jour reconstruiront le conteneur à partir d'une balise d'image plus récente plutôt que de corriger les packages à l'intérieur.",
|
||||
"ociRestoredTitle": "Application OCI restaurée",
|
||||
"ociRestoredBody": "Ce conteneur a été restauré depuis une sauvegarde et cet hôte n'a pas l'enregistrement de son application ; il ne peut donc pas encore être mis à jour d'ici. Récupérer l'enregistre à nouveau sur cet hôte, comme OCI manager Apps → Gérer les applications OCI installées.",
|
||||
"dockerImagesTitle": "Images Docker",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Images",
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Recupera",
|
||||
"terminalTitleUpdate": "Aggiorna applicazione OCI",
|
||||
"terminalTitleRecreate": "Ricrea applicazione OCI",
|
||||
"terminalTitleRecover": "Recupera applicazione OCI ripristinata",
|
||||
"terminalDescription": "Lo stesso percorso di OCI manager Apps → Gestisci le applicazioni OCI installate.",
|
||||
"keepBackup": "Conserva il backup eseguito prima dell'aggiornamento",
|
||||
"noKeepBackup": "Il backup eseguito prima dell'aggiornamento non viene conservato",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Nessuna informazione di aggiornamento ancora: controlla da Sicurezza → Secure Gateway.",
|
||||
"ociTitle": "Contenitore di immagini OCI",
|
||||
"ociBody": "Questo contenitore è stato creato da un'immagine OCI (Docker). La gestione degli aggiornamenti per i contenitori OCI arriverà con la prossima funzionalità di installazione OCI: gli aggiornamenti ricostruiranno il contenitore da un tag immagine più recente anziché applicare patch ai pacchetti all'interno.",
|
||||
"ociRestoredTitle": "Applicazione OCI ripristinata",
|
||||
"ociRestoredBody": "Questo contenitore è stato ripristinato da un backup e questo host non ha il registro della sua applicazione, quindi non può ancora essere aggiornato da qui. Recupera lo registra di nuovo su questo host, come OCI manager Apps → Gestisci le applicazioni OCI installate.",
|
||||
"dockerImagesTitle": "Immagini Docker",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Immagini",
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Recuperar",
|
||||
"terminalTitleUpdate": "Atualizar aplicação OCI",
|
||||
"terminalTitleRecreate": "Recriar aplicação OCI",
|
||||
"terminalTitleRecover": "Recuperar aplicação OCI restaurada",
|
||||
"terminalDescription": "O mesmo percurso de OCI manager Apps → Gerir aplicações OCI instaladas.",
|
||||
"keepBackup": "Manter o backup feito antes da atualização",
|
||||
"noKeepBackup": "O backup feito antes da atualização não é mantido",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Nenhuma informação de atualização ainda — verifique em Segurança → Secure Gateway.",
|
||||
"ociTitle": "Contêiner de imagem OCI",
|
||||
"ociBody": "Este contêiner foi criado a partir de uma imagem OCI (Docker). O gerenciamento de atualizações para contêineres OCI vem com o próximo recurso de instalação do OCI – as atualizações reconstruirão o contêiner a partir de uma tag de imagem mais recente, em vez de corrigir os pacotes internos.",
|
||||
"ociRestoredTitle": "Aplicação OCI restaurada",
|
||||
"ociRestoredBody": "Este contêiner foi restaurado a partir de um backup e este host não tem o registro da sua aplicação, por isso ainda não pode ser atualizado daqui. Recuperar o registra novamente neste host, tal como OCI manager Apps → Gerenciar aplicações OCI instaladas.",
|
||||
"dockerImagesTitle": "Imagens Docker",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Imagens",
|
||||
|
||||
@@ -1225,6 +1225,7 @@
|
||||
"recover": "Obnoviť",
|
||||
"terminalTitleUpdate": "Aktualizovať OCI aplikáciu",
|
||||
"terminalTitleRecreate": "Znovu vytvoriť OCI aplikáciu",
|
||||
"terminalTitleRecover": "Obnoviť obnovenú OCI aplikáciu",
|
||||
"terminalDescription": "Použije sa rovnaký postup ako v ponuke Správca OCI aplikácií → Spravovať nainštalované OCI aplikácie.",
|
||||
"keepBackup": "Ponechať zálohu vytvorenú pred aktualizáciou",
|
||||
"noKeepBackup": "Záloha vytvorená pred aktualizáciou sa neponecháva",
|
||||
@@ -1436,6 +1437,8 @@
|
||||
"noManagedUpdateInfo": "Zatiaľ nie sú žiadne informácie o aktualizácii – skontrolujte ich v časti Zabezpečenie → Secure Gateway.",
|
||||
"ociTitle": "Kontajner z OCI obrazu",
|
||||
"ociBody": "Tento kontajner bol vytvorený z OCI (Docker) obrazu mimo Správcu OCI aplikácií. Aktualizuje sa nahradením obrazu, čo ProxMenux pri tomto kontajneri nespravuje.",
|
||||
"ociRestoredTitle": "Obnovená OCI aplikácia",
|
||||
"ociRestoredBody": "Tento kontajner bol obnovený zo zálohy a tento hostiteľ nemá záznam o jeho aplikácii, preto ho zatiaľ nemožno aktualizovať odtiaľto. Obnoviť ho znova zaregistruje na tomto hostiteľovi, rovnako ako OCI manager Apps → Spravovať nainštalované OCI aplikácie.",
|
||||
"dockerImagesTitle": "Docker obrazy",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Obrazy",
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"recover": "Återställ",
|
||||
"terminalTitleUpdate": "Uppdatera OCI-applikation",
|
||||
"terminalTitleRecreate": "Återskapa OCI-applikation",
|
||||
"terminalTitleRecover": "Återställ återställd OCI-applikation",
|
||||
"terminalDescription": "Samma flöde som OCI manager Apps → Hantera installerade OCI-applikationer.",
|
||||
"keepBackup": "Behåll säkerhetskopian som tas före uppdateringen",
|
||||
"noKeepBackup": "Säkerhetskopian som tas före uppdateringen behålls inte",
|
||||
@@ -1415,6 +1416,8 @@
|
||||
"noManagedUpdateInfo": "Ingen uppdateringsinformation ännu — kolla från Säkerhet → Secure Gateway.",
|
||||
"ociTitle": "OCI-bildbehållare",
|
||||
"ociBody": "Den här behållaren skapades från en OCI-bild (Docker). Uppdateringshantering för OCI-behållare kommer med den kommande OCI-installationsfunktionen - uppdateringar kommer att bygga om behållaren från en nyare bildtagg snarare än att patcha paket inuti.",
|
||||
"ociRestoredTitle": "Återställd OCI-applikation",
|
||||
"ociRestoredBody": "Den här behållaren återställdes från en säkerhetskopia och den här värden har ingen post för dess applikation, så den kan ännu inte uppdateras härifrån. Återställ registrerar den igen på den här värden, på samma sätt som OCI manager Apps → Hantera installerade OCI-applikationer.",
|
||||
"dockerImagesTitle": "Docker-avbilder",
|
||||
"dockerAppTitle": "Docker",
|
||||
"dockerImagesSubheading": "Avbilder",
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
Read-only view of the installation record OCI manager Apps keeps for every
|
||||
container it created: whether the container is one, whether it belongs to a
|
||||
multi-container application, whether it uses host directories (which its
|
||||
backup does not revert) and whether an operation is pending. Nothing here
|
||||
backup does not revert), whether an operation is pending and whether it was
|
||||
restored from a backup and is not registered on this host yet. Nothing here
|
||||
changes the record or runs inside the container.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
|
||||
import oci_console_logs
|
||||
|
||||
@@ -26,6 +29,30 @@ def _record(vmid: int) -> dict | None:
|
||||
return record if isinstance(record, dict) else None
|
||||
|
||||
|
||||
def _installation(vmid: int) -> str | None:
|
||||
"""The installation the container says it belongs to: the mark OCI manager
|
||||
Apps leaves in its notes, which a backup keeps."""
|
||||
path = f"/etc/pve/nodes/{socket.gethostname().split('.', 1)[0]}/lxc/{int(vmid)}.conf"
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="ignore") as handle:
|
||||
text = handle.read().split("\n[", 1)[0]
|
||||
except OSError:
|
||||
return None
|
||||
match = re.search(r"^#.*proxmenux-instance=([0-9a-f-]{36})(?![0-9a-f-])", text, re.MULTILINE)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def _unrecoverable(vmid: int, installation: str) -> bool:
|
||||
"""A restored container that carries no copy of its record: the menu
|
||||
found nothing to recover it from and left it as an ordinary container."""
|
||||
try:
|
||||
with open(os.path.join(ROOT, ".unrecoverable.json"), encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return isinstance(value, dict) and value.get(str(int(vmid))) == installation
|
||||
|
||||
|
||||
def _host_dirs(record: dict) -> bool:
|
||||
mounts = (record.get("deployment") or {}).get("mounts") or []
|
||||
return any(isinstance(m, dict) and m.get("type") == "host-bind" for m in mounts)
|
||||
@@ -42,8 +69,14 @@ def info(vmid: int) -> dict:
|
||||
"members": [],
|
||||
"host_directories": False,
|
||||
"pending": False,
|
||||
"restored": False,
|
||||
}
|
||||
record = _record(vmid)
|
||||
installation = _installation(vmid)
|
||||
if installation and (record is None or record.get("installation_id") != installation):
|
||||
# Restored from a backup: the record stayed on the host it came from.
|
||||
result["restored"] = not _unrecoverable(vmid, installation)
|
||||
return result
|
||||
if record is None:
|
||||
return result
|
||||
primary_id = int((record.get("stack_member") or {}).get("primary_vmid") or vmid)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""A container restored from a backup carries the mark of its installation and
|
||||
has no record on this host: the modal says so instead of treating it as an
|
||||
ordinary container."""
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
SCRIPTS = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
import oci_instance_info
|
||||
|
||||
APP = "83e373f2-5ccc-4548-9b89-22d0956d1c77"
|
||||
OTHER = "488ed3cb-1145-477a-b90f-c46777d69fb2"
|
||||
|
||||
|
||||
class RestoredInstanceTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.root = Path(tmp.name)
|
||||
for target, value in (("ROOT", str(self.root)),):
|
||||
patcher = patch.object(oci_instance_info, target, value)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher = patch.object(oci_instance_info.oci_console_logs, "configured_log", return_value=None)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def record(self, vmid, identity):
|
||||
folder = self.root / str(vmid)
|
||||
folder.mkdir()
|
||||
(folder / "oci-compose.json").write_text(json.dumps(
|
||||
{"vmid": vmid, "installation_id": identity, "status": "installed", "deployment": {}}))
|
||||
|
||||
def info(self, vmid, installation):
|
||||
with patch.object(oci_instance_info, "_installation", return_value=installation):
|
||||
return oci_instance_info.info(vmid)
|
||||
|
||||
def test_a_marked_container_without_record_was_restored(self):
|
||||
result = self.info(119, APP)
|
||||
self.assertTrue(result["restored"])
|
||||
self.assertFalse(result["oci_instance"])
|
||||
|
||||
def test_the_record_of_another_installation_does_not_register_it(self):
|
||||
self.record(119, OTHER)
|
||||
result = self.info(119, APP)
|
||||
self.assertTrue(result["restored"])
|
||||
self.assertFalse(result["oci_instance"])
|
||||
|
||||
def test_a_container_the_menu_could_not_recover_is_an_ordinary_one(self):
|
||||
(self.root / ".unrecoverable.json").write_text(json.dumps({"119": APP}))
|
||||
self.assertFalse(self.info(119, APP)["restored"])
|
||||
self.assertTrue(self.info(119, OTHER)["restored"])
|
||||
|
||||
def test_a_registered_container_is_an_oci_instance(self):
|
||||
self.record(119, APP)
|
||||
result = self.info(119, APP)
|
||||
self.assertFalse(result["restored"])
|
||||
self.assertTrue(result["oci_instance"])
|
||||
|
||||
def test_an_ordinary_container_is_neither(self):
|
||||
result = self.info(100, None)
|
||||
self.assertFalse(result["restored"])
|
||||
self.assertFalse(result["oci_instance"])
|
||||
|
||||
def test_the_mark_is_read_from_the_notes_of_the_container(self):
|
||||
text = f"#<div>Tandoor</div>\n#<!-- proxmenux-instance={APP} -->\narch: amd64\n[snap]\n#proxmenux-instance={OTHER}\n"
|
||||
with patch("builtins.open", unittest.mock.mock_open(read_data=text)):
|
||||
self.assertEqual(oci_instance_info._installation(119), APP)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user