220 lines
8.5 KiB
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';
|
|
}
|
|
}
|