Files
portal-api/app/Services/VirtualMachine/Rebuild/VmRebuildIpService.php

220 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\VirtualMachine\Rebuild;
use App\DTOs\VirtualMachine\Rebuild\RebuildContext;
use App\Http\Facade\SignRSA;
use App\Models\IpPool;
use App\Models\ModuleVmware;
use App\Models\Os;
use App\Models\vCenter as VCenterModel;
use App\Services\VsphereHostSshService;
use Illuminate\Support\Facades\Log;
use RuntimeException;
/**
* VmRebuildIpService
*
* Mirrors the production AssignIpToGuestJob flow 1:1 for the rebuild path —
* uses the SAME VsphereHostSshService helpers, which means the SAME PS1
* scripts on the vSphere host:
*
* C:\CLISave\AssignIP\assignIP{Linux|Windows}.ps1
* C:\CLISave\AssignIP\verifyIP{Linux|Windows}.ps1
*
* Differences vs create flow (hard rules, per the rebuild spec):
* - NEVER reserve a new IP
* - NEVER mutate ip_pools / module_ip_address ownership
* - The IP, gateway, cidr, dns are read from the EXISTING service:
* gateway/cidr ← IpPool row whose ip_address == current VM IP
* dns ← product.ipAddress.dns (same as AssignIpToGuestJob)
* hostId ← osGroup.host.id of the NEW OS (same as AssignIpToGuestJob)
*
* In-place retry loop because we MUST reuse the same IP — we can't fall
* through to a different IP like AssignIpToGuestJob does.
*/
final class VmRebuildIpService
{
private const MAX_ATTEMPTS = 4;
private const ATTEMPT_DELAY = 25;
public function __construct(private readonly VsphereHostSshService $ssh)
{
}
public function assignAndVerify(RebuildContext $ctx): void
{
if ($ctx->newVmId === null) {
throw new RuntimeException('Rebuild IP: newVmId is null');
}
if ($ctx->ip === '') {
throw new RuntimeException('Rebuild IP: missing existing IP in context');
}
// Load ModuleVmware + relations EXACTLY like AssignIpToGuestJob does
// so we read parameters from the same DB locations.
$moduleVmware = ModuleVmware::with([
'order.orderItems.product.ipAddress',
'vcenter',
])->find($ctx->moduleVmwareId);
if (!$moduleVmware) {
throw new RuntimeException(
"Rebuild IP: ModuleVmware #{$ctx->moduleVmwareId} not found"
);
}
// DNS comes from product.ipAddress.dns — same source AssignIpToGuestJob uses.
$orderItem = $moduleVmware->order?->orderItems?->first();
$dns = $orderItem?->product?->ipAddress?->dns
?: ($ctx->dns ?: '8.8.8.8');
// Host id is osGroup.host.id of the NEW OS — same source AssignIpToGuestJob uses
// (it loads Os->osGroup->host for whatever OS is on the order item).
$osModel = Os::with('osGroup.host')->find($ctx->newOsId);
$hostId = $osModel?->osGroup?->host?->id ?? $ctx->hostId ?? 1;
// Gateway + CIDR come from the ip_pools row already assigned to this
// service — same DB source the create flow reads at reservation time.
// For rebuild we never reserve; we just look it up by ip_address.
$ipPool = IpPool::where('ip_address', $ctx->ip)->first();
$gateway = (string) ($ipPool?->gateway ?? $ctx->gateway);
$cidr = (string) ((int) ($ipPool?->cidr ?? $ctx->cidr ?: 24));
if ($gateway === '') {
throw new RuntimeException(
"Rebuild IP: cannot resolve gateway for IP {$ctx->ip} (no ip_pools row and context empty)"
);
}
// vCenter credentials — identical decode path as AssignIpToGuestJob.
$vcenter = $moduleVmware->vcenter
?? VCenterModel::find($ctx->vcenterId)
?? VCenterModel::first();
if (!$vcenter) {
throw new RuntimeException('Rebuild IP: no vCenter record available');
}
$vcenterHost = (string) $vcenter->host;
$vcenterUser = (string) $vcenter->user;
$vcenterPass = SignRSA::DecryptFunc($vcenter->password);
// Guest OS default admin user per OS family — same convention.
$guestUser = strtoupper($ctx->osFamily) === 'WINDOWS' ? 'Administrator' : 'root';
Log::info('VmRebuildIpService: assigning existing IP to new VM', [
'module_vmware_id' => $ctx->moduleVmwareId,
'new_vm_id' => $ctx->newVmId,
'ip' => $ctx->ip,
'gateway' => $gateway,
'cidr' => $cidr,
'dns' => $dns,
'host_id' => $hostId,
'os_family' => $ctx->osFamily,
'guest_user' => $guestUser,
]);
$lastAssignOutput = '';
$lastVerifyOutput = '';
for ($attempt = 1; $attempt <= self::MAX_ATTEMPTS; $attempt++) {
// [4] Call assignIP{Linux|Windows}.ps1 — IDENTICAL invocation
// to AssignIpToGuestJob, same VsphereHostSshService::assignIp().
$lastAssignOutput = $this->ssh->assignIp(
vmId: $ctx->newVmId,
ip: $ctx->ip,
gateway: $gateway,
cidr: $cidr,
vcenterHost: $vcenterHost,
vcenterUser: $vcenterUser,
vcenterPassword: $vcenterPass,
guestPassword: VmRebuildService::templatePassword(),
osFamily: $ctx->osFamily,
dns: $dns,
guestUser: $guestUser,
);
if (!$this->isSuccess($lastAssignOutput)) {
Log::warning('VmRebuildIpService: assignIp FAIL', [
'ip' => $ctx->ip,
'attempt' => $attempt,
'new_vm_id' => $ctx->newVmId,
'output' => trim($lastAssignOutput),
]);
if ($attempt < self::MAX_ATTEMPTS) {
sleep(self::ATTEMPT_DELAY);
continue;
}
throw new RuntimeException(
'Rebuild IP: assign script failed after ' . self::MAX_ATTEMPTS
. ' attempts — last output: ' . trim($lastAssignOutput),
);
}
if (strtoupper($ctx->osFamily) === VmRebuildService::OS_WINDOWS) {
Log::info('VmRebuildIpService: Windows IP assigned', [
'module_vmware_id' => $ctx->moduleVmwareId,
'new_vm_id' => $ctx->newVmId,
'ip' => $ctx->ip,
'attempt' => $attempt,
]);
return;
}
// [6] Call verifyIP{Linux|Windows}.ps1 — same as AssignIpToGuestJob.
$lastVerifyOutput = $this->ssh->verifyIpInGuest(
vmId: $ctx->newVmId,
ip: $ctx->ip,
vcenterHost: $vcenterHost,
vcenterUser: $vcenterUser,
vcenterPassword: $vcenterPass,
guestPassword: VmRebuildService::templatePassword(),
osFamily: $ctx->osFamily,
guestUser: $guestUser,
);
if ($this->isSuccess($lastVerifyOutput)) {
Log::info('VmRebuildIpService: IP assigned and verified', [
'module_vmware_id' => $ctx->moduleVmwareId,
'new_vm_id' => $ctx->newVmId,
'ip' => $ctx->ip,
'attempt' => $attempt,
]);
return;
}
Log::warning('VmRebuildIpService: verifyIpInGuest FAIL', [
'ip' => $ctx->ip,
'attempt' => $attempt,
'new_vm_id' => $ctx->newVmId,
'output' => trim($lastVerifyOutput),
]);
if ($attempt < self::MAX_ATTEMPTS) {
sleep(self::ATTEMPT_DELAY);
}
}
throw new RuntimeException(
'Rebuild IP: verify script failed after ' . self::MAX_ATTEMPTS
. ' attempts — last output: ' . trim($lastVerifyOutput),
);
}
/** PS1 scripts return JSON {"status":"ok"} or plain SUCCESS — same parser as AssignIpToGuestJob. */
private function isSuccess(string $output): bool
{
if (str_contains(strtoupper(trim($output)), 'SUCCESS')) {
return true;
}
$data = json_decode(trim($output), true);
return is_array($data) && ($data['status'] ?? '') === 'ok';
}
}