sima-cli 0.0.20__py3-none-any.whl → 0.0.22__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/cli.py +109 -15
- sima_cli/download/downloader.py +7 -3
- sima_cli/install/metadata_info.py +57 -0
- sima_cli/install/metadata_installer.py +447 -0
- sima_cli/install/metadata_validator.py +138 -0
- sima_cli/network/network.py +208 -0
- sima_cli/nvme/nvme.py +123 -0
- sima_cli/update/bootimg.py +5 -4
- sima_cli/update/local.py +10 -8
- sima_cli/update/netboot.py +4 -3
- sima_cli/update/query.py +7 -5
- sima_cli/update/remote.py +46 -25
- sima_cli/update/updater.py +71 -40
- sima_cli/utils/disk.py +15 -0
- sima_cli/utils/env.py +46 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/METADATA +2 -1
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/RECORD +22 -16
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
from InquirerPy import inquirer
|
2
|
+
import subprocess
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import time
|
6
|
+
from sima_cli.utils.env import is_sima_board
|
7
|
+
|
8
|
+
IP_CMD = "/sbin/ip"
|
9
|
+
|
10
|
+
def extract_interface_index(name):
|
11
|
+
"""Extract numeric index from interface name for sorting (e.g., end0 → 0)."""
|
12
|
+
match = re.search(r'(\d+)$', name)
|
13
|
+
return int(match.group(1)) if match else float('inf')
|
14
|
+
|
15
|
+
def get_interfaces():
|
16
|
+
interfaces = []
|
17
|
+
ip_output = subprocess.check_output([IP_CMD, '-o', 'link', 'show']).decode()
|
18
|
+
for line in ip_output.splitlines():
|
19
|
+
match = re.match(r'\d+: (\w+):', line)
|
20
|
+
if match:
|
21
|
+
iface = match.group(1)
|
22
|
+
if iface.startswith('lo'):
|
23
|
+
continue
|
24
|
+
try:
|
25
|
+
with open(f"/sys/class/net/{iface}/carrier") as f:
|
26
|
+
carrier = f.read().strip() == "1"
|
27
|
+
except FileNotFoundError:
|
28
|
+
carrier = False
|
29
|
+
|
30
|
+
try:
|
31
|
+
ip_addr = subprocess.check_output([IP_CMD, '-4', 'addr', 'show', iface]).decode()
|
32
|
+
ip_match = re.search(r'inet (\d+\.\d+\.\d+\.\d+)', ip_addr)
|
33
|
+
ip = ip_match.group(1) if ip_match else "IP Not Assigned"
|
34
|
+
except subprocess.CalledProcessError:
|
35
|
+
ip = "IP Not Assigned"
|
36
|
+
|
37
|
+
# Check internet connectivity only if carrier is up
|
38
|
+
internet = False
|
39
|
+
if carrier:
|
40
|
+
try:
|
41
|
+
result = subprocess.run(
|
42
|
+
["ping", "-I", iface, "-c", "1", "-W", "1", "8.8.8.8"],
|
43
|
+
stdout=subprocess.DEVNULL,
|
44
|
+
stderr=subprocess.DEVNULL
|
45
|
+
)
|
46
|
+
internet = result.returncode == 0
|
47
|
+
except Exception:
|
48
|
+
internet = False
|
49
|
+
|
50
|
+
interfaces.append({
|
51
|
+
"name": iface,
|
52
|
+
"carrier": carrier,
|
53
|
+
"ip": ip,
|
54
|
+
"internet": internet
|
55
|
+
})
|
56
|
+
|
57
|
+
interfaces.sort(key=lambda x: extract_interface_index(x["name"]))
|
58
|
+
return interfaces
|
59
|
+
|
60
|
+
def move_network_file(iface, mode):
|
61
|
+
try:
|
62
|
+
networkd_dir = "/etc/systemd/network"
|
63
|
+
files = os.listdir(networkd_dir)
|
64
|
+
|
65
|
+
# Match any static file for this iface
|
66
|
+
pattern = re.compile(r"(\d+)-(%s)-static\.network" % re.escape(iface))
|
67
|
+
static_file = next((f for f in files if pattern.match(f)), None)
|
68
|
+
if not static_file:
|
69
|
+
print(f"⚠️ No static .network file found for {iface}")
|
70
|
+
return
|
71
|
+
|
72
|
+
src = os.path.join(networkd_dir, static_file)
|
73
|
+
desired_prefix = "02" if mode == "static" else "20"
|
74
|
+
dst_file = f"{desired_prefix}-{iface}-static.network"
|
75
|
+
dst = os.path.join(networkd_dir, dst_file)
|
76
|
+
|
77
|
+
if static_file == dst_file:
|
78
|
+
print(f"✅ Interface {iface} is already set to {mode.upper()}. No changes made.")
|
79
|
+
else:
|
80
|
+
print(f"🔧 Changing mode of {iface} to {mode.upper()}...")
|
81
|
+
subprocess.run(["sudo", "mv", src, dst], check=True)
|
82
|
+
|
83
|
+
# Modify content only if going to static
|
84
|
+
if mode == "static":
|
85
|
+
# Read as normal user
|
86
|
+
with open(dst, "r") as f:
|
87
|
+
lines = f.readlines()
|
88
|
+
cleaned = [line for line in lines if "KernelCommandLine=!netcfg=dhcp" not in line]
|
89
|
+
|
90
|
+
# Only write if change is needed
|
91
|
+
if len(cleaned) != len(lines):
|
92
|
+
temp_path = f"/tmp/{iface}-static.network"
|
93
|
+
with open(temp_path, "w") as tmpf:
|
94
|
+
tmpf.writelines(cleaned)
|
95
|
+
subprocess.run(["sudo", "cp", temp_path, dst], check=True)
|
96
|
+
os.remove(temp_path)
|
97
|
+
print(f"✂️ Removed KernelCommandLine override from {dst_file}")
|
98
|
+
else:
|
99
|
+
print(f"✅ No KernelCommandLine override found — file already clean.")
|
100
|
+
|
101
|
+
# Restart networkd
|
102
|
+
subprocess.run(["sudo", "systemctl", "restart", "systemd-networkd"])
|
103
|
+
time.sleep(2)
|
104
|
+
except Exception as e:
|
105
|
+
print(f"❌ Unable to change configuration, error: {e}")
|
106
|
+
|
107
|
+
def get_gateway_for_interface(ip):
|
108
|
+
"""Guess the gateway from the IP address, assuming .1 is the router."""
|
109
|
+
if ip == "IP Not Assigned":
|
110
|
+
return None
|
111
|
+
parts = ip.split('.')
|
112
|
+
parts[-1] = "1"
|
113
|
+
return ".".join(parts)
|
114
|
+
|
115
|
+
def populate_resolv_conf(dns_server="8.8.8.8"):
|
116
|
+
"""
|
117
|
+
Use sudo to write a DNS entry into /etc/resolv.conf even if not running as root.
|
118
|
+
"""
|
119
|
+
content = f"nameserver {dns_server}\n"
|
120
|
+
|
121
|
+
try:
|
122
|
+
# Write using echo and sudo tee
|
123
|
+
cmd = f"echo '{content.strip()}' | sudo tee /etc/resolv.conf > /dev/null"
|
124
|
+
result = subprocess.run(cmd, shell=True, check=True)
|
125
|
+
print(f"✅ /etc/resolv.conf updated with nameserver {dns_server}")
|
126
|
+
except subprocess.CalledProcessError as e:
|
127
|
+
print(f"❌ Failed to update /etc/resolv.conf: {e}")
|
128
|
+
|
129
|
+
def set_default_route(iface, ip):
|
130
|
+
gateway = get_gateway_for_interface(ip)
|
131
|
+
if not gateway:
|
132
|
+
print(f"❌ Cannot set default route — IP not assigned for {iface}")
|
133
|
+
return
|
134
|
+
|
135
|
+
print(f"🔧 Setting default route via {iface} ({gateway})")
|
136
|
+
|
137
|
+
try:
|
138
|
+
# Delete all existing default routes
|
139
|
+
subprocess.run(["sudo", "/sbin/ip", "route", "del", "default"], check=False)
|
140
|
+
|
141
|
+
# Add new default route for this iface
|
142
|
+
subprocess.run(
|
143
|
+
["sudo", "/sbin/ip", "route", "add", "default", "via", gateway, "dev", iface],
|
144
|
+
check=True
|
145
|
+
)
|
146
|
+
print(f"✅ Default route set via {iface} ({gateway})")
|
147
|
+
except subprocess.CalledProcessError:
|
148
|
+
print(f"❌ Failed to set default route via {iface}")
|
149
|
+
|
150
|
+
def network_menu():
|
151
|
+
if not is_sima_board():
|
152
|
+
print("❌ This command only runs on the DevKit")
|
153
|
+
return
|
154
|
+
|
155
|
+
while True:
|
156
|
+
interfaces = get_interfaces()
|
157
|
+
choices = ["🚪 Quit Menu"]
|
158
|
+
iface_map = {}
|
159
|
+
|
160
|
+
for iface in interfaces:
|
161
|
+
status_icon = "carrier (✅)" if iface["carrier"] else "carrier (❌)"
|
162
|
+
internet_icon = "internet (🌐)" if iface.get("internet") else "internet (🚫)"
|
163
|
+
label = f"{iface['name']:<10} {status_icon} {internet_icon} {iface['ip']:<20}"
|
164
|
+
choices.append(label)
|
165
|
+
iface_map[label] = iface
|
166
|
+
|
167
|
+
try:
|
168
|
+
iface_choice = inquirer.fuzzy(
|
169
|
+
message="Select Ethernet Interface:",
|
170
|
+
choices=choices,
|
171
|
+
instruction="(Type or use ↑↓)",
|
172
|
+
).execute()
|
173
|
+
except KeyboardInterrupt:
|
174
|
+
print("\nExiting.")
|
175
|
+
break
|
176
|
+
|
177
|
+
if iface_choice is None or iface_choice == "🚪 Quit Menu":
|
178
|
+
print("Exiting.")
|
179
|
+
break
|
180
|
+
|
181
|
+
selected_iface = iface_map[iface_choice]
|
182
|
+
|
183
|
+
try:
|
184
|
+
second = inquirer.select(
|
185
|
+
message=f"Configure {selected_iface['name']}:",
|
186
|
+
choices=[
|
187
|
+
"Set to DHCP",
|
188
|
+
"Set to Default Static IP",
|
189
|
+
"Set as Default Route",
|
190
|
+
"Back to Interface Selection"
|
191
|
+
]
|
192
|
+
).execute()
|
193
|
+
except KeyboardInterrupt:
|
194
|
+
print("\nExiting.")
|
195
|
+
break
|
196
|
+
|
197
|
+
if second == "Set to DHCP":
|
198
|
+
move_network_file(selected_iface["name"], "dhcp")
|
199
|
+
populate_resolv_conf()
|
200
|
+
elif second == "Set to Default Static IP":
|
201
|
+
move_network_file(selected_iface["name"], "static")
|
202
|
+
elif second == "Set as Default Route":
|
203
|
+
set_default_route(selected_iface["name"], selected_iface["ip"])
|
204
|
+
else:
|
205
|
+
continue
|
206
|
+
|
207
|
+
if __name__ == '__main__':
|
208
|
+
network_menu()
|
sima_cli/nvme/nvme.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
import subprocess
|
2
|
+
import click
|
3
|
+
from sima_cli.utils.env import is_modalix_devkit
|
4
|
+
|
5
|
+
def scan_nvme():
|
6
|
+
try:
|
7
|
+
nvme_list = subprocess.check_output("sudo nvme list", shell=True, text=True).strip()
|
8
|
+
if "/dev/nvme0n1" in nvme_list:
|
9
|
+
return nvme_list
|
10
|
+
except subprocess.CalledProcessError:
|
11
|
+
pass
|
12
|
+
return None
|
13
|
+
|
14
|
+
def get_lba_format_index():
|
15
|
+
try:
|
16
|
+
lba_output = subprocess.check_output("sudo nvme id-ns -H /dev/nvme0n1 | grep 'Relative Performance'", shell=True, text=True)
|
17
|
+
lbaf_line = lba_output.strip().split(":")[0]
|
18
|
+
lbaf_index = lbaf_line.split()[-1]
|
19
|
+
return lbaf_index
|
20
|
+
except Exception:
|
21
|
+
return None
|
22
|
+
|
23
|
+
def format_nvme(lbaf_index):
|
24
|
+
cmds = [
|
25
|
+
f"sudo nvme format /dev/nvme0n1 --lbaf={lbaf_index}",
|
26
|
+
"sudo parted -a optimal /dev/nvme0n1 mklabel gpt",
|
27
|
+
"sudo parted -a optimal /dev/nvme0n1 mkpart primary ext4 0% 100%",
|
28
|
+
"sudo mkfs.ext4 /dev/nvme0n1p1",
|
29
|
+
"sudo nvme smart-log -H /dev/nvme0n1"
|
30
|
+
]
|
31
|
+
for cmd in cmds:
|
32
|
+
subprocess.run(cmd, shell=True, check=True)
|
33
|
+
|
34
|
+
def add_nvme_to_fstab():
|
35
|
+
"""
|
36
|
+
Add /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /mnt/nvme.
|
37
|
+
Only appends if the entry does not already exist.
|
38
|
+
Requires root permission to modify /etc/fstab.
|
39
|
+
"""
|
40
|
+
fstab_path = "/etc/fstab"
|
41
|
+
nvme_entry = "/dev/nvme0n1p1 /mnt/nvme ext4 defaults 0 2"
|
42
|
+
|
43
|
+
try:
|
44
|
+
# Check if the entry already exists
|
45
|
+
with open(fstab_path, "r") as f:
|
46
|
+
for line in f:
|
47
|
+
if "/dev/nvme0n1p1" in line or "/mnt/nvme" in line:
|
48
|
+
click.echo("ℹ️ NVMe mount entry already exists in /etc/fstab.")
|
49
|
+
return
|
50
|
+
|
51
|
+
# Append the entry as sudo
|
52
|
+
append_cmd = f"echo '{nvme_entry}' | sudo tee -a {fstab_path} > /dev/null"
|
53
|
+
subprocess.run(append_cmd, shell=True, check=True)
|
54
|
+
click.echo("✅ /etc/fstab updated to include NVMe auto-mount.")
|
55
|
+
except Exception as e:
|
56
|
+
click.echo(f"❌ Failed to update /etc/fstab: {e}")
|
57
|
+
|
58
|
+
def mount_nvme():
|
59
|
+
try:
|
60
|
+
# Create mount point
|
61
|
+
subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
|
62
|
+
|
63
|
+
# Mount the NVMe partition
|
64
|
+
subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
|
65
|
+
|
66
|
+
add_nvme_to_fstab()
|
67
|
+
|
68
|
+
subprocess.run("sudo mount -a", shell=True, check=True)
|
69
|
+
|
70
|
+
# Change ownership to user 'sima'
|
71
|
+
subprocess.run("sudo chown sima:sima /mnt/nvme", shell=True, check=True)
|
72
|
+
|
73
|
+
subprocess.run("sudo chmod 755 /mnt/nvme", shell=True, check=True)
|
74
|
+
|
75
|
+
|
76
|
+
print("✅ NVMe mounted and write permission granted to user 'sima'.")
|
77
|
+
|
78
|
+
except subprocess.CalledProcessError as e:
|
79
|
+
print(f"❌ Error during NVMe mount: {e}")
|
80
|
+
|
81
|
+
def nvme_format():
|
82
|
+
if not is_modalix_devkit():
|
83
|
+
click.echo("❌ This command can only be run on the Modalix DevKit.")
|
84
|
+
return
|
85
|
+
|
86
|
+
nvme_info = scan_nvme()
|
87
|
+
if not nvme_info:
|
88
|
+
click.echo("❌ No NVMe drive detected.")
|
89
|
+
return
|
90
|
+
click.echo(nvme_info)
|
91
|
+
|
92
|
+
lbaf_index = get_lba_format_index()
|
93
|
+
if lbaf_index is None:
|
94
|
+
click.echo("❌ Failed to detect LBA format index.")
|
95
|
+
return
|
96
|
+
click.echo(f"ℹ️ Detected LBA format index: {lbaf_index}")
|
97
|
+
|
98
|
+
if not click.confirm("⚠️ Are you sure you want to format /dev/nvme0n1? This will erase all data."):
|
99
|
+
click.echo("❌ Aborted by user.")
|
100
|
+
return
|
101
|
+
|
102
|
+
try:
|
103
|
+
# Unmount before formatting, ignore error if not mounted
|
104
|
+
subprocess.run("sudo umount /mnt/nvme", shell=True, check=False)
|
105
|
+
|
106
|
+
# Format and mount
|
107
|
+
format_nvme(lbaf_index)
|
108
|
+
mount_nvme()
|
109
|
+
click.echo("✅ NVMe drive formatted and mounted at /mnt/nvme.")
|
110
|
+
except subprocess.CalledProcessError:
|
111
|
+
click.echo("❌ Formatting process failed.")
|
112
|
+
|
113
|
+
|
114
|
+
def nvme_remount():
|
115
|
+
if not is_modalix_devkit():
|
116
|
+
click.echo("❌ This command can only be run on the Modalix DevKit.")
|
117
|
+
return
|
118
|
+
|
119
|
+
try:
|
120
|
+
mount_nvme()
|
121
|
+
|
122
|
+
except subprocess.CalledProcessError as e:
|
123
|
+
raise RuntimeError(f"Failed to remount NVMe: {e}")
|
sima_cli/update/bootimg.py
CHANGED
@@ -5,8 +5,8 @@ import sys
|
|
5
5
|
import os
|
6
6
|
import select
|
7
7
|
import re
|
8
|
-
|
9
|
-
from
|
8
|
+
|
9
|
+
from sima_cli.update.updater import download_image
|
10
10
|
|
11
11
|
try:
|
12
12
|
from tqdm import tqdm
|
@@ -299,7 +299,7 @@ def write_bootimg(image_path):
|
|
299
299
|
click.echo("ℹ️ Please manually eject the device.")
|
300
300
|
|
301
301
|
|
302
|
-
def write_image(version: str, board: str, swtype: str, internal: bool = False):
|
302
|
+
def write_image(version: str, board: str, swtype: str, internal: bool = False, flavor: str = 'headless'):
|
303
303
|
"""
|
304
304
|
Download and write a bootable firmware image to a removable storage device.
|
305
305
|
|
@@ -308,13 +308,14 @@ def write_image(version: str, board: str, swtype: str, internal: bool = False):
|
|
308
308
|
board (str): Target board type, e.g., "modalix" or "mlsoc".
|
309
309
|
swtype (str): Software image type, e.g., "yocto" or "exlr".
|
310
310
|
internal (bool): Whether to use internal download sources. Defaults to False.
|
311
|
+
flavor (str): Flavor of the software package - can be either headless or full.
|
311
312
|
|
312
313
|
Raises:
|
313
314
|
RuntimeError: If the download or write process fails.
|
314
315
|
"""
|
315
316
|
try:
|
316
317
|
click.echo(f"⬇️ Downloading boot image for version: {version}, board: {board}, swtype: {swtype}")
|
317
|
-
file_list = download_image(version, board, swtype, internal, update_type='bootimg')
|
318
|
+
file_list = download_image(version, board, swtype, internal, update_type='bootimg', flavor=flavor)
|
318
319
|
if not isinstance(file_list, list):
|
319
320
|
raise ValueError("Expected list of extracted files, got something else.")
|
320
321
|
|
sima_cli/update/local.py
CHANGED
@@ -73,7 +73,7 @@ def get_local_board_info() -> Tuple[str, str]:
|
|
73
73
|
return board_type, build_version
|
74
74
|
|
75
75
|
|
76
|
-
def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str):
|
76
|
+
def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str, flavor: str):
|
77
77
|
"""
|
78
78
|
Perform local firmware update using swupdate commands.
|
79
79
|
Calls swupdate directly on the provided file paths.
|
@@ -82,15 +82,17 @@ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str)
|
|
82
82
|
|
83
83
|
try:
|
84
84
|
# Run tRoot update
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
85
|
+
if troot_path != None:
|
86
|
+
click.echo("⚙️ Flashing tRoot image...")
|
87
|
+
if not _run_local_cmd(f"sudo swupdate -H simaai-image-troot:1.0 -i {troot_path}", passwd):
|
88
|
+
click.echo("❌ tRoot update failed.")
|
89
|
+
return
|
90
|
+
click.echo("✅ tRoot update completed.")
|
91
|
+
|
91
92
|
# Run Palette update
|
92
93
|
click.echo("⚙️ Flashing System image...")
|
93
|
-
|
94
|
+
_flavor = 'palette' if flavor == 'headless' else 'graphics'
|
95
|
+
if not _run_local_cmd(f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i {palette_path}", passwd):
|
94
96
|
click.echo("❌ System image update failed.")
|
95
97
|
return
|
96
98
|
click.echo("✅ System image update completed. Please powercycle the device")
|
sima_cli/update/netboot.py
CHANGED
@@ -127,7 +127,7 @@ class ClientManager:
|
|
127
127
|
|
128
128
|
while not self.shutdown_event.is_set():
|
129
129
|
try:
|
130
|
-
board_type, build_version = get_remote_board_info(ip)
|
130
|
+
board_type, build_version, _ = get_remote_board_info(ip)
|
131
131
|
|
132
132
|
if board_type and build_version:
|
133
133
|
with self.lock:
|
@@ -346,7 +346,7 @@ def run_cli(client_manager):
|
|
346
346
|
click.echo("\n🛑 Exiting netboot session.")
|
347
347
|
return True
|
348
348
|
|
349
|
-
def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False):
|
349
|
+
def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless'):
|
350
350
|
"""
|
351
351
|
Download and serve a bootable image for network boot over TFTP with client monitoring.
|
352
352
|
|
@@ -355,6 +355,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
355
355
|
board (str): Target board type, e.g., "modalix" or "davinci".
|
356
356
|
internal (bool): Whether to use internal download sources. Defaults to False.
|
357
357
|
autoflash (bool): Whether to automatically flash the devkit when networked booted. Defaults to False.
|
358
|
+
flavor (str): The software flavor, can be either headless or full.
|
358
359
|
|
359
360
|
Raises:
|
360
361
|
RuntimeError: If the download or TFTP setup fails.
|
@@ -367,7 +368,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
367
368
|
|
368
369
|
try:
|
369
370
|
click.echo(f"⬇️ Downloading netboot image for version: {version}, board: {board}")
|
370
|
-
file_list = download_image(version, board, swtype="yocto", internal=internal, update_type='netboot')
|
371
|
+
file_list = download_image(version, board, swtype="yocto", internal=internal, update_type='netboot', flavor=flavor)
|
371
372
|
if not isinstance(file_list, list):
|
372
373
|
raise ValueError("Expected list of extracted files, got something else.")
|
373
374
|
extract_dir = os.path.dirname(file_list[0])
|
sima_cli/update/query.py
CHANGED
@@ -5,7 +5,7 @@ from sima_cli.utils.config import get_auth_token
|
|
5
5
|
|
6
6
|
ARTIFACTORY_BASE_URL = artifactory_url() + '/artifactory'
|
7
7
|
|
8
|
-
def _list_available_firmware_versions_internal(board: str, match_keyword: str = None):
|
8
|
+
def _list_available_firmware_versions_internal(board: str, match_keyword: str = None, flavor: str = 'headless'):
|
9
9
|
fw_path = f"{board}"
|
10
10
|
aql_query = f"""
|
11
11
|
items.find({{
|
@@ -46,7 +46,7 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
46
46
|
|
47
47
|
return top_level_folders
|
48
48
|
|
49
|
-
def _list_available_firmware_versions_external(board: str, match_keyword: str = None):
|
49
|
+
def _list_available_firmware_versions_external(board: str, match_keyword: str = None, flavor: str = 'headless'):
|
50
50
|
"""
|
51
51
|
Construct and return a list containing a single firmware download URL for a given board.
|
52
52
|
|
@@ -56,6 +56,7 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
|
|
56
56
|
Args:
|
57
57
|
board (str): The name of the hardware board.
|
58
58
|
match_keyword (str, optional): A version string to match (e.g., '1.6' or '1.6.0').
|
59
|
+
flavor (str, optional): A string indicating firmware flavor - headless or full.
|
59
60
|
|
60
61
|
Returns:
|
61
62
|
list[str]: A list containing one formatted firmware download URL.
|
@@ -75,7 +76,7 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
|
|
75
76
|
return [firmware_download_url]
|
76
77
|
|
77
78
|
|
78
|
-
def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False):
|
79
|
+
def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False, flavor: str = 'headless'):
|
79
80
|
"""
|
80
81
|
Public interface to list available firmware versions.
|
81
82
|
|
@@ -83,11 +84,12 @@ def list_available_firmware_versions(board: str, match_keyword: str = None, inte
|
|
83
84
|
- board: str – Name of the board (e.g. 'davinci')
|
84
85
|
- match_keyword: str – Optional keyword to filter versions (case-insensitive)
|
85
86
|
- internal: bool – Must be True to access internal Artifactory
|
87
|
+
- flavor (str, optional): A string indicating firmware flavor - headless or full.
|
86
88
|
|
87
89
|
Returns:
|
88
90
|
- List[str] of firmware version folder names, or None if access is not allowed
|
89
91
|
"""
|
90
92
|
if not internal:
|
91
|
-
return _list_available_firmware_versions_external(board, match_keyword)
|
93
|
+
return _list_available_firmware_versions_external(board, match_keyword, flavor)
|
92
94
|
|
93
|
-
return _list_available_firmware_versions_internal(board, match_keyword)
|
95
|
+
return _list_available_firmware_versions_internal(board, match_keyword, flavor)
|
sima_cli/update/remote.py
CHANGED
@@ -51,41 +51,51 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
|
|
51
51
|
else:
|
52
52
|
print("\r✅ Board is online! \n")
|
53
53
|
|
54
|
-
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str]:
|
54
|
+
def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str]:
|
55
55
|
"""
|
56
|
-
Connect to the remote board and retrieve board type
|
56
|
+
Connect to the remote board and retrieve board type, build version, and fdt_name.
|
57
57
|
|
58
58
|
Args:
|
59
59
|
ip (str): IP address of the board.
|
60
60
|
|
61
61
|
Returns:
|
62
|
-
(board_type, build_version): Tuple of strings, or ('', '') on failure.
|
62
|
+
(board_type, build_version, fdt_name): Tuple of strings, or ('', '', '') on failure.
|
63
63
|
"""
|
64
64
|
board_type = ""
|
65
65
|
build_version = ""
|
66
|
+
fdt_name = ""
|
66
67
|
|
67
68
|
try:
|
68
69
|
ssh = paramiko.SSHClient()
|
69
70
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
70
71
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
71
72
|
|
72
|
-
#
|
73
|
+
# Retrieve build info
|
73
74
|
stdin, stdout, stderr = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
|
74
|
-
|
75
|
+
build_output = stdout.read().decode()
|
76
|
+
|
77
|
+
# Retrieve fdt_name from fw_printenv
|
78
|
+
stdin, stdout, stderr = ssh.exec_command("fw_printenv fdt_name 2>/dev/null")
|
79
|
+
fdt_output = stdout.read().decode()
|
80
|
+
|
75
81
|
ssh.close()
|
76
82
|
|
77
|
-
for line in
|
83
|
+
for line in build_output.splitlines():
|
78
84
|
line = line.strip()
|
79
85
|
if line.startswith("MACHINE"):
|
80
86
|
board_type = line.split("=", 1)[-1].strip()
|
81
87
|
elif line.startswith("SIMA_BUILD_VERSION"):
|
82
88
|
build_version = line.split("=", 1)[-1].strip()
|
83
89
|
|
84
|
-
|
90
|
+
for line in fdt_output.splitlines():
|
91
|
+
if line.startswith("fdt_name"):
|
92
|
+
fdt_name = line.split("=", 1)[-1].strip()
|
93
|
+
|
94
|
+
return board_type, build_version, fdt_name
|
85
95
|
|
86
96
|
except Exception as e:
|
87
97
|
click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
|
88
|
-
return "", ""
|
98
|
+
return "", "", ""
|
89
99
|
|
90
100
|
|
91
101
|
def _scp_file(sftp, local_path: str, remote_path: str):
|
@@ -187,7 +197,7 @@ def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd:
|
|
187
197
|
|
188
198
|
return False
|
189
199
|
|
190
|
-
def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool):
|
200
|
+
def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool, flavor: str = 'headless'):
|
191
201
|
"""
|
192
202
|
Upload and install firmware images to remote board over SSH.
|
193
203
|
Assumes default credentials: sima / edgeai.
|
@@ -200,21 +210,24 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
200
210
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
201
211
|
sftp = ssh.open_sftp()
|
202
212
|
remote_dir = "/tmp"
|
203
|
-
|
204
|
-
# Upload tRoot image
|
205
|
-
troot_name = os.path.basename(troot_path)
|
206
213
|
palette_name = os.path.basename(palette_path)
|
207
|
-
_scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
|
208
|
-
click.echo("🚀 Uploaded tRoot image.")
|
209
214
|
|
210
|
-
#
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
215
|
+
# Upload tRoot image
|
216
|
+
if troot_path is not None:
|
217
|
+
troot_name = os.path.basename(troot_path)
|
218
|
+
_scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
|
219
|
+
click.echo("🚀 Uploaded tRoot image.")
|
220
|
+
|
221
|
+
# Run tRoot update
|
222
|
+
run_remote_command(
|
223
|
+
ssh,
|
224
|
+
f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
|
225
|
+
)
|
226
|
+
click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
|
227
|
+
click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
|
228
|
+
_wait_for_ssh(ip, timeout=120)
|
229
|
+
else:
|
230
|
+
click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
|
218
231
|
|
219
232
|
# Upload Palette image
|
220
233
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
@@ -223,13 +236,22 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
223
236
|
click.echo("🚀 Uploaded system image.")
|
224
237
|
|
225
238
|
# Run Palette update
|
239
|
+
_flavor = 'palette' if flavor == 'headless' else 'graphics'
|
226
240
|
run_remote_command(
|
227
241
|
ssh,
|
228
|
-
f"sudo swupdate -H simaai-image-
|
242
|
+
f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i /tmp/{palette_name}",
|
229
243
|
password=passwd
|
230
244
|
)
|
231
245
|
click.echo("✅ Board image update complete.")
|
232
246
|
|
247
|
+
if _flavor == 'graphics':
|
248
|
+
click.echo(f"⚠️ With full image, setting U-Boot environment variable to support NVMe and GPU.")
|
249
|
+
run_remote_command(
|
250
|
+
ssh,
|
251
|
+
f"sudo fw_setenv dtbos pcie-4rc-2rc-2rc.dtbo",
|
252
|
+
password=passwd
|
253
|
+
)
|
254
|
+
|
233
255
|
# In the case of PCIe system, we don't need to reboot the card, instead, we will let it finish then update the PCIe driver in the host
|
234
256
|
# After that we can reboot the whole system.
|
235
257
|
if reboot_and_wait:
|
@@ -237,8 +259,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
237
259
|
click.echo("🔁 Rebooting board after update. Waiting for reconnection...")
|
238
260
|
|
239
261
|
try:
|
240
|
-
run_remote_command(ssh, "sudo
|
241
|
-
run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
|
262
|
+
run_remote_command(ssh, "sudo reboot", password=passwd)
|
242
263
|
|
243
264
|
except Exception as reboot_err:
|
244
265
|
click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the board...")
|