Add VM rebuild pipeline with isolated job chain and admin log API
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user