165 lines
5.4 KiB
PHP
165 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\VirtualMachine\Rebuild;
|
|
|
|
use App\DTOs\VirtualMachine\Rebuild\RebuildContext;
|
|
use App\Http\Facade\SignRSA;
|
|
use App\Http\Services\vCenter as VCenterService;
|
|
use App\Models\ModuleVmware;
|
|
use App\Models\ReinstallLog;
|
|
use App\Models\vCenter as VCenterModel;
|
|
use App\Services\VsphereHostSshService;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* VmRebuildPasswordService
|
|
*
|
|
* Generates a new random password for the rebuilt VM and pushes it through
|
|
* VsphereHostSshService::changePassword(). Uses the same low-level SSH helper
|
|
* as the create flow but never dispatches ChangePasswordViaVsphereJob.
|
|
*
|
|
* The new credential is persisted on the EXISTING ModuleVmware row — the
|
|
* service identity is preserved across rebuild.
|
|
*/
|
|
final class VmRebuildPasswordService
|
|
{
|
|
public function __construct(
|
|
private readonly VsphereHostSshService $ssh,
|
|
private readonly VCenterService $vCenter,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @param int|null $reinstallLogId If supplied, the customer-supplied
|
|
* password (if any) is pulled from this
|
|
* log row and the same row is updated
|
|
* with the credential that was applied.
|
|
* @return string the plaintext password actually applied
|
|
*/
|
|
public function rotate(RebuildContext $ctx, ?int $reinstallLogId = null): string
|
|
{
|
|
if ($ctx->newVmId === null) {
|
|
throw new RuntimeException('Rebuild password: newVmId is null');
|
|
}
|
|
|
|
$vcenter = VCenterModel::find($ctx->vcenterId);
|
|
if (!$vcenter) {
|
|
throw new RuntimeException("Rebuild password: vCenter #{$ctx->vcenterId} not found");
|
|
}
|
|
|
|
// Prefer a customer-supplied credential when one was provided.
|
|
$newPassword = null;
|
|
$log = $reinstallLogId ? ReinstallLog::find($reinstallLogId) : null;
|
|
if ($log && $log->password_supplied_by_customer && $log->password) {
|
|
try {
|
|
$newPassword = SignRSA::DecryptFunc($log->password);
|
|
} catch (\Throwable $e) {
|
|
$newPassword = null;
|
|
}
|
|
}
|
|
|
|
if (!is_string($newPassword) || $newPassword === '') {
|
|
$newPassword = $this->generatePassword(16);
|
|
}
|
|
|
|
$output = $this->ssh->changePassword(
|
|
$ctx->newVmId,
|
|
$newPassword,
|
|
$ctx->osFamily,
|
|
(string) $vcenter->host,
|
|
(string) $vcenter->user,
|
|
SignRSA::DecryptFunc($vcenter->password),
|
|
);
|
|
|
|
if (!str_contains(strtoupper(trim($output)), 'SUCCESS')) {
|
|
throw new RuntimeException(
|
|
"Rebuild password: changePassword script failed — " . trim($output),
|
|
);
|
|
}
|
|
|
|
$encrypted = SignRSA::EncryptFunc($newPassword);
|
|
|
|
ModuleVmware::where('id', $ctx->moduleVmwareId)->update([
|
|
'password' => $encrypted,
|
|
'plain_password' => $newPassword,
|
|
]);
|
|
|
|
if ($log) {
|
|
$log->update([
|
|
'password' => $encrypted,
|
|
'plain_password' => $newPassword,
|
|
]);
|
|
}
|
|
|
|
Log::info('VmRebuildPasswordService: password rotated', [
|
|
'module_vmware_id' => $ctx->moduleVmwareId,
|
|
'new_vm_id' => $ctx->newVmId,
|
|
'reinstall_log_id' => $log?->id,
|
|
]);
|
|
|
|
// Windows RDP port is rotated together with the password — a fresh
|
|
// random port each rebuild. Linux keeps port 22 (no remote SSH-port
|
|
// change script exists on the jump host yet).
|
|
if ($ctx->isWindows) {
|
|
$this->rotatePort($ctx, $newPassword);
|
|
}
|
|
|
|
return $newPassword;
|
|
}
|
|
|
|
private function rotatePort(RebuildContext $ctx, string $guestPassword): void
|
|
{
|
|
$newPort = (string) random_int(10000, 60000);
|
|
|
|
$result = $this->vCenter->changeRDPPortWindows(
|
|
$ctx->transientName,
|
|
$ctx->guestUser,
|
|
$guestPassword,
|
|
$newPort,
|
|
);
|
|
|
|
$status = is_array($result) ? strtolower((string) ($result['status'] ?? '')) : '';
|
|
if ($status !== 'ok') {
|
|
throw new RuntimeException(
|
|
'Rebuild port: changeRDPPortWindows failed — ' . (is_string($result) ? $result : json_encode($result)),
|
|
);
|
|
}
|
|
|
|
ModuleVmware::where('id', $ctx->moduleVmwareId)->update([
|
|
'port' => (int) ($result['newPort'] ?? $newPort),
|
|
]);
|
|
|
|
Log::info('VmRebuildPasswordService: RDP port rotated', [
|
|
'module_vmware_id' => $ctx->moduleVmwareId,
|
|
'new_port' => $result['newPort'] ?? $newPort,
|
|
]);
|
|
}
|
|
|
|
private function generatePassword(int $length): string
|
|
{
|
|
$sets = [
|
|
'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
'abcdefghjkmnpqrstuvwxyz',
|
|
'23456789',
|
|
'!@#$*',
|
|
];
|
|
$password = '';
|
|
$allChars = implode('', $sets);
|
|
foreach ($sets as $set) {
|
|
$password .= $set[random_int(0, strlen($set) - 1)];
|
|
}
|
|
for ($i = count($sets); $i < $length; $i++) {
|
|
$password .= $allChars[random_int(0, strlen($allChars) - 1)];
|
|
}
|
|
$chars = str_split($password);
|
|
for ($i = count($chars) - 1; $i > 0; $i--) {
|
|
$j = random_int(0, $i);
|
|
[$chars[$i], $chars[$j]] = [$chars[$j], $chars[$i]];
|
|
}
|
|
return implode('', $chars);
|
|
}
|
|
}
|