Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 268ae9e

Browse files
authored
Merge pull request #346 from jumpstarter-dev/uboot
Make use of standalone uboot driver in flasher
2 parents 860deda + 28308bb commit 268ae9e

10 files changed

Lines changed: 138 additions & 264 deletions

File tree

packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/bundle.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,26 @@ class FileAddress(BaseModel):
99
file: str
1010
address: str
1111

12+
1213
class DtbVariant(BaseModel):
1314
default: str
1415
address: str
1516
variants: dict[str, str]
1617

18+
1719
class FlasherLogin(BaseModel):
1820
login_prompt: str
1921
username: str | None = None
2022
password: str | None = None
2123
prompt: str
2224

25+
2326
class FlashBundleSpecV1Alpha1(BaseModel):
2427
manufacturer: str
2528
link: Optional[str]
2629
bootcmd: str
2730
shelltype: Literal["busybox"] = Field(default="busybox")
28-
login: FlasherLogin = Field(
29-
default_factory=lambda: FlasherLogin(
30-
login_prompt="login:",
31-
prompt="#")
32-
)
31+
login: FlasherLogin = Field(default_factory=lambda: FlasherLogin(login_prompt="login:", prompt="#"))
3332
default_target: str
3433
targets: dict[str, str]
3534
kernel: FileAddress
@@ -41,6 +40,7 @@ class FlashBundleSpecV1Alpha1(BaseModel):
4140
class ObjectMeta(BaseModel):
4241
name: str
4342

43+
4444
class FlasherBundleManifestV1Alpha1(BaseModel):
4545
apiVersion: Literal["jumpstarter.dev/v1alpha1"] = Field(default="jumpstarter.dev/v1alpha1")
4646
kind: Literal["FlashBundleManifest"] = Field(default="FlashBundleManifest")

packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py

Lines changed: 74 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020

2121
from jumpstarter_driver_flashers.bundle import FlasherBundleManifestV1Alpha1
2222

23-
from .uboot import UbootConsole
2423
from jumpstarter.common.exceptions import ArgumentError
2524

2625
debug_console_option = click.option("--console-debug", is_flag=True, help="Enable console debug mode")
2726

27+
2828
@dataclass(kw_only=True)
2929
class BaseFlasherClient(FlasherClient, CompositeClient):
3030
"""
@@ -41,6 +41,7 @@ def __post_init__(self):
4141
def set_console_debug(self, debug: bool):
4242
"""Set console debug mode"""
4343
self._console_debug = debug
44+
# TODO: also set console debug on uboot client
4445

4546
@contextmanager
4647
def busybox_shell(self):
@@ -60,12 +61,8 @@ def bootloader_shell(self):
6061
self.logger.info("Setting up flasher bundle files in exporter")
6162
self.call("setup_flasher_bundle")
6263
with self._services_up():
63-
with self.serial.pexpect() as console:
64-
if self._console_debug:
65-
console.logfile_read = sys.stdout.buffer
66-
uboot = UbootConsole(console=console, power=self.power, logger=self.logger)
67-
uboot.reboot_to_console()
68-
console.sendline("")
64+
with self.uboot.reboot_to_console():
65+
pass
6966
yield self.serial
7067

7168
def flash(
@@ -99,10 +96,11 @@ def flash(
9996
error_queue = Queue()
10097

10198
# Start the storage write operation in the background
102-
storage_thread = threading.Thread(target=self._transfer_bg_thread,
103-
args=(path, operator, operator_scheme,
104-
os_image_checksum, self.http.storage, error_queue),
105-
name="storage_transfer")
99+
storage_thread = threading.Thread(
100+
target=self._transfer_bg_thread,
101+
args=(path, operator, operator_scheme, os_image_checksum, self.http.storage, error_queue),
102+
name="storage_transfer",
103+
)
106104
storage_thread.start()
107105

108106
# Make the exporter download the bundle contents and set files in the right places
@@ -155,7 +153,6 @@ def flash(
155153
self.logger.info("Powering off target")
156154
self.power.off()
157155

158-
159156
def _flash_with_progress(self, console, manifest, path, image_url, target_path):
160157
"""Flash image to target device with progress monitoring.
161158
@@ -170,8 +167,8 @@ def _flash_with_progress(self, console, manifest, path, image_url, target_path):
170167
decompress_cmd = _get_decompression_command(path)
171168
flash_cmd = (
172169
f'( wget -q -O - "{image_url}" | '
173-
f'{decompress_cmd} '
174-
f'dd of={target_path} bs=64k iflag=fullblock oflag=direct) &'
170+
f"{decompress_cmd} "
171+
f"dd of={target_path} bs=64k iflag=fullblock oflag=direct) &"
175172
)
176173
console.sendline(flash_cmd)
177174
console.expect(manifest.spec.login.prompt, timeout=60)
@@ -190,16 +187,16 @@ def _flash_with_progress(self, console, manifest, path, image_url, target_path):
190187
if "No such file or directory" in console.before.decode(errors="ignore"):
191188
break
192189
data = console.before.decode(errors="ignore")
193-
match = re.search(r'pos:\s+(\d+)', data)
190+
match = re.search(r"pos:\s+(\d+)", data)
194191
if match:
195192
current_bytes = int(match.group(1))
196193
current_time = time.time()
197194
elapsed = current_time - last_time
198195

