""" SSH/SCP helper functions dùng chung cho cả 2 luồng flash. Không chứa logic nghiệp vụ — chỉ là transport layer: _create_ssh_client() — kết nối SSH với retry _upload_firmware() — upload file qua SCP lên /tmp/ _verify_firmware() — kiểm tra file tồn tại trên device _sync_and_sysupgrade() — sync + sysupgrade, trả về "DONE" / "FAIL: ..." """ import os import time import paramiko from scp import SCPClient def _create_ssh_client(ip, user, password, timeout=15): """Tạo SSH client với AutoAddPolicy, có retry 3 lần.""" last_err = None for attempt in range(3): try: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect( ip, username=user, password=password, timeout=timeout, look_for_keys=False, allow_agent=False, ) return client except paramiko.AuthenticationException: # Sai password — retry không giúp ích gì raise except Exception as e: last_err = e if attempt < 2: time.sleep(1) raise last_err def _upload_firmware(client, firmware_path, status_cb=None): """ Upload firmware qua SCP lên /tmp/. Trả về remote_path khi thành công, raise RuntimeError nếu thất bại. """ if status_cb: status_cb("Uploading firmware via SCP...") filename = os.path.basename(firmware_path) remote_path = f"/tmp/{filename}" last_err = None for attempt in range(3): try: scp = SCPClient(client.get_transport(), socket_timeout=350) scp.put(firmware_path, remote_path) scp.close() time.sleep(2) return remote_path except Exception as e: last_err = e time.sleep(1.5) raise RuntimeError(f"SCP upload failed (Check Network) — {last_err}") def _verify_firmware(client, remote_path, status_cb=None): """ Xác nhận file firmware tồn tại trên thiết bị. Raise RuntimeError nếu file không có. """ if status_cb: status_cb("Verifying firmware...") _, stdout, _ = client.exec_command( f"test -f {remote_path} && ls -lh {remote_path}", timeout=10 ) output = stdout.read().decode("utf-8", errors="ignore").strip() if not output: raise RuntimeError("Firmware file not found on device after upload") def _sync_and_sysupgrade(client, remote_path, status_cb=None): """ Sync filesystem rồi chạy sysupgrade. Trả về "DONE" khi thành công, "FAIL: ..." khi sysupgrade lỗi sớm. Connection drop trong lúc sysupgrade = thành công (thiết bị đang reboot). """ if status_cb: status_cb("Syncing filesystem...") client.exec_command("sync", timeout=10) time.sleep(2) if status_cb: status_cb("Flashing firmware (sysupgrade)...") try: # -F: bỏ qua kiểm tra metadata (cần cho uImage) # -v: verbose -n: không giữ cấu hình cũ _, stdout, _ = client.exec_command( f"sysupgrade -F -v -n {remote_path} > /tmp/sysup.log 2>&1" ) # Chờ tối đa 4 giây — nếu exit quá sớm thì coi như lỗi time.sleep(4) if stdout.channel.exit_status_ready(): exit_code = stdout.channel.recv_exit_status() _, log_out, _ = client.exec_command("cat /tmp/sysup.log") err_msg = log_out.read().decode("utf-8", errors="ignore").strip() return ( f"FAIL: sysupgrade terminated early " f"(Code {exit_code}). Details:\n{err_msg}" ) except Exception: # Connection drop = device đang reboot = thành công pass if status_cb: status_cb("Rebooting...") time.sleep(3) return "DONE"