diff --git a/.github/scripts/tests/test_oci_pve_version_gate.py b/.github/scripts/tests/test_oci_pve_version_gate.py index 80703fd6..b012197e 100644 --- a/.github/scripts/tests/test_oci_pve_version_gate.py +++ b/.github/scripts/tests/test_oci_pve_version_gate.py @@ -14,15 +14,16 @@ class OciPveVersionGateTests(unittest.TestCase): @classmethod def setUpClass(cls): text = LAUNCHER.read_text(encoding='utf-8') - cls.function = re.search(r'^pve_supports_oci\(\) \{\n.*?^\}\n', text, re.M | re.S).group(0) + cls.function = ''.join(re.search(rf'^{name}\(\) \{{\n.*?^\}}\n', text, re.M | re.S).group(0) + for name in ('pve_manager_version', 'pve_supports_oci')) cls.text = text def supported(self, version): with tempfile.TemporaryDirectory() as folder: - fake = Path(folder, 'pveversion') - # Without Proxmox the command is missing: a stand-in that fails hides the host's own. - fake.write_text('#!/bin/sh\nexit 127\n' if version is None else - f'#!/bin/sh\necho "pve-manager/{version}/b9984c6d90a4bd80 (running kernel: 6.17)"\n') + fake = Path(folder, 'dpkg-query') + # Without Proxmox the package is not installed: dpkg-query fails. + fake.write_text('#!/bin/sh\nexit 1\n' if version is None else + f'#!/bin/sh\nprintf %s "{version}"\n') fake.chmod(0o755) result = subprocess.run(['bash', '-c', f'PATH={folder}:/usr/bin:/bin\n{self.function}pve_supports_oci'], capture_output=True, text=True) @@ -38,6 +39,9 @@ class OciPveVersionGateTests(unittest.TestCase): with self.subTest(version=version): self.assertFalse(self.supported(version)) + def test_the_version_is_read_without_the_slow_pveversion(self): + self.assertNotIn('pveversion', self.function) + def test_it_is_checked_before_anything_is_installed(self): self.assertLess(self.text.index('if ! pve_supports_oci; then'), self.text.index('ensure_oci_dependencies || exit 1')) diff --git a/.github/scripts/tests/test_oci_storage_sk.py b/.github/scripts/tests/test_oci_storage_sk.py index 2ba232b0..deae0954 100644 --- a/.github/scripts/tests/test_oci_storage_sk.py +++ b/.github/scripts/tests/test_oci_storage_sk.py @@ -202,15 +202,17 @@ class InputAndCancellationTests(StorageFixture): def test_real_menu_error_handler_has_slovak_heading_and_preserves_detail(self): ui=Mock();ui.choose.side_effect=['custom',None] - with patch.object(cli,'interactive_ui',return_value=ui),patch.object(custom,'explore',side_effect=ValueError('fiction-detail')), \ + with patch.object(cli,'interactive_ui',return_value=ui),patch.object(cli,'_catalog_options',return_value=([],{}, {}, {}, [])), \ + patch.object(custom,'explore',side_effect=ValueError('fiction-detail')), \ patch.object(updates,'UpdateCheck') as checker, \ patch.object(catalog_refresh,'CatalogRefresh') as catalog_checker: # Keep this wording/error-handler test offline and free of writes. # The real background worker has its own catalog lifecycle tests. catalog_check=catalog_checker.return_value.__enter__.return_value catalog_check.active=False;catalog_check.consume.return_value=False - catalog_check.describe.return_value='' + catalog_check.failed=False;catalog_check.label.return_value='Refresh catalog' check=checker.return_value.__enter__.return_value check.active=False;check.notice.return_value='';check.status.return_value={'available':[]} + check.label.return_value='Check for updates' self.assertEqual(cli.interactive(self.catalog),0) ui.message.assert_called_once_with('Operáciu sa nepodarilo dokončiť:\n\nfiction-detail') diff --git a/.github/workflows/test-features.yml b/.github/workflows/test-features.yml index 27349ac0..5a58cd48 100644 --- a/.github/workflows/test-features.yml +++ b/.github/workflows/test-features.yml @@ -22,6 +22,9 @@ on: - 'tests/test_vm_backup_jobs_api.py' - 'tests/test_backup_storage_retention.cjs' - 'tests/test_figurine_installation.py' + - 'scripts/gpu_tpu/nvidia_installer.sh' + - 'tests/test_nvidia*.py' + - 'AppImage/scripts/managed_installs.py' - 'scripts/post_install/customizable_post_install.sh' - 'AppImage/scripts/post_install_versions.py' - 'AppImage/scripts/flask_proxmenux_routes.py' @@ -101,6 +104,9 @@ on: - 'AppImage/scripts/tests/test_proxmox_node_name.py' - 'AppImage/scripts/tests/test_pve_notifications_cfg_writes.py' - 'tests/test_lvm_disk_read_policy.py' + - 'scripts/gpu_tpu/nvidia_installer.sh' + - 'tests/test_nvidia*.py' + - 'AppImage/scripts/managed_installs.py' - 'oci/**' - 'AppImage/scripts/oci_menu_bridge.py' - 'AppImage/scripts/lxc_apps.py' @@ -169,6 +175,21 @@ jobs: NODE_PATH: ${{ github.workspace }}/.github/ci/offline-node/node_modules run: node tests/test_backup_storage_retention.cjs + nvidia-installer: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Check NVIDIA parser, cache and Secure Boot regressions + run: | + bash -n scripts/gpu_tpu/nvidia_installer.sh + python3 -B -m unittest discover -s tests -p 'test_nvidia*.py' -v + installer-and-release: runs-on: ubuntu-latest timeout-minutes: 10 @@ -387,3 +408,13 @@ jobs: run: | npm ci --prefix .github/ci/offline-node --legacy-peer-deps --ignore-scripts --no-audit --no-fund NODE_PATH="$PWD/.github/ci/offline-node/node_modules" node AppImage/tests/pbs-navigation.cjs + + native-image-import: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Exercise private images with isolated mapped Linux readers + run: sudo /usr/bin/python3 -B -m unittest discover -s oci/tests -p test_native_import_access.py -v diff --git a/AppImage/ProxMenux-1.2.8.AppImage b/AppImage/ProxMenux-1.2.8.AppImage index 106eba7c..0ca8ce2c 100755 Binary files a/AppImage/ProxMenux-1.2.8.AppImage and b/AppImage/ProxMenux-1.2.8.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 5168c059..18e3d0b3 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -92084408d97ec3c2c17499960cc77ba603d87affe0a648720d69909b1dee8b95 ProxMenux-1.2.8.AppImage +488f19197c38af62a1f3356ce5ef3b84dc8370efe51eb65cfc6b52d301b9df74 ProxMenux-1.2.8.AppImage diff --git a/AppImage/scripts/ai_context_enrichment.py b/AppImage/scripts/ai_context_enrichment.py index fd6708ab..68e75b82 100644 --- a/AppImage/scripts/ai_context_enrichment.py +++ b/AppImage/scripts/ai_context_enrichment.py @@ -165,22 +165,16 @@ def get_event_frequency(error_id: str = None, error_key: str = None, try: cursor = conn.cursor() - # Try to find the error - if error_id: + # Try to find the error. The errors table keys a row by error_key. + if error_id or error_key: cursor.execute(''' - SELECT first_seen, last_seen, occurrences, category - FROM errors WHERE error_key = ? OR error_id = ? - ORDER BY last_seen DESC LIMIT 1 - ''', (error_id, error_id)) - elif error_key: - cursor.execute(''' - SELECT first_seen, last_seen, occurrences, category + SELECT first_seen, last_seen, occurrence_count, category FROM errors WHERE error_key = ? ORDER BY last_seen DESC LIMIT 1 - ''', (error_key,)) + ''', (error_id or error_key,)) elif category: cursor.execute(''' - SELECT first_seen, last_seen, occurrences, category + SELECT first_seen, last_seen, occurrence_count, category FROM errors WHERE category = ? AND resolved_at IS NULL ORDER BY last_seen DESC LIMIT 1 ''', (category,)) diff --git a/AppImage/scripts/ai_providers/openai_provider.py b/AppImage/scripts/ai_providers/openai_provider.py index 01a33e8a..8ff3dedf 100644 --- a/AppImage/scripts/ai_providers/openai_provider.py +++ b/AppImage/scripts/ai_providers/openai_provider.py @@ -24,6 +24,9 @@ class OpenAIProvider(AIProvider): """ NAME = "openai" + # The Monitor gives any rewrite 25 s; a local model behind an + # OpenAI-compatible endpoint may need all of it. + REQUEST_TIMEOUT_SECONDS = 25 REQUIRES_API_KEY = True DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions" DEFAULT_MODELS_URL = "https://api.openai.com/v1/models" @@ -226,8 +229,8 @@ class OpenAIProvider(AIProvider): } api_url = self._get_api_url() - result = self._make_request(api_url, payload, headers) - + result = self._make_request(api_url, payload, headers, timeout=self.REQUEST_TIMEOUT_SECONDS) + try: return result['choices'][0]['message']['content'].strip() except (KeyError, IndexError) as e: diff --git a/AppImage/scripts/managed_installs.py b/AppImage/scripts/managed_installs.py index b05ab307..fe3dc74f 100644 --- a/AppImage/scripts/managed_installs.py +++ b/AppImage/scripts/managed_installs.py @@ -1188,21 +1188,15 @@ def _check_oci_app(entry: dict) -> dict: # ── NVIDIA driver checker ── # -# Source of truth for what's available upstream: -# `https://download.nvidia.com/XFree86/Linux-x86_64/latest.txt` -# returns the single newest version, e.g. "580.105.08" -# `https://download.nvidia.com/XFree86/Linux-x86_64/` -# HTML directory listing — we scrape it for per-branch latest -# (so a user on 570.x gets 570.x's latest, without an automatic -# cross-branch upgrade). -# -# Cache TTL is 7 days because NVIDIA's release cadence on each branch -# is roughly monthly. The cache is in-memory only; AppImage restarts -# refresh it for free. +# The driver update is the version the NVIDIA installer itself would +# recommend on this host: it filters by the running kernel, NVIDIA's +# maintained branches, the PCI IDs of the host's GPUs and the NVENC patch, +# and keeps the installed branch when it can. Asking the installer means the +# Monitor never announces a version the installer then refuses. The installer +# keeps its own caches of NVIDIA's pages for 24 hours. -_NVIDIA_BASE = "https://download.nvidia.com/XFree86/Linux-x86_64" -_NVIDIA_CACHE_TTL = 7 * 86400 -_nvidia_cache: dict[str, Any] = {"versions": [], "fetched_at": 0} +_NVIDIA_INSTALLER = "/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh" +_NVIDIA_CHECK_TIMEOUT = 300 def _version_tuple(v: str) -> tuple: @@ -1219,64 +1213,46 @@ def _version_tuple(v: str) -> tuple: return tuple(out[:3]) -def _fetch_nvidia_versions(force: bool = False) -> list[str]: - """Return the cached list of all upstream versions, or fetch fresh.""" - now = time.time() - if not force and _nvidia_cache["versions"] and \ - now - _nvidia_cache["fetched_at"] < _NVIDIA_CACHE_TTL: - return _nvidia_cache["versions"] +def _nvidia_recommended_version(current: str) -> tuple[Optional[str], Optional[str]]: + """The version the installer recommends for this host, or an error.""" try: - req = urllib.request.Request( - _NVIDIA_BASE + "/", - headers={"User-Agent": "ProxMenux-Monitor/1.0"}, + # An older installer would ignore the option and open its menu. + with open(_NVIDIA_INSTALLER, encoding="utf-8", errors="replace") as handle: + if "--recommended-version" not in handle.read(): + return None, "the NVIDIA installer on this host is older than ProxMenux Monitor" + done = subprocess.run( + ["bash", _NVIDIA_INSTALLER, "--recommended-version", current], + capture_output=True, text=True, timeout=_NVIDIA_CHECK_TIMEOUT, + # The host's curl and Python, not the AppImage's libraries. + env={k: v for k, v in os.environ.items() if k not in ("PYTHONPATH", "LD_LIBRARY_PATH")}, ) - with urllib.request.urlopen(req, timeout=15) as resp: - html = resp.read().decode("utf-8", errors="replace") - except Exception as e: - print(f"[ProxMenux] NVIDIA version fetch failed: {e}") - return _nvidia_cache.get("versions", []) - versions = sorted( - {m.group(1) for m in re.finditer( - r"""href=['"](\d+\.\d+(?:\.\d+)?)/?['"]""", html)}, - key=_version_tuple, - reverse=True, - ) + except (OSError, subprocess.TimeoutExpired) as e: + return None, f"the NVIDIA installer could not be run: {e}" + versions = [line.strip() for line in done.stdout.splitlines() + if re.fullmatch(r"\d+\.\d+(\.\d+)?", line.strip())] if versions: - _nvidia_cache["versions"] = versions - _nvidia_cache["fetched_at"] = now - return versions + return versions[-1], None + if done.returncode == 0: + return None, None + return None, "the NVIDIA installer could not read the version list" def _check_nvidia_xfree86(entry: dict) -> dict: - """Report same-branch bugfix upgrades for the installed driver.""" + """Report the update the NVIDIA installer would recommend.""" current = entry.get("current_version") if not current or not re.match(r"^\d+\.\d+(\.\d+)?$", current): return {"available": False, "latest": None, "last_check": _now_iso(), "error": "no installed version"} - versions = _fetch_nvidia_versions() - if not versions: - return {"available": False, "latest": None, - "last_check": _now_iso(), - "error": "could not parse upstream version listing"} - - current_branch = current.split(".")[0] - same_branch = [v for v in versions if v.split(".")[0] == current_branch] - same_branch_latest = same_branch[0] if same_branch else None - - available = False - latest: Optional[str] = None - if same_branch_latest and \ - _version_tuple(same_branch_latest) > _version_tuple(current): - latest = same_branch_latest - available = True - + recommended, error = _nvidia_recommended_version(current) + available = bool(recommended) and _version_tuple(recommended) > _version_tuple(current) + same_branch = bool(recommended) and recommended.split(".")[0] == current.split(".")[0] return { "available": available, - "latest": latest, + "latest": recommended if available else None, "last_check": _now_iso(), - "error": None, - "_upgrade_kind": "patch" if available else None, + "error": error, + "_upgrade_kind": ("patch" if same_branch else "branch") if available else None, } @@ -1842,13 +1818,9 @@ def check_for_updates(force: bool = False) -> list[dict]: UI reads ``get_active_items()`` to render the inline "update available" line. - ``force`` invalidates the per-source caches (currently only the - NVIDIA versions list — OCI keeps its own internal cache). + ``force`` is kept for callers; the NVIDIA installer and OCI keep + their own caches. """ - if force: - _nvidia_cache["versions"] = [] - _nvidia_cache["fetched_at"] = 0 - updates_available: list[dict] = [] successful_lxc_checks: list[tuple[int, int]] = [] with _lock: diff --git a/AppImage/scripts/tests/test_openai_compatible_ai_models.py b/AppImage/scripts/tests/test_openai_compatible_ai_models.py index 96bceffa..1c8441a8 100644 --- a/AppImage/scripts/tests/test_openai_compatible_ai_models.py +++ b/AppImage/scripts/tests/test_openai_compatible_ai_models.py @@ -20,8 +20,9 @@ class CapturingOpenAIProvider(OpenAIProvider): super().__init__(*args, **kwargs) self.captured_payload = None - def _make_request(self, url, payload, headers): + def _make_request(self, url, payload, headers, timeout=15): self.captured_payload = payload + self.captured_timeout = timeout return {"choices": [{"message": {"content": "ok"}}]} @@ -119,6 +120,14 @@ class OpenAICompatibleModelTests(unittest.TestCase): self.assertNotIn("reasoning_effort", provider.captured_payload) self.assertNotIn("max_completion_tokens", provider.captured_payload) + def test_a_request_waits_as_long_as_the_monitor_allows_a_rewrite(self): + # A local model behind an OpenAI-compatible endpoint is slow; the + # request must not give up before the Monitor's own limit. + provider = CapturingOpenAIProvider(api_key="token", model="gemma-3-1b-it-Q4_K_M", + base_url="http://192.0.2.22:8080/v1") + provider.generate("system", "user", max_tokens=100) + self.assertEqual(provider.captured_timeout, notification_manager.AI_REWRITE_TIMEOUT_SECONDS) + def test_official_openai_reasoning_model_still_uses_reasoning_contract(self): provider = CapturingOpenAIProvider( api_key="token", diff --git a/lang/de.json b/lang/de.json index 08d6ad53..4af4b676 100644 --- a/lang/de.json +++ b/lang/de.json @@ -3596,6 +3596,7 @@ "Invalid check package:": "Ungültiges Kontrollpaket:", "Invalid configuration path:": "Ungültiger Konfigurationspfad:", "Invalid consume/export volumes": "Ungültige Verbrauchs-/Ausfuhrmengen", + "Invalid container device GID": "Ungültige Geräte-GID im Container", "Invalid container path:": "Ungültiger Containerpfad:", "Invalid credential file path:": "Ungültiger credential-Dateipfad:", "Invalid declarative entrypoint": "Ungültiger deklarativer Eingangspunkt", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; offizielles GPU-Bild)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (Selkies-Rendering und NVENC)", "NVIDIA Actions": "NVIDIA-Aktionen", "NVIDIA CPU hiding already configured": "NVIDIA-CPU-Ausblendung bereits konfiguriert", "NVIDIA Container Toolkit": "NVIDIA Container Toolkit", @@ -4454,6 +4456,7 @@ "New Search": "Neue Suche", "New VM:": "Neue VM:", "New Virtual Machine": "Neue virtuelle Maschine", + "New applications ({count})": "Neue Anwendungen ({count})", "New applications since your last visit: {count}": "Neue Anwendungen seit Ihrem letzten Besuch: {count}", "New backup job": "Neuer Sicherungsauftrag", "New backups on this host will be unencrypted until a new keyfile is set up.": "Neue Backups auf diesem Host werden unverschlüsselt sein, bis eine neue Schlüsseldatei eingerichtet wird.", @@ -7018,6 +7021,7 @@ "The container could not be removed automatically:": "Der Container konnte nicht automatisch entfernt werden:", "The container could not be started:": "Der Container konnte nicht gestartet werden:", "The container creation does not match the prepared instance": "Die Containererstellung stimmt nicht mit der vorbereiteten Instanz überein", + "The container device settings changed since the last recorded operation": "Die Geräteeinstellungen im Container haben sich seit dem letzten protokollierten Vorgang geändert", "The container devices do not match the saved record": "Die Containergeräte stimmen nicht mit dem gespeicherten Datensatz überein", "The container did not stop to update its persistent configuration:": "Der Container hat nicht angehalten, um seine persistente Konfiguration zu aktualisieren:", "The container did not stop; its disks are not touched": "Der Container hat nicht angehalten; seine Scheiben werden nicht berührt", @@ -7299,6 +7303,7 @@ "The record no longer belongs to this operation": "Der Datensatz gehört nicht mehr zu dieser operation", "The record of a member was replaced; the assembly is not resumed": "Die Aufzeichnung eines Mitglieds wurde ersetzt; die Versammlung wird nicht wieder aufgenommen", "The record or diagnosis could not be completed; no update was run.": "Die Aufzeichnung oder Diagnose konnte nicht abgeschlossen werden; kein Update wurde ausgeführt.", + "The recorded container device GID does not match its configuration": "Die protokollierte Geräte-GID im Container stimmt nicht mit der Konfiguration überein", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "Die Rückforderung war nicht abgeschlossen. Überprüfen Sie das Protokoll und wählen Sie erneut \"Wiederherstellen\" für diesen Container im OCI-Verwaltungsmenü.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "Die Wiederherstellung wurde nicht abgeschlossen. Prüfen Sie das Protokoll und wählen Sie für diesen Container im OCI-Verwaltungsmenü erneut \"{choice}\".", "The registry authentication file is invalid; it was not changed.": "Die Registrierungsauthentifizierungsdatei ist ungültig; es wurde nicht geändert.", diff --git a/lang/es.json b/lang/es.json index 4823976e..9d052db2 100644 --- a/lang/es.json +++ b/lang/es.json @@ -3592,6 +3592,7 @@ "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", + "Invalid container device GID": "El GID del dispositivo del contenedor no es válido", "Invalid container path:": "Carril de contenedor inválido:", "Invalid credential file path:": "Ruta de archivo credential no válida:", "Invalid declarative entrypoint": "Punto de entrada declarativo inválido", @@ -4308,6 +4309,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; imagen oficial para GPU)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (renderizado de Selkies y NVENC)", "NVIDIA Actions": "Acciones de NVIDIA", "NVIDIA CPU hiding already configured": "Ocultación de CPU NVIDIA ya configurada", "NVIDIA Container Toolkit": "Kit de herramientas de contenedor NVIDIA", @@ -4449,6 +4451,7 @@ "New Search": "Nueva búsqueda", "New VM:": "Nueva máquina virtual:", "New Virtual Machine": "Nueva máquina virtual", + "New applications ({count})": "Aplicaciones nuevas ({count})", "New applications since your last visit: {count}": "Aplicaciones nuevas desde tu última visita: {count}", "New backup job": "Nuevo trabajo de backup", "New backups on this host will be unencrypted until a new keyfile is set up.": "Los nuevos backups de este host no se cifrarán hasta que se configure un nuevo archivo de claves.", @@ -7000,6 +7003,7 @@ "The container could not be removed automatically:": "El contenedor no se puede retirar automáticamente:", "The container could not be started:": "No se podía iniciar el contenedor:", "The container creation does not match the prepared instance": "La creación de contenedores no coincide con la instancia preparada", + "The container device settings changed since the last recorded operation": "La configuración del dispositivo del contenedor ha cambiado desde la última operación registrada", "The container devices do not match the saved record": "Los dispositivos del contenedor no coinciden con el registro guardado", "The container did not stop to update its persistent configuration:": "El contenedor no se detuvo para actualizar su configuración persistente:", "The container did not stop; its disks are not touched": "El contenedor no se detuvo; sus discos no se tocan", @@ -7274,6 +7278,7 @@ "The record no longer belongs to this operation": "El registro ya no pertenece a esta operación", "The record of a member was replaced; the assembly is not resumed": "El registro de un miembro fue reemplazado; el ensamblado no se reanuda", "The record or diagnosis could not be completed; no update was run.": "El registro o el diagnóstico no se pudo completar; no se realizó ninguna actualización.", + "The recorded container device GID does not match its configuration": "El GID registrado del dispositivo del contenedor no coincide con su configuración", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "La recuperación no terminó. Revisa el log y elige \"Recuperar la instalación anterior\" de nuevo para este contenedor en el menú de gestión OCI.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "La recuperación no terminó. Revisa el log y elige \"{choice}\" de nuevo para este contenedor en el menú de gestión OCI.", "The registry authentication file is invalid; it was not changed.": "El fichero de autenticación del registro no es válido; no se ha modificado.", diff --git a/lang/fr.json b/lang/fr.json index 660c173d..5a4bf47e 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -3596,6 +3596,7 @@ "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", + "Invalid container device GID": "GID de périphérique du conteneur invalide", "Invalid container path:": "Voie du conteneur non valide:", "Invalid credential file path:": "Chemin de fichier credential incorrect :", "Invalid declarative entrypoint": "Point de déclaration non valide", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; image officielle du GPU)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (rendu Selkies et NVENC)", "NVIDIA Actions": "Actions NVIDIA", "NVIDIA CPU hiding already configured": "Le masquage du processeur NVIDIA est déjà configuré", "NVIDIA Container Toolkit": "Boîte à outils de conteneur NVIDIA", @@ -4454,6 +4456,7 @@ "New Search": "Nouvelle recherche", "New VM:": "Nouvelle VM :", "New Virtual Machine": "Nouvelle machine virtuelle", + "New applications ({count})": "Nouvelles applications ({count})", "New applications since your last visit: {count}": "Nouvelles applications depuis votre dernière visite : {count}", "New backup job": "Nouveau travail de sauvegarde", "New backups on this host will be unencrypted until a new keyfile is set up.": "Les nouvelles sauvegardes sur cet hôte ne seront pas chiffrées jusqu'à ce qu'un nouveau fichier de clés soit configuré.", @@ -7017,6 +7020,7 @@ "The container could not be removed automatically:": "Le conteneur n'a pas pu être enlevé automatiquement:", "The container could not be started:": "Le conteneur n'a pas pu être démarré:", "The container creation does not match the prepared instance": "La création du conteneur ne correspond pas à l'instance préparée", + "The container device settings changed since the last recorded operation": "La configuration du périphérique du conteneur a changé depuis la dernière opération enregistrée", "The container devices do not match the saved record": "Les périphériques de conteneur ne correspondent pas à l'enregistrement enregistré", "The container did not stop to update its persistent configuration:": "Le conteneur ne s'est pas arrêté pour mettre à jour sa configuration persistante:", "The container did not stop; its disks are not touched": "Le conteneur ne s'est pas arrêté; ses disques ne sont pas touchés", @@ -7298,6 +7302,7 @@ "The record no longer belongs to this operation": "Le record n'appartient plus à cette operation", "The record of a member was replaced; the assembly is not resumed": "Le procès-verbal d'un membre a été remplacé; l'assemblée n'est pas reprise", "The record or diagnosis could not be completed; no update was run.": "Le dossier ou le diagnostic n'a pu être complété; aucune mise à jour n'a été effectuée.", + "The recorded container device GID does not match its configuration": "Le GID enregistré du périphérique du conteneur ne correspond pas à sa configuration", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "La récupération n'est pas terminée. Passez en revue le journal et choisissez « Recover » pour ce conteneur dans le menu de gestion OCI.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "La restauration ne s'est pas terminée. Consultez le journal et choisissez de nouveau « {choice} » pour ce conteneur dans le menu de gestion OCI.", "The registry authentication file is invalid; it was not changed.": "Le fichier d'authentification du registre n'est pas valide ; cela n'a pas été changé.", diff --git a/lang/it.json b/lang/it.json index be964a45..3d3811ad 100644 --- a/lang/it.json +++ b/lang/it.json @@ -3596,6 +3596,7 @@ "Invalid check package:": "Pacchetto controllo non valido:", "Invalid configuration path:": "Percorso configurazione non valido:", "Invalid consume/export volumes": "Volumi di acquisizione/esportazione non validi", + "Invalid container device GID": "GID del dispositivo del container non valido", "Invalid container path:": "Percorso nel container non valido:", "Invalid credential file path:": "Percorso file credenziali non valido:", "Invalid declarative entrypoint": "Entrypoint dichiarativo non valido", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; immagine GPU ufficiale)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (rendering Selkies e NVENC)", "NVIDIA Actions": "Azioni NVIDIA", "NVIDIA CPU hiding already configured": "Occultamento CPU per NVIDIA già configurato", "NVIDIA Container Toolkit": "NVIDIA Container Toolkit", @@ -4454,6 +4456,7 @@ "New Search": "Nuova ricerca", "New VM:": "Nuova VM:", "New Virtual Machine": "Nuova VM", + "New applications ({count})": "Nuove applicazioni ({count})", "New applications since your last visit: {count}": "Nuove applicazioni dalla tua ultima visita: {count}", "New backup job": "Nuova attività di backup", "New backups on this host will be unencrypted until a new keyfile is set up.": "I nuovi backup su questo host non saranno crittografati finché non verrà impostato un nuovo file di chiavi.", @@ -7018,6 +7021,7 @@ "The container could not be removed automatically:": "Impossibile rimuovere automaticamente il container:", "The container could not be started:": "Impossibile avviare il container:", "The container creation does not match the prepared instance": "La creazione del container non corrisponde all'istanza preparata", + "The container device settings changed since the last recorded operation": "Le impostazioni del dispositivo del container sono cambiate dall’ultima operazione registrata", "The container devices do not match the saved record": "I dispositivi del container non corrispondono al record salvato", "The container did not stop to update its persistent configuration:": "Il container non si è arrestato per aggiornare la configurazione persistente:", "The container did not stop; its disks are not touched": "Il container non si è arrestato; i suoi dischi non vengono toccati", @@ -7299,6 +7303,7 @@ "The record no longer belongs to this operation": "Il record non appartiene più a questa operazione", "The record of a member was replaced; the assembly is not resumed": "Il record di un componente è stato sostituito; la composizione dello stack non riprende", "The record or diagnosis could not be completed; no update was run.": "Non è stato possibile completare la registrazione o la diagnosi; non è stato eseguito alcun aggiornamento.", + "The recorded container device GID does not match its configuration": "Il GID registrato del dispositivo del container non corrisponde alla sua configurazione", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "Il recupero non è completo. Rivedere il registro e scegliere \"Recuperare\" di nuovo per questo contenitore nel menu di gestione OCI.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "Il ripristino non è stato completato. Controllare il log e scegliere di nuovo \"{choice}\" per questo container nel menu di gestione OCI.", "The registry authentication file is invalid; it was not changed.": "Il file di autenticazione del registro non è valido; non è stato modificato.", diff --git a/lang/pt.json b/lang/pt.json index a21f5998..f7a05c14 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -3596,6 +3596,7 @@ "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", + "Invalid container device GID": "GID do dispositivo do contentor inválido", "Invalid container path:": "Localização do contentor inválida:", "Invalid credential file path:": "Caminho do arquivo credential inválido:", "Invalid declarative entrypoint": "Ponto de entrada declarativo inválido", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; imagem oficial da GPU)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (renderização Selkies e NVENC)", "NVIDIA Actions": "Ações NVIDIA", "NVIDIA CPU hiding already configured": "Ocultação de CPU NVIDIA já configurada", "NVIDIA Container Toolkit": "NVIDIA Container Toolkit", @@ -4454,6 +4456,7 @@ "New Search": "Nova pesquisa", "New VM:": "Nova VM:", "New Virtual Machine": "Nova máquina virtual", + "New applications ({count})": "Novas aplicações ({count})", "New applications since your last visit: {count}": "Novas aplicações desde a sua última visita: {count}", "New backup job": "Nova tarefa de backup", "New backups on this host will be unencrypted until a new keyfile is set up.": "Novos backups neste host serão descriptografados até que um novo arquivo-chave seja configurado.", @@ -7018,6 +7021,7 @@ "The container could not be removed automatically:": "O recipiente não pôde ser removido automaticamente:", "The container could not be started:": "Não foi possível iniciar o recipiente:", "The container creation does not match the prepared instance": "A criação do recipiente não corresponde à instância preparada", + "The container device settings changed since the last recorded operation": "As definições do dispositivo do contentor mudaram desde a última operação registada", "The container devices do not match the saved record": "Os dispositivos do contentor não correspondem ao registo gravado", "The container did not stop to update its persistent configuration:": "O recipiente não parou para atualizar sua configuração persistente:", "The container did not stop; its disks are not touched": "O recipiente não parou; os seus discos não são tocados", @@ -7299,6 +7303,7 @@ "The record no longer belongs to this operation": "O registro já não pertence a esta operation", "The record of a member was replaced; the assembly is not resumed": "O registro de um membro foi substituído; a assembléia não é retomada", "The record or diagnosis could not be completed; no update was run.": "O registro ou diagnóstico não pôde ser concluído; nenhuma atualização foi realizada.", + "The recorded container device GID does not match its configuration": "O GID registado do dispositivo do contentor não corresponde à configuração", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "A recuperação não terminou. Reveja o log e escolha \"Recover\" novamente para este recipiente no menu de gerenciamento OCI.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "A recuperação não foi concluída. Revise o log e escolha \"{choice}\" novamente para este contêiner no menu de gerenciamento OCI.", "The registry authentication file is invalid; it was not changed.": "O arquivo de autenticação do registro é inválido;não foi alterado.", diff --git a/lang/sk.json b/lang/sk.json index 5920f1e8..6235032e 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -3596,6 +3596,7 @@ "Invalid check package:": "Neplatný kontrolný balík:", "Invalid configuration path:": "Neplatná konfiguračná dráha:", "Invalid consume/export volumes": "Neplatné dátové zväzky consume a export", + "Invalid container device GID": "Neplatné GID zariadenia v kontajneri", "Invalid container path:": "Neplatná trasa kontajnera:", "Invalid credential file path:": "Neplatná cesta súboru s prihlasovacími údajmi:", "Invalid declarative entrypoint": "Neplatný deklaratívny vstupný bod", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA; oficiálny obraz GPU)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (vykresľovanie Selkies a NVENC)", "NVIDIA Actions": "Akcie pre NVIDIA", "NVIDIA CPU hiding already configured": "NVIDIA CPU skrytie už je nastavené", "NVIDIA Container Toolkit": "NVIDIA Container Toolkit", @@ -4454,6 +4456,7 @@ "New Search": "Nové hľadanie", "New VM:": "Nová VM:", "New Virtual Machine": "Nová virtuálna mašina", + "New applications ({count})": "Nové aplikácie ({count})", "New applications since your last visit: {count}": "Nové aplikácie od vašej poslednej návštevy: {count}", "New backup job": "Nová úloha zálohy", "New backups on this host will be unencrypted until a new keyfile is set up.": "Nové zálohy na tomto hostovi budú nešifrované, kým nenastavíte nový keyfile.", @@ -7019,6 +7022,7 @@ "The container could not be removed automatically:": "Kontajner sa nepodarilo automaticky odstrániť:", "The container could not be started:": "Kontajner sa nepodarilo spustiť:", "The container creation does not match the prepared instance": "Vytvorenie kontajnera nezodpovedá pripravenej inštancii", + "The container device settings changed since the last recorded operation": "Nastavenia zariadenia v kontajneri sa od poslednej zaznamenanej operácie zmenili", "The container devices do not match the saved record": "Zariadenie kontajnera nezodpovedá uloženému záznamu", "The container did not stop to update its persistent configuration:": "Kontajner sa nezastavil na aktualizáciu svojich trvalých nastavení:", "The container did not stop; its disks are not touched": "Kontajner sa nezastavil; jeho disky nie sú dotknuté", @@ -7300,6 +7304,7 @@ "The record no longer belongs to this operation": "Záznam už nepatrí tejto operácii", "The record of a member was replaced; the assembly is not resumed": "Záznam kontajnera bol nahradený; zostavovanie nebude pokračovať", "The record or diagnosis could not be completed; no update was run.": "Záznam alebo diagnóza nemohla byť dokončená; žiadna aktualizácia nebola spustená.", + "The recorded container device GID does not match its configuration": "Zaznamenané GID zariadenia v kontajneri sa nezhoduje s jeho konfiguráciou", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "Obnova sa nedokončila. Skontrolujte záznam a v ponuke Správca OCI aplikácií pri tomto kontajneri znova vyberte \"Obnoviť\".", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "Obnova sa nedokončila. Skontrolujte záznam a v ponuke Správca OCI aplikácií pri tomto kontajneri znova vyberte \"{choice}\".", "The registry authentication file is invalid; it was not changed.": "Súbor s prihlasovacími údajmi registrov je neplatný; nebol zmenený.", diff --git a/lang/sv.json b/lang/sv.json index 5efd8fdf..d4345ae9 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -3596,6 +3596,7 @@ "Invalid check package:": "Ogiltigt checkpaket:", "Invalid configuration path:": "Ogiltig konfigurationsväg:", "Invalid consume/export volumes": "Ogiltiga konsum/exportvolymer", + "Invalid container device GID": "Ogiltigt enhets-GID i containern", "Invalid container path:": "Invalid containerväg:", "Invalid credential file path:": "Invalid credential filväg:", "Invalid declarative entrypoint": "Invalid deklarativ ingång", @@ -4313,6 +4314,7 @@ "NVIDIA (CUDA; official GPU image)": "NVIDIA (CUDA, officiell GPU-bild)", "NVIDIA (NVDEC/CUDA)": "NVIDIA (NVDEC/CUDA)", "NVIDIA (NVENC/NVDEC)": "NVIDIA (NVENC/NVDEC)", + "NVIDIA (Selkies rendering and NVENC)": "NVIDIA (Selkies-rendering och NVENC)", "NVIDIA Actions": "NVIDIA-åtgärder", "NVIDIA CPU hiding already configured": "NVIDIA CPU-döljning har redan konfigurerats", "NVIDIA Container Toolkit": "NVIDIA Container Toolkit", @@ -4454,6 +4456,7 @@ "New Search": "Ny sökning", "New VM:": "Ny VM:", "New Virtual Machine": "Ny virtuell maskin", + "New applications ({count})": "Nya program ({count})", "New applications since your last visit: {count}": "Nya appar sedan ditt senaste besök: {count}", "New backup job": "Nytt säkerhetskopia jobb", "New backups on this host will be unencrypted until a new keyfile is set up.": "Nya säkerhetskopior på denna värd kommer att vara okrypterade tills en ny nyckelfil ställs in.", @@ -7018,6 +7021,7 @@ "The container could not be removed automatically:": "Behållaren kunde inte tas bort automatiskt:", "The container could not be started:": "Behållaren kunde inte startas:", "The container creation does not match the prepared instance": "Behållarens skapelse matchar inte den förberedda instansen", + "The container device settings changed since the last recorded operation": "Inställningarna för enheten i containern har ändrats sedan den senaste registrerade åtgärden", "The container devices do not match the saved record": "Container-enheterna matchar inte det sparade rekordet", "The container did not stop to update its persistent configuration:": "Behållaren slutade inte uppdatera sin ihållande konfiguration:", "The container did not stop; its disks are not touched": "Behållaren slutade inte; dess diskar rörs inte", @@ -7299,6 +7303,7 @@ "The record no longer belongs to this operation": "Skivan tillhör inte längre denna operation", "The record of a member was replaced; the assembly is not resumed": "En medlems rekord ersattes; församlingen återupptas inte.", "The record or diagnosis could not be completed; no update was run.": "Rekordet eller diagnosen kunde inte slutföras; ingen uppdatering kördes.", + "The recorded container device GID does not match its configuration": "Det registrerade enhets-GID:t i containern stämmer inte med konfigurationen", "The recovery did not complete. Review the log and choose \"Recover\" again for this container in the OCI management menu.": "Återhämtningen slutfördes inte. Granska loggen och välj \"Recover\" igen för den här behållaren i OCI-hanteringsmenyn.", "The recovery did not complete. Review the log and choose \"{choice}\" again for this container in the OCI management menu.": "Återställningen slutfördes inte. Granska loggen och välj \"{choice}\" igen för den här containern i OCI-hanteringsmenyn.", "The registry authentication file is invalid; it was not changed.": "Registerautentiseringsfilen är ogiltig; det ändrades inte.", diff --git a/oci/README.md b/oci/README.md index 78a008c6..fa91bf13 100644 --- a/oci/README.md +++ b/oci/README.md @@ -111,6 +111,8 @@ Selecting an application, or giving a definition, opens the same sequence. 4. **Resolve.** The registry is queried, the architecture of the host is selected and the digest the tag points to at that moment is fixed. 5. **Download and verify.** Skopeo downloads the image as an OCI archive. Every layer is checked against its digest before anything is created. 6. **Build.** `pct create` builds the rootfs from the archive and imports `Entrypoint`, `Cmd`, `Env`, `User`, `WorkingDir` and `StopSignal` from the image. + + Proxmox reads the image inside the container's user namespace. Readable template-storage archives use the existing path without a copy. If a verified archive or its parents are private, the native create wrapper makes a temporary root-owned copy under `/var/tmp`, readable only by the mapped root group, and removes it after creation, failure or a catchable interruption. The cached image, storage directories, registry credentials and caller permissions stay unchanged. Reflinks are used where available; otherwise this uncommon path needs temporary space for one image copy. Only the `pct create` child receives the normal Proxmox creation umask. 7. **Connect.** The volumes are added as mount points, then the devices, the environment, the network and the security settings the oci-compose declares. 8. **Prepare.** Each new volume is made ready for the first start: `lost+found` is removed and the directory is given to the user the application writes with. The files the oci-compose asks to write are written. 9. **Start and check.** The container starts, an address is waited for, the settings that need the running application are applied, and the service is checked. A failed first start is reported as a failed installation. diff --git a/oci/catalog/apps/cadcraft.json b/oci/catalog/apps/cadcraft.json index f17e3aa0..83751f84 100644 --- a/oci/catalog/apps/cadcraft.json +++ b/oci/catalog/apps/cadcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/deckcraft.json b/oci/catalog/apps/deckcraft.json index 9e49abf6..ec0f479f 100644 --- a/oci/catalog/apps/deckcraft.json +++ b/oci/catalog/apps/deckcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/designcraft.json b/oci/catalog/apps/designcraft.json index 1b29acab..816524c8 100644 --- a/oci/catalog/apps/designcraft.json +++ b/oci/catalog/apps/designcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/effectcraft.json b/oci/catalog/apps/effectcraft.json index 83fc3e8d..f2b6ed37 100644 --- a/oci/catalog/apps/effectcraft.json +++ b/oci/catalog/apps/effectcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/figcraft.json b/oci/catalog/apps/figcraft.json index 8ec545a0..80644eb3 100644 --- a/oci/catalog/apps/figcraft.json +++ b/oci/catalog/apps/figcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/filmcraft.json b/oci/catalog/apps/filmcraft.json index 184c4169..8289da25 100644 --- a/oci/catalog/apps/filmcraft.json +++ b/oci/catalog/apps/filmcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/gridcraft.json b/oci/catalog/apps/gridcraft.json index 6d7c9a0a..6920328e 100644 --- a/oci/catalog/apps/gridcraft.json +++ b/oci/catalog/apps/gridcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/lightcraft.json b/oci/catalog/apps/lightcraft.json index 3a9e20ab..afc0582e 100644 --- a/oci/catalog/apps/lightcraft.json +++ b/oci/catalog/apps/lightcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/pdfcraft.json b/oci/catalog/apps/pdfcraft.json index 9d512156..57d452a1 100644 --- a/oci/catalog/apps/pdfcraft.json +++ b/oci/catalog/apps/pdfcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/photocraft.json b/oci/catalog/apps/photocraft.json index 57ea1fc1..7d95f12f 100644 --- a/oci/catalog/apps/photocraft.json +++ b/oci/catalog/apps/photocraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/soundcraft.json b/oci/catalog/apps/soundcraft.json index 9eb74e04..1ad3ac3c 100644 --- a/oci/catalog/apps/soundcraft.json +++ b/oci/catalog/apps/soundcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/stirling-pdf.json b/oci/catalog/apps/stirling-pdf.json index 089af2e4..5798e921 100644 --- a/oci/catalog/apps/stirling-pdf.json +++ b/oci/catalog/apps/stirling-pdf.json @@ -44,9 +44,9 @@ "provider": "stirling-pdf", "repository": "https://github.com/Stirling-Tools/Stirling-PDF", "revision": "e7f6b5fe482945fca3527cc2f4035df717a526cf26c1ebd3e5a272ec140132f0", - "image_repository_url": "https://docker.stirlingpdf.com/stirlingtools/stirling-pdf", + "image_repository_url": "https://ghcr.io/stirling-tools/stirling-pdf", "readme_pushed_at": "", - "compose_sha256": "6fa4793bff33f54b73a99364f26f3b69313a7c39dcd787c836d4f01f75e55146", + "compose_sha256": "9c68478ad9b0d9c9ffdc29917de8d33afbaae66a703daa4c02e0c148ecfb8a10", "generated_at": "2026-10-25T16:06:01+00:00", "readme_raw_url": "https://docs.stirlingpdf.com/Server/Install/Docker/" }, @@ -54,9 +54,9 @@ "service_name": "stirling-pdf", "container_name": "stirling-pdf", "image": { - "reference": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", - "registry": "docker.stirlingpdf.com", - "repository": "docker.stirlingpdf.com/stirlingtools/stirling-pdf", + "reference": "ghcr.io/stirling-tools/stirling-pdf:latest", + "registry": "ghcr.io", + "repository": "ghcr.io/stirling-tools/stirling-pdf", "tag": "latest", "digest": null, "pull_policy": "resolve-selected-tag-to-architecture-digest-at-install" @@ -123,7 +123,7 @@ "related_services": [], "restart": "unless-stopped", "stop_grace_period": null, - "original_compose": "{\n \"name\": \"stirling-pdf\",\n \"services\": {\n \"stirling-pdf\": {\n \"image\": \"docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"stirling-config:/configs\",\n \"stirling-storage:/storage\"\n ],\n \"environment\": {\n \"PUID\": \"1000\",\n \"PGID\": \"1000\"\n },\n \"deploy\": {\n \"resources\": {\n \"limits\": {\n \"memory\": \"4g\"\n }\n }\n }\n }\n }\n}" + "original_compose": "{\n \"name\": \"stirling-pdf\",\n \"services\": {\n \"stirling-pdf\": {\n \"image\": \"ghcr.io/stirling-tools/stirling-pdf:latest\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"stirling-config:/configs\",\n \"stirling-storage:/storage\"\n ],\n \"environment\": {\n \"PUID\": \"1000\",\n \"PGID\": \"1000\"\n },\n \"deploy\": {\n \"resources\": {\n \"limits\": {\n \"memory\": \"4g\"\n }\n }\n }\n }\n }\n}" }, "compose_stack": { "project_name": "stirling-pdf", @@ -134,7 +134,7 @@ "services": [ { "name": "stirling-pdf", - "image": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", + "image": "ghcr.io/stirling-tools/stirling-pdf:latest", "is_main": true, "role": "frontend", "vmid_offset": 0, @@ -142,7 +142,7 @@ "frontend_network": true, "private_network": false, "compose": { - "image": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", + "image": "ghcr.io/stirling-tools/stirling-pdf:latest", "restart": "unless-stopped", "ports": [ "8080:8080" diff --git a/oci/catalog/apps/vectorcraft.json b/oci/catalog/apps/vectorcraft.json index 29db86d6..20ae3977 100644 --- a/oci/catalog/apps/vectorcraft.json +++ b/oci/catalog/apps/vectorcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/apps/wordcraft.json b/oci/catalog/apps/wordcraft.json index 7e0c6602..df437c08 100644 --- a/oci/catalog/apps/wordcraft.json +++ b/oci/catalog/apps/wordcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/cadcraft.json b/oci/catalog/curated/cadcraft.json index f17e3aa0..83751f84 100644 --- a/oci/catalog/curated/cadcraft.json +++ b/oci/catalog/curated/cadcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/deckcraft.json b/oci/catalog/curated/deckcraft.json index 9e49abf6..ec0f479f 100644 --- a/oci/catalog/curated/deckcraft.json +++ b/oci/catalog/curated/deckcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/designcraft.json b/oci/catalog/curated/designcraft.json index 1b29acab..816524c8 100644 --- a/oci/catalog/curated/designcraft.json +++ b/oci/catalog/curated/designcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/effectcraft.json b/oci/catalog/curated/effectcraft.json index 83fc3e8d..f2b6ed37 100644 --- a/oci/catalog/curated/effectcraft.json +++ b/oci/catalog/curated/effectcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/figcraft.json b/oci/catalog/curated/figcraft.json index 8ec545a0..80644eb3 100644 --- a/oci/catalog/curated/figcraft.json +++ b/oci/catalog/curated/figcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/filmcraft.json b/oci/catalog/curated/filmcraft.json index 184c4169..8289da25 100644 --- a/oci/catalog/curated/filmcraft.json +++ b/oci/catalog/curated/filmcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/gridcraft.json b/oci/catalog/curated/gridcraft.json index 6d7c9a0a..6920328e 100644 --- a/oci/catalog/curated/gridcraft.json +++ b/oci/catalog/curated/gridcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/lightcraft.json b/oci/catalog/curated/lightcraft.json index 3a9e20ab..afc0582e 100644 --- a/oci/catalog/curated/lightcraft.json +++ b/oci/catalog/curated/lightcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/pdfcraft.json b/oci/catalog/curated/pdfcraft.json index 9d512156..57d452a1 100644 --- a/oci/catalog/curated/pdfcraft.json +++ b/oci/catalog/curated/pdfcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/photocraft.json b/oci/catalog/curated/photocraft.json index 57ea1fc1..7d95f12f 100644 --- a/oci/catalog/curated/photocraft.json +++ b/oci/catalog/curated/photocraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/soundcraft.json b/oci/catalog/curated/soundcraft.json index 9eb74e04..1ad3ac3c 100644 --- a/oci/catalog/curated/soundcraft.json +++ b/oci/catalog/curated/soundcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/stirling-pdf.json b/oci/catalog/curated/stirling-pdf.json index 089af2e4..5798e921 100644 --- a/oci/catalog/curated/stirling-pdf.json +++ b/oci/catalog/curated/stirling-pdf.json @@ -44,9 +44,9 @@ "provider": "stirling-pdf", "repository": "https://github.com/Stirling-Tools/Stirling-PDF", "revision": "e7f6b5fe482945fca3527cc2f4035df717a526cf26c1ebd3e5a272ec140132f0", - "image_repository_url": "https://docker.stirlingpdf.com/stirlingtools/stirling-pdf", + "image_repository_url": "https://ghcr.io/stirling-tools/stirling-pdf", "readme_pushed_at": "", - "compose_sha256": "6fa4793bff33f54b73a99364f26f3b69313a7c39dcd787c836d4f01f75e55146", + "compose_sha256": "9c68478ad9b0d9c9ffdc29917de8d33afbaae66a703daa4c02e0c148ecfb8a10", "generated_at": "2026-10-25T16:06:01+00:00", "readme_raw_url": "https://docs.stirlingpdf.com/Server/Install/Docker/" }, @@ -54,9 +54,9 @@ "service_name": "stirling-pdf", "container_name": "stirling-pdf", "image": { - "reference": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", - "registry": "docker.stirlingpdf.com", - "repository": "docker.stirlingpdf.com/stirlingtools/stirling-pdf", + "reference": "ghcr.io/stirling-tools/stirling-pdf:latest", + "registry": "ghcr.io", + "repository": "ghcr.io/stirling-tools/stirling-pdf", "tag": "latest", "digest": null, "pull_policy": "resolve-selected-tag-to-architecture-digest-at-install" @@ -123,7 +123,7 @@ "related_services": [], "restart": "unless-stopped", "stop_grace_period": null, - "original_compose": "{\n \"name\": \"stirling-pdf\",\n \"services\": {\n \"stirling-pdf\": {\n \"image\": \"docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"stirling-config:/configs\",\n \"stirling-storage:/storage\"\n ],\n \"environment\": {\n \"PUID\": \"1000\",\n \"PGID\": \"1000\"\n },\n \"deploy\": {\n \"resources\": {\n \"limits\": {\n \"memory\": \"4g\"\n }\n }\n }\n }\n }\n}" + "original_compose": "{\n \"name\": \"stirling-pdf\",\n \"services\": {\n \"stirling-pdf\": {\n \"image\": \"ghcr.io/stirling-tools/stirling-pdf:latest\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"stirling-config:/configs\",\n \"stirling-storage:/storage\"\n ],\n \"environment\": {\n \"PUID\": \"1000\",\n \"PGID\": \"1000\"\n },\n \"deploy\": {\n \"resources\": {\n \"limits\": {\n \"memory\": \"4g\"\n }\n }\n }\n }\n }\n}" }, "compose_stack": { "project_name": "stirling-pdf", @@ -134,7 +134,7 @@ "services": [ { "name": "stirling-pdf", - "image": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", + "image": "ghcr.io/stirling-tools/stirling-pdf:latest", "is_main": true, "role": "frontend", "vmid_offset": 0, @@ -142,7 +142,7 @@ "frontend_network": true, "private_network": false, "compose": { - "image": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", + "image": "ghcr.io/stirling-tools/stirling-pdf:latest", "restart": "unless-stopped", "ports": [ "8080:8080" diff --git a/oci/catalog/curated/vectorcraft.json b/oci/catalog/curated/vectorcraft.json index 29db86d6..20ae3977 100644 --- a/oci/catalog/curated/vectorcraft.json +++ b/oci/catalog/curated/vectorcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/curated/wordcraft.json b/oci/catalog/curated/wordcraft.json index 7e0c6602..df437c08 100644 --- a/oci/catalog/curated/wordcraft.json +++ b/oci/catalog/curated/wordcraft.json @@ -41,8 +41,8 @@ "Use a trusted HTTPS reverse proxy for browser access. Port 3001 uses the upstream self-signed certificate; port 3000 is for an HTTP reverse-proxy backend.", "The upstream default has no login. Optional CUSTOM_USER and PASSWORD variables enable HTTP basic authentication; do not expose an unauthenticated desktop to untrusted networks.", "/config holds settings and desktop state and is included in Proxmox backups. /storage holds documents and supports a managed volume or an explicitly selected host directory; host directories require separate backups.", - "The default uses software rendering. GPU acceleration and other CPU architectures are not claimed tested by a CPU-only amd64 installation.", - "Without an assigned GPU, an application may ask to switch to CPU rendering. Choose the CPU option; hardware acceleration is not required for the default configuration." + "The default installation draws on the CPU. The advanced installation can assign an Intel or AMD GPU, which the desktop, the application and the video stream then use; NVIDIA is offered when the host has its driver and the NVIDIA Container Toolkit.", + "Without a GPU, an application may ask to switch to CPU rendering. Choose the CPU option; the application remembers it." ], "mini_changelog": [], "display_version": null, @@ -112,6 +112,14 @@ "required": false, "sensitive": true, "source": "upstream-documentation" + }, + { + "name": "LC_ALL", + "example": "", + "required": false, + "sensitive": false, + "source": "upstream-documentation", + "prompt": "Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)" } ], "volumes": [ @@ -365,6 +373,111 @@ "stability_seconds": 8, "accept_any_status": true, "verify_tls": true + }, + "selkies": { + "base": "FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute@sha256:6efa3fd775a552938d862f75975d34c418ad26d028ba2c952da58682cfe1cb47", + "dockerfile_url": "https://raw.githubusercontent.com/junkerderprovinz/artcraft-apps/32bee0dea9bdaf5310c693210bea0d732a129ae8/Dockerfile", + "dockerfile_sha256": "69ab1a81d1696d1bd143a6fa5a121c034ddfb3e83008969340e8cd2413547fe1", + "reviewed_on": "2026-11-04", + "documentation": "https://docs.linuxserver.io/images/docker-baseimage-selkies/", + "nvidia": "available-in-advanced-mode-with-compatible-host-driver-and-toolkit", + "validation": "Intel/AMD tested on a Radeon 680M: GL compositor and VAAPI encoding on the render node" + }, + "device_requests": [], + "optional_devices": [], + "hardware_acceleration": { + "prompt": "Selkies desktop and streaming acceleration", + "default": "none", + "profiles": [ + { + "id": "none", + "label": "No GPU (CPU)", + "device_requests": [], + "environment": [ + { + "name": "AUTO_GPU", + "value": "false" + } + ] + }, + { + "id": "vaapi", + "label": "Intel/AMD (streaming rendering and encoding)", + "device_requests": [ + { + "id": "selkies-render", + "kind": "character-device", + "path_prompt": "Intel/AMD render node", + "host_path_default": "/dev/dri/renderD128", + "container_path_strategy": "same-as-host", + "mode": "0660", + "deny_write": false, + "gid_strategy": "host-device-gid", + "drm_vendor_ids": [ + "0x8086", + "0x1002" + ] + } + ], + "environment": [ + { + "name": "PIXELFLUX_WAYLAND", + "value": "true" + }, + { + "name": "AUTO_GPU", + "value": "false" + } + ], + "environment_from_devices": { + "DRINODE": [ + "selkies-render" + ], + "DRI_NODE": [ + "selkies-render" + ], + "ATTACHED_DEVICES_PERMS": [ + "selkies-render" + ] + } + }, + { + "id": "nvidia", + "label": "NVIDIA (Selkies rendering and NVENC)", + "device_requests": [ + { + "id": "selkies-nvidia", + "kind": "nvidia-runtime", + "device_selection": "all-requested-by-compose" + } + ], + "environment": [ + { + "name": "AUTO_GPU", + "value": "true" + }, + { + "name": "NVIDIA_VISIBLE_DEVICES", + "value": "all" + }, + { + "name": "NVIDIA_DRIVER_CAPABILITIES", + "value": "compute,video,graphics,utility,display" + } + ] + } + ] + }, + "gpu_validation": { + "device_inventory": "host-sysfs-and-stat", + "application_acceleration": "requires-workload-test", + "tone_mapping": "not-implied-by-device-access" + }, + "device_permissions": { + "strategy": "linuxserver-native-init", + "service_user": "abc", + "environment": "ATTACHED_DEVICES_PERMS", + "paths": "all-resolved-selected-character-devices" } }, "adaptations": [ diff --git a/oci/catalog/index.json b/oci/catalog/index.json index 7c3387a4..98577d45 100644 --- a/oci/catalog/index.json +++ b/oci/catalog/index.json @@ -15593,7 +15593,7 @@ "arm64" ], "updated_at": null, - "main_image": "docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest", + "main_image": "ghcr.io/stirling-tools/stirling-pdf:latest", "category": "documents", "category_label": "Documents & Notes", "replaces_discovered_ids": [], diff --git a/oci/remote/install_generic_stack.py b/oci/remote/install_generic_stack.py index 3d2ed373..8debe0a5 100644 --- a/oci/remote/install_generic_stack.py +++ b/oci/remote/install_generic_stack.py @@ -14,11 +14,13 @@ import os from pathlib import Path import re import socket +import ssl import subprocess import sys import tempfile import time import urllib.request +import urllib.error import urllib.parse import uuid import xml.etree.ElementTree as ET @@ -662,24 +664,35 @@ def attach_mounts(service, temporary): oci_nested_mounts.follow(vmid) -def web_answers(vmid, url, opener): +def web_answers(vmid, url, opener, verify_tls=False): from oci_installation_state import container_curl curl = container_curl(vmid, lambda *args: run(*args).encode()) if curl != ['curl']: - run(*curl, '-fsS', '--noproxy', '*', '-o', '/dev/null', '--max-time', '4', url) + tls_args = [] if verify_tls else ['--insecure'] + run(*curl, '-fsS', '--noproxy', '*', '-o', '/dev/null', '--max-time', '4', *tls_args, url) return True - with opener.open(url,timeout=4) as response: - return response.status<400 + try: + with opener.open(url,timeout=4) as response: + return response.status<400 + except urllib.error.HTTPError as error: + error.close() + raise def wait_web(primary, url): deadline = time.monotonic()+primary['healthcheck']['timeout_seconds'] - opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + # Match the single-container installer and recovery probes: image-generated + # certificates are valid for local readiness, with strict TLS available. + verify_tls = primary['healthcheck'].get('verify_tls', False) + handlers = [urllib.request.ProxyHandler({})] + if not verify_tls: + handlers.append(urllib.request.HTTPSHandler(context=ssl._create_unverified_context())) + opener = urllib.request.build_opener(*handlers) while True: if run('pct','status',primary['vmid']).strip()!='status: running': raise RuntimeError(f"{translate('The container stopped:')} {primary['name']} (CT {primary['vmid']})") try: - if web_answers(primary['vmid'], url, opener): + if web_answers(primary['vmid'], url, opener, verify_tls=verify_tls): check = primary['healthcheck'] if check.get('type') == 'exec': remaining = max(1, deadline-time.monotonic()) diff --git a/oci/remote/install_oci.sh b/oci/remote/install_oci.sh index dd93e862..99682a82 100755 --- a/oci/remote/install_oci.sh +++ b/oci/remote/install_oci.sh @@ -358,13 +358,13 @@ cleanup_canonical_inputs() { # A derived check accepts any HTTP answer below 500: the application is up, # even when its first page is a redirect or asks for a login. -healthcheck_probe() { - local code status=0 +healthcheck_probe_url() { + local url=$1 code status=0 if [[ ${HC_ANY_STATUS:-false} == true ]]; then - code=$(oci_curl "$VMID" "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null) || status=$? + code=$(oci_curl "$VMID" "${CURL_ARGS[@]}" "$url" 2>/dev/null) || status=$? (( status == 0 )) && [[ $code =~ ^[1-4][0-9][0-9]$ ]] && return 0 else - oci_curl "$VMID" "${CURL_ARGS[@]}" "$HC_URL" >/dev/null 2>&1 || status=$? + oci_curl "$VMID" "${CURL_ARGS[@]}" "$url" >/dev/null 2>&1 || status=$? (( status == 0 )) && return 0 fi # A port taken from the compose file may serve a database or SSH: an answer @@ -373,6 +373,19 @@ healthcheck_probe() { && (( status == 1 || status == 8 || status == 52 || status == 56 )) } +# Any of the addresses answering means the application is up; HC_URL keeps the +# one that answered. +healthcheck_probe() { + local url + for url in "${HC_URLS[@]}"; do + if healthcheck_probe_url "$url"; then + HC_URL=$url + return 0 + fi + done + return 1 +} + cleanup_failed_install() { local status=$? stop_spinner @@ -926,7 +939,7 @@ set_lxc_directive() { add_device() { local host_path=$1 expected_type=$2 mode_spec=$3 gid_strategy=$4 deny_write=$5 - local device_uid=${6:-} + local device_uid=${6:-} container_gid=${7:-} local mode gid dev_value [[ $host_path == /dev/* && $host_path != *[[:space:]]* && $host_path != *","* ]] \ || die "$(translate "Invalid device path:") $host_path" @@ -950,7 +963,12 @@ add_device() { || die "$(translate "Invalid device UID:") $device_uid" dev_value="${dev_value},uid=${device_uid}" fi - if [[ $gid_strategy == host-device-gid ]]; then + if [[ -n $container_gid ]]; then + [[ $container_gid =~ ^[0-9]{1,10}$ ]] && (( container_gid < 4294967295 )) \ + || die "$(translate "Invalid container device GID"): $container_gid" + gid=$container_gid + dev_value="${dev_value},gid=${gid}" + elif [[ $gid_strategy == host-device-gid ]]; then gid=$(stat -c '%g' "$host_path") dev_value="${dev_value},gid=${gid}" elif [[ $gid_strategy != none ]]; then @@ -965,7 +983,7 @@ add_device() { } add_character_device() { - add_device "$1" character "$2" "$3" "$4" "${5:-}" + add_device "$1" character "$2" "$3" "$4" "${5:-}" "${6:-}" } prepare_nvidia_driver_links() { @@ -2030,6 +2048,7 @@ while IFS= read -r DEVICE_ENCODED; do DEVICE=$(printf '%s' "$DEVICE_ENCODED" | base64 -d) DEVICE_KIND=$(jq -r '.kind // "character-device"' <<<"$DEVICE") DEVICE_GID_ENV=$(jq -r '.append_host_device_gid_to_environment // empty' <<<"$DEVICE") + CONTAINER_GID=$(jq -r '.container_gid // empty' <<<"$DEVICE") if [[ $DEVICE_KIND == nvidia-runtime ]]; then NVIDIA_GID_ENV=$DEVICE_GID_ENV if (( NVIDIA_RUNTIME_CONFIGURED == 0 )); then @@ -2049,13 +2068,13 @@ while IFS= read -r DEVICE_ENCODED; do case "$DEVICE_KIND" in character-device) DEVICE_UID=$(jq -r '.uid // empty' <<<"$DEVICE") - add_character_device "$HOST_PATH" "$MODE" "$GID_STRATEGY" "$DENY_WRITE" "$DEVICE_UID" + add_character_device "$HOST_PATH" "$MODE" "$GID_STRATEGY" "$DENY_WRITE" "$DEVICE_UID" "$CONTAINER_GID" if [[ -n $DEVICE_GID_ENV ]]; then append_deployment_environment_csv "$DEVICE_GID_ENV" "$(stat -c '%g' "$HOST_PATH")" fi ;; block-device) - add_device "$HOST_PATH" block "$MODE" "$GID_STRATEGY" "$DENY_WRITE" + add_device "$HOST_PATH" block "$MODE" "$GID_STRATEGY" "$DENY_WRITE" "" "$CONTAINER_GID" if [[ -n $DEVICE_GID_ENV ]]; then append_deployment_environment_csv "$DEVICE_GID_ENV" "$(stat -c '%g' "$HOST_PATH")" fi @@ -2065,7 +2084,7 @@ while IFS= read -r DEVICE_ENCODED; do FOUND_TREE_DEVICE=0 while IFS= read -r TREE_DEVICE; do [[ -n $TREE_DEVICE ]] || continue - add_character_device "$TREE_DEVICE" "$MODE" "$GID_STRATEGY" "$DENY_WRITE" + add_character_device "$TREE_DEVICE" "$MODE" "$GID_STRATEGY" "$DENY_WRITE" "" "$CONTAINER_GID" FOUND_TREE_DEVICE=1 done < <(find "$HOST_PATH" -print 2>/dev/null | while IFS= read -r path; do [[ -c $path ]] && printf '%s\n' "$path" @@ -2333,6 +2352,12 @@ if [[ $START_AFTER == 1 && $HAS_STARTUP_HEALTHCHECK == 1 ]]; then || die "$(translate "The stability period must be shorter than the healthcheck timeout")" [[ -n $IP ]] || die "$(translate "The healthcheck cannot run without an IP address")" HC_URL="${HC_SCHEME}://${IP}:${HC_PORT}${HC_PATH}" + HC_URLS=("$HC_URL") + while IFS= read -r HC_ALT_PORT; do + [[ $HC_ALT_PORT =~ ^[0-9]+$ && $HC_ALT_PORT -ge 1 && $HC_ALT_PORT -le 65535 ]] \ + || die "$(translate "Invalid healthcheck port")" + HC_URLS+=("${HC_SCHEME}://${IP}:${HC_ALT_PORT}${HC_PATH}") + done < <(jq -r '.alternative_ports[]? | tostring' <<<"$STARTUP_HEALTHCHECK") HC_ANY_STATUS=$(jq -r '.accept_any_status // false' <<<"$STARTUP_HEALTHCHECK") HC_ACCEPT_NON_HTTP=$(jq -r '.accept_non_http // false' <<<"$STARTUP_HEALTHCHECK") if [[ $HC_ANY_STATUS == true ]]; then diff --git a/oci/remote/oci_accelerators.py b/oci/remote/oci_accelerators.py index 353356bf..18ea5dd2 100644 --- a/oci/remote/oci_accelerators.py +++ b/oci/remote/oci_accelerators.py @@ -40,6 +40,14 @@ def drm_plan(deployment): return dict(deployment, devices=[d for d in deployment.get('devices', []) if d.get('kind') != 'nvidia-runtime']) +def container_gid(device, host_value): + return drm.container_gid(device, host_value) + + +def preserve_recorded_container_gids(record, candidate, config): + drm.preserve_recorded_container_gids(record, candidate, config) + + def planned(deployment): result = drm.planned(drm_plan(deployment)) if nvidia.enabled(deployment): @@ -69,7 +77,7 @@ def check(config, deployment): filtered = [] for line in config.splitlines(keepends=True): if re.match(rb'dev[0-9]+: ', line): - fields = dict(part.split('=', 1) for part in line.decode().strip().split(': ', 1)[1].split(',')) + fields = drm.device_fields(line.decode().strip().split(': ', 1)[1]) if fields.get('path') in value['devices']: continue filtered.append(line) diff --git a/oci/remote/oci_gpu_devices.py b/oci/remote/oci_gpu_devices.py index e4502470..4ed57c60 100644 --- a/oci/remote/oci_gpu_devices.py +++ b/oci/remote/oci_gpu_devices.py @@ -88,21 +88,98 @@ def planned(deployment): raise ValueError(translate('Invalid device mode')) if device.get('gid_strategy') not in ('none', 'host-device-gid'): raise ValueError(translate('Unsupported device GID strategy')) + if 'container_gid' in device: + container_gid(device, value) result[path] = value return result +def container_gid(device, host_value): + """The GID assigned to the device node in the LXC's user namespace. + + A recorded container GID is distinct from the host device's GID. Proxmox + maps the configured LXC GID through the container's id map when it creates + its passthrough node; the numeric host GID is not automatically translated. + """ + if 'container_gid' in device: + gid = device['container_gid'] + if isinstance(gid, bool) or not isinstance(gid, int) or not 0 <= gid < 4294967295: + raise ValueError(translate('Invalid container device GID')) + return gid + return host_value['gid'] if device['gid_strategy'] == 'host-device-gid' else 0 + + +def _permission_fields(fields): + return (fields.get('path'), int(fields.get('mode', '0660'), 8), + int(fields.get('uid', 0)), int(fields.get('gid', 0)), + fields.get('deny-write', '0')) + + +def preserve_recorded_container_gids(record, candidate, config): + """Carry forward only device GIDs already captured in the saved config. + + This is not a host-to-container GID translation. The saved config is the + baseline for the GID Proxmox previously assigned inside the LXC. Any + unrecorded change is rejected, and the host device is still verified + independently by ``verify_baseline``. + """ + saved_config = record.get('observed', {}).get('config') + if not isinstance(saved_config, str) or not saved_config: + return + saved = actual_devices(saved_config.encode()) + current = actual_devices(config) + if set(saved) != set(current): + return # check() below reports the device set mismatch. + if any(_permission_fields(saved[path]) != _permission_fields(current[path]) for path in saved): + raise ValueError(translate('The container device settings changed since the last recorded operation')) + + original_devices = record.get('deployment', {}).get('devices', []) + old_snapshots = planned(record.get('deployment', {})) + for device in original_devices: + path = device.get('host_path') + if path not in saved or path not in old_snapshots: + continue + observed_gid = int(saved[path].get('gid', 0)) + expected_gid = container_gid(device, old_snapshots[path]) + if observed_gid != expected_gid and 'container_gid' in device: + raise ValueError(translate('The recorded container device GID does not match its configuration')) + if observed_gid != expected_gid: + device['container_gid'] = observed_gid + + for desired in candidate.get('deployment', {}).get('devices', []): + if (desired.get('host_path') == path + and desired.get('gid_strategy') == device.get('gid_strategy') + and 'container_gid' not in desired): + desired['container_gid'] = observed_gid + + def verify(expected): for path, value in expected.items(): if snapshot(path) != value: raise ValueError(translate('The GPU identity or permissions changed; the container is not modified')) +def device_fields(value): + """The options of a devN line. Proxmox writes its path either as `path=` + or, when the device was added from its own interface, as the first value + without a name: `dev0: /dev/dri/renderD128,gid=993`.""" + fields = {} + for index, part in enumerate(value.split(',')): + if '=' in part: + key, item = part.split('=', 1) + fields[key] = item + elif index == 0 and part: + fields['path'] = part + else: + raise ValueError(translate('Incomplete device configuration')) + return fields + + def actual_devices(config): result = {} for key, value in parse_config(config).items(): if re.fullmatch(r'dev[0-9]+', key): - fields = dict(part.split('=', 1) for part in value.split(',')) + fields = device_fields(value) path = fields.get('path') if path in result: raise ValueError(translate('Duplicated GPU device in the container')) @@ -121,7 +198,7 @@ def check(config, deployment): value = expected[path] requested_mode = device.get('mode', '0660') mode = value['mode'] if requested_mode == 'preserve-host' else int(requested_mode, 8) - gid = value['gid'] if device['gid_strategy'] == 'host-device-gid' else 0 + gid = container_gid(device, value) if (set(fields) - {'path', 'mode', 'gid', 'uid', 'deny-write'} or int(fields.get('mode', '0660'), 8) != mode or int(fields.get('gid', 0)) != gid diff --git a/oci/remote/oci_instance_reconcile.py b/oci/remote/oci_instance_reconcile.py index 9a03412d..6df08fee 100644 --- a/oci/remote/oci_instance_reconcile.py +++ b/oci/remote/oci_instance_reconcile.py @@ -58,9 +58,10 @@ def _mount(key, value, vmid): def _device(key, value): parts = value.split(',') - if any('=' not in part for part in parts): - raise ValueError(f'{key}: {translate("Incomplete device configuration")}') - fields = dict(part.split('=', 1) for part in parts) + try: + fields = gpu_devices.device_fields(value) + except ValueError: + raise ValueError(f'{key}: {translate("Incomplete device configuration")}') from None if len(fields) != len(parts) or set(fields) - {'path', 'mode', 'gid', 'uid', 'deny-write'}: raise ValueError(f'{key}: {translate("Unsupported device options")}') path = fields.get('path') diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index fa01b2dc..8e2c44da 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -435,13 +435,22 @@ def probe_http(args): return result.stdout, result.returncode +def setup_then_web_port(template, check): + """AdGuard Home answers its setup wizard on 3000 and, once set up, its web UI + on 80. A check on 3000, derived or saved with an installation, takes 80 too.""" + if (template.get('id') == 'image-adguard-home' and check.get('port') == 3000 + and not check.get('alternative_ports')): + return dict(check, alternative_ports=[80]) + return check + + def effective_healthcheck(template): """The template's own health check or, when it has none, one derived from its web address: any HTTP answer below 500 means the application is up.""" profile = template.get('proxmox', {}).get('installer_profile', {}) check = profile.get('startup_healthcheck') if check: - return check + return setup_then_web_port(template, check) if profile.get('haos_healthcheck'): # Home Assistant OS has a check of its own: Supervisor, Core and the # services it runs inside. The installer runs it on the new container. @@ -453,6 +462,7 @@ def effective_healthcheck(template): check = {'scheme': endpoint['scheme'], 'port': int(endpoint['port']), 'path': path, 'timeout_seconds': 300, 'request_timeout_seconds': 10, 'stability_seconds': 4, 'verify_tls': False, 'accept_any_status': True} + check = setup_then_web_port(template, check) # A port taken from the compose file because the recipe names no # web address may serve a database or SSH: any answer counts. if endpoint.get('source') == GUESSED_ENDPOINT: @@ -735,6 +745,7 @@ def preflight(record, candidate, config, coordinated=None, in_place=False, basel if desired['rootfs'] != deployment['rootfs']: raise ValueError(translate('Changing the rootfs or its storage requires a separate migration')) actual = mounts(config) + gpu_devices.preserve_recorded_container_gids(record, candidate, config) gpu_devices.check(config, deployment) if baseline: gpu_devices.verify_baseline(record['observed'].get('gpu_devices', {}), deployment) @@ -923,10 +934,12 @@ def healthcheck(vmid, template): time.sleep(2) log(f'container {vmid} kept running for {stability}s') return None - scheme, port, path = check['scheme'], int(check['port']), check['path'] + scheme, path = check['scheme'], check['path'] + # Any of the ports answering means the application is up. + ports = [int(check['port']), *(int(p) for p in check.get('alternative_ports') or [])] timeout = int(check.get('timeout_seconds', 120)) stability = int(check.get('stability_seconds', 0)) - if (scheme not in ('http', 'https') or not 1 <= port <= 65535 + if (scheme not in ('http', 'https') or not all(1 <= port <= 65535 for port in ports) or not path.startswith('/') or any(c.isspace() for c in path) or not 0 <= stability < timeout <= 3600): raise ValueError(translate('Invalid health check')) @@ -947,10 +960,13 @@ def healthcheck(vmid, template): args += ['-w', '%{http_code}'] if not check.get('verify_tls', False): args.append('-k') - answer, status = probe_http([*args, f'{scheme}://{ip}:{port}{path}']) - ok = status == 0 and (not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None) - if not ok and check.get('accept_non_http') and status in NON_HTTP_ANSWER: - ok = True + for port in ports: + answer, status = probe_http([*args, f'{scheme}://{ip}:{port}{path}']) + ok = status == 0 and (not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None) + if not ok and check.get('accept_non_http') and status in NON_HTTP_ANSWER: + ok = True + if ok: + break if ok: stable_since = stable_since or time.monotonic() if time.monotonic() - stable_since >= stability: diff --git a/oci/remote/oci_keep_backup.py b/oci/remote/oci_keep_backup.py index 900985fc..665fb010 100644 --- a/oci/remote/oci_keep_backup.py +++ b/oci/remote/oci_keep_backup.py @@ -50,9 +50,12 @@ def before_update(vmid, storage): """A storage that cannot receive the file gets its own backup first.""" if dump_dir(storage) is not None: return None + # Like transaction.run(), scope PVE's creation umask to this child only. + # PBS reads the temporary LXC config as the guest-mapped UID; inheriting + # a private caller umask (077) makes its traversal/config files unreadable. subprocess.run(['vzdump', str(vmid), '--storage', storage, '--mode', 'snapshot', '--compress', 'zstd', '--notes-template', 'ProxMenux OCI: before the image update'], - check=True) + check=True, umask=0o022) return storage diff --git a/oci/remote/oci_native_import.py b/oci/remote/oci_native_import.py new file mode 100644 index 00000000..1586c242 --- /dev/null +++ b/oci/remote/oci_native_import.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Give Proxmox's mapped OCI reader access without publishing a private cache. + +Ordinary readable templates use the existing zero-copy path. An inaccessible +archive gets a short-lived copy (reflink where supported), owned by root and +readable only by the mapped root group. Never chmod/chown the source image, +its parents, registry credentials, or the caller's process umask. +""" +from contextlib import contextmanager +import errno +import ctypes +import fcntl +import os +from pathlib import Path +import shutil +import signal +import stat +import subprocess +import sys +import tempfile + +STAGING_ROOT = Path('/var/tmp') # Deliberately independent of a private TMPDIR. +FICLONE = 0x40049409 + + +def reader_ids(options): + """Fresh pct creates use PVE's standard idmap, not a restored custom map.""" + unprivileged = False + for index, option in enumerate(options): + if option == '--unprivileged': + unprivileged = index + 1 == len(options) or options[index + 1] not in ('0', 'false') + elif option.startswith('--unprivileged='): + unprivileged = option.partition('=')[2] not in ('0', 'false') + return (100000, 100000) if unprivileged else (0, 0) + + +def _allowed(path, uid, gid, required): + info = path.lstat() + if stat.S_ISLNK(info.st_mode): + return False + shift = 6 if info.st_uid == uid else 3 if info.st_gid == gid else 0 + return bool((info.st_mode >> shift) & required) + + +def readable(path, uid, gid): + """Conservative DAC check: an ACL or symlink may make staging unnecessary, + but never justify publishing the cache or broadening a storage directory.""" + path = Path(os.path.abspath(path)) + if uid == 0: + return path.is_file() + return (_allowed(path, uid, gid, 4) + and all(_allowed(parent, uid, gid, 1) for parent in path.parents)) + + +def _copy(source, target): + source_fd = os.open(source, os.O_RDONLY | os.O_NOFOLLOW) + try: + if not stat.S_ISREG(os.fstat(source_fd).st_mode): + raise OSError(errno.EINVAL, os.strerror(errno.EINVAL), str(source)) + with os.fdopen(source_fd, 'rb', closefd=False) as incoming, target.open('xb') as outgoing: + try: + fcntl.ioctl(outgoing.fileno(), FICLONE, incoming.fileno()) + except OSError as error: + if error.errno not in (errno.EXDEV, errno.EOPNOTSUPP, errno.ENOTTY, errno.EINVAL, errno.ENOSYS): + raise + shutil.copyfileobj(incoming, outgoing, length=1024 * 1024) + finally: + os.close(source_fd) + + +def trusted_staging_root(uid, gid): + base = STAGING_ROOT + info = base.lstat() + ancestors = (base, *base.parents) + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 + or any(p.lstat().st_uid != 0 or + (p.lstat().st_mode & 0o022 and not p.lstat().st_mode & stat.S_ISVTX) + for p in ancestors) + or not all(_allowed(p, uid, gid, 1) for p in ancestors)): + raise PermissionError(errno.EACCES, os.strerror(errno.EACCES), str(base)) + return base + + +@contextmanager +def import_archive(source, original, uid, gid): + source = Path(source) + if readable(source, uid, gid): + yield original + return + base = trusted_staging_root(uid, gid) + # mkdtemp is 0700 even if the caller's umask is permissive. Publish group + # traversal only after the root-owned, read-only copy is complete. + directory = Path(tempfile.mkdtemp(prefix='proxmenux-oci-import-', dir=base)) + try: + target = directory / 'image.tar' + _copy(source.resolve(strict=True), target) + os.chown(target, 0, gid) + target.chmod(0o440) + os.chown(directory, 0, gid) + directory.chmod(0o750) + yield str(target) + finally: + shutil.rmtree(directory) + + +def create(vmid, original, source, options): + uid, gid = reader_ids(options) + with import_archive(source, original, uid, gid) as archive: + result = subprocess.run(['pct', 'create', vmid, archive, *options], + close_fds=True, umask=0o022) + return 128 - result.returncode if result.returncode < 0 else result.returncode + + +def main(): + # A catchable interruption must unwind the staging context. subprocess.run + # terminates its child when the handler raises; no background pct is orphaned. + for caught in (signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT): + signal.signal(caught, lambda signum, frame: sys.exit(128 + signum)) + if sys.platform == 'linux': + parent = os.getppid() + libc = ctypes.CDLL(None, use_errno=True) + if libc.prctl(1, signal.SIGTERM, 0, 0, 0) != 0: # PR_SET_PDEATHSIG + raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno())) + if os.getppid() != parent: + return 143 + try: + return create(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:]) + except KeyboardInterrupt: + return 130 + except OSError as error: + print(error, file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/oci/remote/oci_native_modify.py b/oci/remote/oci_native_modify.py index 44670a78..767d8161 100644 --- a/oci/remote/oci_native_modify.py +++ b/oci/remote/oci_native_modify.py @@ -171,7 +171,7 @@ def plan(record, candidate, config): 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(','))) + existing = {v['path']: k for k, v in ((k, devices.device_fields(v)) for k, v in cfg.items() if re.fullmatch(r'dev[0-9]+', k))} used = set(existing.values()) for path, key in existing.items(): @@ -185,7 +185,7 @@ def plan(record, candidate, config): 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"gid={devices.container_gid(device, info)}," f"deny-write={1 if device.get('deny_write') else 0}") if 'mounts' in changed: actual = transaction.mounts(config) diff --git a/oci/remote/oci_nvidia_runtime.py b/oci/remote/oci_nvidia_runtime.py index 3c150fb8..1ba4182b 100644 --- a/oci/remote/oci_nvidia_runtime.py +++ b/oci/remote/oci_nvidia_runtime.py @@ -7,7 +7,7 @@ from pathlib import Path, PurePosixPath import stat import subprocess -from oci_gpu_devices import actual_devices +from oci_gpu_devices import actual_devices, device_fields from oci_ui import translate KEY = '_nvidia_runtime' @@ -104,7 +104,7 @@ def refresh_plan(config, previous, current=None, same_gpu=True): if line.startswith('dev') and ': ' in line: key, properties = line.split(': ', 1) if key[3:].isdigit(): - fields = dict(part.split('=', 1) for part in properties.split(',')) + fields = device_fields(properties) if fields.get('path') in previous['devices']: continue kept.append(line) diff --git a/oci/remote/oci_stack_native.py b/oci/remote/oci_stack_native.py index cd851687..c2c16d18 100644 --- a/oci/remote/oci_stack_native.py +++ b/oci/remote/oci_stack_native.py @@ -585,7 +585,7 @@ class NativeAdapter: if sha(config) != child.get('staged_config_sha256'): raise ValueError(translate('The configuration of a new member changed after it was created')) member_tx.check_runtime_mounts(config, original['deployment']) - member_tx.gpu_devices.check(config, original['deployment']) + member_tx.gpu_devices.check(config, child['candidate_contract']['deployment']) child['candidate_host_sources'] = member_tx.candidate_host_sources(journal, child) child['validated_config_sha256'] = sha(config) instances.write(journal, child) diff --git a/oci/remote/oci_ui.sh b/oci/remote/oci_ui.sh index dbab0cd7..80231103 100644 --- a/oci/remote/oci_ui.sh +++ b/oci/remote/oci_ui.sh @@ -127,7 +127,7 @@ oci_create_container() { total_mib=$(python3 "$VERIFY_OCI_ARCHIVE" --extracted-mib "$path" 2>/dev/null || printf '0') [[ $total_mib =~ ^[0-9]+$ ]] || total_mib=0 fi - pct create "$vmid" "$archive" "$@" >>"$log" 2>&1 & + python3 "${SCRIPT_DIR}/oci_native_import.py" "$vmid" "$archive" "$path" "$@" >>"$log" 2>&1 & pid=$! while kill -0 "$pid" 2>/dev/null; do if mountpoint -q "$rootfs" 2>/dev/null; then @@ -208,7 +208,8 @@ oci_apply_extra_devices() { || die "$(translate "Invalid device path:") $path" [[ -c $path ]] || die "$(translate "The character device does not exist:") $path" [[ $mode =~ ^0?[0-7]{3}$ ]] || die "$(translate "Invalid device mode:") $mode" - pct config "$vmid" | grep -Eq "^dev[0-9]+: (.*,)?path=${path}(,|$)" && continue + # Proxmox may write the path without `path=` when it was added from its interface. + pct config "$vmid" | grep -Eq "^dev[0-9]+: ((.*,)?path=)?${path}(,|$)" && continue index=0 while pct config "$vmid" | grep -q "^dev${index}:"; do index=$((index + 1)); done gid=$(stat -c '%g' "$path") diff --git a/oci/src/proxmenux_oci/catalog_refresh.py b/oci/src/proxmenux_oci/catalog_refresh.py index 86bb65dd..30dded5b 100644 --- a/oci/src/proxmenux_oci/catalog_refresh.py +++ b/oci/src/proxmenux_oci/catalog_refresh.py @@ -450,10 +450,15 @@ class CatalogRefresh: self.recipes = visible_recipes(catalog) self.displayed_directory = catalog.catalog_dir baseline = read_json(self.seen_path).get("recipes") - self.baseline = baseline if isinstance(baseline, dict) else self.recipes.copy() self.process = None self.failed = False self._finished = False + if isinstance(baseline, dict): + self.baseline = baseline + else: + # The first visit sets what counts as already known. + self.baseline = self.recipes.copy() + self.mark_seen() def __enter__(self): self.start() @@ -480,8 +485,8 @@ class CatalogRefresh: return True def __exit__(self, *args): - # The worker can finish downloading after the user leaves. Its result - # is not marked seen until a later menu actually displays that catalog. + # The worker can finish downloading after the user leaves. Its new + # applications stay new until the user opens their list. if self.process and self.process.poll() is not None: self.process.wait() elif self.process: @@ -506,24 +511,26 @@ class CatalogRefresh: self.displayed_directory = directory return changed - def describe(self): - text = translate("Available applications: {count}").format(count=len(self.recipes)) - added = len(self.recipes.keys() - self.baseline.keys()) - updated = sum(self.recipes[key] != self.baseline[key] for key in self.recipes.keys() & self.baseline.keys()) - text += "\n" + translate("New applications since your last visit: {count}").format(count=added) - if updated: - text += "\n" + translate("Updated recipes since your last visit: {count}").format(count=updated) - if self.active: - text += "\n" + translate("Refreshing the application catalog...") - elif self.failed: - text += "\n" + translate("Catalog refresh unavailable; using the local catalog.") - return text + def new_ids(self): + """The applications added since the user last opened the list of new ones.""" + return sorted(self.recipes.keys() - self.baseline.keys()) + + def label(self): + added = len(self.new_ids()) + return (translate("New applications ({count})").format(count=added) if added + else translate("Refresh catalog")) + + def wait(self): + if self.process is not None and not self._finished: + self.process.wait() def mark_seen(self): + """Opening the list of new applications makes them known.""" + self.baseline = self.recipes.copy() try: write_json(self.seen_path, {"recipes": self.recipes}) except OSError: - self.failed = True + pass if __name__ == "__main__": diff --git a/oci/src/proxmenux_oci/cli.py b/oci/src/proxmenux_oci/cli.py index e2c9025f..76706924 100644 --- a/oci/src/proxmenux_oci/cli.py +++ b/oci/src/proxmenux_oci/cli.py @@ -531,6 +531,25 @@ def _app_list(catalog: Catalog, ui, applications: list[dict[str, Any]], title: s _app_detail(catalog, ui, index[selection]) +def _new_applications(catalog: Catalog, ui, applications: list[dict[str, Any]], catalog_refresh) -> bool: + """The applications added to the catalog since the user last opened this + list, ready to install. Returns whether a catalog refresh was asked for.""" + options, index, header = _app_menu(applications) + options = [("refresh", translate("Refresh catalog")), ("", "")] + options + title = translate("New applications ({count})").format(count=len(applications)) + # Opening the list makes them known: the entry stops counting them. + catalog_refresh.mark_seen() + selection = None + while True: + selection = ui.choose(header, options, selection, title=title, size=MENU_SIZE, colors=True) + if selection is None: + return False + if selection == "refresh": + return True + if selection in index: + _app_detail(catalog, ui, index[selection]) + + def _search(catalog: Catalog, ui, applications: list[dict[str, Any]]) -> None: query = ui.ask(translate("Name or part of the description of the application"), required=False).strip() if not query: @@ -557,6 +576,29 @@ def _search(catalog: Catalog, ui, applications: list[dict[str, Any]]) -> None: keep_order=True) +COUNTED_LABEL = re.compile(r"^(.*\S) \((\d+)\)$") + + +def _aligned(entries: list[tuple[str, str]]) -> list[tuple[str, str]]: + """The counts of the menu in one column, two spaces past the longest entry + that has one, whatever the language: "Updates available (28)" lines up + with "(554)".""" + counted = [COUNTED_LABEL.match(label) for _, label in entries] + width = max((len(match[1]) for match in counted if match), default=0) + 2 + return [(tag, f"{match[1]:<{width}}({int(match[2]):>3})" if match else label) + for (tag, label), match in zip(entries, counted)] + + +def _installed_count() -> int | None: + """How many OCI containers the manage list shows, read from their saved + records. None while another operation holds the registry.""" + from .management import saved_inventory + try: + return len(saved_inventory(PROJECT_ROOT)) + except (BlockingIOError, OSError, ValueError): + return None + + def _catalog_options(catalog): applications = _installable(catalog.load_index()["applications"]) categories: dict[str, list[dict[str, Any]]] = {} @@ -569,13 +611,13 @@ def _catalog_options(catalog): category_by_index = {str(number): key for number, key in enumerate(order, 1)} options = [ ("search", translate("Search applications")), - ("all", f"{translate('All applications'):<35} ({len(applications):>3})"), + ("all", f"{translate('All applications')} ({len(applications)})"), ("manage", translate("Manage installed OCI applications")), - ("catalog-refresh", translate("Refresh catalog")), + ("refresh", translate("Refresh catalog")), ("registries", translate("Registry accounts")), ("custom", translate("Install an image that is not in the catalog")), ("", ""), - ] + [(number, f"{translate(labels[key]):<35} ({len(categories[key]):>3})") + ] + [(number, f"{translate(labels[key])} ({len(categories[key])})") for number, key in category_by_index.items()] return applications, categories, labels, category_by_index, options @@ -590,15 +632,30 @@ def interactive(catalog: Catalog) -> int: return 1 applications, categories, labels, category_by_index, options = _catalog_options(catalog) selection = "search" + refresh_requested = False with CatalogRefresh(PROJECT_ROOT, catalog) as catalog_refresh, UpdateCheck(PROJECT_ROOT) as updates: while True: if catalog_refresh.consume(): catalog = Catalog(PROJECT_ROOT) applications, categories, labels, category_by_index, options = _catalog_options(catalog) catalog_refresh.recipes = visible_recipes(catalog) + if refresh_requested: + # Only a refresh the user asked for says that it failed. + refresh_requested = False + if catalog_refresh.failed: + ui.message(translate("Catalog refresh unavailable; using the local catalog.")) state = updates.status() - entries = ReadableTags(options[:3] + [('updates', updates.label(state))] + options[3:]) - text = catalog_refresh.describe() + "\n\n" + translate("Select a category or search for applications:") + # The catalog entry names the new applications when there are some. + installed = _installed_count() + shown = [] + for tag, label in options: + if tag == "refresh": + label = catalog_refresh.label() + elif tag == "manage" and installed: + label = f"{label} ({installed})" + shown.append((tag, label)) + entries = ReadableTags(_aligned(shown[:3] + [('updates', updates.label(state))] + shown[3:])) + text = translate("Select a category or search for applications:") if notice := updates.notice(state): text += '\n\n' + notice if updates.active and not getattr(ui, 'live_refresh', False): @@ -606,13 +663,12 @@ def interactive(catalog: Catalog) -> int: updates.wait() continue try: - kwargs = ({'refresh': lambda: updates.changed() or catalog_refresh.changed()} - if updates.active or catalog_refresh.active else {}) + # The catalog downloads behind the open menu without closing it: + # a new catalog is shown the next time the menu is drawn. + kwargs = {'refresh': updates.changed} if updates.active else {} selected = ui.choose(text, entries, selection, size=MENU_SIZE, **kwargs) except MenuRefresh: continue - finally: - catalog_refresh.mark_seen() if selected is None: return 0 selection = selected @@ -625,8 +681,14 @@ def interactive(catalog: Catalog) -> int: elif selection == "manage": from .management import interactive_management interactive_management(PROJECT_ROOT, ui) - elif selection == "catalog-refresh": - catalog_refresh.start(force=True) + elif selection == "refresh": + new = [item for item in applications if item["id"] in set(catalog_refresh.new_ids())] + if not new or _new_applications(catalog, ui, new, catalog_refresh): + # Asked for: wait for it and say if it could not be done. + catalog_refresh.start(force=True) + ui.info(translate("Refreshing the application catalog...")) + catalog_refresh.wait() + refresh_requested = True elif selection == "updates": if not updates.active: if state['available']: diff --git a/oci/src/proxmenux_oci/gpu.py b/oci/src/proxmenux_oci/gpu.py index f9a7c851..c7d4426b 100644 --- a/oci/src/proxmenux_oci/gpu.py +++ b/oci/src/proxmenux_oci/gpu.py @@ -70,7 +70,12 @@ def apply_gpu_contract(template: dict[str, Any]) -> None: def apply_selkies_contract(template): profile = template['proxmox']['installer_profile'] - if not template['container_contract']['image']['reference'].startswith('lscr.io/linuxserver/'): + # A LinuxServer image, or one whose reviewed Dockerfile is built on the + # LinuxServer Selkies base and so has the same init and GPU variables. + built_on_base = str(profile['selkies'].get('base', '')).startswith( + 'FROM ghcr.io/linuxserver/baseimage-selkies:') + if not (template['container_contract']['image']['reference'].startswith('lscr.io/linuxserver/') + or built_on_base): raise ValueError(translate('The Selkies profile requires a verified LinuxServer image')) environment = template['container_contract']['environment'] if not any(e['name'] == 'LC_ALL' for e in environment): diff --git a/oci/src/proxmenux_oci/installer.py b/oci/src/proxmenux_oci/installer.py index 72f0c284..cf517bf8 100644 --- a/oci/src/proxmenux_oci/installer.py +++ b/oci/src/proxmenux_oci/installer.py @@ -962,15 +962,14 @@ def _ask_immich_acceleration(ui, rootfs_storage: str | None = None) -> tuple[str return software # Recognition on an AMD GPU depends on ROCm: it is offered when the host # can run it, marked as experimental on a GPU ROCm does not support - # officially, and left out, with the reason, when it cannot. + # officially, and left out when it cannot. A GPU of a generation ROCm does + # not run is simply not offered; what the host can fix is explained. amd_blocker = host.rocm_blocker(rootfs_storage) if found["amd"] else None amd_experimental = (found["amd"] and not amd_blocker and host.rocm_support(found.get("amd_gfx_target")) == "experimental") - if amd_blocker: + if amd_blocker in ("kfd", "space"): reason = (translate("The AMD driver does not offer its compute interface (/dev/kfd) on this host.") if amd_blocker == "kfd" else - f"{translate('The ROCm image has no support for the AMD GPU of this host:')} {host.amd_gpu_name()}." - if amd_blocker == "generation" else translate("The storage has less than 40 GB free for the ROCm image.")) real.message(f"{reason}\n\n{translate('Recognition is not offered on the AMD GPU; video transcoding is.')}") mark = f" — {translate('experimental on this GPU')}" if amd_experimental else "" @@ -1611,7 +1610,7 @@ def _run_remote_install( 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') shutil.copy2(project_root / 'remote' / 'oci_image_mountpoints.py', temporary / 'oci_image_mountpoints.py') - for helper in ('oci_carried_record.py', 'oci_compose_jobs.py', 'oci_stack_public_routes.py', '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_managed_volumes.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'): + for helper in ('oci_carried_record.py', 'oci_compose_jobs.py', 'oci_stack_public_routes.py', 'oci_ui.sh', 'oci_ui.py', 'oci_native_import.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_managed_volumes.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') @@ -1648,7 +1647,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_carried_record.py', 'oci_compose_jobs.py', 'oci_stack_public_routes.py', '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_managed_volumes.py', 'oci_image_mountpoints.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'): + for helper in ('oci_carried_record.py', 'oci_compose_jobs.py', 'oci_stack_public_routes.py', 'oci_ui.sh', 'oci_ui.py', 'oci_native_import.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_managed_volumes.py', 'oci_image_mountpoints.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') @@ -1916,11 +1915,6 @@ def configure_acceleration(installer_profile, environment, unprivileged, ui, mod found.get("amd_gfx_target"), tuple(item["amd_gfx_targets"])) == "experimental"} options = [(item["id"], item["label"]) for item in usable] asked = advanced or not installer_profile.get("selkies") - if asked and found["amd"] and any(item.get("amd_gfx_targets") and item not in usable and host.rocm_support( - found.get("amd_gfx_target"), tuple(item["amd_gfx_targets"])) is None for item in profiles): - # Its owner may expect the GPU to be offered: say why it is not. - ui.message(f"{translate('The ROCm image of this application has no support for the AMD GPU of this host:')} " - f"{host.amd_gpu_name()}. {translate('Its ROCm profile is not offered.')}") if asked and len(usable) < len(profiles) and len(usable) == 1: # Nothing but the CPU is left: say why there is nothing to choose. ui.message(translate("No usable GPU was found on this host. The application will be installed " diff --git a/oci/tests/test_catalog_new_apps_entry.py b/oci/tests/test_catalog_new_apps_entry.py new file mode 100644 index 00000000..aa536dfb --- /dev/null +++ b/oci/tests/test_catalog_new_apps_entry.py @@ -0,0 +1,91 @@ +"""The catalog entry of the OCI menu: it names the new applications and leads +to them; a refresh the user asks for says when it could not be done.""" +from pathlib import Path +import sys +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'src')) + +from proxmenux_oci import cli + + +APPS = [{'id': 'alpha', 'title': 'Alpha', 'architectures': ['amd64']}, + {'id': 'beta', 'title': 'Beta', 'architectures': ['amd64']}] + + +class NewApplicationsListTests(unittest.TestCase): + def test_opening_the_list_acknowledges_the_new_apps_and_offers_them(self): + refresh = Mock() + ui = Mock() + ui.choose.side_effect = ['1', None] + with patch.object(cli, '_app_detail') as detail: + self.assertFalse(cli._new_applications(Mock(), ui, APPS, refresh)) + refresh.mark_seen.assert_called_once_with() + detail.assert_called_once() + options = ui.choose.call_args_list[0].args[1] + self.assertEqual(options[0], ('refresh', 'Refresh catalog')) + self.assertIn('New applications (2)', ui.choose.call_args_list[0].kwargs['title']) + + def test_its_refresh_line_asks_for_a_refresh(self): + ui = Mock() + ui.choose.return_value = 'refresh' + self.assertTrue(cli._new_applications(Mock(), ui, APPS, Mock())) + + +class ManualRefreshTests(unittest.TestCase): + def run_menu(self, failed): + status = Mock(failed=failed) + status.consume.return_value = False + status.new_ids.return_value = [] + status.label.return_value = 'Refresh catalog' + context = Mock() + context.__enter__ = Mock(return_value=status) + context.__exit__ = Mock(return_value=False) + checker = Mock(active=False) + checker.status.return_value = {'available': []} + checker.notice.return_value = '' + checker.label.return_value = 'Check for updates' + checker_context = Mock() + checker_context.__enter__ = Mock(return_value=checker) + checker_context.__exit__ = Mock(return_value=False) + ui = Mock(live_refresh=True) + ui.choose.side_effect = ['refresh', None] + catalog = Mock() + catalog.index_path.exists.return_value = True + catalog.load_index.return_value = {'applications': []} + with patch('proxmenux_oci.catalog_refresh.CatalogRefresh', return_value=context), \ + patch('proxmenux_oci.updates.UpdateCheck', return_value=checker_context), \ + patch.object(cli, 'interactive_ui', return_value=ui): + self.assertEqual(cli.interactive(catalog), 0) + status.start.assert_called_once_with(force=True) + status.wait.assert_called_once_with() + return [call.args[0] for call in ui.message.call_args_list] + + def test_a_failed_refresh_asked_for_is_reported(self): + self.assertEqual(self.run_menu(True), ['Catalog refresh unavailable; using the local catalog.']) + + def test_a_successful_one_says_nothing(self): + self.assertEqual(self.run_menu(False), []) + + +class AlignedCountsTests(unittest.TestCase): + def test_counts_line_up_two_spaces_past_the_longest_counted_entry(self): + entries = cli._aligned([('all', 'All applications (554)'), + ('manage', 'Gestionar aplicaciones OCI instaladas (32)'), + ('updates', 'Checking for updates (3/28)'), ('1', 'DNS (2)')]) + self.assertEqual(entries[0][1], f"{'All applications':<39}(554)") + self.assertEqual(entries[1][1], 'Gestionar aplicaciones OCI instaladas ( 32)') + self.assertEqual(entries[2][1], 'Checking for updates (3/28)') # progress keeps its form + self.assertEqual(entries[3][1], f"{'DNS':<39}( 2)") + + def test_the_installed_count_is_left_out_while_the_registry_is_busy(self): + with patch('proxmenux_oci.management.saved_inventory', side_effect=BlockingIOError): + self.assertIsNone(cli._installed_count()) + with patch('proxmenux_oci.management.saved_inventory', return_value=[{}, {}]): + self.assertEqual(cli._installed_count(), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_catalog_refresh.py b/oci/tests/test_catalog_refresh.py index 7ce2abea..fdb049bc 100644 --- a/oci/tests/test_catalog_refresh.py +++ b/oci/tests/test_catalog_refresh.py @@ -95,7 +95,7 @@ class CatalogRefreshTests(unittest.TestCase): self.assertNotIn("sample-added", refresh.visible_recipes(catalog)) self.assertEqual(set(refresh.visible_recipes(catalog)), set(status.recipes)) status.recipes = refresh.visible_recipes(catalog) - self.assertIn("New applications since your last visit: 0", status.describe()) + self.assertEqual(status.new_ids(), []) def test_compatibility_gate_uses_effective_menu_visibility(self): self.add_app() @@ -330,24 +330,27 @@ class CatalogRefreshTests(unittest.TestCase): self.assertFalse(refresh.read_json(refresh.cache_root(self.root) / "main-result.json")["ok"]) self.assertEqual(refresh.current_catalog(self.root), self.root / "catalog") - def test_first_visit_has_zero_new_apps_and_second_visit_clears_acknowledged_news(self): + def test_first_visit_has_no_new_apps_and_opening_their_list_acknowledges_them(self): status = refresh.CatalogRefresh(self.root, Catalog(self.root)) - self.assertIn("New applications since your last visit: 0", status.describe()) + self.assertEqual((status.new_ids(), status.label()), ([], "Refresh catalog")) status.recipes["new-app"] = "new-recipe-hash" - self.assertIn("New applications since your last visit: 1", status.describe()) - status.mark_seen() + self.assertEqual((status.new_ids(), status.label()), (["new-app"], "New applications (1)")) + # A new application stays new on later visits until its list is opened. + later = refresh.CatalogRefresh(self.root, Catalog(self.root)) + later.recipes = status.recipes.copy() + self.assertEqual(later.new_ids(), ["new-app"]) + later.mark_seen() same = refresh.CatalogRefresh(self.root, Catalog(self.root)) same.recipes = status.recipes.copy() - self.assertIn("New applications since your last visit: 0", same.describe()) + self.assertEqual(same.new_ids(), []) - def test_new_app_count_is_not_net_growth_and_modified_recipes_are_separate(self): + def test_new_apps_are_counted_by_id_not_net_growth_or_changed_recipes(self): status = refresh.CatalogRefresh(self.root, Catalog(self.root)) original = self.item["id"] status.baseline = {original: "old-recipe", "removed-app": "hash"} status.recipes["new-app"] = "hash" - self.assertIn("Available applications: 2", status.describe()) - self.assertIn("New applications since your last visit: 1", status.describe()) - self.assertIn("Updated recipes since your last visit: 1", status.describe()) + self.assertEqual(status.new_ids(), ["new-app"]) + self.assertEqual(status.label(), "New applications (1)") def test_exit_before_download_finishes_does_not_acknowledge_remote_apps(self): status = refresh.CatalogRefresh(self.root, Catalog(self.root)) @@ -359,11 +362,10 @@ class CatalogRefreshTests(unittest.TestCase): self.add_app() self.sync() next_visit = refresh.CatalogRefresh(self.root, Catalog(self.root)) - self.assertIn("New applications since your last visit: 1", next_visit.describe()) + self.assertEqual(len(next_visit.new_ids()), 1) - def test_main_menu_redraws_totals_and_news_after_background_refresh(self): + def test_the_download_never_closes_the_open_menu_and_shows_on_the_next_draw(self): from proxmenux_oci import cli, updates - from proxmenux_oci.ui import MenuRefresh status = refresh.CatalogRefresh(self.root, Catalog(self.root)) status.process = Mock(returncode=0) status.process.poll.return_value = None @@ -374,14 +376,16 @@ class CatalogRefreshTests(unittest.TestCase): def choose(self, text, entries, default, **kwargs): screens.append((text, dict(entries))) if len(screens) == 1: + # The catalog finishes downloading while the menu is open: + # nothing may close it, so no refresh is watched. + owner.assertNotIn('refresh', kwargs) + owner.assertNotIn('Refreshing the application catalog', text) owner.add_app() owner.sync() refresh.write_json(refresh.cache_root(owner.root) / 'main-result.json', {"ok": True, "changed": True}) status.process.poll.return_value = 0 - self_refreshed = kwargs['refresh']() - owner.assertTrue(self_refreshed) - raise MenuRefresh() + return 'search' # any action brings the user back to the menu return None checker = Mock(active=False) checker.status.return_value = {"available": []} @@ -396,14 +400,17 @@ class CatalogRefreshTests(unittest.TestCase): patch.object(refresh, 'CatalogRefresh', return_value=status), \ patch.object(cli, 'PROJECT_ROOT', self.root), \ patch.object(cli, 'interactive_ui', return_value=UI()), \ + patch.object(cli, '_search'), \ patch.object(updates, 'UpdateCheck', return_value=checker_context): self.assertEqual(cli.interactive(Catalog(self.root)), 0) - self.assertIn('Available applications: 1', screens[0][0]) - self.assertIn('Available applications: 2', screens[1][0]) - self.assertIn('New applications since your last visit: 1', screens[1][0]) + # The header is one line; the catalog entry names the new application. + self.assertEqual(screens[0][0], cli.translate('Select a category or search for applications:')) + self.assertEqual(screens[0][1]['refresh'], cli.translate('Refresh catalog')) + self.assertRegex(screens[1][1]['refresh'], r' {2,}\( 1\)$') self.assertIn('( 2)', screens[1][1]['all']) next_visit = original_class(self.root, Catalog(self.root)) - self.assertIn('New applications since your last visit: 0', next_visit.describe()) + # Seeing the menu does not acknowledge it: only opening the list does. + self.assertEqual(len(next_visit.new_ids()), 1) def test_explicit_local_apps_survive_refresh_without_preventing_upstream_removal(self): # This app is a local preview, not a member of the upstream manifest. @@ -447,7 +454,8 @@ class CatalogRefreshTests(unittest.TestCase): with patch.object(refresh.threading, "Thread") as thread: with status: self.assertTrue(status.active) - self.assertIn("Refreshing", status.describe()) + # The download is transparent: the open menu does not announce it. + self.assertEqual(status.label(), "Refresh catalog") thread.return_value.start.assert_called_once() spawn.return_value.wait.assert_not_called() @@ -470,7 +478,7 @@ class CatalogRefreshTests(unittest.TestCase): def test_manual_refresh_is_offered_in_the_main_menu(self): from proxmenux_oci.cli import _catalog_options options = dict(_catalog_options(Catalog(self.root))[-1]) - self.assertEqual(options['catalog-refresh'], 'Refresh catalog') + self.assertEqual(options['refresh'], 'Refresh catalog') def test_sequence_new_app_failed_update_retry_and_withdrawal(self): self.sync() @@ -483,7 +491,7 @@ class CatalogRefreshTests(unittest.TestCase): self.assertIn('sample-added', refresh.visible_recipes(Catalog(self.root))) self.assertNotIn('sample-added', refresh.visible_recipes(old)) # Wizard remains pinned. next_visit = refresh.CatalogRefresh(self.root, Catalog(self.root)) - self.assertIn('New applications since your last visit: 1', next_visit.describe()) + self.assertEqual(len(next_visit.new_ids()), 1) next_visit.mark_seen() self.revision = 'c' * 40 recipe_path = self.item['curated_path'].removeprefix('catalog/') @@ -504,8 +512,7 @@ class CatalogRefreshTests(unittest.TestCase): current = Catalog(self.root) self.assertNotIn('sample-added', refresh.visible_recipes(current)) self.assertFalse((current.catalog_dir / 'curated/sample-added.json').exists()) - self.assertIn('New applications since your last visit: 0', - refresh.CatalogRefresh(self.root, current).describe()) + self.assertEqual(refresh.CatalogRefresh(self.root, current).new_ids(), []) def test_http_errors_and_timeouts_keep_snapshot_then_retry_succeeds(self): self.sync() @@ -657,7 +664,7 @@ class CatalogRefreshTests(unittest.TestCase): status = refresh.CatalogRefresh(self.root, Catalog(self.root)) with patch.object(refresh, 'write_json', side_effect=PermissionError('Read only')): status.mark_seen() - self.assertTrue(status.failed) + self.assertFalse(status.failed) def test_concurrent_workers_serialize_the_catalog_transaction(self): barrier = threading.Barrier(2) diff --git a/oci/tests/test_device_line_default_path.py b/oci/tests/test_device_line_default_path.py new file mode 100644 index 00000000..31aae2d7 --- /dev/null +++ b/oci/tests/test_device_line_default_path.py @@ -0,0 +1,77 @@ +"""A device added from the Proxmox interface is written with its path as the +first value, without `path=`. Every reader of the container configuration +accepts both forms.""" +from pathlib import Path +import re +import subprocess +import sys +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'remote')) + +import oci_gpu_devices as devices # noqa: E402 +import oci_instance_reconcile as reconcile # noqa: E402 + + +class DeviceFieldsTests(unittest.TestCase): + def test_both_forms_give_the_same_fields(self): + expected = {'path': '/dev/dri/renderD128', 'gid': '993'} + self.assertEqual(devices.device_fields('/dev/dri/renderD128,gid=993'), expected) + self.assertEqual(devices.device_fields('path=/dev/dri/renderD128,gid=993'), expected) + self.assertEqual(devices.device_fields('/dev/apex_0'), {'path': '/dev/apex_0'}) + + def test_a_value_without_name_after_the_first_is_refused(self): + for value in ('/dev/dri/renderD128,993', 'gid=993,/dev/dri/renderD128', ''): + with self.subTest(value=value), self.assertRaises(ValueError): + devices.device_fields(value) + + def test_actual_devices_reads_a_line_written_by_proxmox(self): + config = (b'arch: amd64\n' + b'dev0: /dev/dri/renderD128,gid=993\n' + b'dev1: path=/dev/apex_0,mode=0660\n') + self.assertEqual(devices.actual_devices(config), { + '/dev/dri/renderD128': {'path': '/dev/dri/renderD128', 'gid': '993'}, + '/dev/apex_0': {'path': '/dev/apex_0', 'mode': '0660'}, + }) + + def test_the_same_device_in_both_forms_is_a_duplicate(self): + config = b'dev0: /dev/dri/renderD128\ndev1: path=/dev/dri/renderD128\n' + with self.assertRaises(ValueError): + devices.actual_devices(config) + + def test_adoption_names_the_line_it_cannot_read(self): + with self.assertRaisesRegex(ValueError, '^dev2: '): + reconcile._device('dev2', '/dev/dri/renderD128,993') + + +class NoPairParsingLeftTests(unittest.TestCase): + def test_no_reader_builds_the_fields_from_pairs_alone(self): + pattern = re.compile(r"dict\(\s*\w+\.split\('=', 1\) for \w+ in .*split\(','\)") + for path in sorted((ROOT / 'remote').glob('*.py')): + source = path.read_text() + for line in source.splitlines(): + if 'dev' in line or 'properties' in line: + with self.subTest(file=path.name): + self.assertIsNone(pattern.search(line), line) + + +class ShellDuplicateCheckTests(unittest.TestCase): + def check(self, config, path): + script = ('pct() { printf "%s" "$CONFIG"; }\n' + f'pct config 1 | grep -Eq "^dev[0-9]+: ((.*,)?path=)?{path}(,|$)"') + return subprocess.run(['bash', '-c', script], env={'CONFIG': config, 'PATH': '/usr/bin:/bin'}).returncode + + def test_the_installer_sees_a_device_already_added_in_either_form(self): + self.assertEqual(self.check('dev0: /dev/ttyUSB0,gid=20\n', '/dev/ttyUSB0'), 0) + self.assertEqual(self.check('dev0: mode=0660,path=/dev/ttyUSB0\n', '/dev/ttyUSB0'), 0) + self.assertEqual(self.check('dev0: /dev/ttyUSB1\n', '/dev/ttyUSB0'), 1) + self.assertEqual(self.check('dev0: /dev/ttyUSB01\n', '/dev/ttyUSB0'), 1) + + def test_the_installer_uses_that_check(self): + source = (ROOT / 'remote/oci_ui.sh').read_text() + self.assertIn('grep -Eq "^dev[0-9]+: ((.*,)?path=)?${path}(,|$)"', source) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_frigate_amd_profile.py b/oci/tests/test_frigate_amd_profile.py index ae121d10..447eae02 100644 --- a/oci/tests/test_frigate_amd_profile.py +++ b/oci/tests/test_frigate_amd_profile.py @@ -191,8 +191,10 @@ class FrigateExperimentalRocmTests(unittest.TestCase): self.assertEqual(plan["hardware_profile"], "vaapi") self.assertEqual(self.override(plan), []) - def test_a_gpu_the_image_cannot_use_is_not_offered_and_the_reason_is_said(self, *_): + def test_a_gpu_the_image_cannot_use_is_simply_not_offered(self, *_): + # The menu offers what works on the GPU; it does not explain ROCm. with patch("proxmenux_oci.installer.host.amd_gpu_name", return_value="Lucienne (gfx90c)"): ui, plan = self.build(90012, {}) self.assertNotIn("rocm", ui.labels[PROMPT]) - self.assertTrue(any("Lucienne (gfx90c)" in message and "not offered" in message for message in ui.messages)) + self.assertIn("vaapi", ui.labels[PROMPT]) + self.assertFalse(any("ROCm" in message for message in ui.messages)) diff --git a/oci/tests/test_gpu_device_gid.py b/oci/tests/test_gpu_device_gid.py new file mode 100644 index 00000000..d6f4d243 --- /dev/null +++ b/oci/tests/test_gpu_device_gid.py @@ -0,0 +1,76 @@ +"""Keep recorded LXC device-node GIDs distinct from host device ownership.""" +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'remote')) + +import oci_gpu_devices as devices +import oci_accelerators as accelerators + + +HOST = { + 'path': '/dev/dri/renderD128', 'major': 226, 'minor': 128, + 'uid': 0, 'gid': 993, 'mode': 0o660, + 'vendor': '0x8086', 'pci_path': '/sys/devices/pci0000:00/0000:00:02.0', +} +DEPLOYMENT = {'devices': [{ + 'kind': 'character-device', 'host_path': '/dev/dri/renderD128', + 'container_path': '/dev/dri/renderD128', 'gid_strategy': 'host-device-gid', + 'mode': '0660', 'drm_vendor_ids': ['0x8086', '0x1002'], +}]} +SAVED_CONFIG = 'dev0: path=/dev/dri/renderD128,mode=0660,deny-write=0,gid=44\n' +CURRENT_CONFIG = SAVED_CONFIG.encode() + + +class ContainerDeviceGidTests(unittest.TestCase): + @patch.object(devices, 'snapshot', return_value=HOST) + def test_recorded_guest_gid_is_carried_to_update_and_still_checks_host_identity(self, _snapshot): + record = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0])]}, + 'observed': {'config': SAVED_CONFIG}} + candidate = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0])]}} + + devices.preserve_recorded_container_gids(record, candidate, CURRENT_CONFIG) + + self.assertEqual(record['deployment']['devices'][0]['container_gid'], 44) + self.assertEqual(candidate['deployment']['devices'][0]['container_gid'], 44) + self.assertEqual(devices.check(CURRENT_CONFIG, record['deployment'])['/dev/dri/renderD128'], HOST) + self.assertEqual(devices.check(CURRENT_CONFIG, candidate['deployment'])['/dev/dri/renderD128'], HOST) + + @patch.object(devices, 'snapshot', return_value=HOST) + def test_unrecorded_guest_gid_change_is_rejected(self, _snapshot): + record = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0])]}, + 'observed': {'config': SAVED_CONFIG}} + candidate = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0])]}} + changed = CURRENT_CONFIG.replace(b'gid=44', b'gid=45') + + with self.assertRaisesRegex(ValueError, 'device settings changed'): + devices.preserve_recorded_container_gids(record, candidate, changed) + + @patch.object(devices, 'snapshot', return_value=HOST) + def test_host_gid_is_not_automatically_mapped_to_guest_gid_44(self, _snapshot): + with self.assertRaisesRegex(ValueError, 'native GPU permissions were not kept'): + devices.check(CURRENT_CONFIG, DEPLOYMENT) + + # Under Proxmox's default unprivileged map, guest GID 44 maps to host + # GID 100044; host GID 993 is outside that map. + self.assertEqual(100000 + 44, 100044) + self.assertFalse(100000 <= 993 < 165536) + + @patch.object(devices, 'snapshot', return_value=HOST) + def test_changed_host_device_identity_or_permissions_still_fail_baseline(self, snapshot): + baseline = {'/dev/dri/renderD128': HOST} + current = dict(HOST, gid=992) + snapshot.return_value = current + with self.assertRaisesRegex(ValueError, 'GPU identity or permissions changed'): + accelerators.verify_baseline(baseline, DEPLOYMENT) + + def test_container_gid_must_be_a_bounded_integer(self): + for invalid in (True, -1, 4294967295, '44'): + with self.subTest(invalid=invalid), self.assertRaisesRegex(ValueError, 'Invalid container device GID'): + devices.container_gid({'gid_strategy': 'host-device-gid', 'container_gid': invalid}, HOST) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_guessed_endpoint_healthcheck.py b/oci/tests/test_guessed_endpoint_healthcheck.py index b5393948..afbfa9aa 100644 --- a/oci/tests/test_guessed_endpoint_healthcheck.py +++ b/oci/tests/test_guessed_endpoint_healthcheck.py @@ -23,8 +23,8 @@ DECLARED = {'first_run': {'endpoints': [{'label': 'Web UI', 'scheme': 'http', 'p def probe_function(): source = INSTALLER.read_text() - start = source.index('healthcheck_probe() {') - end = source.index('\n}\n', start) + 3 + start = source.index('healthcheck_probe_url() {') + end = source.index('\n}\n', source.index('healthcheck_probe() {')) + 3 return source[start:end] @@ -38,7 +38,7 @@ class GuessedEndpointCheckTests(unittest.TestCase): fake = Path(directory) / 'curl' fake.write_text(f'#!/bin/sh\nprintf %s {code}\nexit {exit_code}\n') fake.chmod(0o755) - script = (probe_function() + '\noci_curl() { shift; curl "$@"; }\nCURL_ARGS=()\nHC_URL=http://192.0.2.1:3306/\n' + script = (probe_function() + '\noci_curl() { shift; curl "$@"; }\nCURL_ARGS=()\nHC_URLS=(http://192.0.2.1:3306/)\n' f'HC_ANY_STATUS={any_status}\nHC_ACCEPT_NON_HTTP={accept_non_http}\nhealthcheck_probe\n') env = {**os.environ, 'PATH': f'{directory}:{os.environ["PATH"]}'} return subprocess.run(['bash', '-c', script], env=env, capture_output=True).returncode @@ -83,5 +83,62 @@ class GuessedEndpointCheckTests(unittest.TestCase): transaction.healthcheck(113, template) +ADGUARD = {'id': 'image-adguard-home', + 'first_run': {'endpoints': [{'label': 'Web UI', 'scheme': 'http', 'port': 3000, 'path': '/'}]}} + + +class SetupThenWebPortTests(unittest.TestCase): + """AdGuard Home answers on 3000 until it is set up, then on 80: an update of + a configured installation must not wait for the setup port.""" + + def test_adguard_also_accepts_its_web_port(self): + check = transaction.effective_healthcheck(ADGUARD) + self.assertEqual((check['port'], check['alternative_ports']), (3000, [80])) + self.assertNotIn('alternative_ports', transaction.effective_healthcheck(DECLARED)) + + def test_a_check_saved_with_an_installation_takes_the_web_port_too(self): + # An installation stores the check it was given: still on 3000 only. + saved = {'scheme': 'http', 'port': 3000, 'path': '/', 'timeout_seconds': 300, 'accept_any_status': True} + template = dict(ADGUARD, proxmox={'installer_profile': {'startup_healthcheck': saved}}) + self.assertEqual(transaction.effective_healthcheck(template)['alternative_ports'], [80]) + self.assertNotIn('alternative_ports', saved) + other = dict(template, id='image-other') + self.assertNotIn('alternative_ports', transaction.effective_healthcheck(other)) + + def test_recovery_check_passes_on_the_web_port_when_setup_is_closed(self): + check = transaction.effective_healthcheck(ADGUARD) + check.update(timeout_seconds=6, stability_seconds=0) + template = {'proxmox': {'installer_profile': {'startup_healthcheck': check}}} + + def run(*args): + if args[:2] == ('pct', 'status'): + return b'status: running\n' + if args[:2] == ('pct', 'config'): + return b'' + if args[0] == 'lxc-info': + return b'192.0.2.10\n' + raise AssertionError(args) + + def probe(args): + return (b'200', 0) if args[-1].endswith(':80/') else (b'000', 7) + with patch.object(transaction, 'run', side_effect=run), \ + patch.object(transaction, 'probe_http', side_effect=probe), \ + patch.object(transaction, 'log'): + self.assertEqual(transaction.healthcheck(101, template), 'http://192.0.2.10:80/') + + def test_installer_probe_tries_every_address_and_keeps_the_one_that_answered(self): + with tempfile.TemporaryDirectory() as directory: + fake = Path(directory) / 'curl' + fake.write_text('#!/bin/sh\nfor last; do :; done\ncase "$last" in *:80/) printf 200; exit 0;; esac\n' + 'printf 000\nexit 7\n') + fake.chmod(0o755) + script = (probe_function() + '\noci_curl() { shift; curl "$@"; }\nCURL_ARGS=()\n' + 'HC_URLS=(http://192.0.2.1:3000/ http://192.0.2.1:80/)\nHC_ANY_STATUS=true\n' + 'healthcheck_probe && echo "$HC_URL"\n') + env = {**os.environ, 'PATH': f'{directory}:{os.environ["PATH"]}'} + done = subprocess.run(['bash', '-c', script], env=env, capture_output=True, text=True) + self.assertEqual((done.returncode, done.stdout.strip()), (0, 'http://192.0.2.1:80/')) + + if __name__ == '__main__': unittest.main() diff --git a/oci/tests/test_immich_acceleration.py b/oci/tests/test_immich_acceleration.py index 4a52f6e9..586dca5b 100644 --- a/oci/tests/test_immich_acceleration.py +++ b/oci/tests/test_immich_acceleration.py @@ -92,16 +92,21 @@ class ImmichAccelerationTests(unittest.TestCase): self.assertIn("No usable GPU", ui.messages[0]) self.assertEqual(result, ("cpu", "cpu"), mode) - def test_an_amd_host_that_cannot_run_rocm_offers_video_only_and_says_why(self, *_): - for blocker, text in (("kfd", "/dev/kfd"), ("space", "40 GB"), ("generation", "no support for the AMD GPU")): + def test_an_amd_host_that_cannot_run_rocm_offers_video_only(self, *_): + # What the host can fix is said; a GPU generation ROCm does not run is not. + for blocker, text in (("kfd", "/dev/kfd"), ("space", "40 GB"), ("generation", None)): with patch("proxmenux_oci.installer.host.rocm_blocker", return_value=blocker), \ patch("proxmenux_oci.installer.host.amd_gpu_name", return_value="Lucienne (gfx90c)"): ui, result, _ = self.plan(AMD, DEFAULT_MODE) self.assertEqual(ui.options[PROMPT], ["cpu", "amd-video"], blocker) self.assertEqual(ui.defaults[PROMPT], "amd-video", blocker) self.assertEqual(result, ("vaapi", "cpu"), blocker) - self.assertTrue(any(text in message and "Recognition is not offered" in message - for message in ui.messages), blocker) + if text: + self.assertTrue(any(text in message and "Recognition is not offered" in message + for message in ui.messages), blocker) + else: + self.assertFalse(any("ROCm" in message or "Recognition" in message + for message in ui.messages), blocker) def test_a_gpu_rocm_supports_is_proposed_whole(self, *_): for target in (100300, 110501, None): diff --git a/oci/tests/test_keep_backup_umask.py b/oci/tests/test_keep_backup_umask.py new file mode 100644 index 00000000..06294193 --- /dev/null +++ b/oci/tests/test_keep_backup_umask.py @@ -0,0 +1,86 @@ +"""PBS backup traversal modes must not loosen the OCI caller's private files.""" +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import textwrap +import unittest + +REMOTE = Path(__file__).resolve().parents[1] / 'remote' + + +class KeptBackupPermissions(unittest.TestCase): + def scenario(self, code, exit_code=0): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + binary = root / 'vzdump' + binary.write_text(f'#!{sys.executable}\n' + textwrap.dedent(f''' + import os + from pathlib import Path + import sys + assert sys.argv[1:] == ['4242', '--storage', 'pbs-fixture', '--mode', + 'snapshot', '--compress', 'zstd', '--notes-template', + 'ProxMenux OCI: before the image update'] + root = Path(os.environ['BACKUP_FIXTURE']) + (root / 'pve-tmp').mkdir() + (root / 'pve-tmp/pct.conf').write_text('fixture only') + sys.exit({exit_code}) + ''')) + binary.chmod(0o700) + script = textwrap.dedent(''' + import os + from pathlib import Path + import stat + import subprocess + import sys + sys.path.insert(0, sys.argv[1]) + import oci_instances as instances + import oci_keep_backup as backup + root = Path(os.environ['BACKUP_FIXTURE']) + backup.dump_dir = lambda storage: None + instances.write(root / 'private-before/record.json', {'secret': 'fixture'}) + ''') + textwrap.dedent(code) + result = subprocess.run( + [sys.executable, '-c', script, str(REMOTE)], + env={**os.environ, 'PATH': str(root) + os.pathsep + os.environ['PATH'], + 'BACKUP_FIXTURE': str(root)}, + umask=0o077, capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_pbs_child_is_readable_while_parent_records_stay_private(self): + self.scenario(''' + assert backup.before_update(4242, 'pbs-fixture') == 'pbs-fixture' + assert stat.S_IMODE((root / 'pve-tmp').stat().st_mode) == 0o755 + assert stat.S_IMODE((root / 'pve-tmp/pct.conf').stat().st_mode) == 0o644 + assert os.umask(0o077) == 0o077 + instances.write(root / 'private-after/record.json', {'secret': 'fixture'}) + for name in ('private-before', 'private-after'): + assert stat.S_IMODE((root / name).stat().st_mode) == 0o700 + assert stat.S_IMODE((root / name / 'record.json').stat().st_mode) == 0o600 + ''') + + def test_backup_failure_still_propagates_without_changing_parent_umask(self): + self.scenario(''' + try: + backup.before_update(4242, 'pbs-fixture') + except subprocess.CalledProcessError as error: + assert error.returncode == 7 + else: + raise AssertionError('backup failure was hidden') + assert os.umask(0o077) == 0o077 + assert stat.S_IMODE((root / 'private-before/record.json').stat().st_mode) == 0o600 + ''', exit_code=7) + + def test_file_storage_keeps_the_existing_no_extra_backup_route(self): + self.scenario(''' + backup.dump_dir = lambda storage: root / 'dump' + assert backup.before_update(4242, 'pbs-fixture') is None + assert not (root / 'pve-tmp').exists() + assert os.umask(0o077) == 0o077 + ''') + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_native_import_access.py b/oci/tests/test_native_import_access.py new file mode 100644 index 00000000..13f1dd3d --- /dev/null +++ b/oci/tests/test_native_import_access.py @@ -0,0 +1,181 @@ +"""Native mapped readers may read an image without exposing its private cache.""" +import errno +import importlib.util +import os +from pathlib import Path +import signal +import stat +import subprocess +import sys +import tempfile +import time +import types +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('native_import_access', ROOT / 'remote/oci_native_import.py') +imports = importlib.util.module_from_spec(spec) +spec.loader.exec_module(imports) + + +class PrivateImageImportTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.source = self.root / 'private-image.tar' + self.source.write_bytes(b'OCI image fixture\x00private layers') + self.source.chmod(0o600) + + def staging(self): + return patch.object(imports, 'trusted_staging_root', return_value=self.root) + + def test_normal_readable_image_uses_original_storage_volume_without_copy(self): + with patch.object(imports, 'readable', return_value=True), \ + patch.object(imports, '_copy', side_effect=AssertionError('must stay zero-copy')): + with imports.import_archive(self.source, 'local:vztmpl/public.tar', 100000, 100000) as path: + self.assertEqual(path, 'local:vztmpl/public.tar') + + def test_private_copy_preserves_source_and_limits_mapped_reader_group(self): + before = self.source.stat() + with self.staging(), patch.object(imports.os, 'chown') as chown, \ + patch.object(imports, 'readable', return_value=False): + with imports.import_archive(self.source, str(self.source), 100000, 100000) as path: + staged = Path(path) + self.assertEqual(staged.read_bytes(), self.source.read_bytes()) + self.assertEqual(stat.S_IMODE(staged.stat().st_mode), 0o440) + self.assertEqual(stat.S_IMODE(staged.parent.stat().st_mode), 0o750) + chown.assert_any_call(staged, 0, 100000) + chown.assert_any_call(staged.parent, 0, 100000) + self.assertFalse(staged.parent.exists()) + after = self.source.stat() + self.assertEqual((before.st_uid, before.st_gid, before.st_mode), + (after.st_uid, after.st_gid, after.st_mode)) + + def test_copy_error_cleans_stage_and_never_invokes_pct(self): + with self.staging(), patch.object(imports, 'readable', return_value=False), \ + patch.object(imports, '_copy', side_effect=OSError(errno.ENOSPC, 'fixture')), \ + patch.object(imports.subprocess, 'run') as run: + with self.assertRaises(OSError): + imports.create('9161', str(self.source), str(self.source), ['--unprivileged', '1']) + run.assert_not_called() + self.assertEqual(list(self.root.iterdir()), [self.source]) + + def test_pct_failure_and_interrupt_both_clean_stage(self): + for error in (None, KeyboardInterrupt()): + with self.staging(), patch.object(imports.os, 'chown'), \ + patch.object(imports, 'readable', return_value=False), \ + patch.object(imports.subprocess, 'run', side_effect=error, + return_value=types.SimpleNamespace(returncode=23)) as run: + if error: + with self.assertRaises(KeyboardInterrupt): + imports.create('9161', str(self.source), str(self.source), ['--unprivileged', '1']) + else: + self.assertEqual(imports.create('9161', str(self.source), str(self.source), + ['--unprivileged', '1']), 23) + self.assertEqual(run.call_args.kwargs['umask'], 0o022) + self.assertEqual(list(self.root.iterdir()), [self.source]) + + def test_standard_map_and_privileged_profile_initial_unprivileged_create(self): + for options in (['--unprivileged', '1'], ['--unprivileged=1'], ['--unprivileged', 'true']): + self.assertEqual(imports.reader_ids(options), (100000, 100000)) + self.assertEqual(imports.reader_ids(['--unprivileged', '0']), (0, 0)) + installer = (ROOT / 'remote/install_oci.sh').read_text() + self.assertIn('CREATE_UNPRIVILEGED_FLAG=1', installer) + self.assertIn('--unprivileged "$CREATE_UNPRIVILEGED_FLAG"', installer) + + def test_derived_numeric_user_image_is_staged_without_mutating_its_source(self): + derived = self.root / 'image.user-databases.123.tar' + derived.write_bytes(b'derived OCI image') + derived.chmod(0o600) + with self.staging(), patch.object(imports.os, 'chown'), \ + patch.object(imports, 'readable', return_value=False): + with imports.import_archive(derived, str(derived), 100000, 100000) as path: + self.assertEqual(Path(path).read_bytes(), derived.read_bytes()) + self.assertEqual(stat.S_IMODE(derived.stat().st_mode), 0o600) + + def test_reflink_unavailable_falls_back_to_an_identical_copy(self): + target = self.root / 'copy.tar' + with patch.object(imports.fcntl, 'ioctl', side_effect=OSError(errno.EOPNOTSUPP, 'fixture')): + imports._copy(self.source, target) + self.assertEqual(target.read_bytes(), self.source.read_bytes()) + + def test_local_and_ssh_installer_stage_the_native_helper(self): + import ast + tree = ast.parse((ROOT / 'src/proxmenux_oci/installer.py').read_text()) + function = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == '_run_remote_install') + lists = [n.iter for n in ast.walk(function) if isinstance(n, ast.For) + and isinstance(n.iter, ast.Tuple) + and any(isinstance(v, ast.Constant) and v.value == 'oci_ui.sh' for v in n.iter.elts)] + self.assertEqual(len(lists), 2) + for values in lists: + self.assertIn('oci_native_import.py', [v.value for v in values.elts]) + self.assertIn('oci_native_import.py', (ROOT / 'remote/oci_ui.sh').read_text()) + + +@unittest.skipUnless(sys.platform == 'linux' and os.geteuid() == 0, + 'isolated mapped UID/GID proof runs as root in Linux CI') +class MappedReaderProof(unittest.TestCase): + def reader(self, path, uid, gid): + return subprocess.run([sys.executable, '-c', + 'import os,sys; os.setgroups([]); os.setgid(int(sys.argv[2])); ' + 'os.setuid(int(sys.argv[1])); print(open(sys.argv[3],"rb").read().decode())', + str(uid), str(gid), str(path)], capture_output=True, text=True) + + def test_mapped_reader_only_private_source_and_caller_state_are_preserved(self): + with tempfile.TemporaryDirectory(dir='/var/tmp') as folder: + root = Path(folder) + root.chmod(0o700) + source = root / 'private.tar' + source.write_text('private image contents') + source.chmod(0o600) + self.assertNotEqual(self.reader(source, 100000, 100000).returncode, 0) + previous = os.umask(0o077) + try: + with imports.import_archive(source, str(source), 100000, 100000) as staged: + path = Path(staged) + self.assertEqual(self.reader(path, 100000, 100000).stdout.strip(), 'private image contents') + self.assertNotEqual(self.reader(path, 100001, 100001).returncode, 0) + self.assertFalse(path.stat().st_mode & 0o022) + self.assertEqual(path.stat().st_uid, 0) + self.assertFalse(path.parent.exists()) + record = root / 'record.json' + record.write_text('private credentials fixture') + self.assertEqual(stat.S_IMODE(record.stat().st_mode), 0o600) + self.assertEqual(os.umask(0o077), 0o077) + self.assertEqual(stat.S_IMODE(source.stat().st_mode), 0o600) + self.assertEqual(stat.S_IMODE(root.stat().st_mode), 0o700) + finally: + os.umask(previous) + + def test_sigterm_removes_staging_and_preserves_native_output_and_status(self): + with tempfile.TemporaryDirectory(dir='/var/tmp') as folder: + root = Path(folder) + source = root / 'private.tar'; source.write_text('private image'); source.chmod(0o600) + pct = root / 'pct' + pct.write_text('#!/bin/sh\nmkdir "$FIXTURE/pve-tmp"\ntouch "$FIXTURE/pve-tmp/pct.conf"\nprintf "%s\\n" "$3" >"$MARKER"\necho native-output\nexec sleep 60\n') + pct.chmod(0o700) + marker = root / 'marker' + env = dict(os.environ, PATH=str(root) + ':' + os.environ['PATH'], MARKER=str(marker), FIXTURE=str(root)) + process = subprocess.Popen([sys.executable, str(ROOT / 'remote/oci_native_import.py'), + '9161', str(source), str(source), '--unprivileged', '1'], env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, umask=0o077) + try: + for _ in range(100): + if marker.exists(): break + time.sleep(0.05) + self.assertTrue(marker.exists()) + staged = Path(marker.read_text().strip()) + self.assertTrue(staged.exists()) + self.assertEqual(stat.S_IMODE((root / "pve-tmp").stat().st_mode), 0o755) + self.assertEqual(stat.S_IMODE((root / "pve-tmp/pct.conf").stat().st_mode), 0o644) + process.send_signal(signal.SIGTERM) + # Do not leave a shell sleep grandchild holding capture pipes open. + output, error = process.communicate(timeout=5) + self.assertIn("native-output", output) + self.assertEqual(process.returncode, 143) + self.assertFalse(staged.parent.exists()) + finally: + if process.poll() is None: process.kill(); process.wait() + process.stdout.close(); process.stderr.close() diff --git a/oci/tests/test_registry_accounts.py b/oci/tests/test_registry_accounts.py index 55faeea3..37235012 100644 --- a/oci/tests/test_registry_accounts.py +++ b/oci/tests/test_registry_accounts.py @@ -132,7 +132,8 @@ class RegistryMenuTests(unittest.TestCase): refresher = refresh_type.return_value.__enter__.return_value refresher.active = False refresher.consume.return_value = False - refresher.describe.return_value = '' + refresher.failed = False + refresher.label.return_value = 'Refresh catalog' self.assertEqual(cli.interactive(catalog), 0) open_accounts.assert_called_once_with(ui) diff --git a/oci/tests/test_selkies_base_images.py b/oci/tests/test_selkies_base_images.py new file mode 100644 index 00000000..9c949bba --- /dev/null +++ b/oci/tests/test_selkies_base_images.py @@ -0,0 +1,50 @@ +"""An image built on the LinuxServer Selkies base, but published by someone +else, receives the same GPU profile as the LinuxServer Selkies images.""" +import copy +import json +from pathlib import Path +import sys +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'src')) + +from proxmenux_oci.catalog import Catalog +from proxmenux_oci.gpu import apply_selkies_contract + +ARTCRAFT = ('cadcraft', 'deckcraft', 'designcraft', 'effectcraft', 'figcraft', 'filmcraft', 'gridcraft', + 'lightcraft', 'pdfcraft', 'photocraft', 'soundcraft', 'vectorcraft', 'wordcraft') + + +def template(reference, base): + return {'container_contract': {'image': {'reference': reference}, 'environment': []}, + 'proxmox': {'installer_profile': {'selkies': {'base': base}}}} + + +class SelkiesBaseImageTests(unittest.TestCase): + def test_an_image_built_on_the_selkies_base_gets_the_gpu_profile(self): + built = template('docker.io/junkerderprovinz/photocraft:latest', + 'FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute') + apply_selkies_contract(built) + profiles = built['proxmox']['installer_profile']['hardware_acceleration']['profiles'] + self.assertEqual([item['id'] for item in profiles], ['none', 'vaapi', 'nvidia']) + + def test_any_other_image_is_still_refused(self): + other = template('docker.io/someone/desktop:latest', 'FROM ubuntu:24.04') + with self.assertRaises(ValueError): + apply_selkies_contract(copy.deepcopy(other)) + + def test_every_artcraft_app_offers_the_gpu_to_its_desktop(self): + catalog = Catalog(ROOT) + for app in ARTCRAFT: + with self.subTest(app=app): + profile = catalog.compose(app)['proxmox']['installer_profile'] + vaapi = next(item for item in profile['hardware_acceleration']['profiles'] if item['id'] == 'vaapi') + self.assertEqual(set(vaapi['environment_from_devices']), + {'DRINODE', 'DRI_NODE', 'ATTACHED_DEVICES_PERMS'}) + self.assertEqual(profile['device_permissions']['environment'], 'ATTACHED_DEVICES_PERMS') + self.assertEqual(profile['hardware_acceleration']['default'], 'none') + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_stack_https_readiness.py b/oci/tests/test_stack_https_readiness.py new file mode 100644 index 00000000..140b9290 --- /dev/null +++ b/oci/tests/test_stack_https_readiness.py @@ -0,0 +1,82 @@ +"""Real self-signed image HTTPS readiness, without weakening strict probes.""" +import copy +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +import shutil +import ssl +import subprocess +import sys +import tempfile +import threading +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'remote')) +import install_generic_stack as installer + + +@unittest.skipUnless(shutil.which('openssl'), 'openssl needed for local TLS fixture') +class HTTPSReadiness(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + cert, key = (Path(cls.directory.name) / name for name in ('cert.pem', 'key.pem')) + subprocess.run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', + '-keyout', str(key), '-out', str(cert), '-days', '1', + '-subj', '/CN=localhost'], check=True, capture_output=True) + class Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(503 if self.path == '/unhealthy' else 200) + self.end_headers() + def log_message(self, *args): + pass + cls.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert, key) + cls.server.socket = context.wrap_socket(cls.server.socket, server_side=True) + cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) + cls.thread.start() + cls.url = f'https://127.0.0.1:{cls.server.server_port}' + + @classmethod + def tearDownClass(cls): + cls.server.shutdown() + cls.server.server_close() + cls.thread.join() + cls.directory.cleanup() + + def probe(self, *, verify_tls=False, path='/'): + service = {'vmid': 9070, 'name': 'test', 'healthcheck': { + 'type': 'http', 'timeout_seconds': 1, 'verify_tls': verify_tls}} + # Only Proxmox command discovery is mocked; TLS and HTTP are real. + with patch.object(installer, 'run', return_value='status: running'), \ + patch('oci_installation_state.container_curl', return_value=['curl']): + installer.wait_web(service, self.url + path) + + def test_image_generated_certificate_passes_readiness(self): + self.probe() + + def test_explicit_strict_tls_still_rejects_self_signed_certificate(self): + with self.assertRaisesRegex(RuntimeError, 'HTTP check'): + self.probe(verify_tls=True) + + def test_tls_tolerance_does_not_accept_http_failure(self): + with self.assertRaisesRegex(RuntimeError, 'HTTP check'): + self.probe(path='/unhealthy') + + +class NamespaceTLSReadiness(unittest.TestCase): + def test_vlan_curl_obeys_the_same_verification_policy(self): + for strict in (False, True): + with self.subTest(strict=strict), \ + patch('oci_installation_state.container_curl', return_value=['nsenter', '--net=/proc/123/ns/net', 'curl']), \ + patch.object(installer, 'run') as run: + installer.web_answers(9070, 'https://192.0.2.7/', None, verify_tls=strict) + args = run.call_args.args + self.assertEqual('--insecure' in args, not strict) + self.assertIn('-fsS', args) + self.assertIn('--noproxy', args) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_update_menu.py b/oci/tests/test_update_menu.py index 05fa85d5..84ef3274 100644 --- a/oci/tests/test_update_menu.py +++ b/oci/tests/test_update_menu.py @@ -280,9 +280,9 @@ class MainMenuTests(unittest.TestCase): checker.running = False; checker.done = True raise MenuRefresh() return None - catalog_refresh = Mock(active=False) + catalog_refresh = Mock(active=False, failed=False) catalog_refresh.consume.return_value = False - catalog_refresh.describe.return_value = 'Catalog: 0 applications' + catalog_refresh.label.return_value = 'Refresh catalog' catalog_context = Mock() catalog_context.__enter__ = Mock(return_value=catalog_refresh) catalog_context.__exit__ = Mock(return_value=False) @@ -290,8 +290,10 @@ class MainMenuTests(unittest.TestCase): patch.object(cli, 'interactive_ui', return_value=UI()), \ patch.object(updates, 'UpdateCheck', return_value=checker): self.assertEqual(cli.interactive(cli.Catalog(ROOT)), 0) - self.assertEqual(seen, ['Check for updates', 'Checking for updates (0/2)', - 'Checking for updates (1/2)', 'Updates available (2)']) + # A count lines up with the counts of the categories; progress does not. + self.assertEqual(seen[:3], ['Check for updates', 'Checking for updates (0/2)', + 'Checking for updates (1/2)']) + self.assertRegex(seen[3], r'^Updates available {2,}\( 2\)$') class TranslationTests(unittest.TestCase): diff --git a/scripts/gpu_tpu/nvidia_installer.sh b/scripts/gpu_tpu/nvidia_installer.sh index a296c786..53e2fcd9 100644 --- a/scripts/gpu_tpu/nvidia_installer.sh +++ b/scripts/gpu_tpu/nvidia_installer.sh @@ -866,11 +866,9 @@ KEYLASE_PATCH_URL="https://raw.githubusercontent.com/keylase/nvidia-patch/master NVIDIA_BRANCHES_CACHE="/var/cache/proxmenux/nvidia_stable_branches.txt" NVIDIA_PRODUCTION_HEAD_CACHE="/var/cache/proxmenux/nvidia_production_head.txt" NVIDIA_BRANCH_HEADS_CACHE="/var/cache/proxmenux/nvidia_branch_heads.txt" -# v2: the first generation of these files scraped the whole page and so -# included the legacy table (see _nvidia_supported_ids_from_page). Those -# files say a branch supports GPUs it has dropped, so they are left -# behind rather than reused. -NVIDIA_GPU_SUPPORT_CACHE_PREFIX="/var/cache/proxmenux/nvidia_gpu_support_v2_" +# v3: discard older caches, which either included legacy GPUs (v1) or +# omitted devices listed only with subsystem qualifiers (v2). +NVIDIA_GPU_SUPPORT_CACHE_PREFIX="/var/cache/proxmenux/nvidia_gpu_support_v3_" NVIDIA_BRANCHES_TTL_SECONDS=$((24 * 3600)) NVIDIA_BRANCHES_URL="https://www.nvidia.com/en-us/drivers/unix/" @@ -975,13 +973,17 @@ get_nvidia_branch_head() { # card NVIDIA ever shipped — which is how a Quadro P1000 came to be # offered the 595 branch that refuses it at probe time, after compiling # and installing perfectly. Everything from that heading on is cut away. +# NVIDIA identifies each GPU row as devidXXXX or devidXXXX_VENDOR_SUBDEVICE. +# Read the primary device ID from that identifier, not an exact four-digit +# table cell: T400/T1000 variants have three IDs in the cell. Subsystem IDs +# and GPU names such as A100 must not become independent supported devices. _nvidia_supported_ids_from_page() { awk ' /legacy GPUs that are no longer supported/ { exit } { print } ' \ - | grep -aoiE '[0-9A-F]{4}' \ - | grep -aoiE '[0-9A-F]{4}' \ + | grep -aoiE '/dev/null) versions_list=$(list_available_versions 2>/dev/null) if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then - stop_spinner - hybrid_msgbox "$(translate 'Error')" \ - "$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80 - DRIVER_VERSION="cancel" return 1 fi @@ -2115,6 +2116,47 @@ show_version_menu() { latest=$(printf '%s\n' "$current_list" | head -n1 | tr -d '[:space:]') fi + NVIDIA_OFFERED_VERSIONS="$current_list" + NVIDIA_RECOMMENDED_VERSION="$latest" + NVIDIA_PATCH_FILTERED="$patch_filtered" + NVIDIA_PATCH_FILTER_NOTE="$patch_filter_note" +} + +# For ProxMenux Monitor: the version the menu would recommend on this host for +# the driver in use (passed in, or read from nvidia-smi), with no interface and +# no change to the host. Prints nothing when none is recommended. +print_recommended_version() { + detect_nvidia_gpus + $NVIDIA_GPU_PRESENT || return 1 + CURRENT_DRIVER_VERSION="${1:-}" + if [[ -z "$CURRENT_DRIVER_VERSION" ]] && command -v nvidia-smi >/dev/null 2>&1; then + CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) + fi + compute_nvidia_offer || return 2 + [[ -z "$NVIDIA_RECOMMENDED_VERSION" ]] || printf '%s\n' "$NVIDIA_RECOMMENDED_VERSION" +} + +show_version_menu() { + local latest versions_list + local kernel_version + kernel_version=$(uname -r) + + show_proxmenux_logo + msg_title "$(translate 'NVIDIA GPU Driver Installation')" + msg_info "$(translate 'Fetching NVIDIA driver versions supported by your GPU...')" + + if ! compute_nvidia_offer; then + stop_spinner + hybrid_msgbox "$(translate 'Error')" \ + "$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80 + DRIVER_VERSION="cancel" + return 1 + fi + local current_list="$NVIDIA_OFFERED_VERSIONS" + local patch_filtered="$NVIDIA_PATCH_FILTERED" + local patch_filter_note="$NVIDIA_PATCH_FILTER_NOTE" + latest="$NVIDIA_RECOMMENDED_VERSION" + local menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n" menu_text+="$(translate 'Versions shown belong to maintained NVIDIA branches that list your GPU PCI ID and are new enough to build against the running kernel. DKMS compilation is the final validation. The recommended version keeps the current branch, or uses the NVIDIA Production Branch on a fresh install.')" menu_text+="\n\n$(translate 'Running kernel:') ${kernel_version} — $(translate 'oldest driver offered:') $(nvidia_kernel_driver_floor)" @@ -2546,5 +2588,9 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then auto_reinstall_from_state exit $? fi + if [[ "${1:-}" == "--recommended-version" ]]; then + print_recommended_version "${2:-}" + exit $? + fi main fi diff --git a/scripts/oci/oci_manager_apps.sh b/scripts/oci/oci_manager_apps.sh index 67546cb9..6e6b2143 100755 --- a/scripts/oci/oci_manager_apps.sh +++ b/scripts/oci/oci_manager_apps.sh @@ -32,8 +32,11 @@ initialize_cache ensure_oci_dependencies() { local -a missing=() - python3 -c 'import yaml' >/dev/null 2>&1 || missing+=("python3-yaml") - python3 -c 'import jsonschema' >/dev/null 2>&1 || missing+=("python3-jsonschema") + # One Python start when both are present; each is checked only if not. + if ! python3 -c 'import yaml, jsonschema' >/dev/null 2>&1; then + python3 -c 'import yaml' >/dev/null 2>&1 || missing+=("python3-yaml") + python3 -c 'import jsonschema' >/dev/null 2>&1 || missing+=("python3-jsonschema") + fi command -v skopeo >/dev/null 2>&1 || missing+=("skopeo") [[ ${#missing[@]} -eq 0 ]] && return 0 @@ -57,12 +60,15 @@ ensure_oci_dependencies() { stop_spinner } -# Proxmox VE runs OCI images as LXC containers from 9.1 on. +# Proxmox VE runs OCI images as LXC containers from 9.1 on. The package +# database answers at once; pveversion takes about half a second. +pve_manager_version() { + dpkg-query -W -f='${Version}' pve-manager 2>/dev/null | grep -oE '^[0-9]+(\.[0-9]+)+' | head -1 +} + pve_supports_oci() { - local version major minor - version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1) - major=${version%%.*} - minor=${version#*.} + local major minor + IFS=. read -r major minor _ <<< "$(pve_manager_version)" [[ $major =~ ^[0-9]+$ && $minor =~ ^[0-9]+$ ]] || return 1 (( major > 9 || (major == 9 && minor >= 1) )) } @@ -71,7 +77,7 @@ if ! pve_supports_oci; then show_proxmenux_logo msg_title "$(translate "OCI manager Apps (Technology Preview)")" msg_error "$(translate "OCI containers require Proxmox VE 9.1 or later.")" - PVE_VERSION=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9.]+' | head -1) + PVE_VERSION=$(pve_manager_version) [[ -n $PVE_VERSION ]] && msg_info2 "$(translate "Current version:") $PVE_VERSION" if [[ -t 0 ]]; then echo diff --git a/tests/test_nvidia_monitor_uses_installer.py b/tests/test_nvidia_monitor_uses_installer.py new file mode 100644 index 00000000..202f46b5 --- /dev/null +++ b/tests/test_nvidia_monitor_uses_installer.py @@ -0,0 +1,91 @@ +"""ProxMenux Monitor announces the NVIDIA driver the installer would recommend, +worked out by the same installer function the version menu uses.""" +from pathlib import Path +import re +import subprocess +import sys +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +INSTALLER = (ROOT / "scripts/gpu_tpu/nvidia_installer.sh").read_text() +sys.path.insert(0, str(ROOT / "AppImage/scripts")) + +import managed_installs # noqa: E402 + + +def function(name): + return re.search(rf"^{name}\(\) \{{\n.*?^\}}\n", INSTALLER, re.M | re.S).group(0) + + +def answer(stdout, returncode=0): + return subprocess.CompletedProcess([], returncode, stdout, "") + + +class MonitorAsksTheInstallerTests(unittest.TestCase): + def setUp(self): + installer = patch.object(managed_installs, "_NVIDIA_INSTALLER", str(ROOT / "scripts/gpu_tpu/nvidia_installer.sh")) + installer.start() + self.addCleanup(installer.stop) + + def check(self, current, stdout, returncode=0): + with patch.object(managed_installs.subprocess, "run", return_value=answer(stdout, returncode)) as run: + result = managed_installs._check_nvidia_xfree86({"current_version": current}) + return result, run + + def test_an_older_installer_is_not_run(self): + import tempfile + with tempfile.NamedTemporaryFile("w", suffix=".sh") as old: + old.write("main\n"); old.flush() + with patch.object(managed_installs, "_NVIDIA_INSTALLER", old.name), \ + patch.object(managed_installs.subprocess, "run") as run: + result = managed_installs._check_nvidia_xfree86({"current_version": "595.80"}) + run.assert_not_called() + self.assertFalse(result["available"]) + self.assertIn("older", result["error"]) + + def test_the_recommended_version_is_announced_and_the_host_tools_are_used(self): + with patch.dict(managed_installs.os.environ, {"LD_LIBRARY_PATH": "/appimage/lib"}): + result, run = self.check("595.80", "595.112.07\n") + self.assertEqual((result["available"], result["latest"], result["_upgrade_kind"]), + (True, "595.112.07", "patch")) + argv, kwargs = run.call_args.args[0], run.call_args.kwargs + self.assertEqual(argv[-2:], ["--recommended-version", "595.80"]) + self.assertNotIn("LD_LIBRARY_PATH", kwargs["env"]) + + def test_nothing_is_announced_when_the_installer_recommends_the_installed_driver(self): + result, _ = self.check("580.196.07", "580.196.07\n") + self.assertEqual((result["available"], result["latest"], result["error"]), (False, None, None)) + + def test_nothing_is_announced_when_the_installer_offers_nothing(self): + # A GPU no maintained branch supports: the installer recommends nothing. + result, _ = self.check("470.256.02", "") + self.assertEqual((result["available"], result["error"]), (False, None)) + + def test_a_failed_version_list_is_an_error_not_an_update(self): + result, _ = self.check("595.80", "", returncode=2) + self.assertFalse(result["available"]) + self.assertIn("could not read", result["error"]) + + def test_a_branch_change_is_marked(self): + result, _ = self.check("570.211.01", "595.112.07\n") + self.assertEqual(result["_upgrade_kind"], "branch") + + +class OneCalculationTests(unittest.TestCase): + def test_the_menu_and_the_monitor_mode_share_the_calculation(self): + self.assertIn("compute_nvidia_offer", function("show_version_menu")) + self.assertIn("compute_nvidia_offer", function("print_recommended_version")) + self.assertNotIn("download.nvidia.com", (ROOT / "AppImage/scripts/managed_installs.py").read_text()) + + def test_the_monitor_mode_prints_only_the_recommendation(self): + stubs = ('detect_nvidia_gpus() { NVIDIA_GPU_PRESENT=true; }\n' + 'compute_nvidia_offer() { NVIDIA_RECOMMENDED_VERSION="595.112.07"; }\n') + done = subprocess.run(["bash", "-c", stubs + function("print_recommended_version") + + "print_recommended_version 595.80"], + capture_output=True, text=True) + self.assertEqual((done.returncode, done.stdout), (0, "595.112.07\n")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_nvidia_supported_gpu_ids.py b/tests/test_nvidia_supported_gpu_ids.py new file mode 100644 index 00000000..a50d7b43 --- /dev/null +++ b/tests/test_nvidia_supported_gpu_ids.py @@ -0,0 +1,72 @@ +"""Parse NVIDIA device rows without confusing subsystem or legacy IDs.""" +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +SOURCE = (Path(__file__).resolve().parents[1] / "scripts/gpu_tpu/nvidia_installer.sh").read_text() + + +def function(name): + return re.search(rf"^{name}\(\) \{{\n.*?^\}}\n", SOURCE, re.M | re.S).group(0) + + +class SupportedGpuIdsTests(unittest.TestCase): + def parse(self, html): + result = subprocess.run( + ["bash", "-c", function("_nvidia_supported_ids_from_page") + + "_nvidia_supported_ids_from_page"], + input=html, text=True, capture_output=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + return result.stdout.splitlines() + + def test_subsystem_qualified_t400_and_t1000_are_supported(self): + # NVIDIA 595.112.07 lists these devices only in qualified rows. + self.assertEqual(self.parse(''' +NVIDIA T1000 8GB +1FF0 1028 1612 +NVIDIA T400 4GB +1FF2 10DE 18FF +'''), ["1ff0", "1ff2"]) + + def test_unqualified_and_duplicate_rows_are_sorted_and_deduplicated(self): + self.assertEqual(self.parse(''' +Quadro RTX 60001E30 +Quadro RTX 8000 +A10020B0 +'''), ["1e30", "20b0"]) + + def test_legacy_rows_and_unrelated_hex_cells_are_excluded(self): + self.assertEqual(self.parse(''' +A10020B0 +1234 +

Below are the legacy GPUs that are no longer supported in the +current driver.

+Quadro P10001CB1 +'''), ["20b0"]) + + def test_cache_refresh_does_not_reuse_fresh_v2_device_list(self): + prefix = re.search(r'^NVIDIA_GPU_SUPPORT_CACHE_PREFIX="([^"]+)"', SOURCE, re.M).group(1) + self.assertTrue(prefix.endswith("_v3_")) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "nvidia_gpu_support_v2_595.txt").write_text("1e30\n") + script = '\n'.join([ + 'NVIDIA_GPU_SUPPORT_CACHE_PREFIX="' + str(root / Path(prefix).name) + '"', + 'NVIDIA_BRANCHES_TTL_SECONDS=86400', + 'get_nvidia_branch_head() { echo 595.112.07; }', + 'curl() { echo \'1FF2 10DE 18FF\'; }', + function("_nvidia_supported_ids_from_page"), + function("refresh_nvidia_gpu_support_for_major"), + 'refresh_nvidia_gpu_support_for_major 595', + ]) + result = subprocess.run(["bash", "-c", script], text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((root / "nvidia_gpu_support_v3_595.txt").read_text(), "1ff2\n") + + +if __name__ == "__main__": + unittest.main()