199196
if elapsed >= 1.0: # Update speed every second
200197
bytes_diff = current_bytes - last_pos
201-
speed_mb = (bytes_diff / (1024*1024)) / elapsed
202-
total_mb = current_bytes/(1024*1024)
198+
speed_mb = (bytes_diff / (1024 * 1024)) / elapsed
199+
total_mb = current_bytes / (1024 * 1024)
203200
self.logger.info(f"Flash progress: {total_mb:.2f} MB, Speed: {speed_mb:.2f} MB/s")
204201

205202
last_pos = current_bytes
@@ -209,7 +206,6 @@ def _flash_with_progress(self, console, manifest, path, image_url, target_path):
209206
console.sendline("sync")
210207
console.expect(manifest.spec.login.prompt, timeout=1200)
211208

212-
213209
def _get_target_device(self, target: str, manifest: FlasherBundleManifestV1Alpha1, console) -> str:
214210
"""Get the target device path from the manifest, resolving block devices if needed.
215211
@@ -229,15 +225,19 @@ def _get_target_device(self, target: str, manifest: FlasherBundleManifestV1Alpha
229225
raise ArgumentError(f"Target {target} not found in manifest")
230226

231227
if target_path.startswith("/sys/class/block#"):
232-
target_path = self._lookup_block_device(
233-
console, manifest.spec.login.prompt, target_path.split("#")[1])
228+
target_path = self._lookup_block_device(console, manifest.spec.login.prompt, target_path.split("#")[1])
234229

235230
return target_path
236231

237-
238-
def _transfer_bg_thread(self, src_path: PathBuf, src_operator: Operator, src_operator_scheme: str,
239-
known_hash: str | None,
240-
to_storage: OpendalClient, error_queue):
232+
def _transfer_bg_thread(
233+
self,
234+
src_path: PathBuf,
235+
src_operator: Operator,
236+
src_operator_scheme: str,
237+
known_hash: str | None,
238+
to_storage: OpendalClient,
239+
error_queue,
240+
):
241241
"""Transfer image to storage in the background
242242
243243
Args:
@@ -285,7 +285,6 @@ def _transfer_bg_thread(self, src_path: PathBuf, src_operator: Operator, src_ope
285285
raise
286286

287287
def _sha256_file(self, src_operator, src_path) -> str:
288-
289288
m = hashlib.sha256()
290289
with src_operator.open(src_path, "rb") as f:
291290
while True:
@@ -299,11 +298,13 @@ def _sha256_file(self, src_operator, src_path) -> str:
299298
def _create_metadata_and_json(self, src_operator, src_path) -> tuple[Metadata, str]:
300299
"""Create a metadata json string from a metadata object"""
301300
metadata = src_operator.stat(src_path)
302-
return metadata, json.dumps({
303-
"path": str(src_path),
304-
"content_length": metadata.content_length,
305-
"etag": metadata.etag,
306-
})
301+
return metadata, json.dumps(
302+
{
303+
"path": str(src_path),
304+
"content_length": metadata.content_length,
305+
"etag": metadata.etag,
306+
}
307+
)
307308

308309
def _lookup_block_device(self, console, prompt, address: str) -> str:
309310
"""Lookup block device for a given address.
@@ -317,7 +318,7 @@ def _lookup_block_device(self, console, prompt, address: str) -> str:
317318
# lrwxrwxrwx 1 root root 0 Jan 1
318319
# 00:00 mmcblk1 -> ../../devices/platform/bus@100000/4fb0000.mmc/mmc_host/mmc1/mmc1:aaaa/block/mmcblk1
319320
output = console.before.decode(errors="ignore")
320-
match = re.search(r'\s(\w+)\s->', output)
321+
match = re.search(r"\s(\w+)\s->", output)
321322
if match:
322323
return "/dev/" + match.group(1)
323324
else:
@@ -359,54 +360,53 @@ def _services_up(self):
359360
self.http.stop()
360361
self.tftp.stop()
361362

362-
363363
def _generate_uboot_env(self):
364364
"""Generate a uboot environment dictionary, may need specific overrides for different targets"""
365365
tftp_host = self.tftp.get_host()
366366
return {
367367
"serverip": tftp_host,
368368
}
369369

370-
371370
@contextmanager
372371
def _busybox(self):
373372
"""Start a busybox shell.
374373
375374
This is a helper context manager that boots the device into uboot and returns a console object.
376375
"""
377-
with self.serial.pexpect() as console:
378-
if self._console_debug:
379-
console.logfile_read = sys.stdout.buffer
380-
uboot = UbootConsole(console=console, power=self.power, logger=self.logger)
381-
# make sure that the device is booted into the uboot console
382-
uboot.reboot_to_console()
376+
377+
# make sure that the device is booted into the uboot console
378+
with self.uboot.reboot_to_console():
383379
# run dhcp discovery and gather details useful for later
384-
self._dhcp_details = uboot.setup_dhcp()
380+
self._dhcp_details = self.uboot.setup_dhcp()
385381
self.logger.info(f"discovered dhcp details: {self._dhcp_details}")
386382

387383
# configure the environment necessary
388384
env = self._generate_uboot_env()
389-
uboot.set_env_dict(env)
385+
self.uboot.set_env_dict(env)
390386

391387
# load any necessary files to RAM from the tftp storage
392388
manifest = self.manifest
393389
kernel_filename = Path(manifest.get_kernel_file()).name
394390
kernel_address = manifest.get_kernel_address()
395391

396-
uboot.run_command(f"tftpboot {kernel_address} {kernel_filename}", timeout=120)
392+
self.uboot.run_command(f"tftpboot {kernel_address} {kernel_filename}", timeout=120)
397393

398394
if manifest.get_initram_file():
399395
initram_filename = Path(manifest.get_initram_file()).name
400396
initram_address = manifest.get_initram_address()
401-
uboot.run_command(f"tftpboot {initram_address} {initram_filename}", timeout=120)
397+
self.uboot.run_command(f"tftpboot {initram_address} {initram_filename}", timeout=120)
402398

403399
if manifest.get_dtb_file():
404400
dtb_filename = Path(manifest.get_dtb_file()).name
405401
dtb_address = manifest.get_dtb_address()
406-
uboot.run_command(f"tftpboot {dtb_address} {dtb_filename}", timeout=120)
402+
self.uboot.run_command(f"tftpboot {dtb_address} {dtb_filename}", timeout=120)
403+
404+
with self.serial.pexpect() as console:
405+
if self._console_debug:
406+
console.logfile_read = sys.stdout.buffer
407407

408408
self.logger.info(f"Running boot command: {manifest.spec.bootcmd}")
409-
console.send(manifest.spec.bootcmd +"\n")
409+
console.send(manifest.spec.bootcmd + "\n")
410410

411411
# if manifest has login details, we need to login
412412
if manifest.spec.login.username:
@@ -438,7 +438,7 @@ def use_initram(self, path: PathBuf, operator: Operator | None = None):
438438
def use_kernel(self, path: PathBuf, operator: Operator | None = None):
439439
"""Use kernel file"""
440440
if operator is None:
441-
path, operator, operator_scheme = operator_for_path(path)
441+
path, operator, operator_scheme = operator_for_path(path)
442442

443443
...
444444

@@ -461,27 +461,37 @@ def base():
461461
@base.command()
462462
@click.argument("file")
463463
@click.option("--partition", type=str)
464-
@click.option('--os-image-checksum',
465-
help='SHA256 checksum of OS image (direct value)')
466-
@click.option('--os-image-checksum-file',
467-
help='File containing SHA256 checksum of OS image',
468-
type=click.Path(exists=True, dir_okay=False))
469-
@click.option('--force-exporter-http', is_flag=True, help='Force use of exporter HTTP')
470-
@click.option('--force-flash-bundle', type=str, help='Force use of a specific flasher OCI bundle')
464+
@click.option("--os-image-checksum", help="SHA256 checksum of OS image (direct value)")
465+
@click.option(
466+
"--os-image-checksum-file",
467+
help="File containing SHA256 checksum of OS image",
468+
type=click.Path(exists=True, dir_okay=False),
469+
)
470+
@click.option("--force-exporter-http", is_flag=True, help="Force use of exporter HTTP")
471+
@click.option("--force-flash-bundle", type=str, help="Force use of a specific flasher OCI bundle")
471472
@debug_console_option
472-
def flash(file, partition, os_image_checksum, os_image_checksum_file,
473-
console_debug, force_exporter_http, force_flash_bundle):
473+
def flash(
474+
file,
475+
partition,
476+
os_image_checksum,
477+
os_image_checksum_file,
478+
console_debug,
479+
force_exporter_http,
480+
force_flash_bundle,
481+
):
474482
"""Flash image to DUT from file"""
475483
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
476484
with open(os_image_checksum_file) as f:
477485
os_image_checksum = f.read().strip().split()[0]
478486
self.logger.info(f"Read checksum from file: {os_image_checksum}")
479487

480488
self.set_console_debug(console_debug)
481-
self.flash(file,
482-
partition=partition,
483-
force_exporter_http=force_exporter_http,
484-
force_flash_bundle=force_flash_bundle)
489+
self.flash(
490+
file,
491+
partition=partition,
492+
force_exporter_http=force_exporter_http,
493+
force_flash_bundle=force_flash_bundle,
494+
)
485495

486496
@base.command()
487497
@debug_console_option
@@ -522,8 +532,8 @@ def _get_decompression_command(filename_or_url) -> str:
522532
filename = urlparse(filename_or_url).path.split("/")[-1]
523533

524534
filename = filename.lower()
525-
if filename.endswith(('.gz', '.gzip')):
526-
return 'zcat |'
527-
elif filename.endswith('.xz'):
528-
return 'xzcat |'
529-
return ''
535+
if filename.endswith((".gz", ".gzip")):
536+
return "zcat |"
537+
elif filename.endswith(".xz"):
538+
return "xzcat |"
539+
return ""

0 commit comments

Comments
 (0)