Files
portal-api/app/Http/Controllers/Api/Service/SnapshotController.php
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

251 lines
8.8 KiB
PHP

<?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 App\Services\ActivityLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SnapshotController extends Controller
{
use ResolvesVm;
public function __construct(protected ActivityLogger $activityLogger) {}
public function store(Request $request): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required'],
'description' => ['nullable', 'string', 'max:1000'],
'name' => ['nullable', 'string', 'max:80', 'regex:/^[A-Za-z0-9._-]+$/'],
], [
'name.regex' => 'Tên snapshot chỉ gồm chữ không dấu, số và các ký tự . _ -',
'name.max' => 'Tên snapshot tối đa 80 ký tự.',
]);
$vm = $this->resolveVmById($payload['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$snapshotName = $payload['name'] ?? $this->suggestName($vm);
if (ModuleSnapshot::where('module_vmware_id', $vm->id)->where('name', $snapshotName)->exists()) {
return response()->json(['error' => true, 'message' => 'Tên snapshot đã tồn tại, vui lòng chọn tên khác.'], 422);
}
$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);
$this->activityLogger->log(
event: 'created',
logName: 'Snapshot',
description: "Customer {$customer->email} tạo snapshot: {$snapshot->name} cho VM #{$vm->id}",
subject: $snapshot,
newValues: ['name' => $snapshot->name, 'vm_id' => $vm->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',
],
]);
}
private function suggestName(ModuleVmware $vm): string
{
$prefix = sprintf('%s-%s-', $vm->name, now()->format('d-m-Y'));
$sequence = ModuleSnapshot::where('module_vmware_id', $vm->id)->whereDate('created_at', today())->count() + 1;
while (ModuleSnapshot::where('module_vmware_id', $vm->id)->where('name', $prefix . $sequence)->exists()) {
$sequence++;
}
return $prefix . $sequence;
}
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, requireActiveService: false);
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) {
if ($snapshot->status === 'deleting') {
unset($payload['status']);
}
$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,
'suggested_name' => $this->suggestName($vm),
]);
}
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);
$this->activityLogger->log(
event: 'revert',
logName: 'Snapshot',
description: "Customer {$customer->email} revert snapshot: {$snapshot->name} (ID: {$snapshot->id}) cho VM #{$vm->id}",
subject: $snapshot,
);
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);
}
if ($snapshot->status === 'deleting') {
return response()->json(['error' => true, 'message' => 'Snapshot đang được xóa.'], 422);
}
$deletedNow = $snapshot->status === 'failed';
if ($deletedNow) {
$snapshot->delete();
} else {
$snapshot->update(['status' => 'deleting']);
RemoveSnapshot::dispatch($vm->id, $snapshot->id);
}
$this->activityLogger->log(
event: 'deleted',
logName: 'Snapshot',
description: "Customer {$customer->email} xóa snapshot: {$snapshot->name} (ID: {$snapshot->id}) của VM #{$vm->id}",
subject: $snapshot,
);
return response()->json([
'error' => false,
'message' => $deletedNow ? 'Đã xóa snapshot' : 'Đã gửi yêu cầu xóa snapshot vào hàng đợi',
]);
}
}