newVmId === null) { throw new \RuntimeException('Rebuild swap: newVmId is null'); } $vcenter = VCenterModel::find($ctx->vcenterId); if (!$vcenter || !$this->vCenter->loginServer($vcenter)) { throw new \RuntimeException( "Rebuild swap: cannot login to vCenter #{$ctx->vcenterId}" ); } $detail = $this->vCenter->GetDetailVirtualMachine($ctx->newVmId); if (($detail['error'] ?? true) === true) { throw new \RuntimeException( 'Rebuild swap: GetDetailVirtualMachine failed — ' . json_encode($detail) ); } $power = $detail['data']['power_state'] ?? null; if ($power !== 'POWERED_ON') { throw new \RuntimeException( "Rebuild swap: new VM not POWERED_ON (got '{$power}')" ); } $ipInfo = $this->vCenter->GetIPAddressVirtualMachine($ctx->newVmId); $guestIp = (string) ($ipInfo['data']['ip_address'] ?? ''); if ($guestIp === '') { throw new \RuntimeException('Rebuild swap: new VM reports no guest IP'); } // Soft check — guest may take a moment to settle on the new IP, but // it should at least be reporting *something* on the right subnet. Log::info('VmRebuildSwapService: health check passed', [ 'module_vmware_id' => $ctx->moduleVmwareId, 'new_vm_id' => $ctx->newVmId, 'guest_ip' => $guestIp, 'expected_ip' => $ctx->ip, 'power_state' => $power, ]); } /** * Atomically swap DB references: * - module_vmware.vmware_id → new vSphere id * - module_vmware.os_id → new OS id * - virtual_machines.vm_id → new vSphere id (if a row exists) * * Preserved: services row (incl. vm_id FK), order_item_id, customer_id, * ModuleIpAddress, ServiceAddons. */ public function swapDatabaseReferences(RebuildContext $ctx): void { if ($ctx->newVmId === null) { throw new \RuntimeException('Rebuild swap: newVmId is null'); } DB::transaction(function () use ($ctx) { ModuleVmware::where('id', $ctx->moduleVmwareId)->update([ 'vmware_id' => $ctx->newVmId, 'os_id' => $ctx->newOsId, 'status' => 1, ]); VirtualMachines::where('vm_id', $ctx->oldVmId)->update([ 'vm_id' => $ctx->newVmId, 'ip' => $ctx->ip, ]); if ($ctx->serviceId) { // Keep the existing Service intact — just make sure it stays // marked active and its server_name reflects the VM. Service::where('id', $ctx->serviceId)->update([ 'status' => 'active', ]); } }); Log::info('VmRebuildSwapService: DB references swapped', [ 'module_vmware_id' => $ctx->moduleVmwareId, 'old_vm_id' => $ctx->oldVmId, 'new_vm_id' => $ctx->newVmId, ]); } /** * Delete the old VM from vCenter. Best-effort: the DB is already swapped, * so a leftover ghost VM is recoverable manually and must not throw past * the queue. */ public function deleteOldVm(RebuildContext $ctx): void { if ($ctx->oldVmId === '') { return; } try { $vcenter = VCenterModel::find($ctx->vcenterId); if (!$vcenter || !$this->vCenter->loginServer($vcenter)) { Log::error('VmRebuildSwapService: cannot login to delete old VM', [ 'old_vm_id' => $ctx->oldVmId, ]); return; } // Force power-off before delete to avoid "VM is running" rejection. $this->vCenter->PowerOffVirtualMachine($ctx->oldVmId); $res = $this->vCenter->DeleteVirtualMachine($ctx->oldVmId); Log::info('VmRebuildSwapService: old VM deleted', [ 'old_vm_id' => $ctx->oldVmId, 'result' => $res, ]); } catch (Throwable $e) { Log::error('VmRebuildSwapService: deleteOldVm threw', [ 'old_vm_id' => $ctx->oldVmId, 'error' => $e->getMessage(), ]); } } }