355 lines
12 KiB
PHP
355 lines
12 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 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 = '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 resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string
|
|
{
|
|
$psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1";
|
|
|
|
$args = ' -VMName ' . PowerShellEscaper::singleQuote($vmName);
|
|
$args .= ' -GuestUser ' . PowerShellEscaper::singleQuote($guestUser);
|
|
$args .= ' -GuestPassword ' . PowerShellEscaper::singleQuote($guestPassword);
|
|
$args .= ' -TargetUser ' . PowerShellEscaper::singleQuote($targetUser);
|
|
$args .= ' -NewPassword ' . PowerShellEscaper::singleQuote($newPassword);
|
|
|
|
$cmd = '& ' . PowerShellEscaper::singleQuote($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";
|
|
|
|
$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);
|
|
}
|
|
|
|
public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int
|
|
{
|
|
$ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber";
|
|
|
|
$cmd = 'Invoke-VMScript -VM ' . PowerShellEscaper::singleQuote($vmwareId)
|
|
. ' -ScriptText ' . PowerShellEscaper::singleQuote($ps)
|
|
. ' -ScriptType Powershell'
|
|
. ' -GuestUser ' . PowerShellEscaper::singleQuote($guestUser)
|
|
. ' -GuestPassword ' . PowerShellEscaper::singleQuote($guestPassword);
|
|
|
|
$output = $this->CommandRSP($cmd);
|
|
|
|
if (!$output || str_contains($output, 'Login_Fail')) {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/\b(\d{2,5})\b/', $output, $m)) {
|
|
return (int) $m[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|