feat: Implement comprehensive vCenter integration with new services for VM configuration, power management, templates, snapshots, and resource management, alongside infrastructure management and synchronization job updates.

This commit is contained in:
pbao9
2026-02-12 17:23:01 +07:00
parent f3a679d7f3
commit 64c3182b00
16 changed files with 1979 additions and 223 deletions
+83 -113
View File
@@ -16,9 +16,9 @@ use phpseclib3\Net\SSH2;
class vCenter
{
public ?string $token = null;
public string $apiUri = '10.10.10.6';
private string $username = 'administrator@vsphere.local';
private string $password = 'Tgs@1238989$%^';
public string $apiUri;
private string $username;
private string $password;
private int $diskCapacity = 1073741824;
private int $memory = 1024;
@@ -35,7 +35,7 @@ class vCenter
$response = Http::withoutVerifying()
->withBasicAuth($this->username, $this->password)
->post("https://{$this->apiUri}/rest/com/vmware/cis/session");
->post("https://{$this->apiUri}/rest/com/vmware/cis/session", []);
// Log::info('Response login: ' . $response->body());
@@ -246,69 +246,38 @@ class vCenter
public function loadNetWork($data = [], $host_id = '')
{
$this->username = $data->user;
$this->password = SignRSA::DecryptFunc($data->password);
$this->apiUri = $data->host;
$information = $this->loadInformation($host_id);
// Pass the host_id directly to the API call
$allNetwork = $this->getAllNetwork($host_id);
if ($information) {
if (is_array($information->Network)) {
$information = $information->Network;
} else {
$information = array($information->Network);
}
$netWork = [];
$allNetwork = $this->getAllNetwork();
if ($allNetwork && $allNetwork !== "Login_Again") {
foreach ($allNetwork as $_subItem) {
if (in_array($_subItem['network'], $information)) {
$netWork[] = $_subItem;
}
}
return $netWork;
}
return $allNetwork;
}
return $information;
return $allNetwork;
}
protected function loadInformation($host_id = '')
private function getAllNetwork($host = '')
{
try {
Log::info('host_id', ['data' => $host_id]);
$cmd = "\$vmhost='$host_id'; cd /; C:\CLISave\getvmhostinfo.ps1;";
$results = $this->Command($cmd);
$check = $this->checkApiExits();
if ($check) {
if (!$results) {
return null;
$params = [];
if (!empty($host)) {
$params['filter.hosts'] = $host;
}
$startPos = strpos($results, '{');
$endPos = strrpos($results, '}');
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/network", $params)->json();
if ($startPos === false || $endPos === false) {
return null;
if (!isset($response['type'])) {
return $response['value'];
}
$jsonData = substr($results, $startPos, $endPos - $startPos + 1);
$jsonData = trim($jsonData);
try {
$decoded = json_decode($jsonData, false, 512, JSON_THROW_ON_ERROR);
return $decoded;
} catch (\JsonException $e) {
return null;
}
} catch (\Throwable $e) {
Log::error('Error in loadInformation: ' . $e->getMessage());
return null;
return false;
}
return "Login_Again";
}
protected function Command($cmd)
@@ -597,76 +566,77 @@ PS;
return $data;
}
private function getAllNetwork()
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/network")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function loadDatastore($data = [], $host_id = '')
{
Log::info('[vCenter] loadDatastore()', [
'host' => $data->host,
'esxi_host' => $host_id
]);
$this->username = $data->user;
$allDatastore = $this->getAllDataStore($host_id);
$this->password = SignRSA::DecryptFunc($data->password);
$this->apiUri = $data->host;
$information = $this->loadInformation($host_id);
if ($information) {
if (is_array($information->Datastore)) {
$information = $information->Datastore;
} else {
$information = array($information->Datastore);
}
$dataStore = [];
$allDatastore = $this->getAllDataStore();
if ($allDatastore && $allDatastore !== "Login_Again") {
foreach ($allDatastore as $_item) {
if (in_array($_item['datastore'], $information)) {
$dataStore[] = $_item;
}
}
return $dataStore;
}
return $allDatastore;
if ($allDatastore === "Login_Again") {
Log::warning('[vCenter] Token expired when getAllDataStore');
return "Login_Again";
}
return $information;
}
public function getAllDataStore()
{
$check = $this->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://$this->apiUri/rest/vcenter/datastore")->json();
if (!isset($response['type'])) {
return $response['value'];
}
if (!$allDatastore) {
Log::warning('[vCenter] getAllDataStore return false');
return false;
}
return "Login_Again";
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'];
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Http;
use App\Http\Facade\SignRSA;
use App\Models\Vcenter;
use Exception;
class Client
{
public ?string $token = null;
public string $apiUri;
private string $username;
private string $password;
public function loginServer(Vcenter $vcenterModel): bool|string
{
$this->apiUri = $vcenterModel->host;
$this->username = $vcenterModel->user;
$this->password = SignRSA::DecryptFunc($vcenterModel->password);
$response = Http::withoutVerifying()
->withBasicAuth($this->username, $this->password)
->post("https://{$this->apiUri}/rest/com/vmware/cis/session", []);
if ($response->successful()) {
$json = $response->json();
if (isset($json['value'])) {
$this->token = $json['value'];
$vcenterModel->update(['token' => $this->token]);
return $this->token;
}
$body = $response->body();
if (!empty($body)) {
$this->token = $body;
$vcenterModel->update(['token' => $this->token]);
return $this->token;
}
}
return false;
}
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;
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{
return $this->password;
}
}
@@ -0,0 +1,175 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class PowerManagementService
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function SuspendVirtualMachine($vmware_id = ''): array|string
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->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
{
if ($this->client->loginServer($product->ipaddress->host->datacenter->vCenter)) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->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 StartVirtualMachine($product, $vmware_id = ''): array|string
{
if ($this->client->loginServer($product->ipaddress->host->datacenter->vCenter)) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->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
{
if ($this->client->loginServer($product->ipaddress->host->datacenter->vCenter)) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->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
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->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
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->delete("https://{$this->client->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 GetConsoleTickets($vmware_id = ''): array|string
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->post("https://{$this->client->apiUri}/api/vcenter/vm/$vmware_id/console/tickets", [
"type" => "WEBMKS"
])->json();
if (!isset($response['error_type'])) {
$start = strpos($response['ticket'], '/ticket/');
$ticket = str_replace('/ticket/', '', substr($response['ticket'], $start));
return [
'error' => false,
'data' => $ticket
];
}
return [
'error' => true,
'data' => $response['messages']
];
}
return "Login_Again";
}
}
@@ -0,0 +1,270 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
use Exception;
use App\Models\InfraManagement;
use App\Http\Facade\SignRSA;
class PowerShellService
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
private function LoginAuth()
{
try {
// Retrieve SSH/Jumphost credentials from database
$infra = InfraManagement::where('type', 'jumphost')
->where('status', true)
->first();
if (!$infra) {
Log::error('SSH Login Failed: No active jumphost configuration found in InfraManagement.');
return false;
}
$sshHost = $infra->host;
$sshPort = 22;
if (isset($infra->meta_data['port'])) {
$sshPort = $infra->meta_data['port'];
}
$sshUser = $infra->username;
$sshPass = SignRSA::DecryptFunc($infra->password);
$connection = new SSH2($sshHost, $sshPort);
if ($connection->login($sshUser, $sshPass)) {
$connection->setTimeout(0); // No timeout
return $connection;
}
return false;
} catch (Exception $error) {
Log::error('SSH Login Failed: ' . $error->getMessage());
return false;
}
}
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";
}
// Action wrapper
$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();
}
}
public function executePowerCLI(string $cmd)
{
return $this->CommandRSP($cmd);
}
protected function CommandRSP(string $cmd)
{
$auth = $this->LoginAuth();
if (!$auth) {
return "Login_Fail";
}
$esc = fn(string $v) => str_replace("'", "''", $v);
// Get credentials from Client
$server = $esc($this->client->apiUri ?? '');
$user = $esc($this->client->getUsername() ?? '');
$pass = $esc($this->client->getPassword() ?? '');
$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
& {
<CMD>
} | Out-String
PS;
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd,
]);
$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();
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->client->apiUri ?? '');
$user = $esc($this->client->getUsername() ?? '');
$pass = $esc($this->client->getPassword() ?? '');
$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,
]);
$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) {
$trimmed = trim($out);
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) {
return $m[1];
}
return $trimmed;
}
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
{
$psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1";
$q = fn(string $v) => str_replace("'", "''", $v);
$args = " -VMName '" . $q($vmName) . "'";
$args .= " -GuestUser '" . $q($guestUser) . "'";
$args .= " -GuestPassword '" . $q($guestPassword) . "'";
$args .= " -TargetUser '" . $q($targetUser) . "'";
$args .= " -NewPassword '" . $q($newPassword) . "'";
$cmd = "& '$psFile'$args | Out-String";
$raw = $this->CommandRSP($cmd);
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
return "Login_Fail";
}
$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;
}
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);
$args = " -VMName '" . $q($vmName) . "'";
$args .= " -GuestUser '" . $q($guestUser) . "'";
$args .= " -GuestPassword '" . $q($guestPassword) . "'";
$args .= " -NewPort " . (int) $newPort;
$cmd = "& '$psFile'$args";
$raw = $this->CommandChangePort($cmd, true);
if ($raw === 'Login_Fail' || $raw === false || $raw === null) {
return "Login_Fail";
}
$raw = trim($raw);
if (!preg_match('~(\{(?:[^{}]|(?1))*\}|\[(?:[^\[\]]|(?0))*\])\s*$~s', $raw, $m)) {
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()
];
}
if (isset($data[0])) {
$data = $data[0];
}
return $data;
}
}
@@ -0,0 +1,165 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ResourceService
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getAllDatacenter()
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/datacenter")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getAllHost($datacenter = '')
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->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 = '')
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/folder", [
'filter.datacenters.1' => $datacenter,
'filter.type' => "VIRTUAL_MACHINE",
'filter.names' => "vm"
])->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getAllDataStore($host = '')
{
Log::info('[vCenter] getAllDataStore call', [
'api' => $this->client->apiUri,
'host_filter' => $host
]);
if (!$this->client->checkApiExits()) {
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->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/datastore", $params);
$json = $response->json();
if (!isset($json['type'])) {
return $json['value'];
}
Log::warning('[vCenter] API returned error', [
'response' => $json
]);
return false;
}
public function loadDatastore($host_id = '')
{
Log::info('[vCenter] loadDatastore()', [
'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 getAllNetwork($host = '')
{
if ($this->client->checkApiExits()) {
$params = [];
if (!empty($host)) {
$params['filter.hosts'] = $host;
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/network", $params)->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function loadNetWork($host_id = '')
{
return $this->getAllNetwork($host_id);
}
public function GetFolderByHost($datacenter = '')
{
return \App\Models\ModuleFolder::where('module_datacenter_id', $datacenter)->first();
}
}
@@ -0,0 +1,310 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use App\Http\Facade\SignRSA;
class SnapshotService
{
protected Client $client;
protected PowerShellService $powerShellService;
public function __construct(Client $client, PowerShellService $powerShellService)
{
$this->client = $client;
$this->powerShellService = $powerShellService;
}
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.
// 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;
$this->client->loginServer($vcenterModel);
} catch (\Exception $e) {
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
// In original code, it checks checkApiExits(), if true proceed.
if ($this->client->checkApiExits()) {
try {
$Snapshot = $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");
$Command = "\$vm_name = '$vmware_name';\$name = '$Snapshot'; cd /; C:\CLISave\createsnapshot.ps1;";
Log::channel('Custom')->info("$Command");
$this->powerShellService->executePowerCLI($Command);
$this->clearSnapshotCache($product);
return [
'error' => false,
'data' => $Snapshot
];
} catch (\Exception $error) {
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
{
// ... (Similar logic, using SOAP)
// Since SOAP logic is quite complex and self-contained in vCenter.php,
// I'll copy it here but need to adapt $this->callSoap etc.
// Actually, callSoap uses curl with manual implementation.
// I should probably simplify or copy that helper too.
// This method heavily relies on internal SOAP helpers.
// For simplicity in this 'splitting' task, I will implement the public method
// and add the private helpers it depends on.
try {
$vcenterModel = $product->ipaddress->host->datacenter->vCenter;
$this->client->loginServer($vcenterModel);
} catch (\Exception $e) {
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
$vmId = $product->vmware_id;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
return $cachedResult;
}
try {
$sessionToken = $this->soapLogin();
if (!$sessionToken) {
return "Login_Again";
}
$snapshotInfo = $this->getVMSnapshotInfoSoap($sessionToken, $vmId);
$snapshots = [];
if (isset($snapshotInfo['rootSnapshotList'])) {
$roots = $snapshotInfo['rootSnapshotList'];
if (isset($roots['snapshot'])) {
$snapshots = $this->flattenSnapshotTree($roots);
} elseif (is_array($roots)) {
foreach ($roots as $root) {
$snapshots = array_merge($snapshots, $this->flattenSnapshotTree($root));
}
}
}
$formattedSnapshots = [];
foreach ($snapshots as $snap) {
$formattedSnapshots[] = [
'name' => $snap['name'],
'description' => $snap['description'] ?? '',
'created' => $snap['createTime'],
'id' => $snap['snapshot']['_'] ?? $snap['snapshot'],
'is_current' => false,
'parent_snapshot_id' => '',
'size_gb' => 0
];
}
$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()
{
$username = $this->client->getUsername();
$password = $this->client->getPassword();
$body = '<soapenv:Body>
<Login xmlns="urn:vim25">
<_this type="SessionManager">SessionManager</_this>
<userName>' . htmlspecialchars($username) . '</userName>
<password>' . htmlspecialchars($password) . '</password>
</Login>
</soapenv:Body>';
$result = $this->callSoap($body);
return $result['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);
if (isset($result['body'])) {
try {
$xml = simplexml_load_string($result['body']);
if ($xml === false) return [];
$xml->registerXPathNamespace('u', 'urn:vim25');
$json = json_encode($xml);
$array = json_decode($json, true);
return $this->findSnapshotPropertyInArray($array);
} catch (\Exception $e) {
return [];
}
}
return [];
}
private function findSnapshotPropertyInArray($array)
{
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 = [];
$list[] = $node;
if (isset($node['childSnapshotList'])) {
$children = $node['childSnapshotList'];
// Check if 'snapshot' key exists directly (single child wrapped)
if (isset($children['snapshot'])) {
// If it's an array of snapshots or single object?
// XML conversion: if strictly one child, might be direct. if multiple, array.
// But simplexml usually produces array if multiple same tags.
// However, structure is usually childSnapshotList -> snapshot (array or single)
$snaps = $children['snapshot'];
// If $snaps has numeric keys, it's array. Else single.
if (array_keys($snaps) !== range(0, count($snaps) - 1)) {
$snaps = [$snaps];
}
foreach ($snaps as $child) {
$list = array_merge($list, $this->flattenSnapshotTree($child));
}
}
}
return $list;
}
private function callSoap($soapBody, $cookie = null)
{
$curl = curl_init();
$host = $this->client->apiUri;
$headers = [
'Content-Type: text/xml',
'SOAPAction: "urn:vim25/2.0"'
];
if ($cookie) {
$headers[] = "Cookie: $cookie";
}
$payload = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:vim25">
' . $soapBody . '
</soapenv:Envelope>';
curl_setopt_array($curl, array(
CURLOPT_URL => "https://$host/sdk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HEADER => true // We need headers for cookie
));
$response = curl_exec($curl);
$header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$body = substr($response, $header_size);
curl_close($curl);
// Parse cookie
$cookieValue = null;
if (preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $header, $matches)) {
foreach ($matches[1] as $c) {
if (strpos($c, 'vmware_soap_session') !== false) {
$cookieValue = $c;
break;
}
}
}
return ['body' => $body, 'cookie' => $cookieValue];
}
}
@@ -0,0 +1,104 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Http;
class TemplateService
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getAllTemplate()
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/com/vmware/content/library")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function getTemplatebyID($template = '')
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->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 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 getVersionDetail($version_id = '')
{
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/com/vmware/content/library/item/id:{$version_id}")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
public function getAllVersion($library = ''): bool|array|string
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->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";
}
}
@@ -0,0 +1,195 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use Exception;
class VMConfigurationService
{
protected Client $client;
protected PowerShellService $powerShellService;
public function __construct(Client $client, PowerShellService $powerShellService)
{
$this->client = $client;
$this->powerShellService = $powerShellService;
}
private function setConfig($vcenterModel)
{
// Setup client with vCenter credentials
$this->client->loginServer($vcenterModel);
}
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);
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Configuring Ubuntu for VM #ID $VirtualMachine");
$cmd1 = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createUbuntu1line.ps1;";
Log::channel('Custom')->info($cmd1);
$this->powerShellService->executePowerCLI($cmd1);
$cmd2 = "\$vm = get-vm -Id '$VirtualMachine'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeLinux.ps1;";
Log::channel('Custom')->info($cmd2);
$this->powerShellService->executePowerCLI($cmd2);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
Log::channel('Custom')->info("===================================================================");
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring Ubuntu: " . $error->getMessage());
}
}
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);
try {
Log::channel('Custom')->info("Configuring CentOS 8 for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS81line.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring CentOS 8: " . $error->getMessage());
}
}
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);
try {
Log::channel('Custom')->info("Configuring CentOS 7 for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS71line.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring CentOS 7: " . $error->getMessage());
}
}
public function ConfigWindowsVersion10($vmware_id = '', $product = [], $password = ''): void
{
if (!$this->validateProductData($product, 'ConfigWindowsVersion10', $vmware_id)) return;
$this->setConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
try {
Log::channel('Custom')->info("Configuring Windows 10 for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows10.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring Windows 10: " . $error->getMessage());
}
}
public function ConfigWindowsServer($vmware_id = '', $product = [], $password = ''): void
{
if (!$this->validateProductData($product, 'ConfigWindowsServer', $vmware_id)) return;
$this->setConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
try {
Log::channel('Custom')->info("Configuring Windows Server for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows1line.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring Windows Server: " . $error->getMessage());
}
}
public function ConfigFixWindowsClick($vmware_id = '', $product = []): void
{
if (!$this->validateProductData($product, 'ConfigFixWindowsClick', $vmware_id)) return;
$this->setConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
try {
Log::channel('Custom')->info("Fixing Windows Click for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine'; cd /; C:\CLISave\FixTwoClickWindows.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error fixing Windows Click: " . $error->getMessage());
}
}
public function ConfigRockyLinuxVersion9($vmware_id = '', $product = [], $password = ''): void
{
if (!$this->validateProductData($product, 'ConfigRockyLinuxVersion9', $vmware_id)) return;
$this->setConfig($product->IpAddress->Host->Datacenter->vCenter);
$VirtualMachine = "VirtualMachine-$vmware_id";
$IPAddress = $product->IpAddress->ip;
$Gateway = $product->IpAddress->gateway;
$HostName = strtolower($product->hostname);
try {
Log::channel('Custom')->info("Configuring Rocky Linux 9 for VM #ID $VirtualMachine");
$cmd = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createRocky1line.ps1;";
Log::channel('Custom')->info($cmd);
$this->powerShellService->executePowerCLI($cmd);
Log::channel('Custom')->info("Configuration completed at " . Carbon::now());
} catch (Exception $error) {
Log::channel('Custom')->error("Error configuring Rocky Linux 9: " . $error->getMessage());
}
}
private function validateProductData($product, $methodName, $vmware_id): bool
{
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
Log::error("❌ $methodName: Missing required data", [
'vmware_id' => $vmware_id,
'product_id' => $product?->id ?? 'unknown',
]);
return false;
}
return true;
}
}
@@ -0,0 +1,356 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Http\Services\Discord;
use App\Models\ModuleDatastore;
use Exception;
class VirtualMachineService
{
protected Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getAllVirtualMachine($datacenter = '')
{
if ($this->client->checkApiExits()) {
$params = [];
if (!empty($datacenter)) {
$params['filter.datacenters'] = $datacenter;
}
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/vm", $params)->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function CreateVirtualMachine($product_data): array|string
{
$vcenterModel = $product_data['vcenter'];
Log::info('Vcenter Info', ['data' => $vcenterModel]);
if (!$this->client->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];
$base_cpu = $item->cpu ?? ($host->cpu ?? 1);
$base_ram = $item->ram ?? ($host->ram ?? 1);
$base_disk = $item->disk ?? ($host->disk ?? 20);
$base_cpu = max(1, (int) $base_cpu);
$base_ram = max(1, (int) $base_ram);
$base_disk = max(20, (int) $base_disk);
Log::info("Cấu hình cơ bản", [
'base_cpu' => $base_cpu,
'base_ram' => $base_ram,
'base_disk' => $base_disk,
'addon' => $addon
]);
$datastores = ModuleDatastore::where('module_host_id', $host->id)->get();
$datastoreId = $datastores->isNotEmpty()
? $datastores->first()->datastore
: 'datastore-3004';
$body = [
'spec' => [
'name' => $item->server_name ?? ($userName . '-vm-' . $item->id),
'source' => $template,
'placement' => [
'datacenter' => $datacenter->datacenter_id,
'folder' => 'group-v1008',
'host' => $host->host_id,
'resource_pool' => 'resgroup-3002',
'datastore' => $datastoreId,
],
'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)
->connectTimeout(30)
->withHeaders([
'vmware-api-session-id' => $this->client->token,
'Content-Type' => 'application/json',
])
->post("https://{$this->client->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
]);
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}");
}
$discord = app(Discord::class);
$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()];
}
}
private function updateVMHardware($vmId, $addon, $base_cpu, $base_ram): void
{
try {
Log::info("=== BẮT ĐẦU CẬP NHẬT ADDON ===");
$new_cpu = $base_cpu + $addon['cpu'];
$new_ram_mib = ($base_ram + $addon['ram']) * 1024;
if ($addon['cpu'] > 0) {
$this->updateVMCPU($vmId, $new_cpu);
}
if ($addon['ram'] > 0) {
$this->updateVMMemory($vmId, $new_ram_mib);
}
// Disk update logic was missing in the snippet but present in the original method names?
// Actually the original `updateVMHardware` in the snippet (step 134) calls `updateVMCPU` and `updateVMMemory`.
// It gets `base_disk` but doesn't seem to call `updateVMDisk` inside `updateVMHardware`?
// Let me re-read step 134.
// ... `updateVMHardware` implementation ...
// It calls `updateVMCPU` and `updateVMMemory`.
// It calculates `$addon['disk']` in log, but doesn't call `updateVMDisk`.
// Ah, line 943 defines `updateVMDisk`.
// But `updateVMHardware` (lines 772-832) only calls `updateVMCPU` and `updateVMMemory`.
// I will implement it exactly as the original.
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());
}
}
private function updateVMCPU($vmId, $cpuCount): bool
{
try {
$coresPerSocket = 1;
if ($cpuCount > 1) {
$maxCoresPerSocket = floor($cpuCount / 2);
for ($i = $maxCoresPerSocket; $i >= 1; $i--) {
if ($cpuCount % $i === 0) {
$coresPerSocket = $i;
break;
}
}
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->client->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->client->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()
]);
return false;
}
} catch (Exception $e) {
Log::error("❌ Exception khi cập nhật CPU cho VM {$vmId}: " . $e->getMessage());
return false;
}
}
private function updateVMMemory($vmId, $ramSizeMB): bool
{
try {
$payload = [
'spec' => [
'size_MiB' => $ramSizeMB
]
];
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->client->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->client->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()
]);
return false;
}
} catch (Exception $e) {
Log::error("❌ Exception khi cập nhật RAM cho VM {$vmId}: " . $e->getMessage());
return false;
}
}
private function updateVMDisk($vmId, $diskSizeGB): bool
{
try {
$diskSizeBytes = $diskSizeGB * 1024 * 1024 * 1024;
$payload = [
'spec' => [
'capacity' => $diskSizeBytes
]
];
$diskResponse = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->client->token,
])
->get("https://{$this->client->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk");
if (!$diskResponse->successful()) {
return false;
}
$diskData = $diskResponse->json();
if (!isset($diskData['value']) || empty($diskData['value'])) {
return false;
}
$firstDisk = $diskData['value'][0];
$diskKey = $firstDisk['key'];
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
'vmware-api-session-id' => $this->client->token,
'Content-Type' => 'application/json',
])
->patch("https://{$this->client->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk/{$diskKey}", $payload);
if ($response->successful()) {
return true;
} else {
return false;
}
} catch (Exception $e) {
Log::error("Exception khi cập nhật Disk cho VM {$vmId}: " . $e->getMessage());
return false;
}
}
public function CheckVirtualMachine($vmware_id = '')
{
if ($this->client->checkApiExits()) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
public function GetNetworkVirtualMachine($vmware_id = '')
{
$check = $this->client->checkApiExits();
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->client->token
])
->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/guest/networking")->json();
if (!isset($response['type'])) {
return $response['value'];
}
return false;
}
return "Login_Again";
}
}
+88 -68
View File
@@ -35,6 +35,11 @@ class SyncDatastore implements ShouldQueue
$this->telegram = new Telegram();
}
private function jobLog($message, $context = [])
{
Log::info('[SyncDatastore] ' . $message, $context);
}
/**
* Execute the job.
*
@@ -43,97 +48,112 @@ class SyncDatastore implements ShouldQueue
public function handle()
{
try {
$this->jobLog('JOB START');
// Initialize services
$client = new \App\Http\Services\vCenter\Client();
$resourceService = new \App\Http\Services\vCenter\ResourceService($client);
foreach ($this->data as $item) {
$this->vCenter->apiUri = $item->host;
$this->jobLog('Processing vCenter', [
'vcenter_id' => $item->id,
'host' => $item->host
]);
if (!$item->token) {
$token = $this->vCenter->loginServer($item);
if ($token) {
vCenter::where('id', $item->id)->update(['token' => $token]);
$this->vCenter->token = $token;
}
}
$this->vCenter->token = $item->token;
if (!$this->vCenter->token) {
// Login to vCenter
// loginServer returns token string on success, false on failure.
// It also updates the model instance passed to it.
$token = $client->loginServer($item);
if (!$token) {
$this->jobLog('Login failed');
continue;
}
$dataCenter = ModuleDatacenter::where('vcenter_id', $item->id)->select('id', 'datacenter_id')->get();
$this->jobLog('Login success');
// ================= DATACENTER =================
$dataCenter = ModuleDatacenter::where('vcenter_id', $item->id)
->select('id', 'datacenter_id')
->get();
$this->jobLog('Datacenter count', [
'count' => $dataCenter->count()
]);
foreach ($dataCenter as $_item) {
$allHost = ModuleHost::where('module_datacenter_id', $_item->id)->select('id', 'name')->get();
if ($allHost) {
foreach ($allHost as $hostItem) {
$datastore = $this->vCenter->loadDatastore($item, $hostItem->name);
if ($datastore === "Login_Again") {
$token = $this->vCenter->loginServer($item);
if ($token) {
vCenter::where('id', $item->id)->update(['token' => $token]);
$this->vCenter->token = $token;
}
$this->jobLog('Processing Datacenter', [
'module_datacenter_id' => $_item->id
]);
$allHost = ModuleHost::where('module_datacenter_id', $_item->id)
->select('id', 'name', 'host_id')
->get();
foreach ($allHost as $hostItem) {
$this->jobLog('Processing Host', [
'host_id' => $hostItem->id,
'host_name' => $hostItem->name,
'vcenter_host_id' => $hostItem->host_id
]);
// Use ResourceService to get datastores
$datastore = $resourceService->loadDatastore($hostItem->host_id);
if ($datastore === "Login_Again") {
$this->jobLog('Token expired → relogin');
$token = $client->loginServer($item);
if ($token) {
// Retry once
$datastore = $resourceService->loadDatastore($hostItem->host_id);
} else {
$this->jobLog('Relogin failed');
continue;
}
if ($datastore) {
}
foreach ($datastore as $_subItem) {
if (!$datastore || !is_array($datastore)) {
$this->jobLog('No valid datastore returned', ['response' => $datastore]);
continue;
}
if (!ModuleDatastore::where('datastore', $_subItem['datastore'])->first()) {
$this->jobLog('Datastore count from host', [
'count' => count($datastore)
]);
$template = $this->vCenter->getAllDataStore();
if ($template) {
ModuleDatastore::create([
'name' => $_subItem['name'],
'datastore' => $_subItem['datastore'],
'type' => $_subItem['type'],
'space' => floor($_subItem['free_space'] / 1073741824),
'module_host_id' => $hostItem->id
]);
ModuleHost::where('id', $hostItem->id)->update(['template' => $template]);
} else {
ModuleDatastore::create([
'name' => $_subItem['name'],
'datastore' => $_subItem['datastore'],
'type' => $_subItem['type'],
'space' => floor($_subItem['free_space'] / 1073741824),
'module_host_id' => $hostItem->id
]);
}
}
}
}
foreach ($datastore as $_subItem) {
ModuleDatastore::updateOrCreate(
[
'datastore' => $_subItem['datastore'],
'module_host_id' => $hostItem->id,
],
[
'name' => $_subItem['name'],
'type' => $_subItem['type'],
'space' => floor($_subItem['free_space'] / 1073741824),
]
);
}
}
}
}
// Notify success via Telegram
$this->telegram->SendJobRunInfo('SyncDatastore', 'Success', [
'message' => 'Sync datastore completed successfully',
'synced_vcenters' => $this->data->count(),
]);
$this->jobLog('JOB END SUCCESS');
} catch (\Throwable $exception) {
// Ghi log Laravel
Log::error('SyncDatastore Job failed', [
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// Gửi log lỗi qua Telegram
$this->telegram->SendJobRunInfo('SyncDatastore', 'Failed', [
'error' => $exception->getMessage(),
Log::error('SyncDatastore FAILED', [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'line' => $exception->getLine()
]);
// Quăng lại lỗi để Laravel xử lý retry/failure
throw $exception;
}
}
}
+18 -18
View File
@@ -42,19 +42,16 @@ class SyncHost implements ShouldQueue
public function handle()
{
try {
// Initialize new services
$client = new \App\Http\Services\vCenter\Client();
$resourceService = new \App\Http\Services\vCenter\ResourceService($client);
$templateService = new \App\Http\Services\vCenter\TemplateService($client);
foreach ($this->data as $item) {
$this->vCenter->apiUri = $item->host;
// Login via Client service
$token = $client->loginServer($item);
if (!$item->token) {
$token = $this->vCenter->loginServer($item);
if ($token) {
vCenter::where('id', $item->id)->update(['token' => $token]);
$this->vCenter->token = $token;
}
}
$this->vCenter->token = $item->token;
if (!$this->vCenter->token) {
if (!$token) {
continue;
}
@@ -63,23 +60,26 @@ class SyncHost implements ShouldQueue
->get();
foreach ($dataCenter as $_item) {
$host = $this->vCenter->getAllHost($_item['datacenter_id']);
// Use ResourceService to get hosts
$host = $resourceService->getAllHost($_item['datacenter_id']);
if ($host === "Login_Again") {
$token = $this->vCenter->loginServer($item);
$token = $client->loginServer($item);
if ($token) {
vCenter::where('id', $item->id)->update(['token' => $token]);
$this->vCenter->token = $token;
$host = $resourceService->getAllHost($_item['datacenter_id']);
}
}
$host = $this->vCenter->getAllHost($_item['datacenter_id']);
if (!is_array($host)) {
continue; // Skip if still invalid or Login_Again
}
$template = $this->vCenter->getAllTemplate();
// Use TemplateService
$template = $templateService->getAllTemplate();
if ($host) {
foreach ($host as $_subItem) {
if (!ModuleHost::where('host_id', $_subItem['host'])->first()) {
if (!ModuleHost::where('host_id', $_subItem['host'])->exists()) {
ModuleHost::create([
'name' => $_subItem['name'],
'host_id' => $_subItem['host'],
+19 -24
View File
@@ -57,39 +57,34 @@ class SyncNetwork implements ShouldQueue
$dataCenter = ModuleDatacenter::where('vcenter_id', $item->id)->select('id', 'datacenter_id')->get();
foreach ($dataCenter as $_item) {
$allHost = ModuleHost::where('module_datacenter_id',$_item->id)->select('id','name')->get();
if ($allHost)
{
foreach ($allHost as $hostItem)
{
$allHost = ModuleHost::where('module_datacenter_id', $_item->id)
->select('id', 'name', 'host_id')
->get();
if ($allHost) {
foreach ($allHost as $hostItem) {
$network = $this->vCenter->loadNetWork($item,$hostItem->name);
$network = $this->vCenter->loadNetWork($item, $hostItem->host_id);
if ($network === "Login_Again") {
$token = $this->vCenter->loginServer($item);
if ($token)
{
vCenter::where('id',$item->id)->update(['token' => $token]);
if ($token) {
vCenter::where('id', $item->id)->update(['token' => $token]);
$this->vCenter->token = $token;
$network = $this->vCenter->loadNetWork($item,$hostItem->name);
$network = $this->vCenter->loadNetWork($item, $hostItem->host_id);
}
}
if ($network)
{
foreach ($network as $_subItem)
{
if (! ModuleNetwork::where('network',$_subItem['network'])->first())
{
ModuleNetwork::create([
'name' => $_subItem['name'],
if ($network) {
foreach ($network as $_subItem) {
ModuleNetwork::updateOrCreate(
[
'network' => $_subItem['network'],
'type' => $_subItem['type'],
'module_host_id' => $hostItem->id
]);
}
],
[
'name' => $_subItem['name'],
'type' => $_subItem['type'],
]
);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InfraManagement extends Model
{
use HasFactory;
protected $table = 'infra_management';
protected $fillable = [
'name',
'host',
'username',
'password',
'token',
'type', // e.g. vcenter, esxi
'status',
'description',
'meta_data'
];
protected $casts = [
'meta_data' => 'array',
'status' => 'boolean',
];
}
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('infra_management', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('host')->unique()->comment('IP or Hostname');
$table->string('username');
$table->longText('password'); // Should be encrypted
$table->text('token')->nullable();
$table->string('type')->default('vcenter')->comment('vcenter, esxi, etc.');
$table->boolean('status')->default(true);
$table->text('description')->nullable();
$table->json('meta_data')->nullable(); // For extra config
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('infra_management');
}
};
@@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\InfraManagement;
use App\Http\Facade\SignRSA;
class InfraManagementSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
InfraManagement::updateOrCreate(
['host' => '10.10.10.173'],
[
'name' => 'Jumphost 173',
'username' => 'administrator',
'password' => SignRSA::EncryptFunc('Thegioiso@123**$$8899TgTk2'),
'type' => 'jumphost',
'status' => true,
'meta_data' => [
'port' => 22
],
'description' => 'Main Jumphost for vCenter PowerCLI operations'
]
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class ProvinceWardSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$path = database_path('province_ward_import.sql');
if (File::exists($path)) {
// Read and execute the SQL file
$sql = File::get($path);
// Execute the SQL commands
DB::unprepared($sql);
$this->command->info('Province/Ward data imported successfully from SQL file.');
} else {
$this->command->error('File database/province_ward_import.sql not found.');
}
}
}