Skip to content

Commit 2282aab

Browse files
author
Enric Balletbo i Serra
committed
feat(flasher): enhance flash CLI for partition-based and multiple image flashing
The `flash` CLI command now supports more flexible image flashing options. Users can specify images to flash to specific partitions using the new `-t` option (e.g., `-t rootfs:rootfs.img`). The CLI now also supports flashing multiple images in a single command when using the `-t` option. The command now handles: - Flashing a single file to a default or specified block device (`flash image.img`) - Flashing multiple file-partition pairs to a default or specified block device (`flash -t rootfs:rootfs.img -t boot:boot.img --target emmc`) This is implemented with a new internal `_resolve_flash_parameters` helper to validate and parse the various CLI arguments before executing each flash operation. The core `flash` method is updated to correctly interpret the `partition` argument as a partition label when provided, constructing the appropriate `/dev/disk/by-partlabel/` path. Signed-off-by: Enric Balletbo i Serra <eballetb@redhat.com>
1 parent 1e279c7 commit 2282aab

2 files changed

Lines changed: 254 additions & 35 deletions

File tree

python/packages/jumpstarter-driver-flashers/README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,52 @@ Commands:
113113

114114
### flash
115115
```shell
116-
Usage: j storage flash [OPTIONS] FILE
116+
Usage: j storage flash [OPTIONS] [FILE]
117117

118-
Flash image to DUT from file
118+
Flash image(s) to DUT
119+
120+
Usage examples:
121+
122+
- Flash to default block device and target
123+
124+
j storage flash image.img
125+
126+
- Flash to specific block device (e.g., 'emmc')
127+
128+
j storage flash image.img --target emmc
129+
130+
- Flash to partition(s) on default block device
131+
132+
j storage flash -t rootfs:rootfs.img
133+
134+
- Flash to partition(s) on specific block device
135+
136+
j storage flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
119137

120138
Options:
121-
--partition TEXT
139+
--target TEXT Block device to flash to (e.g., 'usd',
140+
'emmc'). If not provided, uses default
141+
target.
142+
-t TEXT Flash file to partition:
143+
'partition:filename'. Can be repeated for
144+
multiple partitions.
122145
--os-image-checksum TEXT SHA256 checksum of OS image (direct value)
123146
--os-image-checksum-file FILE File containing SHA256 checksum of OS image
124147
--force-exporter-http Force use of exporter HTTP
125148
--force-flash-bundle TEXT Force use of a specific flasher OCI bundle
126-
--console-debug Enable console debug mode
127149
--cacert FILE CA certificate to use for HTTPS
128150
--insecure-tls Skip TLS certificate verification
151+
--header TEXT Custom HTTP header in 'Key: Value' format
152+
--bearer TEXT Bearer token for HTTP authentication
153+
--retries INTEGER Number of retry attempts for flash operation
154+
(default: 3)
155+
--method [fls|shell] Method to use for flash operation (default:
156+
fls)
157+
--fls-version TEXT Download an specific fls version from the
158+
github releases
159+
--fls-binary-url TEXT Custom URL to download FLS binary from
160+
(overrides --fls-version)
161+
--console-debug Enable console debug mode
129162
--help Show this message and exit.
130163
```
131164

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

