feat: implement VM snapshot management, enhance client resource loading, and refactor deployment workflow for production

This commit is contained in:
pbao9
2026-05-11 11:09:55 +07:00
parent 8aef607e1f
commit 96778e7bed
51 changed files with 2536 additions and 1249 deletions
@@ -0,0 +1,129 @@
<?php
namespace App\Http\Controllers\Api\Admin\Invoice;
use App\Http\Controllers\Controller;
use App\Jobs\Mail\SendInformationVirtualMachine;
use App\Jobs\VirtualMachine\Provision\SendVmCredentialsEmailJob;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\Setting;
use App\Services\InvoicePdfService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InvoiceTestController extends Controller
{
public function sendInvoiceEmail(Request $request, string $invoiceNumber): JsonResponse
{
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
->where('invoice_number', $invoiceNumber)
->first();
if (!$invoice) {
return response()->json([
'success' => false,
'message' => 'Không tìm thấy hóa đơn.',
], 404);
}
$customer = $invoice->customer;
if (!$customer?->email) {
return response()->json([
'success' => false,
'message' => 'Khách hàng không có email.',
], 400);
}
try {
$pdfService = app(InvoicePdfService::class);
$invoicePdf = $pdfService->generate($invoice);
$invoicePdfFileName = $pdfService->generateFileName($invoice);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Lỗi tạo PDF: ' . $e->getMessage(),
], 500);
}
$vm = ModuleVmware::with(['customer', 'os.osGroup', 'order.orderItems'])
->where('order_id', $invoice->order_id)
->first();
if ($vm) {
SendVmCredentialsEmailJob::dispatch($vm->id);
return response()->json([
'success' => true,
'message' => 'Đã dispatch SendVmCredentialsEmailJob (có PDF hóa đơn đính kèm).',
'data' => [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'customer_email' => $customer->email,
'vm_id' => $vm->id,
'pdf_file_name' => $invoicePdfFileName,
],
]);
}
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value) : null;
$emailData = [
'company_name' => $settingValue->company ?? 'Thế Giới Số',
'company_logo' => $settingValue->logo ?? '',
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
'product_name' => $invoice->invoiceItems->first()?->product?->name ?? 'Dịch vụ',
'product_billing' => 'N/A',
'product_price' => number_format((float) $invoice->total, 0, ',', '.'),
'service_host' => 'N/A',
'service_username' => 'N/A',
'service_port' => 'N/A',
'service_password' => 'N/A',
'os_name' => 'N/A',
'service_due_exp' => $invoice->due_date ? $invoice->due_date->format('d/m/Y') : 'N/A',
];
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoicePdf, $invoicePdfFileName);
return response()->json([
'success' => true,
'message' => 'Đã gửi email hóa đơn (không có VM liên kết, sử dụng thông tin hóa đơn).',
'data' => [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'customer_email' => $customer->email,
'pdf_file_name' => $invoicePdfFileName,
],
]);
}
public function previewInvoicePdf(Request $request, string $invoiceNumber)
{
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
->where('invoice_number', $invoiceNumber)
->first();
if (!$invoice) {
return response()->json([
'success' => false,
'message' => 'Không tìm thấy hóa đơn.',
], 404);
}
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value) : null;
return Pdf::loadView('pdf.invoice', [
'invoice' => $invoice,
'invoiceItems' => $invoice->invoiceItems,
'customer' => $invoice->customer,
'order' => $invoice->order,
'companyName' => $settingValue->company ?? 'Thế Giới Số',
'companyAddress' => $settingValue->address ?? '',
'companyPhone' => $settingValue->phone ?? '',
'companyEmail' => $settingValue->email ?? '',
])->stream($invoice->invoice_number . '.pdf');
}
}
@@ -87,7 +87,7 @@ class ClientController extends Controller
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource(auth('client')->user()),
'client' => new ClientResource(auth('client')->user()->loadCount(['vps', 'hosting', 'domain'])),
],
]);
}
@@ -133,7 +133,7 @@ class ClientController extends Controller
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource($result['client']),
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
'token' => $result['token'],
'refresh_token' => $result['refresh_token'],
'expires_in' => config('jwt.ttl', 60) * 60, // seconds
@@ -185,7 +185,7 @@ class ClientController extends Controller
return response()->json([
'error' => false,
'data' => [
'client' => new ClientResource($result['client']),
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
'token' => $result['token'],
'refresh_token' => $result['refresh_token'],
'expires_in' => config('jwt.ttl', 60) * 60,
@@ -282,7 +282,7 @@ class ClientController extends Controller
'token' => $result['token'],
'refresh_token' => $result['refresh_token'],
'expires_in' => config('jwt.ttl', 60) * 60,
'client' => new ClientResource($result['client']),
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
],
]);
} catch (\Illuminate\Validation\ValidationException $e) {
@@ -304,7 +304,7 @@ class ClientController extends Controller
'error' => false,
'message' => 'Cập nhật thông tin thành công',
'data' => [
'client' => new ClientResource($user),
'client' => new ClientResource($user->loadCount(['vps', 'hosting', 'domain'])),
],
]);
}
@@ -6,9 +6,11 @@ use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Client\Service\ServiceClientDetailResource;
use \App\Http\Resources\Api\Client\Service\ServiceClientResource;
use App\Http\Services\vCenter;
use App\Models\Order;
use App\Models\Service;
use App\Repositories\Api\ServiceClient\ServiceClientRepository;
use App\Services\Api\ServiceClient\ServiceClientService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller
@@ -103,4 +105,61 @@ class ServiceController extends Controller
]);
}
public function getTicketByOrderNum(Request $request, vCenter $serviceVCenter): JsonResponse
{
$orderNum = (string) $request->query('order_num', '');
if ($orderNum === '') {
return response()->json(['error' => true, 'message' => 'Thiếu order_num'], 422);
}
$customer = $request->user();
$order = Order::with(['vmWare.vCenter'])
->where('customer_id', $customer->id)
->where('order_num', $orderNum)
->first();
if (!$order) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy đơn hàng'], 404);
}
$vm = $order->vmWare->first();
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Đơn hàng chưa có VM'], 404);
}
$vcenter = $vm->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404);
}
if (!$serviceVCenter->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter']);
}
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
if ($consoleTicket === 'Login_Again') {
if ($serviceVCenter->loginServer($vcenter)) {
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
} else {
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter']);
}
}
if (is_string($consoleTicket)) {
return response()->json(['error' => true, 'message' => "Failed to get console ticket: $consoleTicket"]);
}
if ($consoleTicket['error']) {
return response()->json(['error' => true, 'message' => $consoleTicket['data']]);
}
return response()->json([
'error' => false,
'data' => $consoleTicket['data'],
'order_num' => $order->order_num,
'vm_id' => $vm->id,
]);
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Api\Service\Concerns;
use App\Models\ModuleVmware;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
trait ResolvesVm
{
protected function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
{
$customer = $request->user();
$vm = null;
if (is_string($productId) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId)) {
$service = Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
->where('uuid', $productId)
->where('customer_id', $customer->id)
->first();
if ($service && $service->vmware) {
$vm = $service->vmware;
}
} elseif (is_numeric($productId)) {
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->find((int) $productId);
} else {
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->where('vmware_id', $productId)
->where('customer_id', $customer->id)
->first();
}
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
}
if ((int) $vm->customer_id !== (int) $customer->id) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
}
return $vm;
}
protected function getConsoleTicket(\App\Http\Services\vCenter $serviceVCenter, ModuleVmware $vm): array|JsonResponse
{
$vcenter = $vm->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404);
}
if (!$serviceVCenter->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter']);
}
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
if ($consoleTicket === 'Login_Again') {
if ($serviceVCenter->loginServer($vcenter)) {
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
} else {
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter']);
}
}
if (is_string($consoleTicket)) {
return response()->json(['error' => true, 'message' => "Failed to get console ticket: $consoleTicket"]);
}
if ($consoleTicket['error']) {
return response()->json(['error' => true, 'message' => $consoleTicket['data']]);
}
return $consoleTicket;
}
}
@@ -7,12 +7,14 @@ use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Http\Controllers\Controller;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Jobs\VirtualMachine\Provision\RetryProvisioningOrchestrator;
use App\Models\Log as CustomerActivityLog;
use App\Http\Requests\Api\Order\PayOrderRequest;
use App\Http\Requests\Api\Order\StoreOrderRequest;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\Product;
use App\Models\Service;
@@ -194,6 +196,41 @@ class OrderController extends Controller
}
}
/**
* POST /v1/client/orders/{id}/retry — retry VM provisioning for a paid order
*
* Dispatches RetryProvisioningOrchestrator which:
* 1. Searches vSphere for existing VM by server_name
* 2. If found → links VM to module_vmware + services
* 3. If not found → dispatches CreateVirtualMachineJob
* 4. Each failure step notifies Telegram
*/
public function retryOrderProvisioning(Request $request, int $id): JsonResponse
{
try {
$order = Order::where('id', $id)
->where('customer_id', $request->user()->id)
->first();
if (!$order) {
return $this->notFound('Đơn hàng không tồn tại.');
}
if (!$order->invoice || (int) $order->invoice->status !== InvoiceStatus::PAID->value) {
return $this->error('Đơn hàng chưa được thanh toán.');
}
RetryProvisioningOrchestrator::dispatch($order->id, $request->user()->id);
return response()->json([
'success' => true,
'message' => 'Đã yêu cầu khởi tạo lại dịch vụ. Hệ thống sẽ kiểm tra và xử lý trong vài phút.',
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Invoices
// =========================================================================
@@ -305,15 +342,13 @@ class OrderController extends Controller
{
try {
$services = Service::query()
->with(['product', 'addons', 'invoice', 'orderItem'])
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os'])
->where('customer_id', $request->user()->id)
->orderByDesc('created_at')
->get();
$data = $services->map(function (Service $service) {
$vmware = $service->vm_id
? ModuleVmware::with(['ipAddress', 'os'])->find((int) $service->vm_id)
: null;
$vmware = $service->vmware;
return [
'id' => $service->id,
@@ -357,7 +392,7 @@ class OrderController extends Controller
{
try {
$service = Service::query()
->with(['product', 'addons', 'invoice', 'orderItem'])
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os', 'vmware.vCenter'])
->where('uuid', $uuid)
->where('customer_id', $request->user()->id)
->first();
@@ -366,9 +401,7 @@ class OrderController extends Controller
return $this->notFound('Không tìm thấy dịch vụ.');
}
$vmware = $service->vm_id
? ModuleVmware::with(['ipAddress', 'os', 'vCenter'])->find((int) $service->vm_id)
: null;
$vmware = $service->vmware;
$data = [
'id' => $service->id,
@@ -0,0 +1,197 @@
<?php
namespace App\Http\Controllers\Api\Service;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
use App\Http\Services\vCenter;
use App\Jobs\VirtualMachine\Snapshot\CreateSnapshot;
use App\Jobs\VirtualMachine\Snapshot\RevertSnapshot;
use App\Jobs\VirtualMachine\Snapshot\RemoveSnapshot;
use App\Models\ModuleSnapshot;
use App\Models\ModuleVmware;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SnapshotController extends Controller
{
use ResolvesVm;
public function store(Request $request): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required'],
'description' => ['nullable', 'string', 'max:1000'],
]);
$vm = $this->resolveVmById($payload['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$today = now()->format('d-m-Y');
$existingCount = ModuleSnapshot::where('module_vmware_id', $vm->id)
->whereDate('created_at', today())
->count();
$snapshotName = sprintf('%s-%s-%d', $vm->name, $today, $existingCount + 1);
$snapshot = ModuleSnapshot::create([
'name' => $snapshotName,
'description' => $payload['description'] ?? null,
'customer_id' => $customer->id,
'module_vmware_id' => $vm->id,
'status' => 'pending',
'snapshot_ref' => null,
]);
CreateSnapshot::dispatch($snapshot->id);
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu tạo snapshot vào hàng đợi',
'data' => [
'id' => $snapshot->id,
'name' => $snapshot->name,
'description' => $snapshot->description,
'status' => 'pending',
],
]);
}
public function index(Request $request, vCenter $serviceVCenter): JsonResponse
{
$productParam = $request->query('product');
if (!$productParam) {
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
}
$vm = $this->resolveVmById($productParam, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$remote = $serviceVCenter->getSnapShot($vm);
if (is_string($remote) || ($remote['error'] ?? true)) {
return response()->json([
'error' => true,
'message' => is_string($remote) ? $remote : ($remote['message'] ?? 'Không thể lấy danh sách snapshot'),
], 400);
}
$remoteItems = $remote['data'] ?? [];
foreach ($remoteItems as $item) {
$ref = (string) ($item['id'] ?? $item['name'] ?? '');
$name = (string) ($item['name'] ?? $ref);
if ($name === '') {
continue;
}
$snapshot = ModuleSnapshot::query()
->where('module_vmware_id', $vm->id)
->where(function ($q) use ($ref, $name) {
$q->where('snapshot_ref', $ref)->orWhere('name', $name);
})
->first();
$payload = [
'name' => $name,
'description' => $item['description'] ?? null,
'customer_id' => $request->user()->id,
'module_vmware_id' => $vm->id,
'status' => 'success',
'snapshot_ref' => $ref ?: $name,
'size_gb' => isset($item['size_gb']) ? (float) $item['size_gb'] : null,
'size_last_synced_at' => now(),
];
if ($snapshot) {
$snapshot->update($payload);
} else {
ModuleSnapshot::create($payload);
}
}
$dbSnapshots = ModuleSnapshot::query()
->where('module_vmware_id', $vm->id)
->where('customer_id', $request->user()->id)
->orderByDesc('created_at')
->get()
->map(function ($snap) {
return [
'id' => $snap->id,
'name' => $snap->name,
'description' => $snap->description,
'snapshot_ref' => $snap->snapshot_ref,
'status' => $snap->status,
'size_gb' => $snap->size_gb,
'created_at' => $snap->created_at?->toISOString(),
];
});
return response()->json([
'error' => false,
'data' => $dbSnapshots,
]);
}
public function revert(Request $request): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required'],
'snapshot_id' => ['required', 'numeric'],
]);
$vm = $this->resolveVmById($payload['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$snapshot = ModuleSnapshot::where('id', $payload['snapshot_id'])
->where('module_vmware_id', $vm->id)
->where('customer_id', $customer->id)
->first();
if (!$snapshot) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
}
RevertSnapshot::dispatch($vm->id, $snapshot->id);
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu revert snapshot vào hàng đợi',
]);
}
public function destroy(Request $request): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required'],
'snapshot_id' => ['required', 'numeric'],
]);
$vm = $this->resolveVmById($payload['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$snapshot = ModuleSnapshot::where('id', $payload['snapshot_id'])
->where('module_vmware_id', $vm->id)
->where('customer_id', $customer->id)
->first();
if (!$snapshot) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
}
RemoveSnapshot::dispatch($vm->id, $snapshot->id);
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu xóa snapshot vào hàng đợi',
]);
}
}
@@ -6,8 +6,10 @@ 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\Models\ModuleVmware;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
@@ -16,6 +18,9 @@ use Illuminate\Support\Facades\Log;
class VmController extends Controller
{
use ResolvesVm {
ResolvesVm::resolveVmById as resolveVmForClient;
}
// -------------------------------------------------------------------------
// Power ON
// -------------------------------------------------------------------------
@@ -240,6 +245,65 @@ class VmController extends Controller
], 400);
}
// -------------------------------------------------------------------------
// Console Ticket
// -------------------------------------------------------------------------
public function consoleTicket(Request $request, vCenter $serviceVCenter): JsonResponse
{
$productId = $request->get('product');
if (!$productId) {
return response()->json(['error' => true, 'message' => 'Please choose product']);
}
$vm = ModuleVmware::with('vCenter')->find((int) $productId);
if (!$vm) {
return response()->json(['error' => true, 'message' => "Can't find product"]);
}
$result = $this->getConsoleTicket($serviceVCenter, $vm);
if ($result instanceof JsonResponse) {
return $result;
}
return response()->json([
'error' => false,
'data' => $result['data'],
]);
}
// -------------------------------------------------------------------------
// Restart VM
// -------------------------------------------------------------------------
public function restart(Request $request, vCenter $serviceVCenter): JsonResponse
{
$productId = $request->input('product');
if (!$productId) {
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
}
$vm = $this->resolveVmForClient($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$result = $serviceVCenter->restartVM($vm, $vm->vmware_id);
if (is_string($result) || ($result['error'] ?? true)) {
return response()->json([
'error' => true,
'message' => is_string($result) ? $result : ($result['data'] ?? 'Không thể restart VM'),
], 400);
}
return response()->json([
'error' => false,
'message' => 'Đã gửi lệnh restart VM',
'data' => $result['data'] ?? 'success',
]);
}
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
@@ -0,0 +1,219 @@
<?php
namespace App\Http\Controllers\Api\Service;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
use App\Http\Services\vCenter;
use App\Models\ModuleVmware;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class VmRebuildController extends Controller
{
use ResolvesVm;
public function options(Request $request): JsonResponse
{
$productId = (int) $request->query('product');
if (!$productId) {
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
}
$vm = ModuleVmware::with(['order.orderItems.product.osVersions.osGroup'])
->find($productId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
}
if ((int) $vm->customer_id !== (int) $request->user()->id) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
}
$product = optional($vm->order?->orderItems?->first())->product;
if (!$product) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
}
$osOptions = $product->osVersions
->where('status', 1)
->map(fn($os) => [
'id' => $os->id,
'name' => $os->name,
'version' => $os->version,
'group' => $os->osGroup?->name,
])
->values();
return response()->json([
'error' => false,
'data' => [
'product_id' => $product->id,
'product_uuid' => $product->uuid,
'product_name' => $product->name,
'os_options' => $osOptions,
],
]);
}
public function rebuild(Request $request, vCenter $serviceVCenter): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required', 'integer'],
'os_id' => ['required', 'integer', 'exists:os,id'],
]);
$vm = ModuleVmware::with(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter'])
->find($payload['product']);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
}
if ((int) $vm->customer_id !== (int) $customer->id) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
}
$product = optional($vm->order?->orderItems?->first())->product;
if (!$product) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
}
$allowedOs = $product->osVersions()->where('status', 1)->pluck('id')->toArray();
if (!in_array((int) $payload['os_id'], array_map('intval', $allowedOs), true)) {
return response()->json(['error' => true, 'message' => 'OS không hợp lệ cho product hiện tại'], 422);
}
DB::table('reinstall_logs')->insert([
'product_id' => $vm->id,
'product_type' => 4,
'custome_id' => $customer->id,
'os_id' => $payload['os_id'],
'status' => 'pending',
'initiator' => 'client',
'note' => 'Client request rebuild from portal-client',
'created_at' => now(),
'updated_at' => now(),
]);
$result = $serviceVCenter->reBuildVPS((string) $vm->name);
if (is_string($result) || ($result['error'] ?? false)) {
return response()->json([
'error' => true,
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể rebuild VM'),
], 400);
}
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu rebuild VM',
'data' => $result,
]);
}
public function addonOptions(Request $request): JsonResponse
{
$productId = (int) $request->query('product');
if (!$productId) {
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
}
$vm = ModuleVmware::with(['order.orderItems.product.addons'])
->find($productId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
}
if ((int) $vm->customer_id !== (int) $request->user()->id) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
}
$product = optional($vm->order?->orderItems?->first())->product;
if (!$product) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
}
$addons = $product->addons()->where('status', true)->get()->map(function ($addon) {
return [
'id' => $addon->id,
'code' => strtolower((string) $addon->code),
'name' => $addon->name,
'price' => (float) $addon->price,
'min' => (int) ($addon->min ?? 0),
'max' => (int) ($addon->max ?? 0),
'step' => (int) ($addon->step ?? 1),
];
});
return response()->json([
'error' => false,
'data' => [
'product_id' => $product->id,
'product_name' => $product->name,
'base_specs' => [
'cpu' => (int) ($product->cpu ?? 0),
'ram' => (int) ($product->ram ?? 0),
'disk' => (int) ($product->disk ?? 0),
'bandwidth' => (int) ($product->bandwidth ?? 0),
'gpu' => $product->gpu,
],
'addons' => $addons->values(),
],
]);
}
public function upgradeAddons(Request $request, vCenter $serviceVCenter): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required', 'integer'],
'cpu' => ['nullable', 'integer', 'min:0'],
'ram' => ['nullable', 'integer', 'min:0'],
'disk' => ['nullable', 'integer', 'min:0'],
'bandwidth' => ['nullable', 'integer', 'min:0'],
'gpu' => ['nullable', 'string', 'max:255'],
]);
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->find($payload['product']);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
}
if ((int) $vm->customer_id !== (int) $customer->id) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
}
$vcenter = $vm->vCenter ?? $vm->ipAddress?->host?->datacenter?->vCenter;
if (!$vcenter || !$serviceVCenter->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vCenter'], 400);
}
$applied = [];
if (isset($payload['cpu']) && $payload['cpu'] > 0) {
$res = $serviceVCenter->UpgradeCPUVirtualMachine($vm->vmware_id, (int) $payload['cpu']);
if (is_array($res) && !($res['error'] ?? true)) {
$applied[] = 'cpu';
}
}
if (isset($payload['ram']) && $payload['ram'] > 0) {
$res = $serviceVCenter->UpgradeMemoryVirtualMachine($vm->vmware_id, (int) $payload['ram']);
if (is_array($res) && !($res['error'] ?? true)) {
$applied[] = 'ram';
}
}
if (isset($payload['disk']) && $payload['disk'] > 0) {
$res = $serviceVCenter->UpgradeDiskVirtualMachine($vm, (int) $payload['disk']);
if (is_array($res) && !($res['error'] ?? true)) {
$applied[] = 'disk';
}
}
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu nâng cấp addons',
'data' => [
'applied' => $applied,
'note' => 'bandwidth/gpu hiện được ghi nhận yêu cầu, chưa có luồng tự động áp vào VM',
],
]);
}
}