Agent 1.21.0. The hourly update check now covers the package managers beyond apt and dnf (#5803 "The agent's update check reads pacman, zypper and yum"): - yum: `yum -C check-update`, `updateinfo list security`, `needs-restarting -r`, as dnf. - zypper: `--no-refresh list-updates` (`--dup` on Tumbleweed), needed security patches from `list-patches --category security`, and `zypper needs-rebooting`. - pacman: `checkupdates` from pacman-contrib, which install-deps.sh now installs. pacman has no security notion; a reboot is required once the running kernel's /usr/lib/modules/<release> is gone. The checks sit in one UPDATE_CHECKS table, and a failure names its manager. The root helper's update runs on the same hosts (#5804 "The scheduled update runs on pacman, zypper and yum hosts"): yum upgrade, zypper refresh + update (dup on Tumbleweed, with 102/103 counted as success), pacman -Syu. Each step carries a label so a failure says which one, and the reboot check reads zypper and the pacman kernel too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+157
-37
@@ -22,7 +22,7 @@ from collections import deque
|
||||
from concurrent.futures import ThreadPoolExecutor, wait as wait_futures
|
||||
from datetime import datetime, timezone
|
||||
|
||||
AGENT_VERSION = "1.20.1"
|
||||
AGENT_VERSION = "1.21.0"
|
||||
|
||||
# Default path to the local Docker Engine socket. Overridable via the
|
||||
# `docker_socket` config key; collection is silently skipped if it's absent or
|
||||
@@ -1308,18 +1308,21 @@ class RingBuffer:
|
||||
return len(self._dq)
|
||||
|
||||
|
||||
# ─── pending updates and reboots (#5765) ─────────────────────────────────────
|
||||
# ─── pending updates and reboots (#5765, #5803) ──────────────────────────────
|
||||
#
|
||||
# What the package manager has waiting, read once an hour in a background
|
||||
# thread so a slow apt never holds up a sample, and whether the host needs a
|
||||
# reboot, read every sample (a file test). Nothing here needs root or the
|
||||
# network: apt-check and `apt list` read the lists apt-daily already fetched,
|
||||
# and dnf runs from its cache (-C).
|
||||
# reboot, read every sample (a file test). Nothing here needs root. apt-check
|
||||
# and `apt list` read the lists apt-daily already fetched; dnf, yum and zypper
|
||||
# run from their caches (-C, --no-refresh). pacman has no cache to ask without
|
||||
# root, so checkupdates (pacman-contrib) syncs its own copy of the package
|
||||
# lists, in the agent's private /tmp, and does reach the mirrors.
|
||||
|
||||
UPDATES_INTERVAL_SECONDS = 3600.0
|
||||
UPDATES_DEADLINE_SECONDS = 120.0
|
||||
REBOOT_REQUIRED = "/var/run/reboot-required"
|
||||
REBOOT_PACKAGES_MAX = 20
|
||||
KERNEL_MODULES = "/usr/lib/modules"
|
||||
|
||||
|
||||
def _run(cmd: list, deadline: float = UPDATES_DEADLINE_SECONDS):
|
||||
@@ -1332,6 +1335,11 @@ def _run(cmd: list, deadline: float = UPDATES_DEADLINE_SECONDS):
|
||||
return done.returncode, done.stdout, done.stderr
|
||||
|
||||
|
||||
def _failed(manager: str, got, what: str, at: str) -> dict:
|
||||
return {"manager": manager, "error": ((got[2] or got[1]) or what + " failed").strip()[:200],
|
||||
"checked_at": at}
|
||||
|
||||
|
||||
def parse_apt_check(stderr: str):
|
||||
"""apt-check writes "<pending>;<security>" to stderr; (pending, security) or None."""
|
||||
m = re.search(r"(\d+);(\d+)", stderr or "")
|
||||
@@ -1354,47 +1362,153 @@ def parse_apt_list(stdout: str) -> tuple:
|
||||
|
||||
|
||||
def parse_dnf_check_update(stdout: str) -> int:
|
||||
"""Packages `dnf check-update` lists, up to its "Obsoleting Packages" part."""
|
||||
"""Packages `dnf check-update` (or yum's) lists, up to its "Obsoleting
|
||||
Packages" part. yum also prints "Loaded plugins:" lines first."""
|
||||
count = 0
|
||||
for line in (stdout or "").splitlines():
|
||||
if line.startswith("Obsoleting"):
|
||||
break
|
||||
if line.strip() and not line.startswith((" ", "Last metadata")):
|
||||
if line.strip() and not line.startswith((" ", "Last metadata", "Loaded plugins",
|
||||
"Security:")):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def parse_checkupdates(stdout: str) -> int:
|
||||
"""Packages pacman-contrib's checkupdates lists: "name old -> new"."""
|
||||
return sum(1 for line in (stdout or "").splitlines() if " -> " in line)
|
||||
|
||||
|
||||
def _zypper_rows(stdout: str) -> list:
|
||||
"""The data rows of a zypper table: lines with "|" after the "---+---" rule."""
|
||||
rows, seen_rule = [], False
|
||||
for line in (stdout or "").splitlines():
|
||||
if re.match(r"^-+\+[-+]+$", line.strip()):
|
||||
seen_rule = True
|
||||
elif seen_rule and "|" in line:
|
||||
rows.append([c.strip() for c in line.split("|")])
|
||||
return rows
|
||||
|
||||
|
||||
def parse_zypper_updates(stdout: str) -> int:
|
||||
"""Packages `zypper list-updates` lists (one table row each)."""
|
||||
return len(_zypper_rows(stdout))
|
||||
|
||||
|
||||
def parse_zypper_patches(stdout: str) -> int:
|
||||
"""Security patches `zypper list-patches --category security` says are
|
||||
still needed (its Status column), not those already applied."""
|
||||
return sum(1 for row in _zypper_rows(stdout) if "needed" in row)
|
||||
|
||||
|
||||
def _os_release_id(path: str = "/etc/os-release") -> str:
|
||||
try:
|
||||
with open(path) as fh:
|
||||
for line in fh:
|
||||
if line.startswith("ID="):
|
||||
return line.split("=", 1)[1].strip().strip('"')
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _check_apt(at: str):
|
||||
got = _run(["/usr/lib/update-notifier/apt-check"])
|
||||
if got is not None:
|
||||
counts = parse_apt_check(got[2])
|
||||
if counts:
|
||||
return {"manager": "apt", "pending": counts[0], "security": counts[1], "checked_at": at}
|
||||
got = _run(["apt", "list", "--upgradable"])
|
||||
if got is not None and got[0] == 0:
|
||||
pending, security = parse_apt_list(got[1])
|
||||
return {"manager": "apt", "pending": pending, "security": security, "checked_at": at}
|
||||
return None
|
||||
|
||||
|
||||
def _needs_restarting(out: dict) -> dict:
|
||||
"""dnf and yum hosts: needs-restarting -r exits 1 when a reboot is needed."""
|
||||
restart = _run(["needs-restarting", "-r"])
|
||||
if restart is not None and restart[0] in (0, 1):
|
||||
out["reboot_required"] = restart[0] == 1
|
||||
return out
|
||||
|
||||
|
||||
def _check_dnf_like(manager: str, at: str):
|
||||
"""dnf, and yum where there is no dnf: the same output and exit codes."""
|
||||
got = _run([manager, "-q", "-C", "check-update"])
|
||||
if got is None:
|
||||
return None
|
||||
if got[0] not in (0, 100): # 100: updates are waiting
|
||||
return _failed(manager, got, manager + " check-update", at)
|
||||
out = {"manager": manager, "pending": parse_dnf_check_update(got[1]),
|
||||
"security": None, "checked_at": at}
|
||||
sec = _run([manager, "-q", "-C", "updateinfo", "list", "--security"] if manager == "dnf"
|
||||
else [manager, "-q", "-C", "updateinfo", "list", "security"])
|
||||
if sec is not None and sec[0] == 0:
|
||||
out["security"] = len({ln.split()[-1] for ln in sec[1].splitlines()
|
||||
if ln.strip() and not ln.startswith("Loaded plugins")})
|
||||
return _needs_restarting(out)
|
||||
|
||||
|
||||
def _check_zypper(at: str):
|
||||
"""openSUSE / SLES. Tumbleweed upgrades with dup, so its pending list is
|
||||
dup's, and it has no patches to call security."""
|
||||
tumbleweed = _os_release_id() == "opensuse-tumbleweed"
|
||||
got = _run(["zypper", "--no-refresh", "-q", "list-updates"] + (["--dup"] if tumbleweed else []))
|
||||
if got is None:
|
||||
return None
|
||||
if got[0] != 0:
|
||||
return _failed("zypper", got, "zypper list-updates", at)
|
||||
out = {"manager": "zypper", "pending": parse_zypper_updates(got[1]), "security": None,
|
||||
"checked_at": at}
|
||||
if not tumbleweed:
|
||||
sec = _run(["zypper", "--no-refresh", "-q", "list-patches", "--category", "security"])
|
||||
if sec is not None and sec[0] in (0, 100, 101):
|
||||
out["security"] = parse_zypper_patches(sec[1])
|
||||
restart = _run(["zypper", "needs-rebooting"])
|
||||
if restart is not None and restart[0] in (0, 102): # 102: a reboot is needed
|
||||
out["reboot_required"] = restart[0] == 102
|
||||
return out
|
||||
|
||||
|
||||
def _check_pacman(at: str):
|
||||
"""Arch and its family (CachyOS, Manjaro, EndeavourOS). pacman keeps no
|
||||
security list, so security is None. Without pacman-contrib there is no
|
||||
safe check: `pacman -Sy` needs root and leaves a partial upgrade."""
|
||||
if shutil.which("pacman") is None:
|
||||
return None
|
||||
got = _run(["checkupdates"])
|
||||
if got is None:
|
||||
return {"manager": "pacman", "error": "checkupdates isn't installed (pacman-contrib)",
|
||||
"checked_at": at}
|
||||
if got[0] not in (0, 2): # 2: nothing to update
|
||||
return _failed("pacman", got, "checkupdates", at)
|
||||
return {"manager": "pacman", "pending": parse_checkupdates(got[1]), "security": None,
|
||||
"checked_at": at}
|
||||
|
||||
|
||||
# Asked in this order; the first that answers is the host's. dnf comes before
|
||||
# yum because dnf hosts often keep a yum alias that is dnf underneath.
|
||||
UPDATE_CHECKS = (
|
||||
("apt", _check_apt),
|
||||
("dnf", lambda at: _check_dnf_like("dnf", at)),
|
||||
("yum", lambda at: _check_dnf_like("yum", at)),
|
||||
("zypper", _check_zypper),
|
||||
("pacman", _check_pacman),
|
||||
)
|
||||
|
||||
|
||||
def collect_updates() -> dict:
|
||||
"""{"manager", "pending", "security", "checked_at"} (security None when the
|
||||
manager can't say), {"error"} when the check failed, or {} on a host with
|
||||
no package manager we know. dnf hosts also say whether a reboot is
|
||||
required, from needs-restarting."""
|
||||
no package manager we know. dnf, yum and zypper hosts also say whether a
|
||||
reboot is required."""
|
||||
at = _now_iso()
|
||||
try:
|
||||
got = _run(["/usr/lib/update-notifier/apt-check"])
|
||||
if got is not None:
|
||||
counts = parse_apt_check(got[2])
|
||||
if counts:
|
||||
return {"manager": "apt", "pending": counts[0], "security": counts[1],
|
||||
"checked_at": at}
|
||||
got = _run(["apt", "list", "--upgradable"])
|
||||
if got is not None and got[0] == 0:
|
||||
pending, security = parse_apt_list(got[1])
|
||||
return {"manager": "apt", "pending": pending, "security": security, "checked_at": at}
|
||||
got = _run(["dnf", "-q", "-C", "check-update"])
|
||||
if got is not None:
|
||||
if got[0] not in (0, 100): # 100: updates are waiting
|
||||
return {"manager": "dnf", "error": (got[2] or "dnf check-update failed").strip()[:200],
|
||||
"checked_at": at}
|
||||
out = {"manager": "dnf", "pending": parse_dnf_check_update(got[1]),
|
||||
"security": None, "checked_at": at}
|
||||
sec = _run(["dnf", "-q", "-C", "updateinfo", "list", "--security"])
|
||||
if sec is not None and sec[0] == 0:
|
||||
out["security"] = len({ln.split()[-1] for ln in sec[1].splitlines() if ln.strip()})
|
||||
restart = _run(["needs-restarting", "-r"])
|
||||
if restart is not None and restart[0] in (0, 1):
|
||||
out["reboot_required"] = restart[0] == 1
|
||||
return out
|
||||
for _name, check in UPDATE_CHECKS:
|
||||
got = check(at)
|
||||
if got is not None:
|
||||
return got
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"error": "the package manager didn't answer in %.0f s" % UPDATES_DEADLINE_SECONDS,
|
||||
"checked_at": at}
|
||||
@@ -1428,13 +1542,19 @@ def refresh_updates(state: dict, now_mono: float | None = None) -> None:
|
||||
threading.Thread(target=work, name="updates", daemon=True).start()
|
||||
|
||||
|
||||
def collect_reboot(path: str = REBOOT_REQUIRED):
|
||||
"""{"required", "since", "packages"} from Debian's reboot-required marker,
|
||||
or None where there is no such marker system (no apt) to ask."""
|
||||
def collect_reboot(path: str = REBOOT_REQUIRED, modules: str = KERNEL_MODULES):
|
||||
"""{"required", "since", "packages"} from Debian's reboot-required marker;
|
||||
on pacman hosts, whether the running kernel's modules are gone (an upgrade
|
||||
replaced the kernel); None where neither can be asked (dnf, yum and zypper
|
||||
say it with the hourly update check)."""
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except FileNotFoundError:
|
||||
return {"required": False} if shutil.which("apt-get") else None
|
||||
if shutil.which("apt-get"):
|
||||
return {"required": False}
|
||||
if shutil.which("pacman"):
|
||||
return {"required": not os.path.isdir(os.path.join(modules, os.uname().release))}
|
||||
return None
|
||||
packages: list = []
|
||||
try:
|
||||
with open(path + ".pkgs") as fh:
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
# and the host_agent playbooks: one copy of the list.
|
||||
#
|
||||
# smartmontools gives the helper smartctl, which reads each drive's health.
|
||||
# On pacman hosts, pacman-contrib gives the agent checkupdates, the one safe
|
||||
# way to see pending updates without root (#5803).
|
||||
# A package that can't be installed is reported, never fatal: the agent runs
|
||||
# without it and the host page says what is missing.
|
||||
#
|
||||
# Recommended / weak dependencies are skipped: on Debian, smartmontools
|
||||
# recommends a mail server.
|
||||
|
||||
PACKAGES="smartmontools"
|
||||
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
@@ -39,12 +40,15 @@ install_with() {
|
||||
|
||||
missing=""
|
||||
have smartctl || missing="$missing smartmontools"
|
||||
[ -n "$missing" ] || { echo "dependencies present: $PACKAGES"; exit 0; }
|
||||
if have pacman && ! have checkupdates; then
|
||||
missing="$missing pacman-contrib"
|
||||
fi
|
||||
[ -n "$missing" ] || { echo "dependencies present"; exit 0; }
|
||||
|
||||
# shellcheck disable=SC2086 # one word per package
|
||||
if install_with $missing; then
|
||||
echo "installed:$missing"
|
||||
else
|
||||
echo "WARNING: could not install$missing; drive health will read \"smartctl not installed\" until it is"
|
||||
echo "WARNING: could not install$missing; until it is, drive health reads \"smartctl not installed\" (smartmontools) and the update check reads \"checkupdates isn't installed\" (pacman-contrib)"
|
||||
fi
|
||||
exit 0
|
||||
|
||||
@@ -76,8 +76,10 @@ def valid(req) -> bool:
|
||||
return node is None or (isinstance(node, str) and bool(_NODE.match(node)))
|
||||
|
||||
|
||||
def run(cmd: list, deadline: int = COMMAND_DEADLINE_SECONDS, env: dict | None = None):
|
||||
"""(ok, output tail) of a command, bounded by `deadline`."""
|
||||
def run(cmd: list, deadline: int = COMMAND_DEADLINE_SECONDS, env: dict | None = None,
|
||||
ok_codes: tuple = (0,)):
|
||||
"""(ok, output tail) of a command, bounded by `deadline`. `ok_codes` are
|
||||
the exit codes that mean it worked (zypper says more than 0)."""
|
||||
try:
|
||||
done = subprocess.run(cmd, capture_output=True, text=True, timeout=deadline,
|
||||
env=dict(os.environ, LANG="C", LC_ALL="C", **(env or {})))
|
||||
@@ -86,55 +88,122 @@ def run(cmd: list, deadline: int = COMMAND_DEADLINE_SECONDS, env: dict | None =
|
||||
except OSError as exc:
|
||||
return False, str(exc)
|
||||
out = (done.stdout or "") + (done.stderr or "")
|
||||
return done.returncode == 0, out[-OUTPUT_KEPT_CHARS:]
|
||||
return done.returncode in ok_codes, out[-OUTPUT_KEPT_CHARS:]
|
||||
|
||||
|
||||
REBOOT_MARKER = "/var/run/reboot-required" # Debian and Ubuntu
|
||||
KERNEL_MODULES = "/usr/lib/modules"
|
||||
# zypper's own exit codes for "done, and": 102 a reboot is needed, 103 zypper
|
||||
# updated itself and would run again (its next run picks up the rest).
|
||||
ZYPPER_OK = (0, 102, 103)
|
||||
|
||||
|
||||
def reboot_required() -> bool | None:
|
||||
if os.path.exists("/var/run/reboot-required"):
|
||||
"""Whether the host needs a reboot after an update, asked the way its
|
||||
package manager says it; None when it can't say."""
|
||||
if os.path.exists(REBOOT_MARKER):
|
||||
return True
|
||||
if shutil.which("apt-get"):
|
||||
return False
|
||||
if shutil.which("needs-restarting"):
|
||||
try:
|
||||
try:
|
||||
if shutil.which("needs-restarting"): # dnf, yum
|
||||
return subprocess.run(["needs-restarting", "-r"], capture_output=True,
|
||||
timeout=60).returncode == 1
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if shutil.which("zypper"):
|
||||
code = subprocess.run(["zypper", "needs-rebooting"], capture_output=True,
|
||||
timeout=60).returncode
|
||||
return {0: False, 102: True}.get(code)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if shutil.which("pacman"):
|
||||
# An upgraded kernel removes the running one's modules.
|
||||
return not os.path.isdir(os.path.join(KERNEL_MODULES, os.uname().release))
|
||||
return None
|
||||
|
||||
|
||||
def upgraded_count(output: str) -> int | None:
|
||||
"""Packages an apt or dnf run changed, from its summary line."""
|
||||
"""Packages an update changed, from the manager's summary line."""
|
||||
m = re.search(r"(\d+) upgraded, (\d+) newly installed", output) # apt
|
||||
if m:
|
||||
return int(m.group(1)) + int(m.group(2))
|
||||
counts = re.findall(r"^\s*(?:Upgrade|Install)\s+(\d+) Packages?", output, re.MULTILINE) # dnf
|
||||
m = re.search(r"(\d+) packages? to upgrade(?:, (\d+) new)?", output) # zypper
|
||||
if m:
|
||||
return int(m.group(1)) + int(m.group(2) or 0)
|
||||
m = re.search(r"^Packages \((\d+)\)", output, re.MULTILINE) # pacman
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
counts = re.findall(r"^\s*(?:Upgrade|Install)\s+(\d+) Packages?", output, re.MULTILINE) # dnf, yum
|
||||
return sum(int(c) for c in counts) if counts else None
|
||||
|
||||
|
||||
def do_update() -> dict:
|
||||
"""Upgrade every package, keeping existing config files and leaving
|
||||
service restarts to a reboot: needrestart only lists, so the Docker daemon
|
||||
isn't restarted under its containers mid-upgrade."""
|
||||
if shutil.which("apt-get"):
|
||||
def _os_release_id(path: str = "/etc/os-release") -> str:
|
||||
try:
|
||||
with open(path) as fh:
|
||||
for line in fh:
|
||||
if line.startswith("ID="):
|
||||
return line.split("=", 1)[1].strip().strip('"')
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def update_steps(manager: str) -> list:
|
||||
"""The commands an update runs on a host with `manager`, in order: each
|
||||
(what it is, command, deadline, env, ok exit codes). The last one is the upgrade
|
||||
whose output is counted. Existing config files are kept, and service
|
||||
restarts are left to a reboot: needrestart only lists, so the Docker
|
||||
daemon isn't restarted under its containers mid-upgrade."""
|
||||
long = UPDATE_DEADLINE_SECONDS
|
||||
if manager == "apt":
|
||||
env = {"DEBIAN_FRONTEND": "noninteractive", "NEEDRESTART_MODE": "l"}
|
||||
ok, out = run(["apt-get", "update"], env=env)
|
||||
return [("apt-get update", ["apt-get", "update"], COMMAND_DEADLINE_SECONDS, env, (0,)),
|
||||
("apt-get dist-upgrade", ["apt-get", "-y", "-o", "Dpkg::Options::=--force-confdef",
|
||||
"-o", "Dpkg::Options::=--force-confold", "dist-upgrade"],
|
||||
long, env, (0,))]
|
||||
if manager == "dnf":
|
||||
return [("dnf upgrade", ["dnf", "-y", "upgrade", "--refresh"], long, None, (0,))]
|
||||
if manager == "yum":
|
||||
return [("yum update", ["yum", "-y", "update"], long, None, (0,))]
|
||||
if manager == "zypper":
|
||||
# Tumbleweed is a rolling release: it upgrades with dup, not update.
|
||||
upgrade = "dup" if _os_release_id() == "opensuse-tumbleweed" else "update"
|
||||
return [("zypper refresh", ["zypper", "--non-interactive", "refresh"],
|
||||
COMMAND_DEADLINE_SECONDS, None, ZYPPER_OK),
|
||||
("zypper " + upgrade, ["zypper", "--non-interactive", upgrade, "--auto-agree-with-licenses"],
|
||||
long, None, ZYPPER_OK)]
|
||||
if manager == "pacman":
|
||||
# Packages from the AUR aren't updated: no AUR helper runs as root.
|
||||
return [("pacman -Syu", ["pacman", "-Syu", "--noconfirm"], long, None, (0,))]
|
||||
return []
|
||||
|
||||
|
||||
# Which manager a host has; dnf before yum, since dnf hosts often keep a yum
|
||||
# that is dnf underneath.
|
||||
UPDATE_MANAGERS = (("apt", "apt-get"), ("dnf", "dnf"), ("yum", "yum"), ("zypper", "zypper"),
|
||||
("pacman", "pacman"))
|
||||
|
||||
|
||||
def host_manager() -> str | None:
|
||||
for manager, binary in UPDATE_MANAGERS:
|
||||
if shutil.which(binary):
|
||||
return manager
|
||||
return None
|
||||
|
||||
|
||||
def do_update() -> dict:
|
||||
"""Upgrade every package with whichever package manager the host has."""
|
||||
manager = host_manager()
|
||||
if manager is None:
|
||||
return {"ok": False, "detail": "no package manager this helper knows "
|
||||
"(apt, dnf, yum, zypper, pacman) on this host"}
|
||||
out = ""
|
||||
for label, cmd, deadline, env, ok_codes in update_steps(manager):
|
||||
ok, out = run(cmd, deadline=deadline, env=env, ok_codes=ok_codes)
|
||||
if not ok:
|
||||
return {"ok": False, "detail": "apt-get update failed: " + out}
|
||||
ok, out = run(["apt-get", "-y", "-o", "Dpkg::Options::=--force-confdef",
|
||||
"-o", "Dpkg::Options::=--force-confold", "dist-upgrade"],
|
||||
deadline=UPDATE_DEADLINE_SECONDS, env=env)
|
||||
if not ok:
|
||||
return {"ok": False, "detail": "apt-get dist-upgrade failed: " + out}
|
||||
changed = upgraded_count(out)
|
||||
run(["apt-get", "-y", "autoremove"], env=env)
|
||||
elif shutil.which("dnf"):
|
||||
ok, out = run(["dnf", "-y", "upgrade", "--refresh"], deadline=UPDATE_DEADLINE_SECONDS)
|
||||
if not ok:
|
||||
return {"ok": False, "detail": "dnf upgrade failed: " + out}
|
||||
changed = upgraded_count(out)
|
||||
else:
|
||||
return {"ok": False, "detail": "no apt-get or dnf on this host"}
|
||||
return {"ok": False, "detail": f"{label} failed: " + out}
|
||||
changed = upgraded_count(out)
|
||||
if manager == "apt":
|
||||
run(["apt-get", "-y", "autoremove"], env={"DEBIAN_FRONTEND": "noninteractive"})
|
||||
return {"ok": True, "packages": changed, "reboot_required": reboot_required(),
|
||||
"detail": out[-500:]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user