236 lines
8.6 KiB
PHP
236 lines
8.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Jobs\VirtualMachine\Create;
|
||
|
||
use App\Http\Services\vCenter;
|
||
use App\Jobs\VirtualMachine\Provision\AssignIpToGuestJob;
|
||
use App\Models\OrderItem;
|
||
use App\Models\VmProvisionLock;
|
||
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\VmPowerService;
|
||
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,
|
||
VmPowerService $powerService,
|
||
VmModuleService $moduleService,
|
||
VmServiceSyncService $serviceSync,
|
||
VmNotificationService $notification,
|
||
VmVcenterErrorTranslator $errorTranslator,
|
||
VmIpReservationService $ipReservation,
|
||
): 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,
|
||
]);
|
||
|
||
$result = $vCenter->CreateVirtualMachine([
|
||
'item' => $this->item,
|
||
'host' => $infra->host,
|
||
'datacenter' => $infra->datacenter,
|
||
'vcenter' => $infra->vcenter,
|
||
'template' => $infra->template,
|
||
'addon' => $addonData,
|
||
'datastore' => $infra->datastore,
|
||
]);
|
||
|
||
if (is_array($result) && ($result['error'] ?? false)) {
|
||
throw new RuntimeException(
|
||
$errorTranslator->translate($result, $this->item->server_name)
|
||
);
|
||
}
|
||
|
||
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,
|
||
]);
|
||
|
||
$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', [
|
||
'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,
|
||
]);
|
||
|
||
AssignIpToGuestJob::dispatch(
|
||
$moduleVmware->id,
|
||
(string) $vmId,
|
||
$infra->isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX,
|
||
VmProvisionService::TEMPLATE_PASSWORD,
|
||
$reservation->id,
|
||
$ipPoolId,
|
||
)->onQueue('default');
|
||
|
||
$vmLogger->success('AssignIpToGuestJob 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']);
|
||
|
||
$vmLogger->exception('Create VM job failed', $e, [
|
||
'order_item_id' => $this->item->id,
|
||
'is_retry' => $this->isRetry,
|
||
]);
|
||
|
||
if (!$this->isRetry) {
|
||
$notification->failed($this->item, $e->getMessage());
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
}
|