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.
@@ -1,9 +1,295 @@
1
1
  import click
2
2
  import os
3
+ import re
4
+ import time
5
+ import tempfile
6
+ import tarfile
7
+ import subprocess
8
+ from urllib.parse import urlparse
9
+ from typing import List
3
10
  from sima_cli.utils.env import get_environment_type
4
11
  from sima_cli.download import download_file_from_url
12
+ from sima_cli.utils.config_loader import load_resource_config
13
+ from sima_cli.update.remote import push_and_update_remote_board, get_remote_board_info, reboot_remote_board
14
+ from sima_cli.update.local import get_local_board_info, push_and_update_local_board
5
15
 
6
- def perform_update(version_or_url: str, ip: str = None):
16
+ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = False) -> str:
17
+ """
18
+ Resolve the final firmware download URL based on board, version, and environment.
19
+
20
+ Args:
21
+ version_or_url (str): Either a version string (e.g. 1.6.0_master_B1611) or a full URL.
22
+ board (str): Board type ('davinci' or 'modalix').
23
+ internal (bool): Whether to use internal config for URL construction.
24
+
25
+ Returns:
26
+ str: Full download URL.
27
+ """
28
+ # If it's already a full URL, return it as-is
29
+ if re.match(r'^https?://', version_or_url):
30
+ return version_or_url
31
+
32
+ # Load internal or public config
33
+ cfg = load_resource_config()
34
+
35
+ repo_cfg = cfg.get("internal" if internal else "public", {}).get("download")
36
+ artifactory_cfg = cfg.get("internal" if internal else "public", {}).get("artifactory")
37
+ base_url = artifactory_cfg.get("url", {})
38
+
39
+ url = f"{base_url}/{repo_cfg.get("download_url")}"
40
+ if not url:
41
+ raise RuntimeError("⚠️ 'url' is not defined in resource config.")
42
+
43
+ # Format full download path, internal for now.
44
+ download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/release.tar.gz"
45
+ return download_url
46
+
47
+ def _sanitize_url_to_filename(url: str) -> str:
48
+ """
49
+ Convert a URL to a safe filename by replacing slashes and removing protocol.
50
+
51
+ Args:
52
+ url (str): Original URL.
53
+
54
+ Returns:
55
+ str: Safe, descriptive filename (e.g., soc-images__modalix__1.6.0__release.tar.gz)
56
+ """
57
+ parsed = urlparse(url)
58
+ path = parsed.netloc + parsed.path
59
+ safe_name = re.sub(r'[^\w.-]', '__', path)
60
+ return safe_name
61
+
62
+
63
+ def _extract_required_files(tar_path: str, board: str) -> list:
64
+ """
65
+ Extract required files from a .tar.gz or .tar archive into the same folder
66
+ and return the full paths to the extracted files (with subfolder if present).
67
+ Skips files that already exist.
68
+
69
+ Args:
70
+ tar_path (str): Path to the downloaded or provided firmware archive.
71
+ board (str): Board type ('davinci' or 'modalix').
72
+
73
+ Returns:
74
+ list: List of full paths to extracted files.
75
+ """
76
+ extract_dir = os.path.dirname(tar_path)
77
+
78
+ # Define required filenames (not full paths)
79
+ target_filenames = {
80
+ "troot-upgrade-simaai-ev.swu",
81
+ f"simaai-image-palette-upgrade-{board}.swu"
82
+ }
83
+
84
+ env_type, _os = get_environment_type()
85
+ if env_type == "host" and _os == "linux":
86
+ target_filenames.add("sima_pcie_host_pkg.sh")
87
+
88
+ extracted_paths = []
89
+
90
+ try:
91
+ try:
92
+ tar = tarfile.open(tar_path, mode="r:gz")
93
+ except tarfile.ReadError:
94
+ tar = tarfile.open(tar_path, mode="r:")
95
+
96
+ with tar:
97
+ for member in tar.getmembers():
98
+ base_name = os.path.basename(member.name)
99
+ if base_name in target_filenames:
100
+ full_dest_path = os.path.join(extract_dir, member.name)
101
+
102
+ if os.path.exists(full_dest_path):
103
+ click.echo(f"⚠️ Skipping existing file: {full_dest_path}")
104
+ extracted_paths.append(full_dest_path)
105
+ continue
106
+
107
+ # Ensure directory structure exists
108
+ os.makedirs(os.path.dirname(full_dest_path), exist_ok=True)
109
+
110
+ tar.extract(member, path=extract_dir)
111
+ extracted_paths.append(full_dest_path)
112
+ click.echo(f"✅ Extracted: {full_dest_path}")
113
+
114
+ if not extracted_paths:
115
+ click.echo("⚠️ No matching files were found or extracted.")
116
+
117
+ return extracted_paths
118
+
119
+ except Exception as e:
120
+ click.echo(f"❌ Failed to extract files from archive: {e}")
121
+ return []
122
+
123
+ def _download_image(version_or_url: str, board: str, internal: bool = False):
124
+ """
125
+ Download or use a firmware image for the specified board and version or file path.
126
+
127
+ Args:
128
+ version_or_url (str): Version string, HTTP(S) URL, or local file path.
129
+ board (str): Target board type ('davinci' or 'modalix').
130
+ internal (bool): Whether to use internal Artifactory resources.
131
+
132
+ Notes:
133
+ - If a local file is provided, it skips downloading.
134
+ - Downloads the firmware into the system's temporary directory otherwise.
135
+ - Target file name is uniquely derived from the URL or preserved from local path.
136
+ """
137
+ try:
138
+ # Case 1: Local file provided
139
+ if os.path.exists(version_or_url) and os.path.isfile(version_or_url):
140
+ click.echo(f"📁 Using local firmware file: {version_or_url}")
141
+ return _extract_required_files(version_or_url, board)
142
+
143
+ # Case 2: Treat as custom full URL
144
+ if version_or_url.startswith("http://") or version_or_url.startswith("https://"):
145
+ image_url = version_or_url
146
+ else:
147
+ # Case 3: Resolve standard version string (Artifactory/AWS)
148
+ image_url = _resolve_firmware_url(version_or_url, board, internal)
149
+
150
+ # Determine platform-safe temp directory
151
+ temp_dir = tempfile.gettempdir()
152
+ os.makedirs(temp_dir, exist_ok=True)
153
+
154
+ # Build safe filename based on the URL
155
+ safe_filename = _sanitize_url_to_filename(image_url)
156
+ dest_path = os.path.join(temp_dir, safe_filename)
157
+
158
+ # Download the file
159
+ click.echo(f"📦 Downloading from {image_url}")
160
+ firmware_path = download_file_from_url(image_url, dest_path, internal=internal)
161
+
162
+ click.echo(f"📦 Firmware downloaded to: {firmware_path}")
163
+ return _extract_required_files(firmware_path, board)
164
+
165
+ except Exception as e:
166
+ click.echo(f"❌ Host update failed: {e}")
167
+
168
+ def _update_host(script_path: str, board: str, boardip: str, passwd: str):
169
+ """
170
+ Perform PCIe host update by running the sima_pcie_host_pkg.sh script.
171
+
172
+ Args:
173
+ script_path (str): Full path of the extracted host package script
174
+ board (str): Board type (e.g., 'davinci' or 'modalix').
175
+ """
176
+ try:
177
+ if not script_path or not os.path.isfile(script_path):
178
+ click.echo("❌ sima_pcie_host_pkg.sh not found in extracted files.")
179
+ return
180
+
181
+ click.echo(f"🚀 Running PCIe host install script: {script_path}")
182
+
183
+ # Start subprocess with live output streaming
184
+ process = subprocess.Popen(
185
+ ["sudo", "bash", script_path],
186
+ stdout=subprocess.PIPE,
187
+ stderr=subprocess.STDOUT,
188
+ text=True,
189
+ bufsize=1
190
+ )
191
+
192
+ # Stream output line by line
193
+ for line in process.stdout:
194
+ click.echo(f"📄 {line.strip()}")
195
+
196
+ process.stdout.close()
197
+ returncode = process.wait()
198
+
199
+ if returncode != 0:
200
+ click.echo(f"❌ Host driver install script exited with code {returncode}.")
201
+ return
202
+
203
+ click.echo("✅ PCIe host update completed successfully.")
204
+
205
+ # Ask for reboot
206
+ if click.confirm("🔄 Do you want to reboot your system now?", default=True):
207
+ click.echo("♻️ Rebooting system...")
208
+ # This workaround reboots the PCIe card before we reboot the system
209
+ reboot_remote_board(boardip, passwd)
210
+ time.sleep(2)
211
+ subprocess.run(["sudo", "reboot"])
212
+ else:
213
+ click.echo("🕒 Reboot skipped. Please powercycle to apply changes.")
214
+
215
+ except Exception as e:
216
+ click.echo(f"❌ Host update failed: {e}")
217
+
218
+
219
+ def _update_sdk(version_or_url: str, board: str):
220
+ click.echo(f"⚙️ Simulated SDK firmware update logic for board '{board}' (not implemented).")
221
+ # TODO: Implement update via SDK-based communication or tools
222
+
223
+ def _update_board(extracted_paths: List[str], board: str, passwd: str):
224
+ """
225
+ Perform local firmware update using extracted files.
226
+
227
+ Args:
228
+ extracted_paths (List[str]): Paths to the extracted .swu files.
229
+ board (str): Board type expected (e.g. 'davinci', 'modalix').
230
+ """
231
+ click.echo(f"⚙️ Starting local firmware update for board '{board}'...")
232
+
233
+ # Locate the needed files
234
+ troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
235
+ palette_path = next((p for p in extracted_paths if f"palette-upgrade-{board}" in os.path.basename(p)), None)
236
+
237
+ if not troot_path or not palette_path:
238
+ click.echo("❌ Required firmware files not found in extracted paths.")
239
+ return
240
+
241
+ # Optionally verify the board type
242
+ board_type, _ = get_local_board_info()
243
+ if board_type.lower() != board.lower():
244
+ click.echo(f"❌ Board mismatch: expected '{board}', but found '{board_type}'")
245
+ return
246
+
247
+ click.echo("✅ Board verified. Starting update...")
248
+ push_and_update_local_board(troot_path, palette_path, passwd)
249
+
250
+ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str, reboot_and_wait: bool = True):
251
+ """
252
+ Perform remote firmware update to the specified board via SSH.
253
+
254
+ Args:
255
+ extracted_paths (List[str]): Paths to the extracted .swu files.
256
+ ip (str): IP of the remote board.
257
+ board (str): Expected board type ('davinci' or 'modalix').
258
+ passwd (str): password to access the board, if it's not default
259
+ """
260
+ click.echo(f"⚙️ Starting remote update on '{ip}' for board type '{board}'...")
261
+
262
+ # Locate files
263
+ troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
264
+ palette_path = next((p for p in extracted_paths if f"palette-upgrade-{board}" in os.path.basename(p)), None)
265
+ script_path = next((p for p in extracted_paths if p.endswith("sima_pcie_host_pkg.sh")), None)
266
+
267
+ if not troot_path or not palette_path:
268
+ click.echo("❌ Required firmware files not found in extracted paths.")
269
+ return
270
+
271
+ # Get remote board info
272
+ click.echo("🔍 Checking remote board type and version...")
273
+ remote_board, remote_version = get_remote_board_info(ip, passwd)
274
+
275
+ if not remote_board:
276
+ click.echo("❌ Could not determine remote board type.")
277
+ return
278
+
279
+ click.echo(f"🔍 Remote board: {remote_board} | Version: {remote_version}")
280
+
281
+ if remote_board.lower() != board.lower():
282
+ click.echo(f"❌ Board mismatch: expected '{board}', but got '{remote_board}' on device.")
283
+ return
284
+
285
+ # Proceed with update
286
+ click.echo("✅ Board type verified. Proceeding with firmware update...")
287
+ push_and_update_remote_board(ip, troot_path, palette_path, passwd=passwd, reboot_and_wait=reboot_and_wait)
288
+
289
+ return script_path
290
+
291
+
292
+ def perform_update(version_or_url: str, ip: str = None, board: str = "davinci", internal: bool = False, passwd: str = "edgeai"):
7
293
  r"""
8
294
  Update the system based on environment and input.
9
295
 
@@ -11,45 +297,43 @@ def perform_update(version_or_url: str, ip: str = None):
11
297
  - On SiMa board: applies firmware update.
12
298
  - In SDK: allows simulated or direct board update.
13
299
  - Unknown env: requires --ip to specify remote device.
300
+
301
+ Args:
302
+ version_or_url (str): Version string or direct URL.
303
+ ip (str): Optional remote target IP.
304
+ board (str): Board type, must be 'davinci' or 'modalix'.
305
+ passwd : non-default password in case user has changed the password of the board user `sima`
14
306
  """
