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()); } }