vcenterId); if (!$vcenter || !$this->vCenter->loginServer($vcenter)) { throw new RuntimeException("Rebuild clone: cannot login to vCenter #{$ctx->vcenterId}"); } $datastore = ModuleDatastore::where('module_host_id', $ctx->hostId)->first(); $datastoreRef = $datastore?->datastore ?: 'datastore-3004'; $body = [ 'spec' => [ 'name' => $ctx->transientName, 'source' => $ctx->template, 'placement' => [ 'datacenter' => $ctx->datacenterRef, 'folder' => 'group-v1008', 'host' => $ctx->hostRef, 'resource_pool' => 'resgroup-3002', 'datastore' => $datastoreRef, ], 'power_on' => false, 'fallback_to_datastore_default_policy' => true, ], ]; Log::info('VmRebuildCloneService: clone start', [ 'module_vmware_id' => $ctx->moduleVmwareId, 'transient_name' => $ctx->transientName, 'template' => $ctx->template, ]); $response = Http::withoutVerifying() ->timeout(self::CLONE_TIMEOUT) ->connectTimeout(30) ->withHeaders([ 'vmware-api-session-id' => $this->vCenter->token, 'Content-Type' => 'application/json', ]) ->post( "https://{$this->vCenter->apiUri}/rest/vcenter/vm?action=clone", $body, ); $data = $response->json(); if (!$response->successful() || isset($data['type'])) { throw new RuntimeException( 'Rebuild clone failed: ' . json_encode($data ?: $response->body()) ); } $newVmId = (string) ($data['value'] ?? ''); if ($newVmId === '' || in_array($newVmId, ['unknown', 'UNKNOWN'], true)) { throw new RuntimeException('Rebuild clone: empty vm_id returned by vCenter'); } Log::info('VmRebuildCloneService: clone success', [ 'module_vmware_id' => $ctx->moduleVmwareId, 'new_vm_id' => $newVmId, ]); return $newVmId; } /** * Power on the freshly cloned VM and wait for VMware Tools to report * a guest IP — without this, IP assignment scripts cannot run. */ public function powerOnAndWaitTools(RebuildContext $ctx): void { if ($ctx->newVmId === null) { throw new RuntimeException('powerOnAndWaitTools: newVmId is null'); } $vcenter = VCenterModel::find($ctx->vcenterId); if (!$vcenter || !$this->vCenter->loginServer($vcenter)) { throw new RuntimeException("Rebuild powerOn: cannot login to vCenter #{$ctx->vcenterId}"); } $powerResp = Http::withoutVerifying() ->timeout(self::POWER_ON_TIMEOUT) ->withHeaders(['vmware-api-session-id' => $this->vCenter->token]) ->post("https://{$this->vCenter->apiUri}/rest/vcenter/vm/{$ctx->newVmId}/power/start"); if (!$powerResp->successful() && $powerResp->status() !== 400) { // 400 = already powered on; treat as OK. throw new RuntimeException( "Rebuild powerOn failed: status={$powerResp->status()} body={$powerResp->body()}" ); } Log::info('VmRebuildCloneService: powered on, waiting for tools', [ 'new_vm_id' => $ctx->newVmId, ]); for ($i = 1; $i <= self::TOOLS_MAX_ATTEMPTS; $i++) { $info = Http::withoutVerifying() ->timeout(10) ->withHeaders(['vmware-api-session-id' => $this->vCenter->token]) ->get("https://{$this->vCenter->apiUri}/rest/vcenter/vm/{$ctx->newVmId}/guest/identity") ->json(); $guestIp = trim((string) ($info['value']['ip_address'] ?? '')); if ($guestIp !== '' && $guestIp !== '0.0.0.0') { Log::info('VmRebuildCloneService: tools ready — applying grace', [ 'new_vm_id' => $ctx->newVmId, 'guest_ip' => $guestIp, 'attempt' => $i, 'grace_seconds' => self::TOOLS_READY_GRACE_SECONDS, ]); // Give the guest ops daemon time to accept Invoke-VMScript. sleep(self::TOOLS_READY_GRACE_SECONDS); return; } sleep(self::TOOLS_POLL_DELAY); } throw new RuntimeException( "Rebuild powerOn: VMware Tools never became ready on new VM {$ctx->newVmId}" ); } /** * Best-effort: power off the OLD VM so it stops responding on the shared IP * before we reassign it inside the guest of the new VM. We do NOT delete it * here — deletion is the very last step, after full validation. */ public function powerOffOldVm(RebuildContext $ctx): void { if ($ctx->oldVmId === '') { return; } $vcenter = VCenterModel::find($ctx->vcenterId); if (!$vcenter || !$this->vCenter->loginServer($vcenter)) { Log::warning('VmRebuildCloneService: powerOff old VM skipped — vCenter login failed', [ 'old_vm_id' => $ctx->oldVmId, ]); return; } try { Http::withoutVerifying() ->timeout(30) ->withHeaders(['vmware-api-session-id' => $this->vCenter->token]) ->post("https://{$this->vCenter->apiUri}/rest/vcenter/vm/{$ctx->oldVmId}/power/stop"); Log::info('VmRebuildCloneService: old VM powered off', [ 'old_vm_id' => $ctx->oldVmId, ]); } catch (\Throwable $e) { // Non-fatal — guest may already be off, or vCenter may flake. Log::warning('VmRebuildCloneService: powerOff old VM failed', [ 'old_vm_id' => $ctx->oldVmId, 'error' => $e->getMessage(), ]); } } }