15
- env_type, env_subtype = get_environment_type()
16
- click.echo(f"🔄 Running update for environment: {env_type} ({env_subtype})")
17
- click.echo(f"Requested version or URL: {version_or_url}")
18
-
19
- if env_type == "host" and env_subtype == 'linux':
20
- click.echo("👉 Updating PCIe host driver and downloading firmware...")
21
- _update_host(version_or_url)
22
- elif env_type == "board":
23
- click.echo("👉 Updating firmware on SiMa board...")
24
- _update_board(version_or_url)
25
- elif env_type == "sdk":
26
- click.echo("👉 Updating firmware from within the Palette SDK...")
27
- _update_sdk(version_or_url)
28
- elif ip:
29
- click.echo(f"👉 Updating firmware on remote board at {ip}...")
30
- _update_remote(ip, version_or_url)
31
- else:
32
- click.echo("❌ Unknown environment. Use --ip to specify target device.")
33
-
34
- def _update_host(version_or_url: str):
35
- # Simulate firmware download
36
- try:
37
- dest_dir = "/tmp/sima-firmware"
38
- os.makedirs(dest_dir, exist_ok=True)
39
- firmware_path = download_file_from_url(version_or_url, dest_dir)
40
- click.echo(f"📦 Firmware downloaded to: {firmware_path}")
41
- click.echo("⚙️ Simulated PCIe host driver reload (not implemented).")
42
- except Exception as e:
43
- click.echo(f"❌ Host update failed: {e}")
307
+ board = board.lower()
308
+ if board not in ("davinci", "modalix"):
309
+ click.echo(f" Invalid board type '{board}'. Must be 'davinci' or 'modalix'.")
310
+ return
44
311
 
