184 lines
7.9 KiB
PHP
184 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Host;
|
|
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Ssh\RemoteShell;
|
|
|
|
/**
|
|
* Installs Proxmox VE on the Debian base: add the no-subscription repo + key,
|
|
* full-upgrade, install proxmox-ve. Long-running; apt/network failures retry,
|
|
* a bad repo/key fails.
|
|
*
|
|
* The Debian release is READ FROM THE HOST, never assumed. Proxmox publishes one
|
|
* suite per Debian release and the two must match: pointing a Debian 13 (trixie)
|
|
* base at the bookworm suite installs a PVE built against a different libc and
|
|
* kernel, which is the kind of breakage that ends with a machine that has no
|
|
* working package manager and no working hypervisor. A server ordered today
|
|
* boots trixie, so the codename this step used to hard-code was already wrong.
|
|
*
|
|
* An unrecognised release FAILS and names what it found. Guessing a suite for an
|
|
* unknown base — "trixie is newest, so use trixie" — is exactly the mistake this
|
|
* replaces: the next Debian will be unknown too, and by then nobody is watching.
|
|
*/
|
|
class InstallProxmoxVe extends HostStep
|
|
{
|
|
/**
|
|
* Debian codename -> 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<string, array{suite: string, key_url: string, keyring: string}>
|
|
*/
|
|
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,
|
|
'',
|
|
]);
|
|
}
|
|
}
|