From 64c3182b007de4a7d310a431f8655d95f526ece5 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Thu, 12 Feb 2026 17:23:01 +0700 Subject: [PATCH] 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. --- app/Http/Services/vCenter.php | 196 ++++------ app/Http/Services/vCenter/Client.php | 69 ++++ .../vCenter/PowerManagementService.php | 175 +++++++++ .../Services/vCenter/PowerShellService.php | 270 +++++++++++++ app/Http/Services/vCenter/ResourceService.php | 165 ++++++++ app/Http/Services/vCenter/SnapshotService.php | 310 +++++++++++++++ app/Http/Services/vCenter/TemplateService.php | 104 +++++ .../vCenter/VMConfigurationService.php | 195 ++++++++++ .../vCenter/VirtualMachineService.php | 356 ++++++++++++++++++ .../VirtualMachine/Sync/SyncDatastore.php | 156 ++++---- app/Jobs/VirtualMachine/Sync/SyncHost.php | 36 +- app/Jobs/VirtualMachine/Sync/SyncNetwork.php | 43 +-- app/Models/InfraManagement.php | 30 ++ ...2_164158_create_infra_management_table.php | 36 ++ database/seeders/InfraManagementSeeder.php | 31 ++ database/seeders/ProvinceWardSeeder.php | 30 ++ 16 files changed, 1979 insertions(+), 223 deletions(-) create mode 100644 app/Http/Services/vCenter/Client.php create mode 100644 app/Http/Services/vCenter/PowerManagementService.php create mode 100644 app/Http/Services/vCenter/PowerShellService.php create mode 100644 app/Http/Services/vCenter/ResourceService.php create mode 100644 app/Http/Services/vCenter/SnapshotService.php create mode 100644 app/Http/Services/vCenter/TemplateService.php create mode 100644 app/Http/Services/vCenter/VMConfigurationService.php create mode 100644 app/Http/Services/vCenter/VirtualMachineService.php create mode 100644 app/Models/InfraManagement.php create mode 100644 database/migrations/2026_02_12_164158_create_infra_management_table.php create mode 100644 database/seeders/InfraManagementSeeder.php create mode 100644 database/seeders/ProvinceWardSeeder.php diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index da6e43c..309bdb1 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -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']; diff --git a/app/Http/Services/vCenter/Client.php b/app/Http/Services/vCenter/Client.php new file mode 100644 index 0000000..ee64813 --- /dev/null +++ b/app/Http/Services/vCenter/Client.php @@ -0,0 +1,69 @@ +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; + } +} diff --git a/app/Http/Services/vCenter/PowerManagementService.php b/app/Http/Services/vCenter/PowerManagementService.php new file mode 100644 index 0000000..ba6c1f8 --- /dev/null +++ b/app/Http/Services/vCenter/PowerManagementService.php @@ -0,0 +1,175 @@ +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"; + } +} diff --git a/app/Http/Services/vCenter/PowerShellService.php b/app/Http/Services/vCenter/PowerShellService.php new file mode 100644 index 0000000..2738137 --- /dev/null +++ b/app/Http/Services/vCenter/PowerShellService.php @@ -0,0 +1,270 @@ +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 '' -User '' -Password '' | Out-Null + +& { + +} | Out-String +PS; + + $psScript = strtr($psTemplate, [ + '' => $server, + '' => $user, + '' => $pass, + '' => $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 '' -User '' -Password '' | Out-Null + +& { + +} +PS; + + $psScript = strtr($psTemplate, [ + '' => $server, + '' => $user, + '' => $pass, + '' => $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; + } +} diff --git a/app/Http/Services/vCenter/ResourceService.php b/app/Http/Services/vCenter/ResourceService.php new file mode 100644 index 0000000..4b8b67b --- /dev/null +++ b/app/Http/Services/vCenter/ResourceService.php @@ -0,0 +1,165 @@ +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(); + } +} diff --git a/app/Http/Services/vCenter/SnapshotService.php b/app/Http/Services/vCenter/SnapshotService.php new file mode 100644 index 0000000..1c857db --- /dev/null +++ b/app/Http/Services/vCenter/SnapshotService.php @@ -0,0 +1,310 @@ +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 = ' + + <_this type="SessionManager">SessionManager + ' . htmlspecialchars($username) . ' + ' . htmlspecialchars($password) . ' + + '; + + $result = $this->callSoap($body); + return $result['cookie'] ?? null; + } + + private function getVMSnapshotInfoSoap($cookie, $vmId) + { + $body = ' + + <_this type="PropertyCollector">propertyCollector + + + VirtualMachine + false + snapshot + + + ' . $vmId . ' + + + + + '; + + $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 = ' + ' . $soapBody . ' + '; + + 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]; + } +} diff --git a/app/Http/Services/vCenter/TemplateService.php b/app/Http/Services/vCenter/TemplateService.php new file mode 100644 index 0000000..dda4908 --- /dev/null +++ b/app/Http/Services/vCenter/TemplateService.php @@ -0,0 +1,104 @@ +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"; + } +} diff --git a/app/Http/Services/vCenter/VMConfigurationService.php b/app/Http/Services/vCenter/VMConfigurationService.php new file mode 100644 index 0000000..08044c0 --- /dev/null +++ b/app/Http/Services/vCenter/VMConfigurationService.php @@ -0,0 +1,195 @@ +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; + } +} diff --git a/app/Http/Services/vCenter/VirtualMachineService.php b/app/Http/Services/vCenter/VirtualMachineService.php new file mode 100644 index 0000000..2f92a1f --- /dev/null +++ b/app/Http/Services/vCenter/VirtualMachineService.php @@ -0,0 +1,356 @@ +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"; + } +} diff --git a/app/Jobs/VirtualMachine/Sync/SyncDatastore.php b/app/Jobs/VirtualMachine/Sync/SyncDatastore.php index 35f24e1..7176289 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncDatastore.php +++ b/app/Jobs/VirtualMachine/Sync/SyncDatastore.php @@ -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; } - - } + } diff --git a/app/Jobs/VirtualMachine/Sync/SyncHost.php b/app/Jobs/VirtualMachine/Sync/SyncHost.php index fc4625d..ad47e24 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncHost.php +++ b/app/Jobs/VirtualMachine/Sync/SyncHost.php @@ -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'], diff --git a/app/Jobs/VirtualMachine/Sync/SyncNetwork.php b/app/Jobs/VirtualMachine/Sync/SyncNetwork.php index 00b80a3..fe39c06 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncNetwork.php +++ b/app/Jobs/VirtualMachine/Sync/SyncNetwork.php @@ -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'], + ] + ); } } } diff --git a/app/Models/InfraManagement.php b/app/Models/InfraManagement.php new file mode 100644 index 0000000..40ffad9 --- /dev/null +++ b/app/Models/InfraManagement.php @@ -0,0 +1,30 @@ + 'array', + 'status' => 'boolean', + ]; +} diff --git a/database/migrations/2026_02_12_164158_create_infra_management_table.php b/database/migrations/2026_02_12_164158_create_infra_management_table.php new file mode 100644 index 0000000..3b24922 --- /dev/null +++ b/database/migrations/2026_02_12_164158_create_infra_management_table.php @@ -0,0 +1,36 @@ +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'); + } +}; diff --git a/database/seeders/InfraManagementSeeder.php b/database/seeders/InfraManagementSeeder.php new file mode 100644 index 0000000..fd5c48f --- /dev/null +++ b/database/seeders/InfraManagementSeeder.php @@ -0,0 +1,31 @@ + '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' + ] + ); + } +} diff --git a/database/seeders/ProvinceWardSeeder.php b/database/seeders/ProvinceWardSeeder.php new file mode 100644 index 0000000..18c0476 --- /dev/null +++ b/database/seeders/ProvinceWardSeeder.php @@ -0,0 +1,30 @@ +command->info('Province/Ward data imported successfully from SQL file.'); + } else { + $this->command->error('File database/province_ward_import.sql not found.'); + } + } +}