- Implemented client access token verification middleware to secure API endpoints. - Updated ServiceController to fetch active hosting services for authenticated users. - Refactored OrderController to ensure order retrieval is customer-specific. - Improved error logging in VmController for unexpected vCenter responses. - Disabled VNPay auto-deposit in TransactionController, switching to manual deposit flow. - Enhanced ServiceClientDetailResource to include Plesk credentials for hosting accounts. - Adjusted VpsServiceResource to dynamically set page type based on product type. - Added logging for PowerShellService to capture errors in RDP port changes. - Provisioning job now resolves service plans and logs relevant information for hosting accounts. - Updated database migrations to add necessary fields for service plans and DNS servers. - Refactored Service and ServicePlan models to establish relationships with hosting accounts and DNS servers.
348 lines
11 KiB
PHP
348 lines
11 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;
|
|
|
|
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);
|
|
|
|
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();
|
|
$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);
|
|
}
|
|
|
|
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 '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
|
|
|
|
& {
|
|
<CMD>
|
|
} | Out-String
|
|
PS;
|
|
|
|
$psScript = strtr($psTemplate, [
|
|
'<SERVER>' => $server,
|
|
'<USER>' => $user,
|
|
'<PASS>' => $pass,
|
|
'<CMD>' => $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();
|
|
// PowerCLI banner is CLIXML informational records — not an error
|
|
$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";
|
|
}
|
|
|
|
$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 '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
|
|
|
|
& {
|
|
<CMD>
|
|
}
|
|
PS;
|
|
|
|
$psScript = strtr($psTemplate, [
|
|
'<SERVER>' => $server,
|
|
'<USER>' => $user,
|
|
'<PASS>' => $pass,
|
|
'<CMD>' => $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) {
|
|
\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 {
|
|
$q = fn(string $v) => str_replace("'", "''", $v);
|
|
|
|
$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 = "& '{$psFile}'"
|
|
. " -VMName '" . $q($vmName) . "'"
|
|
. " -IP '" . $q($ipAddress) . "'"
|
|
. " -Gateway '" . $q($gateway) . "'"
|
|
. " -DNS '" . $q($dns) . "'"
|
|
. " -GuestUser '" . $q($guestUser) . "'"
|
|
. " -GuestPassword '" . $q($guestPassword) . "'";
|
|
|
|
Log::channel('Custom')->info('[changeIpVM:linux] ' . $Command);
|
|
|
|
return (string) $this->CommandChangePort($Command, true);
|
|
}
|
|
|
|
$VirtualMachine = "VirtualMachine-$vmwareId";
|
|
$hn = strtolower($hostname ?: $vmName);
|
|
$Command = "\$vm = get-vm -Id '{$VirtualMachine}';"
|
|
. " \$hostname = '{$q($hn)}';"
|
|
. " \$ipadd = '{$q($ipAddress)}';"
|
|
. " \$gw = '{$q($gateway)}';"
|
|
. " \$password = \"'{$q($guestPassword)}'\"; "
|
|
. " cd /; C:\\CLISave\\changeWindows1line.ps1;";
|
|
|
|
Log::channel('Custom')->info('[changeIpVM:windows] ' . $Command);
|
|
|
|
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 '$vmwareId' "
|
|
. "-ScriptText \"$ps\" "
|
|
. "-ScriptType Powershell "
|
|
. "-GuestUser '$guestUser' "
|
|
. "-GuestPassword '$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;
|
|
}
|
|
}
|