sima-cli 0.0.1__py3-none-any.whl → 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sima_cli/__version__.py +1 -1
- sima_cli/auth/login.py +103 -0
- sima_cli/cli.py +105 -42
- sima_cli/download/downloader.py +30 -19
- sima_cli/model_zoo/model.py +148 -0
- sima_cli/update/local.py +94 -0
- sima_cli/update/remote.py +238 -0
- sima_cli/update/updater.py +323 -39
- sima_cli/utils/artifactory.py +63 -0
- sima_cli/utils/config.py +25 -19
- sima_cli/utils/config_loader.py +30 -0
- sima_cli-0.0.11.dist-info/METADATA +182 -0
- {sima_cli-0.0.1.dist-info → sima_cli-0.0.11.dist-info}/RECORD +17 -13
- {sima_cli-0.0.1.dist-info → sima_cli-0.0.11.dist-info}/WHEEL +1 -1
- sima_cli-0.0.1.dist-info/METADATA +0 -112
- {sima_cli-0.0.1.dist-info → sima_cli-0.0.11.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.1.dist-info → sima_cli-0.0.11.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.1.dist-info → sima_cli-0.0.11.dist-info}/top_level.txt +0 -0
sima_cli/update/local.py
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import tempfile
|
4
|
+
from typing import List
|
5
|
+
import pty
|
6
|
+
import click
|
7
|
+
|
8
|
+
def _run_local_cmd(command: str, passwd: str) -> bool:
|
9
|
+
"""
|
10
|
+
Run a local command using a pseudo-terminal (pty) to force live output flushing,
|
11
|
+
and optionally pass a sudo password.
|
12
|
+
"""
|
13
|
+
click.echo(f"🖥️ Running: {command}")
|
14
|
+
|
15
|
+
needs_sudo = command.strip().startswith("sudo")
|
16
|
+
if needs_sudo:
|
17
|
+
command = f"sudo -S {command[len('sudo '):]}"
|
18
|
+
|
19
|
+
try:
|
20
|
+
pid, fd = pty.fork()
|
21
|
+
|
22
|
+
if pid == 0:
|
23
|
+
# Child process: execute the shell command
|
24
|
+
os.execvp("sh", ["sh", "-c", command])
|
25
|
+
else:
|
26
|
+
if needs_sudo:
|
27
|
+
os.write(fd, (passwd + "\n").encode())
|
28
|
+
|
29
|
+
while True:
|
30
|
+
try:
|
31
|
+
output = os.read(fd, 1024).decode()
|
32
|
+
if not output:
|
33
|
+
break
|
34
|
+
for line in output.splitlines():
|
35
|
+
click.echo(line)
|
36
|
+
except OSError:
|
37
|
+
break
|
38
|
+
|
39
|
+
_, status = os.waitpid(pid, 0)
|
40
|
+
return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
|
41
|
+
|
42
|
+
except Exception as e:
|
43
|
+
click.echo(f"❌ Command execution error: {e}")
|
44
|
+
return False
|
45
|
+
|
46
|
+
|
47
|
+
def get_local_board_info() -> tuple[str, str]:
|
48
|
+
"""
|
49
|
+
Retrieve the local board type and build version by reading /etc/build.
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
(board_type, build_version): Tuple of strings, or ('', '') on failure.
|
53
|
+
"""
|
54
|
+
board_type = ""
|
55
|
+
build_version = ""
|
56
|
+
build_file_path = "/etc/build"
|
57
|
+
|
58
|
+
try:
|
59
|
+
with open(build_file_path, "r") as f:
|
60
|
+
for line in f:
|
61
|
+
line = line.strip()
|
62
|
+
if line.startswith("MACHINE"):
|
63
|
+
board_type = line.split("=")[-1].strip()
|
64
|
+
elif line.startswith("SIMA_BUILD_VERSION"):
|
65
|
+
build_version = line.split("=")[-1].strip()
|
66
|
+
return board_type, build_version
|
67
|
+
except Exception:
|
68
|
+
return "", ""
|
69
|
+
|
70
|
+
|
71
|
+
def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str):
|
72
|
+
"""
|
73
|
+
Perform local firmware update using swupdate commands.
|
74
|
+
Calls swupdate directly on the provided file paths.
|
75
|
+
"""
|
76
|
+
click.echo("📦 Starting local firmware update...")
|
77
|
+
|
78
|
+
try:
|
79
|
+
# Run tRoot update
|
80
|
+
click.echo("⚙️ Flashing tRoot image...")
|
81
|
+
if not _run_local_cmd(f"sudo swupdate -H simaai-image-troot:1.0 -i {troot_path}", passwd):
|
82
|
+
click.echo("❌ tRoot update failed.")
|
83
|
+
return
|
84
|
+
click.echo("✅ tRoot update completed.")
|
85
|
+
|
86
|
+
# Run Palette update
|
87
|
+
click.echo("⚙️ Flashing System image...")
|
88
|
+
if not _run_local_cmd(f"sudo swupdate -H simaai-image-palette:1.0 -i {palette_path}", passwd):
|
89
|
+
click.echo("❌ System image update failed.")
|
90
|
+
return
|
91
|
+
click.echo("✅ System image update completed. Please powercycle the device")
|
92
|
+
|
93
|
+
except Exception as e:
|
94
|
+
click.echo(f"❌ Local update failed: {e}")
|
@@ -0,0 +1,238 @@
|
|
1
|
+
import paramiko
|
2
|
+
import os
|
3
|
+
import click
|
4
|
+
import time
|
5
|
+
import socket
|
6
|
+
import itertools
|
7
|
+
import threading
|
8
|
+
import sys
|
9
|
+
import select
|
10
|
+
|
11
|
+
DEFAULT_USER = "sima"
|
12
|
+
DEFAULT_PASSWORD = "edgeai"
|
13
|
+
|
14
|
+
def _wait_for_ssh(ip: str, timeout: int = 120):
|
15
|
+
"""
|
16
|
+
Show an animated spinner while waiting for SSH on the target IP to become available.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
ip (str): IP address of the target board.
|
20
|
+
timeout (int): Maximum seconds to wait.
|
21
|
+
"""
|
22
|
+
spinner = itertools.cycle(['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'])
|
23
|
+
stop_event = threading.Event()
|
24
|
+
|
25
|
+
def animate():
|
26
|
+
while not stop_event.is_set():
|
27
|
+
sys.stdout.write(f"\r🔁 Waiting for board to reboot {next(spinner)} ")
|
28
|
+
sys.stdout.flush()
|
29
|
+
time.sleep(0.1)
|
30
|
+
|
31
|
+
thread = threading.Thread(target=animate)
|
32
|
+
thread.start()
|
33
|
+
|
34
|
+
start_time = time.time()
|
35
|
+
success = False
|
36
|
+
while time.time() - start_time < timeout:
|
37
|
+
try:
|
38
|
+
sock = socket.create_connection((ip, 22), timeout=3)
|
39
|
+
sock.close()
|
40
|
+
success = True
|
41
|
+
break
|
42
|
+
except (socket.error, paramiko.ssh_exception.SSHException):
|
43
|
+
time.sleep(2) # wait and retry
|
44
|
+
|
45
|
+
stop_event.set()
|
46
|
+
thread.join()
|
47
|
+
|
48
|
+
if not success:
|
49
|
+
print(f"❌ Timeout: SSH did not become available on {ip} within {timeout} seconds.")
|
50
|
+
else:
|
51
|
+
print("\r✅ Board is online! \n")
|
52
|
+
|
53
|
+
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> tuple[str, str]:
|
54
|
+
"""
|
55
|
+
Connect to the remote board and retrieve board type and build version.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
ip (str): IP address of the board.
|
59
|
+
timeout (int): SSH timeout in seconds.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
(board_type, build_version): Tuple of strings, or ('', '') on failure.
|
63
|
+
"""
|
64
|
+
board_type = ""
|
65
|
+
build_version = ""
|
66
|
+
|
67
|
+
try:
|
68
|
+
ssh = paramiko.SSHClient()
|
69
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
70
|
+
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
71
|
+
|
72
|
+
stdin, stdout, stderr = ssh.exec_command("cat /etc/build")
|
73
|
+
output = stdout.read().decode()
|
74
|
+
ssh.close()
|
75
|
+
|
76
|
+
for line in output.splitlines():
|
77
|
+
line = line.strip()
|
78
|
+
if line.startswith("MACHINE"):
|
79
|
+
board_type = line.split("=")[-1].strip()
|
80
|
+
elif line.startswith("SIMA_BUILD_VERSION"):
|
81
|
+
build_version = line.split("=")[-1].strip()
|
82
|
+
|
83
|
+
return board_type, build_version
|
84
|
+
|
85
|
+
except Exception:
|
86
|
+
return "", ""
|
87
|
+
|
88
|
+
|
89
|
+
def _scp_file(sftp, local_path: str, remote_path: str):
|
90
|
+
"""Upload file via SFTP and report."""
|
91
|
+
filename = os.path.basename(local_path)
|
92
|
+
click.echo(f"📤 Uploading {filename} → {remote_path}")
|
93
|
+
sftp.put(local_path, remote_path)
|
94
|
+
click.echo("✅ Upload complete")
|
95
|
+
|
96
|
+
def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
|
97
|
+
"""
|
98
|
+
Run a remote command over SSH and stream its output live to the console.
|
99
|
+
If the command starts with 'sudo', pipe in the password.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
ssh (paramiko.SSHClient): Active SSH connection.
|
103
|
+
command (str): The command to run on the remote host.
|
104
|
+
password (str): Password to use if the command requires sudo.
|
105
|
+
"""
|
106
|
+
click.echo(f"🚀 Running on remote: {command}")
|
107
|
+
|
108
|
+
needs_sudo = command.strip().startswith("sudo")
|
109
|
+
if needs_sudo:
|
110
|
+
# Use -S to allow password from stdin
|
111
|
+
command = f"sudo -S {command[len('sudo '):]}"
|
112
|
+
|
113
|
+
stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
|
114
|
+
|
115
|
+
if needs_sudo:
|
116
|
+
# Send password immediately, followed by newline
|
117
|
+
stdin.write(password + "\n")
|
118
|
+
stdin.flush()
|
119
|
+
|
120
|
+
while not stdout.channel.exit_status_ready():
|
121
|
+
rl, _, _ = select.select([stdout.channel], [], [], 0.5)
|
122
|
+
if rl:
|
123
|
+
if stdout.channel.recv_ready():
|
124
|
+
output = stdout.channel.recv(4096).decode("utf-8", errors="replace")
|
125
|
+
for line in output.splitlines():
|
126
|
+
click.echo(f"📄 {line}")
|
127
|
+
if stdout.channel.recv_stderr_ready():
|
128
|
+
err_output = stdout.channel.recv_stderr(4096).decode("utf-8", errors="replace")
|
129
|
+
for line in err_output.splitlines():
|
130
|
+
click.echo(f"⚠️ {line}")
|
131
|
+
|
132
|
+
# Final remaining output
|
133
|
+
remaining = stdout.read().decode("utf-8", errors="replace")
|
134
|
+
for line in remaining.splitlines():
|
135
|
+
click.echo(f"📄 {line}")
|
136
|
+
|
137
|
+
remaining_err = stderr.read().decode("utf-8", errors="replace")
|
138
|
+
for line in remaining_err.splitlines():
|
139
|
+
click.echo(f"⚠️ {line}")
|
140
|
+
|
141
|
+
def reboot_remote_board(ip: str, passwd: str):
|
142
|
+
"""
|
143
|
+
Reboot remote board by sending SSH command
|
144
|
+
"""
|
145
|
+
try:
|
146
|
+
ssh = paramiko.SSHClient()
|
147
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
148
|
+
|
149
|
+
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
150
|
+
|
151
|
+
_run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
|
152
|
+
_run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
|
153
|
+
|
154
|
+
except Exception as reboot_err:
|
155
|
+
click.echo(f"⚠️ Unable to connect to the remote board")
|
156
|
+
|
157
|
+
def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool):
|
158
|
+
"""
|
159
|
+
Upload and install firmware images to remote board over SSH.
|
160
|
+
Assumes default credentials: sima / edgeai.
|
161
|
+
Includes reboot and SSH wait after each step.
|
162
|
+
"""
|
163
|
+
ssh = paramiko.SSHClient()
|
164
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
165
|
+
|
166
|
+
try:
|
167
|
+
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
168
|
+
sftp = ssh.open_sftp()
|
169
|
+
remote_dir = "/tmp"
|
170
|
+
|
171
|
+
# Upload tRoot image
|
172
|
+
troot_name = os.path.basename(troot_path)
|
173
|
+
palette_name = os.path.basename(palette_path)
|
174
|
+
_scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
|
175
|
+
click.echo("🚀 Uploaded tRoot image.")
|
176
|
+
|
177
|
+
# Run tRoot update
|
178
|
+
_run_remote_command(
|
179
|
+
ssh,
|
180
|
+
f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
|
181
|
+
)
|
182
|
+
click.echo("✅ tRoot update complete.")
|
183
|
+
|
184
|
+
# Upload Palette image
|
185
|
+
_scp_file(sftp, palette_path, os.path.join(remote_dir, palette_name))
|
186
|
+
click.echo("🚀 Uploaded system image.")
|
187
|
+
|
188
|
+
# Run Palette update
|
189
|
+
_run_remote_command(
|
190
|
+
ssh,
|
191
|
+
f"sudo swupdate -H simaai-image-palette:1.0 -i /tmp/{palette_name}",
|
192
|
+
password=passwd
|
193
|
+
)
|
194
|
+
click.echo("✅ Board image update complete.")
|
195
|
+
|
196
|
+
# In the case of PCIe system, we don't need to reboot the card, instead, we will let it finish then update the PCIe driver in the host
|
197
|
+
# After that we can reboot the whole system.
|
198
|
+
if reboot_and_wait:
|
199
|
+
# Reboot and expect disconnect
|
200
|
+
click.echo("🔁 Rebooting board after update. Waiting for reconnection...")
|
201
|
+
|
202
|
+
try:
|
203
|
+
_run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
|
204
|
+
_run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
|
205
|
+
|
206
|
+
except Exception as reboot_err:
|
207
|
+
click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the device...")
|
208
|
+
|
209
|
+
try:
|
210
|
+
ssh.close()
|
211
|
+
except Exception:
|
212
|
+
pass
|
213
|
+
|
214
|
+
# Wait for board to come back
|
215
|
+
time.sleep(5)
|
216
|
+
_wait_for_ssh(ip, timeout=120)
|
217
|
+
|
218
|
+
# Reconnect and verify version
|
219
|
+
try:
|
220
|
+
click.echo("🔍 Reconnecting to verify build version...")
|
221
|
+
ssh = paramiko.SSHClient()
|
222
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
223
|
+
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
224
|
+
|
225
|
+
_run_remote_command(ssh, "cat /etc/build | grep SIMA_BUILD_VERSION", password=passwd)
|
226
|
+
ssh.close()
|
227
|
+
except Exception as e:
|
228
|
+
click.echo(f"❌ Unable to validate the version: {e}")
|
229
|
+
|
230
|
+
click.echo("✅ Firmware update process complete.")
|
231
|
+
|
232
|
+
except Exception as e:
|
233
|
+
click.echo(f"❌ Remote update failed: {e}")
|
234
|
+
|
235
|
+
|
236
|
+
if __name__ == "__main__":
|
237
|
+
_wait_for_ssh("192.168.2.20", timeout=60)
|
238
|
+
print(get_remote_board_info("192.168.2.20"))
|