Files
portal-api/app/Http/Controllers/Api/Service/VmController.php
T
baodev0 2deb4ad74e feat: Enhance service management and transaction handling
- Implemented client access token verification middleware to secure API endpoints.
- Updated ServiceController to fetch active hosting services for authenticated users.
- Refactored OrderController to ensure order retrieval is customer-specific.
- Improved error logging in VmController for unexpected vCenter responses.
- Disabled VNPay auto-deposit in TransactionController, switching to manual deposit flow.
- Enhanced ServiceClientDetailResource to include Plesk credentials for hosting accounts.
- Adjusted VpsServiceResource to dynamically set page type based on product type.
- Added logging for PowerShellService to capture errors in RDP port changes.
- Provisioning job now resolves service plans and logs relevant information for hosting accounts.
- Updated database migrations to add necessary fields for service plans and DNS servers.
- Refactored Service and ServicePlan models to establish relationships with hosting accounts and DNS servers.
2026-08-17 11:55:48 +07:00

563 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api\Service;
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\vCenter;
use App\Jobs\VirtualMachine\ChangeIpVMJob;
use App\Jobs\VirtualMachine\ChangeVmPasswordJob;
use App\Models\ModuleVmware;
use App\Models\Os;
use App\Models\Service;
use App\Services\ActivityLogger;
use App\Services\VmProvisionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class VmController extends Controller
{
use ResolvesVm {
ResolvesVm::resolveVmById as resolveVmForClient;
}
public function __construct(protected ActivityLogger $activityLogger) {}
// -------------------------------------------------------------------------
// Power ON
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-power-on
*
* Body: { "product": <ModuleVmware.id|vmware_id|service_uuid> }
*/
public function powerOn(Request $request, PowerOnVMAction $action): JsonResponse
{
$productId = $request->input('product');
if (!$productId) {
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
}
$vm = $this->resolveVmById($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$lock = $this->acquireVmLock($vm);
if (!$lock) {
return $this->vmLocked();
}
try {
$result = $action->execute($vm);
} finally {
$lock->release();
}
$this->activityLogger->log(
event: 'power_on',
logName: 'VM',
description: "Customer {$request->user()->email} bật VM: {$vm->name} (ID: {$vm->vmware_id})",
subject: $vm,
);
return $this->vmResponse($result, 'Đã gửi lệnh bật VM.');
}
// -------------------------------------------------------------------------
// Power OFF
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-power-off
*
* Body: { "product": <ModuleVmware.id|vmware_id|service_uuid> }
*/
public function powerOff(Request $request, PowerOffVMAction $action): JsonResponse
{
$productId = $request->input('product');
if (!$productId) {
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
}
$vm = $this->resolveVmById($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$lock = $this->acquireVmLock($vm);
if (!$lock) {
return $this->vmLocked();
}
try {
$result = $action->execute($vm);
} finally {
$lock->release();
}
$this->activityLogger->log(
event: 'power_off',
logName: 'VM',
description: "Customer {$request->user()->email} tắt VM: {$vm->name} (ID: {$vm->vmware_id})",
subject: $vm,
);
return $this->vmResponse($result, 'Đã gửi lệnh tắt VM.');
}
// -------------------------------------------------------------------------
// Change IP
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-change-ip
*
* Body:
* server_name string Service.server_name (e.g. "xasdf.com.vnx")
* ip_address string New IP to assign
* gateway string (optional) Gateway — falls back to existing IP record
*/
/**
* Dispatches ChangeIpVMJob — returns immediately, runs async.
*/
public function changeIp(Request $request): JsonResponse
{
$validated = $request->validate([
'server_name' => ['required', 'string', 'max:255'],
'ip_address' => ['required', 'ip'],
'gateway' => ['nullable', 'ip'],
'dns' => ['nullable', 'ip'],
'guest_user' => ['required', 'string', 'max:255'],
'guest_password' => ['required', 'string', 'max:255'],
]);
$customer = $request->user();
// Look up Service by server_name + owner
// VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource)
$service = Service::with([
'orderItem.order.vmWare.vCenter',
'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter',
'orderItem.os.osGroup',
])
->where('server_name', $validated['server_name'])
->where('customer_id', $customer->id)
->first();
if (!$service) {
return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404);
}
$vm = $service->orderItem?->order?->vmWare?->first();
if (!$vm) {
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404);
}
// Auto-detect OS type from the service's order item OS group
$osGroup = strtolower($service->orderItem?->os?->osGroup?->name ?? '');
$osType = (str_contains($osGroup, 'linux') || str_contains($osGroup, 'ubuntu') || str_contains($osGroup, 'centos'))
? 'linux'
: 'windows';
$gateway = $validated['gateway'] ?? '';
$dns = $validated['dns'] ?? '8.8.8.8';
// ponytail: don't release — lock expires after 5 min while the async job runs
$lock = $this->acquireVmLock($vm, 300);
if (!$lock) {
return $this->vmLocked();
}
ChangeIpVMJob::dispatch(
$vm->id,
$validated['ip_address'],
$gateway,
$osType,
$dns,
$validated['guest_user'],
$validated['guest_password'],
);
$this->activityLogger->log(
event: 'change_ip',
logName: 'VM',
description: "Customer {$request->user()->email} yêu cầu đổi IP VM: {$vm->name} → {$validated['ip_address']}",
subject: $vm,
newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType],
);
return response()->json([
'success' => true,
'message' => 'Yêu cầu đổi IP đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.',
]);
}
// -------------------------------------------------------------------------
// Change Port (RDP)
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-change-port
*
* Body:
* product int|string ModuleVmware.id|vmware_id|service_uuid
* new_port int New RDP port (1024–65535)
*/
public function changePort(Request $request, ChangePortVMAction $action): JsonResponse
{
$validated = $request->validate([
'product' => ['required'],
'new_port' => ['required', 'integer', 'min:1024', 'max:65535'],
]);
$vm = $this->resolveVmById($validated['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$lock = $this->acquireVmLock($vm);
if (!$lock) {
return $this->vmLocked();
}
try {
$result = $action->execute($vm, (string) $validated['new_port']);
} finally {
$lock->release();
}
// changeRDPPortWindows returns array on success/failure or string on login fail
if ($result === 'Login_Again' || $result === 'Login_Fail') {
return response()->json([
'success' => false,
'message' => 'Không thể đăng nhập vào vCenter.',
], 400);
}
if (is_array($result)) {
$ok = isset($result['success'])
? (bool) $result['success']
: !($result['error'] ?? true);
if ($ok) {
$this->activityLogger->log(
event: 'change_port',
logName: 'VM',
description: "Customer {$request->user()->email} đổi cổng RDP VM: {$vm->name} → port {$validated['new_port']}",
subject: $vm,
newValues: ['new_port' => $validated['new_port']],
);
}
return response()->json([
'success' => $ok,
'message' => $ok ? 'Đổi cổng RDP thành công.' : ($result['message'] ?? 'Đổi cổng thất bại.'),
'data' => $result,
], $ok ? 200 : 400);
}
\Illuminate\Support\Facades\Log::error('changePort: unexpected vCenter response', [
'vm_id' => $vm->id,
'result' => $result,
]);
return response()->json([
'success' => false,
'message' => 'Phản hồi không hợp lệ từ vCenter.',
], 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 = $this->resolveVmById($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$result = $this->getConsoleTicket($serviceVCenter, $vm);
if ($result instanceof JsonResponse) {
return $result;
}
$this->activityLogger->log(
event: 'console_access',
logName: 'VM',
description: "Customer {$request->user()->email} truy cập console VM: {$vm->name} (ID: {$vm->vmware_id})",
subject: $vm,
newValues: ['product_id' => $productId, 'vm_name' => $vm->name],
);
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;
}
$lock = $this->acquireVmLock($vm);
if (!$lock) {
return $this->vmLocked();
}
try {
$result = $serviceVCenter->restartVM($vm, $vm->vmware_id);
} finally {
$lock->release();
}
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);
}
$this->activityLogger->log(
event: 'restart',
logName: 'VM',
description: "Customer {$request->user()->email} restart VM: {$vm->name} (ID: {$vm->vmware_id})",
subject: $vm,
);
return response()->json([
'error' => false,
'message' => 'Đang restart máy ảo vui lòng chờ!',
'data' => $result['data'] ?? 'success',
]);
}
// -------------------------------------------------------------------------
// Change Password
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-change-password
*
* Body: { product, current_password, new_password }
* Dispatches ChangeVmPasswordJob — returns immediately, runs async.
*/
public function changePassword(Request $request): JsonResponse
{
$validated = $request->validate([
'product' => ['required'],
'current_password' => ['required', 'string'],
'new_password' => ['required', 'string', 'min:8'],
]);
$vm = $this->resolveVmById($validated['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
// ponytail: don't release — lock expires after 5 min while the async job runs
$lock = $this->acquireVmLock($vm, 300);
if (!$lock) {
return $this->vmLocked();
}
// Detect OS family from VM's os_id record
$os = $vm->os_id ? Os::with('osGroup')->find($vm->os_id) : null;
$osGroupName = strtolower($os?->osGroup?->name ?? '');
$osFamily = str_contains($osGroupName, 'windows')
? VmProvisionService::OS_WINDOWS
: VmProvisionService::OS_LINUX;
ChangeVmPasswordJob::dispatch(
$vm->id,
$vm->vmware_id,
$osFamily,
$validated['new_password'],
$validated['current_password'],
);
$this->activityLogger->log(
event: 'change_password',
logName: 'VM',
description: "Customer {$request->user()->email} yêu cầu đổi mật khẩu VM: {$vm->name}",
subject: $vm,
newValues: ['os_family' => $osFamily],
);
return response()->json([
'success' => true,
'message' => 'Yêu cầu đổi mật khẩu đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.',
]);
}
// -------------------------------------------------------------------------
// Power State
// -------------------------------------------------------------------------
/**
* GET /v1/system/vm-power-state?product={id|uuid|vmware_id}
*
* Calls vSphere REST API directly: GET /rest/vcenter/vm/{vmware_id}/power
* Returns: { success: true, data: { state: "POWERED_ON"|"POWERED_OFF"|"SUSPENDED", clean_power_off: bool } }
*/
public function powerState(Request $request, vCenter $serviceVCenter): JsonResponse
{
$productId = $request->input('product');
if (!$productId) {
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
}
$vm = $this->resolveVmById($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$result = $serviceVCenter->getVmPowerState($vm, $vm->vmware_id);
if ($result === 'Login_Again') {
return response()->json(['success' => false, 'message' => 'Không thể đăng nhập vào vCenter.'], 400);
}
if (is_array($result) && ($result['error'] ?? true)) {
return response()->json([
'success' => false,
'message' => $result['data'] ?? 'Không thể lấy trạng thái VM.',
], 400);
}
return response()->json([
'success' => true,
'data' => [
'state' => $result['data']['state'] ?? null,
'clean_power_off' => $result['data']['clean_power_off'] ?? null,
],
]);
}
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
/**
* Load a ModuleVmware by ID (int), vmware_id (string), or Service UUID.
* Verify the authenticated user owns it.
* Returns JsonResponse on error, ModuleVmware on success.
*/
private function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
{
$customer = $request->user();
$vm = null;
// Check if it's a UUID (Service UUID)
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)) {
// Numeric ID
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->find((int) $productId);
} else {
// vmware_id string (e.g., "vm-3238")
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->where('vmware_id', $productId)
->where('customer_id', $customer->id)
->first();
}
if (!$vm) {
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404);
}
if ((int) $vm->customer_id !== (int) $customer->id) {
return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403);
}
return $vm;
}
/**
* Load a ModuleVmware by ID, verify the authenticated user owns it.
* Returns JsonResponse on error, ModuleVmware on success.
* @deprecated Use resolveVmById instead for better ID format support
*/
private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse
{
return $this->resolveVmById($id, $request);
}
/** Try to acquire a per-VM operation lock. Returns null if already locked. */
private function acquireVmLock(ModuleVmware $vm, int $ttl = 60): ?\Illuminate\Contracts\Cache\Lock
{
$lock = Cache::lock("vm_op:{$vm->id}", $ttl);
return $lock->get() ? $lock : null;
}
private function vmLocked(): JsonResponse
{
return response()->json([
'success' => false,
'message' => 'VM đang có thao tác đang xử lý, vui lòng chờ rồi thử lại.',
], 429);
}
/**
* Normalise the array|string return value from Power ON/OFF actions.
*/
private function vmResponse(array|string $result, string $successMessage): JsonResponse
{
if ($result === 'Login_Again') {
return response()->json([
'success' => false,
'message' => 'Không thể đăng nhập vào vCenter.',
], 400);
}
if (is_string($result)) {
return response()->json(['success' => false, 'message' => $result], 400);
}
if ($result['error'] ?? true) {
return response()->json([
'success' => false,
'message' => $result['data'] ?? 'Thao tác thất bại.',
'data' => $result,
], 400);
}
return response()->json([
'success' => true,
'message' => $successMessage,
'data' => $result['data'] ?? 'success',
]);
}
}