instance($run); $node = (string) $run->context('node'); $plan = $this->plan($run); $templateVmid = (int) ($plan['template_vmid'] ?? 0); if ($templateVmid === 0) { return StepResult::fail('template_missing'); } $pve = $this->pve->forHost($instance->host); // Idempotent: vmid already minted → resume polling the clone task. if ($this->hasResource($run, 'vmid')) { return $this->pollClone($pve, $node, (string) $run->context('clone_upid')); } $vmid = $pve->nextVmid(); $upid = $pve->cloneVm($node, $templateVmid, $vmid, 'nc-'.$instance->subdomain); // [E] persist vmid + task ref BEFORE polling (crash-safe). $instance->update(['vmid' => $vmid, 'status' => 'provisioning']); $run->mergeContext(['vmid' => $vmid, 'clone_upid' => $upid]); $this->recordResource($run, $instance->host, 'vmid', (string) $vmid); return $this->pollClone($pve, $node, $upid); } private function pollClone(ProxmoxClient $pve, string $node, string $upid): StepResult { $task = $pve->taskStatus($node, $upid); if (($task['status'] ?? '') === 'running') { return StepResult::poll(10, 'cloning template'); } if (($task['exitstatus'] ?? 'OK') !== 'OK') { return StepResult::fail('clone_failed: '.($task['exitstatus'] ?? 'unknown')); } return StepResult::advance(); } }