45
- def _update_board(version_or_url: str):
46
- click.echo("⚙️ Simulated board firmware update (not implemented).")
47
- # TODO: Validate path, unpack firmware, flash/update on device
312
+ try:
313
+ env_type, env_subtype = get_environment_type()
314
+ click.echo(f"🔄 Running update for environment: {env_type} ({env_subtype})")
315
+ click.echo(f"🔧 Requested version or URL: {version_or_url}")
316
+ click.echo(f"🔧 Target board: {board}")
48
317
 
49
- def _update_sdk(version_or_url: str):
50
- click.echo("⚙️ Simulated SDK firmware update logic (not implemented).")
51
- # TODO: Implement update via SDK-based communication or tools
318
+ extracted_paths = _download_image(version_or_url, board, internal)
319
+ click.echo("⚠️ DO NOT INTERRUPT THE UPDATE PROCESS...")
320
+
321
+ if len(extracted_paths) > 0:
322
+ if env_type == "host" and env_subtype == 'linux':
323
+ # Always update the remote device first then update the host driver, otherwise the host would
324
+ # not be able to connect to the board
325
+ click.echo("👉 Updating PCIe host driver and downloading firmware...")
326
+ script_path = _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait = False)
327
+ _update_host(script_path, board, ip, passwd)
328
+ elif env_type == "board":
329
+ _update_board(extracted_paths, board, passwd)
330
+ elif env_type == "sdk":
331
+ click.echo("👉 Updating firmware from within the Palette SDK...: Not implemented yet")
332
+ elif ip:
333
+ click.echo(f"👉 Updating firmware on remote board at {ip}...")
334
+ _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait = True)
335
+ else:
336
+ click.echo("❌ Unknown environment. Use --ip to specify target device.")
337
+ except Exception as e:
338
+ click.echo(f"❌ Update failed {e}")
52
339
 
