Skip to content

Commit a5107a3

Browse files
authored
Merge pull request #736 from jumpstarter-dev/backport-735-to-release-0.7
[Backport release-0.7] flasher-driver: add support for using fls as flasher
2 parents ab6b285 + 9c45e82 commit a5107a3

File tree

1 file changed

+162
-16
lines changed
  • packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers

1 file changed

+162
-16
lines changed

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

Lines changed: 162 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from urllib.parse import urlparse
1313

1414
import click
15+
import pexpect
1516
import requests
1617
from jumpstarter_driver_composite.client import CompositeClient
1718
from jumpstarter_driver_opendal.client import FlasherClient, OpendalClient, operator_for_path
@@ -96,6 +97,8 @@ def flash( # noqa: C901
9697
headers: dict[str, str] | None = None,
9798
bearer_token: str | None = None,
9899
retries: int = 3,
100+
method: str = "fls",
101+
fls_version: str = "",
99102
):
100103
if bearer_token:
101104
bearer_token = self._validate_bearer_token(bearer_token)
@@ -169,7 +172,7 @@ def flash( # noqa: C901
169172
self._perform_flash_operation(
170173
partition, path, image_url, should_download_to_httpd,
171174
storage_thread, error_queue, cacert_file, insecure_tls,
172-
headers, bearer_token
175+
headers, bearer_token, method, fls_version
173176
)
174177
self.logger.info(f"Flash operation succeeded on attempt {attempt + 1}")
175178
break
@@ -287,6 +290,8 @@ def _perform_flash_operation(
287290
insecure_tls: bool,
288291
headers: dict[str, str] | None,
289292
bearer_token: str | None,
293+
method: str,
294+
fls_version: str,
290295
):
291296
"""Perform the actual flash operation with console setup.
292297
@@ -314,8 +319,15 @@ def _perform_flash_operation(
314319
console.expect(manifest.spec.login.prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
315320

316321
# make sure that the device is connected to the network and has an IP address
317-
console.sendline("udhcpc")
318-
console.expect(manifest.spec.login.prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
322+
try:
323+
console.sendline("udhcpc")
324+
console.expect(manifest.spec.login.prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
325+
except pexpect.TIMEOUT as e:
326+
self.logger.error(f"Timeout waiting for udhcpc to complete: {e}")
327+
raise FlashRetryableError("Timeout waiting for udhcpc to complete") from e
328+
except Exception as e:
329+
self.logger.error(f"Error running udhcpc: {e}")
330+
raise FlashRetryableError(f"Error running udhcpc: {e}") from e
319331

320332
stored_cacert = None
321333
if should_download_to_httpd:
@@ -325,17 +337,32 @@ def _perform_flash_operation(
325337

326338
header_args = self._prepare_headers(headers, bearer_token)
327339

328-
# Perform the actual flash operation
329-
self._flash_with_progress(
330-
console,
331-
manifest,
332-
path,
333-
image_url,
334-
target_device,
335-
insecure_tls,
336-
stored_cacert,
337-
header_args,
338-
)
340+
341+
if method == "fls":
342+
self._flash_with_fls(
343+
console,
344+
manifest,
345+
path,
346+
image_url,
347+
target_device,
348+
insecure_tls,
349+
stored_cacert,
350+
header_args,
351+
fls_version,
352+
)
353+
elif method == "shell":
354+
self._flash_with_progress(
355+
console,
356+
manifest,
357+
path,
358+
image_url,
359+
target_device,
360+
insecure_tls,
361+
stored_cacert,
362+
header_args,
363+
)
364+
else:
365+
raise ArgumentError(f"Invalid method: {method}")
339366

340367
console.sendline("reboot")
341368
time.sleep(2)
@@ -382,7 +409,7 @@ def _setup_flasher_ssl(self, console, manifest, cacert_file: str | None) -> str
382409

383410
return None
384411

385-
def _curl_tls_args(self, insecure_tls: bool, stored_cacert: str | None) -> str:
412+
def _cmdline_tls_args(self, insecure_tls: bool, stored_cacert: str | None) -> str:
386413
"""Generate TLS arguments for curl command.
387414
388415
Args:
@@ -418,6 +445,109 @@ def _sq(s: str) -> str:
418445

419446
return " ".join(parts)
420447

448+
def _flash_with_fls(
449+
self,
450+
console,
451+
manifest,
452+
path,
453+
image_url,
454+
target_path,
455+
insecure_tls,
456+
stored_cacert,
457+
header_args: str,
458+
fls_version: str,
459+
):
460+
"""Flash image to target device with progress monitoring.
461+
462+
Args:
463+
console: Console object for device interaction
464+
manifest: Flasher manifest containing target definitions
465+
path: Path to the source image
466+
image_url: URL to download the image from
467+
target_path: Target device path to flash to
468+
insecure_tls: Whether to use insecure TLS
469+
stored_cacert: Path to the stored CA certificate in the DUT flasher
470+
header_args: Header arguments for curl command
471+
fls_version: Version of FLS to use
472+
"""
473+
474+
# Calculate decompress and tls arguments for curl
475+
prompt = manifest.spec.login.prompt
476+
tls_args = self._cmdline_tls_args(insecure_tls, stored_cacert)
477+
478+
if fls_version != "":
479+
self.logger.info(f"Downloading FLS version {fls_version} from GitHub releases")
480+
# Download fls binary to the target device (until it is available on the target device)
481+
fls_url = (
482+
f"https://github.com/jumpstarter-dev/fls/releases/download/{fls_version}/"
483+
f"fls-aarch64-linux"
484+
)
485+
console.sendline(f"curl -L {fls_url} -o /sbin/fls")
486+
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
487+
console.sendline("echo $?")
488+
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
489+
490+
exit_code = int(console.before.decode(errors="ignore").strip().splitlines()[-1])
491+
492+
if exit_code != 0:
493+
raise FlashRetryableError(f"Failed to download FLS from {fls_url}, exit code: {exit_code}")
494+
console.sendline("chmod +x /sbin/fls")
495+
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
496+
497+
# Flash the image
498+
flash_cmd = f'fls from-url -i 1.0 -n {tls_args} {header_args} --o-direct "{image_url}" {target_path}'
499+
console.sendline(flash_cmd)
500+
501+
# Start monitoring the flash operation
502+
self._monitor_fls_progress(console, prompt)
503+
504+
self.logger.info("Flushing buffers")
505+
console.sendline("sync")
506+
console.expect(prompt, timeout=EXPECT_TIMEOUT_SYNC)
507+
508+
def _monitor_fls_progress(self, console, prompt):
509+
"""Monitor FLS flash progress by printing console output as it arrives."""
510+
last_printed_length = 0
511+
while True:
512+
try:
513+
# Try to expect the prompt with a short timeout to read output incrementally
514+
console.expect([prompt, pexpect.TIMEOUT], timeout=1)
515+
516+
# Get the output that was read - this contains all output since last match
517+
# We need to track what we've already printed to avoid duplicates
518+
current_output = console.before.decode(errors="ignore")
519+
520+
# Only process new output that we haven't seen before
521+
if len(current_output) > last_printed_length:
522+
new_output = current_output[last_printed_length:]
523+
if new_output:
524+
print(new_output, end='', flush=True)
525+
last_printed_length = len(current_output)
526+
527+
# Check if we matched the prompt (index 0 means prompt matched)
528+
if console.match_index == 0:
529+
# Prompt was matched, flash operation is complete
530+
break
531+
# If match_index is 1, it means TIMEOUT was matched, so we continue the loop
532+
533+
if 'panicked at' in current_output:
534+
raise FlashRetryableError(f"FLS panicked: {current_output}")
535+
536+
except pexpect.EOF as err:
537+
# End of file - connection closed
538+
self.logger.error("Console connection closed unexpectedly")
539+
raise FlashRetryableError("Console connection closed during flash operation") from err
540+
except Exception as err:
541+
self.logger.error(f"Error monitoring FLS progress: {err}")
542+
raise FlashRetryableError(f"Error monitoring FLS progress: {err}") from err
543+
544+
# check the fls exit code
545+
console.sendline("echo $?")
546+
console.expect(prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
547+
exit_code = int(console.before.decode(errors="ignore").strip().splitlines()[-1])
548+
if exit_code != 0:
549+
raise FlashRetryableError(f"FLS flash operation failed, exit code: {exit_code}")
550+
421551
def _flash_with_progress(
422552
self,
423553
console,
@@ -444,7 +574,7 @@ def _flash_with_progress(
444574
# Calculate decompress and tls arguments for curl
445575
prompt = manifest.spec.login.prompt
446576
decompress_cmd = _get_decompression_command(path)
447-
tls_args = self._curl_tls_args(insecure_tls, stored_cacert)
577+
tls_args = self._cmdline_tls_args(insecure_tls, stored_cacert)
448578

449579
# Check if the image URL is accessible using curl and the TLS arguments
450580
self._check_url_access(console, prompt, image_url, tls_args, header_args)
@@ -1016,6 +1146,18 @@ def base():
10161146
default=3,
10171147
help="Number of retry attempts for flash operation (default: 3)",
10181148
)
1149+
@click.option(
1150+
"--method",
1151+
type=click.Choice(["fls", "shell"]),
1152+
default="fls",
1153+
help="Method to use for flash operation (default: fls)",
1154+
)
1155+
@click.option(
1156+
"--fls-version",
1157+
type=str,
1158+
default="0.1.5", # TODO(majopela): set default to "" once fls is included in our images
1159+
help="Download an specific fls version from the github releases",
1160+
)
10191161
@debug_console_option
10201162
def flash(
10211163
file,
@@ -1030,6 +1172,8 @@ def flash(
10301172
header,
10311173
bearer,
10321174
retries,
1175+
method,
1176+
fls_version,
10331177
):
10341178
"""Flash image to DUT from file"""
10351179
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
@@ -1051,6 +1195,8 @@ def flash(
10511195
headers=headers,
10521196
bearer_token=bearer,
10531197
retries=retries,
1198+
method=method,
1199+
fls_version=fls_version,
10541200
)
10551201

10561202
@base.command()

0 commit comments

Comments
 (0)