instance($run); if ($instance === null) { return StepResult::fail('no_instance'); } $node = (string) $run->context('node'); $vmid = (int) $run->context('vmid'); $pve = $this->pve->forHost($instance->host); // Idempotent, and the ordinary way this step ends: the guest is gone. if (($pve->vmStatus($node, $vmid)['status'] ?? '') !== 'running') { return StepResult::advance(); } // Asked once, remembered on the run. Re-sending ACPI to a guest that is // already half-way through its own shutdown achieves nothing and reads, // in the guest's log, like a second person pressing the power button. if ($run->context('shutdown_upid') === null) { $run->mergeContext([ 'shutdown_upid' => $pve->shutdownVm($node, $vmid, self::GRACE_SECONDS), ]); return StepResult::poll(10, 'waiting for the guest to shut down'); } // A poll does not reset started_at, so this deadline genuinely // accumulates across the whole wait rather than restarting every time. if ($run->started_at !== null && $run->started_at->copy()->addSeconds(self::GRACE_SECONDS)->isPast()) { $run->events()->create([ 'step' => $this->key(), 'attempt' => $run->attempt, 'outcome' => 'info', 'message' => 'Der Gast hat sich in '.self::GRACE_SECONDS.' Sekunden nicht heruntergefahren. ' .'Die Maschine läuft unverändert weiter — sie wird bewusst nicht hart ausgeschaltet, ' .'weil eine Nextcloud eine laufende Datenbank ist.', ]); return StepResult::fail('shutdown_timeout'); } return StepResult::poll(10, 'waiting for the guest to shut down'); } }