Lines changed: 217 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def flash( # noqa: C901
9090
path: PathBuf,
9191
*,
9292
partition: str | None = None,
93+
block_device: str | None = None,
9394
operator: Operator | None = None,
9495
os_image_checksum: str | None = None,
9596
force_exporter_http: bool = False,
@@ -177,6 +178,7 @@ def flash( # noqa: C901
177178
try:
178179
self._perform_flash_operation(
179180
partition,
181+
block_device,
180182
path,
181183
image_url,
182184
should_download_to_httpd,
@@ -299,9 +301,86 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
299301
current = getattr(current, "__cause__", None)
300302
return None
301303

304+
def _resolve_and_prepare_target(
305+
self,
306+
partition: str | None,
307+
block_device: str | None,
308+
manifest,
309+
console,
310+
) -> str:
311+
"""Resolve partition/target device and prepare flash target path.
312+
313+
Resolves the target for flashing, with priority to manifest targets when there
314+
is ambiguity. Supports two modes:
315+
1. partition matches a manifest target name - use it as target (if collision with
316+
partition_label suspected, log warning mentioning both interpretations)
317+
2. partition is a partition label - resolve block_device or use default target,
318+
then construct the /dev/disk/by-partlabel path
319+
320+
Args:
321+
partition: Either a target device name (from manifest.spec.targets) or a
322+
partition label. If it matches a manifest target, it takes priority.
323+
block_device: Optional block device name (e.g., 'usd', 'emmc'). Ignored if
324+
partition matches a manifest target. If partition is a partition
325+
label, this specifies which block_device to target; if omitted,
326+
uses default target from call("get_default_target") or
327+
manifest.spec.default_target.
328+
manifest: Flasher manifest containing targets and default_target
329+
console: Console object for device interaction
330+
331+
Returns:
332+
The flash target path (either device path from _get_target_device() or
333+
/dev/disk/by-partlabel/<partition_label> path)
334+
335+
Raises:
336+
ArgumentError: If no valid target can be determined
337+
"""
338+
# Check if partition is a valid target device name in the manifest
339+
is_target_name = partition and partition in manifest.spec.targets
340+
341+
if is_target_name:
342+
# Collision detection: partition matches a manifest target
343+
if block_device:
344+
self.logger.warning(
345+
f"Ambiguous partition argument '{partition}': matches both a "
346+
f"manifest target name and potentially a partition label. "
347+
f"Treating as manifest target (ignoring block_device='{block_device}'). "
348+
f"To use '{partition}' as a partition label on block_device '{block_device}', "
349+
f"rename the partition label or use only the -t option with partition:file pairs."
350+
)
351+
target = partition
352+
target_device = self._get_target_device(target, manifest, console)
353+
self.logger.info(f"Using manifest target: {target} -> {target_device}")
354+
return target_device
355+
356+
# partition is a partition label (or None), not a manifest target
357+
partition_label = partition
358+
if block_device:
359+
target = block_device
360+
self.logger.debug(f"Using block_device '{block_device}' for partition label '{partition_label}'")
361+
else:
362+
target = self.call("get_default_target") or manifest.spec.default_target
363+
if partition_label:
364+
self.logger.debug(f"Using default target for partition label '{partition_label}'")
365+
366+
if not target:
367+
raise ArgumentError("No partition or default target specified")
368+
369+
target_device = self._get_target_device(target, manifest, console)
370+
371+
if partition_label:
372+
flash_target = f"/dev/disk/by-partlabel/{partition_label}"
373+
self.logger.info(f"Using partition label: {partition_label} on {target} -> {flash_target}")
374+
else:
375+
flash_target = target_device
376+
self.logger.info(f"Using target block device: {target_device}")
377+
378+
return flash_target
379+
302380
def _perform_flash_operation(
303381
self,
304382
partition: str | None,
383+
block_device: str | None,
305384
path: PathBuf,
306385
image_url: str,
307386
should_download_to_httpd: bool,
@@ -321,13 +400,8 @@ def _perform_flash_operation(
321400
"""
322401
with self._busybox() as console:
323402
manifest = self.manifest
324-
target = partition or self.call("get_default_target") or manifest.spec.default_target
325-
if not target:
326-
raise ArgumentError("No partition or default target specified")
327-
328-
target_device = self._get_target_device(target, manifest, console)
403+
flash_target = self._resolve_and_prepare_target(partition, block_device, manifest, console)
329404

330-
self.logger.info(f"Using target block device: {target_device}")
331405
console.sendline(f"export dhcp_addr={self._dhcp_details.ip_address}")
332406
console.expect(manifest.spec.login.prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
333407
console.sendline(f"export gw_addr={self._dhcp_details.gateway}")
@@ -372,7 +446,7 @@ def _perform_flash_operation(
372446
manifest,
373447
path,
374448
image_url,
375-
target_device,
449+
flash_target,
376450
insecure_tls,
377451
stored_cacert,
378452
header_args,
@@ -385,7 +459,7 @@ def _perform_flash_operation(
385459
manifest,
386460
path,
387461
image_url,
388-
target_device,
462+
flash_target,
389463
insecure_tls,
390464
stored_cacert,
391465
header_args,
@@ -1162,15 +1236,90 @@ def _validate_bearer_token(self, token: str | None) -> str | None:
11621236

11631237
return token
11641238

1239+
def _resolve_flash_parameters(
1240+
self, file: str | None, partitions: tuple[str, ...] | None, block_device: str | None
1241+
) -> list[tuple[str, str | None, str | None]]:
1242+
"""Resolve and validate flash parameters from CLI options.
1243+
1244+
Supports multiple modes:
1245+
1. Single file to default block device:
1246+
flash image.img
1247+
2. Single file to specific block device:
1248+
flash image.img --target usd
1249+
3. Multiple file-partition pairs:
1250+
flash -t rootfs:rootfs.img -t boot:boot.img
1251+
4. Multiple file-partition pairs to specific block device:
1252+
flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1253+
1254+
Args:
1255+
file: The image file argument (positional, optional). When provided alone,
1256+
flashes to the default block device.
1257+
partitions: The -t options in 'partition:file' format (repeatable).
1258+
Use this to specify partition and file together.
1259+
block_device: The --target option (block device name like 'usd', 'emmc').
1260+
Can be used with both file and -t options.
1261+
1262+
Returns:
1263+
list[tuple]: List of (image_file, target_partition, block_device) tuples
1264+
1265+
Raises:
1266+
click.UsageError: If parameters are invalid or conflicting
1267+
"""
1268+
flash_ops: list[tuple[str, str | None, str | None]] = []
1269+
1270+
# Mode 1 & 2: Single file with optional block device
1271+
if file:
1272+
if partitions:
1273+
raise click.UsageError(
1274+
"Cannot specify FILE argument with -t options. "
1275+
"Use either 'flash image.img' or 'flash -t partition:file'"
1276+
)
1277+
flash_ops.append((file, None, block_device))
1278+
1279+
# Mode 3 & 4: Multiple file-partition pairs with optional block device
1280+
elif partitions:
1281+
for spec in partitions:
1282+
if ':' not in spec:
1283+
raise click.UsageError(
1284+
f"Invalid flash spec format: '{spec}'. "
1285+
"Expected 'partition:filename'"
1286+
)
1287+
partition_label, filename = spec.split(':', 1)
1288+
if not partition_label or not filename:
1289+
raise click.UsageError(
1290+
f"Invalid flash spec format: '{spec}'. "
1291+
"Both partition label and filename are required"
1292+
)
1293+
flash_ops.append((filename, partition_label, block_device))
1294+
1295+
# No input provided
1296+
else:
1297+
raise click.UsageError(
1298+
"Must provide either FILE argument or -t options. "
1299+
"Use 'j storage flash --help' for usage examples"
1300+
)
1301+
1302+
return flash_ops
1303+
11651304
def cli(self):
11661305
@driver_click_group(self)
11671306
def base():
11681307
"""Software-defined flasher interface"""
11691308
pass
11701309

11711310
@base.command()
1172-
@click.argument("file")
1173-
@click.option("--target", type=str)
1311+
@click.argument("file", required=False)
1312+
@click.option(
1313+
"--target",
1314+
type=str,
1315+
help="Block device to flash to (e.g., 'usd', 'emmc'). If not provided, uses default target."
1316+
)
1317+
@click.option(
1318+
"-t",
1319+
"partitions",
1320+
multiple=True,
1321+
help="Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions.",
1322+
)
11741323
@click.option("--os-image-checksum", help="SHA256 checksum of OS image (direct value)")
11751324
@click.option(
11761325
"--os-image-checksum-file",
@@ -1219,6 +1368,7 @@ def base():
12191368
def flash(
12201369
file,
12211370
target,
1371+
partitions,
12221372
os_image_checksum,
12231373
os_image_checksum_file,
12241374
console_debug,
@@ -1233,31 +1383,67 @@ def flash(
12331383
fls_version,
12341384
fls_binary_url,
12351385
):
1236-
"""Flash image to DUT from file"""
1237-
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
1238-
with open(os_image_checksum_file) as f:
1239-
os_image_checksum = f.read().strip().split()[0]
1240-
self.logger.info(f"Read checksum from file: {os_image_checksum}")
1386+
"""Flash image(s) to DUT
12411387
1242-
self.set_console_debug(console_debug)
1388+
Usage examples:
1389+
1390+
- Flash to default block device and target
1391+
1392+
j storage flash image.img
1393+
1394+
- Flash to specific block device (e.g., 'emmc')
1395+
1396+
j storage flash image.img --target emmc
1397+
1398+
- Flash to partition(s) on default block device
1399+
1400+
j storage flash -t rootfs:rootfs.img
12431401
1244-
headers = self._parse_headers(header) if header else None
1245-
1246-
self.flash(
1247-
file,
1248-
partition=target,
1249-
force_exporter_http=force_exporter_http,
1250-
force_flash_bundle=force_flash_bundle,
1251-
cacert_file=cacert,
1252-
insecure_tls=insecure_tls,
1253-
headers=headers,
1254-
bearer_token=bearer,
1255-
retries=retries,
1256-
method=method,
1257-
fls_version=fls_version,
1258-
fls_binary_url=fls_binary_url,
1402+
- Flash to partition(s) on specific block device
1403+
1404+
j storage flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1405+
"""
1406+
# Validate and resolve flash parameters
1407+
flash_operations = self._resolve_flash_parameters(
1408+
file, partitions, target
12591409
)
12601410

1411+
# Setup common options
1412+
self.set_console_debug(console_debug)
1413+
headers_dict = self._parse_headers(header) if header else None
1414+
1415+
# Load checksum from file if provided (used for all operations)
1416+
checksum = os_image_checksum
1417+
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
1418+
with open(os_image_checksum_file) as f:
1419+
checksum = f.read().strip().split()[0]
1420+
self.logger.info(f"Read checksum from file: {checksum}")
1421+
1422+
# Execute each flash operation
1423+
for idx, (image_file, target_partition, block_device) in enumerate(flash_operations):
1424+
op_num = f"{idx + 1}/{len(flash_operations)}" if len(flash_operations) > 1 else ""
1425+
op_desc = f"partition '{target_partition}'" if target_partition else "default target"
1426+
1427+
self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
1428+
1429+
# Perform the flash operation
1430+
self.flash(
1431+
image_file,
1432+
partition=target_partition,
1433+
block_device=block_device,
1434+
os_image_checksum=checksum,
1435+
force_exporter_http=force_exporter_http,
1436+
force_flash_bundle=force_flash_bundle,
1437+
cacert_file=cacert,
1438+
insecure_tls=insecure_tls,
1439+
headers=headers_dict,
1440+
bearer_token=bearer,
1441+
retries=retries,
1442+
method=method,
1443+
fls_version=fls_version,
1444+
fls_binary_url=fls_binary_url,
1445+
)
1446+
12611447
@base.command()
12621448
@debug_console_option
12631449
def bootloader_shell(console_debug):

0 commit comments

Comments
 (0)