53
- def _update_remote(ip: str, version_or_url: str):
54
- click.echo("⚙️ Simulated remote update via SSH/SCP (not implemented).")
55
- # TODO: Validate network reachability, copy and trigger update on remote
@@ -0,0 +1,63 @@
1
+ import requests
2
+ from typing import Optional, Tuple
3
+
4
+ def exchange_identity_token(
5
+ identity_token: str,
6
+ exchange_url: str,
7
+ expires_in: int = 604800,
8
+ scope: Optional[str] = None
9
+ ) -> Tuple[Optional[str], Optional[str]]:
10
+ """
11
+ Exchange an identity token for a short-lived access token.
12
+
13
+ Args:
14
+ identity_token (str): Long-lived identity token.
15
+ exchange_url (str): Artifactory /api/security/token endpoint.
16
+ expires_in (int): Access token lifetime in seconds (default: 7 days).
17
+ scope (Optional[str]): Optional scope string (e.g. 'member-of-groups:readers').
18
+
19
+ Returns:
20
+ Tuple: (access_token, username) if successful, or (None, None) on failure.
21
+ """
22
+ headers = {
23
+ "Content-Type": "application/x-www-form-urlencoded",
24
+ "Authorization": f"Bearer {identity_token}" # Assuming token is passed in header
25
+ }
26
+ data = {
27
+ "grant_type": "client_credentials", # Adjust based on Artifactory's requirements
28
+ "expires_in": str(expires_in)
29
+ }
30
+ if scope:
31
+ data["scope"] = scope
32
+
33
+ try:
34
+ response = requests.post(exchange_url, headers=headers, data=data)
35
+ response.raise_for_status()
36
+ result = response.json()
37
+ access_token = result.get("access_token")
38
+ username = result.get("username") or result.get("sub")
39
+ return access_token, username
40
+ except requests.RequestException as e:
41
+ logging.error(f"Token exchange failed: {e}")
42
+ return None, None
43
+
44
+ def validate_token(token: str, validate_url: str) -> Tuple[bool, Optional[str]]:
45
+ """
46
+ Validate a token by calling a lightweight Artifactory-protected endpoint.
47
+
48
+ Args:
49
+ token (str): Access token to validate.
50
+ validate_url (str): Endpoint such as /api/security/users/$self.
51
+
52
+ Returns:
53
+ Tuple: (True, username) if valid, or (False, None) if invalid or unauthorized.
54
+ """
55
+ headers = {"Authorization": f"Bearer {token}"}
56
+ try:
57
+ response = requests.get(validate_url, headers=headers)
58
+ response.raise_for_status()
59
+ data = response.json() if "application/json" in response.headers.get("Content-Type", "") else {}
60
+ return True, data.get("name") # Assumes /api/security/users/$self
61
+ except requests.RequestException as e:
62
+ logging.error(f"Token validation failed: {e}")
63
+ return False, None
sima_cli/utils/config.py CHANGED
@@ -1,52 +1,58 @@
1
1
  import os
