Merge develop into feature/oci-nextcloud-mariadb

This commit is contained in:
MacRimi
2026-11-05 19:21:29 +01:00
115 changed files with 9340 additions and 336 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Reject foreign ELF files in a native AppDir without executing any binary."""
import argparse
from pathlib import Path
import struct
def check(directory, architecture):
expected = {'x86_64': 62, 'aarch64': 183}[architecture]
checked = 0
for path in Path(directory).rglob('*'):
if path.is_symlink() or not path.is_file():
continue
with path.open('rb') as stream:
header = stream.read(20)
if not header.startswith(b'\x7fELF'):
continue
if len(header) != 20 or header[5] not in (1, 2):
raise ValueError(f'Invalid ELF header: {path}')
machine = struct.unpack('<H' if header[5] == 1 else '>H', header[18:20])[0]
if machine != expected:
raise ValueError(f'Foreign ELF machine {machine} in {architecture} AppDir: {path}')
checked += 1
if not checked:
raise ValueError('No native ELF files found in AppDir')
return checked
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('directory', type=Path)
parser.add_argument('architecture', choices=('x86_64', 'aarch64'))
args = parser.parse_args()
print(f'PASS: {check(args.directory, args.architecture)} native ELF files')
+301
View File
@@ -0,0 +1,301 @@
"""Disposable QEMU-only real-kernel USB/IP regression lab. Never use on a host."""
import contextlib, importlib.util, os, select, socket, subprocess, sys, tempfile, termios, time, tty
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts/devices"))
import usbip_manager as m
from usbip_protocol import Gate
def run(*args):
result = subprocess.run(args, text=True, capture_output=True)
assert result.returncode == 0, (args, result.stdout, result.stderr)
return result.stdout
def put(p, v):
Path(p).write_text(v)
def wait(test):
for _ in range(100):
value = test()
if value:
return value
time.sleep(0.05)
raise AssertionError("condition timed out")
def gadget(name, serial, udc):
g = Path("/sys/kernel/config/usb_gadget") / name
g.mkdir()
for f, v in [("idVendor", "0x1d6b"), ("idProduct", "0x0104")]:
put(g / f, v)
(g / "strings/0x409").mkdir()
for f, v in [
("serialnumber", serial),
("manufacturer", "Regression lab"),
("product", name),
]:
put(g / "strings/0x409" / f, v)
(g / "configs/c.1").mkdir()
(g / "functions/acm.usb0").mkdir()
(g / "configs/c.1/acm.usb0").symlink_to(g / "functions/acm.usb0")
put(g / "UDC", udc)
return g
def find(serial):
return next((d for d in m.inventory() if d["serial"] == serial), None)
def chosen(serial, id):
return dict(wait(lambda: find(serial)), id=id)
def control(port, busid):
import struct
with socket.create_connection(("127.0.0.1", port)) as s:
s.sendall(
struct.pack("!HHI", 0x111, 0x8003, 0) + busid.encode().ljust(32, b"\0")
)
reply = s.recv(8)
assert len(reply) == 8
return struct.unpack("!HHI", reply)[2]
def main():
assert sys.argv[1:] == [
"--disposable-vm"
], "Requires --disposable-vm on a fresh throwaway QEMU VM"
assert (
os.geteuid() == 0 and "QEMU" in Path("/sys/class/dmi/id/sys_vendor").read_text()
)
assert not Path("/sys/kernel/config/usb_gadget").exists() or not list(
Path("/sys/kernel/config/usb_gadget").iterdir()
), "Start with a fresh disposable VM"
run("modprobe", "dummy_hcd", "num=3")
run("modprobe", "libcomposite")
run("modprobe", "usbip-host")
run("modprobe", "vhci-hcd")
udcs = sorted(p.name for p in Path("/sys/class/udc").iterdir())
assert len(udcs) == 3
a_udc = udcs[0]
a = gadget("selected-a", "original-a", udcs[0])
b = gadget("selected-b", "original-b", udcs[1])
time.sleep(0.5)
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
m.CONFIG_DIR = root / "config"
m.SERVER_RUN_DIR = root / "run"
m.RUN_DIR = root / "client"
selected_a = chosen("original-a", "abcdef123456")
selected_b = chosen("original-b", "fedcba654321")
config = {"server": True, "exports": [selected_a, selected_b], "imports": []}
m.atomic_json(m.CONFIG_DIR / "config.json", config)
owned = {}
leases = {}
daemon = subprocess.Popen(
["usbipd", "-4"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
gate = None
proxy = None
try:
assert not (errors := m.reconcile_exports(config, owned, leases)), errors
def claim(name):
for selected in config["exports"]:
id = selected["id"]
lease = leases.get(id)
record = owned.get(id)
if lease and lease.ready and record and record["busid"] == name:
return dict(record, id=id)
gate = Gate(root / "gate.sock", 0, claim)
proxy = subprocess.Popen(
[
"socat",
"TCP-LISTEN:33240,bind=127.0.0.1,reuseaddr,fork",
"UNIX-CONNECT:" + str(root / "gate.sock"),
]
)
time.sleep(0.2)
# A native independent client remains attached throughout A's physical cycle.
print(
run(
"usbip",
"--tcp-port",
"33240",
"attach",
"-r",
"127.0.0.1",
"-b",
selected_b["busid"],
),
flush=True,
)
def serial_transfer(gadget=b, serial="original-b"):
# Gadget B's function tty and remote vhci serial port, not local A's tty.
gadget_tty = (
"/dev/ttyGS"
+ (gadget / "functions/acm.usb0/port_num").read_text().strip()
)
client = wait(
lambda: next(
(
p
for p in Path("/sys/class/tty").glob("ttyACM*")
if "vhci_hcd" in str(p.resolve())
and any(
(q / "serial").exists()
and (q / "serial").read_text().strip() == serial
for q in p.resolve().parents
)
),
None,
)
)
with contextlib.ExitStack() as stack:
fds = [
os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
for path in (gadget_tty, "/dev/" + client.name)
]
for fd in fds:
stack.callback(os.close, fd)
tty.setraw(fd)
termios.tcflush(fd, termios.TCIOFLUSH)
payload = b"independent-b-still-transferring\n"
os.write(fds[0], payload)
assert select.select([fds[1]], [], [], 3)[0]
assert os.read(fds[1], len(payload)) == payload
serial_transfer()
for iteration in range(20):
put(a / "UDC", "\n")
time.sleep(0.03)
m.reconcile_exports(config, owned, leases)
assert selected_a["busid"] not in m.MATCH_BUSID.read_text().split()
put(a / "UDC", a_udc)
wait(lambda: find("original-a"))
time.sleep(0.25)
assert not (
errors := m.reconcile_exports(config, owned, leases)
), errors
assert leases[selected_a["id"]].ready
serial_transfer()
print(
"PASS: 20 unplug/replug cycles; stale slot removed and original exported; independent native serial transfer survives",
flush=True,
)
put(a / "UDC", "\n")
time.sleep(0.05)
m.reconcile_exports(config, owned, leases)
a_udc = udcs[2]
put(a / "UDC", a_udc)
wait(lambda: find("original-a"))
time.sleep(0.25)
assert not (errors := m.reconcile_exports(config, owned, leases)), errors
assert owned[selected_a["id"]]["busid"] != selected_a["busid"]
serial_transfer()
print(
"PASS: moving original to another USB port preserves independent serial transfer",
flush=True,
)
# An unselected replacement returns to a local driver and cannot import.
put(a / "UDC", "\n")
time.sleep(0.05)
put(a / "strings/0x409/serialnumber", "unselected-replacement")
put(a / "UDC", a_udc)
replacement = wait(lambda: find("unselected-replacement"))
errors = m.reconcile_exports(config, owned, leases)
assert selected_a["id"] in errors
assert find("unselected-replacement")["driver"] == "usb"
assert control(33240, replacement["busid"]) != 0
# Also bind that unselected replacement in a separate native setup: the
# daemon can see it, but a direct import through our gate must still fail.
run("usbip", "bind", "-b", replacement["busid"])
assert replacement["busid"] in run("usbip", "list", "-r", "127.0.0.1")
assert control(33240, replacement["busid"]) != 0
m.reconcile_exports(config, owned, leases)
assert find("unselected-replacement")["driver"] == "usbip-host"
run("usbip", "unbind", "-b", replacement["busid"])
serial_transfer()
print(
"PASS: same-port replacement stays local, native import rejected, independent serial survives",
flush=True,
)
# Same original then can be selected, imported and exchange native URBs.
put(a / "UDC", "\n")
time.sleep(0.1)
put(a / "strings/0x409/serialnumber", "original-a")
put(a / "UDC", a_udc)
wait(lambda: find("original-a"))
time.sleep(0.25)
assert not (errors := m.reconcile_exports(config, owned, leases)), errors
print(
run(
"usbip",
"--tcp-port",
"33240",
"attach",
"-r",
"127.0.0.1",
"-b",
find("original-a")["busid"],
),
flush=True,
)
wait(lambda: len(m.imported_ports()) == 2)
serial_transfer(a, "original-a")
serial_transfer()
for port in m.imported_ports():
if port["busid"] == find("original-a")["busid"]:
run("usbip", "detach", "-p", str(port["port"]))
# Simulate losing process-owned descriptors before a present-device
# release. The manager must acquire and verify fresh usbfs pins.
leases.pop(selected_a["id"]).close()
config["exports"] = [selected_b]
assert not (errors := m.reconcile_exports(config, owned, leases)), errors
assert find("original-a")["driver"] == "usb"
serial_transfer()
print(
"PASS: original reimport and present-device release restore local driver",
flush=True,
)
finally:
if proxy:
proxy.terminate()
proxy.wait()
if gate:
gate.terminate()
for port in m.imported_ports():
with contextlib.suppress(Exception):
run("usbip", "detach", "-p", str(port["port"]))
config["exports"] = []
print("cleanup", m.reconcile_exports(config, owned, leases), flush=True)
daemon.terminate()
daemon.wait()
for lease in leases.values():
lease.close()
print("PASS: no Oops/BUG in kernel log", flush=True)
for g in (a, b):
if (g / "UDC").read_text().strip():
put(g / "UDC", "\n")
(g / "configs/c.1/acm.usb0").unlink()
(g / "functions/acm.usb0").rmdir()
(g / "configs/c.1").rmdir()
(g / "strings/0x409").rmdir()
g.rmdir()
assert not any(
x in run("dmesg") for x in ["BUG:", "Oops:", "general protection fault"]
)
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
"""Opt-in fresh-QEMU lab: actual generated units, restricted SSH and native USB URBs."""
import contextlib, os, select, shutil, sys, termios, time, tty
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts/devices"))
from check_usbip_kernel import m, run, put, gadget, find
def wait(test, seconds=30):
end = time.monotonic() + seconds
while time.monotonic() < end:
value = test()
if value:
return value
time.sleep(0.1)
raise AssertionError("timeout")
def transfer(g, serial):
client = wait(
lambda: next(
(
p
for p in Path("/sys/class/tty").glob("ttyACM*")
if "vhci_hcd" in str(p.resolve())
and any(
(q / "serial").exists()
and (q / "serial").read_text().strip() == serial
for q in p.resolve().parents
)
),
None,
)
)
with contextlib.ExitStack() as stack:
fds = [
os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
for path in (
"/dev/ttyGS" + (g / "functions/acm.usb0/port_num").read_text().strip(),
"/dev/" + client.name,
)
]
for fd in fds:
stack.callback(os.close, fd)
tty.setraw(fd)
termios.tcflush(fd, termios.TCIOFLUSH)
for src, dst in ((fds[0], fds[1]), (fds[1], fds[0])):
payload = b"actual-systemd-ssh-usbip-serial\n"
os.write(src, payload)
assert select.select([dst], [], [], 5)[0]
assert os.read(dst, len(payload)) == payload
def main():
assert sys.argv[1:] == [
"--disposable-vm"
], "Requires --disposable-vm on a fresh throwaway QEMU VM"
assert not Path("/etc/proxmenux/usbip").exists()
assert "QEMU" in Path("/sys/class/dmi/id/sys_vendor").read_text()
for name in ("usbip_manager.py", "usbip_protocol.py", "usbip_lifetime.py"):
target = Path("/usr/local/share/proxmenux/scripts/devices") / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(m.SCRIPT.parent / name, target)
run("modprobe", "dummy_hcd", "num=3")
run("modprobe", "libcomposite")
run("modprobe", "usbip-host")
run("modprobe", "vhci-hcd")
udcs = sorted(p.name for p in Path("/sys/class/udc").iterdir())
assert len(udcs) == 3
assert not list(Path("/sys/kernel/config/usb_gadget").iterdir())
a = gadget("service-a", "service-a", udcs[0])
b = gadget("service-b", "service-b", udcs[1])
time.sleep(0.5)
key = Path("/root/service-usbip-key")
run("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key))
m.authorize_key(str(key) + ".pub")
public = Path("/etc/ssh/ssh_host_ed25519_key.pub").read_text().split()
Path("/root/.ssh/known_hosts").write_text(
"127.0.0.1 " + public[0] + " " + public[1] + "\n"
)
imports = []
try:
m.update_server(True)
sa = m.add_export(wait(lambda: find("service-a"))["busid"])
sb = m.add_export(wait(lambda: find("service-b"))["busid"])
wait(lambda: len([e for e in m.catalog() if e["status"] == "exported"]) == 2)
remote = m.validate_remote("127.0.0.1", 22, str(key))
for selected, port in ((sa, 33240), (sb, 33241)):
imports.append(m.add_import(remote, selected["id"], port))
wait(lambda: len(m.imported_ports()) == 2)
transfer(a, "service-a")
transfer(b, "service-b")
print(
"PASS: actual generated server/client systemd units and restricted SSH carry bidirectional native serial on two USBs",
flush=True,
)
for cycle in range(3):
put(a / "UDC", "\n")
time.sleep(0.2)
transfer(b, "service-b")
put(a / "UDC", udcs[0])
wait(lambda: find("service-a"))
time.sleep(0.5)
# An old vhci port may still exist; wait for the new guest driver and verify data.
wait(
lambda: next(
(
e
for e in m.catalog()
if e["id"] == sa["id"] and e["status"] == "exported"
),
None,
)
)
time.sleep(6)
transfer(a, "service-a")
transfer(b, "service-b")
print(
"PASS: three physical reconnects automatically reattach through restricted SSH; second serial connection survives",
flush=True,
)
m.remove_export(sa["id"])
assert find("service-a")["driver"] == "usb"
transfer(b, "service-b")
print(
"PASS: stop-sharing waits for real release and returns local driver while independent client keeps transferring",
flush=True,
)
for item in imports:
m.remove_import(item["id"])
imports = []
m.update_server(False)
assert not m.read_json(m.state_path(), {})
assert find("service-b")["driver"] == "usb"
print(
"PASS: disable stops listeners, removes owned bindings, leaves both local USB drivers restored",
flush=True,
)
finally:
for item in imports:
with contextlib.suppress(Exception):
m.remove_import(item["id"])
with contextlib.suppress(Exception):
m.update_server(False)
print(
run("journalctl", "-u", m.SERVER_UNIT, "--no-pager", "-n", "20"), flush=True
)
if __name__ == "__main__":
main()
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""Linux-only integration: real namespaces, socat and SSH; no USB hardware.
Run as root in the disposable CI runner. All keys, configuration, listeners and
child processes belong to a temporary fixture; no systemd units are installed.
"""
import importlib.util
import os
from pathlib import Path
import pwd
import signal
import socket
import stat
import subprocess
import sys
import tempfile
import time
SOURCE = Path(__file__).resolve().parents[2] / "scripts/devices/usbip_manager.py"
def port():
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
def wait_until(test, processes, log, timeout=15):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
for process in processes:
if process.poll() is not None:
raise AssertionError(f"Fixture exited ({process.returncode}): {log.read_text()}")
try:
if test():
return
except (ConnectionError, FileNotFoundError, socket.timeout):
pass
time.sleep(0.1)
raise AssertionError(f"Fixture did not become ready: {log.read_text()}")
def echo(sock):
# Exercise the production protocol gate through the real SSH stream. Empty
# fixture selection must produce an empty native DEVLIST, never raw echo.
import struct
sock.settimeout(10)
sock.sendall(struct.pack("!HHI", 0x0111, 0x8005, 0))
received = bytearray()
while len(received) < 12:
chunk = sock.recv(12 - len(received))
if not chunk:
break
received.extend(chunk)
assert bytes(received) == struct.pack("!HHII", 0x0111, 5, 0, 0), received
def main():
assert sys.platform == "linux" and os.geteuid() == 0, "Use the disposable Linux CI runner as root"
spec = importlib.util.spec_from_file_location("usbip_manager", SOURCE)
manager = importlib.util.module_from_spec(spec)
spec.loader.exec_module(manager)
# A mistaken direct launch must never create a listener in this namespace.
try:
manager.require_private_network()
except manager.USBIPError:
pass
else:
raise AssertionError("Expected the host namespace to be rejected")
processes = []
tunnel = None
username = f"pmx-usbip-test-{os.getpid()}"
subprocess.run(["useradd", "--system", "--user-group", "--home-dir", "/nonexistent", "--shell", "/bin/sh", "--password", "*", username], check=True)
account = pwd.getpwnam(username)
manager.TRANSPORT_USER = username
# StrictModes checks every ancestor of AuthorizedKeysFile. A private
# subdirectory of /tmp still fails because /tmp itself is world writable.
# Keep the disposable fixture below root's home without changing its SSH
# configuration, account settings or existing authorized_keys.
with tempfile.TemporaryDirectory(prefix="pmx-usbip-", dir="/var/lib") as directory:
root = Path(directory)
os.chown(root, 0, account.pw_gid)
root.chmod(0o750)
fixture_source = root / "usbip_manager.py"
fixture_source.write_bytes(SOURCE.read_bytes())
fixture_source.chmod(0o644)
for name in ("usbip_protocol.py", "usbip_lifetime.py"):
(root / name).write_bytes((SOURCE.parent / name).read_bytes())
subprocess.run(["usermod", "--home", str(root), username], check=True)
log = root / "transport.log"
with log.open("w") as log_handle:
try:
fake_bin = root / "bin"
fake_bin.mkdir()
# The real daemon requires usbip-host and physical devices.
# Echo stands in only for its TCP endpoint, inside the namespace.
fake_daemon = fake_bin / "usbipd"
fake_daemon.write_text("""#!/usr/bin/python3
import socketserver
class Echo(socketserver.BaseRequestHandler):
def handle(self):
import struct
data = b''
while len(data) < 8:
block = self.request.recv(8 - len(data))
if not block:
return
data += block
if data == struct.pack('!HHI', 0x0111, 0x8005, 0):
self.request.sendall(struct.pack('!HHII', 0x0111, 5, 0, 0))
socketserver.ThreadingTCPServer.allow_reuse_address = True
socketserver.ThreadingTCPServer(('0.0.0.0', 3240), Echo).serve_forever()
""")
fake_daemon.chmod(0o755)
config = root / "config"
manager.atomic_json(config / "config.json", {"server": True, "exports": [], "imports": []})
server_run = root / "server"
runner = root / "server.py"
runner.write_text(f"""import importlib.util
from pathlib import Path
import subprocess
spec = importlib.util.spec_from_file_location('usbip_manager', {str(fixture_source)!r})
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
m.CONFIG_DIR = Path({str(config)!r})
m.SERVER_RUN_DIR = Path({str(server_run)!r})
m.SYSFS = Path({str(root / 'no-usb-devices')!r})
m.TRANSPORT_USER = {username!r}
m.TRANSPORT_HOME = Path({str(root)!r})
subprocess.run(['ip', 'link', 'set', 'lo', 'up'], check=True)
m.serve()
""")
server = subprocess.Popen(["unshare", "--net", sys.executable, str(runner)],
env={**os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}"},
stdout=log_handle, stderr=log_handle, start_new_session=True)
processes.append(server)
unix_path = server_run / "usbip.sock"
wait_until(lambda: unix_path.exists(), [server], log)
assert os.readlink(f"/proc/{server.pid}/ns/net") != os.readlink("/proc/self/ns/net")
assert stat.S_IMODE(server_run.stat().st_mode) == 0o750
assert stat.S_IMODE(unix_path.stat().st_mode) == 0o660
# Linux /proc/net reflects the caller's network namespace.
listening = subprocess.check_output(["ss", "-H", "-ltn", "sport", "=", ":3240"], text=True)
assert not listening.strip(), "Raw USB/IP TCP leaked into the host network namespace"
# Wait for both the proxy and its private TCP peer.
def unix_ready():
with socket.socket(socket.AF_UNIX) as connection:
connection.connect(str(unix_path))
echo(connection)
return True
wait_until(unix_ready, [server], log)
for name in ("host", "client", "wrong-client"):
subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(root / name)], check=True)
ssh_dir = root / ".ssh"
ssh_dir.mkdir(mode=0o750)
os.chown(ssh_dir, 0, account.pw_gid)
authorized = ssh_dir / "authorized_keys"
gate = root / "gate.py"
gate.write_text(f"""import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location('usbip_manager', {str(fixture_source)!r})
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
m.TRANSPORT_USER = {username!r}
m.TRANSPORT_HOME = Path({str(root)!r})
m.SERVER_RUN_DIR = Path({str(server_run)!r})
m.REMOTE_SOCKET = {str(unix_path)!r}
m.transport()
""")
gate.chmod(0o644)
authorized.write_text(f'restrict,command="{sys.executable} {gate}" ' + (root / "client.pub").read_text())
os.chown(authorized, 0, account.pw_gid)
authorized.chmod(0o640)
known = root / "known_hosts"
known.write_text("")
known.chmod(0o600)
ssh_port, local_port = port(), port()
ssh_config = root / "sshd_config"
ssh_config.write_text(f"""Port {ssh_port}
ListenAddress 127.0.0.1
HostKey {root / 'host'}
PidFile {root / 'sshd.pid'}
AuthorizedKeysFile {authorized}
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
StrictModes yes
AllowUsers {username}
AllowTcpForwarding local
AllowStreamLocalForwarding local
LogLevel VERBOSE
""")
Path("/run/sshd").mkdir(exist_ok=True, mode=0o755)
sshd = subprocess.Popen(["/usr/sbin/sshd", "-D", "-e", "-f", str(ssh_config)],
stdout=log_handle, stderr=log_handle, start_new_session=True)
processes.append(sshd)
def ssh_ready():
with socket.create_connection(("127.0.0.1", ssh_port), timeout=1):
return True
wait_until(ssh_ready, [server, sshd], log)
remote = manager.validate_remote("127.0.0.1", ssh_port, str(root / "client"))
extra = ["-o", f"UserKnownHostsFile={known}", "-o", "GlobalKnownHostsFile=/dev/null"]
def check_ssh(selected):
args = manager.ssh_args(selected)
args[1:1] = extra
return subprocess.run(args + ["--", selected["host"], "catalog"], capture_output=True, text=True, timeout=15)
unknown = check_ssh(remote)
assert unknown.returncode != 0 and "Host key verification failed" in unknown.stderr, unknown.stderr
host_public = (root / "host.pub").read_text().split()
known.write_text(f"[127.0.0.1]:{ssh_port} {host_public[0]} {host_public[1]}\n")
wrong = check_ssh(remote | {"key": str(root / "wrong-client")})
assert wrong.returncode != 0 and "Permission denied" in wrong.stderr, wrong.stderr
accepted = check_ssh(remote)
assert accepted.returncode == 0, accepted.stderr + "\n" + log.read_text()
forbidden = manager.ssh_args(remote)
forbidden[1:1] = extra
denied = subprocess.run(forbidden + ["--", remote["host"], "id"], capture_output=True, text=True, timeout=15)
assert denied.returncode != 0, "Forced command allowed a shell command"
forwarded = subprocess.run(forbidden + ["-W", "127.0.0.1:22", "--", remote["host"]], capture_output=True, text=True, timeout=15)
assert forwarded.returncode != 0 and "prohibited" in forwarded.stderr, forwarded.stderr
original = manager.ssh_args
def fixture_ssh(selected):
args = original(selected)
args[1:1] = extra
return args
manager.ssh_args = fixture_ssh
tunnel = manager.LoopbackTunnel(remote | {"local_port": local_port})
with socket.socket() as attacker:
try:
attacker.bind(("127.0.0.1", local_port))
except OSError:
pass
else:
raise AssertionError("Another process stole the tunnel port")
def tunnel_ready():
with socket.create_connection(("127.0.0.1", local_port), timeout=1) as connection:
echo(connection)
return True
wait_until(tunnel_ready, processes, log)
listener = subprocess.check_output(["ss", "-H", "-ltn", "sport", "=", f":{local_port}"], text=True)
assert f"127.0.0.1:{local_port}" in listener and f"0.0.0.0:{local_port}" not in listener, listener
print("PASS: private namespace, dedicated-group Unix socket, no host USB/IP listener")
print("PASS: unknown/wrong keys, shell commands and arbitrary forwarding rejected; dedicated key accepted")
print("PASS: native USB/IP DEVLIST across real SSH -> Unix protocol gate -> private TCP")
finally:
if tunnel:
tunnel.terminate()
for process in reversed(processes):
if process.poll() is None:
os.killpg(process.pid, signal.SIGTERM)
for process in reversed(processes):
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=5)
subprocess.run(["pkill", "-KILL", "-u", str(account.pw_uid)], check=False)
# PAM/sshd children need to exit and be reaped before userdel.
for attempt in range(50):
cleanup = subprocess.run(["userdel", username], capture_output=True, text=True)
if cleanup.returncode == 0:
break
if cleanup.returncode != 8:
raise AssertionError(cleanup.stderr)
time.sleep(0.1)
else:
subprocess.run(["ps", "-fu", username], check=False)
raise AssertionError(cleanup.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Exercise the extracted launcher and its real native Python environment,
# intercepting only the final server exec so no host-monitoring server starts.
set -eu
appdir="$1"
export PMX_SMOKE_PYTHON
PMX_SMOKE_PYTHON="$(command -v python)"
fixture_bin="$(mktemp -d)"
trap 'rm -rf "$fixture_bin"' EXIT
cat > "$fixture_bin/python3" <<'WRAPPER'
#!/bin/sh
case "$1" in
*/flask_server.py)
exec "$PMX_SMOKE_PYTHON" -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print("PASS: extracted AppRun native runtime")'
;;
*) exec "$PMX_SMOKE_PYTHON" "$@" ;;
esac
WRAPPER
chmod +x "$fixture_bin/python3"
PATH="$fixture_bin:$PATH" bash "$appdir/AppRun"
@@ -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'))
+4 -2
View File
@@ -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')
+488
View File
@@ -0,0 +1,488 @@
"""USB/IP ownership, device identity, SSH policy and service regression tests.
No physical USB devices, live systemd units or host configuration are changed.
The optional Linux integration lane exercises actual SSH, socat and namespaces.
"""
import copy
import importlib.util
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import sys
import tempfile
import unittest
from types import SimpleNamespace
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[3]
SOURCE = ROOT / "scripts/devices/usbip_manager.py"
EXPORT = "abcdef123456"
CLIENT = "fedcba654321"
def load():
spec = importlib.util.spec_from_file_location("usbip_manager", SOURCE)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class USBIPTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.m = load()
for attr, path in (("CONFIG_DIR", "config"), ("RUN_DIR", "client-run"),
("SERVER_RUN_DIR", "server-run"), ("UNIT_DIR", "units"), ("SYSFS", "sysfs"),
("NET_SYSFS", "net")):
setattr(self.m, attr, self.root / path)
getattr(self.m, attr).mkdir()
class MatchTable:
# Mirror the kernel contract, not regular-file write semantics:
# del(ALLOC)=REMOV, del(ADDED/REMOV) retains the named slot.
def __init__(self):
self.slots = {}
def write_text(self, value):
action, busid = value.split()
if self.slots.get(busid) == "ALLOC":
self.slots[busid] = "REMOV"
def read_text(self):
return " ".join(self.slots)
self.m.MATCH_BUSID = MatchTable()
self.account = SimpleNamespace(pw_uid=1234, pw_gid=1234)
self.calls = []
self.key = self.root / "id_ed25519"
self.key.write_text("test-key")
self.key.chmod(0o600)
self.remote = {"host": "usb.example.test", "ssh_port": 2222, "key": str(self.key)}
def config(self, exports=None, imports=None, server=True):
value = {"server": server, "exports": exports or [], "imports": imports or []}
self.m.atomic_json(self.m.CONFIG_DIR / "config.json", value)
return value
def test_a_usb_network_adapter_is_not_offered(self):
self.device("1-3", serial="nic-1")
(self.m.NET_SYSFS / "enx001122").mkdir()
(self.m.NET_SYSFS / "enx001122" / "device").symlink_to(self.m.SYSFS / "1-3" / "1-3:1.0")
self.assertEqual(next(d for d in self.m.inventory() if d["busid"] == "1-3")["blocked"],
"USB network adapters cannot be shared")
self.device("1-4", serial="zigbee-9")
self.assertEqual(next(d for d in self.m.inventory() if d["busid"] == "1-4")["blocked"], "")
def device(self, busid="1-2", serial="zigbee-123", device_class="00", interface="03", driver="usb"):
path = self.m.SYSFS / busid
path.mkdir()
values = {"idVendor": "10c4", "idProduct": "ea60", "serial": serial,
"product": "Test Zigbee adapter", "bDeviceClass": device_class}
for key, value in values.items():
(path / key).write_text(value)
child = path / f"{busid}:1.0"
child.mkdir()
(child / "bInterfaceClass").write_text(interface)
self.set_driver(busid, driver)
return next(d for d in self.m.inventory() if d["busid"] == busid)
def set_driver(self, busid, driver):
target = self.root / "drivers" / driver
target.mkdir(parents=True, exist_ok=True)
path = self.m.SYSFS / busid / "driver"
path.unlink(missing_ok=True)
path.symlink_to(target, target_is_directory=True)
def selected(self, device):
return {k: device[k] for k in ("busid", "vendor", "product", "serial", "name")} | {"id": EXPORT}
def usb(self, args, timeout=20):
self.calls.append(args)
if args[:2] == ["usbip", "bind"]:
self.set_driver(args[-1], "usbip-host")
self.m.MATCH_BUSID.slots[args[-1]] = "ALLOC"
elif args[:2] == ["usbip", "unbind"]:
self.set_driver(args[-1], "usb")
self.m.MATCH_BUSID.slots.pop(args[-1], None)
return ""
def test_storage_composite_interfaces_and_hubs_are_never_exported(self):
for index, (base, interface) in enumerate((("08", "06"), ("09", "00"), ("00", "08"), ("", "03"))):
device = self.device(busid=f"1-{index + 1}", device_class=base, interface=interface)
self.assertTrue(device["blocked"])
self.config()
with self.assertRaises(self.m.USBIPError), patch.object(self.m, "run") as command:
self.m.add_export(device["busid"])
command.assert_not_called()
self.assertEqual(self.m.load_config()["exports"], [])
def test_export_intent_is_private_and_bind_is_idempotent(self):
device = self.device()
self.config()
selected = self.m.add_export(device["busid"])
owned = {}
with patch.object(self.m, "run", side_effect=self.usb):
self.assertEqual(self.m.reconcile_exports(self.m.load_config(), owned), {})
self.assertEqual(self.m.reconcile_exports(self.m.load_config(), owned), {})
self.assertEqual(self.calls, [["usbip", "bind", "--busid", "1-2"]])
self.assertEqual(self.m.catalog()[0]["id"], selected["id"])
self.assertEqual(self.m.catalog()[0]["status"], "exported")
for path in (self.m.CONFIG_DIR / "config.json", self.m.state_path()):
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
self.assertNotEqual(self.m.RUN_DIR, self.m.SERVER_RUN_DIR)
def test_port_move_fails_closed_when_kernel_retains_old_slot(self):
selected = self.selected(self.device())
config = self.config([selected])
owned = {}
with patch.object(self.m, "run", side_effect=self.usb):
self.m.reconcile_exports(config, owned)
shutil.rmtree(self.m.SYSFS / "1-2")
self.device(serial="other-device", driver="usbip-host")
self.device(busid="2-4")
errors = self.m.reconcile_exports(config, owned)
self.assertIn("blocked", errors[EXPORT])
self.assertEqual(owned[EXPORT]["busid"], "1-2")
self.assertEqual(self.calls, [["usbip", "bind", "--busid", "1-2"]])
self.assertIsNone(self.m.catalog()[0]["busid"])
def test_retained_slot_is_rejected_before_detaching_reconnected_local_usb(self):
device = self.device()
selected = self.selected(device)
config = self.config([selected])
self.m.MATCH_BUSID.slots[device["busid"]] = "REMOV"
with patch.object(self.m, "run", side_effect=self.usb):
errors = self.m.reconcile_exports(config, {EXPORT: selected}, {})
self.assertIn("local driver", errors[EXPORT])
self.assertEqual(self.calls, [])
self.assertEqual(self.m.inventory()[0]["driver"], "usb")
def test_imported_usb_does_not_shadow_physical_original(self):
device = self.device()
imported = self.m.SYSFS / "2-1"
imported.symlink_to(self.m.SYSFS / "1-2", target_is_directory=True)
target = self.root / "vhci_hcd.0" / "2-1"
target.parent.mkdir()
shutil.copytree(self.m.SYSFS / "1-2", target, symlinks=True)
imported.unlink()
imported.symlink_to(target, target_is_directory=True)
devices = self.m.inventory()
self.assertEqual(len(devices), 1)
self.assertEqual(self.m.resolve_device(device, devices)["busid"], "1-2")
def test_no_serial_requires_same_busid_and_ambiguous_serial_is_rejected(self):
selected = self.selected(self.device(serial=""))
shutil.rmtree(self.m.SYSFS / "1-2")
self.device(busid="2-4", serial="")
with self.assertRaises(self.m.USBIPError):
self.m.resolve_device(selected, self.m.inventory())
self.device(busid="1-3", serial="duplicate")
first = self.device(busid="1-4", serial="duplicate")
with self.assertRaises(self.m.USBIPError):
self.m.resolve_device(first, self.m.inventory())
def test_foreign_exports_are_not_adopted_and_disable_only_releases_owned_devices(self):
own = self.selected(self.device())
foreign = self.device(busid="1-3", serial="foreign", driver="usbip-host")
config = self.config([own, self.selected(foreign) | {"id": CLIENT}])
owned = {}
with patch.object(self.m, "run", side_effect=self.usb):
errors = self.m.reconcile_exports(config, owned)
self.assertIn("outside ProxMenux", errors[CLIENT])
config["server"] = False
self.m.reconcile_exports(config, owned)
self.assertEqual(self.calls, [["usbip", "bind", "--busid", "1-2"], ["usbip", "unbind", "--busid", "1-2"]])
self.assertEqual(owned, {})
self.assertEqual(next(d for d in self.m.inventory() if d["busid"] == "1-3")["driver"], "usbip-host")
def test_recover_bind_intent_and_remove_selected_export(self):
selected = self.selected(self.device(driver="usbip-host"))
self.config([selected])
owned = {EXPORT: selected}
self.m.atomic_json(self.m.state_path(), owned)
with patch.object(self.m, "run", side_effect=self.usb):
self.m.reconcile_exports(self.m.load_config(), owned)
self.assertEqual(self.calls, [])
with patch.object(self.m.time, "sleep", side_effect=lambda _: self.m.reconcile_exports(self.m.load_config(), owned)):
self.m.remove_export(EXPORT)
self.m.reconcile_exports(self.m.load_config(), owned)
self.assertEqual(self.calls, [["usbip", "unbind", "--busid", "1-2"]])
def test_failed_bind_is_reported_and_retains_recoverable_intent(self):
selected = self.selected(self.device())
config = self.config([selected])
owned = {}
with patch.object(self.m, "run", side_effect=self.m.USBIPError("bind failed")):
self.assertEqual(self.m.reconcile_exports(config, owned), {EXPORT: "bind failed"})
self.assertIn(EXPORT, self.m.read_json(self.m.state_path(), {}))
self.assertEqual(self.m.status()["server_status"]["errors"], {EXPORT: "bind failed"})
self.assertEqual(self.m.catalog()[0]["status"], "waiting")
def test_unplugged_kernel_match_is_disarmed_but_not_falsely_forgotten(self):
selected = self.selected(self.device())
config = self.config([selected])
owned = {}
with patch.object(self.m, "run", side_effect=self.usb):
self.m.reconcile_exports(config, owned)
shutil.rmtree(self.m.SYSFS / "1-2")
errors = self.m.reconcile_exports(config, owned)
self.assertEqual(owned[EXPORT]["busid"], "1-2")
self.assertIn("blocked", errors[EXPORT])
self.assertEqual(self.m.MATCH_BUSID.slots["1-2"], "REMOV")
self.assertEqual(len(self.calls), 1)
def test_failed_stale_cleanup_does_not_forget_or_replace_ownership(self):
selected = self.selected(self.device())
config = self.config([selected])
owned = {EXPORT: selected}
shutil.rmtree(self.m.SYSFS / "1-2")
self.device(busid="2-4")
with patch.object(self.m, "disarm_match", side_effect=self.m.USBIPError("disarm failed")), patch.object(self.m, "run") as command:
errors = self.m.reconcile_exports(config, owned)
self.assertIn("disarm", errors[EXPORT])
self.assertEqual(owned[EXPORT]["busid"], "1-2")
command.assert_not_called()
def test_authorized_key_is_restricted_and_policy_stays_root_owned(self):
self.m.TRANSPORT_HOME = self.root / "transport-home"
account = SimpleNamespace(pw_uid=1234, pw_gid=1234, pw_dir=str(self.m.TRANSPORT_HOME))
public = self.root / "client.pub"
public.write_text("ssh-ed25519 AAAA test-client")
with patch.object(self.m.pwd, "getpwnam", side_effect=[KeyError(), account]), patch.object(
self.m, "run"
) as command, patch.object(self.m.os, "chown"), patch.object(self.m.os, "fchown"):
self.m.authorize_key(public)
command.assert_called_once()
self.assertEqual(command.call_args.args[0][0], "useradd")
policy = self.m.TRANSPORT_HOME / ".ssh/authorized_keys"
self.assertEqual(policy.stat().st_mode & 0o777, 0o640)
self.assertEqual(policy.read_text(), 'restrict,command="/usr/bin/python3 ' + self.m.REMOTE_SCRIPT + ' transport" ssh-ed25519 AAAA\n')
with patch.object(self.m.pwd, "getpwnam", return_value=account), patch.object(
self.m.os, "chown"
), patch.object(self.m.os, "fchown"):
self.m.authorize_key(public)
self.assertEqual(len(policy.read_text().splitlines()), 1)
def test_authorize_never_adopts_an_existing_foreign_account(self):
self.m.TRANSPORT_HOME = self.root / "foreign-home"
account = SimpleNamespace(pw_uid=1234, pw_gid=1234, pw_dir=str(self.m.TRANSPORT_HOME))
public = self.root / "client.pub"
public.write_text("ssh-ed25519 AAAA test-client")
with patch.object(self.m.pwd, "getpwnam", return_value=account), patch.object(self.m, "run") as command:
with self.assertRaisesRegex(self.m.USBIPError, "existing"):
self.m.authorize_key(public)
command.assert_not_called()
self.assertFalse(self.m.TRANSPORT_HOME.exists())
def test_forced_command_rejects_shell_and_wrong_identity(self):
for command in ("id", "catalog; id", "", "stream --socket /run/other.sock"):
with patch.object(self.m, "transport_account", return_value=self.account), patch.object(
self.m.os, "geteuid", return_value=1234
), patch.dict(os.environ, {"SSH_ORIGINAL_COMMAND": command}), patch.object(self.m.os, "execvp") as execute:
with self.assertRaisesRegex(self.m.USBIPError, "Only USB/IP"):
self.m.transport()
execute.assert_not_called()
with patch.object(self.m, "transport_account", return_value=self.account), patch.object(self.m.os, "geteuid", return_value=0):
with self.assertRaisesRegex(self.m.USBIPError, "unprivileged"):
self.m.transport()
def test_forced_command_catalog_and_stream_are_fixed(self):
(self.m.SERVER_RUN_DIR / "catalog.json").write_text("[]")
with patch.object(self.m, "transport_account", return_value=self.account), patch.object(self.m.os, "geteuid", return_value=1234):
with patch.dict(os.environ, {"SSH_ORIGINAL_COMMAND": "catalog"}), patch("builtins.print") as output:
self.m.transport()
output.assert_called_once_with("[]")
with patch.dict(os.environ, {"SSH_ORIGINAL_COMMAND": "stream"}), patch.object(self.m.os, "execvp") as execute:
self.m.transport()
execute.assert_called_once_with("socat", ["socat", "STDIO", "UNIX-CONNECT:" + self.m.REMOTE_SOCKET])
def test_loopback_port_is_held_until_tunnel_stops(self):
with socket.socket() as unused:
unused.bind(("127.0.0.1", 0))
port = unused.getsockname()[1]
tunnel = self.m.LoopbackTunnel(self.remote | {"local_port": port})
try:
with socket.socket() as attacker:
with self.assertRaises(OSError):
attacker.bind(("127.0.0.1", port))
with self.assertRaisesRegex(self.m.USBIPError, "in use"):
self.m.LoopbackTunnel(self.remote | {"local_port": port})
finally:
tunnel.terminate()
self.assertEqual(tunnel.poll(), 0)
def test_namespace_guard_fails_before_spawning_a_listener(self):
with patch.object(self.m.os, "readlink", return_value="net:[1]"), patch.object(self.m.subprocess, "Popen") as process:
with self.assertRaisesRegex(self.m.USBIPError, "PrivateNetwork"):
self.m.serve()
process.assert_not_called()
def test_generated_units_are_isolated_quoted_and_owned(self):
self.m.SCRIPT = self.root / 'with space/%and$dollar"quote.py'
with patch("shutil.which", return_value="/usr/sbin/modprobe"):
server = self.m.unit_text("server")
client = self.m.unit_text("client", CLIENT)
self.assertIn("PrivateNetwork=yes", server)
self.assertIn("RuntimeDirectory=proxmenux-usbip-server", server)
self.assertIn("RuntimeDirectoryPreserve=yes", server)
self.assertIn('%%and$$dollar\\"quote.py" serve', server)
self.assertNotIn("PrivateNetwork=yes", client)
self.assertNotIn("RuntimeDirectory=", client)
self.assertIn("vhci-hcd", client)
self.assertIn("Restart=on-failure", client)
unit = self.m.UNIT_DIR / self.m.SERVER_UNIT
unit.write_text("[Unit]\nDescription=Someone else's service\n")
with self.assertRaises(self.m.USBIPError), patch.object(self.m, "run") as command:
self.m.update_server(True)
command.assert_not_called()
self.assertFalse((self.m.CONFIG_DIR / "config.json").exists())
def test_failed_server_start_restores_configuration_and_removes_new_unit(self):
previous = self.config(server=False)
def fail_start(args, timeout=20):
self.calls.append(args)
if args[:3] == ["systemctl", "enable", "--now"]:
raise self.m.USBIPError("start failed")
return ""
with patch.object(self.m, "transport_account", return_value=self.account), patch.object(self.m, "run", side_effect=fail_start), patch("shutil.which", return_value="/sbin/modprobe"):
with self.assertRaisesRegex(self.m.USBIPError, "start failed"):
self.m.update_server(True)
self.assertEqual(self.m.load_config(), previous)
self.assertFalse((self.m.UNIT_DIR / self.m.SERVER_UNIT).exists())
self.assertIn(["systemctl", "disable", "--now", self.m.SERVER_UNIT], self.calls)
def test_ssh_policy_and_argument_boundaries(self):
remote = self.m.validate_remote(self.remote["host"], 2222, str(self.key))
args = self.m.tunnel_args(remote | {"local_port": 33240})
for required in ("BatchMode=yes", "StrictHostKeyChecking=yes", "IdentitiesOnly=yes", "ForwardAgent=no",
"PasswordAuthentication=no", "ExitOnForwardFailure=yes"):
self.assertIn(required, args)
self.assertIn("proxmenux-usbip", args)
self.assertNotIn("root", args)
self.assertNotIn("-L", args)
self.assertNotIn("-N", args)
self.assertNotIn("StrictHostKeyChecking=no", args)
self.assertEqual(args[-3:], ["--", "usb.example.test", "stream"])
for host in ("-oProxyCommand=touch /tmp/no", "root@host", "host;id", "host\nother", "$(id)"):
with self.assertRaises(self.m.USBIPError):
self.m.validate_remote(host, 22, str(self.key))
self.key.chmod(0o644)
with self.assertRaisesRegex(self.m.USBIPError, "permissions"):
self.m.validate_remote("192.0.2.1", 22, str(self.key))
def test_ssh_credential_or_catalog_failure_never_saves_a_connection(self):
for failure in (self.m.USBIPError("Permission denied"), self.m.USBIPError("Connection timed out")):
with patch.object(self.m, "run", side_effect=failure):
with self.assertRaises(self.m.USBIPError):
self.m.add_import(self.remote, EXPORT, 33240)
self.assertFalse((self.m.CONFIG_DIR / "config.json").exists())
with patch.object(self.m, "run", return_value='[{"id":"--bad","busid":"1-2"}]'):
with self.assertRaisesRegex(self.m.USBIPError, "valid"):
self.m.remote_catalog(self.remote)
def test_import_service_failure_rolls_back_and_occupied_port_is_rejected(self):
exports = [{"id": EXPORT, "status": "exported", "busid": "1-2"}]
previous = self.config()
with patch.object(self.m, "remote_catalog", return_value=exports):
with socket.socket() as busy:
busy.bind(("127.0.0.1", 0))
with self.assertRaisesRegex(self.m.USBIPError, "in use"):
self.m.add_import(self.remote, EXPORT, busy.getsockname()[1])
with socket.socket() as unused:
unused.bind(("127.0.0.1", 0))
port = unused.getsockname()[1]
with patch("shutil.which", return_value="/sbin/modprobe"), patch.object(self.m, "enable_unit", side_effect=self.m.USBIPError("failed")):
with self.assertRaises(self.m.USBIPError):
self.m.add_import(self.remote, EXPORT, port)
self.assertEqual(self.m.load_config(), previous)
def port(self, **overrides):
return {"port": 2, "status": "Port in Use", "local_busid": "3-1", "host": "127.0.0.1", "tcp_port": 33240,
"busid": "1-2"} | overrides
def test_real_upstream_port_output_and_unowned_port_cleanup(self):
output = """Imported USB devices
====================
Port 02: <Port in Use> at High Speed(480Mbps)
Test Zigbee adapter (10c4:ea60)
3-1 -> usbip://127.0.0.1:33240/1-2
-> remote bus/dev 001/002
Port 03: <Port Error> at Full Speed(12Mbps)
Another adapter
3-2 -> usbip://192.0.2.1:3240/1-3
"""
ports = self.m.parse_ports(output)
self.assertEqual(ports[0], self.port())
self.assertEqual(ports[1]["status"], "Port Error")
owner = self.m.RUN_DIR / f"client-{CLIENT}.json"
self.m.atomic_json(owner, ports[0])
reused = self.port(local_busid="3-7")
with patch.object(self.m, "imported_ports", return_value=[reused, ports[1]]), patch.object(self.m, "run") as command:
self.m.release_import(owner)
command.assert_not_called()
self.assertFalse(owner.exists())
self.m.atomic_json(owner, ports[0])
with patch.object(self.m, "imported_ports", return_value=ports), patch.object(self.m, "run") as command:
self.m.release_import(owner)
command.assert_called_once_with(["usbip", "detach", "--port", "2"])
def test_attach_reuses_live_port_and_recovers_error_with_new_remote_busid(self):
selected = self.remote | {"id": CLIENT, "local_port": 33240, "export_id": EXPORT}
owner = self.m.RUN_DIR / f"client-{CLIENT}.json"
self.m.atomic_json(owner, self.port())
with patch.object(self.m, "imported_ports", return_value=[self.port()]), patch.object(self.m, "remote_catalog") as remote:
self.assertEqual(self.m.attach_once(selected, owner)["port"], 2)
remote.assert_not_called()
ports = [self.port(status="Port Error")]
def usb(args, timeout=20):
self.calls.append(args)
if "detach" in args:
ports.clear()
elif "attach" in args:
ports.append(self.port(busid="2-4", local_busid="3-5"))
return ""
with patch.object(self.m, "imported_ports", side_effect=lambda: copy.deepcopy(ports)), patch.object(
self.m, "remote_catalog", return_value=[{"id": EXPORT, "status": "exported", "busid": "2-4"}]
), patch.object(self.m, "run", side_effect=usb):
attached = self.m.attach_once(selected, owner)
self.assertEqual(attached["busid"], "2-4")
self.assertEqual(self.calls, [["usbip", "detach", "--port", "2"],
["usbip", "--tcp-port", "33240", "attach", "--remote", "127.0.0.1", "--busid", "2-4"]])
self.assertEqual(self.m.read_json(owner, None), attached)
def test_missing_remote_or_unowned_import_is_never_detached(self):
selected = self.remote | {"id": CLIENT, "local_port": 33240, "export_id": EXPORT}
owner = self.m.RUN_DIR / f"client-{CLIENT}.json"
with patch.object(self.m, "remote_catalog", return_value=[]), patch.object(self.m, "run") as command:
with self.assertRaisesRegex(self.m.USBIPError, "unplugged"):
self.m.attach_once(selected, owner)
command.assert_not_called()
with patch.object(self.m, "remote_catalog", return_value=[{"id": EXPORT, "status": "exported", "busid": "1-2"}]), patch.object(
self.m, "imported_ports", return_value=[self.port()]
), patch.object(self.m, "run") as command:
with self.assertRaisesRegex(self.m.USBIPError, "unowned"):
self.m.attach_once(selected, owner)
command.assert_not_called()
@unittest.skipUnless(sys.platform == "linux" and shutil.which("systemd-analyze"), "systemd verifier requires Linux")
def test_systemd_accepts_both_generated_service_files(self):
with patch("shutil.which", return_value="/bin/true"):
for name, content in ((self.m.SERVER_UNIT, self.m.unit_text("server")),
(self.m.unit_name(CLIENT), self.m.unit_text("client", CLIENT))):
path = self.m.UNIT_DIR / name
path.write_text(content)
result = subprocess.run(["systemd-analyze", "verify", str(path)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_menu_and_search_expose_server_and_client_actions(self):
menu = (ROOT / "scripts/menus/hw_grafics_menu.sh").read_text()
self.assertIn('bash "$LOCAL_SCRIPTS/devices/usbip_menu.sh"', menu)
index = json.loads((ROOT / "scripts/menus/search_index.json").read_text())
self.assertTrue(any(i["script"] == "devices/usbip_menu.sh" and "usbip" in i["keywords"] for i in index))
for script in (ROOT / "scripts/devices/usbip_menu.sh", ROOT / "scripts/menus/hw_grafics_menu.sh"):
subprocess.run(["bash", "-n", str(script)], check=True)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,112 @@
"""Absent-slot recovery must require a verified pin and completed kernel removal."""
import importlib.util
import errno
from pathlib import Path
import socket
import tempfile
import unittest
from unittest.mock import Mock, patch
SOURCE = Path(__file__).resolve().parents[3] / "scripts/devices/usbip_lifetime.py"
spec = importlib.util.spec_from_file_location("usbip_lifetime", SOURCE)
l = importlib.util.module_from_spec(spec)
spec.loader.exec_module(l)
class LifetimeTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
lease = l.Lease.__new__(l.Lease)
lease.path = self.root / "1-2"
lease.devpath = "/devices/test/usb1/1-2"
lease.bus = 1
lease.number = 3
lease.sequence = 100
lease.ready = True
lease.removed = False
lease.fds = []
lease.events = Mock()
lease.events.recvmsg.side_effect = BlockingIOError()
self.lease = lease
self.match = Mock()
self.match.parent = self.root
self.match.read_text.side_effect = lambda: (
"" if (self.root / "rebind").exists() else "1-2"
)
def event(self, **changes):
values = {
"ACTION": "remove",
"DEVTYPE": "usb_device",
"DEVPATH": self.lease.devpath,
"BUSNUM": "001",
"DEVNUM": "003",
"SEQNUM": "101",
} | changes
return b"\0".join(f"{k}={v}".encode() for k, v in values.items()) + b"\0"
def test_no_rebind_without_a_verified_lifetime_pin(self):
self.lease.ready = False
self.lease.removed = True
with self.assertRaises(l.LifetimeError):
self.lease.release(self.match, Mock())
self.assertFalse((self.root / "rebind").exists())
def test_rebind_requires_completed_removal_not_only_missing_sysfs(self):
with patch.object(
self.lease, "validate", side_effect=FileNotFoundError()
), patch.object(l.time, "monotonic", side_effect=[0, 3, 4]):
with self.assertRaisesRegex(l.LifetimeError, "Waiting"):
self.lease.release(self.match, Mock())
self.assertFalse((self.root / "rebind").exists())
def test_unrelated_or_userspace_events_do_not_authorize_slot_recovery(self):
packets = [
(self.event(SEQNUM="99"), [], 0, (0, 1)),
(self.event(DEVNUM="004"), [], 0, (0, 1)),
(self.event(DEVPATH="/devices/other"), [], 0, (0, 1)),
(self.event(), [], 0, (12345, 1)),
(self.event(DEVTYPE="usb_interface"), [], 0, (0, 1)),
BlockingIOError(),
]
self.lease.events.recvmsg.side_effect = packets
self.assertFalse(self.lease.poll_removed())
def test_lost_or_truncated_events_invalidate_recovery(self):
for fault in (
OSError(errno.ENOBUFS, "overflow"),
(self.event(), [], socket.MSG_TRUNC, (0, 1)),
):
self.lease.ready = True
self.lease.events.recvmsg.side_effect = [fault]
with self.assertRaises(l.LifetimeError):
self.lease.poll_removed()
self.assertFalse(self.lease.ready)
self.assertFalse((self.root / "rebind").exists())
def test_pinned_completed_removal_cleans_only_its_slot(self):
self.lease.events.recvmsg.side_effect = [
(self.event(), [], 0, (0, 1)),
BlockingIOError(),
]
unbind = Mock()
self.lease.release(self.match, unbind)
unbind.assert_not_called()
self.assertEqual((self.root / "rebind").read_text(), "1-2")
self.assertFalse(self.lease.ready)
def test_present_device_uses_native_unbind_not_absent_recovery(self):
self.match.read_text.return_value = ""
self.match.read_text.side_effect = None
unbind = Mock()
with patch.object(self.lease, "validate"):
self.lease.release(self.match, unbind)
unbind.assert_called_once_with("1-2")
self.assertFalse((self.root / "rebind").exists())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,303 @@
"""Real socket control/URB tests with disposable sysfs-shaped device objects."""
import importlib.util
import os
from pathlib import Path
import socket
import socketserver
import struct
import tempfile
import threading
import time
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[3]
spec = importlib.util.spec_from_file_location(
"usbip_protocol", ROOT / "scripts/devices/usbip_protocol.py"
)
p = importlib.util.module_from_spec(spec)
spec.loader.exec_module(p)
def descriptor(
name="2-1", bus=2, number=3, vendor=0x1D6B, product=0x0104, interfaces=1
):
return p.DEVICE.pack(
b"/sys/devices/disposable",
name.encode(),
bus,
number,
3,
vendor,
product,
0x0100,
0,
0,
0,
1,
1,
interfaces,
)
def exact(connection, count):
result = bytearray()
while len(result) < count:
block = connection.recv(count - len(result))
if not block:
break
result.extend(block)
return bytes(result)
class GateTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="usbip-gate-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.sysfs = self.root / "sysfs"
self.sysfs.mkdir()
self.device("original")
self.selected = {
"2-1": {
"id": "abcdef123456",
"vendor": "1d6b",
"product": "0104",
"serial": "selected-serial",
}
}
self.calls = []
self.reply = descriptor()
self.listed = [
(descriptor(), b"\x02\x02\x01\0"),
(descriptor("3-1", bus=3, number=4), b"\x02\x02\x01\0"),
]
self.hook = lambda: None
self.header_override = None
fixture = self
class Handler(socketserver.BaseRequestHandler):
def handle(self):
try:
header = exact(self.request, 8)
if len(header) != 8:
return
_, code, _ = p.HEADER.unpack(header)
request = header + (
exact(self.request, 32) if code == p.IMPORT else b""
)
fixture.calls.append(request)
fixture.hook()
self.request.sendall(
fixture.header_override
or p.HEADER.pack(p.VERSION, code & 0x7FFF, 0)
)
if code == p.DEVLIST:
self.request.sendall(struct.pack("!I", len(fixture.listed)))
for raw, interfaces in fixture.listed:
self.request.sendall(raw + interfaces)
return
self.request.sendall(fixture.reply)
while True:
block = self.request.recv(65536)
if not block:
self.request.shutdown(socket.SHUT_WR)
return
self.request.sendall(block)
except OSError:
return
class Backend(socketserver.ThreadingMixIn, socketserver.TCPServer):
daemon_threads = True
self.backend = Backend(("127.0.0.1", 0), Handler)
self.backend_thread = threading.Thread(
target=self.backend.serve_forever,
kwargs={"poll_interval": 0.05},
daemon=True,
)
self.backend_thread.start()
self.addCleanup(self.backend.server_close)
self.addCleanup(self.backend.shutdown)
# Production calls this as root; these protocol tests do not alter users.
with patch.object(p.os, "chown"):
self.gate = p.Gate(
self.root / "gate.sock",
os.getgid(),
lambda name: self.selected.get(name),
self.sysfs,
self.backend.server_address,
max_connections=2,
)
self.addCleanup(self.gate.terminate)
def device(self, directory, serial="selected-serial", number=3):
device = self.root / directory
device.mkdir()
for name, value in {
"idVendor": "1d6b",
"idProduct": "0104",
"serial": serial,
"busnum": "2",
"devnum": str(number),
"bDeviceClass": "00",
}.items():
(device / name).write_text(value)
interface = device / "2-1:1.0"
interface.mkdir()
(interface / "bInterfaceClass").write_text("02")
(device / "driver").symlink_to("/sys/bus/usb/drivers/usbip-host")
link = self.sysfs / "2-1"
link.unlink(missing_ok=True)
link.symlink_to(device, target_is_directory=True)
return device
def connect(self):
connection = socket.socket(socket.AF_UNIX)
connection.settimeout(3)
connection.connect(str(self.gate.path))
self.addCleanup(connection.close)
return connection
def request(self, name=b"2-1\0", code=p.IMPORT):
return p.HEADER.pack(p.VERSION, code, 0) + (
name.ljust(32, b"\0") if code == p.IMPORT else b""
)
def reject(self, request):
connection = self.connect()
connection.sendall(request)
result = exact(connection, 8)
self.assertTrue(not result or p.HEADER.unpack(result)[2] != 0, result)
self.assertEqual(connection.recv(1), b"")
return result
def test_fragmented_selected_import_and_large_binary_full_duplex(self):
connection = self.connect()
for value in self.request():
connection.sendall(bytes([value]))
self.assertEqual(
exact(connection, 320), p.HEADER.pack(p.VERSION, 3, 0) + self.reply
)
payload = bytes(range(256)) * 4096
writer = threading.Thread(target=connection.sendall, args=(payload,))
writer.start()
self.assertEqual(exact(connection, len(payload)), payload)
writer.join(3)
self.assertFalse(writer.is_alive())
connection.shutdown(socket.SHUT_WR)
self.assertEqual(connection.recv(1), b"")
def test_foreign_device_is_rejected_before_native_backend_import(self):
self.reject(self.request(b"3-1\0"))
self.assertEqual(self.calls, [])
def test_malformed_busids_never_reach_backend(self):
for value in (b"2-1\0x", b"../../etc\0", b"2-1\xff\0", b"x" * 32):
with self.subTest(value=value):
self.reject(self.request(value))
self.assertEqual(self.calls, [])
def test_unsupported_control_headers_never_reach_backend(self):
for header in (
p.HEADER.pack(0x0110, p.IMPORT, 0),
p.HEADER.pack(p.VERSION, 0x8001, 0),
p.HEADER.pack(p.VERSION, p.IMPORT, 1),
):
self.reject(header)
self.assertEqual(self.calls, [])
def test_devlist_filters_unselected_export_and_preserves_interface_records(self):
connection = self.connect()
connection.sendall(self.request(code=p.DEVLIST))
reply = exact(connection, 12)
self.assertEqual(reply, p.HEADER.pack(p.VERSION, 5, 0) + struct.pack("!I", 1))
self.assertEqual(exact(connection, 316), self.reply + b"\x02\x02\x01\0")
self.assertEqual(connection.recv(1), b"")
def test_changed_selection_during_import_receives_no_success_payload(self):
self.hook = self.selected.clear
self.reject(self.request())
self.assertEqual(len(self.calls), 1)
def test_mutated_callback_dictionary_cannot_rewrite_saved_selection(self):
self.hook = lambda: self.selected["2-1"].update(id="fedcba654321")
self.reject(self.request())
def test_replaced_sysfs_object_cannot_use_identical_reply_metadata(self):
self.hook = lambda: self.device("replacement")
self.reject(self.request())
def test_driver_change_during_import_is_rejected(self):
def replace():
driver = self.root / "original/driver"
driver.unlink()
driver.symlink_to("/sys/bus/usb/drivers/usb")
self.hook = replace
self.reject(self.request())
def test_descriptor_identity_and_devnum_must_match_pinned_object(self):
for reply in (
descriptor(number=4),
descriptor(vendor=0x05AC),
descriptor("3-1"),
descriptor(bus=3),
):
self.reply = reply
self.reject(self.request())
def test_composite_storage_or_hub_device_is_rejected_before_backend(self):
for name in ("bDeviceClass", "2-1:1.0/bInterfaceClass"):
path = self.root / "original" / name
old = path.read_text()
for value in ("08", "09"):
path.write_text(value)
self.reject(self.request())
path.write_text(old)
self.assertEqual(self.calls, [])
def test_unicode_serial_is_valid_without_changing_busid_encoding(self):
serial = "selected-ñ-设备"
(self.root / "original/serial").write_text(serial)
self.selected["2-1"]["serial"] = serial
connection = self.connect()
connection.sendall(self.request())
self.assertEqual(
exact(connection, 320), p.HEADER.pack(p.VERSION, 3, 0) + self.reply
)
def test_backend_response_version_opcode_and_status_are_validated(self):
for header in (
p.HEADER.pack(0x0110, 3, 0),
p.HEADER.pack(p.VERSION, 5, 0),
p.HEADER.pack(p.VERSION, 3, 0xFFFFFFFF),
):
self.header_override = header
self.reject(self.request())
def test_incomplete_header_times_out_but_accepted_idle_stream_does_not(self):
with patch.object(p, "CONTROL_SECONDS", 0.1):
partial = self.connect()
partial.sendall(b"\x01")
self.assertEqual(partial.recv(1), b"")
connection = self.connect()
connection.sendall(self.request())
self.assertEqual(len(exact(connection, 320)), 320)
time.sleep(0.2)
connection.sendall(b"idle-still-works")
self.assertEqual(exact(connection, 16), b"idle-still-works")
def test_stop_closes_idle_backend_and_client_and_removes_listener(self):
connection = self.connect()
connection.sendall(self.request())
self.assertEqual(len(exact(connection, 320)), 320)
self.gate.terminate()
self.assertEqual(connection.recv(1), b"")
self.assertFalse(self.gate.path.exists())
self.assertIsNotNone(self.gate.poll())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,79 @@
name: Native Monitor AppImage validation
on:
pull_request:
paths:
- '.github/workflows/test-appimage-native.yml'
- 'AppImage/scripts/build_appimage.sh'
- 'AppImage/scripts/appimage_arch.sh'
- 'AppImage/scripts/borg_native_arm.sh'
- '.github/scripts/smoke_native_apprun.sh'
- '.github/scripts/check_appimage_elf.py'
- 'AppImage/scripts/AppRun'
- 'tests/test_appimage_arch.py'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: native-appimage-${{ github.ref }}
cancel-in-progress: true
jobs:
native-build:
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
arch: x86_64
- runner: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Validate native architecture selection
run: |
test "$(uname -m)" = '${{ matrix.arch }}'
python -B -m unittest discover -s tests -p test_appimage_arch.py -v
- name: Refresh package indexes for bundled monitoring tools
# Third-party runner repositories occasionally fail; the build reports missing tools.
run: sudo apt-get update || true
- name: Build native AppImage without publishing it
run: bash AppImage/scripts/build_appimage.sh
- name: Reject foreign ELF payloads
run: python .github/scripts/check_appimage_elf.py /tmp/proxmenux_build/ProxMenux.AppDir '${{ matrix.arch }}'
- name: Extract native AppImage without FUSE
run: |
mkdir /tmp/proxmenux-native-extract
cd /tmp/proxmenux-native-extract
image=$(find "$GITHUB_WORKSPACE/AppImage/dist" -name '*.AppImage' -type f -print -quit)
test -n "$image"
chmod +x "$image"
"$image" --appimage-extract
test -f squashfs-root/usr/bin/flask_server.py
test -f squashfs-root/usr/bin/appimage_arch.sh
test -f squashfs-root/web/index.html
- name: Import bundled Python 3.11 native WebSocket runtime
env:
PYTHONPATH: /tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/vendor/cp311:/tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/dist-packages
run: |
python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print(gevent.__version__)'
bash .github/scripts/smoke_native_apprun.sh /tmp/proxmenux-native-extract/squashfs-root
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Import bundled Python 3.13 native WebSocket runtime
env:
PYTHONPATH: /tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/vendor/cp313:/tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/dist-packages
run: |
python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print(gevent.__version__)'
bash .github/scripts/smoke_native_apprun.sh /tmp/proxmenux-native-extract/squashfs-root
+80 -2
View File
@@ -7,7 +7,9 @@ on:
- 'AppImage/scripts/notification_manager.py'
- 'AppImage/scripts/notification_templates.py'
- 'AppImage/scripts/ai_context_enrichment.py'
- 'AppImage/scripts/ai_providers/base.py'
- 'AppImage/scripts/ai_providers/**'
- 'AppImage/config/verified_ai_models.json'
- 'AppImage/scripts/tests/test_groq_model_recommendations.py'
- 'AppImage/scripts/tests/test_backup_ai_diagnostics.py'
- 'AppImage/scripts/notification_channels.py'
- 'AppImage/components/apprise-service-help.tsx'
@@ -22,11 +24,19 @@ 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'
- 'AppImage/scripts/guest_backup_archives.py'
- 'AppImage/scripts/flask_server.py'
- 'AppImage/scripts/ovs_topology.py'
- 'AppImage/scripts/tests/test_ovs_topology.py'
- 'AppImage/components/network-flow.tsx'
- 'AppImage/components/network-metrics.tsx'
- 'AppImage/tests/ovs-network-flow.cjs'
- 'AppImage/scripts/jwt_middleware.py'
- 'AppImage/scripts/build_appimage.sh'
- 'AppImage/components/guest-backup-list.tsx'
@@ -69,7 +79,9 @@ on:
- 'AppImage/scripts/notification_manager.py'
- 'AppImage/scripts/notification_templates.py'
- 'AppImage/scripts/ai_context_enrichment.py'
- 'AppImage/scripts/ai_providers/base.py'
- 'AppImage/scripts/ai_providers/**'
- 'AppImage/config/verified_ai_models.json'
- 'AppImage/scripts/tests/test_groq_model_recommendations.py'
- 'AppImage/scripts/tests/test_backup_ai_diagnostics.py'
- 'AppImage/scripts/notification_channels.py'
- 'AppImage/components/apprise-service-help.tsx'
@@ -86,6 +98,11 @@ on:
- 'AppImage/scripts/flask_proxmenux_routes.py'
- 'AppImage/scripts/guest_backup_archives.py'
- 'AppImage/scripts/flask_server.py'
- 'AppImage/scripts/ovs_topology.py'
- 'AppImage/scripts/tests/test_ovs_topology.py'
- 'AppImage/components/network-flow.tsx'
- 'AppImage/components/network-metrics.tsx'
- 'AppImage/tests/ovs-network-flow.cjs'
- 'AppImage/scripts/jwt_middleware.py'
- 'AppImage/components/guest-backup-list.tsx'
- 'AppImage/components/virtual-machines.tsx'
@@ -101,6 +118,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 +189,34 @@ 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
ai-model-availability:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Test model ordering and curated availability without network or credentials
run: python -B -m unittest discover -s AppImage/scripts/tests -p test_groq_model_recommendations.py -v
installer-and-release:
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -387,3 +435,33 @@ 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
network-topology:
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'
- uses: actions/setup-node@v6
with:
node-version: '22.14.0'
- name: Test read-only OVS discovery and mixed network API
run: python -B -m unittest discover -s AppImage/scripts/tests -p test_ovs_topology.py -v
- name: Test both Network Flow layouts
run: |
npm ci --prefix .github/ci/offline-node --ignore-scripts --no-audit --no-fund
NODE_PATH="$PWD/.github/ci/offline-node/node_modules" node AppImage/tests/ovs-network-flow.cjs
+50
View File
@@ -0,0 +1,50 @@
name: USB/IP over SSH tests
on:
pull_request:
paths:
- 'scripts/devices/**'
- 'scripts/menus/hw_grafics_menu.sh'
- 'scripts/menus/main_menu.sh'
- 'scripts/menus/search_index.json'
- '.github/scripts/tests/test_usbip*.py'
- '.github/scripts/check_usbip_transport.py'
- '.github/workflows/test-usbip.yml'
- 'docs/usbip.md'
push:
branches: [main, develop]
paths:
- 'scripts/devices/**'
- '.github/scripts/tests/test_usbip*.py'
- '.github/scripts/check_usbip_transport.py'
- '.github/workflows/test-usbip.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
usbip:
runs-on: ubuntu-latest
timeout-minutes: 8
strategy:
fail-fast: false
matrix:
python: ['3.11', '3.13']
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Device ownership, reconnect and systemd validation
env:
PYTHONDONTWRITEBYTECODE: '1'
run: python3 -B -m unittest discover -s .github/scripts/tests -p 'test_usbip*.py' -v
- name: Install fixture-only transport dependencies
run: |
sudo apt-get update -o APT::Update::Error-Mode=any
sudo apt-get install -y socat openssh-client openssh-server iproute2 util-linux
- name: Real SSH and private namespace transport, without USB hardware
run: sudo "$(command -v python3)" -B .github/scripts/check_usbip_transport.py
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
92084408d97ec3c2c17499960cc77ba603d87affe0a648720d69909b1dee8b95 ProxMenux-1.2.8.AppImage
488f19197c38af62a1f3356ce5ef3b84dc8370efe51eb65cfc6b52d301b9df74 ProxMenux-1.2.8.AppImage
+12 -1
View File
@@ -397,6 +397,10 @@ const SELFHST_THEME_LOGOS: Record<string, { lightTheme: string; darkTheme: strin
lightTheme: `${SELFHST_WEBP_BASE}/frigate-dark.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/frigate-light.webp`,
},
"hermes-agent": {
lightTheme: `${SELFHST_WEBP_BASE}/hermes-agent.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/hermes-agent-light.webp`,
},
"llama-cpp": {
lightTheme: `${SELFHST_WEBP_BASE}/llama-cpp.webp`,
darkTheme: `${SELFHST_WEBP_BASE}/llama-cpp-light.webp`,
@@ -415,9 +419,16 @@ const SELFHST_THEME_LOGOS: Record<string, { lightTheme: string; darkTheme: strin
},
}
// Earlier catalog icons that now have a themed selfhst equivalent.
const THEMED_LOGO_ALIASES: Record<string, string> = {
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@main/webp/hermes.webp": "hermes-agent",
}
function selfhstThemeLogos(src: string) {
const alias = THEMED_LOGO_ALIASES[src.toLowerCase()]
if (alias) return SELFHST_THEME_LOGOS[alias]
if (!src.toLowerCase().includes("cdn.jsdelivr.net/gh/selfhst/icons")) return null
const match = src.toLowerCase().match(/\/(frigate|llama-cpp|portainer|tailscale|vaultwarden)(?:-(?:dark|light))?\.webp(?:[?#].*)?$/)
const match = src.toLowerCase().match(/\/(frigate|hermes-agent|llama-cpp|portainer|tailscale|vaultwarden)(?:-(?:dark|light))?\.webp(?:[?#].*)?$/)
return match ? SELFHST_THEME_LOGOS[match[1]] : null
}
+4 -1
View File
@@ -62,7 +62,7 @@ type Bridge = {
type Guest = {
id: string
label: string
kind: "lxc" | "vm" | "host"
kind: "lxc" | "vm" | "host" | "interface"
bridge: string
rx: number
tx: number
@@ -87,6 +87,8 @@ const ICONS: Record<string, string> = {
vm: `<rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/>`,
}
ICONS.interface = ICONS.bridge
const COLORS = {
host: "var(--amber-500, #f59e0b)",
lxc: "var(--cyan-500, #06b6d4)",
@@ -97,6 +99,7 @@ const COLORS = {
// just another NIC.
bond: "var(--orange-500, #f97316)",
bridge: "var(--cyan-500, #06b6d4)",
interface: "var(--cyan-500, #06b6d4)",
gray: "#525252",
}
+12 -4
View File
@@ -101,6 +101,9 @@ interface NetworkInterface {
// Master device resolved from /sys/class/net/<iface>/master — the
// bridge for a guest tap, the bond for a slave NIC.
bridge_owner?: string
network_backend?: "ovs"
ovs_tag?: number | null
ovs_trunks?: number[]
bridge_members?: string[]
bridge_physical_interface?: string
bridge_bond_slaves?: string[]
@@ -568,6 +571,7 @@ export function NetworkMetrics() {
...(networkData.bond_interfaces || []),
...(networkData.bridge_interfaces || []),
...(networkData.vm_lxc_interfaces || []),
...(networkData.interfaces || []).filter((i) => i.network_backend === "ovs" && i.type === "vlan"),
]
const vmLxcInterfaces = (networkData.vm_lxc_interfaces || []).sort((a, b) => {
@@ -867,6 +871,7 @@ export function NetworkMetrics() {
...(networkData.bond_interfaces || []),
...(networkData.bridge_interfaces || []),
...(networkData.vm_lxc_interfaces || []),
...(networkData.interfaces || []).filter((i) => i.network_backend === "ovs" && i.type === "vlan"),
]
const flowData: NetworkFlowData = {
nics: (networkData.physical_interfaces || []).map((p) => ({
@@ -916,7 +921,9 @@ export function NetworkMetrics() {
tx: runningGuests.reduce((a, v) => a + toMBps(v.tx_Bps), 0),
}
})(),
...(networkData.vm_lxc_interfaces || []).map((v) => {
...[...(networkData.vm_lxc_interfaces || []),
...(networkData.interfaces || []).filter((i) => i.network_backend === "ovs" && i.type === "vlan"),
].map((v) => {
// Authoritative bridge from the kernel (read by the
// backend from /sys/class/net/<iface>/master). Fallback
// to bridge_members scan, then first bridge as last
@@ -929,8 +936,8 @@ export function NetworkMetrics() {
(networkData.bridge_interfaces?.[0]?.name || "")
return {
id: v.name,
label: v.vm_name || v.name,
kind: (v.vm_type === "vm" ? "vm" : "lxc") as "vm" | "lxc",
label: v.vm_name || (v.ovs_tag != null ? `${v.name} · VLAN ${v.ovs_tag}` : v.name),
kind: v.type === "vlan" ? "interface" as const : (v.vm_type === "vm" ? "vm" as const : "lxc" as const),
bridge: ownerName,
rx: toMBps(v.rx_Bps),
tx: toMBps(v.tx_Bps),
@@ -1213,6 +1220,7 @@ export function NetworkMetrics() {
...(modalNetworkData.bond_interfaces || []),
...(modalNetworkData.bridge_interfaces || []),
...(modalNetworkData.vm_lxc_interfaces || []),
...(modalNetworkData.interfaces || []).filter((i) => i.network_backend === "ovs" && i.type === "vlan"),
].find((iface) => iface.name === selectedInterface.name)
: selectedInterface
@@ -1298,7 +1306,7 @@ export function NetworkMetrics() {
</div>
<div>
<div className="text-sm text-muted-foreground">MTU</div>
<div className="font-medium">{displayInterface.mtu}</div>
<div className="font-medium">{displayInterface.mtu || t("common.notAvailable")}</div>
</div>
{displayInterface.mac_address && (
<div className="col-span-2">
+2 -4
View File
@@ -110,12 +110,10 @@
"openai/gpt-oss-120b",
"meta-llama/llama-3.1-8b-instruct",
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
"nvidia/nemotron-3-super-120b-a12b:free",
"google/gemma-4-26b-a4b-it:free",
"poolside/laguna-s-2.1:free"
"google/gemma-4-26b-a4b-it:free"
],
"recommended": "meta-llama/llama-3.3-70b-instruct",
"_note": "Curated manually from the 249+ models OpenRouter serves — the auto-verifier passes ~200 today but only a subset are practical for notification translation. Includes the 11 previously-curated paid models plus 22 new mainstream additions (gpt-5.1, claude-opus-4.8, sonnet-5, grok-4.3/4.20, kimi-k2, deepseek-v3.2, nova, cohere-a, sonar, ...) and 6 `:free` variants re-added manually — the free tier is intentionally supported per user request even though today's rate limits blocked some of them from passing the test. Reviewed 2026-10-26: three `:free` variants no longer served by OpenRouter were dropped."
"_note": "Curated manually from the 249+ models OpenRouter serves — the auto-verifier passes ~200 today but only a subset are practical for notification translation. Includes the 11 previously-curated paid models plus 22 new mainstream additions (gpt-5.1, claude-opus-4.8, sonnet-5, grok-4.3/4.20, kimi-k2, deepseek-v3.2, nova, cohere-a, sonar, ...) and 6 `:free` variants re-added manually — the free tier is intentionally supported per user request even though today's rate limits blocked some of them from passing the test. Reviewed 2026-10-26: three `:free` variants no longer served by OpenRouter were dropped. Availability reviewed 2026-11-04 against the public models endpoint: expired poolside/laguna-s-2.1:free and unlisted nvidia/nemotron-3-super-120b-a12b:free removed. This availability check is not a new inference verification; no untested models were added."
},
"ollama": {
"_note": "Ollama models are local, we don't filter them. User manages their own models.",
+4 -1
View File
@@ -6,8 +6,11 @@
# Get the directory where this AppImage is mounted
APPDIR="$(dirname "$(readlink -f "${0}")")"
source "${APPDIR}/usr/bin/appimage_arch.sh" || exit 1
pmx_appimage_arch "$(uname -m)" || exit 1
export PATH="${APPDIR}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/usr/lib:${APPDIR}/lib/x86_64-linux-gnu:${APPDIR}/lib:${LD_LIBRARY_PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/${PMX_APPIMAGE_TRIPLET}:${APPDIR}/usr/lib:${APPDIR}/lib/${PMX_APPIMAGE_TRIPLET}:${APPDIR}/lib:${LD_LIBRARY_PATH}"
# Select the native dependency tree matching the host interpreter. PVE 8
# uses CPython 3.11 and PVE 9 uses CPython 3.13; both runtimes are bundled in
+5 -11
View File
@@ -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,))
@@ -21,8 +21,13 @@ class GroqProvider(AIProvider):
# Exclude non-chat models
EXCLUDED_PATTERNS = ['whisper', 'tts', 'guard', 'tool-use']
# Recommended models (in priority order - versatile/large models first)
RECOMMENDED_PREFIXES = ['llama-3.3', 'llama-3.1-70b', 'llama-3.1-8b', 'mixtral', 'gemma']
# Prefer the verified default and current GPT-OSS models. Llama remains
# available for accounts with access; retired Mixtral/Gemma families have
# no recommendation priority. Sorting never adds IDs to the API response.
RECOMMENDED_PREFIXES = [
'openai/gpt-oss-20b', 'openai/gpt-oss-120b', 'qwen/qwen3.8-27b',
'llama-3.3', 'llama-3.1-8b',
]
def list_models(self) -> List[str]:
"""List available Groq models for chat completions.
@@ -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:
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Shared native build/runtime architecture names. No emulated cross-builds.
pmx_appimage_arch() {
case "$1" in
x86_64|amd64)
PMX_APPIMAGE_ARCH=x86_64
PMX_APPIMAGE_TRIPLET=x86_64-linux-gnu
PMX_APPIMAGE_SUFFIX=""
;;
aarch64|arm64)
PMX_APPIMAGE_ARCH=aarch64
PMX_APPIMAGE_TRIPLET=aarch64-linux-gnu
PMX_APPIMAGE_SUFFIX=-aarch64
;;
*)
echo "Unsupported Monitor AppImage architecture: $1" >&2
return 1
;;
esac
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Borg 1.2.8 has no official ARM64 standalone release. Never ship or run
# its AMD64 executable on ARM; use the host's native distro package instead.
if [ -x /usr/bin/borg ]; then
# The distro package must use its own Python modules and shared libraries.
unset PYTHONPATH PYTHONHOME LD_LIBRARY_PATH
exec /usr/bin/borg "$@"
fi
echo 'Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.' >&2
exit 127
+39 -16
View File
@@ -8,6 +8,8 @@ set -e
WORK_DIR="/tmp/proxmenux_build"
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/appimage_arch.sh"
pmx_appimage_arch "$(uname -m)" || exit 1
DIST_DIR="$SCRIPT_DIR/../dist"
APPIMAGE_ROOT="$SCRIPT_DIR/.."
@@ -24,7 +26,7 @@ ZOPE_EVENT_VERSION="6.2"
GEVENT_WEBSOCKET_VERSION="0.10.1"
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
APPIMAGE_NAME="ProxMenux-${VERSION}${PMX_APPIMAGE_SUFFIX}.AppImage"
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitoring tools..."
@@ -33,17 +35,18 @@ echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitor
# under the non-root GitHub Actions runner — previously hardcoded to
# /var/cache/, which the CI user can't write to (mkdir Permission
# denied → build aborted before doing any actual work).
APPIMAGETOOL_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/proxmenux-build/appimagetool"
APPIMAGETOOL_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/proxmenux-build/appimagetool-${PMX_APPIMAGE_ARCH}"
# Preserve a cached copy of appimagetool across builds. wget -q has bitten
# us repeatedly when GitHub momentarily rate-limits or the runner has no
# network — the result is a 0-byte file that passes the `[ -f ]` check on
# the next run and breaks the build silently.
if [ -f "$WORK_DIR/appimagetool" ] && [ -s "$WORK_DIR/appimagetool" ]; then
mkdir -p "$(dirname "$APPIMAGETOOL_CACHE")"
cp -f "$WORK_DIR/appimagetool" "$APPIMAGETOOL_CACHE"
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${PMX_APPIMAGE_ARCH}.AppImage"
if [ "$PMX_APPIMAGE_ARCH" = aarch64 ]; then
# The old AppImageKit ARM build does not accept ARCH=aarch64 reliably.
# Use a versioned tool; leave the existing AMD64 tool unchanged.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-aarch64.AppImage"
APPIMAGETOOL_CACHE="${APPIMAGETOOL_CACHE}-1.9.1"
fi
# Architecture-specific caches never reuse an executable from a different CPU.
# Clean and create work directory
rm -rf "$WORK_DIR"
mkdir -p "$APP_DIR"
@@ -57,7 +60,7 @@ if [ -s "$APPIMAGETOOL_CACHE" ]; then
fi
if [ ! -s "$WORK_DIR/appimagetool" ]; then
echo "📥 Downloading appimagetool..."
wget --tries=3 --timeout=60 "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool" || true
wget --tries=3 --timeout=60 "$APPIMAGETOOL_URL" -O "$WORK_DIR/appimagetool" || true
if [ ! -s "$WORK_DIR/appimagetool" ]; then
echo "❌ Failed to download appimagetool" >&2
exit 1
@@ -67,6 +70,17 @@ if [ ! -s "$WORK_DIR/appimagetool" ]; then
cp -f "$WORK_DIR/appimagetool" "$APPIMAGETOOL_CACHE"
fi
APPIMAGETOOL_ARGS=()
if [ "$PMX_APPIMAGE_ARCH" = aarch64 ]; then
echo "f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 $WORK_DIR/appimagetool" | sha256sum -c -
# Without --runtime-file, appimagetool 1.9.1 embeds the unpinned
# `continuous` runtime. Use a released runtime checked by its digest.
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-aarch64"
wget --tries=3 --timeout=60 "$RUNTIME_URL" -O "$WORK_DIR/runtime-aarch64"
echo "00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444 $WORK_DIR/runtime-aarch64" | sha256sum -c -
APPIMAGETOOL_ARGS=(--runtime-file "$WORK_DIR/runtime-aarch64")
fi
# Create directory structure
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
@@ -126,6 +140,7 @@ cd "$SCRIPT_DIR"
# Copy Flask server
echo "📋 Copying Flask server..."
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/ovs_topology.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/zram_metrics.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/flask_auth_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_auth_routes.py not found"
cp "$SCRIPT_DIR/auth_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ auth_manager.py not found"
@@ -245,6 +260,7 @@ fi
# hb_ensure_borg used to download on demand — kept in lockstep so both
# code paths see the same version semantics. SHA256 is the upstream
# release checksum; bump both together.
if [ "$PMX_APPIMAGE_ARCH" = x86_64 ]; then
BORG_VERSION="1.2.8"
BORG_URL="https://github.com/borgbackup/borg/releases/download/${BORG_VERSION}/borg-linux64"
BORG_SHA256="cfa50fb704a93d3a4fa258120966345fddb394f960dca7c47fcb774d0172f40b"
@@ -280,6 +296,12 @@ else
exit 1
fi
else
echo "⚠️ No official Borg 1.2.8 ARM64 standalone binary; using native-host adapter"
cp "$SCRIPT_DIR/borg_native_arm.sh" "$APP_DIR/usr/bin/borg"
chmod +x "$APP_DIR/usr/bin/borg"
fi
# Copy Next.js build
echo "📋 Copying web dashboard..."
if [ -d "$APPIMAGE_ROOT/out" ]; then
@@ -310,6 +332,7 @@ fi
echo "📋 Copying AppRun script..."
if [ -f "$SCRIPT_DIR/AppRun" ]; then
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
cp "$SCRIPT_DIR/appimage_arch.sh" "$APP_DIR/usr/bin/appimage_arch.sh"
chmod +x "$APP_DIR/AppRun"
echo "✅ AppRun script copied successfully"
else
@@ -379,7 +402,7 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \
# Phase 3b: Install the complete gevent/WSS runtime separately for every
# supported host ABI. Keeping each dependency tree isolated avoids mixing
# CPython-specific modules from the build runner with those loaded by PVE.
# The two --platform values allow gevent's manylinux_2_28 wheels and the
# The three --platform values allow gevent's manylinux_2_28 wheels and the
# backwards-compatible manylinux2014 zope.interface wheels to be resolved in
# one deterministic installation.
install_gevent_runtime() {
@@ -394,9 +417,9 @@ install_gevent_runtime() {
--target "$target" \
--upgrade \
--only-binary=:all: \
--platform manylinux_2_28_x86_64 \
--platform manylinux_2_27_x86_64 \
--platform manylinux2014_x86_64 \
--platform "manylinux_2_28_${PMX_APPIMAGE_ARCH}" \
--platform "manylinux_2_27_${PMX_APPIMAGE_ARCH}" \
--platform "manylinux2014_${PMX_APPIMAGE_ARCH}" \
--implementation cp \
--python-version "$python_version" \
--abi "$abi" \
@@ -547,7 +570,7 @@ if [ -d "$APP_DIR/bin" ]; then
fi
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/x86_64-linux-gnu:$APP_DIR/usr/lib:$APP_DIR/usr/lib/x86_64-linux-gnu"
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/${PMX_APPIMAGE_TRIPLET}:$APP_DIR/usr/lib:$APP_DIR/usr/lib/${PMX_APPIMAGE_TRIPLET}"
if ! find "$APP_DIR/usr/lib" "$APP_DIR/lib" -maxdepth 3 -name 'libfreeipmi.so.17*' | grep -q .; then
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
@@ -608,7 +631,7 @@ echo "🔨 Building unified AppImage v${VERSION}..."
cd "$WORK_DIR"
export NO_CLEANUP=1
export APPIMAGE_EXTRACT_AND_RUN=1
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
ARCH="$PMX_APPIMAGE_ARCH" ./appimagetool --no-appstream --verbose "${APPIMAGETOOL_ARGS[@]}" "$APP_DIR" "$APPIMAGE_NAME"
# Move to dist directory
mv "$APPIMAGE_NAME" "$DIST_DIR/"
+48 -6
View File
@@ -53,6 +53,7 @@ import threading
import urllib.parse
import uuid
import disk_identity
import ovs_topology
import hardware_monitor
from health_persistence import health_persistence
import xml.etree.ElementTree as ET
@@ -6258,17 +6259,24 @@ def _iface_master(iface_name):
# Chain: veth/tap → fwbr (intermediate) → fwpr (peer) → vmbr (real).
def _resolve_bridge_owner(iface_name):
import re
ovs_owners = ovs_topology.get_topology().get('owners', {})
if iface_name in ovs_owners:
return ovs_owners[iface_name]
master = _iface_master(iface_name)
if not master:
return None
if master.startswith('fwbr'):
fwpr = re.sub(r'^fwbr', 'fwpr', master)
fwpr = re.sub(r'i(\d+)$', r'p\1', fwpr)
# A Linux bridge reaches the firewall bridge through fwprNpM; an OVS
# bridge through its own internal port fwlnNoM.
suffix = master[len('fwbr'):]
links = (re.sub(r'i(\d+)$', r'p\1', 'fwpr' + suffix),
re.sub(r'i(\d+)$', r'o\1', 'fwln' + suffix))
try:
if os.path.exists(f'/sys/class/net/{fwpr}'):
real = _iface_master(fwpr)
if real:
return real
for link in links:
if os.path.exists(f'/sys/class/net/{link}'):
real = ovs_owners.get(link) or _iface_master(link)
if real:
return real
except Exception:
pass
return master
@@ -6351,6 +6359,10 @@ def get_interface_type(interface_name):
if interface_name.startswith(('tun', 'vnet', 'docker', 'virbr')):
return 'skip'
ovs_type = ovs_topology.get_topology().get('types', {}).get(interface_name)
if ovs_type:
return ovs_type
# Check if it's a bond — sysfs first (name-agnostic), then the
# "bondN" convention as fallback when /sys isn't readable.
if _is_bond_master(interface_name) or interface_name.startswith('bond'):
@@ -6590,6 +6602,7 @@ def get_network_info():
pass
vm_lxc_map = get_vm_lxc_names()
ovs = ovs_topology.get_topology()
# Get network interfaces
net_if_addrs = psutil.net_if_addrs()
@@ -6724,8 +6737,10 @@ def get_network_info():
# vmbr so a guest behind the Proxmox firewall still lands
# under its true bridge in the Network Flow diagram.
master = _resolve_bridge_owner(interface_name)
master = ovs.get('owners', {}).get(interface_name) or master
if master:
interface_info['bridge_owner'] = master
interface_info.update(ovs.get('ports', {}).get(interface_name, {}))
if interface_type == 'bond':
bond_info = get_bond_info(interface_name)
@@ -6738,6 +6753,12 @@ def get_network_info():
if interface_type == 'bridge':
bridge_info = get_bridge_info(interface_name)
if interface_name in ovs.get('bridges', {}):
bridge_info['members'] = ovs['bridges'][interface_name]
bridge_info['physical_interface'] = next((member for member in bridge_info['members']
if member in ovs.get('bonds', {}) or _is_physical_dev(member)), None)
bridge_info['bond_slaves'] = ovs.get('bonds', {}).get(bridge_info['physical_interface'], {}).get('bond_slaves', [])
interface_info['network_backend'] = 'ovs'
interface_info['bridge_members'] = bridge_info['members']
interface_info['bridge_physical_interface'] = bridge_info['physical_interface']
interface_info['bridge_physical_duplex'] = bridge_info['physical_duplex']
@@ -6758,6 +6779,22 @@ def get_network_info():
if interface_type == 'bond':
network_data['bond_interfaces'].append(interface_info)
# An OVS bond is an OVSDB port, not a kernel network interface.
for name, detail in ovs.get('bonds', {}).items():
slaves = [item for item in network_data['physical_interfaces']
if item['name'] in detail['bond_slaves']]
network_data['bond_interfaces'] = [item for item in network_data['bond_interfaces'] if item['name'] != name]
bond_interface = {
'name': name, 'type': 'bond', 'addresses': [],
'status': 'up' if any(item['status'] == 'up' for item in slaves) else 'down',
# OVSDB membership does not reveal active-backup capacity.
'speed': 0, 'duplex': 'unknown', 'mtu': 0, 'mac_address': None,
'rx_Bps': sum(item.get('rx_Bps', 0) for item in slaves),
'tx_Bps': sum(item.get('tx_Bps', 0) for item in slaves), **detail}
network_data['bond_interfaces'].append(bond_interface)
network_data['interfaces'] = [item for item in network_data['interfaces'] if item['name'] != name]
network_data['interfaces'].append(bond_interface)
network_data['physical_active_count'] = physical_active_count
network_data['physical_total_count'] = physical_total_count
network_data['bridge_active_count'] = bridge_active_count
@@ -6808,6 +6845,11 @@ def get_network_info():
bridges_per_nic.setdefault(slave, []).append(bridge['name'])
else:
bridges_per_nic.setdefault(phys, []).append(bridge['name'])
for name, members in ovs.get('bridges', {}).items():
for member in members:
for nic in ovs.get('bonds', {}).get(member, {}).get('bond_slaves', [member]):
if name not in bridges_per_nic.setdefault(nic, []):
bridges_per_nic[nic].append(name)
for phy in network_data['physical_interfaces']:
if phy['name'] in bridges_per_nic:
phy['used_by_bridges'] = bridges_per_nic[phy['name']]
+36 -64
View File
@@ -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:
+109
View File
@@ -0,0 +1,109 @@
"""Read-only OVSDB topology; Linux bridges continue to use their sysfs path.
OVS does not publish Linux bridge brif/master links, and an OVSBond has
no kernel netdev. Read the three OVSDB tables once per short cache window
instead of guessing from names or changing the host's network config.
"""
import json
import shutil
import subprocess
import threading
import time
_CACHE_SECONDS = 15
_lock = threading.Lock()
_cached_at = 0
_cached = {}
def _value(value):
if isinstance(value, list) and len(value) == 2:
if value[0] in ('uuid', 'named-uuid'):
return value[1]
if value[0] == 'set':
return [_value(item) for item in value[1]]
return value
def _rows(table):
return [{key: _value(value) for key, value in zip(table['headings'], row)}
for row in table['data']]
def _list(value):
return value if isinstance(value, list) else [] if value is None or value == '' else [value]
def parse_topology(bridges, ports, interfaces):
"""Normalize OVSDB UUID references, including singleton sets and bonds."""
iface_by_id = {row['_uuid']: row for row in _rows(interfaces)}
port_by_id = {row['_uuid']: row for row in _rows(ports)}
topology = {'types': {}, 'owners': {}, 'bridges': {}, 'bonds': {}, 'ports': {}}
for bridge in _rows(bridges):
name = bridge['name']
topology['types'][name] = 'bridge'
members = []
for port_id in _list(bridge.get('ports')):
port = port_by_id.get(port_id)
if not port:
continue
port_name = port['name']
ifaces = [iface_by_id[i] for i in _list(port.get('interfaces')) if i in iface_by_id]
# The bridge's own internal port is not a downstream member.
if port_name == name:
continue
members.append(port_name)
detail = {'bridge_owner': name, 'network_backend': 'ovs',
'ovs_tag': next(iter(_list(port.get('tag'))), None),
'ovs_trunks': _list(port.get('trunks'))}
topology['ports'][port_name] = detail
topology['owners'][port_name] = name
is_bond = len(ifaces) > 1
if is_bond:
topology['types'][port_name] = 'bond'
mode = port.get('bond_mode') or 'balance-slb'
topology['bonds'][port_name] = {
'bond_mode': mode, 'bond_slaves': [i['name'] for i in ifaces],
# OVSDB alone does not tell us which slave is active.
'bond_active_slave': None, 'bond_supports_failover': False,
'bond_slave_status': {}, **detail}
for iface in ifaces:
iface_name = iface['name']
topology['owners'][iface_name] = name
topology['ports'][iface_name] = detail
if is_bond:
topology['ports'][iface_name] = {**detail, 'bond_master': port_name,
'bond_role': 'member'}
elif iface.get('type') == 'internal' and not iface_name.startswith(('fwpr', 'fwln')):
topology['types'][iface_name] = 'vlan'
topology['bridges'][name] = members
return topology
def get_topology():
"""Bounded, cached reads; missing OVS or an unavailable daemon is harmless."""
global _cached_at, _cached
with _lock:
now = time.monotonic()
if now - _cached_at < _CACHE_SECONDS:
return _cached
binary = shutil.which('ovs-vsctl')
result = {}
if binary:
try:
tables = []
for table, columns in (
('Bridge', '_uuid,name,ports'),
('Port', '_uuid,name,interfaces,bond_mode,tag,trunks'),
('Interface', '_uuid,name,type'),
):
proc = subprocess.run(
[binary, '--timeout=1', '--format=json', f'--columns={columns}', 'list', table],
capture_output=True, text=True, timeout=2, check=True)
tables.append(json.loads(proc.stdout))
result = parse_topology(*tables)
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError):
# Do not keep stale ownership when OVS disappears or is reconfigured.
result = {}
_cached, _cached_at = result, now
return _cached
@@ -0,0 +1,52 @@
"""Provider model ordering and curated availability, without credentials/network."""
import io
import json
from pathlib import Path
import sys
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from ai_providers.groq_provider import GroqProvider
class GroqModelRecommendationsTests(unittest.TestCase):
def fetch(self, ids):
response = io.BytesIO(json.dumps({'data': [{'id': model} for model in ids]}).encode())
with patch('ai_providers.groq_provider.urllib.request.urlopen', return_value=response) as request:
models = GroqProvider(api_key='fixture-only').list_models()
self.assertEqual(request.call_args.kwargs['timeout'], 10)
return models
def test_current_verified_models_precede_legacy_and_unknown_families(self):
listed = ['mixtral-fixture', 'allam-2-7b', 'llama-3.3-70b-versatile',
'openai/gpt-oss-120b', 'qwen/qwen3.8-27b', 'openai/gpt-oss-20b']
self.assertEqual(self.fetch(listed)[:3],
['openai/gpt-oss-20b', 'openai/gpt-oss-120b', 'qwen/qwen3.8-27b'])
self.assertCountEqual(self.fetch(listed), listed)
def test_recommendations_never_invent_models_or_remove_arabic_model(self):
self.assertEqual(self.fetch(['allam-2-7b']), ['allam-2-7b'])
self.assertEqual(self.fetch([]), [])
def test_non_chat_models_still_excluded(self):
self.assertEqual(self.fetch(['whisper-large-v3', 'llama-prompt-guard-2-22m',
'openai/gpt-oss-20b']), ['openai/gpt-oss-20b'])
def test_missing_credentials_do_not_fetch_models(self):
with patch('ai_providers.groq_provider.urllib.request.urlopen') as request:
self.assertEqual(GroqProvider().list_models(), [])
request.assert_not_called()
def test_curated_catalog_keeps_verified_default_and_removes_unavailable_free_ids(self):
catalog = json.loads((Path(__file__).resolve().parents[2] / 'config/verified_ai_models.json').read_text())
self.assertEqual(GroqProvider.RECOMMENDED_PREFIXES[0], catalog['groq']['recommended'])
for provider in ('groq', 'openrouter'):
self.assertIn(catalog[provider]['recommended'], catalog[provider]['models'])
self.assertNotIn('poolside/laguna-s-2.1:free', catalog['openrouter']['models'])
self.assertNotIn('nvidia/nemotron-3-super-120b-a12b:free', catalog['openrouter']['models'])
self.assertIn('allam-2-7b', catalog['groq']['models'])
if __name__ == '__main__':
unittest.main()
@@ -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",
+148
View File
@@ -0,0 +1,148 @@
"""OVSDB fixtures exercise live ownership, UUID sets, bonds and API output."""
import ast
import importlib.util
import json
import os
from pathlib import Path
import socket
import subprocess
import types
import unittest
from unittest.mock import patch
SCRIPTS = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('ovs_topology_test', SCRIPTS / 'ovs_topology.py')
ovs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ovs)
def uuid(name):
return ['uuid', name]
def refs(*names):
return ['set', [uuid(name) for name in names]]
def table(headings, *rows):
return {'headings': headings.split(','), 'data': list(rows)}
def fixture():
# Mixed Linux/OVS host, named bond without a kernel netdev, tagged
# internal port, direct guest and guest behind a Proxmox firewall,
# which on OVS links its fwbr bridge through the internal port fwlnNoM.
return (
table('_uuid,name,ports', ['b1', 'switch0', refs('p0', 'p1', 'p2', 'p3', 'p4', 'p5')]),
table('_uuid,name,interfaces,bond_mode,tag,trunks',
['p0', 'switch0', uuid('i0'), '', ['set', []], ['set', []]],
['p1', 'eno1', uuid('i1'), '', ['set', []], ['set', [10, 20]]],
['p2', 'uplink', refs('i2', 'i3'), 'balance-tcp', ['set', []], ['set', []]],
['p3', 'vlan100', uuid('i4'), '', 100, ['set', []]],
['p4', 'tap103i0', uuid('i5'), '', 100, ['set', []]],
['p5', 'fwln115o0', uuid('i6'), '', 10, ['set', []]]),
table('_uuid,name,type', ['i0', 'switch0', 'internal'], ['i1', 'eno1', ''],
['i2', 'eno2', ''], ['i3', 'eno3', ''], ['i4', 'vlan100', 'internal'],
['i5', 'tap103i0', ''], ['i6', 'fwln115o0', 'internal']))
class OvsTopology(unittest.TestCase):
def setUp(self):
ovs._cached_at = 0
ovs._cached = {}
self.addCleanup(setattr, ovs, '_cached_at', 0)
self.addCleanup(setattr, ovs, '_cached', {})
def test_ovsdb_singletons_bonds_tags_and_internal_ports(self):
data = ovs.parse_topology(*fixture())
self.assertEqual(data['types']['switch0'], 'bridge')
self.assertEqual(data['types']['vlan100'], 'vlan')
self.assertNotIn('switch0', data['bridges']['switch0'])
self.assertEqual(data['owners']['tap103i0'], 'switch0')
self.assertEqual(data['ports']['vlan100']['ovs_tag'], 100)
self.assertEqual(data['ports']['eno1']['ovs_trunks'], [10, 20])
self.assertEqual(data['bonds']['uplink']['bond_slaves'], ['eno2', 'eno3'])
self.assertIsNone(data['bonds']['uplink']['bond_active_slave'])
self.assertEqual(data['ports']['eno2']['bond_master'], 'uplink')
def test_zero_vlan_tag_is_preserved(self):
tables = fixture()
tables[1]['data'][3][4] = 0
self.assertEqual(ovs.parse_topology(*tables)['ports']['vlan100']['ovs_tag'], 0)
def test_no_ovs_does_not_spawn_any_process(self):
with patch.object(ovs.shutil, 'which', return_value=None), patch.object(ovs.subprocess, 'run') as run:
self.assertEqual(ovs.get_topology(), {})
run.assert_not_called()
def test_read_only_queries_cached_and_failure_drops_stale_topology(self):
replies = [types.SimpleNamespace(stdout=json.dumps(t)) for t in fixture()]
with patch.object(ovs.shutil, 'which', return_value='/usr/bin/ovs-vsctl'), \
patch.object(ovs.subprocess, 'run', side_effect=replies) as run:
first = ovs.get_topology()
self.assertEqual(ovs.get_topology(), first)
self.assertEqual(run.call_count, 3)
for call in run.call_args_list:
self.assertEqual(call.args[0][-2], 'list')
self.assertIn('--timeout=1', call.args[0])
ovs._cached_at = 0
with patch.object(ovs.shutil, 'which', return_value='/usr/bin/ovs-vsctl'), \
patch.object(ovs.subprocess, 'run', side_effect=subprocess.TimeoutExpired('ovs-vsctl', 2)):
self.assertEqual(ovs.get_topology(), {})
def test_malformed_or_reconfigured_database_is_harmless(self):
for payload in ('not-json', '{}', '{"headings": [], "data": [[]]}'):
ovs._cached_at = 0
with patch.object(ovs.shutil, 'which', return_value='/usr/bin/ovs-vsctl'), \
patch.object(ovs.subprocess, 'run', return_value=types.SimpleNamespace(stdout=payload)):
self.assertEqual(ovs.get_topology(), {})
def api_namespace(self):
names = {'get_network_info', 'get_interface_type', '_resolve_bridge_owner'}
tree = ast.parse((SCRIPTS / 'flask_server.py').read_text())
nodes = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in names]
data = ovs.parse_topology(*fixture())
names = ['eno1', 'eno2', 'eno3', 'switch0', 'vlan100', 'tap103i0', 'tap115i0', 'vmbr1']
stats = {name: types.SimpleNamespace(isup=True, speed=1000, duplex=2, mtu=1500) for name in names}
counter = types.SimpleNamespace(bytes_sent=1000, bytes_recv=2000, packets_sent=10, packets_recv=20,
errin=0, errout=0, dropin=0, dropout=0)
ns = {'os': os, 'socket': socket,
'ovs_topology': types.SimpleNamespace(get_topology=lambda: data),
'get_proxmox_node_name': lambda: 'pve', 'get_vm_lxc_names': lambda: {},
'psutil': types.SimpleNamespace(net_if_addrs=lambda: dict.fromkeys(names, []),
net_if_stats=lambda: stats,
net_io_counters=lambda **kw: dict.fromkeys(names, counter) if kw else counter),
'_ensure_net_rate_sampler': lambda: None, '_compute_per_nic_rates_live': lambda _: {},
'_is_bond_master': lambda _: False, '_is_physical_dev': lambda name: name.startswith('eno'),
'_ethtool_max_speed': lambda _: 1000,
'_iface_master': lambda name: {'tap115i0': 'fwbr115i0', 'fwln115o0': 'ovs-system'}.get(name),
'extract_vmid_from_interface': lambda name: (103 if '103' in name else 115, 'vm'),
'get_bridge_info': lambda _: {'members': [], 'physical_interface': None,
'physical_duplex': 'unknown', 'bond_slaves': []}}
exec(compile(ast.Module(body=nodes, type_ignores=[]), 'flask_server.py', 'exec'), ns)
return ns
def test_api_preserves_linux_bridge_and_assigns_ovs_guests_and_bond(self):
ns = self.api_namespace()
# OVS creates no fwpr peer for the firewall bridge.
with patch('os.path.exists', side_effect=lambda path: 'fwpr' not in str(path)):
result = ns['get_network_info']()
self.assertNotIn('error', result)
self.assertEqual({i['name'] for i in result['bridge_interfaces']}, {'switch0', 'vmbr1'})
self.assertEqual(result['bond_interfaces'][0]['name'], 'uplink')
self.assertEqual(result['bond_interfaces'][0]['speed'], 0)
self.assertIsNone(result['bond_interfaces'][0]['mac_address'])
self.assertIn('uplink', {i['name'] for i in result['interfaces']})
for guest in result['vm_lxc_interfaces']:
self.assertEqual(guest['bridge_owner'], 'switch0')
self.assertEqual(next(i for i in result['interfaces'] if i['name'] == 'vlan100')['ovs_tag'], 100)
for nic in result['physical_interfaces']:
self.assertIn('switch0', nic['used_by_bridges'])
def test_packaged_module_is_shipped_with_backend(self):
self.assertIn('cp "$SCRIPT_DIR/ovs_topology.py" "$APP_DIR/usr/bin/"',
(SCRIPTS / 'build_appimage.sh').read_text())
if __name__ == '__main__':
unittest.main()
+39
View File
@@ -0,0 +1,39 @@
// Exercise both shipped SVG layouts with OVS internal ports and bond topology.
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const ts = require('typescript')
const source = fs.readFileSync(path.join(__dirname, '../components/network-flow.tsx'), 'utf8')
const code = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX },
}).outputText
const sandbox = { exports: {}, require: () => ({}), console }
vm.runInNewContext(code, sandbox)
const data = {
nics: [
{ id: 'eno1', link: '1 Gb/s', rx: 1, tx: 2, status: 'up' },
{ id: 'eno2', link: '1 Gb/s', rx: 1, tx: 2, status: 'up', bond: 'uplink', role: 'member' },
{ id: 'eno3', link: '1 Gb/s', rx: 1, tx: 2, status: 'up', bond: 'uplink', role: 'member' },
],
bonds: [{ id: 'uplink', mode: 'balance-tcp', rx: 2, tx: 4, status: 'up' }],
bridges: [{ id: 'switch0', parent: 'eno1' }, { id: 'vmbr1' }],
consumers: [
{ id: 'host', kind: 'host', label: 'host', bridge: 'switch0', rx: 1, tx: 2 },
{ id: 'vlan100', kind: 'interface', label: 'vlan100 · VLAN 100', bridge: 'switch0', rx: 0, tx: 0 },
{ id: 'tap103i0', kind: 'vm', label: 'VM 103', bridge: 'switch0', rx: 1, tx: 2 },
{ id: 'veth115i0', kind: 'lxc', label: 'LXC 115', bridge: 'switch0', rx: 1, tx: 2 },
{ id: 'tap104i0', kind: 'vm', label: 'VM 104', bridge: 'vmbr1', rx: 1, tx: 2 },
],
}
const labels = { down: 'Down', active: 'Active', standby: 'Standby' }
for (const render of [sandbox.renderHorizontal, sandbox.renderVertical]) {
const result = render(data, 1500, labels, 'Bytes')
for (const id of ['switch0', 'vmbr1', 'vlan100', 'tap103i0', 'veth115i0', 'tap104i0', 'uplink']) {
assert.ok(result.svg.includes(`data-node-id="${id}"`), `layout must draw ${id}`)
}
assert.ok(result.svg.includes('vlan100 · VLAN 100'))
assert.ok(!result.svg.includes('NaN'))
assert.ok(!result.svg.includes('undefined'))
}
console.log('PASS: both Network Flow layouts render OVS bonds, internal VLAN ports and mixed Linux/OVS guests')
+37
View File
@@ -0,0 +1,37 @@
# Native Monitor AppImage builds
The builder selects its native architecture using `uname -m`. Supported targets
are x86_64 and aarch64 (ARM64). It selects matching appimagetool executables,
separate download caches, manylinux wheels and GNU library paths. Cross-building
by overriding a target variable is intentionally unsupported: APT packages and
other native Python dependencies must also match the build machine.
Run `bash AppImage/scripts/build_appimage.sh` on a Linux builder with Node.js,
Python/pip, APT and the usual AppImage build dependencies. AMD64 keeps the existing
`ProxMenux-VERSION.AppImage` name; ARM64 produces
`ProxMenux-VERSION-aarch64.AppImage`.
The native validation workflow builds both architectures on GitHub's native
Ubuntu 22.04 runners, extracts each AppImage without FUSE, and imports its bundled
CPython 3.11 and 3.13 WebSocket/image runtimes, including the extracted launcher
with only its final server execution intercepted. It does not upload artifacts,
change release assets, or modify an installed Monitor.
This build lane is a prerequisite for ARM distribution, not a declaration of
PXVIRT or Raspberry Pi hardware compatibility. ARM release/checksum publication,
installer selection and validation on the maintainer's PXVIRT lab remain separate
steps before ARM installations can use a published image. The current release
and installer distribution remain unchanged.
## Borg host backups on ARM64
Borg 1.2.8 publishes an AMD64 standalone executable but no ARM64 equivalent.
ARM64 builds therefore include a small adapter that uses `/usr/bin/borg` from
the host's native `borgbackup` package. Without that package it exits with a
clear error; it never runs the AMD64 binary. The host-backup helper likewise
refuses AMD64 cached/downloaded binaries on non-AMD64 hosts. The existing AMD64
standalone binary and checksum remain unchanged. Operators must install their
native distro package separately; this build does not install it on the host.
ARM64 uses the official versioned appimagetool 1.9.1 with SHA256 verification;
the existing AMD64 packaging tool remains unchanged.
+216
View File
@@ -0,0 +1,216 @@
# USB/IP over SSH
Open **Hardware: GPUs and Coral-TPU → USB/IP over SSH** to share a
selected USB device or connect this Linux machine to a device on another host.
The same menu is available through the main menu search (`usbip`, `zigbee`,
`bluetooth` or `printer`). This implements the host and Linux client setup
requested in [#350](https://github.com/MacRimi/ProxMenux/issues/350).
## Server
1. Enable the USB/IP server. Missing `usbip`, `socat`, `kmod` and `openssh-client`
packages are offered from the existing Debian repositories. Repository and
SSH server settings are preserved.
2. Choose **Share a local USB device** and select a device by its bus ID, product
and serial number. Confirm that it will become unavailable to local programs
and guests using ordinary USB passthrough.
3. Check **View USB/IP status**. The device should show `exported` and a bus ID.
Binding errors are reported separately rather than treated as a successful
export. Inspect the service log for startup errors:
```sh
journalctl -u proxmenux-usbip-server.service
```
The server reloads saved selections every three seconds. It keeps open usbfs
references to each original USB device and its USB ancestors before binding.
After binding it marks the live match REMOV and verifies the original is still
bound. Replacement devices cannot use that match for automatic USB/IP probing.
On physical removal, the worker waits for the original device's completed kernel
remove event, uses the still-pinned original reference to clear that one retained
slot, and verifies disappearance before binding again. Other exports are left
running. Reconnection at the same port, movement to another port and normal
present-device release do not require reloading a shared module.
If the worker is killed, a bind cannot be verified, removal events are lost, or
an older installation left a retained slot, that slot has no proven lifetime
reference. The worker reports an error and refuses to detach a new local device
or attempt absent-slot recovery. Stop sharing while the original remains present;
if that cannot clear the slot, a host reboot is the supported recovery. This
exceptional recovery interrupts the host, so schedule it yourself; the manager
never reboots it or reloads USB modules automatically. Graceful service stop
releases managed bindings before dropping the references.
Without a serial number, the vendor/product and original bus ID must match;
the manager will not guess which identical dongle to bind. Duplicate serial
numbers are rejected as ambiguous. USB hubs and mass-storage devices, including
composite devices with a storage interface, are excluded. So are USB network
adapters: on many mini PCs one is the management link of the host.
**Stop sharing** removes one saved selection and waits for confirmed release.
A cleanup error or timeout is reported instead of claiming that eject succeeded.
**Disable server** stops the service and releases its
managed exports, retaining selections for the next enable. Devices already
bound by another USB/IP setup are not adopted or unbound by this manager.
Native USB/IP unbind reprobes the device so its normal local driver can return.
## Transport and authentication
USB/IP has no built-in authentication or encryption. The generated server unit
uses systemd `PrivateNetwork=yes`: `usbipd --ipv4 --tcp-port 3240` and its
Python protocol gate run in an isolated network namespace. There is **no USB/IP TCP
listener in the host network namespace**, and this feature does not open a
firewall port. Running the manager's `serve` command outside a private network
namespace is rejected before spawning a listener.
The gate exposes `/run/proxmenux-usbip-server/usbip.sock` to the dedicated
`proxmenux-usbip` group (0660), inside a root-owned 0750 directory. For each native
DEVLIST and IMPORT, the gate checks the saved selection,
verified ownership, current USB identity, driver and sysfs incarnation. It checks
again before releasing a successful import reply; an unselected export bound by
another setup cannot bypass selection by requesting its bus ID directly. Control
requests have bounded time, memory and connection counts. Accepted URB streams
retain full-duplex transfer, half-close and backpressure without an idle timeout.
Root alone can read or change the managed device/configuration state.
A separate group-readable
catalog contains only the selected USB device metadata.
Before enabling the server, choose **Authorize a client SSH public key** and
supply the path to a dedicated client's `.pub` file already copied to the server.
This creates a local system account `proxmenux-usbip` with a disabled password,
no administrative group membership, and a root-owned home and `authorized_keys`.
Each managed key uses OpenSSH `restrict` and a fixed forced command. The command
accepts only `catalog` (read selected devices) and `stream` (connect the one USB/IP
Unix socket). Shell commands, PTY, agent/X11 forwarding and TCP/Unix forwarding
are prohibited. This does not use or modify Proxmox's cluster-wide root keys,
root `authorized_keys`, or SSH server settings. A conflicting pre-existing
account/home is rejected rather than adopted. Authorize keys before starting
the server; `useradd` and an existing OpenSSH server are required.
The client holds its loopback TCP listening socket continuously, then launches
an SSH `stream` session for each connection. It does not release a port probe
and later ask SSH to bind the same port. Unknown or changed host keys and wrong
client keys fail closed. Create a dedicated key, not one used for root access.
Anyone with that key can access **all selected USB/IP exports**; it is not a
per-device authorization policy. Local users of the client can also connect to
the loopback port, so use trusted clients. To revoke a client, remove its managed
line from `/var/lib/proxmenux-usbip/.ssh/authorized_keys` and terminate its active
connections; deleting a key does not disconnect an existing SSH session.
Old configurations using root keys do not silently keep root access: clients
now connect only as `proxmenux-usbip`. Authorize a separate key on the server and
replace the saved client connection if it previously referenced a root key.
## Linux VM/client
Run the client **inside the Linux VM that needs the device**. Connecting it to
a Proxmox node alone does not make that device migrate with a VM. A supported
Linux kernel must provide `vhci-hcd`; the server needs `usbip-host`. Proxmox
kernel packages normally provide these modules, but activation fails visibly
if they are missing. No out-of-tree kernel modules are installed.
The client can use the menu when ProxMenux is installed, or the standalone
standard-library Python manager. On a Debian Linux guest, install the packages
and copy `scripts/devices/usbip_manager.py` from the reviewed ProxMenux checkout:
```sh
apt-get install usbip openssh-client kmod
install -D -m 0755 usbip_manager.py /usr/local/share/proxmenux/scripts/devices/usbip_manager.py
```
Generate a dedicated SSH client key (for example `/root/.ssh/id_usbip`) and
authorize its public key through the server menu. Verify the server host-key
fingerprint through a trusted channel and install it in the client's root
`known_hosts` file. Do not attempt an interactive login: the account intentionally
rejects a shell. SSH config aliases and `ProxyJump` settings are not used; enter
the actual address and port.
Then choose **Connect a remote USB device** and provide the server address,
SSH port and absolute path to an existing private key (0400 or 0600). The menu
authenticates and lists the server's configured exports before saving a client.
Choose an unused local port for each device (33240 by default).
Equivalent standalone commands:
```sh
MANAGER=/usr/local/share/proxmenux/scripts/devices/usbip_manager.py
python3 "$MANAGER" remote --host 192.0.2.10 --ssh-port 22 --key /root/.ssh/id_usbip
# Substitute the export ID returned by the server:
python3 "$MANAGER" add-import --host 192.0.2.10 --ssh-port 22 --key /root/.ssh/id_usbip --export-id abcdef123456 --local-port 33240
python3 "$MANAGER" status
```
The client has an enabled systemd service per device. It reconnects the SSH
tunnel and reattaches the device after a network interruption or restart. On
each new attach it resolves the **saved export ID** to the server's current bus
ID; physical port movement is resolved only after confirmed cleanup of the old
slot. The client keeps an ownership
record of the exact imported kernel port, local bus ID and remote endpoint;
cleanup detaches only a matching owned import. Removing a connection disables
its service, releases its device and removes its saved configuration.
Only **one client at a time** can attach a physical USB device. A device already
in use by another client may be listed as exported, but the kernel rejects a
second attach; the client reports the error and waits. Applications must handle
USB disconnect/reconnect themselves. A brief network interruption can still
reset the USB device and interrupt a Zigbee coordinator, printer or Bluetooth
session; this is not transparent USB high availability.
Unprivileged LXC, Home Assistant OS and non-Linux clients require their own
kernel/device permissions or add-on integration. This feature does not change
LXC privileges, edit VM passthrough settings or deploy HAOS add-ons. A standard
addon that expects raw USB/IP TCP also needs the SSH transport configured; it
cannot directly connect to this server's isolated daemon.
## Files and checks
- Root-only configuration: `/etc/proxmenux/usbip/config.json` (0600).
- Owned server unit: `proxmenux-usbip-server.service`.
- Owned client units: `proxmenux-usbip-client-<id>.service`.
- Server ownership/status: `/run/proxmenux-usbip-server/`.
- Client ownership/status: `/run/proxmenux-usbip/`, independent of server cleanup.
- Menu actions and package installations use the ProxMenux journal when present.
Validation includes fake sysfs/USB commands for identity and ownership, service
rollback and quoting, malformed catalogs, credential failures and reconnects.
The Linux CI also verifies generated units and exercises actual namespace
isolation and restricted SSH sessions with native USB/IP control messages.
Protocol socket tests cover binary transfers, malformed requests, selection
changes, foreign exports and replaced device objects. Lifetime tests require a
verified pin and a matching completed kernel removal event before slot recovery.
The kernel regression lab is deliberately opt-in and requires a fresh, disposable
QEMU Linux VM with usbip, socat and the dummy_hcd/libcomposite/usbip-host/vhci-hcd
modules. Never run it on a production host. In that VM, from the checkout:
```sh
sudo unshare --net sh -c 'ip link set lo up; python3 -B .github/scripts/check_usbip_kernel.py --disposable-vm'
```
It creates two virtual ACM gadgets, performs 20 physical unplug/replug cycles,
rejects an unselected same-port replacement, reimports the original and confirms
local-driver restoration. An independent native USB/IP serial transfer must keep
working during the cycles. The lab passed on Debian 13 / Linux 6.12.115; actual
hardware and guest-driver compatibility still depends on the deployment.
The companion service lab must run in a separate fresh disposable QEMU VM:
```sh
sudo python3 -B .github/scripts/check_usbip_services.py --disposable-vm
```
It installs the actual generated server/client units and a dedicated SSH key
inside that VM, transfers native serial data in both directions through restricted
SSH, checks three automatic reconnects with an independent connection alive, and
verifies synchronous stop-sharing and disable cleanup. Destroy the VM afterwards.
The lifetime contract is based on [usbfs open/release references](https://github.com/torvalds/linux/blob/v6.12/drivers/usb/core/devio.c),
[USB/IP match/rebind](https://github.com/torvalds/linux/blob/v6.12/drivers/usb/usbip/stub_main.c)
and [completed stub disconnect](https://github.com/torvalds/linux/blob/v6.12/drivers/usb/usbip/stub_dev.c).
A sysfs pathname alone is never sufficient authority for absent-slot rebind.
Protocol and transport references:
[Linux USB/IP tools](https://github.com/torvalds/linux/tree/master/tools/usb/usbip),
[Debian usbip manual](https://manpages.debian.org/trixie/usbip/usbip.8.en.html),
[OpenSSH authorized_keys restrictions](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT),
[systemd PrivateNetwork](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#PrivateNetwork=).
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Abgebrochen",
"About to restore": "Wird gerade wiederhergestellt",
"Absolute directory path to use as backup target:": "Absoluter Verzeichnispfad zur Verwendung als Sicherungsziel:",
"Absolute path of the client SSH private key:": "Absoluter Pfad des privaten SSH-Schlüssels des Clients:",
"Absolute path to a file or directory you want backed up:": "Absoluter Pfad zu einer Datei oder einem Verzeichnis, die/das Sie sichern möchten:",
"Acceleration": "Beschleunigung",
"Acceleration configuration cancelled": "Beschleunigungskonfiguration abgesagt",
@@ -619,6 +620,8 @@
"Authentication:": "Authentifizierung:",
"Authorization failed": "Die Autorisierung ist fehlgeschlagen",
"Authorization successful": "Autorisierung erfolgreich",
"Authorize a client SSH public key": "Öffentlichen SSH-Schlüssel eines Clients autorisieren",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Autorisiere zuerst einen eigenen öffentlichen Clientschlüssel auf dem Server. Das eingeschränkte USB/IP-Konto kann keine Shell starten oder andere Sockets weiterleiten. Prüfe vor dem Verbinden den Hostschlüssel des Servers; unbekannte Schlüssel werden abgelehnt.",
"Authorize this key on the server": "Autorisieren Sie diesen Schlüssel auf dem Server",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autorisieren über „pct exec“ (Borg-Server ist ein LXC auf einem PVE-Host, in den ich SSH einbinden kann)",
"Authorized": "Autorisiert",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Widersprüchliche Dienstprogramme entfernt",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Verbinde Zigbee-Geräte mit MQTT über eine webbasierte Einrichtungsoberfläche.",
"Connect a Coral Accelerator and try again.": "Schließen Sie einen Coral Accelerator an und versuchen Sie es erneut.",
"Connect a remote USB device": "Entferntes USB-Gerät verbinden",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Verbinden Sie es oder entfernen Sie es aus dem Container in Proxmox (Ressourcen) und führen Sie die Wiederherstellung erneut aus.",
"Connect your devices and users together in your own secure virtual private network.": "Verbinden Sie Ihre Geräte und Benutzer in Ihrem eigenen sicheren virtuellen privaten Netzwerk.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Verbinden Sie Ihre Geräte mit einem sicheren WireGuard®-basierten Overlay-Netzwerk mit SSO, MFA und granularen Zugriffskontrollen.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Directus-Administrator",
"Disable NIC Offloading (Intel e1000e)": "NIC-Offloading deaktivieren (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Deaktivieren Sie Secure Boot in der UEFI-Firmware und führen Sie das Installationsprogramm erneut aus. Es wurde nichts geändert.",
"Disable USB/IP server": "USB/IP-Server deaktivieren",
"Disable autostart on conflicting VM": "Deaktivieren Sie den Autostart auf in Konflikt stehenden VMs",
"Disable autostart on one VM to avoid startup conflicts.": "Deaktivieren Sie den Autostart auf einer VM, um Startkonflikte zu vermeiden.",
"Disable job": "Job deaktivieren",
"Disable kernel audit messages (optional but recommended):": "Deaktivieren Sie Kernel-Überwachungsmeldungen (optional, aber empfohlen):",
"Disable onboot on affected VM(s)/CT(s)": "Onboot auf betroffenen VM(s)/CT(s) deaktivieren",
"Disable portmapper/rpcbind": "Portmapper/rpcbind deaktivieren",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Den Server deaktivieren und alle von ProxMenux verwalteten USB-Geräte freigeben? Die gespeicherte Auswahl bleibt für die nächste Aktivierung erhalten.",
"Disabled": "Deaktiviert",
"Disabling High Availability services...": "Hochverfügbarkeitsdienste werden deaktiviert...",
"Disabling ZFS autotrim applied by ProxMenux...": "Deaktivieren der von ProxMenux angewendeten ZFS-Autotrim-Funktion ...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Aktivieren Sie Remote Desktop (RDP), bevor Sie die virtuelle Anzeige deaktivieren.",
"Enable SSD emulation for this disk?": "SSD-Emulation für diese Festplatte aktivieren?",
"Enable TCP BBR/Fast Open control": "Aktivieren Sie die TCP-BBR/Fast-Open-Steuerung",
"Enable USB/IP server": "USB/IP-Server aktivieren",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Aktivieren von VA-API Transcoding und HDR10/Dolby Vision to SDR Tone Mapping",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Aktivieren Sie VA-API, Hardware-Codierung und OpenCL-Tone-Mapping in Jellyfin",
"Enable VFIO IOMMU support": "VFIO IOMMU-Unterstützung aktivieren",
@@ -3213,6 +3220,7 @@
"If pvesm path returned a DEVICE (LVM):": "Wenn pvesm path ein GERÄT (LVM) zurückgegeben hat:",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Wenn pvesm path ein VERZEICHNIS (ZFS/BTRFS) zurückgegeben hat:",
"If repositories error:": "Bei Repository-Fehler:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Wenn der USB/IP-Dienst unerwartet stoppt, beenden Sie die Freigabe, solange das Gerät noch angeschlossen ist. In seltenen Fällen ist es erst nach einem Neustart des Hosts wieder verfügbar.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Wenn der Container bereits geändert wurde, wird sein vorheriges natives Backup wiederhergestellt. Freigegebene Hostverzeichnisse werden nicht zurückgesetzt. Die Festplatten des fehlgeschlagenen Versuchs werden entfernt, wenn die Bereinigung erfolgreich ist.",
"If the host freezes, remove hostpci entries from": "Wenn der Host einfriert, entfernen Sie die Hostpci-Einträge aus",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Wenn der Image-Kanal eine neue Version hat, wird CT gestoppt, gesichert und überprüft und durch das neue Image ersetzt, um den Container zu aktualisieren. Wenn etwas fehlschlägt, wird das Backup wiederhergestellt.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Installieren Sie Serverpakete in CT:",
"Install terminal multiplexers": "Installieren Sie Terminal-Multiplexer",
"Install the Edge TPU runtime (libedgetpu1-std)": "Installieren Sie die Edge TPU-Laufzeit (libedgetpu1-std).",
"Install the USB/IP dependencies from the configured Debian repositories?": "USB/IP-Abhängigkeiten aus den konfigurierten Debian-Repositories installieren?",
"Install this image?": "Installieren Sie dieses Bild?",
"Install with Cloud-Init script": "Installation mit Cloud-Init-Skript",
"Install with ISO from UUP Dump": "Mit ISO von UUP Dump installieren",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Einzufügende Zeile (einzelne Zeile, einschließlich „command=…“-Präfix):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Linux-Installationsoptionen",
"Linux guest setup and migration notes": "Hinweise zur Einrichtung und Migration von Linux-Gästen",
"Linux/Mac path:": "Linux/Mac-Pfad:",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin mit optionalem GPU-Passthrough",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB requires ein Benutzer, Datenbank und Passwort",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Name oder Teil der Option oder der Anwendung",
"Name:": "Name:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Benannte Konten benötigen einen privaten Modus. Stoppen Sie den Container, setzen Sie public: false in /config/config.js, starten Sie ihn erneut und erstellen Sie jeden Benutzer mit: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Natives Borg ist nicht verfügbar. Installieren Sie das native Paket borgbackup auf diesem Host, um Borg-Backups zu verwenden.",
"Needed:": "Erforderlich:",
"Needs": "Benötigt",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Weder /etc/kernel/cmdline noch /etc/default/grub gefunden.",
@@ -4456,6 +4469,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.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Keine doppelten IP-Adressen",
"No duplicate repositories found": "Keine doppelten Repositorys gefunden",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Nach der SR-IOV-Filterung bleiben keine berechtigten Controller/NVMe-Geräte übrig. Überspringen.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Keine geeigneten USB-Geräte gefunden. Hubs, Massenspeicher und Netzwerkadapter sind ausgeschlossen.",
"No eligible controllers remain after SR-IOV filtering.": "Nach der SR-IOV-Filterung bleiben keine berechtigten Controller übrig.",
"No encryption key is stored on this host. Choose how to set one up:": "Auf diesem Host ist kein Verschlüsselungsschlüssel gespeichert. Wählen Sie aus, wie Sie eines einrichten möchten:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Es wurden keine exportierbaren VM-Festplatten gefunden (CD-ROM/Cloud-Init sind ausgeschlossen).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Kein Selbsttestprotokoll verfügbar für",
"No server IP or hostname provided.": "Keine Server-IP oder Hostname angegeben.",
"No share access test succeeded with guest authentication.": "Bei der Gastauthentifizierung war kein Freigabezugriffstest erfolgreich.",
"No shared devices are available on this server.": "Auf diesem Server sind keine freigegebenen Geräte verfügbar.",
"No shared media content.": "Keine geteilten Medieninhalte.",
"No shared mount detected. Applying standard local access.": "Kein gemeinsamer Mount erkannt. Anwenden des standardmäßigen lokalen Zugriffs.",
"No shares configured.": "Keine Freigaben konfiguriert.",
@@ -5129,6 +5145,7 @@
"Path of the Compose file": "Pfad der Compose-Datei",
"Path of the oci-compose file": "Pfad der oci-compose-Datei",
"Path removed:": "Pfad entfernt:",
"Path to the client SSH public key on this server:": "Pfad zum öffentlichen SSH-Schlüssel des Clients auf diesem Server:",
"Path:": "Weg:",
"Paths applied:": "Angewandte Pfade:",
"Paths included in backup": "Im Backup enthaltene Pfade",
@@ -5675,6 +5692,7 @@
"Remove Proxmox NFS storage:": "Entfernen Sie den Proxmox NFS-Speicher:",
"Remove Proxmox iSCSI storage:": "Entfernen Sie den Proxmox iSCSI-Speicher:",
"Remove Secure Gateway? State will be preserved.": "Secure Gateway entfernen? Der Zustand bleibt erhalten.",
"Remove a remote USB connection": "Entfernte USB-Verbindung entfernen",
"Remove custom paths": "Benutzerdefinierte Pfade entfernen",
"Remove disk references from affected VM(s)/CT(s) config": "Entfernen Sie Festplattenverweise aus der Konfiguration der betroffenen VM(s)/CT(s).",
"Remove every registered gasket DKMS version": "Alle registrierten gasket-DKMS-Versionen entfernen",
@@ -5695,6 +5713,7 @@
"Remove the lock left on a container by an interrupted task": "Die Sperre entfernen, die eine unterbrochene Aufgabe auf einem Container hinterlassen hat",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Die gespeicherten Anmeldeinformationen für {registry} entfernen?\nLaufende Apps funktionieren weiterhin.",
"Remove the unprivileged flag from configuration:": "Entfernen Sie das Unprivileged-Flag aus der Konfiguration:",
"Remove this connection and detach its USB device?": "Diese Verbindung entfernen und das USB-Gerät trennen?",
"Remove unused packages and their config": "Entfernen Sie nicht verwendete Pakete und deren Konfiguration",
"Remove: delete the application and its containers": "Entfernen: Löschen Sie die Anwendung und ihre Container",
"Removed": "ENTFERNT",
@@ -5917,9 +5936,11 @@
"Run mode: Unattended": "Betriebsmodus: Unbeaufsichtigt",
"Run now": "Jetzt ausführen",
"Run security audit now": "Führen Sie jetzt ein Sicherheitsaudit durch",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Führen Sie den Client in der Linux-VM aus, die das USB-Gerät benötigt. Installieren Sie usbip, openssh-client und kmod und kopieren Sie usbip_manager.py an denselben Pfad wie auf dem Server. Richten Sie die Schlüsselauthentifizierung ein und prüfen Sie den Hostschlüssel des Servers, bevor Sie eine Verbindung hinzufügen. Der Client verwendet den Gastkernel und kann sich nach einer Migration erneut verbinden, während das physische USB-Gerät am Server bleibt. Unprivilegierte LXC und Home Assistant OS benötigen eine eigene Kernel-/Geräte- oder Add-on-Einrichtung. Ein physisches Gerät kann jeweils nur mit einem Client verbunden sein.",
"Run the following inside the VM:": "Führen Sie Folgendes in der VM aus:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Führen Sie den Release-Channel-Schalter von einer SSH-Sitzung oder der Proxmox-Hostkonsole aus mit:",
"Run the update from an SSH session or the Proxmox host console with:": "Führen Sie das Update über eine SSH-Sitzung oder die Proxmox-Hostkonsole aus mit:",
"Run this menu as root.": "Führen Sie dieses Menü als root aus.",
"Run this operation as root.": "Führen Sie diesen Vorgang als root aus.",
"Run upgrade checklist script:": "Führen Sie das Upgrade-Checklisten-Skript aus:",
"Running": "Läuft",
@@ -6144,6 +6165,7 @@
"Select a new language for the menu:": "Wählen Sie eine neue Sprache für das Menü:",
"Select a post-installation script:": "Wählen Sie ein Nachinstallationsskript aus:",
"Select a pre-configured Linux VM script to execute:": "Wählen Sie ein vorkonfiguriertes Linux-VM-Skript zur Ausführung aus:",
"Select a saved USB/IP device:": "Gespeichertes USB/IP-Gerät auswählen:",
"Select a script or action:": "Wählen Sie ein Skript oder eine Aktion aus:",
"Select a share to delete:": "Wählen Sie eine Freigabe zum Löschen aus:",
"Select a specific Coral or USB node, not the whole /dev": "Wählen Sie einen bestimmten Coral- oder USB-Knoten, nicht den gesamten /dev",
@@ -6221,6 +6243,7 @@
"Select the Linux distribution to install:": "Wählen Sie die zu installierende Linux-Distribution aus:",
"Select the NAS system to install:": "Wählen Sie das zu installierende NAS-System aus:",
"Select the NVIDIA driver version to install:": "Wählen Sie die zu installierende NVIDIA-Treiberversion aus:",
"Select the USB device to share:": "Freizugebendes USB-Gerät auswählen:",
"Select the VM to add the GPU to:": "Wählen Sie die VM aus, zu der Sie die GPU hinzufügen möchten:",
"Select the VM to which you want to add disks:": "Wählen Sie die VM aus, zu der Sie Festplatten hinzufügen möchten:",
"Select the VM where you want to import the disk image:": "Wählen Sie die VM aus, in die Sie das Disk-Image importieren möchten:",
@@ -6245,6 +6268,7 @@
"Select the parent job to attach to:": "Wählen Sie den übergeordneten Job aus, an den Sie anhängen möchten:",
"Select the paths to restore (one per backup entry):": "Wählen Sie die wiederherzustellenden Pfade aus (einen pro Sicherungseintrag):",
"Select the privileged LXC container to convert:": "Wählen Sie den zu konvertierenden privilegierten LXC-Container aus:",
"Select the remote USB device:": "Entferntes USB-Gerät auswählen:",
"Select the storage that will receive the backups:": "Wählen Sie den Speicher aus, der die Backups erhalten soll:",
"Select the storage volume for disk import:": "Wählen Sie das Speichervolume für den Festplattenimport aus:",
"Select the sync interval (in hours):": "Wählen Sie das Synchronisierungsintervall (in Stunden):",
@@ -6331,6 +6355,7 @@
"Server IDs to test against, separated by commas (optional)": "Server-IDs zum Testen, durch Kommas getrennt (optional)",
"Server IP/hostname is correct": "Server-IP/Hostname ist korrekt",
"Server IP:": "Server-IP:",
"Server SSH port:": "SSH-Port des Servers:",
"Server is accessible:": "Server ist erreichbar:",
"Server is online": "Server ist online",
"Server is running": "Server läuft",
@@ -6392,6 +6417,7 @@
"Share Access Error": "Fehler beim Freigabezugriff",
"Share Name": "Teilen Sie den Namen",
"Share Options": "Freigabeoptionen",
"Share a local USB device": "Lokales USB-Gerät freigeben",
"Share added successfully.": "Teilen erfolgreich hinzugefügt.",
"Share deleted and Samba service restarted.": "Freigabe gelöscht und Samba-Dienst neu gestartet.",
"Share exists:": "Freigabe existiert:",
@@ -6399,6 +6425,7 @@
"Share name:": "Freigabename:",
"Share not found on server:": "Freigabe nicht auf Server gefunden:",
"Share path:": "Freigabepfad:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Geben Sie ausgewählte USB-Geräte über SSH frei oder verbinden Sie diesen Linux-Rechner mit einem USB/IP-Server.",
"Share updated successfully.": "Freigabe erfolgreich aktualisiert.",
"Share:": "Aktie:",
"ShareCoveX panel": "ShareCoveX-Panel",
@@ -6667,6 +6694,8 @@
"Still has issues": "Es gibt immer noch Probleme",
"Stop - shuts the guest down during backup": "Stopp – fährt den Gast während der Sicherung herunter",
"Stop it first and run this option again.": "Stoppen Sie es zuerst und führen Sie diese Option erneut aus.",
"Stop sharing a USB device": "USB-Gerätefreigabe beenden",
"Stop sharing this device? An attached remote client will disconnect.": "Freigabe dieses Geräts beenden? Ein verbundener entfernter Client wird getrennt.",
"Stop that VM, then choose:": "Stoppen Sie VM und wählen Sie dann:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Stoppen Sie den CT, unmounten Sie die Festplatte auf dem HOST und mounten Sie sie erneut mit:",
"Stop the VM/CT before formatting this disk.": "Stoppen Sie die VM/CT, bevor Sie diese Festplatte formatieren.",
@@ -7022,6 +7051,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",
@@ -7303,6 +7333,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.",
@@ -7370,6 +7401,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "Die ausgewählte Intel-GPU verfügt über Nicht-FLR-Reset-Unterstützung und einen unbekannten Subtyp",
"The selected Intel GPU is currently in power state D3cold": "Die ausgewählte Intel-GPU befindet sich derzeit im Energiezustand D3cold",
"The selected Intel render device does not exist:": "Das ausgewählte Intel-Rendergerät existiert nicht:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "Das ausgewählte USB-Gerät steht lokalen Anwendungen und Gästen nicht mehr zur Verfügung. Nur ein entfernter Client kann es gleichzeitig verwenden. Dieses Gerät freigeben?",
"The selected VM": "Die ausgewählte VM",
"The selected VM is running.": "Die ausgewählte VM läuft.",
"The selected base folder does not exist and could not be created:": "Der ausgewählte Basisordner existiert nicht und konnte nicht erstellt werden:",
@@ -7709,6 +7741,9 @@
"USB drives mounted now:": "Jetzt gemountete USB-Laufwerke:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "USB- oder serieller Geräteknoten",
"USB/IP configuration updated. Open status to check the connection.": "USB/IP-Konfiguration aktualisiert. Öffnen Sie den Status, um die Verbindung zu prüfen.",
"USB/IP over SSH": "USB/IP über SSH",
"USB/IP server IP address or DNS hostname:": "IP-Adresse oder DNS-Hostname des USB/IP-Servers:",
"UUP Dump script not found.": "UUP-Dump-Skript nicht gefunden.",
"UUp Dump ISO creator Custom": "UUp Dump ISO-Ersteller Benutzerdefiniert",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity ist ein kostenloser, leichter und benutzerfreundlicher Heimserver für Ihre Comics und E-Books. Verwenden Sie es, um von überall auf Ihre Dateien zuzugreifen, mit einem Tablet, einem E-Reader, einem Telefon oder einem Computer.",
@@ -7865,6 +7900,7 @@
"Untrusted or modified NVIDIA hook": "Nicht vertrauenswürdiger oder modifizierter NVIDIA-Hook",
"Unused image removed from the cache:": "Nicht verwendetes Bild aus dem Cache entfernt:",
"Unused images removed from the cache:": "Nicht verwendete Bilder aus dem Cache entfernt:",
"Unused local loopback TCP port for this device:": "Freier lokaler Loopback-TCP-Port für dieses Gerät:",
"Update": "Aktualisierung",
"Update Available": "Update verfügbar",
"Update Ceph repository (Only if using Ceph):": "Ceph-Repository aktualisieren (nur bei Verwendung von Ceph):",
@@ -8148,6 +8184,7 @@
"View Disk Storages": "Festplattenspeicher anzeigen",
"View Mount Points": "Mount-Punkte anzeigen",
"View NFS Mounts (pvesm + fstab)": "NFS-Mounts anzeigen (pvesm + fstab)",
"View USB/IP status": "USB/IP-Status anzeigen",
"View backup metadata": "Sicherungsmetadaten anzeigen",
"View current paths": "Aktuelle Pfade anzeigen",
"View existing folders in /mnt": "Vorhandene Ordner in /mnt anzeigen",
+37
View File
@@ -245,6 +245,7 @@
"Aborted": "Abortado",
"About to restore": "A punto de restaurar",
"Absolute directory path to use as backup target:": "Ruta absoluta del directorio para usar como destino del backup:",
"Absolute path of the client SSH private key:": "Ruta absoluta de la clave privada SSH del cliente:",
"Absolute path to a file or directory you want backed up:": "Ruta absoluta de un archivo o directorio del que desea hacer backup:",
"Acceleration": "Aceleración",
"Acceleration configuration cancelled": "Configuración de aceleración cancelada",
@@ -617,6 +618,8 @@
"Authentication:": "Autenticación:",
"Authorization failed": "Autorización fallida",
"Authorization successful": "Autorización correcta",
"Authorize a client SSH public key": "Autorizar una clave pública SSH de un cliente",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Autoriza primero una clave pública dedicada del cliente en el servidor. La cuenta restringida de USB/IP no puede abrir una shell ni redirigir otros sockets. Verifica la clave del servidor antes de conectarte; las claves desconocidas se rechazan.",
"Authorize this key on the server": "Autorizar esta clave en el servidor",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autorizar a través de 'pct exec' (el servidor Borg es un LXC en un host PVE en el que puedo acceder mediante SSH)",
"Authorized": "Autorizado",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Se eliminaron las utilidades conflictivas",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Conecta dispositivos Zigbee a MQTT mediante una interfaz web de configuración.",
"Connect a Coral Accelerator and try again.": "Conecta un Coral Accelerator y vuelve a intentarlo.",
"Connect a remote USB device": "Conectar un dispositivo USB remoto",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Conéctalo, o elimínalo del contenedor en Proxmox (Recursos), y vuelve a lanzar la recuperación.",
"Connect your devices and users together in your own secure virtual private network.": "Conecta tus dispositivos y usuarios en tu propia red privada virtual segura.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Conecta tus dispositivos en una red overlay segura basada en WireGuard®, con SSO, MFA y controles de acceso granulares.",
@@ -1960,12 +1964,14 @@
"Directus administrator": "Administrador de Directus",
"Disable NIC Offloading (Intel e1000e)": "Desactivar offloading de NIC (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Desactiva Secure Boot en el firmware UEFI y vuelve a ejecutar el instalador. No se ha cambiado nada.",
"Disable USB/IP server": "Desactivar el servidor USB/IP",
"Disable autostart on conflicting VM": "Deshabilitar el inicio automático en VM en conflicto",
"Disable autostart on one VM to avoid startup conflicts.": "Desactiva el inicio automático en una VM para evitar conflictos de inicio.",
"Disable job": "Desactivar trabajo",
"Disable kernel audit messages (optional but recommended):": "Desactivar los mensajes de auditoría del kernel (opcional pero recomendado):",
"Disable onboot on affected VM(s)/CT(s)": "Deshabilitar el arranque en las VM/CT afectadas",
"Disable portmapper/rpcbind": "Desactivar portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "¿Desactivar el servidor y liberar todos los dispositivos USB gestionados por ProxMenux? La selección guardada se conserva para la próxima activación.",
"Disabled": "Desactivado",
"Disabling High Availability services...": "Deshabilitando los servicios de alta disponibilidad...",
"Disabling ZFS autotrim applied by ProxMenux...": "Deshabilitando el recorte automático ZFS aplicado por ProxMenux...",
@@ -2198,6 +2204,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Activa el Escritorio remoto (RDP) antes de desactivar la pantalla virtual.",
"Enable SSD emulation for this disk?": "¿Habilitar la emulación SSD para este disco?",
"Enable TCP BBR/Fast Open control": "Activar control TCP BBR/Fast Open",
"Enable USB/IP server": "Activar el servidor USB/IP",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Activar la transcodificación VA-API y el mapeo de tonos de HDR10/Dolby Vision a SDR",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Activar VA-API, la codificación por hardware y el mapeo de tonos OpenCL en Jellyfin",
"Enable VFIO IOMMU support": "Activar soporte VFIO IOMMU",
@@ -3209,6 +3216,7 @@
"If pvesm path returned a DEVICE (LVM):": "Si pvesm path devolvió un DISPOSITIVO (LVM):",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Si pvesm path devolvió un DIRECTORIO (ZFS/BTRFS):",
"If repositories error:": "Si hay error en los repositorios:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Si el servicio USB/IP se detiene de forma inesperada, deja de compartir el dispositivo mientras sigue conectado para liberarlo. En casos excepcionales solo se libera reiniciando el host.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Si el contenedor ya se había modificado, se restaura su backup nativo anterior. Los directorios compartidos del host no se revierten. Los discos del intento fallido se eliminan si la limpieza termina bien.",
"If the host freezes, remove hostpci entries from": "Si el host se congela, elimina las entradas hostpci de",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If the update fails after the container was changed, the backup is restored. An update that stops halfway is recovered from the OCI management menu.": "Si el canal de imagen tiene una versión nueva, el CT se detiene, se hace un backup, se verifica y se sustituye por la nueva imagen, actualizando el contenedor. Si la actualización falla después de modificar el contenedor, se restaura el backup. Una actualización que se queda a medias se recupera desde el menú de gestión OCI.",
@@ -3403,6 +3411,7 @@
"Install server packages inside CT:": "Instalar paquetes de servidor dentro de CT:",
"Install terminal multiplexers": "Instalar multiplexores de terminales",
"Install the Edge TPU runtime (libedgetpu1-std)": "Instalar el tiempo de ejecución de Edge TPU (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "¿Instalar las dependencias de USB/IP desde los repositorios de Debian configurados?",
"Install this image?": "¿Instalar esta imagen?",
"Install with Cloud-Init script": "Instalar con el script Cloud-Init",
"Install with ISO from UUP Dump": "Instalar con ISO desde UUP Dump",
@@ -3593,6 +3602,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",
@@ -3886,6 +3896,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Línea para pegar (una sola línea, incluido el prefijo \"comando=...\"):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Opciones de instalación de Linux",
"Linux guest setup and migration notes": "Configuración del invitado Linux y notas sobre migración",
"Linux/Mac path:": "Ruta Linux/Mac:",
"LinuxServer Jellyfin with optional GPU passthrough": "Jellyfin de LinuxServer con passthrough de GPU opcional",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB requiere un usuario, base de datos y contraseña",
@@ -4310,6 +4321,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",
@@ -4393,6 +4405,7 @@
"Name or part of the option or of the application": "Nombre o parte de la opción o de la aplicación",
"Name:": "Nombre:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Las cuentas con nombre necesitan el modo privado. Detén el contenedor, pon public: false en /config/config.js, vuelve a arrancarlo y crea cada usuario con: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Borg nativo no está disponible. Instala el paquete borgbackup nativo en este host para usar los backups de Borg.",
"Needed:": "Necesario:",
"Needs": "Necesita",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "No se encontraron /etc/kernel/cmdline ni /etc/default/grub.",
@@ -4451,6 +4464,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.",
@@ -4641,6 +4655,7 @@
"No duplicate IP addresses": "Sin direcciones IP duplicadas",
"No duplicate repositories found": "No se encontraron repositorios duplicados",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "No quedan dispositivos de controlador/NVMe elegibles después del filtrado SR-IOV. Salto a la comba.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "No hay dispositivos USB compatibles. Se excluyen los concentradores, el almacenamiento masivo y los adaptadores de red.",
"No eligible controllers remain after SR-IOV filtering.": "No quedan controladores elegibles después del filtrado SR-IOV.",
"No encryption key is stored on this host. Choose how to set one up:": "No se almacena ninguna clave de cifrado en este host. Elige cómo configurar una:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "No se encontraron discos de VM exportables (se excluyen CD-ROM/cloud-init).",
@@ -4725,6 +4740,7 @@
"No self-test log available for": "No hay ningún log de autoprueba disponible para",
"No server IP or hostname provided.": "No se proporcionó ninguna IP de servidor ni nombre de host.",
"No share access test succeeded with guest authentication.": "Ninguna prueba de acceso a recursos compartidos funcionó con autenticación de invitado.",
"No shared devices are available on this server.": "No hay dispositivos compartidos disponibles en este servidor.",
"No shared media content.": "No hay contenido multimedia compartido.",
"No shared mount detected. Applying standard local access.": "No se detectó ningún montaje compartido. Aplicando acceso local estándar.",
"No shares configured.": "No hay recursos compartidos configurados.",
@@ -5120,6 +5136,7 @@
"Path of the Compose file": "Ruta del archivo Compose",
"Path of the oci-compose file": "Ruta del archivo oci-compose",
"Path removed:": "Ruta eliminada:",
"Path to the client SSH public key on this server:": "Ruta de la clave pública SSH del cliente en este servidor:",
"Path:": "Ruta:",
"Paths applied:": "Rutas aplicadas:",
"Paths included in backup": "Rutas incluidas en el backup",
@@ -5658,6 +5675,7 @@
"Remove Proxmox NFS storage:": "Eliminar el almacenamiento Proxmox NFS:",
"Remove Proxmox iSCSI storage:": "Eliminar el almacenamiento iSCSI de Proxmox:",
"Remove Secure Gateway? State will be preserved.": "¿Eliminar Secure Gateway? Se conservará el estado.",
"Remove a remote USB connection": "Eliminar una conexión USB remota",
"Remove custom paths": "Eliminar rutas personalizadas",
"Remove disk references from affected VM(s)/CT(s) config": "Eliminar referencias de disco de la configuración de VM/CT(s) afectados",
"Remove every registered gasket DKMS version": "Eliminar todas las versiones de gasket registradas en DKMS",
@@ -5678,6 +5696,7 @@
"Remove the lock left on a container by an interrupted task": "Quitar el bloqueo que deja en un contenedor una tarea interrumpida",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "¿Eliminar las credenciales guardadas de {registry}?\nLas aplicaciones en marcha seguirán funcionando.",
"Remove the unprivileged flag from configuration:": "Elimina el flag unprivileged de la configuración:",
"Remove this connection and detach its USB device?": "¿Eliminar esta conexión y desconectar su dispositivo USB?",
"Remove unused packages and their config": "Eliminar paquetes no utilizados y su configuración",
"Remove: delete the application and its containers": "Eliminar: la aplicación y sus contenedores",
"Removed": "Remoto",
@@ -5900,9 +5919,11 @@
"Run mode: Unattended": "Modo de ejecución: desatendido",
"Run now": "Ejecutar ahora",
"Run security audit now": "Ejecutar la auditoría de seguridad ahora",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Ejecuta el cliente dentro de la VM Linux que necesita el USB. Instala usbip, openssh-client y kmod y copia usbip_manager.py en la misma ruta que en el servidor. Configura la autenticación por clave y verifica la clave del servidor antes de añadir la conexión. El cliente usa el kernel del invitado y puede reconectarse después de migrarlo mientras el USB físico sigue en el servidor. Los LXC sin privilegios y Home Assistant OS necesitan su propia configuración de kernel, dispositivos o complementos. Solo un cliente puede usar cada dispositivo físico a la vez.",
"Run the following inside the VM:": "Ejecuta lo siguiente dentro de la VM:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Ejecuta el cambio de canal de versiones desde una sesión SSH o la consola del host Proxmox con:",
"Run the update from an SSH session or the Proxmox host console with:": "Ejecuta la actualización desde una sesión SSH o la consola del host Proxmox con:",
"Run this menu as root.": "Ejecuta este menú como root.",
"Run this operation as root.": "Ejecuta esta operación como root.",
"Run upgrade checklist script:": "Ejecuta el script de la lista de verificación de actualización:",
"Running": "En ejecución",
@@ -6127,6 +6148,7 @@
"Select a new language for the menu:": "Selecciona un nuevo idioma para el menú:",
"Select a post-installation script:": "Selecciona un script de post-instalación:",
"Select a pre-configured Linux VM script to execute:": "Selecciona un script de VM Linux preconfigurado para ejecutar:",
"Select a saved USB/IP device:": "Selecciona un dispositivo USB/IP guardado:",
"Select a script or action:": "Selecciona un script o acción:",
"Select a share to delete:": "Selecciona un recurso compartido para eliminar:",
"Select a specific Coral or USB node, not the whole /dev": "Selecciona un Coral específico o nodo USB, no todo /dev",
@@ -6204,6 +6226,7 @@
"Select the Linux distribution to install:": "Selecciona la distribución de Linux para instalar:",
"Select the NAS system to install:": "Selecciona el sistema NAS a instalar:",
"Select the NVIDIA driver version to install:": "Selecciona la versión del controlador NVIDIA para instalar:",
"Select the USB device to share:": "Selecciona el dispositivo USB que quieres compartir:",
"Select the VM to add the GPU to:": "Selecciona la VM a la que añadir la GPU:",
"Select the VM to which you want to add disks:": "Selecciona la VM a la que deseas añadir discos:",
"Select the VM where you want to import the disk image:": "Selecciona la VM donde deseas importar la imagen del disco:",
@@ -6228,6 +6251,7 @@
"Select the parent job to attach to:": "Selecciona el trabajo principal al que adjuntarlo:",
"Select the paths to restore (one per backup entry):": "Selecciona las rutas para restaurar (una por entrada de backup):",
"Select the privileged LXC container to convert:": "Selecciona el contenedor LXC privilegiado para convertir:",
"Select the remote USB device:": "Selecciona el dispositivo USB remoto:",
"Select the storage that will receive the backups:": "Selecciona el almacenamiento que recibirá los backups:",
"Select the storage volume for disk import:": "Selecciona el volumen de almacenamiento para la importación de disco:",
"Select the sync interval (in hours):": "Selecciona el intervalo de sincronización (en horas):",
@@ -6314,6 +6338,7 @@
"Server IDs to test against, separated by commas (optional)": "ID de los servidores contra los que probar, separados por comas (opcional)",
"Server IP/hostname is correct": "La IP/nombre de host del servidor es correcta",
"Server IP:": "IP del servidor:",
"Server SSH port:": "Puerto SSH del servidor:",
"Server is accessible:": "El servidor es accesible:",
"Server is online": "El servidor está en línea",
"Server is running": "El servidor está funcionando",
@@ -6375,6 +6400,7 @@
"Share Access Error": "Error de acceso al recurso compartido",
"Share Name": "Nombre compartido",
"Share Options": "Opciones para compartir",
"Share a local USB device": "Compartir un dispositivo USB local",
"Share added successfully.": "Recurso compartido añadido correctamente.",
"Share deleted and Samba service restarted.": "Compartir eliminado y servicio Samba reiniciado.",
"Share exists:": "Compartir existe:",
@@ -6382,6 +6408,7 @@
"Share name:": "Nombre compartido:",
"Share not found on server:": "Compartir no encontrado en el servidor:",
"Share path:": "Compartir ruta:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Comparte dispositivos USB mediante SSH o conecta este equipo Linux a un servidor USB/IP.",
"Share updated successfully.": "Compartir actualizado con éxito.",
"Share:": "Compartir:",
"ShareCoveX panel": "Panel de ShareCoveX",
@@ -6651,6 +6678,8 @@
"Still has issues": "Todavía tiene problemas",
"Stop - shuts the guest down during backup": "Stop - apaga la VM o CT durante el backup",
"Stop it first and run this option again.": "Detenlo primero y ejecuta esta opción nuevamente.",
"Stop sharing a USB device": "Dejar de compartir un dispositivo USB",
"Stop sharing this device? An attached remote client will disconnect.": "¿Dejar de compartir este dispositivo? Se desconectará el cliente remoto que lo esté usando.",
"Stop that VM, then choose:": "Detén ese VM, luego elige:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Detén el CT, desmonta el disco en el HOST y vuelve a montarlo con:",
"Stop the VM/CT before formatting this disk.": "Detén el VM/CT antes de formatear este disco.",
@@ -7004,6 +7033,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",
@@ -7278,6 +7308,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.",
@@ -7345,6 +7376,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "La GPU Intel seleccionada tiene soporte de reinicio no FLR y subtipo desconocido",
"The selected Intel GPU is currently in power state D3cold": "La GPU Intel seleccionada se encuentra actualmente en estado de energía D3cold",
"The selected Intel render device does not exist:": "El dispositivo de renderización Intel seleccionado no existe:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "El dispositivo USB dejará de estar disponible para las aplicaciones y los invitados locales. Solo puede usarlo un cliente remoto a la vez. ¿Compartirlo?",
"The selected VM": "La máquina virtual seleccionada",
"The selected VM is running.": "La VM seleccionada se está ejecutando.",
"The selected base folder does not exist and could not be created:": "La carpeta base seleccionada no existe y no se pudo crear:",
@@ -7682,6 +7714,9 @@
"USB drives mounted now:": "Unidades USB montadas ahora:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "Nodo de dispositivo USB o serie",
"USB/IP configuration updated. Open status to check the connection.": "Configuración de USB/IP actualizada. Abre el estado para comprobar la conexión.",
"USB/IP over SSH": "USB/IP mediante SSH",
"USB/IP server IP address or DNS hostname:": "Dirección IP o nombre DNS del servidor USB/IP:",
"UUP Dump script not found.": "No se encontró el script de UUP Dump.",
"UUp Dump ISO creator Custom": "Creador de ISO personalizada con UUP Dump",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity es un servidor doméstico gratuito, ligero y fácil de usar para tus cómics y ebooks. Úsalo para acceder a tus archivos desde cualquier lugar, con una tableta, un lector electrónico, un teléfono o un ordenador.",
@@ -7836,6 +7871,7 @@
"Untrusted or modified NVIDIA hook": "Hook NVIDIA sin confianza o modificado",
"Unused image removed from the cache:": "Imagen sin uso eliminada de la caché:",
"Unused images removed from the cache:": "Imágenes sin uso eliminadas de la caché:",
"Unused local loopback TCP port for this device:": "Puerto TCP de loopback local libre para este dispositivo:",
"Update": "Actualización",
"Update Available": "Actualización disponible",
"Update Ceph repository (Only if using Ceph):": "Actualiza el repositorio de Ceph (solo si usas Ceph):",
@@ -8121,6 +8157,7 @@
"View Disk Storages": "Ver almacenamientos en disco",
"View Mount Points": "Ver puntos de montaje",
"View NFS Mounts (pvesm + fstab)": "Ver montajes NFS (pvesm + fstab)",
"View USB/IP status": "Ver el estado de USB/IP",
"View backup metadata": "Ver metadatos de backup",
"View current paths": "Ver rutas actuales",
"View existing folders in /mnt": "Ver carpetas existentes en /mnt",
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Avorté",
"About to restore": "Sur le point de restaurer",
"Absolute directory path to use as backup target:": "Chemin du répertoire absolu à utiliser comme cible de sauvegarde :",
"Absolute path of the client SSH private key:": "Chemin absolu de la clé privée SSH du client :",
"Absolute path to a file or directory you want backed up:": "Chemin absolu vers un fichier ou un répertoire que vous souhaitez sauvegarder :",
"Acceleration": "Accélération",
"Acceleration configuration cancelled": "Configuration d'accélération annulée",
@@ -619,6 +620,8 @@
"Authentication:": "Authentification:",
"Authorization failed": "L'autorisation a échoué",
"Authorization successful": "Autorisation réussie",
"Authorize a client SSH public key": "Autoriser la clé publique SSH d’un client",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Autorisez d’abord une clé publique dédiée au client sur le serveur. Le compte USB/IP restreint ne peut ni ouvrir de shell ni rediriger d’autres sockets. Vérifiez la clé d’hôte du serveur avant la connexion ; les clés inconnues sont refusées.",
"Authorize this key on the server": "Autoriser cette clé sur le serveur",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autoriser via 'pct exec' (le serveur Borg est un LXC sur un hôte PVE sur lequel je peux me connecter en SSH)",
"Authorized": "Autorisé",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Utilitaires en conflit supprimés",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Connectez les appareils Zigbee à MQTT via une interface web de configuration.",
"Connect a Coral Accelerator and try again.": "Connectez un accélérateur Coral et réessayez.",
"Connect a remote USB device": "Connecter un périphérique USB distant",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Connectez-le ou supprimez-le du conteneur dans Proxmox (Ressources) et réexécutez la récupération.",
"Connect your devices and users together in your own secure virtual private network.": "Connectez vos appareils et utilisateurs ensemble dans votre propre réseau privé virtuel sécurisé.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Connectez vos appareils à un réseau de superposition sécurisé basé sur WireGuard® avec des contrôles d'accès SSO, MFA et granulaires.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Administrateur Directus",
"Disable NIC Offloading (Intel e1000e)": "Désactiver le déchargement de la carte réseau (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Désactivez Secure Boot dans le firmware UEFI et relancez le programme d'installation. Rien n'a été modifié.",
"Disable USB/IP server": "Désactiver le serveur USB/IP",
"Disable autostart on conflicting VM": "Désactiver le démarrage automatique sur la VM en conflit",
"Disable autostart on one VM to avoid startup conflicts.": "Désactivez le démarrage automatique sur une VM pour éviter les conflits de démarrage.",
"Disable job": "Désactiver le travail",
"Disable kernel audit messages (optional but recommended):": "Désactivez les messages d'audit du noyau (facultatif mais recommandé) :",
"Disable onboot on affected VM(s)/CT(s)": "Désactiver le démarrage sur les VM/CT concernés",
"Disable portmapper/rpcbind": "Désactiver portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Désactiver le serveur et libérer tous les périphériques USB gérés par ProxMenux ? Les sélections enregistrées seront conservées pour la prochaine activation.",
"Disabled": "Désactivé",
"Disabling High Availability services...": "Désactivation des services haute disponibilité...",
"Disabling ZFS autotrim applied by ProxMenux...": "Désactivation du découpage automatique ZFS appliqué par ProxMenux...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Activez le Bureau à distance (RDP) avant de désactiver l'affichage virtuel.",
"Enable SSD emulation for this disk?": "Activer l'émulation SSD pour ce disque ?",
"Enable TCP BBR/Fast Open control": "Activer le contrôle TCP BBR/Fast Open",
"Enable USB/IP server": "Activer le serveur USB/IP",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Activer le transcodage VA-API et HDR10/Dolby Vision pour la cartographie des tons SDR",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Activer le VA-API, l'encodage matériel et la cartographie des tons OpenCL dans Jellyfin",
"Enable VFIO IOMMU support": "Activer la prise en charge de VFIO IOMMU",
@@ -3213,6 +3220,7 @@
"If pvesm path returned a DEVICE (LVM):": "Si pvesm path a renvoyé un PÉRIPHÉRIQUE (LVM) :",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Si pvesm path a renvoyé un RÉPERTOIRE (ZFS/BTRFS) :",
"If repositories error:": "Si erreur de référentiels :",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Si le service USB/IP s'arrête de manière inattendue, arrêtez le partage tant que le périphérique est encore branché pour le libérer. Dans de rares cas, seul un redémarrage de l'hôte le libère.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Si le conteneur a déjà été modifié, sa sauvegarde native précédente est restaurée. Les répertoires hôtes partagés ne sont pas rétablis. Les disques de la tentative ayant échoué sont supprimés une fois le nettoyage réussi.",
"If the host freezes, remove hostpci entries from": "Si l'hôte se bloque, supprimez les entrées hostpci de",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Si le canal d'image a une nouvelle version, le CT est arrêté, sauvegardé et vérifié, puis remplacé par la nouvelle image pour mettre à jour le conteneur. Si quelque chose échoue, la sauvegarde est restaurée.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Installez les packages de serveur dans CT :",
"Install terminal multiplexers": "Installer des multiplexeurs de terminaux",
"Install the Edge TPU runtime (libedgetpu1-std)": "Installez le runtime Edge TPU (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "Installer les dépendances USB/IP depuis les dépôts Debian configurés ?",
"Install this image?": "Installer cette image ?",
"Install with Cloud-Init script": "Installer avec le script Cloud-Init",
"Install with ISO from UUP Dump": "Installer avec ISO à partir de UUP Dump",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Ligne à coller (une seule ligne, incluant le préfixe \"command=...\") :",
"LinkStack": "LinkStack",
"Linux Installation Options": "Options d'installation Linux",
"Linux guest setup and migration notes": "Configuration des invités Linux et notes de migration",
"Linux/Mac path:": "Chemin Linux/Mac :",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin avec passage GPU optionnel",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB requires un utilisateur, une base de données et un mot de passe",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Nom ou partie de l'option ou de l'application",
"Name:": "Nom:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Les comptes nommés nécessitent un mode privé. Arrêter le conteneur, définir public: false in /config/config.js, recommencer et créer chaque utilisateur avec: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge ajouter <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "La version native de Borg n’est pas disponible. Installez le paquet natif borgbackup sur cet hôte pour utiliser les sauvegardes Borg.",
"Needed:": "Nécessaire :",
"Needs": "Nécessite",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Ni /etc/kernel/cmdline ni /etc/default/grub n'ont été trouvés.",
@@ -4456,6 +4469,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é.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Pas d'adresses IP en double",
"No duplicate repositories found": "Aucun référentiel en double trouvé",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Il ne reste aucun périphérique contrôleur/NVMe éligible après le filtrage SR-IOV. Saut.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Aucun périphérique USB éligible trouvé. Les concentrateurs, les périphériques de stockage de masse et les adaptateurs réseau sont exclus.",
"No eligible controllers remain after SR-IOV filtering.": "Il ne reste aucun contrôleur éligible après le filtrage SR-IOV.",
"No encryption key is stored on this host. Choose how to set one up:": "Aucune clé de cryptage n’est stockée sur cet hôte. Choisissez comment en configurer un :",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Aucun disque de VM exportable n'a été trouvé (les CD-ROM/cloud-init sont exclus).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Aucun journal d'autotest disponible pour",
"No server IP or hostname provided.": "Aucune adresse IP de serveur ou nom d'hôte fourni.",
"No share access test succeeded with guest authentication.": "Aucun test d'accès au partage n'a réussi avec l'authentification des invités.",
"No shared devices are available on this server.": "Aucun périphérique partagé n'est disponible sur ce serveur.",
"No shared media content.": "Pas de contenu multimédia partagé.",
"No shared mount detected. Applying standard local access.": "Aucun montage partagé détecté. Application d'un accès local standard.",
"No shares configured.": "Aucun partage configuré.",
@@ -5129,6 +5145,7 @@
"Path of the Compose file": "Chemin du fichier Composer",
"Path of the oci-compose file": "Chemin du fichier oci-compose",
"Path removed:": "Chemin supprimé :",
"Path to the client SSH public key on this server:": "Chemin de la clé publique SSH du client sur ce serveur :",
"Path:": "Chemin:",
"Paths applied:": "Chemins appliqués :",
"Paths included in backup": "Chemins inclus dans la sauvegarde",
@@ -5675,6 +5692,7 @@
"Remove Proxmox NFS storage:": "Supprimez le stockage Proxmox NFS :",
"Remove Proxmox iSCSI storage:": "Supprimez le stockage iSCSI Proxmox :",
"Remove Secure Gateway? State will be preserved.": "Supprimer Secure Gateway ? L'État sera préservé.",
"Remove a remote USB connection": "Supprimer une connexion USB distante",
"Remove custom paths": "Supprimer les chemins personnalisés",
"Remove disk references from affected VM(s)/CT(s) config": "Supprimer les références de disque de la configuration des VM/CT concernées",
"Remove every registered gasket DKMS version": "Supprimer toutes les versions DKMS de gasket enregistrées",
@@ -5695,6 +5713,7 @@
"Remove the lock left on a container by an interrupted task": "Supprimer le verrou laissé sur un conteneur par une tâche interrompue",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Supprimer les informations d'identification enregistrées pour {registry} ?\nLes applications en cours d’exécution continueront de fonctionner.",
"Remove the unprivileged flag from configuration:": "Supprimez l'indicateur sans privilèges de la configuration :",
"Remove this connection and detach its USB device?": "Supprimer cette connexion et détacher son périphérique USB ?",
"Remove unused packages and their config": "Supprimer les packages inutilisés et leur configuration",
"Remove: delete the application and its containers": "Supprimer: supprimer l'application et ses conteneurs",
"Removed": "Supprimé",
@@ -5917,9 +5936,11 @@
"Run mode: Unattended": "Mode d'exécution : sans surveillance",
"Run now": "Exécuter maintenant",
"Run security audit now": "Exécutez un audit de sécurité maintenant",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Exécutez le client dans la VM Linux qui a besoin du périphérique USB. Installez usbip, openssh-client et kmod, puis copiez usbip_manager.py au même chemin que sur le serveur. Configurez l'authentification par clé et vérifiez la clé d'hôte du serveur avant d'ajouter une connexion. Le client utilise le noyau de l'invité et peut se reconnecter après une migration tandis que le périphérique USB physique reste sur le serveur. Les LXC non privilégiés et Home Assistant OS nécessitent une configuration spécifique du noyau, des périphériques ou des modules complémentaires. Un périphérique physique ne peut être attaché qu'à un seul client à la fois.",
"Run the following inside the VM:": "Exécutez la commande suivante dans la VM :",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Exécutez le commutateur de canal de version à partir d'une session SSH ou de la console hôte Proxmox avec :",
"Run the update from an SSH session or the Proxmox host console with:": "Exécutez la mise à jour depuis une session SSH ou la console hôte Proxmox avec :",
"Run this menu as root.": "Exécutez ce menu en tant que root.",
"Run this operation as root.": "Exécutez cette opération en tant que root.",
"Run upgrade checklist script:": "Exécutez le script de liste de contrôle de mise à niveau :",
"Running": "En cours d'exécution",
@@ -6144,6 +6165,7 @@
"Select a new language for the menu:": "Sélectionnez une nouvelle langue pour le menu :",
"Select a post-installation script:": "Sélectionnez un script de post-installation :",
"Select a pre-configured Linux VM script to execute:": "Sélectionnez un script de machine virtuelle Linux préconfiguré à exécuter :",
"Select a saved USB/IP device:": "Sélectionnez un périphérique USB/IP enregistré :",
"Select a script or action:": "Sélectionnez un script ou une action :",
"Select a share to delete:": "Sélectionnez un partage à supprimer :",
"Select a specific Coral or USB node, not the whole /dev": "Sélectionnez un noeud spécifique Coral ou USB, pas l'ensemble /dev",
@@ -6221,6 +6243,7 @@
"Select the Linux distribution to install:": "Sélectionnez la distribution Linux à installer :",
"Select the NAS system to install:": "Sélectionnez le système NAS à installer :",
"Select the NVIDIA driver version to install:": "Sélectionnez la version du pilote NVIDIA à installer :",
"Select the USB device to share:": "Sélectionnez le périphérique USB à partager :",
"Select the VM to add the GPU to:": "Sélectionnez la VM à laquelle ajouter le GPU :",
"Select the VM to which you want to add disks:": "Sélectionnez la VM à laquelle vous souhaitez ajouter des disques :",
"Select the VM where you want to import the disk image:": "Sélectionnez la VM sur laquelle vous souhaitez importer l'image disque :",
@@ -6245,6 +6268,7 @@
"Select the parent job to attach to:": "sélectionnez la tâche parent à attacher :",
"Select the paths to restore (one per backup entry):": "Sélectionnez les chemins à restaurer (un par entrée de sauvegarde) :",
"Select the privileged LXC container to convert:": "Sélectionnez le conteneur LXC privilégié à convertir :",
"Select the remote USB device:": "Sélectionnez le périphérique USB distant :",
"Select the storage that will receive the backups:": "Sélectionnez le stockage qui recevra les sauvegardes :",
"Select the storage volume for disk import:": "Sélectionnez le volume de stockage pour l'importation de disque :",
"Select the sync interval (in hours):": "Sélectionnez l'intervalle de synchronisation (en heures) :",
@@ -6331,6 +6355,7 @@
"Server IDs to test against, separated by commas (optional)": "ID de serveur à tester, séparés par des virgules (facultatif)",
"Server IP/hostname is correct": "L'adresse IP/le nom d'hôte du serveur est correct",
"Server IP:": "IP du serveur :",
"Server SSH port:": "Port SSH du serveur :",
"Server is accessible:": "Le serveur est accessible :",
"Server is online": "Le serveur est en ligne",
"Server is running": "Le serveur est en cours d'exécution",
@@ -6392,6 +6417,7 @@
"Share Access Error": "Erreur d'accès au partage",
"Share Name": "Partager le nom",
"Share Options": "Options de partage",
"Share a local USB device": "Partager un périphérique USB local",
"Share added successfully.": "Partage ajouté avec succès.",
"Share deleted and Samba service restarted.": "Partage supprimé et service Samba redémarré.",
"Share exists:": "Le partage existe :",
@@ -6399,6 +6425,7 @@
"Share name:": "Nom du partage :",
"Share not found on server:": "Partage introuvable sur le serveur :",
"Share path:": "Chemin de partage :",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Partagez les périphériques USB sélectionnés via SSH ou connectez cette machine Linux à un serveur USB/IP.",
"Share updated successfully.": "Partage mis à jour avec succès.",
"Share:": "Partager:",
"ShareCoveX panel": "Panneau ShareCoveX",
@@ -6667,6 +6694,8 @@
"Still has issues": "A encore des problèmes",
"Stop - shuts the guest down during backup": "Arrêter - arrête l'invité pendant la sauvegarde",
"Stop it first and run this option again.": "Arrêtez-le d'abord et réexécutez cette option.",
"Stop sharing a USB device": "Arrêter le partage d'un périphérique USB",
"Stop sharing this device? An attached remote client will disconnect.": "Arrêter le partage de ce périphérique ? Tout client distant attaché sera déconnecté.",
"Stop that VM, then choose:": "Arrêtez ce VM, puis choisissez :",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Arrêtez le CT, démontez le disque sur le HOST et remontez avec :",
"Stop the VM/CT before formatting this disk.": "Arrêtez la VM/CT avant de formater ce disque.",
@@ -7021,6 +7050,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",
@@ -7302,6 +7332,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é.",
@@ -7369,6 +7400,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "Le GPU Intel sélectionné prend en charge la réinitialisation non FLR et sous-type inconnu",
"The selected Intel GPU is currently in power state D3cold": "Le GPU Intel sélectionné est actuellement en état d'alimentation D3cold",
"The selected Intel render device does not exist:": "Le périphérique de rendu Intel sélectionné n'existe pas :",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "Le périphérique USB sélectionné ne sera plus disponible pour les applications et les invités locaux. Un seul client distant peut l'utiliser à la fois. Partager ce périphérique ?",
"The selected VM": "La VM sélectionnée",
"The selected VM is running.": "La VM sélectionnée est en cours d'exécution.",
"The selected base folder does not exist and could not be created:": "Le dossier de base sélectionné n'existe pas et n'a pas pu être créé :",
@@ -7708,6 +7740,9 @@
"USB drives mounted now:": "Clés USB montées maintenant :",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "nœud de périphérique USB ou série",
"USB/IP configuration updated. Open status to check the connection.": "Configuration USB/IP mise à jour. Ouvrez l'état pour vérifier la connexion.",
"USB/IP over SSH": "USB/IP via SSH",
"USB/IP server IP address or DNS hostname:": "Adresse IP ou nom d'hôte DNS du serveur USB/IP :",
"UUP Dump script not found.": "Script de vidage UUP introuvable.",
"UUp Dump ISO creator Custom": "UUp Dump Créateur ISO Personnalisé",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity est un serveur à domicile gratuit, léger et facile à utiliser pour vos BD et ebooks. Utilisez-le pour accéder à vos fichiers de n'importe où, avec une tablette, un lecteur électronique, un téléphone ou un ordinateur.",
@@ -7864,6 +7899,7 @@
"Untrusted or modified NVIDIA hook": "Crochet NVIDIA non fiable ou modifié",
"Unused image removed from the cache:": "Image non utilisée retirée du cache :",
"Unused images removed from the cache:": "Images inutilisées supprimées du cache :",
"Unused local loopback TCP port for this device:": "Port TCP local de bouclage inutilisé pour ce périphérique :",
"Update": "Mise à jour",
"Update Available": "mise à jour disponible",
"Update Ceph repository (Only if using Ceph):": "Mettre à jour le référentiel Ceph (uniquement si vous utilisez Ceph) :",
@@ -8147,6 +8183,7 @@
"View Disk Storages": "Afficher les stockages sur disque",
"View Mount Points": "Afficher les points de montage",
"View NFS Mounts (pvesm + fstab)": "Afficher les montages NFS (pvesm + fstab)",
"View USB/IP status": "Afficher l'état USB/IP",
"View backup metadata": "Afficher les métadonnées de sauvegarde",
"View current paths": "afficher les chemins actuels",
"View existing folders in /mnt": "Afficher les dossiers existants dans /mnt",
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Interrotto",
"About to restore": "Ripristino imminente di",
"Absolute directory path to use as backup target:": "Percorso assoluto della directory di destinazione del backup:",
"Absolute path of the client SSH private key:": "Percorso assoluto della chiave privata SSH del client:",
"Absolute path to a file or directory you want backed up:": "Percorso assoluto del file o della directory da includere nel backup:",
"Acceleration": "Accelerazione",
"Acceleration configuration cancelled": "Configurazione accelerazione annullata",
@@ -619,6 +620,8 @@
"Authentication:": "Autenticazione:",
"Authorization failed": "Autorizzazione fallita",
"Authorization successful": "Autorizzazione riuscita",
"Authorize a client SSH public key": "Autorizza la chiave pubblica SSH di un client",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Autorizza prima una chiave pubblica dedicata del client sul server. L’account USB/IP con accesso limitato non può avviare una shell né inoltrare altri socket. Verifica la chiave host del server prima di connetterti; le chiavi sconosciute vengono rifiutate.",
"Authorize this key on the server": "Autorizzare questa chiave sul server",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autorizza tramite 'pct exec' (il server Borg è un LXC su un host PVE a cui posso accedere tramite SSH)",
"Authorized": "Autorizzato",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Utilità in conflitto rimosse",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Collega i dispositivi Zigbee a MQTT tramite un’interfaccia web di configurazione.",
"Connect a Coral Accelerator and try again.": "Collega un Coral Accelerator e riprova.",
"Connect a remote USB device": "Collega un dispositivo USB remoto",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Collegalo o rimuovilo dal container in Proxmox (Risorse), poi ripeti il recupero.",
"Connect your devices and users together in your own secure virtual private network.": "Collega dispositivi e utenti nella tua rete privata virtuale sicura.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Collega i tuoi dispositivi in una rete overlay sicura basata su WireGuard®, con SSO, MFA e controlli granulari degli accessi.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Amministratore Directus",
"Disable NIC Offloading (Intel e1000e)": "Disabilita offload NIC (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Disattiva Secure Boot nel firmware UEFI ed esegui di nuovo il programma di installazione. Non è stato modificato nulla.",
"Disable USB/IP server": "Disabilita il server USB/IP",
"Disable autostart on conflicting VM": "Disabilita l'avvio automatico sulla VM in conflitto",
"Disable autostart on one VM to avoid startup conflicts.": "Disabilita l'avvio automatico su una VM per evitare conflitti di avvio.",
"Disable job": "Disabilita job",
"Disable kernel audit messages (optional but recommended):": "Disabilita i messaggi di audit del kernel (facoltativo ma consigliato):",
"Disable onboot on affected VM(s)/CT(s)": "Disabilita onboot sulle VM/CT interessati",
"Disable portmapper/rpcbind": "Disabilita portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Disabilitare il server e liberare tutti i dispositivi USB gestiti da ProxMenux? Le selezioni salvate verranno mantenute per la prossima attivazione.",
"Disabled": "Disabilitato",
"Disabling High Availability services...": "Disabilitazione dei servizi ad alta disponibilità...",
"Disabling ZFS autotrim applied by ProxMenux...": "Disattivazione di autotrim ZFS applicato da ProxMenux...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Abilita Desktop remoto (RDP) prima di disabilitare il display virtuale.",
"Enable SSD emulation for this disk?": "Abilitare l'emulazione SSD per questo disco?",
"Enable TCP BBR/Fast Open control": "Abilita controllo TCP BBR/Fast Open",
"Enable USB/IP server": "Abilita il server USB/IP",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Abilita la transcodifica VA-API e la conversione dei toni da HDR10/Dolby Vision a SDR",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Abilita VA-API, la codifica hardware e la mappatura dei toni OpenCL in Jellyfin",
"Enable VFIO IOMMU support": "Abilita supporto VFIO IOMMU",
@@ -3213,6 +3220,7 @@
"If pvesm path returned a DEVICE (LVM):": "Se pvesm path ha restituito un DISPOSITIVO (LVM):",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Se pvesm path ha restituito una DIRECTORY (ZFS/BTRFS):",
"If repositories error:": "Se si verificano errori nei repository:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Se il servizio USB/IP si arresta in modo imprevisto, interrompi la condivisione mentre il dispositivo è ancora collegato per liberarlo. In rari casi solo un riavvio dell'host lo libera.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Se il container era già stato modificato, viene ripristinato il suo backup nativo precedente. Le directory condivise dell'host non vengono ripristinate. I dischi del tentativo fallito vengono rimossi se la pulizia riesce.",
"If the host freezes, remove hostpci entries from": "Se l'host si blocca, rimuovi le voci hostpci da",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Se il canale immagine ha una nuova versione, CT viene arrestato, sottoposto a backup, verificato e sostituito con la nuova immagine per aggiornare il contenitore. Se qualcosa fallisce, il backup viene ripristinato.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Installa i pacchetti server nel CT:",
"Install terminal multiplexers": "Installa multiplexer di terminale",
"Install the Edge TPU runtime (libedgetpu1-std)": "Installa il runtime Edge TPU (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "Installare le dipendenze USB/IP dai repository Debian configurati?",
"Install this image?": "Installare questa immagine?",
"Install with Cloud-Init script": "Installa con lo script Cloud-Init",
"Install with ISO from UUP Dump": "Installa con ISO da UUP Dump",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Riga da incollare (una sola riga, incluso il prefisso \"command=...\"):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Opzioni installazione Linux",
"Linux guest setup and migration notes": "Configurazione degli ospiti Linux e note sulla migrazione",
"Linux/Mac path:": "Percorso Linux/Mac:",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin con passthrough GPU opzionale",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB richiede un utente, un database e una password",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Nome dell'opzione o dell'applicazione, anche parziale",
"Name:": "Nome:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Gli account nominativi richiedono la modalità privata. Arresta il container, imposta public: false in /config/config.js, riavvialo e crea ogni utente con: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "La versione nativa di Borg non è disponibile. Installa il pacchetto borgbackup nativo su questo host per utilizzare i backup Borg.",
"Needed:": "Spazio richiesto:",
"Needs": "Richiede",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Non sono stati trovati né /etc/kernel/cmdline né /etc/default/grub.",
@@ -4456,6 +4469,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.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Nessun indirizzo IP duplicato",
"No duplicate repositories found": "Nessun repository duplicato trovato",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Dopo il filtraggio SR-IOV non rimane alcun dispositivo controller/NVMe idoneo. Operazione saltata.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Nessun dispositivo USB idoneo trovato. Gli hub, i dispositivi di archiviazione di massa e gli adattatori di rete sono esclusi.",
"No eligible controllers remain after SR-IOV filtering.": "Dopo il filtraggio SR-IOV non rimane alcun controller idoneo.",
"No encryption key is stored on this host. Choose how to set one up:": "Nessuna chiave di crittografia archiviata su questo host. Scegli come configurarne una:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Nessun disco VM esportabile trovato (CD-ROM/cloud-init esclusi).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Nessun registro di autotest disponibile per",
"No server IP or hostname provided.": "Nessun IP del server o nome host fornito.",
"No share access test succeeded with guest authentication.": "Nessun test di accesso alle condivisioni è riuscito con l'autenticazione guest.",
"No shared devices are available on this server.": "Nessun dispositivo condiviso disponibile su questo server.",
"No shared media content.": "Nessun contenuto multimediale condiviso.",
"No shared mount detected. Applying standard local access.": "Nessun montaggio condiviso rilevato. Applicazione dell'accesso locale standard.",
"No shares configured.": "Nessuna condivisione configurata.",
@@ -5129,6 +5145,7 @@
"Path of the Compose file": "Percorso del file Compose",
"Path of the oci-compose file": "Percorso del file oci-compose",
"Path removed:": "Percorso rimosso:",
"Path to the client SSH public key on this server:": "Percorso della chiave pubblica SSH del client su questo server:",
"Path:": "Percorso:",
"Paths applied:": "Percorsi applicati:",
"Paths included in backup": "Percorsi inclusi nel backup",
@@ -5675,6 +5692,7 @@
"Remove Proxmox NFS storage:": "Rimuovi storage NFS Proxmox:",
"Remove Proxmox iSCSI storage:": "Rimuovi storage iSCSI Proxmox:",
"Remove Secure Gateway? State will be preserved.": "Rimuovere Secure Gateway? Lo stato sarà preservato.",
"Remove a remote USB connection": "Rimuovi una connessione USB remota",
"Remove custom paths": "Rimuovi percorsi personalizzati",
"Remove disk references from affected VM(s)/CT(s) config": "Rimuovi i riferimenti al disco dalle configurazioni VM/CT interessate",
"Remove every registered gasket DKMS version": "Rimuovi ogni versione DKMS di gasket registrata",
@@ -5695,6 +5713,7 @@
"Remove the lock left on a container by an interrupted task": "Rimuovere il blocco lasciato su un container da un’attività interrotta",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Rimuovere le credenziali salvate per {registry}?\nLe app in esecuzione continueranno a funzionare.",
"Remove the unprivileged flag from configuration:": "Rimuovi il flag unprivileged dalla configurazione:",
"Remove this connection and detach its USB device?": "Rimuovere questa connessione e scollegare il relativo dispositivo USB?",
"Remove unused packages and their config": "Rimuovi i pacchetti non utilizzati e la loro configurazione",
"Remove: delete the application and its containers": "Rimuovi: elimina l'applicazione e i suoi container",
"Removed": "RIMOSSO",
@@ -5917,9 +5936,11 @@
"Run mode: Unattended": "Modalità di esecuzione: non presidiata",
"Run now": "Esegui ora",
"Run security audit now": "Esegui subito il controllo di sicurezza",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Esegui il client nella VM Linux che necessita del dispositivo USB. Installa usbip, openssh-client e kmod, poi copia usbip_manager.py nello stesso percorso usato sul server. Configura l'autenticazione tramite chiave e verifica la chiave host del server prima di aggiungere una connessione. Il client usa il kernel dell'ospite e può riconnettersi dopo una migrazione mentre il dispositivo USB fisico rimane sul server. Gli LXC non privilegiati e Home Assistant OS richiedono una configurazione specifica del kernel, dei dispositivi o dei componenti aggiuntivi. Un dispositivo fisico può essere collegato a un solo client alla volta.",
"Run the following inside the VM:": "Esegui quanto segue nella VM:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Esegui il cambio del canale di rilascio da una sessione SSH o dalla console dell’host Proxmox con:",
"Run the update from an SSH session or the Proxmox host console with:": "Esegui l’aggiornamento da una sessione SSH o dalla console dell’host Proxmox con:",
"Run this menu as root.": "Esegui questo menu come root.",
"Run this operation as root.": "Esegui questa operazione come root.",
"Run upgrade checklist script:": "Esegui lo script di controllo dell’aggiornamento:",
"Running": "In esecuzione",
@@ -6144,6 +6165,7 @@
"Select a new language for the menu:": "Seleziona una nuova lingua per il menu:",
"Select a post-installation script:": "Seleziona uno script post-installazione:",
"Select a pre-configured Linux VM script to execute:": "Seleziona uno script VM Linux preconfigurato da eseguire:",
"Select a saved USB/IP device:": "Seleziona un dispositivo USB/IP salvato:",
"Select a script or action:": "Seleziona uno script o un'azione:",
"Select a share to delete:": "Seleziona una condivisione da eliminare:",
"Select a specific Coral or USB node, not the whole /dev": "Seleziona un nodo Coral o USB specifico, non l’intera directory /dev",
@@ -6221,6 +6243,7 @@
"Select the Linux distribution to install:": "Seleziona la distribuzione Linux da installare:",
"Select the NAS system to install:": "Seleziona il sistema NAS da installare:",
"Select the NVIDIA driver version to install:": "Seleziona la versione del driver NVIDIA da installare:",
"Select the USB device to share:": "Seleziona il dispositivo USB da condividere:",
"Select the VM to add the GPU to:": "Seleziona la VM a cui aggiungere la GPU:",
"Select the VM to which you want to add disks:": "Seleziona la VM a cui aggiungere i dischi:",
"Select the VM where you want to import the disk image:": "Seleziona la VM in cui importare l’immagine disco:",
@@ -6245,6 +6268,7 @@
"Select the parent job to attach to:": "Seleziona l’attività principale a cui collegarsi:",
"Select the paths to restore (one per backup entry):": "Seleziona i percorsi da ripristinare (uno per voce di backup):",
"Select the privileged LXC container to convert:": "Seleziona il container LXC privilegiato da convertire:",
"Select the remote USB device:": "Seleziona il dispositivo USB remoto:",
"Select the storage that will receive the backups:": "Seleziona lo storage di destinazione dei backup:",
"Select the storage volume for disk import:": "Seleziona il volume storage per importare il disco:",
"Select the sync interval (in hours):": "Seleziona l'intervallo di sincronizzazione (in ore):",
@@ -6331,6 +6355,7 @@
"Server IDs to test against, separated by commas (optional)": "ID dei server da usare per i test, separati da virgole (facoltativi)",
"Server IP/hostname is correct": "L'IP/nome host del server è corretto",
"Server IP:": "IP del server:",
"Server SSH port:": "Porta SSH del server:",
"Server is accessible:": "Il server è accessibile:",
"Server is online": "Il server è in linea",
"Server is running": "Il server è in esecuzione",
@@ -6392,6 +6417,7 @@
"Share Access Error": "Errore di accesso alla condivisione",
"Share Name": "Nome condivisione",
"Share Options": "Opzioni di condivisione",
"Share a local USB device": "Condividi un dispositivo USB locale",
"Share added successfully.": "Condivisione aggiunta con successo.",
"Share deleted and Samba service restarted.": "Condivisione eliminata e servizio Samba riavviato.",
"Share exists:": "La condivisione esiste:",
@@ -6399,6 +6425,7 @@
"Share name:": "Nome condivisione:",
"Share not found on server:": "Condivisione non trovata sul server:",
"Share path:": "Percorso condivisione:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Condividi i dispositivi USB selezionati tramite SSH oppure collega questa macchina Linux a un server USB/IP.",
"Share updated successfully.": "Condivisione aggiornata con successo.",
"Share:": "Condivisione:",
"ShareCoveX panel": "Pannello ShareCoveX",
@@ -6667,6 +6694,8 @@
"Still has issues": "Ha ancora problemi",
"Stop - shuts the guest down during backup": "Stop - arresta il guest durante il backup",
"Stop it first and run this option again.": "Interrompilo prima ed esegui nuovamente questa opzione.",
"Stop sharing a USB device": "Interrompi la condivisione di un dispositivo USB",
"Stop sharing this device? An attached remote client will disconnect.": "Interrompere la condivisione di questo dispositivo? Il client remoto collegato verrà disconnesso.",
"Stop that VM, then choose:": "Arresta quella VM, quindi scegli:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Arresta il CT, smonta il disco sull’HOST e rimontalo con:",
"Stop the VM/CT before formatting this disk.": "Arresta la VM o il CT prima di formattare questo disco.",
@@ -7022,6 +7051,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",
@@ -7303,6 +7333,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.",
@@ -7370,6 +7401,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "La GPU Intel selezionata ha il supporto per il ripristino non FLR e un sottotipo sconosciuto",
"The selected Intel GPU is currently in power state D3cold": "La GPU Intel selezionata è nello stato di alimentazione D3cold",
"The selected Intel render device does not exist:": "Il dispositivo di rendering Intel selezionato non esiste:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "Il dispositivo USB selezionato non sarà più disponibile per le applicazioni e gli ospiti locali. Può usarlo un solo client remoto alla volta. Condividere questo dispositivo?",
"The selected VM": "La VM selezionata",
"The selected VM is running.": "La VM selezionata è in esecuzione.",
"The selected base folder does not exist and could not be created:": "La cartella di base selezionata non esiste e non è stato possibile crearla:",
@@ -7709,6 +7741,9 @@
"USB drives mounted now:": "Unità USB montate ora:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "Nodo dispositivo USB o seriale",
"USB/IP configuration updated. Open status to check the connection.": "Configurazione USB/IP aggiornata. Apri lo stato per verificare la connessione.",
"USB/IP over SSH": "USB/IP tramite SSH",
"USB/IP server IP address or DNS hostname:": "Indirizzo IP o nome host DNS del server USB/IP:",
"UUP Dump script not found.": "Script UUP Dump non trovato.",
"UUp Dump ISO creator Custom": "UUp Dump - Creazione ISO personalizzate",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity è un server domestico gratuito, leggero e facile da usare per fumetti ed ebook. Consente l’accesso ai file ovunque, da tablet, e-reader, telefono o computer.",
@@ -7865,6 +7900,7 @@
"Untrusted or modified NVIDIA hook": "Hook NVIDIA non attendibile o modificato",
"Unused image removed from the cache:": "Immagine inutilizzata rimossa dalla cache:",
"Unused images removed from the cache:": "Immagini non utilizzate rimosse dalla cache:",
"Unused local loopback TCP port for this device:": "Porta TCP locale di loopback inutilizzata per questo dispositivo:",
"Update": "Aggiornamento",
"Update Available": "Aggiornamento disponibile",
"Update Ceph repository (Only if using Ceph):": "Aggiorna repository Ceph (solo se si utilizza Ceph):",
@@ -8148,6 +8184,7 @@
"View Disk Storages": "Visualizza storage su disco",
"View Mount Points": "Visualizza punti di montaggio",
"View NFS Mounts (pvesm + fstab)": "Visualizza montaggi NFS (pvesm + fstab)",
"View USB/IP status": "Visualizza lo stato USB/IP",
"View backup metadata": "Visualizza metadati backup",
"View current paths": "Visualizza percorsi correnti",
"View existing folders in /mnt": "Visualizza cartelle esistenti in /mnt",
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Abortado",
"About to restore": "Prestes a restaurar",
"Absolute directory path to use as backup target:": "caminho absoluto do diretório para usar como destino de backup:",
"Absolute path of the client SSH private key:": "Caminho absoluto da chave privada SSH do cliente:",
"Absolute path to a file or directory you want backed up:": "Caminho absoluto para um arquivo ou diretório do qual deseja fazer backup:",
"Acceleration": "Aceleração",
"Acceleration configuration cancelled": "Configuração da aceleração cancelada",
@@ -619,6 +620,8 @@
"Authentication:": "Autenticação:",
"Authorization failed": "Falha na autorização",
"Authorization successful": "Autorização bem-sucedida",
"Authorize a client SSH public key": "Autorizar a chave pública SSH de um cliente",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Autorize primeiro uma chave pública dedicada do cliente no servidor. A conta USB/IP restrita não pode abrir uma shell nem encaminhar outros sockets. Verifique a chave do servidor antes de ligar; as chaves desconhecidas são rejeitadas.",
"Authorize this key on the server": "Autorize esta chave no servidor",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autorizar via 'pct exec' (o servidor Borg é um LXC em um host PVE no qual posso usar SSH)",
"Authorized": "Autorizado",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Utilitários conflitantes removidos",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Conecte dispositivos Zigbee ao MQTT por meio de uma interface web de configuração.",
"Connect a Coral Accelerator and try again.": "Conecte um Coral Accelerator e tente novamente.",
"Connect a remote USB device": "Ligar um dispositivo USB remoto",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Conecte-o ou remova-o do contêiner em Proxmox (Recursos) e execute a recuperação novamente.",
"Connect your devices and users together in your own secure virtual private network.": "Conecte seus dispositivos e usuários em sua própria rede privada virtual segura.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Conecte seus dispositivos a uma rede segura de sobreposição WireGuard® com controles de acesso SSO, MFA e granular.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Administrador do Directus",
"Disable NIC Offloading (Intel e1000e)": "Desativar descarregamento de NIC (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Desative o Secure Boot no firmware UEFI e execute o instalador novamente. Nada foi alterado.",
"Disable USB/IP server": "Desativar o servidor USB/IP",
"Disable autostart on conflicting VM": "Desative a inicialização automática em VM conflitante",
"Disable autostart on one VM to avoid startup conflicts.": "Desative a inicialização automática em uma VM para evitar conflitos de inicialização.",
"Disable job": "Desativar trabalho",
"Disable kernel audit messages (optional but recommended):": "Desative as mensagens de auditoria do kernel (opcional, mas recomendado):",
"Disable onboot on affected VM(s)/CT(s)": "Desativar inicialização em VM(s)/CT(s) afetados",
"Disable portmapper/rpcbind": "Desativar portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Desativar o servidor e libertar todos os dispositivos USB geridos pelo ProxMenux? As seleções guardadas serão mantidas para a próxima ativação.",
"Disabled": "Desabilitado",
"Disabling High Availability services...": "Desativando serviços de alta disponibilidade...",
"Disabling ZFS autotrim applied by ProxMenux...": "Desativando o ajuste automático do ZFS aplicado pelo ProxMenux...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Habilite a Área de Trabalho Remota (RDP) antes de desabilitar a exibição virtual.",
"Enable SSD emulation for this disk?": "Ativar emulação SSD para este disco?",
"Enable TCP BBR/Fast Open control": "Habilitar controle TCP BBR/Fast Open",
"Enable USB/IP server": "Ativar o servidor USB/IP",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Habilitar transcodificação VA-API e HDR10/Dolby Vision para mapeamento de tom SDR",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Habilitar VA-API, codificação de hardware e mapeamento de tom OpenCL em Jellyfin",
"Enable VFIO IOMMU support": "Habilitar suporte VFIO IOMMU",
@@ -3213,6 +3220,7 @@
"If pvesm path returned a DEVICE (LVM):": "Se pvesm path devolveu um DISPOSITIVO (LVM):",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Se pvesm path devolveu um DIRETÓRIO (ZFS/BTRFS):",
"If repositories error:": "Se houver erro nos repositórios:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Se o serviço USB/IP parar inesperadamente, deixe de partilhar o dispositivo enquanto ainda estiver ligado para o libertar. Em casos raros, só um reinício do host o liberta.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Se o contêiner já tiver sido alterado, seu backup nativo anterior será restaurado. Os diretórios de host compartilhados não são revertidos. Os discos da tentativa fracassada serão removidos quando a limpeza for bem-sucedida.",
"If the host freezes, remove hostpci entries from": "Se o host congelar, remova as entradas hostpci de",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Se o canal de imagem tiver uma nova versão, o CT é interrompido, feito backup e verificado e substituído pela nova imagem para atualizar o contêiner. Se algo falhar, o backup será restaurado.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Instale pacotes de servidores dentro do CT:",
"Install terminal multiplexers": "Instale multiplexadores de terminal",
"Install the Edge TPU runtime (libedgetpu1-std)": "Instale o tempo de execução do Edge TPU (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "Instalar as dependências USB/IP a partir dos repositórios Debian configurados?",
"Install this image?": "Instalar esta imagem?",
"Install with Cloud-Init script": "Instalar com script Cloud-Init",
"Install with ISO from UUP Dump": "Instalar com ISO do UUP Dump",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Linha a ser colada (linha única, incluindo o prefixo \"command=...\"):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Opções de instalação do Linux",
"Linux guest setup and migration notes": "Configuração de convidados Linux e notas de migração",
"Linux/Mac path:": "Caminho Linux/Mac:",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin com passagem opcional de GPU",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB requires um usuário, banco de dados e senha",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Nome ou parte da opção ou do aplicativo",
"Name:": "Nome:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Contas nomeadas precisam de modo privado. Pare o contêiner, defina público: false in /config/config.js, inicie-o novamente e crie cada usuário com: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "A versão nativa do Borg não está disponível. Instale o pacote borgbackup nativo neste host para utilizar as cópias de segurança do Borg.",
"Needed:": "Necessário:",
"Needs": "Requer",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Nem /etc/kernel/cmdline nem /etc/default/grub foram encontrados.",
@@ -4456,6 +4469,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.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Nenhum endereço IP duplicado",
"No duplicate repositories found": "Nenhum repositório duplicado encontrado",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Nenhum dispositivo Controlador/NVMe qualificado permanece após a filtragem SR-IOV. Pulando.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Não foram encontrados dispositivos USB elegíveis. Os hubs, os dispositivos de armazenamento em massa e os adaptadores de rede estão excluídos.",
"No eligible controllers remain after SR-IOV filtering.": "Nenhum controlador elegível permanece após a filtragem SR-IOV.",
"No encryption key is stored on this host. Choose how to set one up:": "Nenhuma chave de criptografia é armazenada neste host. Escolha como configurar um:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Nenhum disco VM exportável foi encontrado (CD-ROM/cloud-init foram excluídos).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Nenhum registro de autoteste disponível para",
"No server IP or hostname provided.": "Nenhum IP do servidor ou nome de host fornecido.",
"No share access test succeeded with guest authentication.": "Nenhum teste de acesso de compartilhamento foi bem-sucedido com a autenticação de convidado.",
"No shared devices are available on this server.": "Não há dispositivos partilhados disponíveis neste servidor.",
"No shared media content.": "Sem conteúdo de mídia compartilhado.",
"No shared mount detected. Applying standard local access.": "Nenhuma montagem compartilhada detectada. Aplicando acesso local padrão.",
"No shares configured.": "Nenhum compartilhamento configurado.",
@@ -5129,6 +5145,7 @@
"Path of the Compose file": "Localização do ficheiro Compor",
"Path of the oci-compose file": "Caminho do arquivo oci-compose",
"Path removed:": "Caminho removido:",
"Path to the client SSH public key on this server:": "Caminho da chave pública SSH do cliente neste servidor:",
"Path:": "Caminho:",
"Paths applied:": "Caminhos aplicados:",
"Paths included in backup": "Caminhos incluídos no backup",
@@ -5675,6 +5692,7 @@
"Remove Proxmox NFS storage:": "Remova o armazenamento Proxmox NFS:",
"Remove Proxmox iSCSI storage:": "Remova o armazenamento iSCSI Proxmox:",
"Remove Secure Gateway? State will be preserved.": "Remover Secure Gateway? O estado será preservado.",
"Remove a remote USB connection": "Remover uma ligação USB remota",
"Remove custom paths": "Remover caminhos personalizados",
"Remove disk references from affected VM(s)/CT(s) config": "Remover referências de disco da configuração de VM(s)/CT(s) afetadas",
"Remove every registered gasket DKMS version": "Remover todas as versões DKMS de gasket registadas",
@@ -5695,6 +5713,7 @@
"Remove the lock left on a container by an interrupted task": "Remover o bloqueio deixado num contentor por uma tarefa interrompida",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Remover as credenciais salvas para {registry}?\nOs aplicativos em execução continuarão funcionando.",
"Remove the unprivileged flag from configuration:": "Remova o sinalizador sem privilégios da configuração:",
"Remove this connection and detach its USB device?": "Remover esta ligação e desligar o respetivo dispositivo USB?",
"Remove unused packages and their config": "Remova pacotes não utilizados e suas configurações",
"Remove: delete the application and its containers": "Remover: apagar o aplicativo e seus recipientes",
"Removed": "Removido",
@@ -5917,9 +5936,11 @@
"Run mode: Unattended": "Modo de execução: Autônomo",
"Run now": "Executar agora",
"Run security audit now": "Execute a auditoria de segurança agora",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Execute o cliente na VM Linux que precisa do dispositivo USB. Instale usbip, openssh-client e kmod e copie usbip_manager.py para o mesmo caminho usado no servidor. Configure a autenticação por chave e verifique a chave de anfitrião do servidor antes de adicionar uma ligação. O cliente usa o kernel do convidado e pode voltar a ligar-se após uma migração enquanto o dispositivo USB físico permanece no servidor. Os LXC sem privilégios e o Home Assistant OS precisam de uma configuração própria do kernel, dos dispositivos ou dos complementos. Um dispositivo físico só pode estar ligado a um cliente de cada vez.",
"Run the following inside the VM:": "Execute o seguinte dentro da VM:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "execute a opção de canal de lançamento a partir de uma sessão SSH ou do console do host Proxmox com:",
"Run the update from an SSH session or the Proxmox host console with:": "execute a atualização a partir de uma sessão SSH ou do console do host Proxmox com:",
"Run this menu as root.": "Execute este menu como root.",
"Run this operation as root.": "Execute esta operação como root.",
"Run upgrade checklist script:": "Execute o script da lista de verificação de atualização:",
"Running": "Correndo",
@@ -6144,6 +6165,7 @@
"Select a new language for the menu:": "Selecione um novo idioma para o menu:",
"Select a post-installation script:": "Selecione um script pós-instalação:",
"Select a pre-configured Linux VM script to execute:": "Selecione um script de VM Linux pré-configurado para executar:",
"Select a saved USB/IP device:": "Selecione um dispositivo USB/IP guardado:",
"Select a script or action:": "Selecione um script ou ação:",
"Select a share to delete:": "Selecione um compartilhamento para excluir:",
"Select a specific Coral or USB node, not the whole /dev": "Selecione um nó Coral ou USB específico, não o /dev inteiro",
@@ -6221,6 +6243,7 @@
"Select the Linux distribution to install:": "Selecione a distribuição Linux para instalar:",
"Select the NAS system to install:": "Selecione o sistema NAS para instalar:",
"Select the NVIDIA driver version to install:": "Selecione a versão do driver NVIDIA para instalar:",
"Select the USB device to share:": "Selecione o dispositivo USB a partilhar:",
"Select the VM to add the GPU to:": "Selecione a VM à qual adicionar a GPU:",
"Select the VM to which you want to add disks:": "Selecione a VM à qual você deseja adicionar discos:",
"Select the VM where you want to import the disk image:": "Selecione a VM para a qual deseja importar a imagem de disco:",
@@ -6245,6 +6268,7 @@
"Select the parent job to attach to:": "Selecione o trabalho pai ao qual deseja anexar:",
"Select the paths to restore (one per backup entry):": "Selecione os caminhos para restaurar (um por entrada de backup):",
"Select the privileged LXC container to convert:": "Selecione o contêiner LXC privilegiado para converter:",
"Select the remote USB device:": "Selecione o dispositivo USB remoto:",
"Select the storage that will receive the backups:": "Selecione o armazenamento que receberá os backups:",
"Select the storage volume for disk import:": "Selecione o volume de armazenamento para importação de disco:",
"Select the sync interval (in hours):": "Selecione o intervalo de sincronização (em horas):",
@@ -6331,6 +6355,7 @@
"Server IDs to test against, separated by commas (optional)": "IDs de servidor para testar, separados por vírgulas (opcional)",
"Server IP/hostname is correct": "O IP/nome do host do servidor está correto",
"Server IP:": "IP do servidor:",
"Server SSH port:": "Porta SSH do servidor:",
"Server is accessible:": "O servidor está acessível:",
"Server is online": "O servidor está on-line",
"Server is running": "O servidor está em execução",
@@ -6392,6 +6417,7 @@
"Share Access Error": "Erro de acesso ao compartilhamento",
"Share Name": "Compartilhar nome",
"Share Options": "Opções de compartilhamento",
"Share a local USB device": "Partilhar um dispositivo USB local",
"Share added successfully.": "Compartilhamento adicionado com sucesso.",
"Share deleted and Samba service restarted.": "Compartilhamento excluído e serviço Samba reiniciado.",
"Share exists:": "Existe compartilhamento:",
@@ -6399,6 +6425,7 @@
"Share name:": "Nome do compartilhamento:",
"Share not found on server:": "Compartilhamento não encontrado no servidor:",
"Share path:": "Compartilhar caminho:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Partilhe os dispositivos USB selecionados através de SSH ou ligue esta máquina Linux a um servidor USB/IP.",
"Share updated successfully.": "Compartilhamento atualizado com sucesso.",
"Share:": "Compartilhar:",
"ShareCoveX panel": "Painel ShareCoveX",
@@ -6667,6 +6694,8 @@
"Still has issues": "Ainda tem problemas",
"Stop - shuts the guest down during backup": "Parar - desliga o convidado durante o backup",
"Stop it first and run this option again.": "Pare primeiro e execute esta opção novamente.",
"Stop sharing a USB device": "Parar a partilha de um dispositivo USB",
"Stop sharing this device? An attached remote client will disconnect.": "Parar a partilha deste dispositivo? O cliente remoto ligado será desligado.",
"Stop that VM, then choose:": "Pare esse VM e escolha:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Pare o CT, desmonte o disco no HOST e remonte com:",
"Stop the VM/CT before formatting this disk.": "Pare a VM/CT antes de formatar este disco.",
@@ -7022,6 +7051,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",
@@ -7303,6 +7333,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.",
@@ -7370,6 +7401,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "A GPU Intel selecionada tem suporte para redefinição não FLR e subtipo desconhecido",
"The selected Intel GPU is currently in power state D3cold": "A GPU Intel selecionada está atualmente no estado de energia D3cold",
"The selected Intel render device does not exist:": "O dispositivo de renderização Intel selecionado não existe:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "O dispositivo USB selecionado ficará indisponível para aplicações e convidados locais. Só um cliente remoto o pode usar de cada vez. Partilhar este dispositivo?",
"The selected VM": "A VM selecionada",
"The selected VM is running.": "A VM selecionada está em execução.",
"The selected base folder does not exist and could not be created:": "A pasta base selecionada não existe e não pôde ser criada:",
@@ -7709,6 +7741,9 @@
"USB drives mounted now:": "unidades USB montadas agora:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "nó de dispositivo USB ou serial",
"USB/IP configuration updated. Open status to check the connection.": "Configuração USB/IP atualizada. Abra o estado para verificar a ligação.",
"USB/IP over SSH": "USB/IP através de SSH",
"USB/IP server IP address or DNS hostname:": "Endereço IP ou nome de anfitrião DNS do servidor USB/IP:",
"UUP Dump script not found.": "Script de despejo UUP não encontrado.",
"UUp Dump ISO creator Custom": "Criador UUp Dump ISO personalizado",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity é um servidor doméstico gratuito, leve e fácil de usar para seus quadrinhos e ebooks. Use-o para acessar seus arquivos de qualquer lugar, com um tablet, um e-leitor, um telefone ou um computador.",
@@ -7865,6 +7900,7 @@
"Untrusted or modified NVIDIA hook": "Gancho NVIDIA não confiável ou modificado",
"Unused image removed from the cache:": "Imagem não usada removida da cache:",
"Unused images removed from the cache:": "Imagens não usadas removidas da cache:",
"Unused local loopback TCP port for this device:": "Porta TCP local de loopback livre para este dispositivo:",
"Update": "Atualizar",
"Update Available": "atualização disponível",
"Update Ceph repository (Only if using Ceph):": "Atualize o repositório do Ceph (somente se estiver usando o Ceph):",
@@ -8148,6 +8184,7 @@
"View Disk Storages": "Ver armazenamentos em disco",
"View Mount Points": "Ver pontos de montagem",
"View NFS Mounts (pvesm + fstab)": "Ver montagens NFS (pvesm + fstab)",
"View USB/IP status": "Ver estado USB/IP",
"View backup metadata": "Ver metadados de backup",
"View current paths": "Ver caminhos atuais",
"View existing folders in /mnt": "Veja as pastas existentes em /mnt",
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Zrušené",
"About to restore": "Chystá sa obnova",
"Absolute directory path to use as backup target:": "Absolútna cesta k priečinku, ktorý sa má použiť ako cieľ záloh:",
"Absolute path of the client SSH private key:": "Absolútna cesta k súkromnému SSH kľúču klienta:",
"Absolute path to a file or directory you want backed up:": "Absolútna cesta k súboru alebo priečinku, ktorý chcete zálohovať:",
"Acceleration": "Zrýchlenie",
"Acceleration configuration cancelled": "Konfigurácia zrýchlenia zrušená",
@@ -619,6 +620,8 @@
"Authentication:": "Prihlásenie:",
"Authorization failed": "Autorizácia zlyhala",
"Authorization successful": "Autorizácia úspešná",
"Authorize a client SSH public key": "Povoliť verejný SSH kľúč klienta",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Najprv na serveri povoľte samostatný verejný kľúč klienta. Obmedzený účet USB/IP nemôže spustiť shell ani presmerovať iné sockety. Pred pripojením overte hostiteľský kľúč servera; neznáme kľúče sa odmietnu.",
"Authorize this key on the server": "Autorizovať tento kľúč na serveri",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Autorizovať cez 'pct exec' (Borg server je LXC na PVE hostovi, kam sa viem pripojiť cez SSH)",
"Authorized": "Autorizované",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Konfliktné nástroje odstránené",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Pripojte zariadenia Zigbee k MQTT prostredníctvom webového rozhrania na nastavenie.",
"Connect a Coral Accelerator and try again.": "Pripojte Coral Accelerator a skúste to znova.",
"Connect a remote USB device": "Pripojiť vzdialené USB zariadenie",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Pripojte zariadenie alebo ho odstráňte z konfigurácie kontajnera v Proxmoxe (Zdroje) a registráciu zopakujte.",
"Connect your devices and users together in your own secure virtual private network.": "Spojte svoje zariadenia a užívateľov vo vlastnej zabezpečenej virtuálnej súkromnej sieti.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Pripojte svoje zariadenia do zabezpečenej prekryvnej siete založenej na WireGuard® s ovládacími prvkami SSO, MFA a granulovaného prístupu.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Správca Directus",
"Disable NIC Offloading (Intel e1000e)": "Vypnúť NIC offloading (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Vypnite Secure Boot vo firmvéri UEFI a spustite inštalátor znova. Nič sa nezmenilo.",
"Disable USB/IP server": "Zakázať server USB/IP",
"Disable autostart on conflicting VM": "Vypnúť automatický štart na konfliktnej VM",
"Disable autostart on one VM to avoid startup conflicts.": "Na jednej VM vypnite automatický štart, aby nevznikol konflikt pri spúšťaní.",
"Disable job": "Zakázať úlohu",
"Disable kernel audit messages (optional but recommended):": "Vypnite audit správy kernelu (voliteľné, ale odporúčané):",
"Disable onboot on affected VM(s)/CT(s)": "Vypnúť onboot na dotknutých VM/CT",
"Disable portmapper/rpcbind": "Vypnúť portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Zakázať server a uvoľniť všetky USB zariadenia spravované ProxMenuxom? Uložený výber sa zachová pre ďalšie povolenie.",
"Disabled": "Vypnuté",
"Disabling High Availability services...": "Vypínam služby High Availability...",
"Disabling ZFS autotrim applied by ProxMenux...": "Vypínam ZFS autotrim, ktorý nastavil ProxMenux...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Pred vypnutím virtuálneho zobrazenia zapnite Vzdialenú plochu (RDP).",
"Enable SSD emulation for this disk?": "Zapnúť pre tento disk emuláciu SSD?",
"Enable TCP BBR/Fast Open control": "Zapnúť TCP BBR/Fast Open",
"Enable USB/IP server": "Povoliť server USB/IP",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Povoliť prevod videa cez VA-API a mapovanie tónov HDR10/Dolby Vision na SDR",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Povoliť VA-API, hardvérové kódovanie a mapovanie tónov pomocou OpenCL v Jellyfine",
"Enable VFIO IOMMU support": "Zapnúť podporu VFIO/IOMMU",
@@ -3212,6 +3219,7 @@
"If pvesm path returned a DEVICE (LVM):": "Ak pvesm path vrátil ZARIADENIE (LVM):",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Ak pvesm path vrátil PRIEČINOK (ZFS/BTRFS):",
"If repositories error:": "Ak zlyhajú repozitáre:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Ak sa služba USB/IP neočakávane zastaví, ukončite zdieľanie, kým je zariadenie ešte pripojené, aby sa uvoľnilo. V zriedkavých prípadoch ho uvoľní iba reštart hosta.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Ak už bol kontajner zmenený, obnoví sa jeho predchádzajúca natívna záloha. Zdieľané hostiteľské adresáre sa nevrátia späť. Disky neúspešného pokusu sa po úspešnom vyčistení odstránia.",
"If the host freezes, remove hostpci entries from": "Ak server zamrzne, odstráňte položky hostpci z",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Ak má kanál obrázkov novú verziu, CT sa zastaví, zálohuje a overí a nahradí sa novým obrázkom, aby sa aktualizoval kontajner. Ak niečo zlyhá, záloha sa obnoví.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Nainštalovať serverové balíky vo vnútri CT:",
"Install terminal multiplexers": "Nainštalovať terminálové multiplexery",
"Install the Edge TPU runtime (libedgetpu1-std)": "Nainštaluje Edge TPU runtime (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "Nainštalovať závislosti USB/IP z nakonfigurovaných repozitárov Debianu?",
"Install this image?": "Nainštalovať tento obraz?",
"Install with Cloud-Init script": "Inštalovať cez Cloud-Init skript",
"Install with ISO from UUP Dump": "Inštalovať z ISO cez UUP Dump",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Riadok na vloženie (jeden riadok vrátane predpony \"command=...\"):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Možnosti inštalácie Linuxu",
"Linux guest setup and migration notes": "Nastavenie hostí Linux a poznámky k migrácii",
"Linux/Mac path:": "Cesta pre Linux/Mac:",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin s voliteľným GPU priechodom",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB vyžaduje používateľa, databázu a heslo",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Názov alebo časť možnosti alebo aplikácie",
"Name:": "Názov:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Používateľské účty vyžadujú súkromný režim. Zastavte kontajner, v /config/config.js nastavte public: false, znova ho spustite a každého používateľa vytvorte príkazom: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Natívna verzia Borg nie je dostupná. Ak chcete používať zálohy Borg, nainštalujte na tento hostiteľ natívny balík borgbackup.",
"Needed:": "Potrebné:",
"Needs": "Vyžaduje",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Nenašiel sa /etc/kernel/cmdline ani /etc/default/grub.",
@@ -4456,6 +4469,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.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Nenašli sa duplicitné IP adresy",
"No duplicate repositories found": "Nenašli sa žiadne duplicitné repozitáre",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Po odfiltrovaní SR-IOV nezostali žiadne vhodné Controller/NVMe zariadenia. Preskakuje sa.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Nenašli sa žiadne vhodné USB zariadenia. Rozbočovače, veľkokapacitné úložiská a sieťové adaptéry sú vylúčené.",
"No eligible controllers remain after SR-IOV filtering.": "Po odfiltrovaní SR-IOV nezostali žiadne vhodné controllery.",
"No encryption key is stored on this host. Choose how to set one up:": "Na tomto hostovi nie je uložený žiadny šifrovací kľúč. Vyberte, ako ho nastaviť:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Nenašli sa žiadne exportovateľné VM disky (CD-ROM/cloud-init sú vylúčené).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Nie je dostupný log samo-testu pre",
"No server IP or hostname provided.": "Nebola zadaná IP adresa ani názov servera.",
"No share access test succeeded with guest authentication.": "S overením hosťa nebol úspešný žiadny test prístupu k zdieľaniu.",
"No shared devices are available on this server.": "Na tomto serveri nie sú k dispozícii žiadne zdieľané zariadenia.",
"No shared media content.": "Žiadny spoločný mediálny obsah.",
"No shared mount detected. Applying standard local access.": "Nenašlo sa zdieľané pripojenie. Nastavujem bežný lokálny prístup.",
"No shares configured.": "Nie sú nastavené žiadne zdieľania.",
@@ -5131,6 +5147,7 @@
"Path of the Compose file": "Cesta k súboru Compose",
"Path of the oci-compose file": "Cesta k súboru oci-compose",
"Path removed:": "Cesta bola odstránená:",
"Path to the client SSH public key on this server:": "Cesta k verejnému SSH kľúču klienta na tomto serveri:",
"Path:": "Cesta:",
"Paths applied:": "Použité cesty:",
"Paths included in backup": "Cesty zahrnuté v zálohe",
@@ -5677,6 +5694,7 @@
"Remove Proxmox NFS storage:": "Odstrániť NFS úložisko z Proxmoxu:",
"Remove Proxmox iSCSI storage:": "Odstrániť iSCSI úložisko z Proxmoxu:",
"Remove Secure Gateway? State will be preserved.": "Odstrániť Secure Gateway? Stav bude zachovaný.",
"Remove a remote USB connection": "Odstrániť vzdialené USB pripojenie",
"Remove custom paths": "Odstrániť vlastné cesty",
"Remove disk references from affected VM(s)/CT(s) config": "Odstrániť referencie na disk z nastavení dotknutých VM/CT",
"Remove every registered gasket DKMS version": "Odstrániť všetky registrované verzie gasket DKMS",
@@ -5697,6 +5715,7 @@
"Remove the lock left on a container by an interrupted task": "Odstrániť zámok, ktorý na kontajneri zanechala prerušená úloha",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Odstrániť uložené prihlasovacie údaje pre {registry}?\nSpustené aplikácie budú naďalej fungovať.",
"Remove the unprivileged flag from configuration:": "Odstrániť príznak neprivilegovaného kontajnera z nastavenia:",
"Remove this connection and detach its USB device?": "Odstrániť toto pripojenie a odpojiť jeho USB zariadenie?",
"Remove unused packages and their config": "Odstrániť nepoužívané balíky aj ich nastavenia",
"Remove: delete the application and its containers": "Odstrániť: zmazať aplikáciu a jej kontajnery",
"Removed": "Odstránené",
@@ -5919,9 +5938,11 @@
"Run mode: Unattended": "Režim spustenia: bez zásahu",
"Run now": "Bež teraz",
"Run security audit now": "Spustiť bezpečnostný audit teraz",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Klienta spustite vo virtuálnom stroji Linux, ktorý potrebuje USB zariadenie. Nainštalujte usbip, openssh-client a kmod a potom skopírujte usbip_manager.py na rovnakú cestu ako na serveri. Pred pridaním pripojenia nastavte overovanie kľúčom a overte hostiteľský kľúč servera. Klient používa jadro hosťa a po migrácii sa môže znova pripojiť, zatiaľ čo fyzické USB zariadenie zostáva na serveri. Neprivilegované LXC a Home Assistant OS vyžadujú vlastné nastavenie jadra, zariadení alebo doplnkov. Fyzické zariadenie môže byť pripojené iba k jednému klientovi naraz.",
"Run the following inside the VM:": "Vo vnútri VM spustite:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Prepnutie vetvy vydania spustite cez SSH alebo priamo z konzoly Proxmox servera pomocou:",
"Run the update from an SSH session or the Proxmox host console with:": "Spustite aktualizáciu zo SSH relácie alebo z konzoly Proxmox hosta pomocou:",
"Run this menu as root.": "Túto ponuku spustite ako root.",
"Run this operation as root.": "Túto operáciu spustite ako root.",
"Run upgrade checklist script:": "Spustite kontrolný skript aktualizácie:",
"Running": "Beží",
@@ -6146,6 +6167,7 @@
"Select a new language for the menu:": "Vyberte nový jazyk menu:",
"Select a post-installation script:": "Vyberte skript po inštalácii:",
"Select a pre-configured Linux VM script to execute:": "Vyberte pripravený skript pre Linux VM, ktorý sa má spustiť:",
"Select a saved USB/IP device:": "Vyberte uložené zariadenie USB/IP:",
"Select a script or action:": "Vyberte skript alebo akciu:",
"Select a share to delete:": "Vyberte zdieľanie na vymazanie:",
"Select a specific Coral or USB node, not the whole /dev": "Vyberte špecifický Coral alebo USB uzol, nie celý /dev",
@@ -6223,6 +6245,7 @@
"Select the Linux distribution to install:": "Vyberte Linux distribúciu na inštaláciu:",
"Select the NAS system to install:": "Vyberte NAS systém na inštaláciu:",
"Select the NVIDIA driver version to install:": "Vyberte verziu ovládača NVIDIA na inštaláciu:",
"Select the USB device to share:": "Vyberte USB zariadenie na zdieľanie:",
"Select the VM to add the GPU to:": "Vyberte VM, do ktorej sa má GPU pridať:",
"Select the VM to which you want to add disks:": "Vyberte VM, do ktorej chcete pridať disky:",
"Select the VM where you want to import the disk image:": "Vyberte VM, do ktorej chcete importovať diskový obraz:",
@@ -6247,6 +6270,7 @@
"Select the parent job to attach to:": "Vyberte nadradenú úlohu, ku ktorej sa má pripojiť:",
"Select the paths to restore (one per backup entry):": "Vyberte cesty na obnovu (jedna za každú položku zálohy):",
"Select the privileged LXC container to convert:": "Vyberte privilegovaný LXC kontajner na prevod:",
"Select the remote USB device:": "Vyberte vzdialené USB zariadenie:",
"Select the storage that will receive the backups:": "Vyberte úložisko, ktoré bude prijímať zálohy:",
"Select the storage volume for disk import:": "Vyberte úložisko pre import disku:",
"Select the sync interval (in hours):": "Vyberte interval synchronizácie (v hodinách):",
@@ -6333,6 +6357,7 @@
"Server IDs to test against, separated by commas (optional)": "ID serverov, proti ktorým sa majú spustiť testy (voliteľné)",
"Server IP/hostname is correct": "IP/hostname servera je správny",
"Server IP:": "IP servera:",
"Server SSH port:": "SSH port servera:",
"Server is accessible:": "Server je dostupný:",
"Server is online": "Server je online",
"Server is running": "Server beží",
@@ -6394,6 +6419,7 @@
"Share Access Error": "Chyba prístupu k zdieľaniu",
"Share Name": "Názov zdieľania",
"Share Options": "Možnosti zdieľania",
"Share a local USB device": "Zdieľať miestne USB zariadenie",
"Share added successfully.": "Zdieľanie bolo úspešne pridané.",
"Share deleted and Samba service restarted.": "Zdieľanie bolo vymazané a služba Samba reštartovaná.",
"Share exists:": "Zdieľanie existuje:",
@@ -6401,6 +6427,7 @@
"Share name:": "Názov zdieľania:",
"Share not found on server:": "Zdieľanie sa na serveri nenašlo:",
"Share path:": "Cesta zdieľania:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Zdieľajte vybrané USB zariadenia cez SSH alebo pripojte tento počítač Linux k serveru USB/IP.",
"Share updated successfully.": "Zdieľanie bolo úspešne aktualizované.",
"Share:": "Zdieľanie:",
"ShareCoveX panel": "Panel ShareCoveX",
@@ -6669,6 +6696,8 @@
"Still has issues": "Stále má problémy",
"Stop - shuts the guest down during backup": "Stop – vypne hosťa počas zálohovania",
"Stop it first and run this option again.": "Najprv ju zastavte a potom túto možnosť spustite znova.",
"Stop sharing a USB device": "Ukončiť zdieľanie USB zariadenia",
"Stop sharing this device? An attached remote client will disconnect.": "Ukončiť zdieľanie tohto zariadenia? Pripojený vzdialený klient bude odpojený.",
"Stop that VM, then choose:": "Zastavte VM a potom vyberte:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Zastavte CT, odpojte disk na HOSTE a pripojte ho znova pomocou:",
"Stop the VM/CT before formatting this disk.": "Pred formátovaním tohto disku zastavte VM/CT.",
@@ -7023,6 +7052,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é",
@@ -7304,6 +7334,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ý.",
@@ -7371,6 +7402,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "Vybraná Intel GPU má reset podporu mimo FLR a neznámy podtyp",
"The selected Intel GPU is currently in power state D3cold": "Vybraná Intel GPU je aktuálne v napájacom stave D3cold",
"The selected Intel render device does not exist:": "Vybrané zariadenie Intel render neexistuje:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "Vybrané USB zariadenie prestane byť dostupné miestnym aplikáciám a hosťom. Naraz ho môže používať iba jeden vzdialený klient. Zdieľať toto zariadenie?",
"The selected VM": "Vybraná VM",
"The selected VM is running.": "Vybraná VM beží.",
"The selected base folder does not exist and could not be created:": "Vybraný základný priečinok neexistuje a nepodarilo sa ho vytvoriť:",
@@ -7709,6 +7741,9 @@
"USB drives mounted now:": "Aktuálne pripojené USB disky:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "Uzol USB alebo sériového zariadenia",
"USB/IP configuration updated. Open status to check the connection.": "Konfigurácia USB/IP bola aktualizovaná. Otvorte stav a skontrolujte pripojenie.",
"USB/IP over SSH": "USB/IP cez SSH",
"USB/IP server IP address or DNS hostname:": "IP adresa alebo DNS názov hostiteľa servera USB/IP:",
"UUP Dump script not found.": "Skript UUP Dump sa nenašiel.",
"UUp Dump ISO creator Custom": "Vlastný tvorca ISO cez UUP Dump",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity je bezplatný, ľahký a ľahko použiteľný domáci server pre vaše komiksy a ebooky. Použite ho na prístup k súborom odkiaľkoľvek, s tabletom, e-čítačkou, telefónom alebo počítačom.",
@@ -7865,6 +7900,7 @@
"Untrusted or modified NVIDIA hook": "Nedôveryhodný alebo zmenený spúšťací skript NVIDIA",
"Unused image removed from the cache:": "Nepoužívaný obraz odstránený z vyrovnávacej pamäte:",
"Unused images removed from the cache:": "Nepoužívané obrazy odstránené z vyrovnávacej pamäte:",
"Unused local loopback TCP port for this device:": "Voľný miestny TCP port spätnej slučky pre toto zariadenie:",
"Update": "Aktualizácia",
"Update Available": "Dostupná aktualizácia",
"Update Ceph repository (Only if using Ceph):": "Aktualizujte Ceph repozitár (iba ak používate Ceph):",
@@ -8148,6 +8184,7 @@
"View Disk Storages": "Zobraziť diskové úložiská",
"View Mount Points": "Zobraziť mount pointy",
"View NFS Mounts (pvesm + fstab)": "Zobraziť NFS mounty (pvesm + fstab)",
"View USB/IP status": "Zobraziť stav USB/IP",
"View backup metadata": "Zobraziť metadáta zálohy",
"View current paths": "Zobraziť aktuálne cesty",
"View existing folders in /mnt": "Zobraziť existujúce priečinky v /mnt",
+37
View File
@@ -246,6 +246,7 @@
"Aborted": "Avbruten",
"About to restore": "På väg att återställa",
"Absolute directory path to use as backup target:": "Absolut katalogsökväg att använda som mål för säkerhetskopiering:",
"Absolute path of the client SSH private key:": "Absolut sökväg till klientens privata SSH-nyckel:",
"Absolute path to a file or directory you want backed up:": "Absolut sökväg till en fil eller katalog som du vill säkerhetskopiera:",
"Acceleration": "Acceleration",
"Acceleration configuration cancelled": "Acceleration konfiguration inställd",
@@ -619,6 +620,8 @@
"Authentication:": "Autentisering:",
"Authorization failed": "Auktorisering misslyckades",
"Authorization successful": "Auktoriseringen lyckades",
"Authorize a client SSH public key": "Auktorisera en klients offentliga SSH-nyckel",
"Authorize a dedicated client public key on the server first. The restricted USB/IP account cannot run a shell or forward other sockets. Verify the server host key before connecting; unknown keys are rejected.": "Auktorisera först en separat offentlig klientnyckel på servern. Det begränsade USB/IP-kontot kan inte starta ett skal eller vidarebefordra andra socklar. Verifiera serverns värdnyckel innan du ansluter; okända nycklar avvisas.",
"Authorize this key on the server": "Auktorisera denna nyckel på servern",
"Authorize via 'pct exec' (Borg server is an LXC on a PVE host I can SSH into)": "Auktorisera via 'pct exec' (Borg-servern är en LXC på en PVE-värd jag kan SSH till)",
"Authorized": "Auktoriserad",
@@ -1299,6 +1302,7 @@
"Conflicting utilities removed": "Motstridiga verktyg har tagits bort",
"Connect Zigbee devices to MQTT with a browser-based setup interface.": "Anslut Zigbee-enheter till MQTT med ett webbaserat konfigurationsgränssnitt.",
"Connect a Coral Accelerator and try again.": "Anslut en Coral Accelerator och försök igen.",
"Connect a remote USB device": "Anslut en fjärransluten USB-enhet",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Anslut den, eller ta bort den från behållaren i Proxmox (Resurser), och kör återställningen igen.",
"Connect your devices and users together in your own secure virtual private network.": "Anslut dina enheter och användare tillsammans i ditt eget säkra virtuella privata nätverk.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Anslut dina enheter till ett säkert WireGuard®-baserat överläggsnätverk med SSO, MFA och granulära åtkomstkontroller.",
@@ -1962,12 +1966,14 @@
"Directus administrator": "Directus-administratör",
"Disable NIC Offloading (Intel e1000e)": "Inaktivera NIC-avlastning (Intel e1000e)",
"Disable Secure Boot in the UEFI firmware and run the installer again. Nothing has been changed.": "Inaktivera Secure Boot i UEFI-firmware och kör installationsprogrammet igen. Inget har ändrats.",
"Disable USB/IP server": "Inaktivera USB/IP-servern",
"Disable autostart on conflicting VM": "Inaktivera autostart på VM i konflikt",
"Disable autostart on one VM to avoid startup conflicts.": "Inaktivera autostart på en virtuell dator för att undvika startkonflikter.",
"Disable job": "Inaktivera jobbet",
"Disable kernel audit messages (optional but recommended):": "Inaktivera kärngranskningsmeddelanden (valfritt men rekommenderas):",
"Disable onboot on affected VM(s)/CT(s)": "Inaktivera onboot på berörda virtuella dator(er)/CT(ar)",
"Disable portmapper/rpcbind": "Inaktivera portmapper/rpcbind",
"Disable the server and release all USB devices managed by ProxMenux? Saved selections are kept for the next enable.": "Inaktivera servern och frigör alla USB-enheter som hanteras av ProxMenux? Sparade val behålls till nästa aktivering.",
"Disabled": "Inaktiverad",
"Disabling High Availability services...": "Inaktiverar tjänster med hög tillgänglighet...",
"Disabling ZFS autotrim applied by ProxMenux...": "Inaktiverar ZFS autotrim som tillämpas av ProxMenux...",
@@ -2200,6 +2206,7 @@
"Enable Remote Desktop (RDP) before disabling the virtual display.": "Aktivera Remote Desktop (RDP) innan du inaktiverar den virtuella skärmen.",
"Enable SSD emulation for this disk?": "Vill du aktivera SSD-emulering för den här disken?",
"Enable TCP BBR/Fast Open control": "Aktivera TCP BBR/Fast Open-kontroll",
"Enable USB/IP server": "Aktivera USB/IP-servern",
"Enable VA-API transcoding and HDR10/Dolby Vision to SDR tone mapping": "Aktivera VA-API-transkodning och HDR10/Dolby Vision till SDR-tonkartläggning",
"Enable VA-API, hardware encoding and OpenCL tone mapping in Jellyfin": "Aktivera VA-API, hårdvarukodning och OpenCL tonkartläggning i Jellyfin",
"Enable VFIO IOMMU support": "Aktivera VFIO IOMMU-stöd",
@@ -3213,6 +3220,7 @@
"If pvesm path returned a DEVICE (LVM):": "Om pvesm path returnerade en ENHET (LVM):",
"If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Om pvesm path returnerade en KATALOG (ZFS/BTRFS):",
"If repositories error:": "Om förrådsfel:",
"If the USB/IP service stops unexpectedly, stop sharing while the device is still connected to release it. In rare cases only a host reboot releases it.": "Om USB/IP-tjänsten stoppas oväntat, sluta dela enheten medan den fortfarande är ansluten för att frigöra den. I sällsynta fall frigörs den bara genom en omstart av värden.",
"If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Om behållaren redan hade ändrats, återställs dess tidigare inbyggda säkerhetskopia. Delade värdkataloger återställs inte. Diskarna för det misslyckade försöket tas bort när rensningen lyckas.",
"If the host freezes, remove hostpci entries from": "Om värden fryser, ta bort hostpci-poster från",
"If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If anything fails, the backup is restored.": "Om bildkanalen har en ny version stoppas CT, säkerhetskopieras och verifieras och ersätts med den nya bilden för att uppdatera behållaren. Om något misslyckas återställs säkerhetskopian.",
@@ -3407,6 +3415,7 @@
"Install server packages inside CT:": "Installera serverpaket inuti CT:",
"Install terminal multiplexers": "Installera terminalmultiplexer",
"Install the Edge TPU runtime (libedgetpu1-std)": "Installera Edge TPU runtime (libedgetpu1-std)",
"Install the USB/IP dependencies from the configured Debian repositories?": "Installera USB/IP-beroendena från de konfigurerade Debian-paketförråden?",
"Install this image?": "Installera den här bilden?",
"Install with Cloud-Init script": "Installera med Cloud-Init-skript",
"Install with ISO from UUP Dump": "Installera med ISO från UUP Dump",
@@ -3597,6 +3606,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",
@@ -3891,6 +3901,7 @@
"Line to paste (single line, including \"command=...\" prefix):": "Rad att klistra in (en rad, inklusive \"command=...\" prefix):",
"LinkStack": "LinkStack",
"Linux Installation Options": "Linux installationsalternativ",
"Linux guest setup and migration notes": "Konfiguration av Linux-gäster och information om migrering",
"Linux/Mac path:": "Linux/Mac sökväg:",
"LinuxServer Jellyfin with optional GPU passthrough": "LinuxServer Jellyfin med valfri GPU genomgång",
"LinuxServer MariaDB requires a user, database and password": "LinuxServer MariaDB requires en användare, databas och lösenord",
@@ -4315,6 +4326,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",
@@ -4398,6 +4410,7 @@
"Name or part of the option or of the application": "Namn eller del av alternativet eller applikationen",
"Name:": "Namn:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Namngivna konton behöver privat läge. Stoppa behållaren, ställ in offentligt: falskt i /config/config.js, starta den igen och skapa varje användare med: pct exec <CTID> - env THELOUNGE_HOME=/config s6-setuidgid abc thelounge addera <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "En inbyggd version av Borg är inte tillgänglig. Installera paketet borgbackup för värdens arkitektur för att använda Borg-säkerhetskopior.",
"Needed:": "Behövs:",
"Needs": "Kräver",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Varken /etc/kernel/cmdline eller /etc/default/grub hittades.",
@@ -4456,6 +4469,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.",
@@ -4647,6 +4661,7 @@
"No duplicate IP addresses": "Inga dubbletter av IP-adresser",
"No duplicate repositories found": "Inga dubbletter av arkiv hittades",
"No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.": "Inga lämpliga styrenhets-/NVMe-enheter återstår efter SR-IOV-filtreringen. Hoppar över.",
"No eligible USB devices found. Hubs, mass storage and network adapters are excluded.": "Inga lämpliga USB-enheter hittades. Hubbar, masslagringsenheter och nätverksadaptrar är undantagna.",
"No eligible controllers remain after SR-IOV filtering.": "Inga kvalificerade kontroller finns kvar efter SR-IOV-filtrering.",
"No encryption key is stored on this host. Choose how to set one up:": "Ingen krypteringsnyckel lagras på denna värd. Välj hur du ställer in en:",
"No exportable VM disks were found (CD-ROM/cloud-init are excluded).": "Inga exporterbara VM-diskar hittades (CD-ROM/cloud-init är undantagna).",
@@ -4731,6 +4746,7 @@
"No self-test log available for": "Ingen självtestlogg tillgänglig för",
"No server IP or hostname provided.": "Ingen server-IP eller värdnamn har angetts.",
"No share access test succeeded with guest authentication.": "Inget test av delad åtkomst lyckades med gästautentisering.",
"No shared devices are available on this server.": "Inga delade enheter är tillgängliga på denna server.",
"No shared media content.": "Inget delat medieinnehåll.",
"No shared mount detected. Applying standard local access.": "Inget delat fäste upptäcktes. Använder standard lokal åtkomst.",
"No shares configured.": "Inga delningar har konfigurerats.",
@@ -5129,6 +5145,7 @@
"Path of the Compose file": "Path of the Compose filen",
"Path of the oci-compose file": "Sökväg till oci-compose-filen",
"Path removed:": "Sökväg borttagen:",
"Path to the client SSH public key on this server:": "Sökväg till klientens offentliga SSH-nyckel på den här servern:",
"Path:": "Väg:",
"Paths applied:": "Tillämpade sökvägar:",
"Paths included in backup": "Sökvägar som ingår i säkerhetskopia",
@@ -5675,6 +5692,7 @@
"Remove Proxmox NFS storage:": "Ta bort Proxmox NFS-lagring:",
"Remove Proxmox iSCSI storage:": "Ta bort Proxmox iSCSI-lagring:",
"Remove Secure Gateway? State will be preserved.": "Ta bort Secure Gateway? Staten kommer att bevaras.",
"Remove a remote USB connection": "Ta bort en fjärransluten USB-anslutning",
"Remove custom paths": "Ta bort anpassade sökvägar",
"Remove disk references from affected VM(s)/CT(s) config": "Ta bort diskreferenser från berörda virtuella datorer/CT-konfigurationer",
"Remove every registered gasket DKMS version": "Ta bort alla registrerade gasket DKMS-versioner",
@@ -5695,6 +5713,7 @@
"Remove the lock left on a container by an interrupted task": "Ta bort låset som en avbruten uppgift lämnat på en container",
"Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Ta bort de sparade referenserna för {registry}?\nAppar som körs fortsätter att fungera.",
"Remove the unprivileged flag from configuration:": "Ta bort den oprivilegierade flaggan från konfigurationen:",
"Remove this connection and detach its USB device?": "Ta bort denna anslutning och koppla från dess USB-enhet?",
"Remove unused packages and their config": "Ta bort oanvända paket och deras konfiguration",
"Remove: delete the application and its containers": "Ta bort: ta bort programmet och dess behållare",
"Removed": "Borttagen",
@@ -5917,9 +5936,11 @@
"Run mode: Unattended": "Körläge: Oövervakad",
"Run now": "Kör nu",
"Run security audit now": "Kör säkerhetsgranskning nu",
"Run the client inside the Linux VM that needs the USB device. Install usbip, openssh-client and kmod, then copy usbip_manager.py to the same path as on the server. Configure key authentication and verify the server host key before adding a connection. The client uses the guest kernel and can reconnect after migration while the physical USB device stays on the server. Unprivileged LXC and Home Assistant OS need their own kernel/device or add-on setup. A physical device supports one attached client at a time.": "Kör klienten i den Linux-VM som behöver USB-enheten. Installera usbip, openssh-client och kmod och kopiera sedan usbip_manager.py till samma sökväg som på servern. Konfigurera nyckelautentisering och verifiera serverns värdnyckel innan du lägger till en anslutning. Klienten använder gästens kärna och kan återansluta efter migrering medan den fysiska USB-enheten stannar på servern. Oprivilegierade LXC och Home Assistant OS behöver en egen konfiguration av kärna, enheter eller tillägg. En fysisk enhet kan bara vara ansluten till en klient åt gången.",
"Run the following inside the VM:": "Kör följande inuti den virtuella datorn:",
"Run the release-channel switch from an SSH session or the Proxmox host console with:": "Kör release-kanalväxeln från en SSH-session eller Proxmox-värdkonsolen med:",
"Run the update from an SSH session or the Proxmox host console with:": "Kör uppdateringen från en SSH-session eller Proxmox-värdkonsolen med:",
"Run this menu as root.": "Kör denna meny som root.",
"Run this operation as root.": "Kör denna åtgärd som root.",
"Run upgrade checklist script:": "Kör skript för uppgraderingschecklista:",
"Running": "Spring",
@@ -6144,6 +6165,7 @@
"Select a new language for the menu:": "Välj ett nytt språk för menyn:",
"Select a post-installation script:": "Välj ett efterinstallationsskript:",
"Select a pre-configured Linux VM script to execute:": "Välj ett förkonfigurerat Linux VM-skript att köra:",
"Select a saved USB/IP device:": "Välj en sparad USB/IP-enhet:",
"Select a script or action:": "Välj ett skript eller en åtgärd:",
"Select a share to delete:": "Välj en delning att radera:",
"Select a specific Coral or USB node, not the whole /dev": "Välj en specifik Coral eller USB-nod, inte hela/dev",
@@ -6221,6 +6243,7 @@
"Select the Linux distribution to install:": "Välj Linux-distributionen att installera:",
"Select the NAS system to install:": "Välj det NAS-system som ska installeras:",
"Select the NVIDIA driver version to install:": "Välj NVIDIA-drivrutinsversionen att installera:",
"Select the USB device to share:": "Välj USB-enheten som ska delas:",
"Select the VM to add the GPU to:": "Välj den virtuella datorn för att lägga till GPU:n:",
"Select the VM to which you want to add disks:": "Välj den virtuella datorn som du vill lägga till diskar till:",
"Select the VM where you want to import the disk image:": "Välj den virtuella datorn där du vill importera skivavbildningen:",
@@ -6245,6 +6268,7 @@
"Select the parent job to attach to:": "Välj det överordnade jobbet som ska bifogas:",
"Select the paths to restore (one per backup entry):": "Välj sökvägarna som ska återställas (en per säkerhetskopia):",
"Select the privileged LXC container to convert:": "Välj den privilegierade LXC-behållaren att konvertera:",
"Select the remote USB device:": "Välj den fjärranslutna USB-enheten:",
"Select the storage that will receive the backups:": "Välj lagringsutrymmet som ska ta emot säkerhetskopiorna:",
"Select the storage volume for disk import:": "Välj lagringsvolym för diskimport:",
"Select the sync interval (in hours):": "Välj synkroniseringsintervall (i timmar):",
@@ -6331,6 +6355,7 @@
"Server IDs to test against, separated by commas (optional)": "Server-ID:n att testa mot, separerade med kommatecken (valfritt)",
"Server IP/hostname is correct": "Serverns IP/värdnamn är korrekt",
"Server IP:": "Server IP:",
"Server SSH port:": "Serverns SSH-port:",
"Server is accessible:": "Servern är tillgänglig:",
"Server is online": "Servern är online",
"Server is running": "Servern körs",
@@ -6392,6 +6417,7 @@
"Share Access Error": "Dela åtkomstfel",
"Share Name": "Dela namn",
"Share Options": "Delningsalternativ",
"Share a local USB device": "Dela en lokal USB-enhet",
"Share added successfully.": "Delningen har lagts till.",
"Share deleted and Samba service restarted.": "Delningen raderades och Samba-tjänsten startade om.",
"Share exists:": "Delning finns:",
@@ -6399,6 +6425,7 @@
"Share name:": "Dela namn:",
"Share not found on server:": "Delningen hittades inte på servern:",
"Share path:": "Dela sökväg:",
"Share selected USB devices through SSH, or connect this Linux machine to a USB/IP server.": "Dela valda USB-enheter via SSH eller anslut denna Linux-dator till en USB/IP-server.",
"Share updated successfully.": "Delningen har uppdaterats.",
"Share:": "Dela:",
"ShareCoveX panel": "ShareCoveX panel",
@@ -6667,6 +6694,8 @@
"Still has issues": "Har fortfarande problem",
"Stop - shuts the guest down during backup": "Stopp - stänger av gästen under säkerhetskopiering",
"Stop it first and run this option again.": "Stoppa det först och kör det här alternativet igen.",
"Stop sharing a USB device": "Sluta dela en USB-enhet",
"Stop sharing this device? An attached remote client will disconnect.": "Sluta dela denna enhet? En ansluten fjärrklient kommer att kopplas från.",
"Stop that VM, then choose:": "Stoppa den VM och välj sedan:",
"Stop the CT, unmount the disk on the HOST, and remount with:": "Stoppa CT, avmontera disken på HOST och montera om med:",
"Stop the VM/CT before formatting this disk.": "Stoppa VM/CT innan du formaterar denna disk.",
@@ -7022,6 +7051,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",
@@ -7303,6 +7333,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.",
@@ -7370,6 +7401,7 @@
"The selected Intel GPU has non-FLR reset support and unknown subtype": "Den valda Intel GPU:n har stöd för icke-FLR-återställning och okänd undertyp",
"The selected Intel GPU is currently in power state D3cold": "Den valda Intel GPU:n är för närvarande i strömtillstånd D3cold",
"The selected Intel render device does not exist:": "Den valda Intel render-enheten finns inte:",
"The selected USB device will become unavailable to local applications and guests. Only one remote client can use it at a time. Share this device?": "Den valda USB-enheten blir otillgänglig för lokala program och gäster. Bara en fjärrklient kan använda den åt gången. Dela denna enhet?",
"The selected VM": "Den valda virtuella datorn",
"The selected VM is running.": "Den valda virtuella datorn körs.",
"The selected base folder does not exist and could not be created:": "Den valda basmappen finns inte och kunde inte skapas:",
@@ -7709,6 +7741,9 @@
"USB drives mounted now:": "USB-enheter monterade nu:",
"USB libedgetpu1": "USB libedgetpu1",
"USB or serial device node": "USB eller seriell enhetsnod",
"USB/IP configuration updated. Open status to check the connection.": "USB/IP-konfigurationen har uppdaterats. Öppna status för att kontrollera anslutningen.",
"USB/IP over SSH": "USB/IP via SSH",
"USB/IP server IP address or DNS hostname:": "USB/IP-serverns IP-adress eller DNS-värdnamn:",
"UUP Dump script not found.": "UUP Dump-skript hittades inte.",
"UUp Dump ISO creator Custom": "UUp Dump ISO-skapare Anpassad",
"Ubooquity is a free, lightweight and easy-to-use home server for your comics and ebooks. Use it to access your files from anywhere, with a tablet, an e-reader, a phone or a computer.": "Ubooquity är en gratis, lätt och lättanvänd hemserver för dina serier och e-böcker. Använd den för att komma åt dina filer var som helst, med en surfplatta, en e-läsare, en telefon eller en dator.",
@@ -7865,6 +7900,7 @@
"Untrusted or modified NVIDIA hook": "Untrusted eller modifierad NVIDIA hook",
"Unused image removed from the cache:": "Oanvänd bild bort från cache:",
"Unused images removed from the cache:": "Oanvända bilder som tagits bort från cache:",
"Unused local loopback TCP port for this device:": "Ledig lokal TCP-port på loopback för denna enhet:",
"Update": "Uppdatering",
"Update Available": "Uppdatering tillgänglig",
"Update Ceph repository (Only if using Ceph):": "Uppdatera Ceph arkiv (endast om du använder Ceph):",
@@ -8148,6 +8184,7 @@
"View Disk Storages": "Visa disklagringar",
"View Mount Points": "Se Mount Points",
"View NFS Mounts (pvesm + fstab)": "Visa NFS-fästen (pvesm + fstab)",
"View USB/IP status": "Visa USB/IP-status",
"View backup metadata": "Visa metadata för säkerhetskopiering",
"View current paths": "Visa aktuella vägar",
"View existing folders in /mnt": "Visa befintliga mappar i /mnt",
+2
View File
@@ -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.
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+1 -1
View File
@@ -17,7 +17,7 @@
"category_label": "AI / Coding & Dev-Tools",
"author": "Nous Research",
"developer": "Nous Research",
"icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@main/webp/hermes.webp",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/hermes-agent.webp",
"thumbnail": null,
"screenshots": [],
"architectures": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+8 -8
View File
@@ -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"
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+8 -8
View File
@@ -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"
+115 -2
View File
@@ -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": [
+115 -2
View File
@@ -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": [
+2 -2
View File
@@ -6150,7 +6150,7 @@
"repository": "https://hub.docker.com/r/nousresearch/hermes-agent",
"description": "Hermes Agent is a self-improving AI agent from Nous Research, designed to be a long-running collaborator rather than a one-off chatbot.\nIts built-in learning loop combines persistent memory, cross-session recall, and reusable skills so it can keep context and improve recurring workflows over time.\nWith a unified gateway, rich toolsets, and a browser-based dashboard, Hermes fits research, automation, cross-platform communication, and ongoing operational work.\n\n**Main Features:**\n- Built-in learning loop with persistent memory, session search, and reusable skills\n- One gateway for CLI, Telegram, Discord, Slack, WhatsApp, Signal, and more\n- Tool-driven workflows with terminal access, web search, browser automation, file editing, and MCP integration\n- Automation and coordination with cron jobs, subagents, the web dashboard, and an OpenAI-compatible API\n\n**Learn More:**\n- [Hermes Agent Official Website](https://hermes-agent.nousresearch.com/)\n- [Hermes Agent GitHub](https://github.com/NousResearch/hermes-agent)\n- [Hermes Agent Documentation](https://hermes-agent.nousresearch.com/docs/)\n",
"website": "https://hermes-agent.nousresearch.com/",
"icon": "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@main/webp/hermes.webp",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/hermes-agent.webp",
"architectures": [
"amd64",
"arm64"
@@ -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": [],
+19 -6
View File
@@ -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())
+35 -10
View File
@@ -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
+9 -1
View File
@@ -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)
+76 -2
View File
@@ -88,21 +88,95 @@ 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):
"""Everything but the GID, which the user may change in Proxmox."""
return (fields.get('path'), int(fields.get('mode', '0660'), 8),
int(fields.get('uid', 0)), fields.get('deny-write', '0'))
def preserve_recorded_container_gids(record, candidate, config):
"""Keep the GID each device has inside the LXC, including one changed in
Proxmox since the last operation, like the other Proxmox changes.
This is not a host-to-container GID translation: the configured GID is the
group Proxmox gives the node inside the container. Other permission changes
are still refused, and the host device is verified 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 current or path not in old_snapshots:
continue
observed_gid = int(current[path].get('gid', 0))
if observed_gid != container_gid(device, old_snapshots[path]):
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 +195,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
+4 -3
View File
@@ -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')
+23 -7
View File
@@ -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:
+4 -1
View File
@@ -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
+136
View File
@@ -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())
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+3 -2
View File
@@ -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")
+23 -16
View File
@@ -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__":
+73 -11
View File
@@ -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']:
+6 -1
View File
@@ -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):
+5 -11
View File
@@ -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 "
+91
View File
@@ -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()
+33 -26
View File
@@ -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)
@@ -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()
+4 -2
View File
@@ -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))
+90
View File
@@ -0,0 +1,90 @@
"""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_a_guest_gid_changed_in_proxmox_is_kept(self, _snapshot):
record = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0], container_gid=44)]},
'observed': {'config': SAVED_CONFIG}}
candidate = {'deployment': {'devices': [dict(DEPLOYMENT['devices'][0])]}}
# Written from the Proxmox interface, without `path=`.
changed = b'dev0: /dev/dri/renderD128,mode=0660,deny-write=0,gid=45\n'
devices.preserve_recorded_container_gids(record, candidate, changed)
self.assertEqual(record['deployment']['devices'][0]['container_gid'], 45)
self.assertEqual(candidate['deployment']['devices'][0]['container_gid'], 45)
self.assertEqual(devices.check(changed, candidate['deployment'])['/dev/dri/renderD128'], HOST)
@patch.object(devices, 'snapshot', return_value=HOST)
def test_other_permission_changes_are_still_refused(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'mode=0660', b'mode=0666')
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()
+60 -3
View File
@@ -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()
+9 -4
View File
@@ -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):
+86
View File
@@ -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()
+181
View File
@@ -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()
+2 -1
View File
@@ -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)
+50
View File
@@ -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()

Some files were not shown because too many files have changed in this diff Show More