Files
2026-09-27 11:42:40 +07:00

205 lines
7.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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\ModuleDatastore;
use App\Models\vCenter as VCenterModel;
use App\Support\TlsHttpClientFactory;
use Illuminate\Support\Facades\Log;
use RuntimeException;
/**
* VmRebuildCloneService
*
* Clones a fresh VM from the chosen OS template and powers it on.
* Uses the vCenter REST API directly — never dispatches CreateVirtualMachineJob.
*/
final class VmRebuildCloneService
{
private const CLONE_TIMEOUT = 300;
private const POWER_ON_TIMEOUT = 30;
private const TOOLS_POLL_DELAY = 10;
private const TOOLS_MAX_ATTEMPTS = 30;
/**
* Extra grace after VMware Tools reports an IP. VMware Tools advertises
* the guest IP as soon as the kernel finishes networking, but
* Invoke-VMScript / guest ops daemon needs another ~20–40 s before it
* actually accepts script invocations on a fresh clone. Without this
* delay assignIP/verifyIP scripts often return "FAIL" on the first run.
*/
private const TOOLS_READY_GRACE_SECONDS = 45;
public function __construct(private readonly VCenterService $vCenter)
{
}
/**
* Clone a new VM from the OS template specified in $ctx.
*
* @return string vSphere vm_id of the newly cloned VM
*/
public function clone(RebuildContext $ctx): string
{
$vcenter = VCenterModel::find($ctx->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 = TlsHttpClientFactory::vCenter()
->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 = TlsHttpClientFactory::vCenter()
->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 = TlsHttpClientFactory::vCenter()
->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 {
TlsHttpClientFactory::vCenter()
->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(),
]);
}
}
}