Add VM rebuild pipeline with isolated job chain and admin log API

This commit is contained in:
baodev0
2026-05-29 18:19:38 +07:00
parent 7ae237ee9d
commit 9c68273b4a
27 changed files with 2391 additions and 199 deletions
@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin\Rebuild;
use App\Http\Controllers\Controller;
use App\Models\ReinstallLog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* AdminReinstallLogController
*
* Admin-facing read API for VM rebuild history. Surfaces:
* - which customer initiated each rebuild
* - which service / VM was rebuilt
* - which OS was chosen
* - status lifecycle + timestamps
* - the credential that was applied to the rebuilt VM (decrypted on detail)
*
* Routes:
* GET /v1/administrator/rebuild-logs → paginated index
* GET /v1/administrator/rebuild-logs/stats → counts by status
* GET /v1/administrator/rebuild-logs/{id} → full detail (incl. password)
*/
final class AdminReinstallLogController extends Controller
{
public function index(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 20), 200);
$query = ReinstallLog::query()
->with([
'customer:id,fullname,email',
'service:id,uuid,server_name,status',
'vm:id,name,vmware_id,ip',
'os:id,version,os_group_id',
'os.osGroup:id,name',
])
->orderByDesc('id');
if ($status = $request->query('status')) {
$query->where('status', $status);
}
if ($customerId = $request->query('customer_id')) {
$query->where('custome_id', (int) $customerId);
}
if ($serviceId = $request->query('service_id')) {
$query->where('service_id', (int) $serviceId);
}
if ($osId = $request->query('os_id')) {
$query->where('os_id', (int) $osId);
}
if ($initiator = $request->query('initiator')) {
$query->where('initiator', $initiator);
}
if ($from = $request->query('date_from')) {
$query->whereDate('created_at', '>=', $from);
}
if ($to = $request->query('date_to')) {
$query->whereDate('created_at', '<=', $to);
}
if ($search = trim((string) $request->query('search', ''))) {
$query->where(function ($q) use ($search) {
$q->where('ip_address', 'like', "%{$search}%")
->orWhere('old_vmware_id', 'like', "%{$search}%")
->orWhere('new_vmware_id', 'like', "%{$search}%")
->orWhereHas('customer', function ($cq) use ($search) {
$cq->where('email', 'like', "%{$search}%")
->orWhere('fullname', 'like', "%{$search}%");
})
->orWhereHas('vm', fn ($vq) => $vq->where('name', 'like', "%{$search}%"));
});
}
$paginator = $query->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => collect($paginator->items())->map(fn (ReinstallLog $log) => $this->summarize($log))->values(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
],
]);
}
public function show(int $id): JsonResponse
{
$log = ReinstallLog::with([
'customer:id,fullname,email,phone',
'service:id,uuid,server_name,status,product_id',
'service.product:id,name',
'vm:id,name,vmware_id,ip,port,user,os_id,customer_id',
'os:id,version,os_group_id,template_id',
'os.osGroup:id,name,thumbnail',
])->find($id);
if (!$log) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy log rebuild'], 404);
}
$data = $this->summarize($log);
$data['password'] = $log->decryptedPassword();
$data['error_message'] = $log->error_message;
$data['vm'] = $log->vm ? [
'id' => $log->vm->id,
'name' => $log->vm->name,
'ip' => $log->vm->ip,
'port' => $log->vm->port,
'user' => $log->vm->user,
'vmware_id' => $log->vm->vmware_id,
] : null;
return response()->json([
'error' => false,
'data' => $data,
]);
}
public function stats(): JsonResponse
{
return response()->json([
'error' => false,
'data' => [
'total' => ReinstallLog::count(),
'today' => ReinstallLog::whereDate('created_at', today())->count(),
'by_status' => ReinstallLog::selectRaw('status, COUNT(*) as count')
->groupBy('status')->pluck('count', 'status'),
'by_initiator' => ReinstallLog::selectRaw('initiator, COUNT(*) as count')
->groupBy('initiator')->pluck('count', 'initiator'),
],
]);
}
/**
* Compact list-row projection. Password is intentionally omitted here —
* detail endpoint is where the decrypted credential is returned.
*/
private function summarize(ReinstallLog $log): array
{
return [
'id' => $log->id,
'status' => $log->status,
'initiator' => $log->initiator,
'note' => $log->note,
'ip_address' => $log->ip_address,
'old_vmware_id' => $log->old_vmware_id,
'new_vmware_id' => $log->new_vmware_id,
'password_supplied_by_customer' => (bool) $log->password_supplied_by_customer,
'has_credentials' => $log->password !== null || $log->plain_password !== null,
'created_at' => $log->created_at,
'started_at' => $log->started_at,
'completed_at' => $log->completed_at,
'customer' => $log->customer ? [
'id' => $log->customer->id,
'fullname' => $log->customer->fullname,
'email' => $log->customer->email,
] : null,
'service' => $log->service ? [
'id' => $log->service->id,
'uuid' => $log->service->uuid,
'server_name' => $log->service->server_name,
'status' => $log->service->status,
] : null,
'os' => $log->os ? [
'id' => $log->os->id,
'version' => $log->os->version,
'group' => $log->os->osGroup ? [
'id' => $log->os->osGroup->id,
'name' => $log->os->osGroup->name,
] : null,
] : null,
];
}
}
@@ -11,9 +11,12 @@ use App\Models\Service;
use App\Repositories\Api\ServiceClient\ServiceClientRepository;
use App\Services\ActivityLogger;
use App\Services\Api\ServiceClient\ServiceClientService;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
class ServiceController extends Controller
{
@@ -290,12 +293,12 @@ class ServiceController extends Controller
]);
}
public function rebuild(Request $request, string $uuid, vCenter $serviceVCenter): JsonResponse
public function rebuild(Request $request, string $uuid, VmRebuildService $rebuildService): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'os_id' => ['required', 'integer', 'exists:os,id'],
'password' => ['nullable', 'string', 'max:255'],
'password' => ['nullable', 'string', 'min:8', 'max:255'],
]);
$service = Service::with(['product.osVersions', 'vmware'])
@@ -330,27 +333,26 @@ class ServiceController extends Controller
return response()->json(['success' => false, 'message' => 'Dịch vụ chưa được cấp phát VM'], 404);
}
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 rebuild via service UUID: ' . $uuid,
'created_at' => now(),
'updated_at' => now(),
]);
$result = $serviceVCenter->reBuildVPS((string) $vm->name);
if (is_string($result) || ($result['error'] ?? false)) {
return response()->json(
[
'success' => false,
'message' => is_string($result) ? $result : $result['message'] ?? 'Không thể rebuild VM',
],
400,
try {
$log = $rebuildService->dispatchRebuild(
moduleVmwareId: (int) $vm->id,
newOsId: (int) $payload['os_id'],
initiatorCustomerId: (int) $customer->id,
customPassword: !empty($payload['password']) ? (string) $payload['password'] : null,
initiator: 'client',
note: 'Client rebuild via service UUID: ' . $uuid,
);
} catch (Throwable $e) {
Log::error('Client rebuild dispatch failed', [
'service_uuid' => $uuid,
'module_vmware_id' => $vm->id,
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Không thể gửi yêu cầu rebuild: ' . $e->getMessage(),
], 400);
}
$this->activityLogger->log(
@@ -358,12 +360,21 @@ class ServiceController extends Controller
logName: 'VM',
description: "Customer {$customer->email} rebuild VM: {$vm->name} với OS ID: {$payload['os_id']} qua service UUID: {$uuid}",
subject: $vm,
newValues: ['os_id' => $payload['os_id'], 'service_uuid' => $uuid],
newValues: [
'os_id' => (int) $payload['os_id'],
'service_uuid' => $uuid,
'reinstall_log_id' => $log->id,
'custom_password' => !empty($payload['password']),
],
);
return response()->json([
'success' => true,
'message' => 'Đã gửi yêu cầu rebuild VM',
'data' => [
'reinstall_log_id' => $log->id,
'status' => $log->status,
],
]);
}
}
@@ -6,9 +6,12 @@ use App\Http\Controllers\Controller;
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
use App\Http\Services\vCenter;
use App\Services\ActivityLogger;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
class VmRebuildController extends Controller
{
@@ -56,12 +59,13 @@ class VmRebuildController extends Controller
]);
}
public function rebuild(Request $request, vCenter $serviceVCenter): JsonResponse
public function rebuild(Request $request, VmRebuildService $rebuildService): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'product' => ['required'],
'os_id' => ['required', 'integer', 'exists:os,id'],
'password' => ['nullable', 'string', 'min:8', 'max:255'],
]);
$vm = $this->resolveVmById($payload['product'], $request);
@@ -81,23 +85,25 @@ class VmRebuildController extends Controller
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(),
]);
try {
$log = $rebuildService->dispatchRebuild(
moduleVmwareId: (int) $vm->id,
newOsId: (int) $payload['os_id'],
initiatorCustomerId: (int) $customer->id,
customPassword: !empty($payload['password']) ? (string) $payload['password'] : null,
initiator: 'client',
note: 'Client request rebuild from portal-client',
);
} catch (Throwable $e) {
Log::error('VmRebuildController: dispatchRebuild failed', [
'module_vmware_id' => $vm->id,
'new_os_id' => $payload['os_id'],
'error' => $e->getMessage(),
]);
$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'),
'error' => true,
'message' => 'Không thể gửi yêu cầu rebuild: ' . $e->getMessage(),
], 400);
}
@@ -112,7 +118,12 @@ class VmRebuildController extends Controller
return response()->json([
'error' => false,
'message' => 'Đã gửi yêu cầu rebuild VM',
'data' => $result,
'data' => [
'module_vmware_id' => $vm->id,
'new_os_id' => (int) $payload['os_id'],
'reinstall_log_id' => $log->id,
'status' => $log->status,
],
]);
}