$this->moduleVmwareId, 'ip' => $this->ip, 'username' => $this->username, ]); $output = $this->runWithRetry(); if ($output === null) { $this->fail(new \RuntimeException( "SSH connection to {$this->ip} failed after max attempts" )); return; } Log::info('ChangePasswordViaSshJob: chpasswd output', [ 'module_vmware_id' => $this->moduleVmwareId, 'output' => trim($output), ]); // Update DB $vm = ModuleVmware::find($this->moduleVmwareId); if (!$vm) { Log::error('ChangePasswordViaSshJob: ModuleVmware not found', [ 'module_vmware_id' => $this->moduleVmwareId, ]); return; } $vm->update([ 'password' => SignRSA::EncryptFunc($this->newPassword), 'plain_password' => $this->newPassword, ]); $this->sendPasswordEmail($vm); } // ───────────────────────────────────────────────────────────────────────── /** * Run chpasswd on the remote VM via sshpass + native SSH. * Retries up to 10 times with 30 s delay (VM may still be booting). */ private function runWithRetry(int $maxAttempts = 10, int $sleepSeconds = 30): ?string { // Remote bash command: echo 'user:pass' | chpasswd $credential = $this->username . ':' . $this->newPassword; $remoteCmd = 'echo ' . escapeshellarg($credential) . ' | chpasswd 2>&1'; $sshCmd = sprintf( 'sshpass -p %s ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p 22 %s@%s %s 2>&1', escapeshellarg($this->initialPassword), escapeshellarg($this->username), escapeshellarg($this->ip), escapeshellarg($remoteCmd), ); for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { $output = shell_exec($sshCmd); // null = shell_exec failed (command not found, etc.) // sshpass exits non-zero on connection failure → output contains error text if ($output !== null && !$this->isSshConnectionError($output)) { Log::info('ChangePasswordViaSshJob: SSH connected', [ 'module_vmware_id' => $this->moduleVmwareId, 'attempt' => $attempt, ]); return $output; } Log::warning('ChangePasswordViaSshJob: SSH not ready, retrying', [ 'module_vmware_id' => $this->moduleVmwareId, 'attempt' => $attempt, 'output_preview' => substr((string) $output, 0, 200), ]); if ($attempt < $maxAttempts) { sleep($sleepSeconds); } } return null; } /** * Detect SSH connection failures (not auth errors) in sshpass output. */ private function isSshConnectionError(string $output): bool { $lower = strtolower($output); return str_contains($lower, 'connection refused') || str_contains($lower, 'no route to host') || str_contains($lower, 'connection timed out') || str_contains($lower, 'host is down') || str_contains($lower, 'network is unreachable'); } // ───────────────────────────────────────────────────────────────────────── public function failed(\Throwable $e): void { Log::error('ChangePasswordViaSshJob: failed', [ 'module_vmware_id' => $this->moduleVmwareId, 'ip' => $this->ip, 'error' => $e->getMessage(), ]); } // ───────────────────────────────────────────────────────────────────────── private function sendPasswordEmail(ModuleVmware $vm): void { try { $customer = $vm->customer; if (!$customer) { Log::error('ChangePasswordViaSshJob: customer not found', [ 'module_vmware_id' => $this->moduleVmwareId, ]); return; } $setting = Setting::where('key', 'setting')->first(); $settingValue = $setting ? json_decode((string) $setting->value) : null; $emailData = [ 'company_name' => $settingValue->company ?? 'Thế Giới Số', 'company_logo' => $settingValue->logo ?? '', 'client_name' => trim($customer->firstname . ' ' . $customer->lastname), 'product_name' => 'Cloud VPS', 'product_billing' => 'N/A', 'product_price' => 'N/A', 'service_host' => $this->ip, 'service_username' => $this->username, 'service_port' => 22, 'service_password' => $this->newPassword, 'os_name' => ($vm->os?->osGroup?->name ?? '') . ' ' . ($vm->os?->version ?? ''), 'service_due_exp' => $vm->next_due_date ? date('d/m/Y', (int) $vm->next_due_date) : 'N/A', ]; SendInformationVirtualMachine::dispatch($emailData, $customer) ->delay(now()->addSeconds(5)); Log::info('ChangePasswordViaSshJob: email dispatched', [ 'module_vmware_id' => $this->moduleVmwareId, 'customer_email' => $customer->email, ]); } catch (\Throwable $e) { Log::error('ChangePasswordViaSshJob: email failed: ' . $e->getMessage(), [ 'module_vmware_id' => $this->moduleVmwareId, ]); } } }