99 lines
3.8 KiB
PHP
99 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Customer;
|
|
|
|
use App\Actions\ReapplyInstanceAddress;
|
|
use App\Models\Instance;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
|
|
class ConfigureNetwork extends CustomerStep
|
|
{
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'configure_network';
|
|
}
|
|
|
|
public function maxDuration(): int
|
|
{
|
|
// Above the 90s guest-ip own-deadline below.
|
|
return 150;
|
|
}
|
|
|
|
public function execute(ProvisioningRun $run): StepResult
|
|
{
|
|
$instance = $this->instance($run);
|
|
$node = (string) $run->context('node');
|
|
$vmid = (int) $run->context('vmid');
|
|
$pve = $this->pve->forHost($instance->host);
|
|
|
|
$this->guest($pve, $run, 'hostnamectl set-hostname '.escapeshellarg($instance->subdomain));
|
|
|
|
// Capture the guest's own address so Traefik routes to the VM, not the
|
|
// host. Poll until it's available rather than advancing with no address.
|
|
$ipOut = trim($pve->guestExec($node, $vmid, 'hostname -I')['out-data'] ?? '');
|
|
$guestIp = trim(strtok($ipOut, ' ') ?: '');
|
|
if ($guestIp === '') {
|
|
if ($run->started_at !== null && $run->started_at->copy()->addSeconds(90)->isPast()) {
|
|
return StepResult::fail('guest_ip_unavailable');
|
|
}
|
|
|
|
return StepResult::poll(10, 'waiting for the guest to obtain an address');
|
|
}
|
|
$instance->update(['guest_ip' => $guestIp]);
|
|
|
|
$this->repairAddressIfTheGuestMoved($run, $instance, $guestIp);
|
|
|
|
// Allow HTTP/HTTPS, deny everything else (management stays on the tunnel).
|
|
$pve->applyFirewall($node, $vmid, [
|
|
['type' => 'in', 'action' => 'ACCEPT', 'proto' => 'tcp', 'dport' => '80'],
|
|
['type' => 'in', 'action' => 'ACCEPT', 'proto' => 'tcp', 'dport' => '443'],
|
|
]);
|
|
|
|
return StepResult::advance();
|
|
}
|
|
|
|
/**
|
|
* The guest came up somewhere else — put its address back together.
|
|
*
|
|
* cloud-init hands every guest `ip=dhcp`, so the address is a lease and not a
|
|
* property of the machine: it can move on an expiry or a cold boot, and this
|
|
* step is the only thing in the product that ever reads it. The router on the
|
|
* serving host names it as the backend, so a move that nobody follows up is a
|
|
* customer's cloud answering 502 from then on, for good.
|
|
*
|
|
* The follow-up is an address run, because writing that file is the address
|
|
* pipeline's job and not this step's — one place writes the router, and this
|
|
* one only knows that it is now wrong. That run is allowed to start beside
|
|
* this one because the guard asks whether the run in flight will carry out the
|
|
* address steps rather than whether anything at all is running, and a restart
|
|
* carries neither of them (App\Provisioning\WorkInFlight).
|
|
*
|
|
* Only on a CHANGE from an address we already had. A first build has no
|
|
* previous address to have moved from — and its instance is not live yet, so
|
|
* the re-apply would decline anyway — and a retry of this step reads the
|
|
* address it just wrote, so it asks once.
|
|
*/
|
|
private function repairAddressIfTheGuestMoved(ProvisioningRun $run, Instance $instance, string $guestIp): void
|
|
{
|
|
$routed = $instance->routed_backend;
|
|
|
|
if ($routed === null || $routed === $guestIp) {
|
|
return;
|
|
}
|
|
|
|
$run->events()->create([
|
|
'step' => $this->key(),
|
|
'attempt' => $run->attempt,
|
|
'outcome' => 'info',
|
|
'message' => "Die Maschine ist mit einer neuen Adresse zurückgekommen ({$routed} → {$guestIp}). "
|
|
.'Die Weiterleitung wird neu geschrieben.',
|
|
]);
|
|
|
|
app(ReapplyInstanceAddress::class)($instance->fresh());
|
|
}
|
|
}
|