Files
portal-api/app/Http/Services/vCenter.php
T
pbao9 bb73d6c5a2 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.
2026-04-15 23:38:38 +07:00

3181 lines
118 KiB
PHP

<?php
namespace App\Http\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use App\Models\ModuleFolder;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;
use App\Http\Facade\SignRSA;
use Illuminate\Support\Str;
use phpseclib3\Net\SSH2;
class vCenter
{
public ?string $token = null;
public string $apiUri;
private string $username;
private string $password;
private int $diskCapacity = 1073741824;
private int $memory = 1024;
private ?string $os = null;
private SSH2|WinRMClient|null $process = null;
public function loginServer(\App\Models\Vcenter $vcenterModel): bool|string
{
$this->apiUri = $vcenterModel->host;
$this->username = $vcenterModel->user;
$this->password = SignRSA::DecryptFunc($vcenterModel->password); // nếu đã mã hóa
// Log::info('Đăng nhập với user: ' . $this->username);
$response = Http::withoutVerifying()
->withBasicAuth($this->username, $this->password)
->post("https://{$this->apiUri}/rest/com/vmware/cis/session", []);
// Log::info('Response login: ' . $response->body());
if ($response->successful()) {
$json = $response->json();
if (isset($json['value'])) {
$this->token = $json['value'];
$vcenterModel->update(['token' => $this->token]);
// Log::info('token', ['token' => $this->token]);
return $this->token;
}
$body = $response->body();
if (!empty($body)) {
$this->token = $body;
$vcenterModel->update(['token' => $this->token]);
// Log::info('token raw string', ['token' => $this->token]);
return $this->token;
}
}
return false;
}
public function getAllDatacenter()
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/datacenter")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getAllVirtualMachine($datacenter = '', $host = '')
{
$check = $this->checkApiExits();
if ($check) {
$params = [];
if (!empty($datacenter)) {
$params['filter.datacenters'] = $datacenter;
}
if (!empty($host)) {
$params['filter.hosts.1'] = $host;
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm", $params)->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function checkApiExits(): bool
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/com/vmware/content/library")->json();
if (isset($response['type'])) {
return false;
}
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();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/host", [
'filter.datacenters.1' => $datacenter
])->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getAllFolder($datacenter = '')
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/folder", [
'filter.datacenters.1' => $datacenter,
'filter.type' => "VIRTUAL_MACHINE",
])->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getTemplateFromDatastore($datastore = '')
{
$allTemplate = $this->getAllTemplate();
if (!$allTemplate) {
return false;
}
foreach ($allTemplate as $template) {
$detailTemplate = $this->getTemplatebyID($template);
if ($detailTemplate && $detailTemplate === $datastore) {
return $template;
}
}
return false;
}
public function getAllTemplate()
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/com/vmware/content/library")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
protected function getTemplatebyID($template = '')
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/com/vmware/content/library/id:$template")->json();
if (!isset($response['type'])) {
return $response['value']['storage_backings'][0]['datastore_id'];
}
return false;
}
public function getAllVersion($library = ''): bool|array|string
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/com/vmware/content/library/item", [
'library_id' => $library
])->json();
if (!isset($response['type'])) {
$library_item = [];
foreach ($response['value'] as $value) {
$value_detail = $this->getVersionDetail($value);
if ($value_detail) {
$library_item[] = [
'name' => $value_detail['name'],
'id' => $value_detail['id']
];
}
}
return $library_item;
}
return false;
}
return "Login_Again";
}
protected function getVersionDetail($version_id = '')
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/com/vmware/content/library/item/id:$version_id")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
public function loadNetWork($data = [], $host_id = '')
{
$this->username = $data->user;
$this->password = SignRSA::DecryptFunc($data->password);
$this->apiUri = $data->host;
// Pass the host_id directly to the API call
$allNetwork = $this->getAllNetwork($host_id);
return $allNetwork;
}
private function getAllNetwork($host = '')
{
$check = $this->checkApiExits();
if ($check) {
$params = [];
if (!empty($host)) {
$params['filter.hosts'] = $host;
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/network", $params)->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getNetwork($host = ''): array|bool|string
{
return $this->getAllNetwork($host);
}
protected function Command($cmd)
{
$auth = $this->LoginAuth();
if (!$auth) {
return "Login_Fail";
}
try {
// 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();
}
}
private function LoginAuth()
{
try {
$connection = new SSH2('10.10.10.173', 22);
if ($connection->login('administrator', 'Thegioiso@123**$$8899TgTk2')) {
$connection->setTimeout(0); // No timeout
return $connection;
}
return false;
} catch (Exception $error) {
return $error->getCode();
}
}
// Login SSH
public function loginSSH(): bool
{
$conn = $this->LoginAuth();
if ($conn) {
Log::info("✅ Connected to vCenter via SSH successfully");
return true;
}
Log::error("❌ Failed to connect to vCenter via SSH");
return false;
}
public function runPsCommand(string $psCommand): string
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// gọi trực tiếp -Command, thu cả STDERR
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass '
. '-Command "' . str_replace('"', '\"', $psCommand) . '"';
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
protected function CommandRSP(string $cmd)
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// Escape để dùng trong chuỗi single-quoted của PowerShell
$esc = fn(string $v) => str_replace("'", "''", $v);
$server = $esc($this->apiUri ?? '');
$user = $esc($this->username ?? '');
$pass = $esc($this->password ?? '');
// Ghép script PowerShell hoàn chỉnh, chạy trong 1 scriptblock
// Lưu ý: dùng placeholder rồi strtr để tránh lẫn dấu ngoặc
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
# Thực thi lệnh công việc (đã được xây dựng sẵn, ví dụ: & 'C:\path\script.ps1' -Param 'Value' ... )
& {
<CMD>
} | Out-String
PS;
// Chèn dữ liệu
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd, // ở đây $cmd KHÔNG cần ngoặc kép bao ngoài
]);
// Encode UTF-16LE → Base64 cho -EncodedCommand
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
// Dùng -EncodedCommand để tránh mọi lỗi escape/quote
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
protected function CommandChangePort(string $cmd, bool $expectJson = false)
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
$esc = fn(string $v) => str_replace("'", "''", $v);
$server = $esc($this->apiUri ?? '');
$user = $esc($this->username ?? '');
$pass = $esc($this->password ?? '');
// PowerShell script template
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
& {
<CMD>
}
PS;
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd,
]);
// Encode UTF-16LE cho -EncodedCommand
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
if ($expectJson) {
// CẮT JSON cuối: {...} hoặc [...]
$trimmed = trim($out);
// đơn giản: bắt khối { ... } hoặc [ ... ] ở cuối chuỗi
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) {
return $m[1];
}
// không tìm thấy → trả nguyên STDOUT (để caller log raw)
return $trimmed;
}
// Trường hợp không kỳ vọng JSON: giữ nguyên hành vi cũ (append STDERR)
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string
{
// File ps1 chạy được ngoài PowerShell: bạn đã test ok
$psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1";
// Escape chuỗi cho PowerShell single-quoted
$q = fn(string $v) => str_replace("'", "''", $v);
// Xây args kiểu: -Key 'Value'
$args = " -VMName '" . $q($vmName) . "'";
$args .= " -GuestUser '" . $q($guestUser) . "'";
$args .= " -GuestPassword '" . $q($guestPassword) . "'";
$args .= " -TargetUser '" . $q($targetUser) . "'";
$args .= " -NewPassword '" . $q($newPassword) . "'";
// Gọi file trong cùng phiên PS đã Connect-VIServer (nhờ Command())
// Thêm Out-String để ép JSON nằm trên STDOUT gọn
$cmd = "& '$psFile'$args | Out-String";
$raw = $this->CommandRSP($cmd); // <-- dùng lại Command() của bạn
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
return "Login_Fail";
}
// Thử parse JSON (có thể lẫn banner/log)
$trim = trim($raw);
$data = json_decode($trim, true);
if (json_last_error() !== JSON_ERROR_NONE) {
if (preg_match('/(\{.*\}|\[.*\])/s', $trim, $m)) {
$data = json_decode($m[1], true);
}
}
return $data ?: $raw; // nếu chưa parse được, trả raw để debug
}
public function changeRDPPortWindows(string $vmName, string $guestUser, string $guestPassword, string $newPort): array|string
{
$psFile = "C:\\CLISave\\AssignIP\\change-rdp-port.ps1";
$q = fn(string $v) => str_replace("'", "''", $v);
// Dùng -VMName (string). Alias -VMNames cũng ok, nhưng mình set đúng tên tham số.
$args = " -VMName '" . $q($vmName) . "'";
$args .= " -GuestUser '" . $q($guestUser) . "'";
$args .= " -GuestPassword '" . $q($guestPassword) . "'"; // khớp tên tham số trong script 1-VM
$args .= " -NewPort " . (int) $newPort;
$cmd = "& '$psFile'$args";
// Nếu đã có phiên bản CommandRSP($cmd, bool $expectJson) → bật expectJson để không trộn STDERR
$raw = method_exists($this, 'CommandRSP')
? ($this->CommandChangePort($cmd, true) ?? '')
: '';
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
return "Login_Fail";
}
$raw = trim($raw);
// CẮT LẤY KHỐI JSON CUỐI (object hoặc array) nếu có noise
if (!preg_match('~(\{(?:[^{}]|(?1))*\}|\[(?:[^\[\]]|(?0))*\])\s*$~s', $raw, $m)) {
// fallback đơn giản hơn
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $raw, $m)) {
$raw = $m[1];
}
} else {
$raw = $m[1];
}
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'success' => false,
'error' => 'JSON parsing failed',
'raw_output' => $raw,
'json_error' => json_last_error_msg()
];
}
// Script 1-VM trả về 1 object, nhưng nếu lỡ trả mảng thì lấy phần tử đầu
if (isset($data[0])) {
$data = $data[0];
}
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()', [
'host' => $data->host,
'esxi_host' => $host_id
]);
$allDatastore = $this->getAllDataStore($host_id);
if ($allDatastore === "Login_Again") {
Log::warning('[vCenter] Token expired when getAllDataStore');
return "Login_Again";
}
if (!$allDatastore) {
Log::warning('[vCenter] getAllDataStore return false');
return false;
}
Log::info('[vCenter] Datastore count from API', [
'count' => count($allDatastore)
]);
return $allDatastore;
}
public function getAllDataStore($host = '')
{
Log::info('[vCenter] getAllDataStore call', [
'api' => $this->apiUri,
'host_filter' => $host
]);
$check = $this->checkApiExits();
if (!$check) {
Log::warning('[vCenter] checkApiExits = false');
return "Login_Again";
}
$params = [];
if (!empty($host)) {
$params['filter.hosts'] = $host;
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/datastore", $params);
Log::info('[vCenter] datastore raw response', [
'status' => $response->status(),
// 'body' => $response->body() // Comment out to reduce log noise if large
]);
$json = $response->json();
if (!isset($json['type'])) {
return $json['value'];
}
Log::warning('[vCenter] API returned error', [
'response' => $json
]);
return false;
}
public function CreateVirtualMachine($product_data): array|string
{
$vcenterModel = $product_data['vcenter'];
if (!$this->loginServer($vcenterModel)) {
Log::error('Failed to create VM: API check failed, requires login.');
return 'Login_Again';
}
$userName = customer()?->email;
$item = $product_data['item'];
$host = $product_data['host'];
$datacenter = $product_data['datacenter'];
$template = $product_data['template'];
$addon = $product_data['addon'] ?? ['cpu' => 0, 'ram' => 0, 'disk' => 0];
// Lấy cấu hình gốc từ item hoặc product
$base_cpu = $item->cpu ?? ($host->cpu ?? 1);
$base_ram = $item->ram ?? ($host->ram ?? 1);
$base_disk = $item->disk ?? ($host->disk ?? 20);
// Đảm bảo giá trị không bị null hoặc 0
$base_cpu = max(1, (int) $base_cpu);
$base_ram = max(1, (int) $base_ram);
$base_disk = max(20, (int) $base_disk);
// Lấy tất cả datastore liên quan host, không lấy first()
$datastores = \App\Models\ModuleDatastore::where('module_host_id', $host->id)->get();
// Nếu muốn chọn 1 datastore để truyền (ví dụ lấy cái đầu tiên)
$datastoreId = $datastores->isNotEmpty()
? $datastores->first()->datastore
: 'datastore-3004'; // fallback nếu không có
$body = [
'spec' => [
'name' => $item->server_name ?? ($userName . '-vm-' . $item->id),
'source' => $template, // sử dụng template của host, thay 'vm-3007' cứng thành biến
'placement' => [
'datacenter' => $datacenter->datacenter_id,
'folder' => 'group-v1008', // hoặc lấy từ DB nếu cần
'host' => $host->host_id,
'resource_pool' => 'resgroup-3002', // nếu có thể lấy động thì tốt hơn
'datastore' => $datastoreId, // chuỗi datastore đúng định dạng
],
'power_on' => false,
'fallback_to_datastore_default_policy' => true,
],
];
Log::info('Data payload', ['data' => $body]);
try {
Log::info('Bắt đầu clone VM với timeout 300 giây', ['template' => $template, 'target_name' => $body['spec']['name']]);
$response = Http::withoutVerifying()
->timeout(300) // Tăng timeout lên 5 phút cho quá trình clone
->connectTimeout(30) // Giữ connect timeout ngắn để phát hiện lỗi kết nối sớm
->withHeaders([
'vmware-api-session-id' => $this->token,
'Content-Type' => 'application/json',
])
->post("https://{$this->apiUri}/rest/vcenter/vm?action=clone", $body);
$responseData = $response->json();
if ($response->successful() && !isset($responseData['type'])) {
$vmId = $responseData['value'] ?? 'UNKNOWN';
Log::info("VM clone thành công, VM ID: {$vmId}", [
'addon_data' => $addon,
'base_cpu' => $base_cpu,
'base_ram' => $base_ram,
'has_cpu_addon' => $addon['cpu'] > 0,
'has_ram_addon' => $addon['ram'] > 0,
'has_disk_addon' => $addon['disk'] > 0
]);
// Cập nhật cấu hình phần cứng sau khi clone thành công
// Thay đổi điều kiện: chạy nếu có bất kỳ addon nào > 0
if ($vmId !== 'UNKNOWN' && ($addon['cpu'] > 0 || $addon['ram'] > 0 || $addon['disk'] > 0)) {
Log::info("Bắt đầu cập nhật addon cho VM {$vmId}");
$this->updateVMHardware($vmId, $addon, $base_cpu, $base_ram);
} else {
Log::info("Không có addon nào cần cập nhật cho VM {$vmId}", [
'vm_id' => $vmId,
'addon' => $addon,
'condition_check' => $vmId !== 'UNKNOWN' && ($addon['cpu'] > 0 || $addon['ram'] > 0 || $addon['disk'] > 0)
]);
}
// 🟢 Gửi thông báo Discord
$discord = app(\App\Http\Services\Discord::class);
// Thông tin addon
$addonMsg = '';
if (!empty($addon)) {
$addonMsg = "\n**Addon:** " .
($addon['cpu'] ? "CPU +{$addon['cpu']} " : "") .
($addon['ram'] ? "RAM +{$addon['ram']}GB " : "") .
($addon['disk'] ? "Disk +{$addon['disk']}GB " : "");
}
$discord->SendMessage(
message: "**User:** {$item->server_name}\n**VM ID:** `{$vmId}`{$addonMsg}\n**Thời gian:** " . now()->format('d/m/Y H:i:s'),
color: Discord::COLOR_SUCCESS,
title: '🎉 Tạo máy ảo thành công'
);
return ['error' => false, 'data' => $vmId];
}
return ['error' => true, 'data' => $responseData['value'] ?? $response->body()];
} catch (\Exception $e) {
Log::error('Exception when creating VM', ['message' => $e->getMessage()]);
return ['error' => true, 'data' => $e->getMessage()];
}
}
/**
* Cập nhật cấu hình phần cứng VM sau khi clone
*/
private function updateVMHardware($vmId, $addon, $base_cpu, $base_ram): void
{
try {
Log::info("=== BẮT ĐẦU CẬP NHẬT ADDON ===");
Log::info("Thông tin VM và Addon", [
'vm_id' => $vmId,
'addon' => $addon,
'base_cpu' => $base_cpu,
'base_ram' => $base_ram,
'addon_cpu' => $addon['cpu'],
'addon_ram' => $addon['ram'],
'addon_disk' => $addon['disk']
]);
// Tính toán cấu hình mới
$new_cpu = $base_cpu + $addon['cpu'];
// Sửa: RAM đầu vào là GB, convert sang MiB
$new_ram_mib = ($base_ram + $addon['ram']) * 1024; // 1 GB = 1024 MiB
Log::info("Tính toán cấu hình mới", [
'vm_id' => $vmId,
'base_cpu' => $base_cpu,
'addon_cpu' => $addon['cpu'],
'new_cpu' => $new_cpu,
'base_ram_gb' => $base_ram,
'addon_ram_gb' => $addon['ram'],
'new_ram_mib' => $new_ram_mib
]);
// Cập nhật CPU nếu có addon
if ($addon['cpu'] > 0) {
Log::info("🔄 Bắt đầu cập nhật CPU cho VM {$vmId}");
$cpuResult = $this->updateVMCPU($vmId, $new_cpu);
if ($cpuResult) {
Log::info("✅ Cập nhật CPU thành công: {$base_cpu} → {$new_cpu} cores");
} else {
Log::error("❌ Cập nhật CPU thất bại cho VM {$vmId}");
}
} else {
Log::info("⏭️ Bỏ qua cập nhật CPU (không có addon CPU)");
}
// Cập nhật RAM nếu có addon
if ($addon['ram'] > 0) {
Log::info("🔄 Bắt đầu cập nhật RAM cho VM {$vmId}");
$ramResult = $this->updateVMMemory($vmId, $new_ram_mib);
if ($ramResult) {
Log::info("✅ Cập nhật RAM thành công: {$base_ram}GB → " . ($base_ram + $addon['ram']) . "GB ({$new_ram_mib}MiB)");
} else {
Log::error("❌ Cập nhật RAM thất bại cho VM {$vmId}");
}
} else {
Log::info("⏭️ Bỏ qua cập nhật RAM (không có addon RAM)");
}
Log::info("=== HOÀN THÀNH CẬP NHẬT ADDON ===");
} catch (\Exception $e) {
Log::error("❌ Lỗi khi cập nhật cấu hình phần cứng VM {$vmId}: " . $e->getMessage());
Log::error("Stack trace: " . $e->getTraceAsString());
}
}
/**
* Cập nhật CPU cho VM
*/
private function updateVMCPU($vmId, $cpuCount): bool
{
try {
Log::info("🔄 Gọi API cập nhật CPU", [
'vm_id' => $vmId,
'new_cpu_count' => $cpuCount
]);
// Tính toán cores_per_socket dựa trên count
// Đảm bảo count % cores_per_socket == 0
$coresPerSocket = 1;
if ($cpuCount > 1) {
// Tìm ước số lớn nhất <= cpuCount/2 để tối ưu phân bổ socket
$maxCoresPerSocket = floor($cpuCount / 2);
for ($i = $maxCoresPerSocket; $i >= 1; $i--) {
if ($cpuCount % $i === 0) {
$coresPerSocket = $i;
break;
}
}
// Nếu không tìm được ước số phù hợp, sử dụng cpuCount
if ($coresPerSocket === 1 && $cpuCount > 2) {
$coresPerSocket = $cpuCount;
}
}
$payload = [
'spec' => [
'count' => $cpuCount,
'cores_per_socket' => $coresPerSocket,
'hot_add_enabled' => true
]
];
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/cpu", $payload);
if ($response->successful()) {
Log::info("✅ API call cập nhật CPU thành công cho VM {$vmId}");
return true;
} else {
Log::error("❌ API call cập nhật CPU thất bại cho VM {$vmId}", [
'status' => $response->status(),
'response' => $response->body(),
'url' => "https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/cpu"
]);
return false;
}
} catch (\Exception $e) {
Log::error("❌ Exception khi cập nhật CPU cho VM {$vmId}: " . $e->getMessage());
Log::error("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Cập nhật RAM cho VM
*/
private function updateVMMemory($vmId, $ramSizeMB): bool
{
try {
Log::info("🔄 Gọi API cập nhật RAM", [
'vm_id' => $vmId,
'new_ram_mb' => $ramSizeMB
]);
$payload = [
'spec' => [
'size_MiB' => $ramSizeMB
]
];
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/memory", $payload);
if ($response->successful()) {
Log::info("✅ API call cập nhật RAM thành công cho VM {$vmId}");
return true;
} else {
Log::error("❌ API call cập nhật RAM thất bại cho VM {$vmId}", [
'status' => $response->status(),
'response' => $response->body(),
'url' => "https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/memory"
]);
return false;
}
} catch (\Exception $e) {
Log::error("❌ Exception khi cập nhật RAM cho VM {$vmId}: " . $e->getMessage());
Log::error("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Cập nhật Disk cho VM (mở rộng disk)
*/
private function updateVMDisk($vmId, $diskSizeGB): bool
{
try {
// Tính toán dung lượng disk mới (GB sang bytes)
$diskSizeBytes = $diskSizeGB * 1024 * 1024 * 1024; // GB to bytes
$payload = [
'spec' => [
'capacity' => $diskSizeBytes
]
];
// Lấy thông tin disk hiện tại để biết disk ID
$diskResponse = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->token,
])
->get("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk");
if (!$diskResponse->successful()) {
Log::error("Không thể lấy thông tin disk hiện tại cho VM {$vmId}");
return false;
}
$diskData = $diskResponse->json();
if (!isset($diskData['value']) || empty($diskData['value'])) {
Log::error("Không tìm thấy disk nào cho VM {$vmId}");
return false;
}
// Lấy disk đầu tiên (thường là disk chính)
$firstDisk = $diskData['value'][0];
$diskKey = $firstDisk['key'];
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk/{$diskKey}", $payload);
if ($response->successful()) {
Log::info("API call cập nhật Disk thành công cho VM {$vmId}");
return true;
} else {
Log::error("API call cập nhật Disk thất bại cho VM {$vmId}", [
'status' => $response->status(),
'response' => $response->body()
]);
return false;
}
} catch (\Exception $e) {
Log::error("Exception khi cập nhật Disk cho VM {$vmId}: " . $e->getMessage());
return false;
}
}
private function GetFolderByHost($datacenter = '')
{
return ModuleFolder::where('module_datacenter_id', $datacenter)->first();
}
public function CheckVirtualMachine($vmware_id = ''): string|array
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/power")->json();
if (!isset($response['type'])) {
return [
'error' => false,
'data' => $response['value']
];
}
return [
'error' => true,
'message' => $response['value']
];
}
return "Login_Again";
}
public function GetNetworkVirtualMachine($vmware_id = ''): array
{
$response = $this->GetDetailVirtualMachine($vmware_id);
if ($response['error'] === false) {
return [
'error' => false,
'data' => $response['data']['nics'][0]['value']['backing']
];
}
return [
'error' => true,
'message' => $response['data']
];
}
public function ConfigNetworkVirtualMachine($vmware_id = '', $config = []): int
{
return Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/ethernet/4000", $config)
->status();
}
public function ConfigVirtualMachineUbuntu($vmware_id = '', $product = [], $password = ''): void
{
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("\r\n");
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createUbuntu1line.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command 1 |" . Carbon::now());
$Command = "\$vm = get-vm -Id '$VirtualMachine'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeLinux.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command 2 |" . Carbon::now());
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function GetDetailVirtualMachine($vmware_id = ''): array
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id")->json();
if (!isset($response['type'])) {
return [
'error' => false,
'data' => $response['value']
];
}
return [
'error' => true,
'message' => $response['value']
];
}
public function GetIPAddressVirtualMachine($vmware_id = ''): array
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/identity")->json();
if (!isset($response['type'])) {
return [
'error' => false,
'data' => $response['value']
];
}
return [
'error' => true,
'message' => $response['value']
];
}
public function GetVMStats($vmware_id = ''): array
{
$stats = [
'cpu' => null,
'memory' => null,
'disk' => null,
];
try {
$hardwareResponse = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware")->json();
if (is_array($hardwareResponse) && !isset($hardwareResponse['type']) && isset($hardwareResponse['value'])) {
foreach ($hardwareResponse['value'] as $hardware) {
if (isset($hardware['label']) && $hardware['label'] === 'CPU') {
$stats['cpu'] = [
'cores' => $hardware['spec']['num_cores'] ?? 0,
'cores_per_socket' => $hardware['spec']['cores_per_socket'] ?? 0,
];
}
if (isset($hardware['label']) && $hardware['label'] === 'Memory') {
$stats['memory'] = [
'size_MiB' => $hardware['spec']['size_MiB'] ?? 0,
'size_GB' => round(($hardware['spec']['size_MiB'] ?? 0) / 1024, 2),
];
}
if (isset($hardware['label']) && $hardware['label'] === 'Hard disk') {
$stats['disk'][] = [
'capacity' => $hardware['capacity'] ?? 0,
'capacity_GB' => round(($hardware['capacity'] ?? 0) / (1024 * 1024 * 1024), 2),
];
}
}
}
$guestMemoryResponse = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/memory")->json();
if (is_array($guestMemoryResponse) && !isset($guestMemoryResponse['type']) && isset($guestMemoryResponse['value'])) {
$stats['memory']['used_MiB'] = $guestMemoryResponse['value']['used'] ?? 0;
$stats['memory']['used_GB'] = round(($guestMemoryResponse['value']['used'] ?? 0) / 1024, 2);
$stats['memory']['usage_percent'] = $stats['memory']['size_MiB'] > 0
? round(($guestMemoryResponse['value']['used'] ?? 0) / $stats['memory']['size_MiB'] * 100, 1)
: 0;
}
$guestCpuResponse = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/cpu")->json();
if (is_array($guestCpuResponse) && !isset($guestCpuResponse['type']) && isset($guestCpuResponse['value'])) {
$stats['cpu']['used_percent'] = $guestCpuResponse['value']['used'] ?? 0;
}
return ['error' => false, 'data' => $stats];
} catch (\Exception $e) {
return ['error' => true, 'message' => $e->getMessage(), 'data' => $stats];
}
}
public function ConfigCentOSVersion8($vmware_id = '', $product = [], $password = ''): void
{
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("\r\n");
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS81line.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function ConfigCentOSVersion7($vmware_id = '', $product = [], $password = ''): void
{
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("\r\n");
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS71line.ps1;";
Log::channel('Custom')->info($Command);
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function ConfigWindowsVersion10($vmware_id = '', $product = [], $password = ''): void
{
try {
// Kiểm tra null safety
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
Log::error('❌ ConfigWindowsVersion10: Missing required data', [
'vmware_id' => $vmware_id,
'product_id' => $product?->id ?? 'unknown',
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
]);
return;
}
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows10.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function ConfigWindowsServer($vmware_id = '', $product = [], $password = ''): void
{
try {
// Kiểm tra null safety
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
Log::error('❌ ConfigWindowsServer: Missing required data', [
'vmware_id' => $vmware_id,
'product_id' => $product?->id ?? 'unknown',
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
]);
return;
}
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows1line.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function ConfigFixWindowsClick($vmware_id = '', $product = []): void
{
try {
// Kiểm tra null safety
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
Log::error('❌ ConfigFixWindowsClick: Missing required data', [
'vmware_id' => $vmware_id,
'product_id' => $product?->id ?? 'unknown',
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
]);
return;
}
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine'; cd /; C:\CLISave\FixTwoClickWindows.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function ConfigRockyLinuxVersion9($vmware_id = '', $product = [], $password = ''): void
{
try {
// Kiểm tra null safety
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
Log::error('❌ ConfigRockyLinuxVersion9: Missing required data', [
'vmware_id' => $vmware_id,
'product_id' => $product?->id ?? 'unknown',
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
]);
return;
}
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createRocky1line.ps1;";
Log::channel('Custom')->info($Command);
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
}
}
public function GetConsoleTickets($vmware_id = ''): array|string
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/api/vcenter/vm/$vmware_id/console/tickets", [
"type" => "WEBMKS"
])->json();
if (!isset($response['error_type'])) {
return [
'error' => false,
'data' => $response['ticket']
];
}
return [
'error' => true,
'data' => $response['messages']
];
}
return "Login_Again";
}
public function SuspendVirtualMachine($vmware_id = ''): array|string
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/suspend");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function RebootVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/power?action=reboot");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function restartVM($product, $vmware_id = ''): array|string
{
try {
$vmId = $vmware_id ?: ($product->vmware_id ?? null);
if (!$vmId) {
return [
'error' => true,
'data' => 'Không tìm thấy vmware_id',
];
}
// Ưu tiên quan hệ trực tiếp trên ModuleVmware
$vcenter = $product->vCenter ?? null;
// Fallback cho dữ liệu cũ chưa có vcenter_id
if (!$vcenter && isset($product->ipAddress) && $product->ipAddress?->host?->datacenter?->vCenter) {
$vcenter = $product->ipAddress->host->datacenter->vCenter;
}
if (!$vcenter) {
return [
'error' => true,
'data' => 'Không tìm thấy thông tin vCenter cho VM',
];
}
$check = $this->loginServer($vcenter);
if (!$check) {
return "Login_Again";
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmId/guest/power?action=reboot");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response->json('value') ?? $response->body(),
];
} catch (\Throwable $e) {
return [
'error' => true,
'data' => $e->getMessage(),
];
}
}
public function StartVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/start");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function ShutdownGuestVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/power?action=shutdown");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function PowerOffVirtualMachine($vmware_id = ''): array|string
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/stop");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function DeleteVirtualMachine($vmware_id = ''): array|string
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->delete("https://$this->apiUri/rest/vcenter/vm/$vmware_id");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function CreateSnapShot($product, ?string $snapshotName = null): string|array
{
try {
// 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()];
}
$vmware_name = (string) $product->name;
$check = $this->checkApiExits();
if ($check) {
try {
$Snapshot = $snapshotName ?: ($vmware_name . "_" . date('d_m_Y_H_i_s'));
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmware_name - Snap #$Snapshot");
Log::channel('Custom')->info("\r\n");
$Command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name '$vmware_name' -name '$Snapshot'";
Log::channel('Custom')->info("$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);
return [
'error' => false,
'data' => $Snapshot
];
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
return "Login_Again";
}
private function clearSnapshotCache($product): void
{
$vmware_name = (string) $product->name;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
Cache::forget($cacheKey);
Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$vmware_name}");
}
public function getSnapShot($product): string|array
{
$startTime = microtime(true);
try {
// 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()];
}
$vmware_name = (string) $product->name;
$vmId = $product->vmware_id;
// Check cache first
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
Log::channel('Custom')->info("📸 Snapshot cache hit");
return $cachedResult;
}
try {
Log::channel('Custom')->info("🚀 Starting SOAP Snapshot Fetch for VM: $vmId ($vmware_name)");
// 1. SOAP Login
$sessionToken = $this->soapLogin();
if (!$sessionToken) {
return "Login_Again";
}
// 2. Retrieve Snapshot Info
$snapshotInfo = $this->getVMSnapshotInfoSoap($sessionToken, $vmId);
// 3. Flatten Tree
$snapshots = [];
if (isset($snapshotInfo['rootSnapshotList'])) {
// Handle single object vs array in XML to JSON conversion
$roots = $snapshotInfo['rootSnapshotList'];
// Ensure it's an array if single
if (isset($roots['snapshot'])) {
// It's a single node structure, allow recurse
$snapshots = $this->flattenSnapshotTree($roots);
} elseif (is_array($roots)) {
foreach ($roots as $root) {
$snapshots = array_merge($snapshots, $this->flattenSnapshotTree($root));
}
}
}
// Format similar to old output
$formattedSnapshots = [];
foreach ($snapshots as $snap) {
$formattedSnapshots[] = [
'name' => $snap['name'],
'description' => $snap['description'] ?? '',
'created' => $snap['createTime'],
'id' => $snap['snapshot']['_'] ?? $snap['snapshot'], // MO Ref Value
'is_current' => false, // Simplification, need logic to check against currentSnapshot
'parent_snapshot_id' => '', // Hard to determining flat parent in simple recursion without passing parent id
'size_gb' => 0 // SOAP doesnt always return size in tree
];
}
$response = [
'error' => false,
'data' => $formattedSnapshots
];
Cache::put($cacheKey, $response, now()->addMinutes(5));
return $response;
} catch (\Exception $e) {
Log::channel('Custom')->error("❌ SOAP Snapshot Error: " . $e->getMessage());
return [
'error' => true,
'message' => 'Lỗi lấy snapshot qua SOAP: ' . $e->getMessage()
];
}
}
private function soapLogin()
{
$body = '
<soapenv:Body>
<Login xmlns="urn:vim25">
<_this type="SessionManager">SessionManager</_this>
<userName>' . htmlspecialchars($this->username) . '</userName>
<password>' . htmlspecialchars($this->password) . '</password>
</Login>
</soapenv:Body>';
$response = $this->callSoap($body);
// Extract session cookie if needed, but Login typically sets checking cookie.
// Actually for Http Client, we need to grab the cookie "vmware_soap_session"
// But the callSoap helper needs to handle cookies.
return $response['cookie'] ?? null;
}
private function getVMSnapshotInfoSoap($cookie, $vmId)
{
$body = '
<soapenv:Body>
<RetrievePropertiesEx xmlns="urn:vim25">
<_this type="PropertyCollector">propertyCollector</_this>
<specSet>
<propSet>
<type>VirtualMachine</type>
<all>false</all>
<pathSet>snapshot</pathSet>
</propSet>
<objectSet>
<obj type="VirtualMachine">' . $vmId . '</obj>
</objectSet>
</specSet>
<options></options>
</RetrievePropertiesEx>
</soapenv:Body>';
$result = $this->callSoap($body, $cookie);
// Parse simple XML response to finding snapshot info
// This is tricky with Regex/SimpleXML. We need to extract the returnval.
if (isset($result['body'])) {
$xml = simplexml_load_string($result['body']);
$xml->registerXPathNamespace('u', 'urn:vim25');
// Navigate to returnval -> objects -> propSet -> val -> rootSnapshotList
// Path depends on exact response structure
// Simplified return for now:
$json = json_encode($xml);
$array = json_decode($json, true);
// Deep dive into array to find "snapshot" property
// This needs robust traversing
return $this->findSnapshotPropertyInArray($array);
}
return [];
}
private function findSnapshotPropertyInArray($array)
{
// Recursive search for 'rootSnapshotList' or 'currentSnapshot'
if (isset($array['rootSnapshotList']))
return $array;
foreach ($array as $key => $value) {
if (is_array($value)) {
$found = $this->findSnapshotPropertyInArray($value);
if ($found)
return $found;
}
}
return [];
}
private function flattenSnapshotTree($node)
{
$list = [];
// Add self
$list[] = $node;
// Add children
if (isset($node['childSnapshotList'])) {
$children = $node['childSnapshotList'];
// Check if single child or array of children
if (isset($children['snapshot'])) {
$list = array_merge($list, $this->flattenSnapshotTree($children));
} else {
foreach ($children as $child) {
$list = array_merge($list, $this->flattenSnapshotTree($child));
}
}
}
return $list;
}
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:vim25="urn:vim25">
' . $soapBody . '
</soapenv:Envelope>';
$httpHeaders = [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "urn:vim25/6.0"'
];
if ($cookie) {
$httpHeaders[] = "Cookie: {$cookie}";
}
// 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);
$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' => $body,
'cookie' => $responseCookie ?: $cookie
];
}
public function RemoveSnapShot($product, $snap = ''): string|array
{
// 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) {
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Remove Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap");
Log::channel('Custom')->info("\r\n");
$Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name '$vmware_name' -name '$snap'";
Log::channel('Custom')->info("$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");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
// Clear snapshot cache for this VM
$this->clearSnapshotCache($product);
return [
'error' => false
];
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
return "Login_Again";
}
public function RevertSnapShot($product, $snap = ''): string|array
{
// 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) {
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Revert Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap");
Log::channel('Custom')->info("\r\n");
$Command = "& 'C:\CLISave\backSnap.ps1' -vm_name '$vmware_name' -name '$snap'";
Log::channel('Custom')->info("$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");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
// Clear snapshot cache for this VM
$this->clearSnapshotCache($product);
return [
'error' => false
];
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
return "Login_Again";
}
public function UpgradeMemoryVirtualMachine($vmware_id = '', $ram = 0): array|string
{
$Check = $this->checkApiExits();
if ($Check) {
$RAMConfig = [
"spec" => array(
"size_MiB" => $ram * $this->memory,
)
];
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/memory", $RAMConfig);
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function UpgradeCPUVirtualMachine($vmware_id = '', $cpu = 0): array|string
{
$Check = $this->checkApiExits();
if ($Check) {
$CPUConfig = [
"spec" => array(
"cores_per_socket" => 1,
"count" => $cpu
)
];
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/cpu", $CPUConfig);
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
}
return [
'error' => true,
'data' => $response['value']
];
}
return "Login_Again";
}
public function UpgradeDiskVirtualMachine($product, $disk = 0): array|string
{
$this->username = $product->IpAddress->Host->Datacenter->vCenter->user;
$this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password);
$vmware_id = $product->vmware_id;
$check = $this->checkApiExits();
if ($check) {
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Upgrade Disk with VirtualMachine ID # - $vmware_id");
Log::channel('Custom')->info("\r\n");
$Command = "\$vm = 'VirtualMachine-$vmware_id'; \$disksize = $disk; cd /; C:\CLISave\diskresize.ps1;";
Log::channel('Custom')->info("$Command");
$this->Command($Command);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
return [
'error' => false
];
} catch (\Exception $error) {
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Error Log: ");
Log::channel('Custom')->info($error->getMessage());
Log::channel('Custom')->info("===================================================================");
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
return "Login_Again";
}
public function FindVirtualMachine($name = ''): string|bool
{
$check = $this->checkApiExits();
if ($check) {
try {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get(
"https://$this->apiUri/rest/vcenter/vm?filter.names.1=$name"
)->json();
} catch (ConnectionException $error) {
return false;
}
if ($response['value']) {
return $response['value'][0]['vm'];
}
return false;
}
return "Login_Again";
}
protected function SetConfig($data = []): static
{
$this->username = $data['user'];
$this->password = SignRSA::DecryptFunc($data['password']);
return $this;
}
// New Version Update 07.02.2025
protected function loginVPS(array|null $data = []): ?SSH2
{
try {
// Lấy thông tin từ $data - Không có thì lấy giá trị mặc định
$user = $data['user'] ?? 'root';
$ip = $data['ip'] ?? '103.42.58.37';
$password = $data['password'] ?? 'TGS@1232';
// Kiểm tra hệ điều hành
if (str_contains($this->os, "Windows")) {
$user = $data['user'] ?? 'administartor';
$this->process = new WinRMClient($ip, $user, $password);
;
return $this->process;
}
// Tạo kết nối SSH
$this->process = new SSH2($ip, 22, 10);
// Kiểm tra đăng nhập
if (!$this->process->login($user, $password)) {
throw new Exception("Cannot login to VPS with IP: $ip, User: $user");
}
Log::info("Logged into VPS successfully: IP = $ip, User = $user");
return $this->process;
} catch (Exception $exception) {
// Ghi log lỗi chi tiết
Log::error('Login VPS failed: ' . $exception->getMessage());
$this->process = null;
return null;
}
}
protected function config_hosname_linux(string $hostname = '')
{
try {
// Xử lý hostname
$hostname = preg_replace('/[^a-zA-Z0-9-]/', '', $hostname);
$hostname = trim($hostname, '-');
$hostname = substr($hostname, 0, 63);
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Set Host name
$this->process->exec("hostnamectl set-hostname " . escapeshellarg($hostname));
Log::info("Hostname changed successfully to: $hostname");
return true;
} catch (Exception $exception) {
Log::error('Failed to change hostname: ' . $exception->getMessage());
return false;
}
}
protected function set_ip_linux(string $ipaddress = '', string $gateway = ''): bool
{
try {
// Kiểm tra IP address
if (!filter_var($ipaddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new Exception("Invalid IP address: $ipaddress");
}
// Kiểm tra Gateway
if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new Exception("Invalid Gateway address: $gateway");
}
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Thực hiện thay đổi IP và Gateway
$output1 = $this->process->exec("nmcli con mod ens192 ipv4.addresses $ipaddress/24");
$output2 = $this->process->exec("nmcli con mod ens192 ipv4.gateway $gateway");
$output3 = $this->process->exec("nmcli con up ens192");
if (!$output1 || !$output2 || !$output3) {
throw new Exception("Failed to execute one or more network commands.");
}
Log::info("IP Address changed successfully to: $ipaddress with Gateway: $gateway");
return true;
} catch (Exception $exception) {
Log::error('Failed to configure IP address: ' . $exception->getMessage());
return false;
}
}
/*
* ip = 103.42.58.37
* pwd = TGS@123
*/
public function change_password_linux(array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => '']): bool
{
try {
// Kiểm tra mật khẩu mới
if (empty($data['new_password'])) {
throw new Exception("New password cannot be empty.");
}
// Đăng nhập VPS
$this->loginVPS($data);
if (!$this->process) {
throw new Exception("Cannot login to VPS.");
}
// Lấy thông tin từ $data
$username = $data['user'] ?? 'root';
$newPassword = $data['new_password'];
// Đảm bảo username không rỗng
if (empty($username)) {
throw new Exception("Username is invalid.");
}
// Đổi mật khẩu
$this->process->exec("echo " . escapeshellarg("$username:$newPassword") . " | chpasswd");
// Log trạng thái
Log::info("Password for user $username changed successfully.");
return true;
} catch (Exception $exception) {
// Ghi log lỗi
Log::error('Failed to change password: ' . $exception->getMessage());
return false;
}
}
public function extend_disk_centos_7(): bool
{
try {
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Kiểm tra xem thiết bị `/dev/sda2` có tồn tại không
$checkDisk = $this->process->exec("lsblk | grep -w 'sda2'");
if (empty(trim($checkDisk))) {
throw new Exception("Disk /dev/sda2 not found");
}
// Thực hiện các lệnh để mở rộng đĩa
$this->process->exec("sudo tee /sys/class/block/sda/device/rescan");
$this->process->exec("sudo pvcreate /dev/sda2");
$this->process->exec("sudo vgextend /dev/centos /dev/sda2");
$this->process->exec("sudo lvextend /dev/centos/root /dev/sda2");
$this->process->exec("sudo xfs_growfs /dev/centos/root");
Log::info("Disk extended successfully");
return true;
} catch (Exception $exception) {
// Ghi log lỗi
Log::error('Failed to extend disk: ' . $exception->getMessage());
return false;
}
}
public function extend_disk_centos_8(): bool
{
try {
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Kiểm tra xem thiết bị `/dev/sda2` có tồn tại không
$checkDisk = $this->process->exec("lsblk | grep -w 'sda3'");
if (empty(trim($checkDisk))) {
throw new Exception("Disk /dev/sda3 not found");
}
// Thực hiện các lệnh để mở rộng đĩa
$this->process->exec("sudo pvcreate /dev/sda3");
$this->process->exec("sudo vgextend /dev/cl /dev/sda3");
$this->process->exec("sudo lvextend /dev/cl/root /dev/sd3");
$this->process->exec("sudo xfs_growfs /dev/cl/root");
Log::info("Disk extended successfully");
return true;
} catch (Exception $exception) {
// Ghi log lỗi
Log::error('Failed to extend disk: ' . $exception->getMessage());
return false;
}
}
public function extend_disk_ubuntu(): bool
{
try {
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Kiểm tra xem thiết bị `/dev/sda3` có tồn tại không
$checkDisk = $this->process->exec("lsblk | grep -w 'sda3'");
if (empty(trim($checkDisk))) {
throw new Exception("Disk /dev/sda3 not found");
}
// Thực hiện các lệnh để mở rộng đĩa
$this->process->exec("sudo tee /sys/class/block/sda/device/rescan");
$this->process->exec("sudo growpart /dev/sda 3");
$this->process->exec("sudo pvresize /dev/sda3");
$this->process->exec("sudo lvresize -l+100%FREE --resizefs /dev/mapper/ubuntu--vg-ubuntu--lv");
Log::info("Disk extended successfully");
return true;
} catch (Exception $exception) {
// Ghi log lỗi
Log::error('Failed to extend disk: ' . $exception->getMessage());
return false;
}
}
public function set_ip_window(string $ipaddress = '', string $gateway = '')
{
try {
// Kiểm tra IP address
if (!filter_var($ipaddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new Exception("Invalid IP address: $ipaddress");
}
// Kiểm tra Gateway
if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new Exception("Invalid Gateway address: $gateway");
}
// Đăng nhập VPS
$this->loginVPS();
if (!$this->process) {
throw new Exception("Cannot login to VPS");
}
// Thực hiện thay đổi IP và Gateway
$output1 = $this->process->execute_command("netsh int ip set address Ethernet0 static $ipaddress 255.255.255.0 $gateway");
if (!$output1) {
throw new Exception("Failed to execute one or more network commands.");
}
Log::info("IP Address changed successfully to: $ipaddress with Gateway: $gateway");
return true;
} catch (Exception $exception) {
Log::error('Failed to configure IP address: ' . $exception->getMessage());
return false;
}
}
public function change_password_windows(array $data = ['ip' => '103.42.58.37', 'user' => 'administrator', 'password' => 'TGS@123', 'new_password' => '']): bool
{
try {
// Kiểm tra mật khẩu mới
if (empty($data['new_password'])) {
throw new Exception("New password cannot be empty.");
}
// Đăng nhập VPS
$this->loginVPS($data);
if (!$this->process) {
throw new Exception("Cannot login to VPS.");
}
// Lấy thông tin từ $data
$username = $data['user'] ?? 'administrator';
$newPassword = $data['new_password'];
// Đảm bảo username không rỗng
if (empty($username)) {
throw new Exception("Username is invalid.");
}
// Đổi mật khẩu
$response = $this->process->execute_command("net user \"$username\" \"$newPassword\"");
if (stripos($response, 'successfully') === false) {
throw new Exception("Failed to change password. Response: $response");
}
Log::info("Password changed successfully for user: $username");
return true;
} catch (Exception $exception) {
// Ghi log lỗi
Log::error('Failed to change password: ' . $exception->getMessage());
return false;
}
}
/**
* Test method để debug addon updates
*/
public function testAddonUpdate($vmId, $addon, $base_cpu = 1, $base_ram = 1): array
{
try {
Log::info("🧪 TEST: Bắt đầu test addon update", [
'vm_id' => $vmId,
'addon' => $addon,
'base_cpu' => $base_cpu,
'base_ram' => $base_ram
]);
$this->updateVMHardware($vmId, $addon, $base_cpu, $base_ram);
return [
'error' => false,
'message' => 'Test addon update completed'
];
} catch (\Exception $e) {
Log::error("🧪 TEST: Lỗi trong test addon update: " . $e->getMessage());
return [
'error' => true,
'message' => $e->getMessage()
];
}
}
/**
* Test method để debug getSnapShot function
*/
public function testGetSnapShot($product): array
{
try {
Log::info("🧪 TEST: Bắt đầu test getSnapShot", [
'product_id' => $product->id ?? 'unknown',
'product_name' => $product->name ?? 'unknown',
'vmware_id' => $product->vmware_id ?? 'unknown'
]);
$result = $this->getSnapShot($product);
Log::info("🧪 TEST: Kết quả getSnapShot", [
'result' => $result,
'product_id' => $product->id ?? 'unknown'
]);
return [
'error' => false,
'message' => 'Test getSnapShot completed',
'result' => $result
];
} catch (\Exception $e) {
Log::error("🧪 TEST: Lỗi trong test getSnapShot: " . $e->getMessage());
return [
'error' => true,
'message' => $e->getMessage()
];
}
}
/**
* Assign IP to Windows VM using PowerShell script
*/
public function assignIPWindows($product, $vmware_id = ''): array|string
{
try {
// Support both old Product structure and new ModuleVmware structure
$ipAddress = null;
$gateway = null;
$netmask = '255.255.255.0';
$dns = '8.8.8.8';
$vmware_name = '';
$vCenter = null;
// Check if it's ModuleVmware (new structure)
if ($product && isset($product->ipAddress) && $product->ipAddress) {
Log::info('✅ Using ModuleVmware structure for assignIPWindows', [
'module_vmware_id' => $product->id,
'vmware_id' => $vmware_id
]);
// Extract from ModuleVmware->ipAddress relationship
$ipAddress = $product->ipAddress->ip ?? null;
$gateway = $product->ipAddress->gateway ?? null;
$netmask = $product->ipAddress->netmask ?? '255.255.255.0';
$dns = $product->ipAddress->dns ?? '8.8.8.8';
$vmware_name = $product->name ?? $vmware_id;
$vCenter = $product->ipAddress->host->datacenter->vCenter ?? null;
} elseif ($product && isset($product->IpAddress) && $product->IpAddress) {
Log::info('✅ Using old Product structure for assignIPWindows', [
'product_id' => $product->id,
'vmware_id' => $vmware_id
]);
// Extract from old Product->IpAddress relationship
$ipAddress = $product->IpAddress->ip ?? null;
$gateway = $product->IpAddress->gateway ?? null;
$netmask = $product->IpAddress->netmask ?? '255.255.255.0';
$dns = $product->IpAddress->dns ?? '8.8.8.8';
$vmware_name = $product->name ?? $vmware_id;
$vCenter = $product->IpAddress->Host->Datacenter->vCenter ?? null;
}
// Validate required data
if (!$ipAddress || !$gateway || !$vCenter) {
Log::error('❌ assignIPWindows: Missing required data', [
'vmware_id' => $vmware_id,
'ip_address' => $ipAddress,
'gateway' => $gateway,
'has_vcenter' => $vCenter ? 'yes' : 'no',
'product_class' => get_class($product)
]);
return [
'error' => true,
'data' => 'Missing required data for IP assignment (IP, Gateway, or vCenter)'
];
}
$this->SetConfig($vCenter);
// Convert netmask to prefix length (e.g., 255.255.255.0 = 24)
$maskBits = $this->convertNetmaskToPrefixLength($netmask);
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Assign IP to Windows VM - $vmware_name");
Log::channel('Custom')->info("IP: $ipAddress, Gateway: $gateway, DNS: $dns, MaskBits: $maskBits");
Log::channel('Custom')->info("\r\n");
// Create PowerShell command with dynamic values
$Command = "\$vmName = '$vmware_name'; \$IP = '$ipAddress'; \$MaskBits = $maskBits; \$Gateway = '$gateway'; \$DNS = '$dns'; cd /; C:\CLISave\AssignIP\assignIPWindows.ps1;";
Log::channel('Custom')->info("Executing Command: $Command");
$output = $this->Command($Command);
Log::channel('Custom')->info("Script output: " . $output);
Log::channel('Custom')->info("===================================================================");
// Check if execution was successful
if (strpos($output, '🎉') !== false || strpos($output, 'successfully') !== false) {
return [
'error' => false,
'data' => 'IP assigned successfully',
'output' => $output
];
} else {
return [
'error' => true,
'data' => 'Failed to assign IP',
'output' => $output
];
}
} catch (\Exception $e) {
Log::channel('Custom')->error("Exception in assignIPWindows: " . $e->getMessage());
return [
'error' => true,
'data' => $e->getMessage()
];
}
}
/**
* Convert netmask to prefix length (e.g., 255.255.255.0 = 24)
*/
private function convertNetmaskToPrefixLength($netmask): int
{
$netmaskMap = [
'255.255.255.255' => 32,
'255.255.255.254' => 31,
'255.255.255.252' => 30,
'255.255.255.248' => 29,
'255.255.255.240' => 28,
'255.255.255.224' => 27,
'255.255.255.192' => 26,
'255.255.255.128' => 25,
'255.255.255.0' => 24,
'255.255.254.0' => 23,
'255.255.252.0' => 22,
'255.255.248.0' => 21,
'255.255.240.0' => 20,
'255.255.224.0' => 19,
'255.255.192.0' => 18,
'255.255.128.0' => 17,
'255.255.0.0' => 16,
'255.254.0.0' => 15,
'255.252.0.0' => 14,
'255.248.0.0' => 13,
'255.240.0.0' => 12,
'255.224.0.0' => 11,
'255.192.0.0' => 10,
'255.128.0.0' => 9,
'255.0.0.0' => 8
];
return $netmaskMap[$netmask] ?? 24; // Default to /24 if not found
}
/**
* Parse snapshot output from PowerShell text format
*/
private function parseSnapshotTextOutput($output): array
{
$snapshots = [];
// Split the output into individual snapshot blocks
$blocks = preg_split('/\n\s*\n/', $output);
foreach ($blocks as $block) {
$block = trim($block);
if (empty($block))
continue;
// Skip header lines and warnings
if (
strpos($block, 'Name Port User') !== false ||
strpos($block, '---- ---- ----') !== false ||
strpos($block, 'WARNING:') !== false
) {
continue;
}
$snapshot = [];
// Parse each line of the snapshot block
$lines = explode("\n", $block);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line))
continue;
// Parse key-value pairs
if (strpos($line, ' : ') !== false) {
list($key, $value) = explode(' : ', $line, 2);
$key = trim($key);
$value = trim($value);
switch ($key) {
case 'Name':
$snapshot['name'] = $value;
break;
case 'Description':
$snapshot['description'] = $value;
break;
case 'Created':
$snapshot['created'] = $value;
break;
case 'PowerState':
$snapshot['power_state'] = $value;
break;
case 'VM':
$snapshot['vm_name'] = $value;
break;
case 'VMId':
$snapshot['vm_id'] = $value;
break;
case 'Parent':
$snapshot['parent'] = $value;
break;
case 'ParentSnapshotId':
$snapshot['parent_snapshot_id'] = $value;
break;
case 'ParentSnapshot':
$snapshot['parent_snapshot'] = $value;
break;
case 'Children':
$snapshot['children'] = $value;
break;
case 'SizeMB':
$snapshot['size_mb'] = (float) $value;
break;
case 'SizeGB':
$snapshot['size_gb'] = (float) $value;
break;
case 'IsCurrent':
$snapshot['is_current'] = strtolower($value) === 'true';
break;
case 'IsReplaySupported':
$snapshot['is_replay_supported'] = strtolower($value) === 'true';
break;
case 'Id':
$snapshot['id'] = $value;
break;
case 'Uid':
$snapshot['uid'] = $value;
break;
default:
// Store any other properties
$snapshot[strtolower(str_replace(' ', '_', $key))] = $value;
break;
}
}
}
// Only add if we have at least a name
if (!empty($snapshot['name'])) {
$snapshots[] = $snapshot;
}
}
return $snapshots;
}
/**
* Parse snapshot output when JSON parsing fails (legacy method)
*/
private function parseSnapshotOutput($output): array
{
$snapshots = [];
// Split by lines and look for snapshot information
$lines = explode("\n", $output);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line))
continue;
// Look for snapshot name patterns
if (preg_match('/(\w+_\d{2}_\d{2}_\d{4}_\d{2}_\d{2}_\d{2})/', $line, $matches)) {
$snapshots[] = [
'name' => $matches[1],
'raw_line' => $line
];
}
}
return $snapshots;
}
/*
* Get Window Remote Port
*/
public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int
{
$ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber";
$cmd = "Invoke-VMScript -VM '$vmwareId' "
. "-ScriptText \"$ps\" "
. "-ScriptType Powershell "
. "-GuestUser '$guestUser' "
. "-GuestPassword '$guestPassword'";
$output = $this->Command($cmd);
if (!$output || str_contains($output, 'Login_Fail')) {
return null;
}
if (preg_match('/\b(\d{2,5})\b/', $output, $m)) {
return (int) $m[1];
}
return null;
}
public function reBuildVPS(string $vmName): array|string
{
if (!$this->checkApiExits()) {
return "Login_Again";
}
try {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://{$this->apiUri}/rest/vcenter/vm", [
'filter.names' => $vmName
]);
if (!$response->successful()) {
return [
'error' => true,
'message' => 'Failed to search for VM',
'details' => $response->json()
];
}
$vmList = $response->json()['value'] ?? [];
if (empty($vmList)) {
return [
'error' => true,
'message' => "VM not found with name: {$vmName}"
];
}
$vmId = $vmList[0]['vm'];
$detailResponse = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}");
if (!$detailResponse->successful()) {
return [
'error' => true,
'message' => 'Failed to fetch VM details',
'details' => $detailResponse->json()
];
}
$vmData = $detailResponse->json();
$guestOS = $vmData['value']['guest_OS'] ?? 'UNKNOWN';
$scriptPath = '';
$osUpper = strtoupper($guestOS);
if (Str::contains($osUpper, 'WINDOWS')) {
$scriptPath = 'C:\\CLISave\\Windows\\rebuild.ps1';
} elseif (Str::contains($osUpper, ['UBUNTU', 'LINUX', 'CENTOS', 'DEBIAN'])) {
$scriptPath = 'C:\\CLISave\\Ubuntu\\rebuild.ps1';
} else {
return [
'error' => true,
'message' => "Unsupported OS for rebuild: {$guestOS}"
];
}
$scriptDir = dirname($scriptPath);
$scriptFile = basename($scriptPath);
$cmd = "
Write-Output 'STARTING_REBUILD';
\$vm = Get-VM -Name '$vmName';
cd '$scriptDir';
if (Test-Path '.\\$scriptFile') {
Write-Output 'File found, executing...';
& '.\\$scriptFile' | Out-String
} else {
Write-Error 'File NOT FOUND: $scriptPath';
}
Write-Output 'ENDING_REBUILD';
";
$result = $this->Command($cmd);
return [
'error' => false,
'vm_name' => $vmName,
'vm_id' => $vmId,
'os' => $guestOS,
'script' => $scriptPath,
'result' => $result
];
} catch (\Exception $e) {
Log::error("reBuildVPS Exception: " . $e->getMessage());
return [
'error' => true,
'message' => $e->getMessage()
];
}
}
}