121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""
|
|
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/<filename>.
|
|
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"
|