Add VM configuration update endpoint and job; implement CPU/RAM override functionality

This commit is contained in:
baodev0
2026-07-17 15:33:51 +07:00
parent ad20cd5bdc
commit 3f477a559d
4 changed files with 162 additions and 25 deletions
@@ -9,6 +9,7 @@ use App\Http\Services\Telegram;
use App\Models\Addon;
use App\Models\ModuleVmware;
use App\Models\Service;
use App\Models\ServiceAddon;
use App\Models\vCenter;
use App\Services\VirtualMachine\VmProvisionLogger;
use App\Services\VsphereHostSshService;
@@ -18,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
@@ -46,13 +48,15 @@ class UpdateVmConfigJob implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 180;
public int $timeout = 300;
public int $backoff = 30;
public function __construct(
public readonly int $moduleVmwareId,
public readonly string $vmId,
public readonly string $osFamily,
public readonly ?int $cpuOverride = null,
public readonly ?int $ramOverride = null,
) {}
public function handle(VsphereHostSshService $ssh, VmProvisionLogger $logger, Telegram $telegram): void
@@ -75,19 +79,25 @@ class UpdateVmConfigJob implements ShouldQueue
return;
}
$orderItem = $moduleVmware->order?->orderItems?->first();
$product = $orderItem?->product;
$addonsMeta = is_array($orderItem?->addons_meta) ? $orderItem->addons_meta : [];
$isManualOverride = $this->cpuOverride !== null || $this->ramOverride !== null;
$addonCpu = $addonRamGb = 0;
$product = null;
[$addonCpu, $addonRamGb] = $this->resolveAddonCpuRam($addonsMeta);
if (!$isManualOverride) {
$orderItem = $moduleVmware->order?->orderItems?->first();
$product = $orderItem?->product;
$addonsMeta = is_array($orderItem?->addons_meta) ? $orderItem->addons_meta : [];
if ($addonCpu === 0 && $addonRamGb === 0) {
$logger->step('no cpu/ram addon — skipping UpdateVmConfigJob', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
]);
$this->nextStep();
return;
[$addonCpu, $addonRamGb] = $this->resolveAddonCpuRam($addonsMeta);
if ($addonCpu === 0 && $addonRamGb === 0) {
$logger->step('no cpu/ram addon — skipping UpdateVmConfigJob', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
]);
$this->nextStep();
return;
}
}
// Load vCenter credentials — same pattern as ChangePasswordViaVsphereJob
@@ -105,27 +115,42 @@ class UpdateVmConfigJob implements ShouldQueue
$vcenterPass = SignRSA::DecryptFunc($vcenter->password);
$serverName = (string) $moduleVmware->name;
$totalCpu = ((int) ($product?->cpu ?? 0)) + $addonCpu;
$totalRam = ((int) ($product?->ram ?? 0)) + $addonRamGb;
if ($isManualOverride) {
$totalCpu = $this->cpuOverride ?? (int) $moduleVmware->cpu;
$totalRam = $this->ramOverride ?? (int) ($moduleVmware->ram_mb / 1024);
} else {
$totalCpu = ((int) ($product?->cpu ?? 0)) + $addonCpu;
$totalRam = ((int) ($product?->ram ?? 0)) + $addonRamGb;
}
$logger->step('calling updateVMConfig.ps1', [
'module_vmware_id' => $this->moduleVmwareId,
'server_name' => $serverName,
'vcenter_host' => $vcenterHost,
'addon_cpu' => $addonCpu,
'addon_ram_gb' => $addonRamGb,
'manual_override' => $isManualOverride,
'total_cpu' => $totalCpu,
'total_ram_gb' => $totalRam,
]);
$output = $ssh->updateVmConfig(
$serverName,
$totalCpu,
$totalRam,
$vcenterHost,
$vcenterUser,
$vcenterPass,
);
if ($isManualOverride) {
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true);
}
try {
$output = $ssh->updateVmConfig(
$serverName,
$totalCpu,
$totalRam,
$vcenterHost,
$vcenterUser,
$vcenterPass,
);
} finally {
if ($isManualOverride) {
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false);
}
}
Log::info('UpdateVmConfigJob: PS1 output', [
'module_vmware_id' => $this->moduleVmwareId,
@@ -157,7 +182,7 @@ class UpdateVmConfigJob implements ShouldQueue
throw new \RuntimeException($msg);
}
$logger->success('VM config updated via addon script', [
$logger->success('VM config updated', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
'total_cpu' => $totalCpu,
@@ -165,6 +190,12 @@ class UpdateVmConfigJob implements ShouldQueue
'output' => trim($output),
]);
if ($isManualOverride) {
$moduleVmware->update(['cpu' => $totalCpu, 'ram_mb' => $totalRam * 1024]);
$this->syncServiceAddons($moduleVmware, $totalCpu, $totalRam, $logger);
return;
}
$this->nextStep();
}
@@ -240,6 +271,80 @@ class UpdateVmConfigJob implements ShouldQueue
return [$cpu, $ram];
}
/**
* Power off/on the VM via vCenter REST and wait (up to 30s) for the state to land.
* Used only for manual admin overrides — hot-add isn't reliable for RAM on every guest OS,
* so we power-cycle around the resize to guarantee it applies.
*/
private function powerCycle(vCenter $vcenter, string $vmwareId, VmProvisionLogger $logger, bool $powerOff): void
{
if ($vmwareId === '') {
return;
}
$service = new \App\Http\Services\vCenter();
if (!$service->loginServer($vcenter)) {
Log::warning('UpdateVmConfigJob: cannot login to vCenter for power cycle', ['vmware_id' => $vmwareId]);
return;
}
$action = $powerOff ? 'stop' : 'start';
$targetState = $powerOff ? 'POWERED_OFF' : 'POWERED_ON';
Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $service->token])
->post("https://{$service->apiUri}/rest/vcenter/vm/{$vmwareId}/power/{$action}");
for ($i = 0; $i < 15; $i++) {
sleep(2);
$state = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $service->token])
->get("https://{$service->apiUri}/rest/vcenter/vm/{$vmwareId}")
->json('value.power_state');
if ($state === $targetState) {
$logger->step("power cycle reached {$targetState}", ['vmware_id' => $vmwareId]);
return;
}
}
$logger->step("power cycle timed out waiting for {$targetState}", ['vmware_id' => $vmwareId]);
}
/**
* Reflect the new CPU/RAM on the service's addon rows (service_addons.addon_value)
* so billing/admin views match what's actually running on the VM.
*/
private function syncServiceAddons(ModuleVmware $moduleVmware, int $totalCpu, int $totalRam, VmProvisionLogger $logger): void
{
$service = Service::where('vm_id', $moduleVmware->id)->first();
if (!$service) {
$logger->step('syncServiceAddons: no service found for vm_id — skipping', [
'module_vmware_id' => $moduleVmware->id,
]);
return;
}
ServiceAddon::updateOrCreate(
['service_id' => $service->id, 'addon_type' => 'cpu'],
['addon_value' => (string) $totalCpu],
);
ServiceAddon::updateOrCreate(
['service_id' => $service->id, 'addon_type' => 'ram'],
['addon_value' => (string) ($totalRam * 1024)],
);
$logger->step('service_addons synced', [
'service_id' => $service->id,
'total_cpu' => $totalCpu,
'total_ram_mb' => $totalRam * 1024,
]);
}
private function nextStep(): void
{
ChangePasswordViaVsphereJob::dispatch(