sima-cli 0.0.39__py3-none-any.whl → 0.0.41__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 +155 -10
- sima_cli/auth/login.py +6 -40
- sima_cli/cli.py +17 -4
- sima_cli/install/metadata_installer.py +19 -6
- sima_cli/update/elxr.py +72 -0
- sima_cli/update/local.py +4 -3
- sima_cli/update/netboot.py +24 -19
- sima_cli/update/query.py +4 -1
- sima_cli/update/remote.py +62 -19
- sima_cli/update/updater.py +26 -11
- sima_cli/utils/container_registries.py +175 -0
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.dist-info}/METADATA +1 -1
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.dist-info}/RECORD +18 -16
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.39.dist-info → sima_cli-0.0.41.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.41"
|
sima_cli/auth/basic_auth.py
CHANGED
@@ -3,24 +3,40 @@ import click
|
|
3
3
|
import getpass
|
4
4
|
import requests
|
5
5
|
import json
|
6
|
-
import
|
6
|
+
import subprocess
|
7
|
+
import shutil
|
8
|
+
import base64
|
7
9
|
|
10
|
+
from typing import Optional
|
8
11
|
from http.cookiejar import MozillaCookieJar
|
9
12
|
from sima_cli.__version__ import __version__
|
13
|
+
from sima_cli.utils.env import is_sima_board
|
10
14
|
|
11
15
|
HOME_DIR = os.path.expanduser("~/.sima-cli")
|
12
16
|
COOKIE_JAR_PATH = os.path.join(HOME_DIR, ".sima-cli-cookies.txt")
|
13
17
|
CSRF_PATH = os.path.join(HOME_DIR, ".sima-cli-csrf.json")
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
# Base URLs depending on environment
|
20
|
+
# Detect staging or production environment
|
21
|
+
if os.getenv("USE_STAGING_DEV_PORTAL", "false").lower() in ("1", "true", "yes"):
|
22
|
+
DEV_PORTAL = "https://discourse-dev.sima.ai"
|
23
|
+
DOCS_PORTAL = "https://docs-dev.sima.ai"
|
24
|
+
click.secho("⚠️ Using STAGING developer portal: discourse-dev.sima.ai", fg="red", bold=True)
|
25
|
+
else:
|
26
|
+
DEV_PORTAL = "https://developer.sima.ai"
|
27
|
+
DOCS_PORTAL = "https://docs.sima.ai"
|
28
|
+
|
29
|
+
# Derived endpoints
|
30
|
+
CSRF_URL = f"{DEV_PORTAL}/session/csrf"
|
31
|
+
LOGIN_URL = f"{DEV_PORTAL}/session"
|
32
|
+
DUMMY_CHECK_URL = f"{DOCS_PORTAL}/pkg_downloads/validation"
|
33
|
+
|
18
34
|
|
19
35
|
HEADERS = {
|
20
36
|
"User-Agent": f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) sima-cli/{__version__} Chrome/137.0.0.0 Safari/537.36",
|
21
37
|
"X-Requested-With": "XMLHttpRequest",
|
22
|
-
"Referer": "
|
23
|
-
"Origin": "
|
38
|
+
"Referer": f"{DEV_PORTAL}/login",
|
39
|
+
"Origin": f"{DEV_PORTAL}",
|
24
40
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
25
41
|
"sec-ch-ua": '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
26
42
|
"sec-ch-ua-mobile": "?0",
|
@@ -62,6 +78,35 @@ def _is_session_valid(session: requests.Session) -> bool:
|
|
62
78
|
click.echo(f"❌ Error validating session: {e}")
|
63
79
|
return False
|
64
80
|
|
81
|
+
def get_ecr_access_info(session: requests.Session) -> Optional[dict]:
|
82
|
+
"""
|
83
|
+
Retrieve an ECR access token and proxy endpoint using the current authenticated session.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
session (requests.Session): An authenticated session (with valid cookies).
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Optional[dict]: JSON response containing the ECR token info, or None if failed.
|
90
|
+
"""
|
91
|
+
try:
|
92
|
+
ecr_url = f"{DOCS_PORTAL}/pkg_downloads/ecr-token"
|
93
|
+
response = session.get(ecr_url, timeout=10)
|
94
|
+
response.raise_for_status()
|
95
|
+
|
96
|
+
data = response.json()
|
97
|
+
if "authorizationToken" in data and "proxyEndpoint" in data:
|
98
|
+
return data
|
99
|
+
else:
|
100
|
+
click.secho(
|
101
|
+
"⚠️ Container registry token response missing 'authorizationToken'. "
|
102
|
+
"Contact support@sima.ai for help.",
|
103
|
+
fg="yellow",
|
104
|
+
)
|
105
|
+
return data
|
106
|
+
except Exception as e:
|
107
|
+
click.secho(f"❌ Failed to retrieve ECR token: {e}", fg="red")
|
108
|
+
return None
|
109
|
+
|
65
110
|
def _delete_auth_files():
|
66
111
|
for path in [COOKIE_JAR_PATH, CSRF_PATH]:
|
67
112
|
if os.path.exists(path):
|
@@ -106,7 +151,6 @@ def _fetch_and_store_csrf_token(session: requests.Session) -> str:
|
|
106
151
|
click.echo(f"❌ Failed to fetch CSRF token: {e}")
|
107
152
|
return ""
|
108
153
|
|
109
|
-
|
110
154
|
def login_external():
|
111
155
|
"""Interactive login workflow with CSRF token, cookie caching, and TOTP handling."""
|
112
156
|
for attempt in range(1, 4):
|
@@ -163,7 +207,9 @@ def login_external():
|
|
163
207
|
if status == 200 and _success():
|
164
208
|
_save_cookie_jar(session)
|
165
209
|
welcome = (j.get("users", [{}])[0].get("name") if isinstance(j, dict) else "") or ""
|
166
|
-
click.
|
210
|
+
click.secho(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!", fg = 'green')
|
211
|
+
token, endpoint = resolve_public_registry()
|
212
|
+
docker_login_with_token('sima_cli', token, endpoint)
|
167
213
|
return session
|
168
214
|
|
169
215
|
# See if TOTP is required/invalid; then prompt and retry up to 3 times
|
@@ -186,7 +232,9 @@ def login_external():
|
|
186
232
|
if status == 200 and (j2 and j2.get("ok") is True or _is_session_valid(session)):
|
187
233
|
_save_cookie_jar(session)
|
188
234
|
welcome = (j2.get("users", [{}])[0].get("name") if isinstance(j2, dict) else "") or ""
|
189
|
-
click.
|
235
|
+
click.secho(f"✅ Login successful. Welcome to Sima Developer Portal{', ' + welcome if welcome else ''}!", fg = 'green')
|
236
|
+
token, endpoint = resolve_public_registry()
|
237
|
+
docker_login_with_token('sima_cli', token, endpoint)
|
190
238
|
return session
|
191
239
|
|
192
240
|
# If still invalid 2FA, let user try again; otherwise break to outer loop
|
@@ -215,9 +263,106 @@ def login_external():
|
|
215
263
|
else:
|
216
264
|
err_detail = str(raw)[:200]
|
217
265
|
|
218
|
-
click.echo(f"❌ Server response code: {raw}")
|
266
|
+
click.echo(f"❌ Server response code: {json.dumps(raw)}")
|
219
267
|
click.echo(f"❌ Login failed. {err_detail or 'Please check your credentials and try again.'}")
|
220
268
|
|
221
269
|
click.echo("❌ Login failed after 3 attempts.")
|
222
270
|
raise SystemExit(1)
|
223
271
|
|
272
|
+
def resolve_public_registry(name: str = 'ecr'):
|
273
|
+
"""
|
274
|
+
Resolve a short registry alias to its corresponding public container
|
275
|
+
registry endpoint and ensure authentication if required.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
name (str): Short alias for a known registry (e.g. 'ecr').
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
Optional[str]: The resolved registry endpoint, or None on failure.
|
282
|
+
"""
|
283
|
+
name = name.lower().strip()
|
284
|
+
|
285
|
+
if not ensure_docker_available():
|
286
|
+
return None, None
|
287
|
+
|
288
|
+
if name == "ecr":
|
289
|
+
click.secho("🔍 Resolving SiMa.ai container registry...", fg="cyan")
|
290
|
+
|
291
|
+
try:
|
292
|
+
session = login_external()
|
293
|
+
if not session or not isinstance(session, requests.Session):
|
294
|
+
click.secho("❌ No valid session found. Please login first using `sima-cli login`.", fg="red")
|
295
|
+
return None, None
|
296
|
+
|
297
|
+
ecr_info = get_ecr_access_info(session)
|
298
|
+
if not ecr_info:
|
299
|
+
click.secho("❌ Failed to retrieve container registry token information.", fg="red")
|
300
|
+
return None, None
|
301
|
+
|
302
|
+
token = ecr_info.get("authorizationToken")
|
303
|
+
endpoint = ecr_info.get("proxyEndpoint")
|
304
|
+
if not token or not endpoint:
|
305
|
+
click.secho("⚠️ Missing 'authorizationToken' or 'proxyEndpoint' in container registry response.", fg="yellow")
|
306
|
+
return None, None
|
307
|
+
|
308
|
+
return token, endpoint
|
309
|
+
|
310
|
+
except Exception as e:
|
311
|
+
click.secho(f"❌ Unexpected error while resolving registry '{name}': {e}", fg="red")
|
312
|
+
return None, None
|
313
|
+
|
314
|
+
else:
|
315
|
+
raise click.ClickException(f"❌ Unknown public registry alias: {name}")
|
316
|
+
|
317
|
+
|
318
|
+
def ensure_docker_available() -> bool:
|
319
|
+
"""
|
320
|
+
Check if Docker CLI exists on PATH.
|
321
|
+
|
322
|
+
- Returns True if Docker is available.
|
323
|
+
- If missing, prints a gentle warning (only on non-SiMa hosts).
|
324
|
+
- On SiMa boards (Modalix/Davinci), stays completely silent since Docker is optional.
|
325
|
+
"""
|
326
|
+
if shutil.which("docker"):
|
327
|
+
return True
|
328
|
+
|
329
|
+
# Only warn on host systems, not on SiMa devkits
|
330
|
+
if not is_sima_board():
|
331
|
+
click.echo("⚠️ Docker CLI not found — container image pull and registry operations will be skipped until Docker is installed.")
|
332
|
+
|
333
|
+
return False
|
334
|
+
|
335
|
+
def docker_login_with_token(username: str, token: str, registry: str = "artifacts.eng.sima.ai"):
|
336
|
+
"""
|
337
|
+
Use `docker login` with --password-stdin to register Artifactory or ECR token.
|
338
|
+
|
339
|
+
- Automatically detects and decodes AWS ECR base64 tokens of the form 'QVdTOmV5...'
|
340
|
+
- Ensures Docker is available before attempting login.
|
341
|
+
- Works even when Docker uses credential helpers (e.g., credsStore=desktop).
|
342
|
+
"""
|
343
|
+
if ensure_docker_available():
|
344
|
+
# Decode if token looks like base64 (AWS ECR style)
|
345
|
+
try:
|
346
|
+
decoded = base64.b64decode(token).decode("utf-8")
|
347
|
+
if decoded.startswith("AWS:"):
|
348
|
+
# ECR token detected → force username to AWS
|
349
|
+
password = decoded.split("AWS:", 1)[1]
|
350
|
+
username = "AWS"
|
351
|
+
else:
|
352
|
+
password = token
|
353
|
+
except Exception:
|
354
|
+
# Not a valid base64 string — use raw token
|
355
|
+
password = token
|
356
|
+
|
357
|
+
proc = subprocess.run(
|
358
|
+
["docker", "login", registry, "-u", username, "--password-stdin"],
|
359
|
+
input=password.encode(),
|
360
|
+
stdout=subprocess.PIPE,
|
361
|
+
stderr=subprocess.PIPE,
|
362
|
+
)
|
363
|
+
|
364
|
+
if proc.returncode != 0:
|
365
|
+
raise click.ClickException(f"❌ Docker login failed: {proc.stderr.decode().strip()}")
|
366
|
+
|
367
|
+
click.echo(proc.stdout.decode().strip() or f"✅ Logged in to SiMa.ai container registry")
|
368
|
+
return password
|
sima_cli/auth/login.py
CHANGED
@@ -4,7 +4,7 @@ import requests
|
|
4
4
|
from sima_cli.utils.config import set_auth_token, get_auth_token
|
5
5
|
from sima_cli.utils.config_loader import load_resource_config, artifactory_url
|
6
6
|
from sima_cli.utils.artifactory import exchange_identity_token, validate_token
|
7
|
-
from sima_cli.auth.basic_auth import login_external
|
7
|
+
from sima_cli.auth.basic_auth import login_external, docker_login_with_token
|
8
8
|
|
9
9
|
def login(method: str = "external"):
|
10
10
|
"""
|
@@ -30,6 +30,7 @@ def login_internal():
|
|
30
30
|
2. Validate the token using the configured validation URL.
|
31
31
|
3. If valid, exchange it for a short-lived access token.
|
32
32
|
4. Save the short-lived token to local config.
|
33
|
+
5. Also calls docker to login
|
33
34
|
"""
|
34
35
|
|
35
36
|
cfg = load_resource_config()
|
@@ -40,7 +41,6 @@ def login_internal():
|
|
40
41
|
validate_url = f"{base_url}/{validate_url}"
|
41
42
|
exchange_url = f"{base_url}/{internal_url}"
|
42
43
|
|
43
|
-
# Check for required config values
|
44
44
|
if not validate_url or not exchange_url:
|
45
45
|
click.echo("❌ Missing 'validate_url' or 'internal_url' in internal auth config.")
|
46
46
|
click.echo("👉 Please check ~/.sima-cli/resources_internal.yaml")
|
@@ -58,7 +58,7 @@ def login_internal():
|
|
58
58
|
if not is_valid:
|
59
59
|
return click.echo("❌ Token validation failed. Please check your identity token.")
|
60
60
|
|
61
|
-
click.echo(f"✅ Identity token is valid")
|
61
|
+
click.echo(f"✅ Identity token is valid for {username}")
|
62
62
|
|
63
63
|
# Step 2: Exchange for a short-lived access token (default: 30 days)
|
64
64
|
access_token, user_name = exchange_identity_token(identity_token, exchange_url, expires_in=2592000)
|
@@ -70,41 +70,7 @@ def login_internal():
|
|
70
70
|
set_auth_token(access_token, internal=True)
|
71
71
|
click.echo(f"💾 Short-lived access token saved successfully for {user_name} (valid for 30 days).")
|
72
72
|
|
73
|
-
|
74
|
-
def _login_external():
|
75
|
-
"""
|
76
|
-
External login using Developer Portal endpoint defined in the 'public' section of YAML config.
|
77
|
-
Prompts for username/password and retrieves access token.
|
78
|
-
"""
|
79
|
-
|
80
|
-
cfg = load_resource_config()
|
81
|
-
auth_url = cfg.get("public", {}).get("auth", {}).get("auth_url")
|
82
|
-
|
83
|
-
if not auth_url:
|
84
|
-
click.echo("❌ External auth URL not configured in YAML.")
|
85
|
-
return
|
86
|
-
|
87
|
-
click.echo("🌐 Logging in using external Developer Portal...")
|
88
|
-
|
89
|
-
# Prompt for credentials
|
90
|
-
username = click.prompt("Email or Username")
|
91
|
-
password = getpass.getpass("Password: ")
|
92
|
-
|
93
|
-
data = {
|
94
|
-
"username": username,
|
95
|
-
"password": password
|
96
|
-
}
|
97
|
-
|
98
73
|
try:
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
token = response.json().get("access_token")
|
103
|
-
if not token:
|
104
|
-
return click.echo("❌ Failed to retrieve access token.")
|
105
|
-
|
106
|
-
set_auth_token(token)
|
107
|
-
click.echo("✅ External login successful.")
|
108
|
-
|
109
|
-
except requests.RequestException as e:
|
110
|
-
click.echo(f"❌ External login failed: {e}")
|
74
|
+
docker_login_with_token(user_name, access_token)
|
75
|
+
except Exception as e:
|
76
|
+
click.echo(f"⚠️ Docker credential setup failed: {e}")
|
sima_cli/cli.py
CHANGED
@@ -16,6 +16,8 @@ from sima_cli.storage.nvme import nvme_format, nvme_remount
|
|
16
16
|
from sima_cli.storage.sdcard import sdcard_format
|
17
17
|
from sima_cli.network.network import network_menu
|
18
18
|
from sima_cli.utils.pkg_update_check import check_for_update
|
19
|
+
from sima_cli.utils.container_registries import docker_logout_from_registry, install_from_cr
|
20
|
+
from sima_cli.utils.env import is_devkit_running_elxr
|
19
21
|
|
20
22
|
# Entry point for the CLI tool using Click's command group decorator
|
21
23
|
@click.group()
|
@@ -89,6 +91,7 @@ def logout_cmd(ctx):
|
|
89
91
|
|
90
92
|
if internal:
|
91
93
|
target_files = ["config.json"]
|
94
|
+
docker_logout_from_registry()
|
92
95
|
else:
|
93
96
|
target_files = [".sima-cli-cookies.txt", ".sima-cli-csrf.json"]
|
94
97
|
|
@@ -140,7 +143,7 @@ def download(ctx, url, dest):
|
|
140
143
|
# Update Command
|
141
144
|
# ----------------------
|
142
145
|
@main.command(name="update")
|
143
|
-
@click.argument('version_or_url')
|
146
|
+
@click.argument('version_or_url', required=False)
|
144
147
|
@click.option('--ip', help="Target device IP address for remote firmware update.")
|
145
148
|
@click.option(
|
146
149
|
'-y', '--yes',
|
@@ -159,8 +162,14 @@ def update(ctx, version_or_url, ip, yes, passwd, flavor):
|
|
159
162
|
Run system update across different environments.
|
160
163
|
Downloads and applies firmware updates for PCIe host or SiMa board.
|
161
164
|
|
162
|
-
version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package.
|
165
|
+
version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package or a local bundle file.
|
163
166
|
"""
|
167
|
+
if version_or_url is None:
|
168
|
+
if not is_devkit_running_elxr():
|
169
|
+
raise click.UsageError("version_or_url is required on non-ELXR DevKit systems")
|
170
|
+
else:
|
171
|
+
click.echo(f"➡️ Updating with {version_or_url}")
|
172
|
+
|
164
173
|
internal = ctx.obj.get("internal", False)
|
165
174
|
perform_update(version_or_url, ip, internal, passwd=passwd, auto_confirm=yes, flavor=flavor)
|
166
175
|
|
@@ -386,17 +395,21 @@ def install_cmd(ctx, component, version, mirror, tag):
|
|
386
395
|
click.echo("❌ You must specify either a component name or provide --metadata.")
|
387
396
|
ctx.exit(1)
|
388
397
|
|
389
|
-
component = component.lower()
|
390
|
-
|
391
398
|
# if user specified gh: as component, treat it the same as -m
|
392
399
|
if component.startswith("gh:"):
|
393
400
|
return install_from_metadata(metadata_url=component, internal=False)
|
401
|
+
|
402
|
+
# if the user specified cr: as component, install from container registry
|
403
|
+
if component.startswith("cr:"):
|
404
|
+
return install_from_cr(resource_spec=component, internal=internal)
|
394
405
|
|
395
406
|
# Validate version requirement
|
396
407
|
if component in SDK_DEPENDENT_COMPONENTS and not version:
|
397
408
|
click.echo(f"❌ The component '{component}' requires a specific SDK version. Please provide one using -v.")
|
398
409
|
ctx.exit(1)
|
399
410
|
|
411
|
+
component = component.lower()
|
412
|
+
|
400
413
|
if component in SDK_INDEPENDENT_COMPONENTS and version:
|
401
414
|
click.echo(f"ℹ️ The component '{component}' does not require an SDK version. Ignoring -v {version}.")
|
402
415
|
|
@@ -26,6 +26,7 @@ from sima_cli.utils.env import get_environment_type, get_exact_devkit_type, get_
|
|
26
26
|
from sima_cli.download.downloader import download_file_from_url
|
27
27
|
from sima_cli.install.metadata_validator import validate_metadata, MetadataValidationError
|
28
28
|
from sima_cli.install.metadata_info import print_metadata_summary, parse_size_string_to_bytes
|
29
|
+
from sima_cli.utils.container_registries import install_from_cr
|
29
30
|
|
30
31
|
console = Console()
|
31
32
|
|
@@ -260,16 +261,22 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
260
261
|
"""
|
261
262
|
Downloads resources defined in metadata to a local destination folder.
|
262
263
|
|
264
|
+
Supports resource types:
|
265
|
+
- Regular files or URLs
|
266
|
+
- Hugging Face repos (hf:<repo_id>@revision)
|
267
|
+
- GitHub repos (gh:<owner>/<repo>@ref)
|
268
|
+
- Container registries (cr:<registry>/<image>[:tag])
|
269
|
+
|
263
270
|
Args:
|
264
271
|
metadata (dict): Parsed and validated metadata
|
265
272
|
base_url (str): Base URL of the metadata file (used to resolve relative resource paths)
|
266
273
|
dest_folder (str): Local path to download resources into
|
267
|
-
internal (bool): Whether to use internal
|
274
|
+
internal (bool): Whether to use internal routing (e.g., Artifactory Docker registry)
|
268
275
|
skip_models (bool): If True, skips downloading any file path starting with 'models/'
|
269
|
-
tag (str): metadata.json tag from GitHub
|
276
|
+
tag (str): metadata.json tag from GitHub passed into resources if applicable
|
270
277
|
|
271
278
|
Returns:
|
272
|
-
list: Paths to the downloaded local files
|
279
|
+
list: Paths to the downloaded local files or pulled container image identifiers
|
273
280
|
"""
|
274
281
|
resources = metadata.get("resources", [])
|
275
282
|
if not resources:
|
@@ -278,6 +285,7 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
278
285
|
os.makedirs(dest_folder, exist_ok=True)
|
279
286
|
local_paths = []
|
280
287
|
|
288
|
+
# Filter model files if needed
|
281
289
|
filtered_resources = []
|
282
290
|
for r in resources:
|
283
291
|
if skip_models and r.strip().lower().startswith("models/"):
|
@@ -318,9 +326,9 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
318
326
|
local_paths.append(model_path)
|
319
327
|
continue
|
320
328
|
|
329
|
+
# 🐙 GitHub repo
|
321
330
|
if resource.startswith("gh:"):
|
322
331
|
resource_spec = resource[3:]
|
323
|
-
|
324
332
|
if "@" in resource_spec:
|
325
333
|
repo_id, ref = resource_spec.split("@", 1)
|
326
334
|
else:
|
@@ -338,11 +346,15 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
338
346
|
raise click.ClickException(
|
339
347
|
f"❌ Failed to download GitHub repo {owner}/{name}@{ref or 'default'}: {e}"
|
340
348
|
)
|
341
|
-
|
342
349
|
local_paths.append(repo_path)
|
343
350
|
continue
|
344
351
|
|
345
|
-
#
|
352
|
+
# 🐳 Container registry support
|
353
|
+
if resource.startswith("cr:"):
|
354
|
+
install_from_cr(resource, internal=internal)
|
355
|
+
continue
|
356
|
+
|
357
|
+
# 🌐 Standard file or URL
|
346
358
|
resource_url = urljoin(base_url, resource)
|
347
359
|
local_path = download_file_from_url(
|
348
360
|
url=resource_url,
|
@@ -357,6 +369,7 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
357
369
|
|
358
370
|
return local_paths
|
359
371
|
|
372
|
+
|
360
373
|
def selectable_resource_handler(metadata):
|
361
374
|
selectable = metadata.get("selectable-resources")
|
362
375
|
if not selectable:
|
sima_cli/update/elxr.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
import subprocess
|
2
|
+
import click
|
3
|
+
|
4
|
+
from sima_cli.utils.env import is_devkit_running_elxr
|
5
|
+
|
6
|
+
def update_elxr(version_or_url: str | None):
|
7
|
+
"""
|
8
|
+
Update packages on an ELXR-based devkit using simaai-ota.
|
9
|
+
Uses InquirerPy for interactive menus.
|
10
|
+
"""
|
11
|
+
# 1. Check ELXR system
|
12
|
+
if not is_devkit_running_elxr():
|
13
|
+
click.echo("ℹ️ Not an ELXR devkit, skipping update")
|
14
|
+
return
|
15
|
+
|
16
|
+
from InquirerPy import inquirer
|
17
|
+
|
18
|
+
# 2. Check network connectivity
|
19
|
+
if subprocess.call(["ping", "-c", "1", "deb.debian.org"],
|
20
|
+
stdout=subprocess.DEVNULL,
|
21
|
+
stderr=subprocess.DEVNULL) != 0:
|
22
|
+
click.echo("⚠️ ELXR devkit not connected to the network, skipping update")
|
23
|
+
return
|
24
|
+
|
25
|
+
# 3. Choose update path
|
26
|
+
if version_or_url is None:
|
27
|
+
choice = inquirer.select(
|
28
|
+
message="How would you like to update this ELXR devkit?",
|
29
|
+
choices=[
|
30
|
+
{"name": "Update all packages (no reinstall if up-to-date)", "value": "normal"},
|
31
|
+
{"name": "Update all packages (force reinstall)", "value": "force"},
|
32
|
+
{"name": "Update to a specific simaai-palette version", "value": "version"},
|
33
|
+
{"name": "Fix u-boot environment (force reinstall + overwrite)", "value": "fix-uboot"},
|
34
|
+
],
|
35
|
+
default="normal"
|
36
|
+
).execute()
|
37
|
+
|
38
|
+
if choice == "normal":
|
39
|
+
cmd = ["simaai-ota"]
|
40
|
+
desc = "Update all packages (no reinstall)"
|
41
|
+
elif choice == "force":
|
42
|
+
cmd = ["simaai-ota", "-f"]
|
43
|
+
desc = "Update all packages (force reinstall)"
|
44
|
+
elif choice == "version":
|
45
|
+
version = inquirer.text(
|
46
|
+
message="Enter simaai-palette version (e.g., git202510090802.8e94f9f-620):"
|
47
|
+
).execute()
|
48
|
+
cmd = ["simaai-ota", "-v", version]
|
49
|
+
desc = f"Update to specific version {version}"
|
50
|
+
elif choice == "fix-uboot":
|
51
|
+
cmd = ["simaai-ota", "-f", "-o"]
|
52
|
+
desc = "Fix u-boot env (force reinstall + overwrite)"
|
53
|
+
else:
|
54
|
+
click.echo("❌ Invalid choice, aborting update")
|
55
|
+
return
|
56
|
+
else:
|
57
|
+
cmd = ["simaai-ota", "-v", version_or_url]
|
58
|
+
desc = f"Update to specific version {version_or_url}"
|
59
|
+
|
60
|
+
# 4. Execute
|
61
|
+
cmd = ["sudo"] + cmd
|
62
|
+
click.echo(f"➡️ {desc}\n " + click.style(f"Running: {' '.join(cmd)}", fg="cyan"))
|
63
|
+
|
64
|
+
# Check if passwordless sudo is available
|
65
|
+
if subprocess.call(["sudo", "-n", "true"],
|
66
|
+
stdout=subprocess.DEVNULL,
|
67
|
+
stderr=subprocess.DEVNULL) != 0:
|
68
|
+
click.echo("ℹ️ sudo may prompt you for a password...")
|
69
|
+
|
70
|
+
subprocess.check_call(cmd)
|
71
|
+
click.echo("✅ ELXR update completed successfully")
|
72
|
+
|
sima_cli/update/local.py
CHANGED
@@ -5,7 +5,7 @@ import click
|
|
5
5
|
import re
|
6
6
|
|
7
7
|
from typing import Optional
|
8
|
-
from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type
|
8
|
+
from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type, is_devkit_running_elxr
|
9
9
|
from sima_cli.update.cleanlog import LineSquelcher
|
10
10
|
|
11
11
|
|
@@ -86,7 +86,7 @@ def get_local_board_info() -> Tuple[str, str, bool]:
|
|
86
86
|
Retrieve the local board type and build version by reading /etc/build or /etc/buildinfo.
|
87
87
|
|
88
88
|
Returns:
|
89
|
-
(board_type, build_version, devkit_name, full_image): Tuple of strings, or ('', '') on failure.
|
89
|
+
(board_type, build_version, devkit_name, full_image, fwtype): Tuple of strings, or ('', '') on failure.
|
90
90
|
"""
|
91
91
|
board_type = ""
|
92
92
|
build_version = ""
|
@@ -108,8 +108,9 @@ def get_local_board_info() -> Tuple[str, str, bool]:
|
|
108
108
|
continue
|
109
109
|
|
110
110
|
devkit_name = get_exact_devkit_type()
|
111
|
+
fwtype = 'ELXR' if is_devkit_running_elxr() else 'Yocto'
|
111
112
|
|
112
|
-
return board_type, build_version, devkit_name, is_board_running_full_image()
|
113
|
+
return board_type, build_version, devkit_name, is_board_running_full_image(), fwtype
|
113
114
|
|
114
115
|
|
115
116
|
def get_boot_mmc(mounts_path="/proc/mounts", cmdline_path="/proc/cmdline"):
|
sima_cli/update/netboot.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from sima_cli.update.updater import download_image
|
2
2
|
from sima_cli.utils.net import get_local_ip_candidates
|
3
|
-
from sima_cli.update.remote import
|
3
|
+
from sima_cli.update.remote import wait_for_ssh, copy_file_to_remote_board, DEFAULT_PASSWORD, run_remote_command, init_ssh_session
|
4
4
|
import os
|
5
5
|
import platform
|
6
6
|
import threading
|
@@ -29,6 +29,9 @@ def flash_emmc(client_manager, emmc_image_paths):
|
|
29
29
|
if info.get("state") == "Connected"
|
30
30
|
]
|
31
31
|
|
32
|
+
# must comment out when checking in, this is for testing only
|
33
|
+
# clients = [("192.168.1.20", {"type": "devkit", "state": "Connected"})]
|
34
|
+
|
32
35
|
if not clients:
|
33
36
|
click.echo("📭 No connected clients available to flash.")
|
34
37
|
return
|
@@ -75,7 +78,7 @@ def flash_emmc(client_manager, emmc_image_paths):
|
|
75
78
|
|
76
79
|
# Step c: Decide flashing method
|
77
80
|
wic_path = next((p for p in emmc_image_paths if p.endswith(".wic.gz")), None)
|
78
|
-
img_path = next((p for p in emmc_image_paths if p.endswith(".img")), None)
|
81
|
+
img_path = next((p for p in emmc_image_paths if p.endswith(".img.gz")), None)
|
79
82
|
|
80
83
|
if wic_path:
|
81
84
|
filename = os.path.basename(wic_path)
|
@@ -90,7 +93,7 @@ def flash_emmc(client_manager, emmc_image_paths):
|
|
90
93
|
elif img_path:
|
91
94
|
filename = os.path.basename(img_path)
|
92
95
|
remote_path = f"/tmp/{filename}"
|
93
|
-
flash_cmd = f"sudo
|
96
|
+
flash_cmd = f"sudo gzip -dc {remote_path} | sudo dd of=/dev/mmcblk0 bs=16M status=progress"
|
94
97
|
run_remote_command(ssh, flash_cmd)
|
95
98
|
else:
|
96
99
|
click.echo("❌ No .wic.gz or .img image found in emmc_image_paths.")
|
@@ -130,30 +133,32 @@ class ClientManager:
|
|
130
133
|
).start()
|
131
134
|
|
132
135
|
def monitor_client(self, ip, start_time):
|
133
|
-
"""
|
136
|
+
"""
|
137
|
+
Monitor client connectivity by waiting for SSH availability on the target IP.
|
138
|
+
Uses `wait_for_ssh()` instead of retrieving board info.
|
139
|
+
Retries until success or shutdown_event is set.
|
140
|
+
"""
|
134
141
|
try:
|
135
|
-
# Wait
|
142
|
+
# Wait up to 1 minute after the start before first attempt
|
136
143
|
if self.shutdown_event.wait(timeout=max(0, 60 - (time.time() - start_time))):
|
137
144
|
return
|
138
145
|
|
139
146
|
while not self.shutdown_event.is_set():
|
147
|
+
click.echo(f"🔍 Checking SSH availability for {ip}...")
|
140
148
|
try:
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
click.echo(f"✅ Board info retrieved for {ip}: {board_type} with {build_version}")
|
148
|
-
break
|
149
|
-
else:
|
150
|
-
log.debug(f"Incomplete board info for {ip}: {board_type}, {build_version}")
|
149
|
+
wait_for_ssh(ip, timeout=120)
|
150
|
+
with self.lock:
|
151
|
+
self.clients[ip]['state'] = 'Connected'
|
152
|
+
self.clients[ip]['board_info'] = "SSH available"
|
153
|
+
click.echo(f"✅ SSH is available on {ip}")
|
154
|
+
break
|
151
155
|
|
152
156
|
except Exception as e:
|
153
|
-
log.info(f"
|
157
|
+
log.info(f"SSH not yet available for {ip}, retrying in 10s: {e}")
|
154
158
|
|
159
|
+
# Wait before retrying
|
155
160
|
if self.shutdown_event.wait(timeout=10):
|
156
|
-
break
|
161
|
+
break
|
157
162
|
|
158
163
|
except Exception as e:
|
159
164
|
log.error(f"Unexpected error while monitoring {ip}: {e}")
|
@@ -391,7 +396,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
391
396
|
# Extract specific image paths
|
392
397
|
wic_gz_file = next((f for f in file_list if f.endswith(".wic.gz")), None)
|
393
398
|
bmap_file = next((f for f in file_list if f.endswith(".wic.bmap")), None)
|
394
|
-
elxr_img_file = next((f for f in file_list if f.endswith(".img")), None)
|
399
|
+
elxr_img_file = next((f for f in file_list if f.endswith(".img.gz")), None)
|
395
400
|
emmc_image_paths = [p for p in [wic_gz_file, bmap_file, elxr_img_file] if p]
|
396
401
|
|
397
402
|
# Check global custom_rootfs before doing anything else
|
@@ -403,7 +408,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
403
408
|
import glob
|
404
409
|
wic_gz_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.gz"))), None)
|
405
410
|
bmap_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.bmap"))), None)
|
406
|
-
exlr_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.img"))), None)
|
411
|
+
exlr_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.img.gz"))), None)
|
407
412
|
|
408
413
|
if not (wic_gz_file and bmap_file):
|
409
414
|
raise RuntimeError(
|
sima_cli/update/query.py
CHANGED
@@ -25,7 +25,10 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
25
25
|
"path": {{
|
26
26
|
"$match": "{fw_path}/*/artifacts/palette"
|
27
27
|
}},
|
28
|
-
"
|
28
|
+
"$or": [
|
29
|
+
{{"name": "modalix-tftp-boot-palette.tar.gz"}},
|
30
|
+
{{"name": "modalix-tftp-boot.tar.gz"}}
|
31
|
+
],
|
29
32
|
"type": "file"
|
30
33
|
}}).include("repo", "path", "name")
|
31
34
|
""".strip()
|
sima_cli/update/remote.py
CHANGED
@@ -14,7 +14,7 @@ from sima_cli.update.cleanlog import LineSquelcher
|
|
14
14
|
DEFAULT_USER = "sima"
|
15
15
|
DEFAULT_PASSWORD = "edgeai"
|
16
16
|
|
17
|
-
def
|
17
|
+
def wait_for_ssh(ip: str, timeout: int = 120):
|
18
18
|
"""
|
19
19
|
Show an animated spinner while waiting for SSH on the target IP to become available.
|
20
20
|
|
@@ -54,22 +54,24 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
|
|
54
54
|
print("\r✅ Board is online! \n")
|
55
55
|
|
56
56
|
|
57
|
-
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool]:
|
57
|
+
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str, bool, str]:
|
58
58
|
"""
|
59
|
-
Connect to the remote board and retrieve board type, build version,
|
59
|
+
Connect to the remote board and retrieve board type, build version,
|
60
|
+
devkit model, full_image flag, and fwtype (from DISTRO).
|
60
61
|
|
61
62
|
Args:
|
62
63
|
ip (str): IP address of the board.
|
63
64
|
passwd (str): SSH password.
|
64
65
|
|
65
66
|
Returns:
|
66
|
-
(board_type, build_version, devkit_model, full_image):
|
67
|
-
Tuple of strings + bool, or ('', '', '', False) on failure.
|
67
|
+
(board_type, build_version, devkit_model, full_image, fwtype):
|
68
|
+
Tuple of strings + bool, or ('', '', '', False, '') on failure.
|
68
69
|
"""
|
69
70
|
board_type = ""
|
70
71
|
build_version = ""
|
71
72
|
devkit_model = ""
|
72
73
|
full_image = False
|
74
|
+
fwtype = ""
|
73
75
|
|
74
76
|
try:
|
75
77
|
ssh = paramiko.SSHClient()
|
@@ -105,19 +107,42 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
105
107
|
board_type = line.split("=", 1)[-1].strip()
|
106
108
|
elif line.startswith("SIMA_BUILD_VERSION"):
|
107
109
|
build_version = line.split("=", 1)[-1].strip()
|
110
|
+
elif line.startswith("DISTRO "):
|
111
|
+
fwtype = line.split("=", 1)[-1].strip()
|
108
112
|
|
109
|
-
return board_type, build_version, devkit_model, full_image
|
113
|
+
return board_type, build_version, devkit_model, full_image, fwtype
|
110
114
|
|
111
115
|
except Exception as e:
|
112
116
|
click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
|
113
|
-
return "", "", "", False
|
114
|
-
|
117
|
+
return "", "", "", False, ""
|
115
118
|
|
116
119
|
def _scp_file(sftp, local_path: str, remote_path: str):
|
117
|
-
"""Upload file via SFTP
|
120
|
+
"""Upload file via SFTP with tqdm block progress bar (Windows-safe)."""
|
121
|
+
|
122
|
+
from tqdm import tqdm
|
123
|
+
|
118
124
|
filename = os.path.basename(local_path)
|
119
|
-
|
120
|
-
|
125
|
+
total_size = os.path.getsize(local_path)
|
126
|
+
|
127
|
+
# Normalize paths
|
128
|
+
local_path = os.path.abspath(local_path)
|
129
|
+
remote_path = remote_path.replace("\\", "/")
|
130
|
+
|
131
|
+
# Use Unicode blocks instead of hashes
|
132
|
+
with tqdm(
|
133
|
+
total=total_size,
|
134
|
+
unit="B",
|
135
|
+
unit_scale=True,
|
136
|
+
desc=f"📤 {filename}",
|
137
|
+
bar_format="{l_bar}{bar} | {n_fmt}/{total_fmt}",
|
138
|
+
) as pbar:
|
139
|
+
def progress(transferred, total):
|
140
|
+
pbar.update(transferred - pbar.n)
|
141
|
+
|
142
|
+
# Open explicitly to avoid Windows file lock issues
|
143
|
+
with open(local_path, "rb") as f:
|
144
|
+
sftp.putfo(f, remote_path, callback=progress)
|
145
|
+
|
121
146
|
click.echo("✅ Upload complete")
|
122
147
|
|
123
148
|
|
@@ -228,7 +253,7 @@ def run_remote_command_capture(ssh, command: str, password: str = DEFAULT_PASSWO
|
|
228
253
|
return code, out, err
|
229
254
|
|
230
255
|
|
231
|
-
def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> str
|
256
|
+
def get_remote_boot_mmc(ssh, password: str = DEFAULT_PASSWORD) -> Optional[str]:
|
232
257
|
"""
|
233
258
|
Determine the remote boot device: 'mmcblk0', 'mmcblk1', or None.
|
234
259
|
|
@@ -246,7 +271,9 @@ fi
|
|
246
271
|
[ -n "$mmc" ] && printf '%s\n' "$mmc"
|
247
272
|
"""
|
248
273
|
|
249
|
-
code, out, err = run_remote_command_capture(
|
274
|
+
code, out, err = run_remote_command_capture(
|
275
|
+
ssh, f"sh -c {quote_shell(remote_script)}", password=password
|
276
|
+
)
|
250
277
|
mmc = out.strip()
|
251
278
|
return mmc if mmc else None
|
252
279
|
|
@@ -257,20 +284,36 @@ def quote_shell(s: str) -> str:
|
|
257
284
|
|
258
285
|
def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd: str):
|
259
286
|
"""
|
260
|
-
Copy a file to the remote board over SSH.
|
287
|
+
Copy a file to the remote board over SSH with tqdm progress bar.
|
261
288
|
Assumes default credentials: sima / edgeai.
|
262
289
|
"""
|
263
290
|
ssh = paramiko.SSHClient()
|
264
291
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
292
|
+
|
293
|
+
from tqdm import tqdm
|
265
294
|
|
266
295
|
try:
|
267
296
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
268
297
|
sftp = ssh.open_sftp()
|
269
298
|
|
270
|
-
# Upload the file
|
271
299
|
base_file_path = os.path.basename(file_path)
|
272
|
-
|
273
|
-
|
300
|
+
remote_path = os.path.join(remote_dir, base_file_path)
|
301
|
+
file_size = os.path.getsize(file_path)
|
302
|
+
|
303
|
+
click.echo(f"📤 Uploading {file_path} → {remote_path}")
|
304
|
+
|
305
|
+
with tqdm(total=file_size, unit="B", unit_scale=True,
|
306
|
+
desc=f"📤 {base_file_path}", ncols=100) as pbar:
|
307
|
+
|
308
|
+
def progress(transferred, total):
|
309
|
+
pbar.update(transferred - pbar.n)
|
310
|
+
|
311
|
+
sftp.put(file_path, remote_path, callback=progress)
|
312
|
+
|
313
|
+
click.echo("✅ Upload complete")
|
314
|
+
|
315
|
+
sftp.close()
|
316
|
+
ssh.close()
|
274
317
|
return True
|
275
318
|
|
276
319
|
except Exception as e:
|
@@ -362,7 +405,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
362
405
|
|
363
406
|
# Wait for board to come back
|
364
407
|
time.sleep(5)
|
365
|
-
|
408
|
+
wait_for_ssh(ip, timeout=120)
|
366
409
|
|
367
410
|
# Reconnect and verify version
|
368
411
|
try:
|
@@ -384,5 +427,5 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
384
427
|
|
385
428
|
|
386
429
|
if __name__ == "__main__":
|
387
|
-
|
430
|
+
wait_for_ssh("192.168.2.20", timeout=60)
|
388
431
|
print(get_remote_board_info("192.168.2.20"))
|
sima_cli/update/updater.py
CHANGED
@@ -14,7 +14,8 @@ from sima_cli.download import download_file_from_url
|
|
14
14
|
from sima_cli.utils.config_loader import load_resource_config
|
15
15
|
from sima_cli.update.remote import push_and_update_remote_board, get_remote_board_info, reboot_remote_board
|
16
16
|
from sima_cli.update.query import list_available_firmware_versions
|
17
|
-
from sima_cli.utils.env import is_sima_board
|
17
|
+
from sima_cli.utils.env import is_sima_board, is_devkit_running_elxr
|
18
|
+
from sima_cli.update.elxr import update_elxr
|
18
19
|
|
19
20
|
if is_sima_board():
|
20
21
|
from sima_cli.update import local
|
@@ -65,8 +66,8 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
|
|
65
66
|
image_file = 'release.tar.gz' if flavor == 'headless' else 'graphics.tar.gz'
|
66
67
|
download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/{image_file}"
|
67
68
|
elif swtype == 'elxr':
|
68
|
-
image_file = f'{board}-tftp-boot.tar.gz'
|
69
|
-
download_url = url.rstrip("/") + f"/soc-images/elxr/{board}/{version_or_url}/artifacts/
|
69
|
+
image_file = f'{board}-tftp-boot-minimal.tar.gz'
|
70
|
+
download_url = url.rstrip("/") + f"/soc-images/elxr/{board}/{version_or_url}/artifacts/minimal/{image_file}"
|
70
71
|
|
71
72
|
return download_url
|
72
73
|
|
@@ -204,7 +205,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
204
205
|
f"{board}-dvt.dtb",
|
205
206
|
f"{board}-hhhl_x16.dtb",
|
206
207
|
f"pcie-4rc-2rc-2rc.dtbo",
|
207
|
-
f"elxr-
|
208
|
+
f"elxr-minimal-{board}-arm64.cpio.gz",
|
208
209
|
f"simaai-image-{_flavor}-{board}.wic.gz",
|
209
210
|
f"simaai-image-{_flavor}-{board}.wic.bmap",
|
210
211
|
f"simaai-image-{_flavor}-{board}.cpio.gz"
|
@@ -329,14 +330,15 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
|
|
329
330
|
if internal and update_type == "netboot" and swtype == "elxr":
|
330
331
|
base_url = os.path.dirname(image_url)
|
331
332
|
base_version = version_or_url.split('_')[0]
|
332
|
-
extra_files = [f"elxr-palette-{board}-{base_version}-arm64.img.gz"]
|
333
|
+
extra_files = [f"../palette/elxr-palette-{board}-{base_version}-arm64.img.gz"]
|
333
334
|
|
334
335
|
for fname in extra_files:
|
335
336
|
extra_url = f"{base_url}/{fname}"
|
336
337
|
try:
|
337
338
|
click.echo(f"📥 Downloading extra file: {fname} from {extra_url} saving into {dest_path}")
|
338
339
|
netboot_file_path = download_file_from_url(extra_url, dest_path, internal=internal)
|
339
|
-
|
340
|
+
print(netboot_file_path)
|
341
|
+
extracted_files.extend([netboot_file_path])
|
340
342
|
click.echo(f"✅ Saved {fname} to {dest_path}")
|
341
343
|
except Exception as e:
|
342
344
|
click.echo(f"⚠️ Failed to download {fname}: {e}")
|
@@ -428,7 +430,7 @@ def _update_board(extracted_paths: List[str], board: str, passwd: str, flavor: s
|
|
428
430
|
return
|
429
431
|
|
430
432
|
# Optionally verify the board type
|
431
|
-
board_type, _, _, full_image = get_local_board_info()
|
433
|
+
board_type, _, _, full_image, _ = get_local_board_info()
|
432
434
|
if board_type.lower() != board.lower():
|
433
435
|
click.echo(f"❌ Board mismatch: expected '{board}', but found '{board_type}'")
|
434
436
|
return
|
@@ -465,7 +467,7 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
|
|
465
467
|
|
466
468
|
# Get remote board info
|
467
469
|
click.echo("🔍 Checking remote board type and version...")
|
468
|
-
remote_board, remote_version, _, _ = get_remote_board_info(ip, passwd)
|
470
|
+
remote_board, remote_version, _, _, _ = get_remote_board_info(ip, passwd)
|
469
471
|
|
470
472
|
if not remote_board:
|
471
473
|
click.echo("❌ Could not determine remote board type.")
|
@@ -505,6 +507,7 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
|
|
505
507
|
extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor, swtype=swtype)
|
506
508
|
return extracted_paths
|
507
509
|
|
510
|
+
|
508
511
|
def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'auto'):
|
509
512
|
r"""
|
510
513
|
Update the system based on environment and input.
|
@@ -529,19 +532,31 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
529
532
|
click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
|
530
533
|
|
531
534
|
if env_type == 'board':
|
532
|
-
board, version, devkit_name, full_image = get_local_board_info()
|
535
|
+
board, version, devkit_name, full_image, fwtype = get_local_board_info()
|
533
536
|
else:
|
534
|
-
board, version, devkit_name, full_image = get_remote_board_info(ip, passwd)
|
537
|
+
board, version, devkit_name, full_image, fwtype = get_remote_board_info(ip, passwd)
|
535
538
|
|
536
539
|
flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
|
537
540
|
|
538
541
|
if board in ['davinci', 'modalix']:
|
539
|
-
click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}")
|
542
|
+
click.echo(f"🔧 Target board: {board}, specific type: [{devkit_name}], board currently running: {version}, full_image: {full_image}, firmware: {fwtype}")
|
540
543
|
|
541
544
|
if flavor == 'full' and 'modalix' not in devkit_name:
|
542
545
|
click.echo(f"❌ You've requested updating {devkit_name} to full image, this is only supported for the Modalix DevKit")
|
543
546
|
return
|
544
547
|
|
548
|
+
if is_devkit_running_elxr():
|
549
|
+
return update_elxr(version_or_url)
|
550
|
+
elif fwtype.lower() == 'elxr':
|
551
|
+
click.echo(
|
552
|
+
click.style(
|
553
|
+
"⚠️ ELXR does not support remote update.\n"
|
554
|
+
" Please connect the DevKit to the Internet\n"
|
555
|
+
" and run: ", fg="yellow"
|
556
|
+
) + click.style("sima-cli update", fg="cyan", bold=True)
|
557
|
+
)
|
558
|
+
return False
|
559
|
+
|
545
560
|
# Davinci only supports headless build, so ignore the full flavor
|
546
561
|
if board == 'davinci' and flavor != 'headless':
|
547
562
|
click.echo(f"MLSoC only supports headless image, ignoring {flavor} setting")
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import click
|
2
|
+
import subprocess
|
3
|
+
|
4
|
+
from sima_cli.utils.config import get_auth_token
|
5
|
+
from sima_cli.auth.basic_auth import resolve_public_registry, ensure_docker_available, docker_login_with_token
|
6
|
+
|
7
|
+
def _pull_container_from_registry(registry_url: str, image_ref: str) -> str:
|
8
|
+
"""
|
9
|
+
Pulls container image from given registry and returns its local reference.
|
10
|
+
"""
|
11
|
+
if ensure_docker_available():
|
12
|
+
full_image = f"{registry_url.rstrip('/')}/{image_ref}"
|
13
|
+
subprocess.run(["docker", "pull", full_image], check=True)
|
14
|
+
return full_image
|
15
|
+
|
16
|
+
def docker_logout_from_registry(registry: str = "artifacts.eng.sima.ai"):
|
17
|
+
"""
|
18
|
+
Logout from the specified Docker registry.
|
19
|
+
Removes stored credentials (even if managed by a credential helper).
|
20
|
+
Safe to call multiple times — no error if already logged out.
|
21
|
+
"""
|
22
|
+
if ensure_docker_available():
|
23
|
+
click.echo(f"🐳 Logging out of Docker registry")
|
24
|
+
|
25
|
+
try:
|
26
|
+
proc = subprocess.run(
|
27
|
+
["docker", "logout", registry],
|
28
|
+
stdout=subprocess.PIPE,
|
29
|
+
stderr=subprocess.PIPE,
|
30
|
+
)
|
31
|
+
|
32
|
+
if proc.returncode == 0:
|
33
|
+
click.echo(proc.stdout.decode().strip() or f"✅ Logged out from {registry}")
|
34
|
+
else:
|
35
|
+
msg = proc.stderr.decode().strip() or proc.stdout.decode().strip()
|
36
|
+
if "not logged in" in msg.lower():
|
37
|
+
click.echo(f"ℹ️ Already logged out from {registry}")
|
38
|
+
else:
|
39
|
+
raise click.ClickException(f"Docker logout failed: {msg}")
|
40
|
+
|
41
|
+
except Exception as e:
|
42
|
+
raise click.ClickException(f"⚠️ Unexpected error during Docker logout: {e}")
|
43
|
+
|
44
|
+
|
45
|
+
def _select_artifactory_version(image_name: str) -> str:
|
46
|
+
"""
|
47
|
+
Query available tags for an image from SiMa Artifactory and prompt user to select one.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
image_name (str): The image name under sima-docker (e.g., 'modelsdk').
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
str: The user-selected tag (e.g., 'latest_develop').
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
click.ClickException: If no tags are found or query fails.
|
57
|
+
"""
|
58
|
+
import requests
|
59
|
+
from InquirerPy import inquirer
|
60
|
+
|
61
|
+
click.echo(f"🔍 Querying available versions for {image_name} from Artifactory...")
|
62
|
+
|
63
|
+
# Retrieve internal auth token, if available
|
64
|
+
token = get_auth_token(internal=True)
|
65
|
+
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
66
|
+
|
67
|
+
if not token:
|
68
|
+
click.echo("⚠️ No internal token found; querying Artifactory anonymously.")
|
69
|
+
|
70
|
+
tags_url = (
|
71
|
+
f"https://artifacts.eng.sima.ai/artifactory/api/docker/"
|
72
|
+
f"sima-docker/v2/{image_name}/tags/list"
|
73
|
+
)
|
74
|
+
|
75
|
+
try:
|
76
|
+
resp = requests.get(tags_url, headers=headers, timeout=10)
|
77
|
+
if resp.status_code != 200:
|
78
|
+
raise click.ClickException(
|
79
|
+
f"❌ Failed to query tags for '{image_name}': {resp.status_code} {resp.text}"
|
80
|
+
)
|
81
|
+
|
82
|
+
tags = sorted(resp.json().get("tags", []))
|
83
|
+
if not tags:
|
84
|
+
raise click.ClickException(f"❌ No tags found for image '{image_name}'")
|
85
|
+
|
86
|
+
# Interactive tag selection
|
87
|
+
return inquirer.select(
|
88
|
+
message=f"Select a version for {image_name}:",
|
89
|
+
choices=tags,
|
90
|
+
default="latest" if "latest" in tags else tags[0],
|
91
|
+
).execute()
|
92
|
+
|
93
|
+
except requests.exceptions.RequestException as e:
|
94
|
+
raise click.ClickException(f"❌ Network error while querying Artifactory: {e}")
|
95
|
+
|
96
|
+
|
97
|
+
def install_from_cr(resource_spec: str, internal: bool = False) -> str:
|
98
|
+
"""
|
99
|
+
Install a component from a container registry resource.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
resource_spec (str): Resource string in the form:
|
103
|
+
cr:<image>[:tag] or cr:<image>@<digest>
|
104
|
+
internal (bool): Whether to use SiMa internal Artifactory registry.
|
105
|
+
|
106
|
+
Examples:
|
107
|
+
install_from_cr("cr:modelsdk:latest_develop", internal=True)
|
108
|
+
install_from_cr("cr:modelsdk@sha256:abcd1234", internal=False)
|
109
|
+
"""
|
110
|
+
if not ensure_docker_available():
|
111
|
+
click.echo("⚠️ Docker not available; skipping container installation.")
|
112
|
+
return ""
|
113
|
+
|
114
|
+
resource_spec = resource_spec[3:].strip()
|
115
|
+
|
116
|
+
# Parse image and version/digest
|
117
|
+
if "@" in resource_spec:
|
118
|
+
image_name, version = resource_spec.split("@", 1)
|
119
|
+
separator = "@"
|
120
|
+
elif ":" in resource_spec:
|
121
|
+
image_name, version = resource_spec.split(":", 1)
|
122
|
+
separator = ":"
|
123
|
+
else:
|
124
|
+
image_name, version, separator = resource_spec, None, ":"
|
125
|
+
|
126
|
+
# Resolve registry, default to Artifactory, if external resolve again.
|
127
|
+
registry_url = "artifacts.eng.sima.ai/sima-docker"
|
128
|
+
|
129
|
+
if not internal:
|
130
|
+
try:
|
131
|
+
token, registry_url = resolve_public_registry("ecr")
|
132
|
+
if not token or not registry_url:
|
133
|
+
click.secho("⚠️ Failed to resolve container registry or token is missing.", fg="yellow")
|
134
|
+
return None
|
135
|
+
|
136
|
+
success = docker_login_with_token("sima_cli", token, registry_url)
|
137
|
+
if success:
|
138
|
+
crtype = 'internal' if internal else 'SiMa.ai'
|
139
|
+
click.secho(f"✅ Logged in to {crtype} container registry", fg="green")
|
140
|
+
else:
|
141
|
+
click.secho(f"❌ Docker login to container registry failed", fg="red")
|
142
|
+
|
143
|
+
except Exception as e:
|
144
|
+
click.secho(f"❌ Unexpected error during container login: {e}", fg="red")
|
145
|
+
return None
|
146
|
+
|
147
|
+
# If internal and version not specified, prompt for version
|
148
|
+
if internal and version is None:
|
149
|
+
version = _select_artifactory_version(image_name)
|
150
|
+
|
151
|
+
# Compose final ref
|
152
|
+
full_image_ref = f"{registry_url}/{image_name}{separator}{version or 'latest'}"
|
153
|
+
|
154
|
+
# Auto-login if internal and not logged in
|
155
|
+
if internal and not get_auth_token(internal=internal):
|
156
|
+
click.echo(
|
157
|
+
f"⚠️ No internal token found; please login as "
|
158
|
+
+ click.style("sima-cli -i login", fg="cyan", bold=True)
|
159
|
+
)
|
160
|
+
return
|
161
|
+
|
162
|
+
# Pull image
|
163
|
+
try:
|
164
|
+
registry_url = registry_url.replace('https://', '')
|
165
|
+
pulled_ref = _pull_container_from_registry(
|
166
|
+
registry_url, f"{image_name}{separator}{version or 'latest'}"
|
167
|
+
)
|
168
|
+
|
169
|
+
if pulled_ref:
|
170
|
+
click.echo(f"✅ Successfully pulled container: {pulled_ref}")
|
171
|
+
|
172
|
+
except subprocess.CalledProcessError as e:
|
173
|
+
raise click.ClickException(f"❌ Docker pull failed: {e}")
|
174
|
+
|
175
|
+
return full_image_ref
|
@@ -1,12 +1,12 @@
|
|
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=8KtaNfFFX0nxPr6o3A1s7VLJFBSr62xYftsognG1ItU,49
|
4
|
+
sima_cli/cli.py,sha256=hE2NJfpBo5dDH_D9qQgvbn63Xw6R0i5uWluIkXrw2yg,19946
|
5
5
|
sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
sima_cli/app_zoo/app.py,sha256=6u3iIqVZkuMK49kK0f3dVJCCf5-qC-xzLOS78-TkeN8,15738
|
7
7
|
sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
sima_cli/auth/basic_auth.py,sha256=
|
9
|
-
sima_cli/auth/login.py,sha256=
|
8
|
+
sima_cli/auth/basic_auth.py,sha256=Xvi8D1xCNEw7KGfLCXzV5QeiQi0v13056beA514ARVA,14303
|
9
|
+
sima_cli/auth/login.py,sha256=7le0had8uW-v__aEL1gl_bL9DZKJLxZmEoVI86YNmFo,2862
|
10
10
|
sima_cli/data/resources_internal.yaml,sha256=zlQD4cSnZK86bLtTWuvEudZTARKiuIKmB--Jv4ajL8o,200
|
11
11
|
sima_cli/data/resources_public.yaml,sha256=U7hmUomGeQ2ULdo1BU2OQHr0PyKBamIdK9qrutDlX8o,201
|
12
12
|
sima_cli/download/__init__.py,sha256=6y4O2FOCYFR2jdnQoVi3hRtEoZ0Gw6rydlTy1SGJ5FE,218
|
@@ -14,7 +14,7 @@ sima_cli/download/downloader.py,sha256=UQdrBWLQsPQygaoVaufOjbzWmRoNnk6pgLdnbnVi0
|
|
14
14
|
sima_cli/install/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
sima_cli/install/hostdriver.py,sha256=kAWDLebs60mbWIyTbUxmNrChcKW1uD5r7FtWNSUVUE4,5852
|
16
16
|
sima_cli/install/metadata_info.py,sha256=wmMqwzGfXbuilkqaxRVrFOzOtTOiONkmPCyA2oDAQpA,2168
|
17
|
-
sima_cli/install/metadata_installer.py,sha256
|
17
|
+
sima_cli/install/metadata_installer.py,sha256=N9QDbTIv__xhHmr33L1asaqUumaBASRr_7DnNEs3d8o,31943
|
18
18
|
sima_cli/install/metadata_validator.py,sha256=Xp51J68hMvpQeX-11kPLgz1YarQZ2m-95gGpfe3-D3Q,5644
|
19
19
|
sima_cli/install/optiview.py,sha256=r4DYdQDTUbZVCR87hl5T21gsjZrhqpU8hWnYxKmUJ_k,4790
|
20
20
|
sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3930
|
@@ -32,21 +32,23 @@ sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,6
|
|
32
32
|
sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
|
33
33
|
sima_cli/update/bootimg.py,sha256=OA_GyZwI8dbU3kaucKmoxAZbnnSnjXeOkU6yuDPji1k,13632
|
34
34
|
sima_cli/update/cleanlog.py,sha256=-V6eDl3MdsvDmCfkKUJTqkXJ_WnLJE01uxS7z96b15g,909
|
35
|
-
sima_cli/update/
|
36
|
-
sima_cli/update/
|
37
|
-
sima_cli/update/
|
38
|
-
sima_cli/update/
|
39
|
-
sima_cli/update/
|
35
|
+
sima_cli/update/elxr.py,sha256=3mMKeu9AWi3zGY8q2bkJFQOVyacSrPXdKBJFre_BLcE,2776
|
36
|
+
sima_cli/update/local.py,sha256=8HK5j-5HEOC0nPWKkcPPnAxB3Ai8QpH3hGyucN0izXY,5698
|
37
|
+
sima_cli/update/netboot.py,sha256=gHu3Ts23Vlbvcn9dZ2u0KckP8y4WSmlzZCYWBiRsrmQ,20447
|
38
|
+
sima_cli/update/query.py,sha256=aqBeVYOTmy3gBVaxfK2c5f6_DH8_y32ZEB1INiJbOE8,5034
|
39
|
+
sima_cli/update/remote.py,sha256=kAIJEuaH-ULV_RQwbeNgbA8pFKupunVcw1sAYJQt7z4,15946
|
40
|
+
sima_cli/update/updater.py,sha256=Xhmpo41USPZMok8KwTEvYU6BIWv49kYC3zs_ls1LRrg,26325
|
40
41
|
sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
42
|
sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
|
42
43
|
sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
|
43
44
|
sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
|
45
|
+
sima_cli/utils/container_registries.py,sha256=KIvU5bbYBZlD-XMaGKQznGHMbzaBKtBmapfB52AmS7M,6461
|
44
46
|
sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
|
45
47
|
sima_cli/utils/env.py,sha256=0qY1-PJiI1G3uDVv774aimPXhHQBtY56FqXSMeMQTps,10401
|
46
48
|
sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
|
47
49
|
sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
|
48
50
|
sima_cli/utils/pkg_update_check.py,sha256=GD0XVS2_l2WpuzL81TKkEQIDHXXffJeF3LELefncHNM,3467
|
49
|
-
sima_cli-0.0.
|
51
|
+
sima_cli-0.0.41.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
|
50
52
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
53
|
tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
54
|
tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -55,8 +57,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
|
|
55
57
|
tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
58
|
tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
59
|
tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
|
-
sima_cli-0.0.
|
59
|
-
sima_cli-0.0.
|
60
|
-
sima_cli-0.0.
|
61
|
-
sima_cli-0.0.
|
62
|
-
sima_cli-0.0.
|
60
|
+
sima_cli-0.0.41.dist-info/METADATA,sha256=-RebJI2aFDR32sgg_LwSLajM9auXryr2XpCXV09lEaE,3705
|
61
|
+
sima_cli-0.0.41.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
62
|
+
sima_cli-0.0.41.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
|
63
|
+
sima_cli-0.0.41.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
|
64
|
+
sima_cli-0.0.41.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|