Files
portal-api/app/Services/VirtualMachine/Rebuild/VmRebuildSwapService.php
T

161 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\VirtualMachine\Rebuild;
use App\DTOs\VirtualMachine\Rebuild\RebuildContext;
use App\Http\Services\vCenter as VCenterService;
use App\Models\ModuleVmware;
use App\Models\Service;
use App\Models\vCenter as VCenterModel;
use App\Models\VirtualMachines;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
/**
* VmRebuildSwapService
*
* After the new VM has passed every validation step, this service:
* 1. Performs a final health check against the new VM in vCenter.
* 2. Atomically swaps DB references from the old vmware_id to the new one.
* 3. Deletes the old VM from vCenter — and ONLY then.
*
* If health check fails, nothing is swapped and the old VM is preserved
* untouched so a manual recovery is possible.
*/
final class VmRebuildSwapService
{
public function __construct(private readonly VCenterService $vCenter)
{
}
/**
* Health-check the new VM. Throws on failure (no rollback needed — old VM
* still owns the DB).
*/
public function healthCheck(RebuildContext $ctx): void
{
if ($ctx->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(),
]);
}
}
}