360 lines
12 KiB
PHP
360 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Http\Facade\SignRSA;
|
|
use App\Models\InfraManagement;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* VsphereHostSshService
|
|
*
|
|
* Executes pre-deployed PowerShell scripts on the vSphere Windows host
|
|
* via native SSH (sshpass). No phpseclib dependency.
|
|
*
|
|
* 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 CONNECT_TIMEOUT = 20;
|
|
private const MAX_RETRIES = 5;
|
|
private const RETRY_DELAY = 5;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Remote Windows cmd.exe command — all args double-quoted
|
|
$remoteCmd = sprintf(
|
|
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s',
|
|
self::SCRIPT_DIR,
|
|
$script,
|
|
$this->warg($powerCliVmId),
|
|
$this->warg($newPassword),
|
|
$this->warg($vcenterHost),
|
|
$this->warg($vcenterUser),
|
|
$this->warg($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\.
|
|
*
|
|
* Windows script params (7): VMName IP MaskBits Gateway DNS GuestUser GuestPassword
|
|
* Linux script params (10): VCenter VCUser VCPass VMName IP MaskBits Gateway DNS GuestUser 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);
|
|
|
|
// assignIP{Linux|Windows}.ps1:
|
|
// VmId IP Gateway Cidr VCenterHost VCenterUser VCenterPassword GuestPassword
|
|
$remoteCmd = sprintf(
|
|
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s',
|
|
self::ASSIGN_DIR,
|
|
$script,
|
|
$this->warg($powerCliVmId),
|
|
$this->warg($ip),
|
|
$this->warg($gateway),
|
|
$this->warg($cidr),
|
|
$this->warg($vcenterHost),
|
|
$this->warg($vcenterUser),
|
|
$this->warg($vcenterPassword),
|
|
$this->warg($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\.
|
|
*
|
|
* Windows script params (4): VMName IP GuestUser GuestPassword
|
|
* Linux script params (7): VCenter VCUser VCPass VMName IP GuestUser 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);
|
|
|
|
// verifyIP{Linux|Windows}.ps1:
|
|
// VmId IP VCenterHost VCenterUser VCenterPassword GuestPassword
|
|
$remoteCmd = sprintf(
|
|
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s',
|
|
self::ASSIGN_DIR,
|
|
$script,
|
|
$this->warg($powerCliVmId),
|
|
$this->warg($ip),
|
|
$this->warg($vcenterHost),
|
|
$this->warg($vcenterUser),
|
|
$this->warg($vcenterPassword),
|
|
$this->warg($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 sshpass.
|
|
* Retries up to MAX_RETRIES times on connection failure.
|
|
*
|
|
* @throws \RuntimeException
|
|
*/
|
|
private function sshExec(string $remoteCmd): string
|
|
{
|
|
$ssh = $this->sshConfig();
|
|
|
|
// Full native SSH command executed on local shell
|
|
$sshCmd = sprintf(
|
|
'sshpass -p %s ssh -o StrictHostKeyChecking=no -o ConnectTimeout=%d -p %d %s@%s %s 2>&1',
|
|
escapeshellarg($ssh['password']),
|
|
self::CONNECT_TIMEOUT,
|
|
$ssh['port'],
|
|
escapeshellarg($ssh['username']),
|
|
escapeshellarg($ssh['host']),
|
|
escapeshellarg($remoteCmd),
|
|
);
|
|
|
|
for ($attempt = 1; $attempt <= self::MAX_RETRIES; $attempt++) {
|
|
$output = shell_exec($sshCmd);
|
|
|
|
if ($output === null) {
|
|
Log::warning('VsphereHostSshService: shell_exec returned null', [
|
|
'attempt' => $attempt,
|
|
]);
|
|
} else {
|
|
Log::info('VsphereHostSshService: SSH executed', ['attempt' => $attempt]);
|
|
return $output;
|
|
}
|
|
|
|
if ($attempt < self::MAX_RETRIES) {
|
|
sleep(self::RETRY_DELAY);
|
|
}
|
|
}
|
|
|
|
throw new \RuntimeException(
|
|
'SSH exec to ' . $ssh['host'] . ' failed after ' . self::MAX_RETRIES . ' attempts',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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 = sprintf(
|
|
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s',
|
|
self::CHANGE_PWD_DIR,
|
|
$script,
|
|
$this->warg($powerCliVmId),
|
|
$this->warg($newPassword),
|
|
$this->warg($vcenterHost),
|
|
$this->warg($vcenterUser),
|
|
$this->warg($vcenterPassword),
|
|
$this->warg($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;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Windows cmd.exe safe argument: wrap in double quotes, escape inner double quotes.
|
|
*/
|
|
private function warg(string $value): string
|
|
{
|
|
return '"' . str_replace('"', '\\"', $value) . '"';
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Single-quote wrap for passwords — prevents PowerShell variable expansion on $, %, ^ etc.
|
|
* Inside a single-quoted PS string, a literal single quote is escaped as ''.
|
|
*/
|
|
private function sarg(string $value): string
|
|
{
|
|
return "'" . str_replace("'", "''", $value) . "'";
|
|
}
|
|
}
|