Files
portal-api/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php
T
baodev0andClaude Sonnet 5.5 62596d9063 feat(vm): hoàn tiền khi cấp VM lỗi, theo dõi job queue, uuid dịch vụ cho ticket, chuẩn hoá RAM addon
- VmProvisionException/VmProvisionRefund xử lý lỗi cấp phát VM
- QueueJobInspector + QueueMonitorController xem chi tiết job
- Thêm service uuid cho ticket, thời lượng cho activity_logs
- Migration chuẩn hoá RAM addon sang MB, trạng thái deleting cho snapshot

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 11:54:05 +07:00

254 lines
9.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Jobs\VirtualMachine\Create;
use App\Http\Services\vCenter;
use App\Jobs\VirtualMachine\Provision\UpdateVmConfigJob;
use App\Exceptions\VmProvisionException;
use App\Models\OrderItem;
use App\Models\Service;
use App\Models\VmProvisionLock;
use App\Services\ActivityLogger;
use App\Services\VirtualMachine\VmAddonResolver;
use App\Services\VirtualMachine\VmInfrastructureResolver;
use App\Services\VirtualMachine\VmIpReservationService;
use App\Services\VirtualMachine\VmModuleService;
use App\Services\VirtualMachine\VmNotificationService;
use App\Services\VirtualMachine\VmProvisionRefund;
use App\Services\VirtualMachine\VmProvisionLogger;
use App\Services\VirtualMachine\VmServiceSyncService;
use App\Services\VirtualMachine\VmVcenterErrorTranslator;
use App\Services\VmProvisionService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
use Throwable;
class CreateVirtualMachineJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** VM clone can take 5–8 minutes */
public int $timeout = 600;
/** No auto-retry — VM errors need manual review */
public int $tries = 1;
public function __construct(
public OrderItem $item,
public bool $isRetry = false,
) {
}
public function handle(
vCenter $vCenter,
VmProvisionLogger $vmLogger,
VmInfrastructureResolver $infraResolver,
VmAddonResolver $addonResolver,
VmModuleService $moduleService,
VmServiceSyncService $serviceSync,
VmNotificationService $notification,
VmVcenterErrorTranslator $errorTranslator,
VmIpReservationService $ipReservation,
VmProvisionRefund $refund,
): bool {
$reservation = null;
$ipPoolId = null;
$lock = null;
$vmLogger->for('CreateVirtualMachineJob');
try {
$customerId = $this->item->order->customer_id;
$lock = VmProvisionLock::updateOrCreate(
['order_item_id' => $this->item->id],
['customer_id' => $customerId, 'status' => 'running'],
);
$vmLogger->step('Create VM job started', [
'order_item_id' => $this->item->id,
'is_retry' => $this->isRetry,
]);
$product = $this->item->getProductAttribute();
if (!$product) {
throw new RuntimeException("Không tìm thấy sản phẩm từ OrderItem #{$this->item->id}");
}
$infra = $infraResolver->resolve($this->item, $product);
$addonData = $addonResolver->resolve($this->item);
// Reserve IP BEFORE clone so VM name on vCenter, ModuleVmware.name
// and services.server_name all carry the format {ip}_{server_name}.
// Network comes from the active chain (active host → active network);
// product->ipAddress is no longer consulted for network selection.
if (!$infra->network) {
throw new RuntimeException("Active host id={$infra->host->id} chưa có network active.");
}
$networkId = $infra->network->id;
$dns = $product->ipAddress->dns ?? '8.8.8.8';
$picked = $ipReservation->reserve($networkId, $infra->host->id, $dns);
if (!$picked) {
throw new RuntimeException(
"Không còn IP free trong pool (network_id={$networkId}) để tạo VM."
);
}
$reservation = $picked['reservation'];
$ipPoolId = $picked['ip_pool_id'];
$reservedIp = $reservation->ip;
$originalName = $this->item->server_name ?? ('VM-' . $this->item->id);
$combinedName = "{$reservedIp}_{$originalName}";
$this->item->server_name = $combinedName;
$vmLogger->step('IP reserved for VM', [
'order_item_id' => $this->item->id,
'ip' => $reservedIp,
'ip_pool_id' => $ipPoolId,
'server_name' => $combinedName,
]);
if (!$vCenter->loginServer($infra->vcenter)) {
throw new RuntimeException("Login vào vCenter thất bại với host: {$infra->vcenter->host}");
}
$vmLogger->step('VM clone started', [
'order_item_id' => $this->item->id,
'product_id' => $product->id,
'template' => $infra->template,
'host_id' => $infra->host->id,
'datastore_id' => $infra->datastore?->id,
'vcenter_id' => $infra->vcenter->id,
'folder' => $infra->folderId,
'resource_pool' => $infra->resourcePoolId,
]);
$result = $vCenter->CreateVirtualMachine([
'item' => $this->item,
'product' => $product,
'host' => $infra->host,
'datacenter' => $infra->datacenter,
'vcenter' => $infra->vcenter,
'template' => $infra->template,
'addon' => $addonData,
'datastore' => $infra->datastore,
'folder' => $infra->folderId,
'resource_pool' => $infra->resourcePoolId,
]);
if (is_array($result) && ($result['error'] ?? false)) {
throw new VmProvisionException(
$errorTranslator->translate($result, $this->item->server_name),
$errorTranslator->detail($result),
);
}
if ($result === 'Login_Again') {
throw new RuntimeException('Tạo máy ảo thất bại: cần đăng nhập lại vCenter.');
}
$vmId = is_array($result) ? ($result['data'] ?? null) : $result;
if (!$vmId || in_array($vmId, ['unknown', 'UNKNOWN'], true)) {
throw new RuntimeException('Không lấy được vm_id sau khi tạo máy ảo.');
}
$vmLogger->success('VM clone success', [
'order_item_id' => $this->item->id,
'vm_id' => $vmId,
]);
$moduleVmware = $moduleService->create($this->item, $infra, $vmId, [
'cpu' => (int) $product->cpu + $addonData['cpu'],
'ram_mb' => ((int) $product->ram + $addonData['ram']) * 1024,
'disk_gb' => (int) $product->disk + $addonData['disk'],
]);
$vmLogger->success('ModuleVmware created', [
'module_vmware_id' => $moduleVmware->id,
'vm_id' => $vmId,
]);
$serviceSync->sync($moduleVmware, $this->item);
$vmLogger->success('Service synced', [
'module_vmware_id' => $moduleVmware->id,
'order_item_id' => $this->item->id,
]);
// 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,
reservationId: $reservation->id,
reservedIpPoolId: $ipPoolId,
)->onQueue('default');
$vmLogger->success('UpdateVmConfigJob dispatched', [
'module_vmware_id' => $moduleVmware->id,
'vm_id' => $vmId,
'os_family' => $infra->isWindows ? 'WINDOWS' : 'LINUX',
]);
$notification->success($this->item, $moduleVmware, (string) $vmId);
$vmLogger->success('Create VM job success', [
'order_item_id' => $this->item->id,
'module_vmware_id' => $moduleVmware->id,
'vm_id' => $vmId,
]);
$lock?->update(['status' => 'success']);
return true;
} catch (Throwable $e) {
if ($reservation) {
$ipReservation->release($reservation, $ipPoolId);
}
$lock?->update(['status' => 'failed']);
Service::where('order_item_id', $this->item->id)
->whereIn('status', ['pending', 'provisioning'])
->update(['status' => 'failed']);
$adminDetail = $e instanceof VmProvisionException ? $e->adminDetail : $e->getMessage();
$vmLogger->exception('Create VM job failed', $e, [
'order_item_id' => $this->item->id,
'is_retry' => $this->isRetry,
'admin_detail' => $adminDetail,
]);
$failedService = Service::where('order_item_id', $this->item->id)->first();
if ($failedService) {
app(ActivityLogger::class)->log(
'provision_failed',
'service',
'Khởi tạo máy ảo thất bại — ' . $adminDetail,
$failedService,
);
}
if (!$this->isRetry) {
$refunded = $refund->refund($this->item, $e->getMessage());
$notification->failed($this->item, $e->getMessage() . ($refunded ? ' (đã tự động hoàn tiền cho khách)' : ''));
}
return false;
}
}
}