feat: implement VM snapshot management, enhance client resource loading, and refactor deployment workflow for production
This commit is contained in:
@@ -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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user