Files
portal-api/app/Jobs/VirtualMachine/Provision/UpdateVmConfigJob.php
T

252 lines
9.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Provision;
use App\Http\Facade\SignRSA;
use App\Http\Services\Telegram;
use App\Models\Addon;
use App\Models\ModuleVmware;
use App\Models\Service;
use App\Models\vCenter;
use App\Services\VirtualMachine\VmProvisionLogger;
use App\Services\VsphereHostSshService;
use Carbon\Carbon;
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;
/**
* UpdateVmConfigJob
*
* Applies customer-selected CPU/RAM addons (from addons_meta on the OrderItem)
* to the live vSphere VM via updateVMConfig.ps1.
*
* Skipped if the order has no cpu/ram addon → goes straight to ChangePasswordViaVsphereJob.
*
* PS1 on vSphere host must accept:
* $server_name, $cpu, $memoryGB, $vcenter_host, $vcenter_user, $vcenter_password
* and call Connect-VIServer before Get-VM.
*
* Flow:
* [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
* [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
*/
class UpdateVmConfigJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 180;
public int $backoff = 30;
public function __construct(
public readonly int $moduleVmwareId,
public readonly string $vmId,
public readonly string $osFamily,
) {}
public function handle(VsphereHostSshService $ssh, VmProvisionLogger $logger, Telegram $telegram): void
{
$logger->for('UpdateVmConfigJob');
$logger->step('UpdateVmConfigJob started', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
]);
$moduleVmware = ModuleVmware::with(['order.orderItems.product', 'vcenter'])->find($this->moduleVmwareId);
if (!$moduleVmware) {
Log::error('UpdateVmConfigJob: ModuleVmware not found', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
]);
$this->nextStep();
return;
}
$orderItem = $moduleVmware->order?->orderItems?->first();
$product = $orderItem?->product;
$addonsMeta = is_array($orderItem?->addons_meta) ? $orderItem->addons_meta : [];
[$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
$vcenter = $moduleVmware->vcenter ?? vCenter::first();
if (!$vcenter) {
$msg = "UpdateVmConfigJob: no vCenter record found for server [{$moduleVmware->name}]";
Log::error($msg, ['module_vmware_id' => $this->moduleVmwareId, 'server_name' => $moduleVmware->name]);
$telegram->SendMessage("🚨 <b>UpdateVmConfigJob: Lỗi</b>\n⚠️ Không tìm thấy vCenter\n🖥️ Server: <code>{$moduleVmware->name}</code>\n🕒 " . Carbon::now()->format('d/m/Y H:i:s'));
throw new \RuntimeException($msg);
}
$vcenterHost = $vcenter->host;
$vcenterUser = $vcenter->user;
$vcenterPass = SignRSA::DecryptFunc($vcenter->password);
$serverName = (string) $moduleVmware->name;
$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,
'total_cpu' => $totalCpu,
'total_ram_gb' => $totalRam,
]);
$output = $ssh->updateVmConfig(
$serverName,
$totalCpu,
$totalRam,
$vcenterHost,
$vcenterUser,
$vcenterPass,
);
Log::info('UpdateVmConfigJob: PS1 output', [
'module_vmware_id' => $this->moduleVmwareId,
'server_name' => $serverName,
'output' => trim($output),
]);
if (!str_contains(strtoupper(trim($output)), 'SUCCESS')) {
$msg = "updateVMConfig.ps1 failed for server [{$serverName}]. Output: " . trim($output);
Log::error('UpdateVmConfigJob: script returned FAIL', [
'module_vmware_id' => $this->moduleVmwareId,
'server_name' => $serverName,
'total_cpu' => $totalCpu,
'total_ram_gb' => $totalRam,
'output' => trim($output),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
]);
$telegram->SendMessage(
"⚠️ <b>UpdateVmConfigJob thất bại</b>\n" .
"🖥️ Server: <code>{$serverName}</code>\n" .
"🔁 Lần thử: {$this->attempts()}/{$this->tries}\n" .
"💻 CPU: {$totalCpu} | RAM: {$totalRam} GB\n" .
"📋 Output: <code>" . htmlspecialchars(trim($output) ?: '(trống)') . "</code>\n" .
"🕒 " . Carbon::now()->format('d/m/Y H:i:s')
);
throw new \RuntimeException($msg);
}
$logger->success('VM config updated via addon script', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
'total_cpu' => $totalCpu,
'total_ram_gb' => $totalRam,
'output' => trim($output),
]);
$this->nextStep();
}
public function failed(\Throwable $e): void
{
Log::error('UpdateVmConfigJob: all retries exhausted', [
'module_vmware_id' => $this->moduleVmwareId,
'vm_id' => $this->vmId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]);
Service::where('vm_id', $this->moduleVmwareId)->update(['status' => 'failed']);
try {
$telegram = app(Telegram::class);
$vm = ModuleVmware::with(['order.customer'])->find($this->moduleVmwareId);
$customer = $vm?->order?->customer;
$telegram->SendMessage(
"🚨 <b>UpdateVmConfigJob: Thất bại hoàn toàn</b>\n" .
"━━━━━━━━━━━━━━━━━━━━\n" .
"🖥️ VM ID: <code>{$this->vmId}</code>\n" .
"📦 Server: <code>" . ($vm?->name ?? 'N/A') . "</code>\n" .
"👤 Khách hàng: " . ($customer ? trim($customer->firstname . ' ' . $customer->lastname) : 'N/A') . "\n" .
"📧 Email: " . ($customer?->email ?? 'N/A') . "\n" .
"❌ Lỗi: <code>" . htmlspecialchars($e->getMessage()) . "</code>\n" .
"⚡ Trạng thái: VM đã bị đánh dấu inactive\n" .
"🕒 " . Carbon::now()->format('d/m/Y H:i:s') . "\n" .
"━━━━━━━━━━━━━━━━━━━━\n" .
"⚠️ Vui lòng kiểm tra và xử lý thủ công!"
);
} catch (\Throwable $te) {
Log::warning('UpdateVmConfigJob: could not send Telegram failure notification', [
'error' => $te->getMessage(),
]);
}
}
// ─────────────────────────────────────────────────────────────────────────
/**
* Resolve cpu/ram qty from addons_meta.
* Uses 'code' field saved by OrderService::resolveAddons() — no extra DB query needed.
* Falls back to Addon::find() if 'code' is missing (legacy meta without code).
*
* @param array<int, array{addon_id?:int, qty?:int, code?:string}> $addonsMeta
* @return array{int, int} [cpu_count, ram_gb]
*/
private function resolveAddonCpuRam(array $addonsMeta): array
{
$cpu = 0;
$ram = 0;
foreach ($addonsMeta as $meta) {
$qty = (int) ($meta['qty'] ?? 0);
$code = strtolower((string) ($meta['code'] ?? ''));
// Fallback: look up code from DB if not in meta (old orders)
if ($code === '' && isset($meta['addon_id'])) {
$addon = Addon::find($meta['addon_id']);
$code = strtolower((string) ($addon?->code ?? ''));
}
if ($code === 'cpu') {
$cpu += $qty;
} elseif ($code === 'ram') {
$ram += $qty;
}
}
return [$cpu, $ram];
}
private function nextStep(): void
{
ChangePasswordViaVsphereJob::dispatch(
$this->moduleVmwareId,
$this->vmId,
$this->osFamily,
);
}
}