Refactor vCenter snapshot handling and VM operations
- Introduced a new method `resolveVCenterFromProduct` in `SnapshotService` to streamline vCenter resolution from product data. - Updated `CreateSnapshot`, `RemoveSnapshot`, and `RevertSnapshot` jobs to utilize the new vCenter resolution method. - Enhanced the `VmController` to support UUID-based service identification for VM operations. - Added new actions for changing VM IP and RDP port, encapsulating the logic for these operations. - Improved error handling and logging for VM operations, including detailed responses for API consumers. - Removed unnecessary relationships in model queries to optimize performance.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\VM;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
|
||||
class ChangeIpVMAction
|
||||
{
|
||||
public function __construct(private vCenter $vcenter) {}
|
||||
|
||||
/**
|
||||
* Change IP address of a VM.
|
||||
*
|
||||
* Linux → changeIPLinux.ps1 (-VMName, -IP, -Gateway, -DNS, -GuestUser, -GuestPassword)
|
||||
* Windows → changeWindows1line.ps1 (legacy inline variable pattern)
|
||||
*
|
||||
* @param ModuleVmware $vm
|
||||
* @param string $ipAddress
|
||||
* @param string $gateway Empty string if not provided
|
||||
* @param string $osType 'windows' | 'linux'
|
||||
* @param string $dns DNS server, default 8.8.8.8
|
||||
* @return string
|
||||
*/
|
||||
public function execute(
|
||||
ModuleVmware $vm,
|
||||
string $ipAddress,
|
||||
string $gateway,
|
||||
string $osType,
|
||||
string $dns = '8.8.8.8',
|
||||
string $guestUser = '',
|
||||
string $guestPassword = ''
|
||||
): string {
|
||||
try {
|
||||
$vcenter = $vm->vCenter
|
||||
?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
|
||||
if (!$vcenter) {
|
||||
return 'vCenter_Not_Found';
|
||||
}
|
||||
|
||||
if (!$this->vcenter->loginServer($vcenter)) {
|
||||
return 'Login_Again';
|
||||
}
|
||||
|
||||
// Use provided guest credentials, fallback to VM stored credentials
|
||||
$user = $guestUser ?: (string) $vm->user;
|
||||
$pass = $guestPassword ?: (string) $vm->getRawOriginal('password');
|
||||
|
||||
return $this->vcenter->changeIpVM(
|
||||
(string) $vm->name, // -VMName (Get-VM -Name)
|
||||
(string) $vm->vmware_id, // VMware ID (Windows -Id pattern)
|
||||
$ipAddress,
|
||||
$gateway,
|
||||
$osType,
|
||||
$user, // -GuestUser
|
||||
$pass, // -GuestPassword (plaintext)
|
||||
$dns,
|
||||
(string) $vm->hostname
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\VM;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
|
||||
class ChangePortVMAction
|
||||
{
|
||||
public function __construct(private vCenter $vcenter) {}
|
||||
|
||||
/**
|
||||
* Change the RDP port for a Windows VM.
|
||||
*
|
||||
* Delegates EXACTLY to the existing changeRDPPortWindows() — no logic is
|
||||
* duplicated here. loginServer() must be called first because
|
||||
* CommandChangePort() uses $this->apiUri / username / password to build
|
||||
* the VMware.PowerCLI connection string.
|
||||
*
|
||||
* @param ModuleVmware $vm
|
||||
* @param string $newPort
|
||||
* @return array|string
|
||||
*/
|
||||
public function execute(ModuleVmware $vm, string $newPort): array|string
|
||||
{
|
||||
try {
|
||||
$vcenter = $vm->vCenter
|
||||
?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.'];
|
||||
}
|
||||
|
||||
if (!$this->vcenter->loginServer($vcenter)) {
|
||||
return 'Login_Again';
|
||||
}
|
||||
|
||||
return $this->vcenter->changeRDPPortWindows(
|
||||
(string) $vm->name,
|
||||
(string) $vm->user,
|
||||
(string) $vm->getRawOriginal('password'),
|
||||
$newPort
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return ['error' => true, 'data' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\VM;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
|
||||
class PowerOffVMAction
|
||||
{
|
||||
public function __construct(private vCenter $vcenter) {}
|
||||
|
||||
/**
|
||||
* Hard power-off a VM (equivalent of pulling the plug).
|
||||
*
|
||||
* PowerOffVirtualMachine() calls checkApiExits() which requires a valid
|
||||
* session token, so loginServer() MUST be called first to set the token.
|
||||
*/
|
||||
public function execute(ModuleVmware $vm): array|string
|
||||
{
|
||||
try {
|
||||
$vcenter = $vm->vCenter
|
||||
?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.'];
|
||||
}
|
||||
|
||||
if (!$this->vcenter->loginServer($vcenter)) {
|
||||
return 'Login_Again';
|
||||
}
|
||||
|
||||
return $this->vcenter->PowerOffVirtualMachine($vm->vmware_id);
|
||||
} catch (\Throwable $e) {
|
||||
return ['error' => true, 'data' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\VM;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
|
||||
class PowerOnVMAction
|
||||
{
|
||||
public function __construct(private vCenter $vcenter) {}
|
||||
|
||||
/**
|
||||
* Power on a VM.
|
||||
*
|
||||
* Resolves the vCenter, logs in (sets apiUri / token), then delegates
|
||||
* to the existing StartVirtualMachine() method.
|
||||
*/
|
||||
public function execute(ModuleVmware $vm): array|string
|
||||
{
|
||||
try {
|
||||
$vcenter = $vm->vCenter
|
||||
?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.'];
|
||||
}
|
||||
|
||||
if (!$this->vcenter->loginServer($vcenter)) {
|
||||
return 'Login_Again';
|
||||
}
|
||||
|
||||
return $this->vcenter->StartVirtualMachine($vm, $vm->vmware_id);
|
||||
} catch (\Throwable $e) {
|
||||
return ['error' => true, 'data' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Service;
|
||||
|
||||
use App\Actions\VM\ChangeIpVMAction;
|
||||
use App\Actions\VM\ChangePortVMAction;
|
||||
use App\Actions\VM\PowerOffVMAction;
|
||||
use App\Actions\VM\PowerOnVMAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class VmController extends Controller
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Power ON
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* POST /v1/system/vm-power-on
|
||||
*
|
||||
* Body: { "product": <ModuleVmware.id> }
|
||||
*/
|
||||
public function powerOn(Request $request, PowerOnVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['product' => ['required', 'integer']]);
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$result = $action->execute($vm);
|
||||
|
||||
return $this->vmResponse($result, 'Đã gửi lệnh bật VM.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Power OFF
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* POST /v1/system/vm-power-off
|
||||
*
|
||||
* Body: { "product": <ModuleVmware.id> }
|
||||
*/
|
||||
public function powerOff(Request $request, PowerOffVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['product' => ['required', 'integer']]);
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$result = $action->execute($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
|
||||
*/
|
||||
public function changeIp(Request $request, ChangeIpVMAction $action): 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';
|
||||
|
||||
$raw = $action->execute(
|
||||
$vm,
|
||||
$validated['ip_address'],
|
||||
$gateway,
|
||||
$osType,
|
||||
$dns,
|
||||
$validated['guest_user'],
|
||||
$validated['guest_password']
|
||||
);
|
||||
|
||||
// Always log the raw PowerShell output for debugging
|
||||
Log::channel('Custom')->info('[changeIp] raw output', [
|
||||
'server_name' => $validated['server_name'],
|
||||
'ip_address' => $validated['ip_address'],
|
||||
'gateway' => $gateway,
|
||||
'os_type' => $osType,
|
||||
'vm_name' => $vm->name,
|
||||
'vmware_id' => $vm->vmware_id,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
|
||||
$telegram = new Telegram();
|
||||
|
||||
if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') {
|
||||
$errMsg = $raw === 'vCenter_Not_Found'
|
||||
? 'Không tìm thấy vCenter cho VM này.'
|
||||
: 'Không thể đăng nhập vào vCenter.';
|
||||
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "🚫 Lỗi: {$errMsg}");
|
||||
|
||||
return response()->json(['success' => false, 'message' => $errMsg], 400);
|
||||
}
|
||||
|
||||
if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) {
|
||||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "📋 Output:\n" . mb_substr((string) $raw, 0, 800));
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Lệnh đổi IP thất bại. Admin đã được thông báo.', 'data' => ['raw' => $raw]], 400);
|
||||
}
|
||||
|
||||
// Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo)
|
||||
// Nhưng vẫn gửi cảnh báo để theo dõi
|
||||
if (trim((string) $raw) === '') {
|
||||
$telegram->SendMessage("⚠️ Đổi IP [{$osType}] - Script chạy nhưng không có output\n"
|
||||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||||
. "📍 IP mới: {$validated['ip_address']}\n"
|
||||
. "🔗 GW: {$gateway}\n"
|
||||
. "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa.");
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã gửi lệnh đổi IP.',
|
||||
'data' => ['raw' => $raw],
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Change Port (RDP)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* POST /v1/system/vm-change-port
|
||||
*
|
||||
* Body:
|
||||
* product int ModuleVmware.id
|
||||
* new_port int New RDP port (1024–65535)
|
||||
*/
|
||||
public function changePort(Request $request, ChangePortVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'new_port' => ['required', 'integer', 'min:1024', 'max:65535'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$result = $action->execute($vm, (string) $validated['new_port']);
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Phản hồi không hợp lệ từ vCenter.',
|
||||
'data' => ['raw' => $result],
|
||||
], 400);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load a ModuleVmware by ID, verify the authenticated user owns it.
|
||||
* Returns JsonResponse on error, ModuleVmware on success.
|
||||
*/
|
||||
private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find($id);
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404);
|
||||
}
|
||||
|
||||
if ((int) $vm->customer_id !== (int) $request->user()->id) {
|
||||
return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403);
|
||||
}
|
||||
|
||||
return $vm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+271
-42
@@ -118,6 +118,49 @@ class vCenter
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy thông tin vCenter từ product (ModuleVmware) qua nhiều cách:
|
||||
* 1. Direct relation qua vcenter_id
|
||||
* 2. Qua ipAddress → host → datacenter → vCenter
|
||||
* 3. Qua Service relation (nếu Service có vcenter_id)
|
||||
*
|
||||
* @param \App\Models\ModuleVmware $product
|
||||
* @return \App\Models\vCenter|null
|
||||
*/
|
||||
public function resolveVCenterFromProduct(\App\Models\ModuleVmware $product): ?\App\Models\vCenter
|
||||
{
|
||||
// Cách 1: Ưu tiên quan hệ trực tiếp trên ModuleVmware
|
||||
if ($product->vCenter) {
|
||||
Log::channel('Custom')->debug('resolveVCenter: Found via direct vCenter relation', ['vcenter_id' => $product->vCenter->id]);
|
||||
return $product->vCenter;
|
||||
}
|
||||
|
||||
// Cách 2: Fallback qua ipAddress chain
|
||||
if ($product->ipAddress?->host?->datacenter?->vCenter) {
|
||||
Log::channel('Custom')->debug('resolveVCenter: Found via ipAddress chain');
|
||||
return $product->ipAddress->host->datacenter->vCenter;
|
||||
}
|
||||
|
||||
// Cách 3: Qua Service relation (nếu Service có vcenter_id)
|
||||
// Lấy service đầu tiên liên kết với VM này
|
||||
$service = $product->services()->first();
|
||||
if ($service) {
|
||||
// Load vmware relation với vCenter
|
||||
$service->load('vmware.vCenter');
|
||||
if ($service->vmware?->vCenter) {
|
||||
Log::channel('Custom')->debug('resolveVCenter: Found via Service→Vmware→vCenter');
|
||||
return $service->vmware->vCenter;
|
||||
}
|
||||
}
|
||||
|
||||
Log::channel('Custom')->warning('resolveVCenter: Could not find vCenter for product', [
|
||||
'product_id' => $product->id,
|
||||
'vmware_id' => $product->vmware_id,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAllHost($datacenter = '')
|
||||
{
|
||||
$check = $this->checkApiExits();
|
||||
@@ -294,27 +337,28 @@ class vCenter
|
||||
if (!$auth) {
|
||||
return "Login_Fail";
|
||||
}
|
||||
|
||||
try {
|
||||
$psCommand = <<<PS
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
|
||||
Import-Module VMware.PowerCLI;
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:\$false;
|
||||
|
||||
\$cred = Import-Clixml -Path 'C:\secure\vcenter.cred';
|
||||
|
||||
Connect-VIServer -Server '{$this->apiUri}' -Credential \$cred;
|
||||
|
||||
$cmd;
|
||||
|
||||
Disconnect-VIServer -Confirm:\$false
|
||||
"
|
||||
PS;
|
||||
// Build PowerShell command inline
|
||||
$psCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "' .
|
||||
'Import-Module VMware.PowerCLI; ' .
|
||||
'Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope Session; ' .
|
||||
'$cred = Import-Clixml -Path \'C:\secure\vcenter.cred\'; ' .
|
||||
'Connect-VIServer -Server \'' . $this->apiUri . '\' -Credential $cred; ' .
|
||||
$cmd . '; ' .
|
||||
'Disconnect-VIServer -Confirm:$false" 2>&1';
|
||||
|
||||
Log::channel('Custom')->info('PowerShell Command: ' . substr($psCommand, 0, 200) . '...');
|
||||
|
||||
$out = $auth->exec($psCommand);
|
||||
$err = $auth->getStdError();
|
||||
|
||||
Log::channel('Custom')->info('PowerShell Output length: ' . strlen($out));
|
||||
|
||||
return $out . ($err ? "\n[STDERR]\n" . $err : '');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('Custom')->error('Command Exception: ' . $e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
@@ -573,6 +617,69 @@ PS;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change IP address of a VM via existing PowerShell scripts.
|
||||
*
|
||||
* Linux → C:\CLISave\changeIPLinux.ps1
|
||||
* Params: -VMName -IP -Gateway -DNS -GuestUser -GuestPassword
|
||||
* Uses: Get-VM -Name $VMName + Invoke-VMScript (requires guest creds)
|
||||
* Returns JSON → parsed via CommandChangePort($cmd, true)
|
||||
*
|
||||
* Windows → C:\CLISave\changeWindows1line.ps1 (legacy inline variable pattern)
|
||||
*
|
||||
* loginServer() MUST be called before this so $this->apiUri / username /
|
||||
* password are set for the Connect-VIServer call inside CommandChangePort.
|
||||
*/
|
||||
public function changeIpVM(
|
||||
string $vmName, // VM display name used with Get-VM -Name
|
||||
string $vmwareId, // VMware ID (used for Windows -Id pattern)
|
||||
string $ipAddress,
|
||||
string $gateway,
|
||||
string $osType,
|
||||
string $guestUser = '',
|
||||
string $guestPassword = '',
|
||||
string $dns = '8.8.8.8',
|
||||
string $hostname = ''
|
||||
): string {
|
||||
$q = fn(string $v) => str_replace("'", "''", $v);
|
||||
|
||||
$isLinux = str_contains(strtolower($osType), 'linux')
|
||||
|| str_contains(strtolower($osType), 'ubuntu')
|
||||
|| str_contains(strtolower($osType), 'centos')
|
||||
|| str_contains(strtolower($osType), 'rocky');
|
||||
|
||||
if ($isLinux) {
|
||||
// changeIPLinux.ps1 param(VMName, IP, Gateway, DNS, GuestUser, GuestPassword)
|
||||
$psFile = "C:\\CLISave\\changeIPLinux.ps1";
|
||||
$Command = "& '{$psFile}'"
|
||||
. " -VMName '" . $q($vmName) . "'"
|
||||
. " -IP '" . $q($ipAddress) . "'"
|
||||
. " -Gateway '" . $q($gateway) . "'"
|
||||
. " -DNS '" . $q($dns) . "'"
|
||||
. " -GuestUser '" . $q($guestUser) . "'"
|
||||
. " -GuestPassword '" . $q($guestPassword) . "'";
|
||||
|
||||
Log::channel('Custom')->info('[changeIpVM:linux] ' . $Command);
|
||||
|
||||
// Script returns JSON → use CommandChangePort with expectJson=true
|
||||
return (string) $this->CommandChangePort($Command, true);
|
||||
}
|
||||
|
||||
// Windows: legacy inline variable pattern → changeWindows1line.ps1
|
||||
$VirtualMachine = "VirtualMachine-$vmwareId";
|
||||
$hn = strtolower($hostname ?: $vmName);
|
||||
$Command = "\$vm = get-vm -Id '{$VirtualMachine}';"
|
||||
. " \$hostname = '{$q($hn)}';"
|
||||
. " \$ipadd = '{$q($ipAddress)}';"
|
||||
. " \$gw = '{$q($gateway)}';"
|
||||
. " \$password = \"'{$q($guestPassword)}'\"; "
|
||||
. " cd /; C:\\CLISave\\changeWindows1line.ps1;";
|
||||
|
||||
Log::channel('Custom')->info('[changeIpVM:windows] ' . $Command);
|
||||
|
||||
return (string) $this->Command($Command);
|
||||
}
|
||||
|
||||
public function loadDatastore($data = [], $host_id = '')
|
||||
{
|
||||
Log::info('[vCenter] loadDatastore()', [
|
||||
@@ -1672,9 +1779,22 @@ PS;
|
||||
public function CreateSnapShot($product, ?string $snapshotName = null): string|array
|
||||
{
|
||||
try {
|
||||
$this->username = $product->ipaddress->host->datacenter->vCenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($product->ipaddress->host->datacenter->vCenter->password);
|
||||
$this->apiUri = $product->ipaddress->host->datacenter->vCenter->host;
|
||||
// Sử dụng helper method để resolve vCenter từ product
|
||||
$vcenter = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->username = $vcenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($vcenter->password);
|
||||
$this->apiUri = $vcenter->host;
|
||||
|
||||
// Login to get token
|
||||
$loginResult = $this->loginServer($vcenter);
|
||||
if (!$loginResult) {
|
||||
return "Login_Again";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
|
||||
}
|
||||
@@ -1692,18 +1812,34 @@ PS;
|
||||
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
$Command = "\$vm_name = '$vmware_name';\$name = '$Snapshot'; cd /; C:\CLISave\createsnapshot.ps1;";
|
||||
$Command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name '$vmware_name' -name '$Snapshot'";
|
||||
|
||||
Log::channel('Custom')->info("$Command");
|
||||
|
||||
$this->Command($Command);
|
||||
$cmdResult = $this->Command($Command);
|
||||
|
||||
Log::channel('Custom')->info("Command Result: " . $cmdResult);
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
Log::channel('Custom')->info("After run Command |" . Carbon::now());
|
||||
|
||||
Log::channel('Custom')->info("===================================================================");
|
||||
|
||||
// Check if command failed
|
||||
if (strpos($cmdResult, 'Login_Fail') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'SSH Login to PowerShell server failed'
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'PowerShell command failed: ' . $cmdResult
|
||||
];
|
||||
}
|
||||
|
||||
// Clear snapshot cache for this VM
|
||||
$this->clearSnapshotCache($product);
|
||||
|
||||
@@ -1739,9 +1875,16 @@ PS;
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$this->username = $product->ipaddress->host->datacenter->vCenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($product->ipaddress->host->datacenter->vCenter->password);
|
||||
$this->apiUri = $product->ipaddress->host->datacenter->vCenter->host;
|
||||
// Sử dụng helper method để resolve vCenter từ product
|
||||
$vcenter = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->username = $vcenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($vcenter->password);
|
||||
$this->apiUri = $vcenter->host;
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
|
||||
}
|
||||
@@ -1919,28 +2062,52 @@ PS;
|
||||
private function callSoap($soapBody, $cookie = null)
|
||||
{
|
||||
$envelope = '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:vim25="urn:vim25">
|
||||
' . $soapBody . '
|
||||
</soapenv:Envelope>';
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'text/xml; charset=utf-8',
|
||||
'SOAPAction' => 'urn:vim25/2005'
|
||||
$httpHeaders = [
|
||||
'Content-Type: text/xml; charset=utf-8',
|
||||
'SOAPAction: "urn:vim25/6.0"'
|
||||
];
|
||||
|
||||
if ($cookie) {
|
||||
$headers['Cookie'] = $cookie;
|
||||
$httpHeaders[] = "Cookie: {$cookie}";
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
->withHeaders($headers)
|
||||
->post("https://{$this->apiUri}/sdk", $envelope);
|
||||
// Use cURL instead of Laravel Http to avoid HTTP/2 issues with vCenter
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "https://{$this->apiUri}/sdk");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
|
||||
|
||||
$responseCookie = $response->header('Set-Cookie');
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$headers = substr($response, 0, $headerSize);
|
||||
$body = substr($response, $headerSize);
|
||||
curl_close($ch);
|
||||
|
||||
// Parse Set-Cookie header
|
||||
$responseCookie = null;
|
||||
if (preg_match('/Set-Cookie:\s*([^\r\n]+)/i', $headers, $matches)) {
|
||||
$responseCookie = trim($matches[1]);
|
||||
}
|
||||
|
||||
Log::channel('Custom')->debug('SOAP call', [
|
||||
'http_code' => $httpCode,
|
||||
'has_cookie' => !empty($responseCookie),
|
||||
'body_preview' => substr($body, 0, 200)
|
||||
]);
|
||||
|
||||
return [
|
||||
'body' => $response->body(),
|
||||
'cookie' => $responseCookie ? $responseCookie : $cookie
|
||||
'body' => $body,
|
||||
'cookie' => $responseCookie ?: $cookie
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1948,8 +2115,22 @@ PS;
|
||||
|
||||
public function RemoveSnapShot($product, $snap = ''): string|array
|
||||
{
|
||||
$this->username = $product->IpAddress->Host->Datacenter->vCenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password);
|
||||
// Sử dụng helper method để resolve vCenter từ product
|
||||
$vcenter = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->username = $vcenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($vcenter->password);
|
||||
|
||||
// Login to get token
|
||||
$loginResult = $this->loginServer($vcenter);
|
||||
if (!$loginResult) {
|
||||
return "Login_Again";
|
||||
}
|
||||
|
||||
$vmware_name = (string) $product->name;
|
||||
$check = $this->checkApiExits();
|
||||
if ($check) {
|
||||
@@ -1961,11 +2142,28 @@ PS;
|
||||
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
$Command = "\$vm_name = '$vmware_name';\$name = '$snap'; cd /; C:\CLISave\deletesnap.ps1;";
|
||||
$Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name '$vmware_name' -name '$snap'";
|
||||
|
||||
Log::channel('Custom')->info("$Command");
|
||||
|
||||
$this->Command($Command);
|
||||
$cmdResult = $this->Command($Command);
|
||||
|
||||
Log::channel('Custom')->info("Command Result: " . $cmdResult);
|
||||
|
||||
// Check if command failed
|
||||
if (strpos($cmdResult, 'Login_Fail') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'SSH Login to PowerShell server failed'
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'PowerShell command failed: ' . $cmdResult
|
||||
];
|
||||
}
|
||||
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
@@ -1996,9 +2194,23 @@ PS;
|
||||
|
||||
public function RevertSnapShot($product, $snap = ''): string|array
|
||||
{
|
||||
$this->username = $product->IpAddress->Host->Datacenter->vCenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password);
|
||||
$this->apiUri = $product->IpAddress->Host->Datacenter->vCenter->host;
|
||||
// Sử dụng helper method để resolve vCenter từ product
|
||||
$vcenter = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->username = $vcenter->user;
|
||||
$this->password = SignRSA::DecryptFunc($vcenter->password);
|
||||
$this->apiUri = $vcenter->host;
|
||||
|
||||
// Login to get token
|
||||
$loginResult = $this->loginServer($vcenter);
|
||||
if (!$loginResult) {
|
||||
return "Login_Again";
|
||||
}
|
||||
|
||||
$vmware_name = (string) $product->name;
|
||||
$check = $this->checkApiExits();
|
||||
if ($check) {
|
||||
@@ -2010,11 +2222,28 @@ PS;
|
||||
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
$Command = "\$vm_name = '$vmware_name';\$name = '$snap'; cd /; C:\CLISave\backSnap.ps1;";
|
||||
$Command = "& 'C:\CLISave\backSnap.ps1' -vm_name '$vmware_name' -name '$snap'";
|
||||
|
||||
Log::channel('Custom')->info("$Command");
|
||||
|
||||
$this->Command($Command);
|
||||
$cmdResult = $this->Command($Command);
|
||||
|
||||
Log::channel('Custom')->info("Command Result: " . $cmdResult);
|
||||
|
||||
// Check if command failed
|
||||
if (strpos($cmdResult, 'Login_Fail') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'SSH Login to PowerShell server failed'
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'PowerShell command failed: ' . $cmdResult
|
||||
];
|
||||
}
|
||||
|
||||
Log::channel('Custom')->info("\r\n");
|
||||
|
||||
|
||||
@@ -17,19 +17,54 @@ class SnapshotService
|
||||
$this->powerShellService = $powerShellService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve vCenter từ product (ModuleVmware) qua nhiều cách:
|
||||
* 1. Direct relation qua vcenter_id
|
||||
* 2. Qua ipAddress → host → datacenter → vCenter
|
||||
* 3. Qua Service relation
|
||||
*/
|
||||
protected function resolveVCenterFromProduct($product): ?\App\Models\vCenter
|
||||
{
|
||||
// Cách 1: Ưu tiên quan hệ trực tiếp trên ModuleVmware
|
||||
if ($product->vCenter) {
|
||||
return $product->vCenter;
|
||||
}
|
||||
|
||||
// Cách 2: Fallback qua ipAddress chain
|
||||
if ($product->ipaddress?->host?->datacenter?->vCenter) {
|
||||
return $product->ipaddress->host->datacenter->vCenter;
|
||||
}
|
||||
|
||||
// Cách 3: Qua Service relation
|
||||
$service = $product->services()->first();
|
||||
if ($service) {
|
||||
$service->load('vmware.vCenter');
|
||||
if ($service->vmware?->vCenter) {
|
||||
return $service->vmware->vCenter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function CreateSnapShot($product): string|array
|
||||
{
|
||||
try {
|
||||
// Note: credentials setup logic from original vCenter.php seemed redundant with Client logic,
|
||||
// but CreateSnapShot was overwriting $this->username/password.
|
||||
// but CreateSnapShot was overwriting $this->username/password.
|
||||
// Here we assume Client is used, or we need to update Client.
|
||||
// Client stores token. If we need username/password for something else, Client has them now via getters.
|
||||
// But CreateSnapshot used $product->ipaddress...->vCenter->user/password.
|
||||
|
||||
|
||||
// Re-login to ensure we have the right credentials for the vCenter associated with the product
|
||||
$vcenterModel = $product->ipaddress->host->datacenter->vCenter;
|
||||
$vcenterModel = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenterModel) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->client->loginServer($vcenterModel);
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
|
||||
}
|
||||
@@ -86,7 +121,12 @@ class SnapshotService
|
||||
// and add the private helpers it depends on.
|
||||
|
||||
try {
|
||||
$vcenterModel = $product->ipaddress->host->datacenter->vCenter;
|
||||
$vcenterModel = $this->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenterModel) {
|
||||
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
|
||||
}
|
||||
|
||||
$this->client->loginServer($vcenterModel);
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
|
||||
|
||||
@@ -25,25 +25,36 @@ class CreateSnapshot implements ShouldQueue
|
||||
|
||||
public function handle(ServiceVCenter $serviceVCenter)
|
||||
{
|
||||
$snapshot = ModuleSnapshot::with('vm.ipAddress.host.datacenter.vCenter')
|
||||
$snapshot = ModuleSnapshot::with('vm')
|
||||
->find($this->snapshotId);
|
||||
|
||||
if (!$snapshot) {
|
||||
Log::error('Snapshot not found', ['snapshot_id' => $this->snapshotId]);
|
||||
return;
|
||||
}
|
||||
|
||||
$snapshot->update(['status' => 'processing']);
|
||||
|
||||
$product = $snapshot->vm;
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter;
|
||||
|
||||
// Resolve vCenter using helper method
|
||||
$vcenter = $serviceVCenter->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
$snapshot->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => 'Không tìm thấy thông tin vCenter',
|
||||
]);
|
||||
throw new \Exception('Không tìm thấy thông tin vCenter cho VM');
|
||||
}
|
||||
|
||||
try {
|
||||
// Config vCenter
|
||||
$serviceVCenter->token = $vcenter->token;
|
||||
$serviceVCenter->apiUri = $vcenter->host;
|
||||
|
||||
// Call vCenter
|
||||
$result = $serviceVCenter->CreateSnapShot($product);
|
||||
// Call vCenter with snapshot name from DB
|
||||
$result = $serviceVCenter->CreateSnapShot($product, $snapshot->name);
|
||||
|
||||
if ($result === 'Login_Again') {
|
||||
$token = $serviceVCenter->loginServer($vcenter);
|
||||
@@ -53,7 +64,7 @@ class CreateSnapshot implements ShouldQueue
|
||||
|
||||
$vcenter->update(['token' => $token]);
|
||||
$serviceVCenter->token = $token;
|
||||
$result = $serviceVCenter->CreateSnapShot($product);
|
||||
$result = $serviceVCenter->CreateSnapShot($product, $snapshot->name);
|
||||
}
|
||||
|
||||
if (is_array($result) && ($result['error'] ?? false)) {
|
||||
@@ -62,10 +73,8 @@ class CreateSnapshot implements ShouldQueue
|
||||
|
||||
// Update DB SUCCESS
|
||||
$snapshot->update([
|
||||
'name' => $result['name'] ?? $snapshot->name,
|
||||
'status' => 'success',
|
||||
'snapshot_ref' => $result['snapshot_id'] ?? null,
|
||||
'vcenter_task_id' => $result['task_id'] ?? null,
|
||||
'snapshot_ref' => $result['data'] ?? $snapshot->name,
|
||||
]);
|
||||
|
||||
// Increment quota CHỈ khi success
|
||||
|
||||
@@ -45,21 +45,21 @@ class RemoveSnapshot implements ShouldQueue
|
||||
public function handle(ServiceVCenter $serviceVCenter)
|
||||
{
|
||||
try {
|
||||
// Load product and snapshot with necessary relationships
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId);
|
||||
// Load product and snapshot
|
||||
$product = ModuleVmware::find($this->productId);
|
||||
$snapshot = ModuleSnapshot::find($this->snapshotId);
|
||||
|
||||
if (!$product || !$snapshot) {
|
||||
throw new \Exception("Không tìm thấy thông tin VPS hoặc snapshot");
|
||||
}
|
||||
|
||||
// Kiểm tra cấu hình vCenter
|
||||
if (!$product->ipAddress?->host?->datacenter?->vCenter) {
|
||||
// Resolve vCenter using helper method
|
||||
$vcenter = $serviceVCenter->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
throw new \Exception("VPS không có thông tin vCenter được cấu hình");
|
||||
}
|
||||
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter;
|
||||
|
||||
// Set vCenter connection info
|
||||
$serviceVCenter->token = $vcenter->token;
|
||||
$serviceVCenter->apiUri = $vcenter->host;
|
||||
@@ -72,7 +72,7 @@ class RemoveSnapshot implements ShouldQueue
|
||||
]);
|
||||
|
||||
// Attempt to remove snapshot
|
||||
$result = $serviceVCenter->RemoveSnapShot($product, $snapshot->name);
|
||||
$result = $serviceVCenter->RemoveSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name);
|
||||
|
||||
// Handle re-login if needed
|
||||
if ($result === 'Login_Again') {
|
||||
@@ -81,7 +81,7 @@ class RemoveSnapshot implements ShouldQueue
|
||||
if ($token) {
|
||||
$vcenter->update(['token' => $token]);
|
||||
$serviceVCenter->token = $token;
|
||||
$result = $serviceVCenter->RemoveSnapShot($product, $snapshot->name);
|
||||
$result = $serviceVCenter->RemoveSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name);
|
||||
} else {
|
||||
throw new \Exception("Không thể đăng nhập lại vào vCenter");
|
||||
}
|
||||
|
||||
@@ -44,21 +44,21 @@ class RevertSnapshot implements ShouldQueue
|
||||
public function handle(ServiceVCenter $serviceVCenter)
|
||||
{
|
||||
try {
|
||||
// Load product and snapshot with necessary relationships
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId);
|
||||
// Load product and snapshot
|
||||
$product = ModuleVmware::find($this->productId);
|
||||
$snapshot = ModuleSnapshot::find($this->snapshotId);
|
||||
|
||||
if (!$product || !$snapshot) {
|
||||
throw new \Exception("Không tìm thấy thông tin VPS hoặc snapshot");
|
||||
}
|
||||
|
||||
// Kiểm tra cấu hình vCenter
|
||||
if (!$product->ipAddress?->host?->datacenter?->vCenter) {
|
||||
// Resolve vCenter using helper method
|
||||
$vcenter = $serviceVCenter->resolveVCenterFromProduct($product);
|
||||
|
||||
if (!$vcenter) {
|
||||
throw new \Exception("VPS không có thông tin vCenter được cấu hình");
|
||||
}
|
||||
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter;
|
||||
|
||||
// Set vCenter connection info
|
||||
$serviceVCenter->token = $vcenter->token;
|
||||
$serviceVCenter->apiUri = $vcenter->host;
|
||||
@@ -71,7 +71,7 @@ class RevertSnapshot implements ShouldQueue
|
||||
]);
|
||||
|
||||
// Attempt to revert snapshot
|
||||
$result = $serviceVCenter->RevertSnapShot($product, $snapshot->name);
|
||||
$result = $serviceVCenter->RevertSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name);
|
||||
|
||||
// Handle re-login if needed
|
||||
if ($result === 'Login_Again') {
|
||||
@@ -80,7 +80,7 @@ class RevertSnapshot implements ShouldQueue
|
||||
if ($token) {
|
||||
$vcenter->update(['token' => $token]);
|
||||
$serviceVCenter->token = $token;
|
||||
$result = $serviceVCenter->RevertSnapShot($product, $snapshot->name);
|
||||
$result = $serviceVCenter->RevertSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name);
|
||||
} else {
|
||||
throw new \Exception("Không thể đăng nhập lại vào vCenter");
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ class ModuleVmware extends Model
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = ['password' => 'hashed'];
|
||||
// NOTE: password must NOT use 'hashed' cast — it is a VM guest credential
|
||||
// used in plaintext by PowerShell scripts (Invoke-VMScript) via SSH/PowerCLI.
|
||||
protected $casts = [];
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Customer::class);
|
||||
@@ -56,6 +58,11 @@ class ModuleVmware extends Model
|
||||
return $this->belongsTo(\App\Models\Order::class);
|
||||
}
|
||||
|
||||
public function services()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Service::class, 'vm_id', 'vmware_id');
|
||||
}
|
||||
|
||||
public function getStatus()
|
||||
{
|
||||
return match ($this->status) {
|
||||
|
||||
@@ -61,6 +61,12 @@ class Service extends Model
|
||||
return $this->hasMany(ServiceAddon::class, 'service_id');
|
||||
}
|
||||
|
||||
/** The VPS/VM associated with this service (if applicable). */
|
||||
public function vmware()
|
||||
{
|
||||
return $this->belongsTo(ModuleVmware::class, 'vm_id', 'vmware_id');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
+146
-48
@@ -5,6 +5,7 @@ use App\Http\Controllers\Api\Client\ClientController;
|
||||
use App\Http\Controllers\Api\Service\OrderController;
|
||||
use App\Http\Controllers\Api\Service\ProductCategoryController;
|
||||
use App\Http\Controllers\Api\Service\ServiceController;
|
||||
use App\Http\Controllers\Api\Service\VmController;
|
||||
use App\Http\Controllers\Api\Client\Service\ServiceController as ServiceClientController;
|
||||
use App\Http\Controllers\Api\Transaction\TransactionController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -163,6 +164,14 @@ Route::prefix('v1')->as('api.')->group(function () {
|
||||
Route::get('v1/payment/vnpay/return', [TransactionController::class, 'returnCallback']);
|
||||
|
||||
Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
// VM power & network operations
|
||||
Route::controller(VmController::class)->group(function () {
|
||||
Route::post('vm-power-on', 'powerOn') ->name('vm.power-on');
|
||||
Route::post('vm-power-off', 'powerOff') ->name('vm.power-off');
|
||||
Route::post('vm-change-ip', 'changeIp') ->name('vm.change-ip');
|
||||
Route::post('vm-change-port','changePort') ->name('vm.change-port');
|
||||
});
|
||||
|
||||
Route::get('get-ticket-console', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) {
|
||||
$product_id = $request->get('product');
|
||||
if (!$product_id) {
|
||||
@@ -249,15 +258,36 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
]);
|
||||
});
|
||||
|
||||
Route::post('vm-create-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) {
|
||||
Route::post('vm-create-snapshot', function (\Illuminate\Http\Request $request) {
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'name' => ['nullable', 'string', 'max:191'],
|
||||
'product' => ['required'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']);
|
||||
// Kiểm tra nếu là UUID (Service UUID)
|
||||
$isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']);
|
||||
|
||||
if ($isUuid) {
|
||||
// Tìm qua Service UUID
|
||||
$service = \App\Models\Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service || !$service->vmware) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
$product = $service->vmware;
|
||||
} elseif (is_numeric($payload['product'])) {
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find((int)$payload['product']);
|
||||
} else {
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')
|
||||
->where('vmware_id', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
@@ -265,44 +295,68 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$result = $serviceVCenter->CreateSnapShot($product, $payload['name'] ?? null);
|
||||
if (is_string($result) || ($result['error'] ?? true)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể tạo snapshot'),
|
||||
], 400);
|
||||
}
|
||||
// Auto-generate snapshot name: {vm_name}-{date}-{count}
|
||||
$today = now()->format('d-m-Y');
|
||||
$existingCount = \App\Models\ModuleSnapshot::where('module_vmware_id', $product->id)
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
$snapshotName = sprintf('%s-%s-%d', $product->name, $today, $existingCount + 1);
|
||||
|
||||
$snapshotName = $result['data'] ?? ($payload['name'] ?? null);
|
||||
// Create snapshot record with pending status
|
||||
$snapshot = \App\Models\ModuleSnapshot::create([
|
||||
'name' => $snapshotName,
|
||||
'description' => $payload['description'] ?? null,
|
||||
'customer_id' => $customer->id,
|
||||
'module_vmware_id' => $product->id,
|
||||
'status' => 'success',
|
||||
'snapshot_ref' => $snapshotName,
|
||||
'status' => 'pending',
|
||||
'snapshot_ref' => null,
|
||||
]);
|
||||
|
||||
// Dispatch job to create snapshot
|
||||
\App\Jobs\VirtualMachine\Snapshot\CreateSnapshot::dispatch($snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã tạo snapshot thành công',
|
||||
'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,
|
||||
'snapshot_ref' => $snapshot->snapshot_ref,
|
||||
'status' => 'pending',
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
Route::get('vm-snapshots', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) {
|
||||
$customer = $request->user();
|
||||
$productId = (int)$request->query('product');
|
||||
if (!$productId) {
|
||||
$productParam = $request->query('product');
|
||||
if (!$productParam) {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($productId);
|
||||
// Kiểm tra nếu là UUID (Service UUID)
|
||||
$isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productParam);
|
||||
|
||||
if ($isUuid) {
|
||||
// Tìm qua Service UUID
|
||||
$service = \App\Models\Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $productParam)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service || !$service->vmware) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
$product = $service->vmware;
|
||||
} elseif (is_numeric($productParam)) {
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find((int)$productParam);
|
||||
} else {
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')
|
||||
->where('vmware_id', $productParam)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
@@ -374,14 +428,35 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
]);
|
||||
});
|
||||
|
||||
Route::post('vm-revert-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) {
|
||||
Route::post('vm-revert-snapshot', function (\Illuminate\Http\Request $request) {
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'snapshot' => ['required', 'string'],
|
||||
'product' => ['required'],
|
||||
'snapshot_id' => ['required', 'numeric'],
|
||||
]);
|
||||
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']);
|
||||
// Kiểm tra nếu là UUID (Service UUID)
|
||||
$isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']);
|
||||
|
||||
if ($isUuid) {
|
||||
// Tìm qua Service UUID
|
||||
$service = \App\Models\Service::with('vmware')
|
||||
->where('uuid', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service || !$service->vmware) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
$product = $service->vmware;
|
||||
} elseif (is_numeric($payload['product'])) {
|
||||
$product = \App\Models\ModuleVmware::find((int)$payload['product']);
|
||||
} else {
|
||||
$product = \App\Models\ModuleVmware::where('vmware_id', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
@@ -389,28 +464,54 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$result = $serviceVCenter->RevertSnapShot($product, $payload['snapshot']);
|
||||
if (is_string($result) || ($result['error'] ?? true)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể revert snapshot'),
|
||||
], 400);
|
||||
// Find snapshot
|
||||
$snapshot = \App\Models\ModuleSnapshot::where('id', $payload['snapshot_id'])
|
||||
->where('module_vmware_id', $product->id)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$snapshot) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
|
||||
}
|
||||
|
||||
// Dispatch job
|
||||
\App\Jobs\VirtualMachine\Snapshot\RevertSnapshot::dispatch($product->id, $snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi lệnh revert snapshot',
|
||||
'message' => 'Đã gửi yêu cầu revert snapshot vào hàng đợi',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::post('vm-delete-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) {
|
||||
Route::post('vm-delete-snapshot', function (\Illuminate\Http\Request $request) {
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'snapshot' => ['required', 'string'],
|
||||
'product' => ['required'],
|
||||
'snapshot_id' => ['required', 'numeric'],
|
||||
]);
|
||||
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']);
|
||||
// Kiểm tra nếu là UUID (Service UUID)
|
||||
$isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']);
|
||||
|
||||
if ($isUuid) {
|
||||
// Tìm qua Service UUID
|
||||
$service = \App\Models\Service::with('vmware')
|
||||
->where('uuid', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$service || !$service->vmware) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
$product = $service->vmware;
|
||||
} elseif (is_numeric($payload['product'])) {
|
||||
$product = \App\Models\ModuleVmware::find((int)$payload['product']);
|
||||
} else {
|
||||
$product = \App\Models\ModuleVmware::where('vmware_id', $payload['product'])
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
@@ -418,25 +519,22 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$result = $serviceVCenter->RemoveSnapShot($product, $payload['snapshot']);
|
||||
if (is_string($result) || ($result['error'] ?? true)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể xoá snapshot'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
\App\Models\ModuleSnapshot::query()
|
||||
// Find snapshot
|
||||
$snapshot = \App\Models\ModuleSnapshot::where('id', $payload['snapshot_id'])
|
||||
->where('module_vmware_id', $product->id)
|
||||
->where('customer_id', $customer->id)
|
||||
->where(function ($q) use ($payload) {
|
||||
$q->where('snapshot_ref', $payload['snapshot'])->orWhere('name', $payload['snapshot']);
|
||||
})
|
||||
->delete();
|
||||
->first();
|
||||
|
||||
if (!$snapshot) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
|
||||
}
|
||||
|
||||
// Dispatch job
|
||||
\App\Jobs\VirtualMachine\Snapshot\RemoveSnapshot::dispatch($product->id, $snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã xoá snapshot thành công',
|
||||
'message' => 'Đã gửi yêu cầu xóa snapshot vào hàng đợi',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user