- 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>
229 lines
7.5 KiB
PHP
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();
|
|
}
|
|
}
|
|
}
|