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:
pbao9
2026-04-15 23:38:38 +07:00
parent 64a9d603e2
commit bb73d6c5a2
14 changed files with 987 additions and 120 deletions
BIN
View File
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
View File
@@ -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");
+45 -5
View File
@@ -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()];