242 lines
8.9 KiB
Python
242 lines
8.9 KiB
Python
"""
|
|
SSH-based Firmware Flasher for OpenWrt Devices
|
|
|
|
Replicates the flash.ps1 logic using Python paramiko/scp:
|
|
1. Auto-accept SSH host key
|
|
2. Set device password via `passwd` command
|
|
3. Upload firmware via SCP to /tmp/
|
|
4. Verify, sync, and flash via sysupgrade
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import paramiko
|
|
from scp import SCPClient
|
|
|
|
|
|
def _create_ssh_client(ip, user, password, timeout=10):
|
|
"""Create an SSH client with auto-accept host key policy."""
|
|
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
|
|
|
|
|
|
import telnetlib
|
|
|
|
def set_device_password(ip, user="root", old_password="", new_password="admin123a",
|
|
status_cb=None):
|
|
"""
|
|
Set device password via Telnet (if raw/reset) or SSH.
|
|
"""
|
|
if status_cb:
|
|
status_cb("Checking Telnet port for raw device...")
|
|
|
|
# 1. Thử Telnet trước (OpenWrt mặc định mở Telnet 23 và cấm SSH Root khi chưa có Pass)
|
|
try:
|
|
tn = telnetlib.Telnet(ip, timeout=5)
|
|
# Nếu vô được Telnet tức là thiết bị vừa Reset cứng chưa có pass
|
|
if status_cb:
|
|
status_cb("Telnet connected! Setting password...")
|
|
|
|
# Đợi logo OpenWrt và prompt "root@OpenWrt:/# "
|
|
time.sleep(1)
|
|
tn.read_very_eager()
|
|
|
|
# Gửi lệnh đổi pass
|
|
tn.write(b"passwd\n")
|
|
time.sleep(1)
|
|
tn.write(new_password.encode('ascii') + b"\n")
|
|
time.sleep(1)
|
|
tn.write(new_password.encode('ascii') + b"\n")
|
|
time.sleep(1)
|
|
|
|
# Thoát telnet
|
|
tn.write(b"exit\n")
|
|
time.sleep(0.5)
|
|
tn.close()
|
|
|
|
# Chờ 3 giây để OpenWrt kịp đóng Telnet và nổ tiến trình Dropbear (SSH Server)
|
|
if status_cb:
|
|
status_cb("Password set via Telnet. Waiting for SSH to start...")
|
|
time.sleep(3)
|
|
return "DONE"
|
|
|
|
except ConnectionRefusedError:
|
|
# Port 23 đóng -> Tức là thiết bị đã có Pass và đã bật SSH, chuyển qua luồng mồi mật khẩu cũ
|
|
pass
|
|
except Exception as e:
|
|
# Các lỗi timeout khác có thể do ping không tới
|
|
return f"FAIL: Telnet check -> {e}"
|
|
|
|
# 2. Rơi xuống luồng SSH nếu thiết bị cũ (cổng Telnet đóng)
|
|
if status_cb:
|
|
status_cb("Connecting SSH for password update...")
|
|
|
|
try:
|
|
client = _create_ssh_client(ip, user, old_password, timeout=5)
|
|
except Exception as e:
|
|
return f"FAIL: Cannot connect SSH (Old pass incorrect?) — {e}"
|
|
|
|
try:
|
|
if status_cb:
|
|
status_cb("Setting password via SSH...")
|
|
|
|
shell = client.invoke_shell()
|
|
time.sleep(1)
|
|
if shell.recv_ready():
|
|
shell.recv(65535)
|
|
|
|
shell.send("passwd\n")
|
|
time.sleep(2)
|
|
shell.send(f"{new_password}\n")
|
|
time.sleep(1)
|
|
shell.send(f"{new_password}\n")
|
|
time.sleep(2)
|
|
|
|
if shell.recv_ready():
|
|
shell.recv(65535)
|
|
|
|
shell.send("exit\n")
|
|
time.sleep(0.5)
|
|
shell.close()
|
|
|
|
if status_cb:
|
|
status_cb("Password set ✓")
|
|
return "DONE"
|
|
|
|
except Exception as e:
|
|
return f"FAIL: {e}"
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def flash_device_ssh(ip, firmware_path, user="root", password="admin123a",
|
|
set_passwd=False, status_cb=None):
|
|
"""
|
|
Flash firmware to an OpenWrt device via SSH/SCP.
|
|
|
|
Steps (mirroring flash.ps1):
|
|
1. (Optional) Set device password via passwd
|
|
2. Upload firmware via SCP to /tmp/
|
|
3. Verify uploaded file
|
|
4. Sync filesystem
|
|
5. Execute sysupgrade
|
|
|
|
Returns:
|
|
"DONE" on success, "FAIL: reason" on error
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════
|
|
# STEP 0: Set password (optional)
|
|
# ═══════════════════════════════════════════
|
|
if set_passwd:
|
|
result = set_device_password(ip, user, "", password, status_cb)
|
|
if result.startswith("FAIL"):
|
|
# Try with current password as old password
|
|
result = set_device_password(ip, user, password, password, status_cb)
|
|
if result.startswith("FAIL"):
|
|
return result
|
|
|
|
# ═══════════════════════════════════════════
|
|
# STEP 1: Connect SSH
|
|
# ═══════════════════════════════════════════
|
|
if status_cb:
|
|
status_cb("Connecting SSH...")
|
|
|
|
try:
|
|
client = _create_ssh_client(ip, user, password)
|
|
except paramiko.AuthenticationException:
|
|
return "FAIL: SSH authentication failed"
|
|
except paramiko.SSHException as e:
|
|
return f"FAIL: SSH error — {e}"
|
|
except Exception as e:
|
|
return f"FAIL: Cannot connect — {e}"
|
|
|
|
try:
|
|
# ═══════════════════════════════════════════
|
|
# STEP 2: Upload firmware via SCP
|
|
# ═══════════════════════════════════════════
|
|
if status_cb:
|
|
status_cb("Uploading firmware via SCP...")
|
|
|
|
filename = os.path.basename(firmware_path)
|
|
remote_path = f"/tmp/{filename}"
|
|
|
|
try:
|
|
scp_client = SCPClient(client.get_transport(), socket_timeout=300)
|
|
scp_client.put(firmware_path, remote_path)
|
|
scp_client.close()
|
|
except Exception as e:
|
|
return f"FAIL: SCP upload failed — {e}"
|
|
|
|
time.sleep(2)
|
|
|
|
# ═══════════════════════════════════════════
|
|
# STEP 3: Verify firmware uploaded
|
|
# ═══════════════════════════════════════════
|
|
if status_cb:
|
|
status_cb("Verifying firmware...")
|
|
|
|
stdin, stdout, stderr = client.exec_command(
|
|
f"test -f {remote_path} && ls -lh {remote_path}",
|
|
timeout=10
|
|
)
|
|
verify_output = stdout.read().decode("utf-8", errors="ignore").strip()
|
|
verify_err = stderr.read().decode("utf-8", errors="ignore").strip()
|
|
|
|
if not verify_output:
|
|
return f"FAIL: Firmware file not found on device after upload"
|
|
|
|
# ═══════════════════════════════════════════
|
|
# STEP 4: Sync filesystem
|
|
# ═══════════════════════════════════════════
|
|
if status_cb:
|
|
status_cb("Syncing filesystem...")
|
|
|
|
client.exec_command("sync", timeout=10)
|
|
time.sleep(2)
|
|
|
|
# ═══════════════════════════════════════════
|
|
# STEP 5: Flash firmware (sysupgrade)
|
|
# ═══════════════════════════════════════════
|
|
if status_cb:
|
|
status_cb("Flashing firmware (sysupgrade)...")
|
|
|
|
try:
|
|
# Capture output by redirecting to a file in /tmp first, or read from stdout
|
|
# Use -F to force upgrade and bypass "Image metadata not present" error for uImage files
|
|
stdin, stdout, stderr = client.exec_command(f"sysupgrade -F -v -n {remote_path} > /tmp/sysup.log 2>&1")
|
|
|
|
# Wait up to 4 seconds to see if it immediately fails
|
|
# Sysupgrade takes time and normally drops the connection, so if it finishes in < 4s, it's an error.
|
|
time.sleep(4)
|
|
|
|
if stdout.channel.exit_status_ready():
|
|
exit_code = stdout.channel.recv_exit_status()
|
|
|
|
# Fetch the error log
|
|
_, 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 (Code {exit_code}). Details:\n{err_msg}"
|
|
|
|
except Exception:
|
|
# Connection drop during sysupgrade is exactly what we expect if it succeeds
|
|
pass
|
|
|
|
if status_cb:
|
|
status_cb("Rebooting...")
|
|
time.sleep(3)
|
|
|
|
return "DONE"
|
|
|
|
except Exception as e:
|
|
return f"FAIL: {e}"
|
|
finally:
|
|
try:
|
|
client.close()
|
|
except Exception:
|
|
pass
|