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