Files
portal-api/app/Services/VsphereHostSshService.php
2026-09-27 11:42:40 +07:00

391 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Services;
use App\Http\Facade\SignRSA;
use App\Models\InfraManagement;
use App\Support\PowerShellEscaper;
use App\Support\SshHostKeyVerifier;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
/**
* VsphereHostSshService
*
* Executes pre-deployed PowerShell scripts on the vSphere Windows host
* via phpseclib SSH (no sshpass required).
*
* Scripts in C:/CLISave/ accept 5 positional params:
* VM_ID NEW_PASSWORD VCENTER_HOST VCENTER_USER VCENTER_PASSWORD
*
* Scripts only output "SUCCESS" or "FAIL".
*/
final class VsphereHostSshService
{
private const SCRIPT_DIR = 'C:\\CLISave'; // backslash required for Windows cmd.exe
private const SCRIPT_LINUX = 'changePasswordLinux.ps1';
private const SCRIPT_WINDOWS = 'changePasswordWindows.ps1';
private const ASSIGN_DIR = 'C:\\CLISave\\AssignIP';
private const SCRIPT_ASSIGN_LINUX = 'assignIPLinux.ps1';
private const SCRIPT_ASSIGN_WINDOWS = 'assignIPWindows.ps1';
private const SCRIPT_VERIFY_LINUX = 'verifyIPLinux.ps1';
private const SCRIPT_VERIFY_WINDOWS = 'verifyIPWindows.ps1';
private const CHANGE_PWD_DIR = 'C:\\CLISave\\changePassword';
private const SCRIPT_CHANGE_PWD_LINUX = 'changePwdLinux.ps1';
private const SCRIPT_CHANGE_PWD_WINDOWS = 'changePwdWindows.ps1';
private const ADDON_DIR = 'C:\\CLISave\\Addon';
private const SCRIPT_UPDATE_VM_CONFIG = 'updateVMConfig.ps1';
private const CONNECT_TIMEOUT = 20;
// ─────────────────────────────────────────────────────────────────────────
/**
* Run the OS-appropriate PS1 script on the vSphere host.
*
* @throws \RuntimeException on SSH failure
*/
public function changePassword(
string $vmId,
string $newPassword,
string $osFamily,
string $vcenterHost,
string $vcenterUser,
string $vcenterPassword,
): string {
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
? self::SCRIPT_WINDOWS
: self::SCRIPT_LINUX;
$powerCliVmId = $this->powerCliVmId($vmId);
$remoteCmd = $this->windowsPowerShellCommand(self::SCRIPT_DIR, $script, [
$powerCliVmId,
$newPassword,
$vcenterHost,
$vcenterUser,
$vcenterPassword,
]);
Log::info('VsphereHostSshService: executing script', [
'vm_id' => $vmId,
'script' => $script,
'vcenter_host' => $vcenterHost,
]);
$output = $this->sshExec($remoteCmd);
Log::info('VsphereHostSshService: script output', [
'vm_id' => $vmId,
'output' => trim($output),
]);
return $output;
}
// ─────────────────────────────────────────────────────────────────────────
/**
* Assign a static IP to the VM guest OS via PS1 script.
* Calls assignIPLinux.ps1 or assignIPWindows.ps1 in C:\CLISave\AssignIP\.
*
* Script params:
* VmId IP Gateway Cidr VCenterHost VCenterUser VCenterPassword GuestPassword
*
* Script output: JSON {"status":"ok"} or {"status":"error","message":"..."}
*/
public function assignIp(
string $vmId,
string $ip,
string $gateway,
string $cidr,
string $vcenterHost,
string $vcenterUser,
string $vcenterPassword,
string $guestPassword,
string $osFamily,
string $dns,
string $guestUser,
): string {
$isWindows = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS;
$script = $isWindows ? self::SCRIPT_ASSIGN_WINDOWS : self::SCRIPT_ASSIGN_LINUX;
$powerCliVmId = $this->powerCliVmId($vmId);
$remoteCmd = $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [
$powerCliVmId,
$ip,
$gateway,
$cidr,
$vcenterHost,
$vcenterUser,
$vcenterPassword,
$guestPassword,
]);
Log::info('VsphereHostSshService: executing assignIp', [
'vm_id' => $vmId,
'powercli_vm_id' => $powerCliVmId,
'ip' => $ip,
'script' => $script,
'os' => $osFamily,
]);
$output = $this->sshExec($remoteCmd);
Log::info('VsphereHostSshService: assignIp output', [
'vm_id' => $vmId,
'ip' => $ip,
'output' => trim($output),
]);
return $output;
}
/**
* Verify that an IP is active inside the VM guest OS.
* Calls verifyIPLinux.ps1 or verifyIPWindows.ps1 in C:\CLISave\AssignIP\.
*
* Script params:
* VmId IP VCenterHost VCenterUser VCenterPassword GuestPassword
*
* Script output: JSON {"status":"ok"} or {"status":"error","message":"..."}
*/
public function verifyIpInGuest(
string $vmId,
string $ip,
string $vcenterHost,
string $vcenterUser,
string $vcenterPassword,
string $guestPassword,
string $osFamily,
string $guestUser,
): string {
$isWindows = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS;
$script = $isWindows ? self::SCRIPT_VERIFY_WINDOWS : self::SCRIPT_VERIFY_LINUX;
$powerCliVmId = $this->powerCliVmId($vmId);
$remoteCmd = $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [
$powerCliVmId,
$ip,
$vcenterHost,
$vcenterUser,
$vcenterPassword,
$guestPassword,
]);
Log::info('VsphereHostSshService: executing verifyIpInGuest', [
'vm_id' => $vmId,
'powercli_vm_id' => $powerCliVmId,
'ip' => $ip,
'script' => $script,
'os' => $osFamily,
]);
$output = $this->sshExec($remoteCmd);
Log::info('VsphereHostSshService: verifyIpInGuest output', [
'vm_id' => $vmId,
'ip' => $ip,
'output' => trim($output),
]);
return $output;
}
// ─────────────────────────────────────────────────────────────────────────
/**
* Run a command on the vSphere SSH host via phpseclib (no sshpass required).
*
* @throws \RuntimeException
*/
private function sshExec(string $remoteCmd): string
{
$config = $this->sshConfig();
$client = new SSH2($config['host'], $config['port'], self::CONNECT_TIMEOUT);
SshHostKeyVerifier::verify($client, $config['host']);
if (!$client->login($config['username'], $config['password'])) {
throw new \RuntimeException(
"SSH login failed: {$config['host']}:{$config['port']}"
);
}
$client->setTimeout(0); // PS1 scripts can run 30–90 s; block until done
$stdout = $client->exec($remoteCmd);
$stderr = $client->getStdError();
Log::info('VsphereHostSshService: SSH executed', [
'host' => $config['host'],
'port' => $config['port'],
'stdout' => trim((string) $stdout),
'stderr' => trim((string) $stderr),
]);
if ($stdout === false) {
throw new \RuntimeException(
"SSH exec failed on {$config['host']}:{$config['port']}. stderr: " . trim((string) $stderr)
);
}
// Windows SSH servers sometimes write script output to stderr instead of stdout
if (trim($stdout) === '' && trim((string) $stderr) !== '') {
return (string) $stderr;
}
return (string) $stdout;
}
/**
* @return array{host:string, port:int, username:string, password:string}
*/
private function sshConfig(): array
{
$infra = InfraManagement::where('type', 'jumphost')
->where('status', true)
->first();
if (!$infra) {
throw new \RuntimeException('No active jumphost configuration found in InfraManagement.');
}
return [
'host' => $infra->host,
'port' => (int) ($infra->meta_data['port'] ?? 22),
'username' => $infra->username,
'password' => SignRSA::DecryptFunc($infra->password),
];
}
/**
* Update VM CPU and memory via updateVMConfig.ps1.
*
* Script path: C:\CLISave\Addon\updateVMConfig.ps1
* Params: $server_name $cpu $memoryGB $vcenter_host $vcenter_user $vcenter_password
* Output: "SUCCESS" or "FAIL"
*
* PS1 must call Connect-VIServer before Get-VM — see updateVMConfig.ps1 on vSphere host.
*/
public function updateVmConfig(
string $serverName,
int $cpu,
int $memoryGB,
string $vcenterHost,
string $vcenterUser,
string $vcenterPassword,
): string {
$remoteCmd = $this->windowsPowerShellCommand(self::ADDON_DIR, self::SCRIPT_UPDATE_VM_CONFIG, [
$serverName,
(string) $cpu,
(string) $memoryGB,
$vcenterHost,
$vcenterUser,
$vcenterPassword,
]);
Log::info('VsphereHostSshService: executing updateVmConfig', [
'server_name' => $serverName,
'cpu' => $cpu,
'memoryGB' => $memoryGB,
'vcenter_host' => $vcenterHost,
]);
$output = $this->sshExec($remoteCmd);
Log::info('VsphereHostSshService: updateVmConfig output', [
'server_name' => $serverName,
'output' => trim($output),
]);
return $output;
}
/**
* Customer-initiated VM password change.
*
* Calls changePwdLinux.ps1 / changePwdWindows.ps1 in C:\CLISave\changePassword\.
*
* Param order (matches PS1 declaration):
* $vm vm_id / vmware display name double-quoted
* $password new password single-quoted
* $vcenter_host vCenter host double-quoted
* $vcenter_user vCenter user double-quoted
* $vcenter_password vCenter password single-quoted
* $guest_password current guest password single-quoted
*
* PS1 outputs "SUCCESS" or "ERROR".
*/
public function changePasswordCustomer(
string $vmId,
string $newPassword,
string $currentGuestPassword,
string $osFamily,
string $vcenterHost,
string $vcenterUser,
string $vcenterPassword,
): string {
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
? self::SCRIPT_CHANGE_PWD_WINDOWS
: self::SCRIPT_CHANGE_PWD_LINUX;
$powerCliVmId = $this->powerCliVmId($vmId);
$remoteCmd = $this->windowsPowerShellCommand(self::CHANGE_PWD_DIR, $script, [
$powerCliVmId,
$newPassword,
$vcenterHost,
$vcenterUser,
$vcenterPassword,
$currentGuestPassword,
]);
Log::info('VsphereHostSshService: changePasswordCustomer', [
'vm_id' => $vmId,
'script' => $script,
'os' => $osFamily,
]);
$output = $this->sshExec($remoteCmd);
Log::info('VsphereHostSshService: changePasswordCustomer output', [
'vm_id' => $vmId,
'output' => trim($output),
]);
return $output;
}
// ─────────────────────────────────────────────────────────────────────────
/**
* @param array<int, string> $arguments
*/
private function windowsPowerShellCommand(string $workingDirectory, string $script, array $arguments): string
{
$scriptPath = $workingDirectory . '\\' . $script;
$psScript = '& ' . PowerShellEscaper::singleQuote($scriptPath) . ' ' . implode(' ', array_map(
fn (string $argument): string => PowerShellEscaper::singleQuote($argument),
$arguments,
));
return 'powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand '
. PowerShellEscaper::encodedCommand($psScript);
}
/**
* vCenter REST returns MoRef strings like "vm-3340"; PowerCLI scripts build
* IDs as "VirtualMachine-$vmId", so keep the MoRef intact.
*/
private function powerCliVmId(string $vmId): string
{
return $vmId;
}
}