the Proxmox suite and release key that go with it. * * `keyring` is the filename upstream's own install guide for that release * uses; `key_url` is the URL from that same guide. Both are kept per release * because Proxmox signs each suite with its own key and renamed the file * between PVE 8 and PVE 9 — one shared name would eventually be pointed at * the wrong key without anything looking wrong. * * Bullseye/PVE 7 is deliberately absent: onboarding a Debian 11 machine * today means somebody picked the wrong image, and the loud failure below is * a better answer than installing a hypervisor that is out of support. * * @var array */ private const RELEASES = [ 'bookworm' => [ 'suite' => 'bookworm', 'key_url' => 'https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg', 'keyring' => 'proxmox-release-bookworm.gpg', ], 'trixie' => [ 'suite' => 'trixie', 'key_url' => 'https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg', 'keyring' => 'proxmox-archive-keyring.gpg', ], ]; /** * Keys go here and are named by `Signed-By`, not into * /etc/apt/trusted.gpg.d/. A key in trusted.gpg.d is trusted for EVERY * repository on the machine, so the Proxmox key would also vouch for * anything else that later appears in sources.list — which is why apt has * been warning about that directory for several releases. */ private const KEYRING_DIR = '/usr/share/keyrings'; private const SOURCES_FILE = '/etc/apt/sources.list.d/pve-install-repo.sources'; /** What this step wrote before it used deb822; removed so the suite is stated once. */ private const LEGACY_SOURCES_FILE = '/etc/apt/sources.list.d/pve-install-repo.list'; public function __construct(private RemoteShell $shell) {} public function key(): string { return 'install_proxmox_ve'; } public function maxDuration(): int { return 1800; } public function execute(ProvisioningRun $run): StepResult { $host = $this->host($run); $this->keyLogin($this->shell, $host); // Idempotent: proxmox-ve already installed. if ($this->shell->run('dpkg -l proxmox-ve 2>/dev/null | grep -q "^ii"')->ok()) { return StepResult::advance(); } [$id, $codename, $prettyName] = $this->readOsRelease(); if ($id !== 'debian' || ! isset(self::RELEASES[$codename])) { return StepResult::fail( 'Refusing to install Proxmox VE: this host does not run a Debian release CluPilot '. 'knows a matching Proxmox suite for. /etc/os-release reports '. 'ID='.($id === '' ? '(empty)' : $id).', '. 'VERSION_CODENAME='.($codename === '' ? '(empty)' : $codename).', '. 'PRETTY_NAME='.($prettyName === '' ? '(empty)' : '"'.$prettyName.'"').'. '. 'Supported: '.implode(', ', array_keys(self::RELEASES)).'. '. 'Reinstall the server on a supported Debian release, or add the release to '. 'InstallProxmoxVe::RELEASES once Proxmox publishes a suite for it.' ); } $release = self::RELEASES[$codename]; $keyringPath = self::KEYRING_DIR.'/'.$release['keyring']; $key = $this->shell->run( 'install -d -m 0755 '.escapeshellarg(self::KEYRING_DIR).' && '. 'curl -fsSL '.escapeshellarg($release['key_url']).' -o '.escapeshellarg($keyringPath) ); if (! $key->ok()) { return StepResult::fail( 'Could not fetch the Proxmox release key for '.$codename.' from '.$release['key_url'].'.' ); } $this->shell->putFile(self::SOURCES_FILE, $this->renderSourcesFile($release['suite'], $keyringPath)); // A host onboarded before this step used deb822 still carries the old // one-line file. Left in place it would name the suite a second time, // and a stale bookworm line on a trixie machine is precisely the // mismatch this step now refuses to create. $this->shell->run('rm -f '.escapeshellarg(self::LEGACY_SOURCES_FILE)); $this->shell->run('rm -f /etc/apt/sources.list.d/pve-enterprise.list'); if (! $this->shell->run('export DEBIAN_FRONTEND=noninteractive; apt-get update')->ok()) { return StepResult::retry(60, 'apt-get update failed'); } if (! $this->shell->run('export DEBIAN_FRONTEND=noninteractive; apt-get -y full-upgrade')->ok()) { return StepResult::retry(60, 'apt-get full-upgrade failed'); } if (! $this->shell->run('export DEBIAN_FRONTEND=noninteractive; apt-get -y install proxmox-ve postfix open-iscsi')->ok()) { return StepResult::retry(60, 'proxmox-ve install failed'); } return StepResult::advance(); } /** * ID, VERSION_CODENAME and PRETTY_NAME, in one read. * * Sourced rather than grepped so the shell does the unquoting for us, and * printed with a separator that cannot appear in any of the three values. * PRETTY_NAME is carried purely so the failure message above can quote what * the machine actually says about itself — a Debian unstable image has no * VERSION_CODENAME at all, and "codename=(empty)" on its own tells an * operator nothing about which image they booted. * * @return array{0: string, 1: string, 2: string} */ private function readOsRelease(): array { $raw = $this->shell->run( '. /etc/os-release 2>/dev/null; printf "%s|%s|%s\n" "$ID" "$VERSION_CODENAME" "$PRETTY_NAME"' )->stdout; $parts = array_pad(explode('|', trim($raw), 3), 3, ''); return [strtolower(trim($parts[0])), strtolower(trim($parts[1])), trim($parts[2])]; } /** * deb822 rather than the one-line format, because `Signed-By` is the only * way to scope the Proxmox key to the Proxmox repository, and because this * is the shape upstream's own PVE 9 guide ships. apt has understood it * since well before bookworm, so both supported releases read the same file. */ private function renderSourcesFile(string $suite, string $keyringPath): string { return implode("\n", [ '# Written by CluPilot\'s InstallProxmoxVe provisioning step.', '# The suite is derived from /etc/os-release on this host, not assumed.', 'Types: deb', 'URIs: http://download.proxmox.com/debian/pve', 'Suites: '.$suite, 'Components: pve-no-subscription', 'Architectures: amd64', 'Signed-By: '.$keyringPath, '', ]); } }