2
2
  import json
3
3
 
4
- # Construct a cross-platform compatible config path
4
+ # Path to the local CLI config file storing tokens and preferences
5
5
  CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".sima-cli", "config.json")
6
6
 
7
7
  def load_config():
8
8
  """
9
- Load the CLI configuration from disk.
9
+ Load the configuration file from disk.
10
10
 
11
11
  Returns:
12
- dict: Parsed configuration data, or an empty dict if no config exists.
12
+ dict: Parsed JSON config, or empty dict if file doesn't exist.
13
13
  """
14
14
  if not os.path.exists(CONFIG_PATH):
15
15
  return {}
16
- try:
17
- with open(CONFIG_PATH, "r") as f:
18
- return json.load(f)
19
- except Exception:
20
- return {}
16
+ with open(CONFIG_PATH, "r") as f:
17
+ return json.load(f)
21
18
 
22
19
  def save_config(data):
23
20
  """
24
- Save a dictionary of configuration data to disk.
21
+ Save the given dictionary to the config file in JSON format.
25
22
 
26
23
  Args:
27
- data (dict): The configuration data to store.
24
+ data (dict): The config data to write to disk.
28
25
  """
29
26
  os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
30
27
  with open(CONFIG_PATH, "w") as f:
31
28
  json.dump(data, f, indent=2)
