Files
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

229 lines
7.5 KiB
PHP

<?php
namespace App\Http\Services\vCenter\Concerns;
use App\Http\Facade\SignRSA;
use App\Models\InfraManagement;
use App\Support\PowerCliCertificatePolicy;
use App\Support\PowerShellEscaper;
use App\Support\SshHostKeyVerifier;
use Exception;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
/**
* SSH / PowerShell execution via jumphost.
*
* Requires on the host class:
* public ?string $token
* public string $apiUri
* private string $username
* private string $password
*/
trait ManagesSsh
{
private function powerCliCertificateLine(): string
{
return PowerCliCertificatePolicy::configurationLine();
}
// ─── Auth ────────────────────────────────────────────────────────────────
private function LoginAuth(): SSH2|false
{
try {
$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 = (int) $infra->meta_data['port'];
}
$connection = new SSH2($sshHost, $sshPort);
SshHostKeyVerifier::verify($connection, $sshHost);
if ($connection->login($infra->username, SignRSA::DecryptFunc($infra->password))) {
$connection->setTimeout(0);
return $connection;
}
Log::error('SSH Login Failed: Invalid credentials for jumphost.');
return false;
} catch (Exception $error) {
Log::error('SSH Login Failed: ' . $error->getMessage());
return false;
}
}
// ─── Command variants ────────────────────────────────────────────────────
/**
* Run a PowerShell command using a stored credential file on the jumphost.
* Uses C:\secure\vcenter.cred for vCenter auth — no plain-text password in command.
*/
protected function Command($cmd): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
try {
$psCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "'
. 'Import-Module VMware.PowerCLI; '
. $this->powerCliCertificateLine() . '; '
. '$cred = Import-Clixml -Path \'C:\secure\vcenter.cred\'; '
. 'Connect-VIServer -Server \'' . $this->apiUri . '\' -Credential $cred; '
. $cmd . '; '
. 'Disconnect-VIServer -Confirm:$false" 2>&1';
Log::channel('Custom')->info('PowerShell Command: ' . substr($psCommand, 0, 200) . '...');
$out = $auth->exec($psCommand);
$errOut = $auth->getStdError();
Log::channel('Custom')->info('PowerShell Output length: ' . strlen($out));
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Exception $e) {
Log::channel('Custom')->error('Command Exception: ' . $e->getMessage());
return $e->getMessage();
}
}
/**
* Run a raw PowerShell command directly — no vCenter connect wrapper.
*/
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();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Run a command using EncodedCommand — connects to vCenter with plain credentials.
* Safer for complex scripts that would break with inline quoting.
*/
protected function CommandRSP(string $cmd): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
$script = strtr(
<<<'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,
[
'<CERT>' => $this->powerCliCertificateLine(),
'<SERVER>' => PowerShellEscaper::singleQuote($this->apiUri ?? ''),
'<USER>' => PowerShellEscaper::singleQuote($this->username ?? ''),
'<PASS>' => PowerShellEscaper::singleQuote($this->password ?? ''),
'<CMD>' => $cmd,
]
);
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($script);
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Run a command using EncodedCommand — variant used for RDP port changes.
* Optionally strips raw output down to the first JSON block found.
*/
protected function CommandChangePort(string $cmd, bool $expectJson = false): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
$script = strtr(
<<<'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,
[
'<CERT>' => $this->powerCliCertificateLine(),
'<SERVER>' => PowerShellEscaper::singleQuote($this->apiUri ?? ''),
'<USER>' => PowerShellEscaper::singleQuote($this->username ?? ''),
'<PASS>' => PowerShellEscaper::singleQuote($this->password ?? ''),
'<CMD>' => $cmd,
]
);
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($script);
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();
}
}
}