Files
portal-api/app/Http/Services/vCenter/PowerShellService.php
T
baodev0andClaude Sonnet 5.5 62596d9063 feat(vm): hoàn tiền khi cấp VM lỗi, theo dõi job queue, uuid dịch vụ cho ticket, chuẩn hoá RAM addon
- VmProvisionException/VmProvisionRefund xử lý lỗi cấp phát VM
- QueueJobInspector + QueueMonitorController xem chi tiết job
- Thêm service uuid cho ticket, thời lượng cho activity_logs
- Migration chuẩn hoá RAM addon sang MB, trạng thái deleting cho snapshot

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 11:54:05 +07:00

291 lines
9.4 KiB
PHP

<?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;
use App\Support\PowerShellEscaper;
use App\Support\PowerCliCertificatePolicy;
use App\Support\SshHostKeyVerifier;
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);
SshHostKeyVerifier::verify($connection, $sshHost);
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 runPsCommand(string $psCommand): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return "Login_Fail";
}
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($psCommand);
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
$realErr = $errOut && !str_starts_with(trim($errOut), '#< CLIXML');
return $out . ($realErr ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
public function executePowerCLI(string $cmd)
{
return $this->CommandRSP($cmd);
}
private function powerCliCertificateLine(): string
{
return PowerCliCertificatePolicy::configurationLine();
}
protected function CommandRSP(string $cmd)
{
$auth = $this->LoginAuth();
if (!$auth) {
return "Login_Fail";
}
$server = PowerShellEscaper::singleQuote($this->client->apiUri ?? '');
$user = PowerShellEscaper::singleQuote($this->client->getUsername() ?? '');
$pass = PowerShellEscaper::singleQuote($this->client->getPassword() ?? '');
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
Import-Module VMware.PowerCLI -ErrorAction Stop
<CERT>
Connect-VIServer -Server <SERVER> -User <USER> -Password <PASS> | Out-Null
& {
<CMD>
} | Out-String
PS;
$psScript = strtr($psTemplate, [
'<CERT>' => $this->powerCliCertificateLine(),
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd,
]);
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($psScript);
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
$realErr = $errOut && !str_starts_with(trim($errOut), '#< CLIXML');
return $out . ($realErr ? "\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";
}
$server = PowerShellEscaper::singleQuote($this->client->apiUri ?? '');
$user = PowerShellEscaper::singleQuote($this->client->getUsername() ?? '');
$pass = PowerShellEscaper::singleQuote($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
<CERT>
Connect-VIServer -Server <SERVER> -User <USER> -Password <PASS> | Out-Null
& {
<CMD>
}
PS;
$psScript = strtr($psTemplate, [
'<CERT>' => $this->powerCliCertificateLine(),
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd,
]);
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($psScript);
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 changeRDPPortWindows(string $vmName, string $guestUser, string $guestPassword, string $newPort): array|string
{
$psFile = "C:\\CLISave\\AssignIP\\change-rdp-port.ps1";
$args = ' -VMName ' . PowerShellEscaper::singleQuote($vmName);
$args .= ' -GuestUser ' . PowerShellEscaper::singleQuote($guestUser);
$args .= ' -GuestPassword ' . PowerShellEscaper::singleQuote($guestPassword);
$args .= ' -NewPort ' . (int) $newPort;
$cmd = '& ' . PowerShellEscaper::singleQuote($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) {
\Illuminate\Support\Facades\Log::error('changeRDPPortWindows: failed to parse PowerCLI output', [
'vm_name' => $vmName,
'raw_output' => $raw,
'json_error' => json_last_error_msg(),
]);
return [
'success' => false,
'error' => 'JSON parsing failed',
];
}
if (isset($data[0])) {
$data = $data[0];
}
return $data;
}
public function changeIpVM(
string $vmName,
string $vmwareId,
string $ipAddress,
string $gateway,
string $osType,
string $guestUser = '',
string $guestPassword = '',
string $dns = '8.8.8.8',
string $hostname = ''
): string {
$isLinux = str_contains(strtolower($osType), 'linux')
|| str_contains(strtolower($osType), 'ubuntu')
|| str_contains(strtolower($osType), 'centos')
|| str_contains(strtolower($osType), 'rocky');
if ($isLinux) {
$psFile = "C:\\CLISave\\changeIPLinux.ps1";
$Command = '& ' . PowerShellEscaper::singleQuote($psFile)
. ' -VMName ' . PowerShellEscaper::singleQuote($vmName)
. ' -IP ' . PowerShellEscaper::singleQuote($ipAddress)
. ' -Gateway ' . PowerShellEscaper::singleQuote($gateway)
. ' -DNS ' . PowerShellEscaper::singleQuote($dns)
. ' -GuestUser ' . PowerShellEscaper::singleQuote($guestUser)
. ' -GuestPassword ' . PowerShellEscaper::singleQuote($guestPassword);
Log::channel('Custom')->info('[changeIpVM:linux]', [
'script' => 'changeIPLinux.ps1',
'vmware_id' => $vmwareId,
]);
return (string) $this->CommandChangePort($Command, true);
}
$VirtualMachine = "VirtualMachine-$vmwareId";
$hn = strtolower($hostname ?: $vmName);
$Command = '$vm = get-vm -Id ' . PowerShellEscaper::singleQuote($VirtualMachine) . ';'
. ' $hostname = ' . PowerShellEscaper::singleQuote($hn) . ';'
. ' $ipadd = ' . PowerShellEscaper::singleQuote($ipAddress) . ';'
. ' $gw = ' . PowerShellEscaper::singleQuote($gateway) . ';'
. ' $password = ' . PowerShellEscaper::singleQuote($guestPassword) . '; '
. ' cd /; C:\\CLISave\\changeWindows1line.ps1;';
Log::channel('Custom')->info('[changeIpVM:windows]', [
'script' => 'changeWindows1line.ps1',
'vmware_id' => $vmwareId,
]);
return (string) $this->CommandRSP($Command);
}
}