mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
202 lines
5.9 KiB
Python
202 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Update only the Docker Engine stack inside one Proxmox LXC.
|
||
|
|
|
||
|
|
This intentionally does not run the community-scripts Docker updater:
|
||
|
|
that updater also performs a full apt/apk upgrade. ProxMenux resolves a
|
||
|
|
small allow-list of Docker packages that are already installed and asks the
|
||
|
|
guest package manager to upgrade only those packages (and required
|
||
|
|
dependencies). Static/manual installations without a supported package
|
||
|
|
manager fail closed instead of guessing how they were installed.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
|
||
|
|
APT_PACKAGES = (
|
||
|
|
"docker-ce",
|
||
|
|
"docker-ce-cli",
|
||
|
|
"docker-ce-rootless-extras",
|
||
|
|
"docker-buildx-plugin",
|
||
|
|
"docker-compose-plugin",
|
||
|
|
"docker-model-plugin",
|
||
|
|
"containerd.io",
|
||
|
|
"docker.io",
|
||
|
|
"docker-compose-v2",
|
||
|
|
"docker-compose",
|
||
|
|
"docker-buildx",
|
||
|
|
"docker-cli",
|
||
|
|
"containerd",
|
||
|
|
"runc",
|
||
|
|
"moby-engine",
|
||
|
|
"moby-cli",
|
||
|
|
"moby-buildx",
|
||
|
|
"moby-compose",
|
||
|
|
"moby-containerd",
|
||
|
|
)
|
||
|
|
|
||
|
|
APK_PACKAGES = (
|
||
|
|
"docker",
|
||
|
|
"docker-cli",
|
||
|
|
"docker-openrc",
|
||
|
|
"docker-cli-buildx",
|
||
|
|
"docker-cli-compose",
|
||
|
|
"docker-compose",
|
||
|
|
"containerd",
|
||
|
|
"runc",
|
||
|
|
)
|
||
|
|
|
||
|
|
RPM_PACKAGES = (
|
||
|
|
"docker-ce",
|
||
|
|
"docker-ce-cli",
|
||
|
|
"docker-ce-rootless-extras",
|
||
|
|
"docker-buildx-plugin",
|
||
|
|
"docker-compose-plugin",
|
||
|
|
"containerd.io",
|
||
|
|
"moby-engine",
|
||
|
|
"moby-cli",
|
||
|
|
"moby-buildx",
|
||
|
|
"moby-compose",
|
||
|
|
"moby-containerd",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def pct(vmid: int, argv: list[str], *, capture: bool = False) -> subprocess.CompletedProcess[str]:
|
||
|
|
return subprocess.run(
|
||
|
|
["/usr/sbin/pct", "exec", str(vmid), "--", *argv],
|
||
|
|
text=True,
|
||
|
|
capture_output=capture,
|
||
|
|
check=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def command_exists(vmid: int, name: str) -> bool:
|
||
|
|
return pct(vmid, ["sh", "-c", f"command -v {name} >/dev/null 2>&1"]).returncode == 0
|
||
|
|
|
||
|
|
|
||
|
|
def docker_version(vmid: int) -> str:
|
||
|
|
result = pct(vmid, ["docker", "version", "--format", "{{.Server.Version}}"], capture=True)
|
||
|
|
return result.stdout.strip() if result.returncode == 0 else ""
|
||
|
|
|
||
|
|
|
||
|
|
def apt_installed(vmid: int) -> list[str]:
|
||
|
|
installed: list[str] = []
|
||
|
|
for package in APT_PACKAGES:
|
||
|
|
result = pct(
|
||
|
|
vmid,
|
||
|
|
["dpkg-query", "-W", "-f=${db:Status-Abbrev}", package],
|
||
|
|
capture=True,
|
||
|
|
)
|
||
|
|
if result.returncode == 0 and result.stdout.startswith("ii"):
|
||
|
|
installed.append(package)
|
||
|
|
return installed
|
||
|
|
|
||
|
|
|
||
|
|
def apk_installed(vmid: int) -> list[str]:
|
||
|
|
return [package for package in APK_PACKAGES if pct(vmid, ["apk", "info", "-e", package], capture=True).returncode == 0]
|
||
|
|
|
||
|
|
|
||
|
|
def rpm_installed(vmid: int) -> list[str]:
|
||
|
|
return [package for package in RPM_PACKAGES if pct(vmid, ["rpm", "-q", package], capture=True).returncode == 0]
|
||
|
|
|
||
|
|
|
||
|
|
def resolve_method(vmid: int) -> tuple[str, list[str]]:
|
||
|
|
if command_exists(vmid, "apt-get") and command_exists(vmid, "dpkg-query"):
|
||
|
|
return "apt", apt_installed(vmid)
|
||
|
|
if command_exists(vmid, "apk"):
|
||
|
|
return "apk", apk_installed(vmid)
|
||
|
|
if command_exists(vmid, "dnf") and command_exists(vmid, "rpm"):
|
||
|
|
return "dnf", rpm_installed(vmid)
|
||
|
|
if command_exists(vmid, "snap") and pct(vmid, ["snap", "list", "docker"], capture=True).returncode == 0:
|
||
|
|
return "snap", ["docker"]
|
||
|
|
return "unsupported", []
|
||
|
|
|
||
|
|
|
||
|
|
def run_update(vmid: int, method: str, packages: list[str]) -> int:
|
||
|
|
if method == "apt":
|
||
|
|
if pct(vmid, ["apt-get", "update"]).returncode != 0:
|
||
|
|
return 1
|
||
|
|
return pct(
|
||
|
|
vmid,
|
||
|
|
[
|
||
|
|
"env",
|
||
|
|
"DEBIAN_FRONTEND=noninteractive",
|
||
|
|
"apt-get",
|
||
|
|
"-y",
|
||
|
|
"-o",
|
||
|
|
"Dpkg::Options::=--force-confold",
|
||
|
|
"install",
|
||
|
|
"--only-upgrade",
|
||
|
|
*packages,
|
||
|
|
],
|
||
|
|
).returncode
|
||
|
|
if method == "apk":
|
||
|
|
if pct(vmid, ["apk", "update"]).returncode != 0:
|
||
|
|
return 1
|
||
|
|
return pct(vmid, ["apk", "upgrade", "--no-cache", *packages]).returncode
|
||
|
|
if method == "dnf":
|
||
|
|
return pct(vmid, ["dnf", "-y", "upgrade", *packages]).returncode
|
||
|
|
if method == "snap":
|
||
|
|
return pct(vmid, ["snap", "refresh", "docker"]).returncode
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--vmid", required=True, type=int)
|
||
|
|
parser.add_argument("--dry-run", action="store_true")
|
||
|
|
args = parser.parse_args()
|
||
|
|
if args.vmid <= 0:
|
||
|
|
parser.error("--vmid must be a positive integer")
|
||
|
|
|
||
|
|
before = docker_version(args.vmid)
|
||
|
|
if not before:
|
||
|
|
print("ERROR: Docker Engine is not running or was not detected in this container.", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
|
||
|
|
method, packages = resolve_method(args.vmid)
|
||
|
|
if method == "unsupported" or not packages:
|
||
|
|
print(
|
||
|
|
"ERROR: Docker was detected, but no supported packaged installation was found. "
|
||
|
|
"Configure a custom update command for this installation.",
|
||
|
|
file=sys.stderr,
|
||
|
|
)
|
||
|
|
return 3
|
||
|
|
|
||
|
|
print(f"Docker Engine before: {before}")
|
||
|
|
print(f"Update method: {method}")
|
||
|
|
print("Installed Docker stack: " + ", ".join(packages))
|
||
|
|
if args.dry_run:
|
||
|
|
print("Dry run: no packages were changed.")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
print("--- Updating only the installed Docker Engine stack ---")
|
||
|
|
if run_update(args.vmid, method, packages) != 0:
|
||
|
|
print("ERROR: the Docker package update failed.", file=sys.stderr)
|
||
|
|
return 4
|
||
|
|
|
||
|
|
after = ""
|
||
|
|
for _ in range(12):
|
||
|
|
after = docker_version(args.vmid)
|
||
|
|
if after:
|
||
|
|
break
|
||
|
|
time.sleep(1)
|
||
|
|
if not after:
|
||
|
|
print("ERROR: Docker did not become available again after the package update.", file=sys.stderr)
|
||
|
|
return 5
|
||
|
|
|
||
|
|
print(f"Docker Engine after: {after}")
|
||
|
|
if after == before:
|
||
|
|
print("Docker Engine was already at the newest package version available.")
|
||
|
|
else:
|
||
|
|
print("Docker Engine updated successfully.")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|