Refactor VM creation and configuration process
- Updated VpsServiceResource to return product UUID instead of ID. - Modified VirtualMachineService to utilize product data for CPU, RAM, and disk settings. - Replaced AssignIpToGuestJob with UpdateVmConfigJob in CreateVirtualMachineJob to streamline VM configuration after cloning. - Adjusted AssignIpToGuestJob to remove unnecessary UpdateVmConfigJob dispatch and focus on IP assignment. - Enhanced UpdateVmConfigJob to handle CPU/RAM configuration and power cycling more effectively. - Introduced ChangeIpVMJob for handling customer-initiated IP changes asynchronously. - Updated InvoiceRepository to set invoice due date based on maximum months instead of a fixed 7 days. - Improved web design quote PDF layout for better readability. - Added Times-Roman font metrics for PDF generation.
This commit is contained in:
@@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Service;
|
||||
|
||||
use App\Actions\VM\ChangeIpVMAction;
|
||||
use App\Actions\VM\ChangePortVMAction;
|
||||
use App\Actions\VM\PowerOffVMAction;
|
||||
use App\Actions\VM\PowerOnVMAction;
|
||||
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\ChangeIpVMJob;
|
||||
use App\Jobs\VirtualMachine\ChangeVmPasswordJob;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Os;
|
||||
@@ -19,7 +18,6 @@ use App\Services\VmProvisionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class VmController extends Controller
|
||||
{
|
||||
@@ -124,7 +122,10 @@ class VmController extends Controller
|
||||
* ip_address string New IP to assign
|
||||
* gateway string (optional) Gateway — falls back to existing IP record
|
||||
*/
|
||||
public function changeIp(Request $request, ChangeIpVMAction $action): JsonResponse
|
||||
/**
|
||||
* Dispatches ChangeIpVMJob — returns immediately, runs async.
|
||||
*/
|
||||
public function changeIp(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'server_name' => ['required', 'string', 'max:255'],
|
||||
@@ -165,87 +166,35 @@ class VmController extends Controller
|
||||
: 'windows';
|
||||
|
||||
$gateway = $validated['gateway'] ?? '';
|
||||
$dns = $validated['dns'] ?? '8.8.8.8';
|
||||
|
||||
$dns = $validated['dns'] ?? '8.8.8.8';
|
||||
|
||||
$lock = $this->acquireVmLock($vm, 120);
|
||||
// ponytail: don't release — lock expires after 5 min while the async job runs
|
||||
$lock = $this->acquireVmLock($vm, 300);
|
||||
if (!$lock) {
|
||||
return $this->vmLocked();
|
||||
}
|
||||
|
||||
try {
|
||||
$raw = $action->execute(
|
||||
$vm,
|
||||
$validated['ip_address'],
|
||||
$gateway,
|
||||
$osType,
|
||||
$dns,
|
||||
$validated['guest_user'],
|
||||
$validated['guest_password']
|
||||
);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
|
||||
// Always log the raw PowerShell output for debugging
|
||||
Log::channel('Custom')->info('[changeIp] raw output', [
|
||||
'server_name' => $validated['server_name'],
|
||||
'ip_address' => $validated['ip_address'],
|
||||
'gateway' => $gateway,
|
||||
'os_type' => $osType,
|
||||
'vm_name' => $vm->name,
|
||||
'vmware_id' => $vm->vmware_id,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
|
||||
$telegram = new Telegram();
|
||||
|
||||
if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') {
|
||||
$errMsg = $raw === 'vCenter_Not_Found'
|
||||
? 'Không tìm thấy vCenter cho VM này.'
|
||||
: 'Không thể đăng nhập vào vCenter.';
|
||||
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "🚫 Lỗi: {$errMsg}");
|
||||
|
||||
return response()->json(['success' => false, 'message' => $errMsg], 400);
|
||||
}
|
||||
|
||||
if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) {
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "📋 Output:\n" . mb_substr((string) $raw, 0, 800));
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Lệnh đổi IP thất bại. Admin đã được thông báo.', 'data' => ['raw' => $raw]], 400);
|
||||
}
|
||||
|
||||
// Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo)
|
||||
// Nhưng vẫn gửi cảnh báo để theo dõi
|
||||
if (trim((string) $raw) === '') {
|
||||
$telegram->SendMessage("⚠️ Đổi IP [{$osType}] - Script chạy nhưng không có output\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa.");
|
||||
}
|
||||
ChangeIpVMJob::dispatch(
|
||||
$vm->id,
|
||||
$validated['ip_address'],
|
||||
$gateway,
|
||||
$osType,
|
||||
$dns,
|
||||
$validated['guest_user'],
|
||||
$validated['guest_password'],
|
||||
);
|
||||
|
||||
$this->activityLogger->log(
|
||||
event: 'change_ip',
|
||||
logName: 'VM',
|
||||
description: "Customer {$request->user()->email} đổi IP VM: {$vm->name} → {$validated['ip_address']}",
|
||||
description: "Customer {$request->user()->email} yêu cầu đổi IP VM: {$vm->name} → {$validated['ip_address']}",
|
||||
subject: $vm,
|
||||
newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType],
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã gửi lệnh đổi IP.',
|
||||
'data' => ['raw' => $raw],
|
||||
'message' => 'Yêu cầu đổi IP đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,8 @@ class VpsServiceResource extends JsonResource
|
||||
$product = $orderItem?->product ?? $this->product;
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'uuid' => $this->uuid,
|
||||
'server_name' => $this->server_name,
|
||||
'invoice_id' => $this->invoice_id,
|
||||
'invoice_num' => $invoice?->invoice_number,
|
||||
'order_item_id' => $this->order_item_id,
|
||||
'order_num' => $order?->order_num,
|
||||
@@ -56,7 +54,7 @@ class VpsServiceResource extends JsonResource
|
||||
'group' => $orderItem?->os?->osGroup?->name,
|
||||
],
|
||||
'product' => $product ? [
|
||||
'id' => $product->id,
|
||||
'uuid' => $product->uuid,
|
||||
'name' => $product->name,
|
||||
'slug' => $product->slug,
|
||||
'product_type' => $product->product_type,
|
||||
|
||||
@@ -59,15 +59,16 @@ class VirtualMachineService
|
||||
$userName = customer()?->email;
|
||||
|
||||
$item = $product_data['item'];
|
||||
$product = $product_data['product'] ?? null;
|
||||
$host = $product_data['host'];
|
||||
$datacenter = $product_data['datacenter'];
|
||||
$template = $product_data['template'];
|
||||
|
||||
$addon = $product_data['addon'] ?? ['cpu' => 0, 'ram' => 0, 'disk' => 0];
|
||||
|
||||
$base_cpu = $item->cpu ?? ($host->cpu ?? 1);
|
||||
$base_ram = $item->ram ?? ($host->ram ?? 1);
|
||||
$base_disk = $item->disk ?? ($host->disk ?? 20);
|
||||
$base_cpu = $product->cpu ?? ($host->cpu ?? 1);
|
||||
$base_ram = $product->ram ?? ($host->ram ?? 1);
|
||||
$base_disk = $product->disk ?? ($host->disk ?? 20);
|
||||
|
||||
$base_cpu = max(1, (int) $base_cpu);
|
||||
$base_ram = max(1, (int) $base_ram);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine;
|
||||
|
||||
use App\Actions\VM\ChangeIpVMAction;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ChangeIpVMJob
|
||||
*
|
||||
* Customer-initiated VM IP change. Runs the same ChangeIpVMAction that used
|
||||
* to execute synchronously inside VmController::changeIp — moved to a queued
|
||||
* job so the SSH → PowerCLI round trip can no longer blow past nginx/php-fpm's
|
||||
* timeout and surface as a 502.
|
||||
*/
|
||||
class ChangeIpVMJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** No auto-retry — mirrors the previous synchronous behaviour (single attempt). */
|
||||
public int $tries = 1;
|
||||
public int $timeout = 180;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $ipAddress,
|
||||
public readonly string $gateway,
|
||||
public readonly string $osType,
|
||||
public readonly string $dns,
|
||||
public readonly string $guestUser,
|
||||
public readonly string $guestPassword,
|
||||
) {}
|
||||
|
||||
public function handle(ChangeIpVMAction $action): void
|
||||
{
|
||||
$vm = ModuleVmware::with([
|
||||
'vCenter',
|
||||
'ipAddress.host.datacenter.vCenter',
|
||||
])->find($this->moduleVmwareId);
|
||||
|
||||
if (!$vm) {
|
||||
Log::error('[ChangeIpVMJob] ModuleVmware not found', ['module_vmware_id' => $this->moduleVmwareId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = $action->execute(
|
||||
$vm,
|
||||
$this->ipAddress,
|
||||
$this->gateway,
|
||||
$this->osType,
|
||||
$this->dns,
|
||||
$this->guestUser,
|
||||
$this->guestPassword,
|
||||
);
|
||||
|
||||
Log::channel('Custom')->info('[ChangeIpVMJob] raw output', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'ip_address' => $this->ipAddress,
|
||||
'gateway' => $this->gateway,
|
||||
'os_type' => $this->osType,
|
||||
'vm_name' => $vm->name,
|
||||
'vmware_id' => $vm->vmware_id,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
|
||||
$telegram = new Telegram();
|
||||
|
||||
if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') {
|
||||
$errMsg = $raw === 'vCenter_Not_Found'
|
||||
? 'Không tìm thấy vCenter cho VM này.'
|
||||
: 'Không thể đăng nhập vào vCenter.';
|
||||
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$this->ipAddress}\n"
|
||||
. "🔗 GW: {$this->gateway}\n"
|
||||
. "🚫 Lỗi: {$errMsg}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) {
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$this->ipAddress}\n"
|
||||
. "🔗 GW: {$this->gateway}\n"
|
||||
. "📋 Output:\n" . mb_substr((string) $raw, 0, 800));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo)
|
||||
// Nhưng vẫn gửi cảnh báo để theo dõi
|
||||
if (trim((string) $raw) === '') {
|
||||
$telegram->SendMessage("⚠️ Đổi IP [{$this->osType}] - Script chạy nhưng không có output\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$this->ipAddress}\n"
|
||||
. "🔗 GW: {$this->gateway}\n"
|
||||
. "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa.");
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('[ChangeIpVMJob] job failed with exception', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'ip_address' => $this->ipAddress,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Jobs\VirtualMachine\Create;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Provision\AssignIpToGuestJob;
|
||||
use App\Jobs\VirtualMachine\Provision\UpdateVmConfigJob;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\VmProvisionLock;
|
||||
use App\Services\VirtualMachine\VmAddonResolver;
|
||||
@@ -13,7 +13,6 @@ use App\Services\VirtualMachine\VmInfrastructureResolver;
|
||||
use App\Services\VirtualMachine\VmIpReservationService;
|
||||
use App\Services\VirtualMachine\VmModuleService;
|
||||
use App\Services\VirtualMachine\VmNotificationService;
|
||||
use App\Services\VirtualMachine\VmPowerService;
|
||||
use App\Services\VirtualMachine\VmProvisionLogger;
|
||||
use App\Services\VirtualMachine\VmServiceSyncService;
|
||||
use App\Services\VirtualMachine\VmVcenterErrorTranslator;
|
||||
@@ -47,7 +46,6 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
VmProvisionLogger $vmLogger,
|
||||
VmInfrastructureResolver $infraResolver,
|
||||
VmAddonResolver $addonResolver,
|
||||
VmPowerService $powerService,
|
||||
VmModuleService $moduleService,
|
||||
VmServiceSyncService $serviceSync,
|
||||
VmNotificationService $notification,
|
||||
@@ -131,6 +129,7 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
|
||||
$result = $vCenter->CreateVirtualMachine([
|
||||
'item' => $this->item,
|
||||
'product' => $product,
|
||||
'host' => $infra->host,
|
||||
'datacenter' => $infra->datacenter,
|
||||
'vcenter' => $infra->vcenter,
|
||||
@@ -162,21 +161,6 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
'vm_id' => $vmId,
|
||||
]);
|
||||
|
||||
$vmLogger->step('VM start started', ['vm_id' => $vmId]);
|
||||
|
||||
$startResult = $powerService->start($vCenter, $vmId);
|
||||
|
||||
if (is_array($startResult) && ($startResult['error'] ?? false)) {
|
||||
throw new RuntimeException(
|
||||
'Start VM thất bại: ' . json_encode($startResult['data'] ?? null)
|
||||
);
|
||||
}
|
||||
if ($startResult === 'Login_Again') {
|
||||
throw new RuntimeException('Start VM thất bại: cần đăng nhập lại vCenter.');
|
||||
}
|
||||
|
||||
$vmLogger->success('VM start success', ['vm_id' => $vmId]);
|
||||
|
||||
$moduleVmware = $moduleService->create($this->item, $infra, $vmId);
|
||||
|
||||
$vmLogger->success('ModuleVmware created', [
|
||||
@@ -191,16 +175,19 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
'order_item_id' => $this->item->id,
|
||||
]);
|
||||
|
||||
AssignIpToGuestJob::dispatch(
|
||||
// VM is left powered off after clone. UpdateVmConfigJob owns power-on:
|
||||
// if the order has a CPU/RAM addon it powers off (no-op, already off),
|
||||
// resizes, then powers on; otherwise it just powers on. Either way it
|
||||
// dispatches AssignIpToGuestJob next, which needs the VM running.
|
||||
UpdateVmConfigJob::dispatch(
|
||||
$moduleVmware->id,
|
||||
(string) $vmId,
|
||||
$infra->isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX,
|
||||
VmProvisionService::TEMPLATE_PASSWORD,
|
||||
$reservation->id,
|
||||
$ipPoolId,
|
||||
reservationId: $reservation->id,
|
||||
reservedIpPoolId: $ipPoolId,
|
||||
)->onQueue('default');
|
||||
|
||||
$vmLogger->success('AssignIpToGuestJob dispatched', [
|
||||
$vmLogger->success('UpdateVmConfigJob dispatched', [
|
||||
'module_vmware_id' => $moduleVmware->id,
|
||||
'vm_id' => $vmId,
|
||||
'os_family' => $infra->isWindows ? 'WINDOWS' : 'LINUX',
|
||||
|
||||
@@ -31,7 +31,8 @@ use Illuminate\Support\Facades\Log;
|
||||
*
|
||||
* Assigns a real static IP to the VM guest OS and verifies it is active.
|
||||
* Only persists the IP to DB (module_vmware + virtual_machines) after
|
||||
* successful guest-side verification, then dispatches UpdateVmConfigJob → ChangePasswordViaVsphereJob.
|
||||
* successful guest-side verification, then dispatches ChangePasswordViaVsphereJob.
|
||||
* CPU/RAM config (UpdateVmConfigJob) already ran before this job, right after clone.
|
||||
*
|
||||
* Flow:
|
||||
* [1] Resolve network config from order item → product → ipAddress template
|
||||
@@ -41,7 +42,7 @@ use Illuminate\Support\Facades\Log;
|
||||
* [5] On FAIL → release IP, try next
|
||||
* [6] Call verifyIPLinux.ps1 / verifyIPWindows.ps1 via SSH
|
||||
* [7] On FAIL → release IP, try next
|
||||
* [8] SUCCESS → persistIp() → dispatch UpdateVmConfigJob → ChangePasswordViaVsphereJob
|
||||
* [8] SUCCESS → persistIp() → dispatch ChangePasswordViaVsphereJob
|
||||
* [9] All attempts exhausted → mark VM inactive (status=0)
|
||||
*/
|
||||
class AssignIpToGuestJob implements ShouldQueue
|
||||
@@ -186,7 +187,7 @@ class AssignIpToGuestJob implements ShouldQueue
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
|
||||
UpdateVmConfigJob::dispatch(
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
$this->moduleVmwareId,
|
||||
$this->vmId,
|
||||
$this->osFamily,
|
||||
@@ -228,8 +229,8 @@ class AssignIpToGuestJob implements ShouldQueue
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
|
||||
// Dispatch addon config update → then password change
|
||||
UpdateVmConfigJob::dispatch(
|
||||
// CPU/RAM config was already applied by UpdateVmConfigJob before IP assignment.
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
$this->moduleVmwareId,
|
||||
$this->vmId,
|
||||
$this->osFamily,
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Models\Service;
|
||||
use App\Models\ServiceAddon;
|
||||
use App\Models\vCenter;
|
||||
use App\Services\VirtualMachine\VmProvisionLogger;
|
||||
use App\Services\VmProvisionService;
|
||||
use App\Services\VsphereHostSshService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
@@ -26,22 +27,27 @@ use Illuminate\Support\Facades\Log;
|
||||
* UpdateVmConfigJob
|
||||
*
|
||||
* Applies customer-selected CPU/RAM addons (from addons_meta on the OrderItem)
|
||||
* to the live vSphere VM via updateVMConfig.ps1.
|
||||
* to the vSphere VM via updateVMConfig.ps1. Runs right after clone, while the
|
||||
* VM is still powered off — this is also what CreateVirtualMachineJob relies
|
||||
* on to power the VM on before the next step.
|
||||
*
|
||||
* Skipped if the order has no cpu/ram addon → goes straight to ChangePasswordViaVsphereJob.
|
||||
* Skipped if the order has no cpu/ram addon → just power on, dispatch AssignIpToGuestJob.
|
||||
*
|
||||
* PS1 on vSphere host must accept:
|
||||
* $server_name, $cpu, $memoryGB, $vcenter_host, $vcenter_user, $vcenter_password
|
||||
* and call Connect-VIServer before Get-VM.
|
||||
*
|
||||
* Flow:
|
||||
* Flow (order-creation path, reservationId set):
|
||||
* [1] Load ModuleVmware → order → orderItems.first() → product
|
||||
* [2] Resolve addon cpu/ram from addons_meta (code field saved by OrderService)
|
||||
* [3] If no cpu/ram addon → skip, dispatch ChangePasswordViaVsphereJob
|
||||
* [3] If no cpu/ram addon → power on, dispatch AssignIpToGuestJob
|
||||
* [4] Load vCenter credentials (same pattern as ChangePasswordViaVsphereJob)
|
||||
* [5] Compute total: base (product.cpu/ram) + addon qty
|
||||
* [6] SSH → vSphere → updateVMConfig.ps1 with vCenter creds
|
||||
* [7] Assert SUCCESS → dispatch ChangePasswordViaVsphereJob
|
||||
* [6] Power off → SSH → vSphere → updateVMConfig.ps1 → power on
|
||||
* [7] Assert SUCCESS → dispatch AssignIpToGuestJob
|
||||
*
|
||||
* Manual-override path (admin panel, reservationId null): same power-off →
|
||||
* resize → power-on, but only updates DB — no further job is dispatched.
|
||||
*/
|
||||
class UpdateVmConfigJob implements ShouldQueue
|
||||
{
|
||||
@@ -57,6 +63,8 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
public readonly string $osFamily,
|
||||
public readonly ?int $cpuOverride = null,
|
||||
public readonly ?int $ramOverride = null,
|
||||
public readonly ?int $reservationId = null,
|
||||
public readonly ?int $reservedIpPoolId = null,
|
||||
) {}
|
||||
|
||||
public function handle(VsphereHostSshService $ssh, VmProvisionLogger $logger, Telegram $telegram): void
|
||||
@@ -91,10 +99,16 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
[$addonCpu, $addonRamGb] = $this->resolveAddonCpuRam($addonsMeta);
|
||||
|
||||
if ($addonCpu === 0 && $addonRamGb === 0) {
|
||||
$logger->step('no cpu/ram addon — skipping UpdateVmConfigJob', [
|
||||
$logger->step('no cpu/ram addon — powering on and skipping resize', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
]);
|
||||
|
||||
$vcenterForPowerOn = $moduleVmware->vcenter ?? vCenter::first();
|
||||
if ($vcenterForPowerOn) {
|
||||
$this->powerCycle($vcenterForPowerOn, (string) $moduleVmware->vmware_id, $logger, powerOff: false);
|
||||
}
|
||||
|
||||
$this->nextStep();
|
||||
return;
|
||||
}
|
||||
@@ -133,9 +147,10 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
'total_ram_gb' => $totalRam,
|
||||
]);
|
||||
|
||||
if ($isManualOverride) {
|
||||
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true);
|
||||
}
|
||||
// Power off before resizing (both the create-flow addon path and admin
|
||||
// manual overrides) — hot CPU/RAM resize isn't reliable across guest
|
||||
// OSes and is what caused Set-VM to reject the config as invalid.
|
||||
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true);
|
||||
|
||||
try {
|
||||
$output = $ssh->updateVmConfig(
|
||||
@@ -147,9 +162,7 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
$vcenterPass,
|
||||
);
|
||||
} finally {
|
||||
if ($isManualOverride) {
|
||||
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false);
|
||||
}
|
||||
$this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false);
|
||||
}
|
||||
|
||||
Log::info('UpdateVmConfigJob: PS1 output', [
|
||||
@@ -273,8 +286,9 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Hot-add isn't reliable for CPU/RAM resize on every guest OS, so we
|
||||
* power-cycle around the resize (and around the power-on-only skip path)
|
||||
* to guarantee the config applies and the VM ends up running either way.
|
||||
*/
|
||||
private function powerCycle(vCenter $vcenter, string $vmwareId, VmProvisionLogger $logger, bool $powerOff): void
|
||||
{
|
||||
@@ -347,10 +361,13 @@ class UpdateVmConfigJob implements ShouldQueue
|
||||
|
||||
private function nextStep(): void
|
||||
{
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
AssignIpToGuestJob::dispatch(
|
||||
$this->moduleVmwareId,
|
||||
$this->vmId,
|
||||
$this->osFamily,
|
||||
VmProvisionService::TEMPLATE_PASSWORD,
|
||||
$this->reservationId,
|
||||
$this->reservedIpPoolId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryIn
|
||||
'tax' => 0,
|
||||
'total' => $order->total,
|
||||
'status' => InvoiceStatus::UNPAID->value,
|
||||
'due_date' => now()->addDays(7)->toDateString(),
|
||||
'due_date' => now()->addMonths($maxMonths)->toDateString(),
|
||||
]);
|
||||
|
||||
// Create one invoice_item per order_item line + addon sub-lines
|
||||
|
||||
Reference in New Issue
Block a user