sima-cli 0.0.29__py3-none-any.whl → 0.0.31__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/basic_auth.py +84 -23
- sima_cli/cli.py +2 -0
- sima_cli/install/optiview.py +51 -5
- sima_cli/update/cleanlog.py +26 -0
- sima_cli/update/local.py +74 -7
- sima_cli/update/query.py +4 -0
- sima_cli/update/remote.py +89 -14
- sima_cli/update/updater.py +9 -5
- sima_cli/utils/pkg_update_check.py +84 -0
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/METADATA +1 -1
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/RECORD +16 -14
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.29.dist-info → sima_cli-0.0.31.dist-info}/top_level.txt +0 -0
sima_cli/__version__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
# sima_cli/__version__.py
|
2
|
-
__version__ = "0.0.
|
2
|
+
__version__ = "0.0.31"
|
sima_cli/auth/basic_auth.py
CHANGED
@@ -78,55 +78,116 @@ def _fetch_and_store_csrf_token(session: requests.Session) -> str:
|
|
78
78
|
|
79
79
|
|
80
80
|
def login_external():
|
81
|
-
"""Interactive login workflow with CSRF token, cookie caching, and
|
81
|
+
"""Interactive login workflow with CSRF token, cookie caching, and TOTP handling."""
|
82
82
|
for attempt in range(1, 4):
|
83
83
|
session = requests.Session()
|
84
84
|
session.headers.update(HEADERS)
|
85
85
|
|
86
86
|
_load_cookie_jar(session)
|
87
|
-
csrf_token = _load_csrf_token()
|
88
|
-
|
89
|
-
if not csrf_token:
|
90
|
-
csrf_token = _fetch_and_store_csrf_token(session)
|
91
|
-
|
87
|
+
csrf_token = _load_csrf_token() or _fetch_and_store_csrf_token(session)
|
92
88
|
if not csrf_token:
|
93
89
|
click.echo("❌ CSRF token is missing or invalid.")
|
94
90
|
continue
|
95
|
-
|
96
91
|
session.headers["X-CSRF-Token"] = csrf_token
|
97
92
|
|
98
93
|
if _is_session_valid(session):
|
99
94
|
click.echo("🚀 You are already logged in.")
|
100
95
|
return session
|
101
96
|
|
102
|
-
#
|
97
|
+
# Fresh login prompt
|
103
98
|
_delete_auth_files()
|
104
99
|
click.echo(f"🔐 Sima.ai Developer Portal Login Attempt {attempt}/3")
|
105
100
|
username = click.prompt("Email or Username")
|
106
101
|
password = getpass.getpass("Password: ")
|
107
102
|
|
108
|
-
|
103
|
+
# Base payload (no TOTP yet)
|
104
|
+
base_data = {
|
109
105
|
"login": username,
|
110
106
|
"password": password,
|
111
|
-
"second_factor_method": "1"
|
107
|
+
"second_factor_method": "1", # TOTP
|
112
108
|
}
|
113
109
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
110
|
+
def _post_login(payload: dict):
|
111
|
+
"""POST and return (status_code, json or None, text) with robust error handling."""
|
112
|
+
try:
|
113
|
+
resp = session.post(LOGIN_URL, data=payload, timeout=30)
|
114
|
+
except Exception as e:
|
115
|
+
return None, None, f"request failed: {e}"
|
116
|
+
j = None
|
117
|
+
try:
|
118
|
+
j = resp.json()
|
119
|
+
except Exception:
|
120
|
+
pass
|
121
|
+
return resp.status_code, j, (j or resp.text)
|
122
|
+
|
123
|
+
# First try without TOTP (server may ask for it)
|
124
|
+
status, j, raw = _post_login(base_data)
|
125
|
+
|
126
|
+
# Helper: decide if success
|
127
|
+
def _success():
|
128
|
+
# Prefer server 'ok': True, but also double-check the session cookie validity
|
129
|
+
if j and j.get("ok") is True:
|
130
|
+
return True
|
131
|
+
return _is_session_valid(session)
|
132
|
+
|
133
|
+
# If immediate success
|
134
|
+
if status == 200 and _success():
|
125
135
|
_save_cookie_jar(session)
|
126
|
-
|
136
|
+
welcome = (j.get("users", [{}])[0].get("name") if isinstance(j, dict) else "") or ""
|
137
|
+
click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
|
127
138
|
return session
|
139
|
+
|
140
|
+
# See if TOTP is required/invalid; then prompt and retry up to 3 times
|
141
|
+
def _needs_totp(payload_json):
|
142
|
+
if not isinstance(payload_json, dict):
|
143
|
+
return False
|
144
|
+
if payload_json.get("totp_enabled") is True:
|
145
|
+
return True
|
146
|
+
reason = payload_json.get("reason") or payload_json.get("error")
|
147
|
+
return str(reason) in {"invalid_second_factor", "second_factor_required"}
|
148
|
+
|
149
|
+
if _needs_totp(j):
|
150
|
+
# Try up to 3 TOTP attempts within this login attempt
|
151
|
+
for totp_try in range(1, 4):
|
152
|
+
totp = click.prompt(f"🔢 Enter TOTP code (attempt {totp_try}/3)", hide_input=True)
|
153
|
+
data = dict(base_data)
|
154
|
+
data["second_factor_token"] = totp
|
155
|
+
|
156
|
+
status, j2, raw2 = _post_login(data)
|
157
|
+
if status == 200 and (j2 and j2.get("ok") is True or _is_session_valid(session)):
|
158
|
+
_save_cookie_jar(session)
|
159
|
+
welcome = (j2.get("users", [{}])[0].get("name") if isinstance(j2, dict) else "") or ""
|
160
|
+
click.echo(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!")
|
161
|
+
return session
|
162
|
+
|
163
|
+
# If still invalid 2FA, let user try again; otherwise break to outer loop
|
164
|
+
msg = ""
|
165
|
+
if isinstance(j2, dict):
|
166
|
+
reason = j2.get("reason") or j2.get("error") or ""
|
167
|
+
msg = f" ({reason})" if reason else ""
|
168
|
+
if isinstance(j2, dict) and str(j2.get("reason")) in {"invalid_second_factor"}:
|
169
|
+
click.echo(f"❌ Invalid authentication code. Please try again.{msg}")
|
170
|
+
continue
|
171
|
+
else:
|
172
|
+
click.echo(f"❌ Login failed with TOTP{msg}.")
|
173
|
+
break # go to next overall attempt
|
174
|
+
|
175
|
+
# exhausted TOTP tries
|
176
|
+
click.echo("❌ TOTP verification failed after 3 attempts.")
|
177
|
+
continue # next overall attempt
|
178
|
+
|
179
|
+
# Not a TOTP case; report error and continue
|
180
|
+
err_detail = ""
|
181
|
+
if isinstance(j, dict):
|
182
|
+
err_detail = j.get("error") or j.get("message") or ""
|
183
|
+
reason = j.get("reason")
|
184
|
+
if reason and reason != err_detail:
|
185
|
+
err_detail = f"{err_detail} ({reason})" if err_detail else reason
|
128
186
|
else:
|
129
|
-
|
187
|
+
err_detail = str(raw)[:200]
|
188
|
+
|
189
|
+
click.echo(f"❌ Login failed. {err_detail or 'Please check your credentials and try again.'}")
|
130
190
|
|
131
191
|
click.echo("❌ Login failed after 3 attempts.")
|
132
192
|
raise SystemExit(1)
|
193
|
+
|
sima_cli/cli.py
CHANGED
@@ -14,6 +14,7 @@ from sima_cli.serial.serial import connect_serial
|
|
14
14
|
from sima_cli.storage.nvme import nvme_format, nvme_remount
|
15
15
|
from sima_cli.storage.sdcard import sdcard_format
|
16
16
|
from sima_cli.network.network import network_menu
|
17
|
+
from sima_cli.utils.pkg_update_check import check_for_update
|
17
18
|
|
18
19
|
# Entry point for the CLI tool using Click's command group decorator
|
19
20
|
@click.group()
|
@@ -26,6 +27,7 @@ def main(ctx, internal):
|
|
26
27
|
Global Options:
|
27
28
|
--internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
|
28
29
|
"""
|
30
|
+
check_for_update('sima-cli')
|
29
31
|
ctx.ensure_object(dict)
|
30
32
|
|
31
33
|
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
sima_cli/install/optiview.py
CHANGED
@@ -6,19 +6,65 @@ import tarfile
|
|
6
6
|
import os
|
7
7
|
import subprocess
|
8
8
|
from pathlib import Path
|
9
|
+
import tempfile
|
10
|
+
import stat
|
9
11
|
from sima_cli.download import download_file_from_url
|
10
12
|
from sima_cli.utils.env import is_sima_board
|
11
13
|
from sima_cli.utils.config_loader import load_resource_config
|
12
14
|
|
15
|
+
|
16
|
+
def install_optiview_devkit():
|
17
|
+
"""
|
18
|
+
Install optiview under /data/optiview with system site packages visible,
|
19
|
+
and add an alias to call it via sudo from .bashrc.
|
20
|
+
"""
|
21
|
+
optiview_dir = "/data/optiview"
|
22
|
+
venv_dir = f"{optiview_dir}/.venv"
|
23
|
+
|
24
|
+
if is_sima_board():
|
25
|
+
click.echo("🛠 Detected SiMa DevKit. Cleaning up existing installation...")
|
26
|
+
|
27
|
+
# Ensure base folder exists
|
28
|
+
subprocess.run(["sudo", "mkdir", "-p", optiview_dir], check=True)
|
29
|
+
|
30
|
+
# Remove any old installation inside venv
|
31
|
+
subprocess.run(["sudo", "pip3", "uninstall", "-y", "optiview"], check=False)
|
32
|
+
|
33
|
+
click.echo("📦 Creating virtual environment with system site packages...")
|
34
|
+
subprocess.run([
|
35
|
+
"sudo", "python3", "-m", "venv", "--system-site-packages", venv_dir
|
36
|
+
], check=True)
|
37
|
+
|
38
|
+
click.echo("📦 Installing Optiview via pip inside venv...")
|
39
|
+
subprocess.run([
|
40
|
+
"sudo", f"{venv_dir}/bin/pip", "install", "optiview"
|
41
|
+
], check=True)
|
42
|
+
|
43
|
+
# Add alias to ~/.bashrc for sudo launch
|
44
|
+
alias_cmd = f"alias optiview='sudo {venv_dir}/bin/optiview'"
|
45
|
+
bashrc_path = os.path.expanduser("~/.bashrc")
|
46
|
+
|
47
|
+
# Ensure idempotent append
|
48
|
+
with open(bashrc_path, "a+") as f:
|
49
|
+
f.seek(0)
|
50
|
+
if alias_cmd not in f.read():
|
51
|
+
f.write(f"\n# Optiview alias\n{alias_cmd}\n")
|
52
|
+
click.echo(f"🔗 Added alias to {bashrc_path}")
|
53
|
+
else:
|
54
|
+
click.echo(f"ℹ️ Alias already exists in {bashrc_path}")
|
55
|
+
|
56
|
+
click.echo("✅ Optiview installed successfully on DevKit.")
|
57
|
+
click.echo("ℹ️ Restart your shell or run 'source ~/.bashrc' to use the alias.")
|
58
|
+
|
59
|
+
return True
|
60
|
+
|
61
|
+
return False
|
62
|
+
|
13
63
|
def install_optiview():
|
14
64
|
try:
|
15
65
|
# Special path for SiMa DevKit
|
16
66
|
if is_sima_board():
|
17
|
-
|
18
|
-
subprocess.run(["sudo", "pip3", "uninstall", "-y", "optiview"], check=True)
|
19
|
-
click.echo("📦 Installing Optiview via pip...")
|
20
|
-
subprocess.run(["sudo", "pip3", "install", "optiview"], check=True)
|
21
|
-
click.echo("✅ Optiview installed successfully on DevKit.")
|
67
|
+
install_optiview_devkit()
|
22
68
|
return
|
23
69
|
|
24
70
|
cfg = load_resource_config()
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Iterable, Pattern, List
|
3
|
+
|
4
|
+
# Common noisy bits you mentioned; add your own here.
|
5
|
+
DEFAULT_NOISE_PATTERNS = [
|
6
|
+
r"Information:\s+You may need to update /etc/fstab\.", # fdisk/parted hint
|
7
|
+
r"resize2fs\s+\d+\.\d+\.\d+\s*\([^)]+\)", # e2fsprogs banner
|
8
|
+
# add more as needed...
|
9
|
+
]
|
10
|
+
|
11
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
12
|
+
|
13
|
+
def strip_ansi(s: str) -> str:
|
14
|
+
return _ANSI_RE.sub("", s)
|
15
|
+
|
16
|
+
class LineSquelcher:
|
17
|
+
"""Return False for lines we want to hide."""
|
18
|
+
def __init__(self,
|
19
|
+
patterns: Iterable[str] = DEFAULT_NOISE_PATTERNS,
|
20
|
+
ignore_case: bool = True):
|
21
|
+
flags = re.IGNORECASE if ignore_case else 0
|
22
|
+
self._rx: List[Pattern[str]] = [re.compile(p, flags) for p in patterns]
|
23
|
+
|
24
|
+
def allow(self, line: str) -> bool:
|
25
|
+
clean = strip_ansi(line)
|
26
|
+
return not any(rx.search(clean) for rx in self._rx)
|
sima_cli/update/local.py
CHANGED
@@ -2,13 +2,22 @@ import os
|
|
2
2
|
from typing import Tuple
|
3
3
|
import pty
|
4
4
|
import click
|
5
|
+
import re
|
5
6
|
|
7
|
+
from typing import Optional
|
6
8
|
from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type
|
9
|
+
from sima_cli.update.cleanlog import LineSquelcher
|
7
10
|
|
8
|
-
|
11
|
+
|
12
|
+
def _run_local_cmd(
|
13
|
+
command: str,
|
14
|
+
passwd: str,
|
15
|
+
squelcher: Optional[LineSquelcher] = None,
|
16
|
+
show_summary: bool = True,
|
17
|
+
) -> bool:
|
9
18
|
"""
|
10
|
-
Run a local command using a
|
11
|
-
|
19
|
+
Run a local command using a PTY for live output.
|
20
|
+
Filters out noisy lines using LineSquelcher (if provided or default).
|
12
21
|
"""
|
13
22
|
click.echo(f"🖥️ Running: {command}")
|
14
23
|
|
@@ -16,6 +25,10 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
|
|
16
25
|
if needs_sudo:
|
17
26
|
command = f"sudo -S {command[len('sudo '):]}"
|
18
27
|
|
28
|
+
squelcher = squelcher or LineSquelcher()
|
29
|
+
suppressed = 0
|
30
|
+
buf = "" # carry partial lines between reads
|
31
|
+
|
19
32
|
try:
|
20
33
|
pid, fd = pty.fork()
|
21
34
|
|
@@ -24,19 +37,43 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
|
|
24
37
|
os.execvp("sh", ["sh", "-c", command])
|
25
38
|
else:
|
26
39
|
if needs_sudo:
|
40
|
+
# Send the sudo password immediately (stdin is the PTY)
|
27
41
|
os.write(fd, (passwd + "\n").encode())
|
28
42
|
|
29
43
|
while True:
|
30
44
|
try:
|
31
|
-
|
32
|
-
if not
|
45
|
+
chunk = os.read(fd, 4096)
|
46
|
+
if not chunk:
|
33
47
|
break
|
34
|
-
|
35
|
-
|
48
|
+
|
49
|
+
# Decode & normalize progress lines: turn lone \r into \n
|
50
|
+
text = chunk.decode("utf-8", errors="replace")
|
51
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
52
|
+
|
53
|
+
buf += text
|
54
|
+
*lines, buf = buf.split("\n")
|
55
|
+
|
56
|
+
for line in lines:
|
57
|
+
if squelcher.allow(line):
|
58
|
+
click.echo(line)
|
59
|
+
else:
|
60
|
+
suppressed += 1
|
61
|
+
|
36
62
|
except OSError:
|
37
63
|
break
|
38
64
|
|
65
|
+
# Flush any remaining partial line
|
66
|
+
if buf:
|
67
|
+
if squelcher.allow(buf):
|
68
|
+
click.echo(buf)
|
69
|
+
else:
|
70
|
+
suppressed += 1
|
71
|
+
|
39
72
|
_, status = os.waitpid(pid, 0)
|
73
|
+
|
74
|
+
if show_summary and suppressed:
|
75
|
+
click.echo(f"🔇 suppressed {suppressed} noisy line(s)")
|
76
|
+
|
40
77
|
return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
|
41
78
|
|
42
79
|
except Exception as e:
|
@@ -75,6 +112,31 @@ def get_local_board_info() -> Tuple[str, str, bool]:
|
|
75
112
|
return board_type, build_version, fdt_name, is_board_running_full_image()
|
76
113
|
|
77
114
|
|
115
|
+
def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
|
116
|
+
"""
|
117
|
+
Figure out which eMMC the device was booted from - local version
|
118
|
+
"""
|
119
|
+
try:
|
120
|
+
with open(mounts_path) as f:
|
121
|
+
for line in f:
|
122
|
+
dev, mnt = line.split()[:2]
|
123
|
+
if mnt == "/":
|
124
|
+
m = re.search(r'(mmcblk\d+)', dev)
|
125
|
+
if m:
|
126
|
+
return m.group(1)
|
127
|
+
except OSError:
|
128
|
+
pass
|
129
|
+
|
130
|
+
try:
|
131
|
+
with open(cmdline_path) as f:
|
132
|
+
m = re.search(r'\broot=(?:/dev/)?(mmcblk\d+)', f.read())
|
133
|
+
if m:
|
134
|
+
return m.group(1)
|
135
|
+
except OSError:
|
136
|
+
pass
|
137
|
+
|
138
|
+
return None
|
139
|
+
|
78
140
|
def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str, flavor: str):
|
79
141
|
"""
|
80
142
|
Perform local firmware update using swupdate commands.
|
@@ -83,6 +145,11 @@ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str,
|
|
83
145
|
click.echo("📦 Starting local firmware update...")
|
84
146
|
|
85
147
|
try:
|
148
|
+
blk = get_boot_mmc()
|
149
|
+
if blk != None:
|
150
|
+
fix_gpt_cmd = f'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/{blk} print'
|
151
|
+
_run_local_cmd(fix_gpt_cmd, passwd)
|
152
|
+
|
86
153
|
# Run tRoot update
|
87
154
|
if troot_path != None:
|
88
155
|
click.echo("⚙️ Flashing tRoot image...")
|
sima_cli/update/query.py
CHANGED
@@ -39,6 +39,10 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
39
39
|
}
|
40
40
|
|
41
41
|
response = requests.post(aql_url, data=aql_query, headers=headers)
|
42
|
+
|
43
|
+
if response.status_code == 401:
|
44
|
+
print('❌ You are not authorized to access Artifactory, use `sima-cli -i login` with your Artifactory identity token to authenticate, then try the command again.')
|
45
|
+
|
42
46
|
if response.status_code != 200:
|
43
47
|
return None
|
44
48
|
|
sima_cli/update/remote.py
CHANGED
@@ -7,7 +7,9 @@ import itertools
|
|
7
7
|
import threading
|
8
8
|
import sys
|
9
9
|
import select
|
10
|
-
from typing import Tuple
|
10
|
+
from typing import Tuple, Optional
|
11
|
+
|
12
|
+
from sima_cli.update.cleanlog import LineSquelcher
|
11
13
|
|
12
14
|
DEFAULT_USER = "sima"
|
13
15
|
DEFAULT_PASSWORD = "edgeai"
|
@@ -97,7 +99,7 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
97
99
|
|
98
100
|
for line in fdt_output.splitlines():
|
99
101
|
if line.startswith("fdt_name"):
|
100
|
-
fdt_name = line.split("=", 1)[-1].strip()
|
102
|
+
fdt_name = line.split("=", 1)[-1].strip().replace('.dtb', '')
|
101
103
|
|
102
104
|
return board_type, build_version, fdt_name, full_image
|
103
105
|
|
@@ -113,7 +115,9 @@ def _scp_file(sftp, local_path: str, remote_path: str):
|
|
113
115
|
sftp.put(local_path, remote_path)
|
114
116
|
click.echo("✅ Upload complete")
|
115
117
|
|
116
|
-
|
118
|
+
|
119
|
+
def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD,
|
120
|
+
squelcher: Optional[LineSquelcher] = None):
|
117
121
|
"""
|
118
122
|
Run a remote command over SSH and stream its output live to the console.
|
119
123
|
If the command starts with 'sudo', pipe in the password.
|
@@ -123,40 +127,56 @@ def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
|
|
123
127
|
command (str): The command to run on the remote host.
|
124
128
|
password (str): Password to use if the command requires sudo.
|
125
129
|
"""
|
126
|
-
|
130
|
+
squelcher = squelcher or LineSquelcher() # use defaults unless you pass a custom one
|
127
131
|
|
132
|
+
click.echo(f"🚀 Running on remote: {command}")
|
128
133
|
needs_sudo = command.strip().startswith("sudo")
|
129
134
|
if needs_sudo:
|
130
|
-
# Use -S to allow password from stdin
|
131
135
|
command = f"sudo -S {command[len('sudo '):]}"
|
132
136
|
|
133
137
|
stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
|
134
|
-
|
135
138
|
if needs_sudo:
|
136
|
-
# Send password immediately, followed by newline
|
137
139
|
stdin.write(password + "\n")
|
138
140
|
stdin.flush()
|
139
141
|
|
142
|
+
suppressed = 0
|
140
143
|
while not stdout.channel.exit_status_ready():
|
141
144
|
rl, _, _ = select.select([stdout.channel], [], [], 0.5)
|
142
145
|
if rl:
|
143
146
|
if stdout.channel.recv_ready():
|
144
147
|
output = stdout.channel.recv(4096).decode("utf-8", errors="replace")
|
145
148
|
for line in output.splitlines():
|
146
|
-
|
149
|
+
if squelcher.allow(line):
|
150
|
+
click.echo(f"↦ {line}")
|
151
|
+
else:
|
152
|
+
suppressed += 1
|
147
153
|
if stdout.channel.recv_stderr_ready():
|
148
154
|
err_output = stdout.channel.recv_stderr(4096).decode("utf-8", errors="replace")
|
149
155
|
for line in err_output.splitlines():
|
150
|
-
|
156
|
+
if squelcher.allow(line):
|
157
|
+
click.echo(f"⚠️ {line}")
|
158
|
+
else:
|
159
|
+
suppressed += 1
|
151
160
|
|
152
161
|
# Final remaining output
|
153
162
|
remaining = stdout.read().decode("utf-8", errors="replace")
|
154
163
|
for line in remaining.splitlines():
|
155
|
-
|
164
|
+
if squelcher.allow(line):
|
165
|
+
click.echo(f"↦ {line}")
|
166
|
+
else:
|
167
|
+
suppressed += 1
|
156
168
|
|
157
169
|
remaining_err = stderr.read().decode("utf-8", errors="replace")
|
158
170
|
for line in remaining_err.splitlines():
|
159
|
-
|
171
|
+
if squelcher.allow(line):
|
172
|
+
click.echo(f"⚠️ {line}")
|
173
|
+
else:
|
174
|
+
suppressed += 1
|
175
|
+
|
176
|
+
# Optional: surface how much noise we hid (comment out if you want it totally silent)
|
177
|
+
if suppressed:
|
178
|
+
click.echo(f"🔇 suppressed {suppressed} noisy line(s)")
|
179
|
+
|
160
180
|
|
161
181
|
def init_ssh_session(ip: str, password: str = DEFAULT_PASSWORD):
|
162
182
|
ssh = paramiko.SSHClient()
|
@@ -182,6 +202,54 @@ def reboot_remote_board(ip: str, passwd: str):
|
|
182
202
|
click.echo(f"⚠️ Unable to connect to the remote board")
|
183
203
|
|
184
204
|
|
205
|
+
def run_remote_command_capture(ssh, command: str, password: str = DEFAULT_PASSWORD):
|
206
|
+
"""
|
207
|
+
Run a remote command over SSH and return (exit_status, stdout_str, stderr_str).
|
208
|
+
Does not stream output to the console.
|
209
|
+
If the command starts with 'sudo', it will send the password via stdin.
|
210
|
+
"""
|
211
|
+
needs_sudo = command.strip().startswith("sudo")
|
212
|
+
if needs_sudo:
|
213
|
+
command = f"sudo -S {command[len('sudo '):]}"
|
214
|
+
|
215
|
+
stdin, stdout, stderr = ssh.exec_command(command, get_pty=needs_sudo)
|
216
|
+
if needs_sudo:
|
217
|
+
stdin.write(password + "\n")
|
218
|
+
stdin.flush()
|
219
|
+
|
220
|
+
out = stdout.read().decode("utf-8", errors="replace")
|
221
|
+
err = stderr.read().decode("utf-8", errors="replace")
|
222
|
+
code = stdout.channel.recv_exit_status()
|
223
|
+
return code, out, err
|
224
|
+
|
225
|
+
|
226
|
+
def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> str | None:
|
227
|
+
"""
|
228
|
+
Determine the remote boot device: 'mmcblk0', 'mmcblk1', or None.
|
229
|
+
|
230
|
+
Strategy (remote):
|
231
|
+
1) Look at the actual device mounted as '/' in /proc/mounts.
|
232
|
+
2) Fallback to parsing /proc/cmdline (root=...).
|
233
|
+
"""
|
234
|
+
# Minimal, BusyBox-friendly shell: grep/awk only, no fancy sed EREs.
|
235
|
+
remote_script = r"""
|
236
|
+
mmc="$(awk '$2=="/"{print $1}' /proc/mounts | grep -oE 'mmcblk[0-9]+' | head -n1)"
|
237
|
+
if [ -z "$mmc" ]; then
|
238
|
+
root_tok="$(sed -n 's/.*root=\([^ ]*\).*/\1/p' /proc/cmdline)"
|
239
|
+
mmc="$(printf '%s\n' "$root_tok" | grep -oE 'mmcblk[0-9]+' | head -n1)"
|
240
|
+
fi
|
241
|
+
[ -n "$mmc" ] && printf '%s\n' "$mmc"
|
242
|
+
"""
|
243
|
+
|
244
|
+
code, out, err = run_remote_command_capture(ssh, f"sh -c {quote_shell(remote_script)}", password=password)
|
245
|
+
mmc = out.strip()
|
246
|
+
return mmc if mmc else None
|
247
|
+
|
248
|
+
def quote_shell(s: str) -> str:
|
249
|
+
"""Safely single-quote a string for sh -c."""
|
250
|
+
# Turn: abc'def -> 'abc'"'"'def'
|
251
|
+
return "'" + s.replace("'", "'\"'\"'") + "'"
|
252
|
+
|
185
253
|
def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd: str):
|
186
254
|
"""
|
187
255
|
Copy a file to the remote board over SSH.
|
@@ -220,6 +288,12 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
220
288
|
remote_dir = "/tmp"
|
221
289
|
palette_name = os.path.basename(palette_path)
|
222
290
|
|
291
|
+
boot_mmc = get_remote_boot_mmc(ssh, passwd)
|
292
|
+
if boot_mmc != None:
|
293
|
+
click.echo(f'✅ Checking partition table GPT record for {boot_mmc}...')
|
294
|
+
fix_gpt_cmd = f'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/{boot_mmc} print'
|
295
|
+
run_remote_command(ssh, fix_gpt_cmd)
|
296
|
+
|
223
297
|
# Upload tRoot image
|
224
298
|
if troot_path is not None:
|
225
299
|
troot_name = os.path.basename(troot_path)
|
@@ -231,9 +305,10 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
231
305
|
ssh,
|
232
306
|
f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
|
233
307
|
)
|
234
|
-
|
235
|
-
click.
|
236
|
-
|
308
|
+
# Disabled the following code per agreement with QA, this reboot step is not required for tRoot update
|
309
|
+
# click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
|
310
|
+
# click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
|
311
|
+
# _wait_for_ssh(ip, timeout=120)
|
237
312
|
else:
|
238
313
|
click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
|
239
314
|
|
sima_cli/update/updater.py
CHANGED
@@ -137,7 +137,7 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
|
|
137
137
|
)
|
138
138
|
raise SystemExit(1)
|
139
139
|
except:
|
140
|
-
click.echo("Unable to determine available versions")
|
140
|
+
click.echo("❌ Unable to determine available versions")
|
141
141
|
exit(0)
|
142
142
|
|
143
143
|
def _sanitize_url_to_filename(url: str) -> str:
|
@@ -511,9 +511,9 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
511
511
|
flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
|
512
512
|
|
513
513
|
if board in ['davinci', 'modalix']:
|
514
|
-
click.echo(f"🔧 Target board: {board} {fdt_name}, board currently running: {version}, full_image: {full_image}")
|
514
|
+
click.echo(f"🔧 Target board: {board} : {fdt_name}, board currently running: {version}, full_image: {full_image}")
|
515
515
|
|
516
|
-
if flavor == 'full' and fdt_name != 'modalix-som
|
516
|
+
if flavor == 'full' and fdt_name != 'modalix-som':
|
517
517
|
click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
|
518
518
|
return
|
519
519
|
|
@@ -539,9 +539,13 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
539
539
|
if env_type == "host" and env_subtype == 'linux':
|
540
540
|
# Always update the remote device first then update the host driver, otherwise the host would
|
541
541
|
# not be able to connect to the board
|
542
|
-
click.echo("👉 Updating PCIe host driver and downloading firmware...")
|
543
542
|
script_path = _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=False, flavor=flavor)
|
544
|
-
|
543
|
+
click.echo("👉 sima-cli detected you are updating the board on a Linux host...")
|
544
|
+
if click.confirm("👉 Do you want to update the host PCIe driver now? If you do not intend to use a Sima PCIe card ever on this machine, enter N", default=False):
|
545
|
+
click.echo("👉 Updating PCIe host driver and downloading firmware...")
|
546
|
+
_update_host(script_path, board, ip, passwd)
|
547
|
+
else:
|
548
|
+
click.echo("⚠️ Skipping Linux host driver update.")
|
545
549
|
elif env_type == "board":
|
546
550
|
_update_board(extracted_paths, board, passwd, flavor=flavor)
|
547
551
|
elif env_type == "sdk":
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import importlib.metadata
|
2
|
+
import urllib.request
|
3
|
+
import subprocess
|
4
|
+
import json
|
5
|
+
import socket
|
6
|
+
import click
|
7
|
+
import sys
|
8
|
+
import shlex
|
9
|
+
import shutil
|
10
|
+
import glob
|
11
|
+
import os
|
12
|
+
|
13
|
+
def cleanup_pip_leftovers():
|
14
|
+
"""Remove ~-prefixed leftover dirs in site-packages."""
|
15
|
+
for path in sys.path:
|
16
|
+
if path.endswith("site-packages") and os.path.isdir(path):
|
17
|
+
junk_dirs = glob.glob(os.path.join(path, "~*"))
|
18
|
+
for d in junk_dirs:
|
19
|
+
try:
|
20
|
+
shutil.rmtree(d, ignore_errors=True)
|
21
|
+
except Exception as e:
|
22
|
+
click.secho(f"⚠️ Failed to remove {d}: {e}", fg="yellow")
|
23
|
+
|
24
|
+
def update_package(package_name: str):
|
25
|
+
"""Suggest manual update on Windows; auto-update elsewhere."""
|
26
|
+
pip_cmd = f"{shlex.quote(sys.executable)} -m pip install --upgrade {package_name}"
|
27
|
+
|
28
|
+
if sys.platform.startswith("win"):
|
29
|
+
click.secho("⚠️ Automatic self-update is not supported on Windows while the CLI is running.", fg="yellow", bold=True)
|
30
|
+
safe_cmd = pip_cmd.replace("'", "")
|
31
|
+
click.echo(f"Please run the following command in a new terminal:\n\n {safe_cmd}\n")
|
32
|
+
return
|
33
|
+
|
34
|
+
try:
|
35
|
+
subprocess.run(shlex.split(pip_cmd), check=True)
|
36
|
+
cleanup_pip_leftovers()
|
37
|
+
click.secho(f"✅ {package_name} updated successfully.", fg="green", bold=True)
|
38
|
+
except subprocess.CalledProcessError as e:
|
39
|
+
click.secho(f"❌ Failed to update {package_name}: {e}", fg="red", bold=True)
|
40
|
+
|
41
|
+
def has_internet(timeout: float = 1.0) -> bool:
|
42
|
+
"""
|
43
|
+
Quick check for internet connectivity by connecting to a known DNS server.
|
44
|
+
Uses IP to avoid DNS lookup delays.
|
45
|
+
"""
|
46
|
+
try:
|
47
|
+
socket.setdefaulttimeout(timeout)
|
48
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
49
|
+
sock.connect(("1.1.1.1", 53))
|
50
|
+
sock.close()
|
51
|
+
return True
|
52
|
+
except OSError:
|
53
|
+
return False
|
54
|
+
|
55
|
+
def check_for_update(package_name: str, timeout: float = 2.0):
|
56
|
+
|
57
|
+
if os.environ.get("SIMA_CLI_CHECK_FOR_UPDATE", "1") != "1":
|
58
|
+
print(f'⚠️ You have disabled update check with SIMA_CLI_CHECK_FOR_UPDATE environment variable, skipping sima-cli update check..')
|
59
|
+
return
|
60
|
+
|
61
|
+
try:
|
62
|
+
current_version = importlib.metadata.version(package_name)
|
63
|
+
except importlib.metadata.PackageNotFoundError:
|
64
|
+
print(f'❌ package not found {package_name}')
|
65
|
+
return
|
66
|
+
|
67
|
+
if not has_internet(timeout=0.2):
|
68
|
+
print(f'⚠️ Offline mode, skipping sima-cli update check..')
|
69
|
+
return
|
70
|
+
|
71
|
+
try:
|
72
|
+
with urllib.request.urlopen(f"https://pypi.org/pypi/{package_name}/json", timeout=timeout) as resp:
|
73
|
+
latest_version = json.load(resp)["info"]["version"]
|
74
|
+
except Exception:
|
75
|
+
return # PyPI unreachable or network error; skip
|
76
|
+
|
77
|
+
if current_version != latest_version:
|
78
|
+
click.secho(f"🔔 Update available: {current_version} → {latest_version}", fg="green", bold=True)
|
79
|
+
click.secho(f"🔔 If you don't want to automatically check for updates, set SIMA_CLI_CHECK_FOR_UPDATE environment variable to 0")
|
80
|
+
if click.confirm(f"🔔 Do you want to update {package_name} now?", default=True):
|
81
|
+
update_package(package_name)
|
82
|
+
exit(0)
|
83
|
+
else:
|
84
|
+
print('✅ sima-cli is up-to-date')
|
@@ -1,11 +1,11 @@
|
|
1
1
|
sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
|
2
2
|
sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
|
3
|
-
sima_cli/__version__.py,sha256=
|
4
|
-
sima_cli/cli.py,sha256=
|
3
|
+
sima_cli/__version__.py,sha256=5Ig2ah-U3Zaaf5Y1Kp0steH9WKPAxGvzGGgVTVSg_tY,49
|
4
|
+
sima_cli/cli.py,sha256=uCZ-9mKBGyGtfx3p97WZkdMjXL2KJlqihfZm8VsWiqw,17220
|
5
5
|
sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
sima_cli/auth/basic_auth.py,sha256=
|
8
|
+
sima_cli/auth/basic_auth.py,sha256=mEmPrj32TVu1s34xR_UrJlIKHA3xBh98i_FzIZvAWag,7364
|
9
9
|
sima_cli/auth/login.py,sha256=yCYXWgrfbP4jSTZ3hITfxlgHkdVQVzsd8hQKpqaqCKs,3780
|
10
10
|
sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
|
11
11
|
sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
|
@@ -16,7 +16,7 @@ sima_cli/install/hostdriver.py,sha256=kAWDLebs60mbWIyTbUxmNrChcKW1uD5r7FtWNSUVUE
|
|
16
16
|
sima_cli/install/metadata_info.py,sha256=wmMqwzGfXbuilkqaxRVrFOzOtTOiONkmPCyA2oDAQpA,2168
|
17
17
|
sima_cli/install/metadata_installer.py,sha256=UPXxXL5gH0iotX8WCUgEbySbYeIHE1UwsfpZvACjZQs,18928
|
18
18
|
sima_cli/install/metadata_validator.py,sha256=7954rp9vFRNnqmIMvCVTjq40kUIEbGXzfc8HmQmChe0,5221
|
19
|
-
sima_cli/install/optiview.py,sha256=
|
19
|
+
sima_cli/install/optiview.py,sha256=r4DYdQDTUbZVCR87hl5T21gsjZrhqpU8hWnYxKmUJ_k,4790
|
20
20
|
sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3930
|
21
21
|
sima_cli/mla/meminfo.py,sha256=ndc8kQJmWGEIdvNh6iIhATGdrkqM2pbddr_eHxaPNfg,1466
|
22
22
|
sima_cli/model_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -31,11 +31,12 @@ sima_cli/storage/sdcard.py,sha256=-WULjdV31-n8v5OOqfxR77qBbIK4hJnrD3xWxUVMoGI,63
|
|
31
31
|
sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
|
32
32
|
sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
|
33
33
|
sima_cli/update/bootimg.py,sha256=Eg8ZSp8LMZXbOMxX4ZPCjFOg3YEufmsVfojKrRc3fug,13631
|
34
|
-
sima_cli/update/
|
34
|
+
sima_cli/update/cleanlog.py,sha256=-V6eDl3MdsvDmCfkKUJTqkXJ_WnLJE01uxS7z96b15g,909
|
35
|
+
sima_cli/update/local.py,sha256=yOMvOu9nrODEzYZBrxUpdmlfqmkahkDk9nAEuG4RyAg,5588
|
35
36
|
sima_cli/update/netboot.py,sha256=hsJQLq4HVwFFkaWjA54VZdkMGDhO0RmylciS78qAfrM,19663
|
36
|
-
sima_cli/update/query.py,sha256=
|
37
|
-
sima_cli/update/remote.py,sha256=
|
38
|
-
sima_cli/update/updater.py,sha256
|
37
|
+
sima_cli/update/query.py,sha256=6RgvQfQT1_EtBGcibvVcz003dRKOq17NaGgL2mhaBbY,4891
|
38
|
+
sima_cli/update/remote.py,sha256=dAMIGpHqpf7VBps9JPe4hfHD_qyi1tG6ZW8E_qXQQiQ,14446
|
39
|
+
sima_cli/update/updater.py,sha256=Bi-9apDXJlnQg_sUQ46rQPXD6zbx4uE_JhXbfJIGBxE,24213
|
39
40
|
sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
41
|
sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
|
41
42
|
sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
|
@@ -44,7 +45,8 @@ sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
|
|
44
45
|
sima_cli/utils/env.py,sha256=IP5HrH0lE7RMSiBeXcEt5GCLMT5p-QQroG-uGzl5XFU,8181
|
45
46
|
sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
|
46
47
|
sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
|
47
|
-
sima_cli
|
48
|
+
sima_cli/utils/pkg_update_check.py,sha256=IAV_NAOsBDL_lYNYMRYfdZWuVq-rJ_zzHjJJZ7UQaoc,3274
|
49
|
+
sima_cli-0.0.31.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
|
48
50
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
51
|
tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
52
|
tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -53,8 +55,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
|
|
53
55
|
tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
56
|
tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
57
|
tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
|
-
sima_cli-0.0.
|
57
|
-
sima_cli-0.0.
|
58
|
-
sima_cli-0.0.
|
59
|
-
sima_cli-0.0.
|
60
|
-
sima_cli-0.0.
|
58
|
+
sima_cli-0.0.31.dist-info/METADATA,sha256=Q18Y7rOEEqe3ejP7IMqt5H6F3KipuWjUk15o_UwSIpg,3705
|
59
|
+
sima_cli-0.0.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
60
|
+
sima_cli-0.0.31.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
|
61
|
+
sima_cli-0.0.31.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
|
62
|
+
sima_cli-0.0.31.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|