32
29
 
33
- def get_auth_token():
30
+ def get_auth_token(internal=False):
34
31
  """
35
- Retrieve the current authentication token from the config.
32
+ Retrieve the saved identity token for the given environment.
33
+
34
+ Args:
35
+ internal (bool): Whether to return the internal or external token.
36
36
 
37
37
  Returns:
38
- str or None: The stored auth token, or None if not set.
38
+ str or None: The saved token, or None if not set.
39
39
  """
40
- config = load_config()
41
- return config.get("auth_token")
40
+ key = "internal" if internal else "external"
41
+ return load_config().get(key, {}).get("auth_token")
42
42
 
43
- def set_auth_token(token):
43
+ def set_auth_token(token, internal=False):
44
44
  """
45
- Store a new authentication token in the config.
45
+ Save the given identity token under the internal or external section.
46
46
 
47
47
  Args:
48
- token (str): The auth token to save.
48
+ token (str): The access token to store.
49
+ internal (bool): Whether to store it in the 'internal' or 'external' section.
49
50
  """
50
51
  config = load_config()
51
- config["auth_token"] = token
52
+ key = "internal" if internal else "external"
53
+
54
+ if key not in config:
55
+ config[key] = {}
56
+
57
+ config[key]["auth_token"] = token
52
58
  save_config(config)
@@ -0,0 +1,30 @@
1
+ import os
2
+ import yaml
3
+
4
+ def load_resource_config():
5
+ """
6
+ Load and separate public and internal resource configuration files.
7
+
8
+ Returns:
9
+ dict: Dictionary with keys 'public' and 'internal' for both configs.
10
+ """
11
+ config = {
12
+ "public": {},
13
+ "internal": {}
14
+ }
15
+
16
+ # Public config (bundled with the package)
17
+ public_path = os.path.abspath(
18
+ os.path.join(os.path.dirname(__file__), "..", "data", "resources_public.yaml")
19
+ )
20
+ if os.path.exists(public_path):
21
+ with open(public_path, "r") as f:
22
+ config["public"] = yaml.safe_load(f) or {}
23
+
24
+ # Internal config (~/.sima-cli/resources_internal.yaml)
25
+ internal_path = os.path.join(os.path.expanduser("~"), ".sima-cli", "resources_internal.yaml")
26
+ if os.path.exists(internal_path):
27
+ with open(internal_path, "r") as f:
28
+ config["internal"] = yaml.safe_load(f) or {}
29
+
30
+ return config