yum ) {} public static function for(OsProfile $os): self { $useYum = $os->packageManager === 'dnf' && ! $os->has('dnf') && $os->has('yum'); return new self($os->packageManager, $useYum); } private function dnf(): string { return $this->useYum ? 'yum' : 'dnf'; } /** * A command that prints `CLUSEV_OK` (only when the query actually ran) and * `CLUSEV_PENDING=`. Runs unprivileged (simulation / metadata read only). */ public function pendingScript(): string { return match ($this->manager) { 'apt' => 'out=$(apt-get -s upgrade 2>/dev/null) && echo CLUSEV_OK; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -c '^Inst ')\"", 'dnf' => 'out=$('.$this->dnf().' -q check-update 2>/dev/null); rc=$?; ' .'if [ "$rc" = 0 ] || [ "$rc" = 100 ]; then echo CLUSEV_OK; fi; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+')\"", 'zypper' => 'out=$(zypper --non-interactive list-updates 2>/dev/null); rc=$?; ' .'[ "$rc" = 0 ] && echo CLUSEV_OK; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^v \\|')\"", default => 'echo CLUSEV_PENDING=0', }; } /** * Like pendingScript, but ALSO emits `CLUSEV_SECURITY=` — the subset of pending upgrades that * come from a security source. One round-trip for both counts. Best-effort per family (the apt * path is solid; dnf/zypper security queries degrade to 0 when the metadata isn't present). */ public function countsScript(): string { return match ($this->manager) { 'apt' => 'out=$(apt-get -s upgrade 2>/dev/null) && echo CLUSEV_OK; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -c '^Inst ')\"; " ."echo \"CLUSEV_SECURITY=\$(printf '%s\\n' \"\$out\" | grep '^Inst ' | grep -ic 'securit')\"", 'dnf' => 'out=$('.$this->dnf().' -q check-update 2>/dev/null); rc=$?; ' .'if [ "$rc" = 0 ] || [ "$rc" = 100 ]; then echo CLUSEV_OK; fi; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+')\"; " .'echo "CLUSEV_SECURITY=$('.$this->dnf().' -q --security check-update 2>/dev/null | grep -cE \'^[[:graph:]]+[[:space:]]+[[:graph:]]+[[:space:]]+[[:graph:]]+\')"', 'zypper' => 'out=$(zypper --non-interactive list-updates 2>/dev/null); rc=$?; ' .'[ "$rc" = 0 ] && echo CLUSEV_OK; ' ."echo \"CLUSEV_PENDING=\$(printf '%s\\n' \"\$out\" | grep -cE '^v \\|')\"; " .'echo "CLUSEV_SECURITY=$(zypper --non-interactive list-patches --category security 2>/dev/null | grep -cE \'^[[:alnum:]]\')"', default => 'echo CLUSEV_PENDING=0; echo CLUSEV_SECURITY=0', }; } /** Refresh index + apply all upgrades (privileged, long-running). */ public function applyScript(): string { return match ($this->manager) { 'apt' => 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y upgrade', 'dnf' => $this->dnf().' -y upgrade', // zypper exits 102 (reboot required) / 103 (restart zypper) on SUCCESS — // normalize those to 0 so a successful upgrade is not reported as failed. 'zypper' => 'zypper --non-interactive refresh && { zypper --non-interactive update; rc=$?; ' .'[ "$rc" = 0 ] || [ "$rc" = 102 ] || [ "$rc" = 103 ]; }', default => 'false', }; } /** Install one package (privileged). */ public function installScript(string $pkg): string { return match ($this->manager) { 'apt' => 'DEBIAN_FRONTEND=noninteractive apt-get install -y '.$pkg, 'dnf' => $this->dnf().' install -y '.$pkg, 'zypper' => 'zypper --non-interactive install '.$pkg, default => 'false', }; } /** A shell CONDITION that exits 0 when $pkg is installed (for use with && / ||). */ public function isInstalledTest(string $pkg): string { return match ($this->manager) { 'apt' => "dpkg -l {$pkg} 2>/dev/null | grep -q '^ii'", 'dnf', 'zypper' => "rpm -q {$pkg} >/dev/null 2>&1", default => 'false', }; } /** Install $pkg only if missing (idempotent). */ public function ensureInstalledScript(string $pkg): string { return '('.$this->isInstalledTest($pkg).' || '.$this->installScript($pkg).')'; } }