Add VM configuration update endpoint and job; implement CPU/RAM override functionality
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\VirtualMachine\Sync\SyncAllPackage;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
@@ -44,6 +45,11 @@ class Kernel extends ConsoleKernel
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping();
|
||||
|
||||
/* Sync all VM packages */
|
||||
$schedule->job(new SyncAllPackage())
|
||||
->everyThreeHours()
|
||||
->withoutOverlapping();
|
||||
|
||||
/* Mark overdue unpaid invoices */
|
||||
$schedule->command('invoices:check-overdue')
|
||||
->timezone("Asia/Ho_Chi_Minh")
|
||||
|
||||
@@ -739,6 +739,31 @@ class VCenterManagerController extends Controller
|
||||
], 400);
|
||||
}
|
||||
|
||||
public function updateConfig(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'vm_name' => 'required|string',
|
||||
'cpu' => 'required|integer|min:1',
|
||||
'ram' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$vmware = ModuleVmware::where('name', $request->input('vm_name'))->first();
|
||||
|
||||
if (!$vmware) {
|
||||
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
|
||||
}
|
||||
|
||||
\App\Jobs\VirtualMachine\Provision\UpdateVmConfigJob::dispatch(
|
||||
$vmware->id,
|
||||
(string) $vmware->vmware_id,
|
||||
'',
|
||||
(int) $request->input('cpu'),
|
||||
(int) $request->input('ram'),
|
||||
);
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã gửi yêu cầu cập nhật cấu hình VM']);
|
||||
}
|
||||
|
||||
public function deleteVM(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -596,6 +596,7 @@ Route::prefix('v1')
|
||||
Route::post('vm/restart', 'restart')->middleware('perm:module-vcenter.edit');
|
||||
Route::post('vm/snapshot', 'snapshot')->middleware('perm:module-vcenter.edit');
|
||||
Route::post('vm/suspend', 'suspend')->middleware('perm:module-vcenter.edit');
|
||||
Route::post('vm/update-config', 'updateConfig')->middleware('perm:module-vcenter.edit');
|
||||
Route::post('vm/delete', 'deleteVM')->middleware('perm:module-vcenter.edit');
|
||||
Route::get('vms/powered-off', 'getPoweredOffVms')->middleware('perm:module-vcenter.view');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user