118 lines
5.4 KiB
PHP
118 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Os;
|
|
|
|
/**
|
|
* Builds SHELL COMMAND STRINGS for package operations, per manager. It never runs
|
|
* anything — FleetService is the single executor (base64-wrapped, sudo + LC_ALL=C
|
|
* handled there). Only apt/dnf/zypper are driven this release; package names passed
|
|
* in are Clusev constants (fail2ban, ufw, …), never operator input.
|
|
*
|
|
* Note: the dnf/zypper command strings are constructed from documented syntax but
|
|
* are exercised live only on the families a tester actually runs; the apt path is
|
|
* the one verified against the live fleet today.
|
|
*/
|
|
final class PackageManager
|
|
{
|
|
/** @param 'apt'|'dnf'|'zypper'|'none' $manager */
|
|
public function __construct(
|
|
private string $manager,
|
|
private bool $useYum = false, // dnf family lacking the dnf binary -> 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=<n>`. 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=<m>` — 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).')';
|
|
}
|
|
}
|