Skip to content

Commit edc5c8a

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 edc5c8a

2 files changed

Lines changed: 190 additions & 31 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: 153 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -321,13 +321,28 @@ def _perform_flash_operation(
321321
"""
322322
with self._busybox() as console:
323323
manifest = self.manifest
324-
target = partition or self.call("get_default_target") or manifest.spec.default_target
324+
# Check if partition is a valid target device name in the manifest
325+
# If not, treat it as a partition label and use the default target
326+
partition_label = None
327+
if partition and partition in manifest.spec.targets:
328+
target = partition
329+
else:
330+
# partition is a partition label, not a target device
331+
partition_label = partition
332+
target = self.call("get_default_target") or manifest.spec.default_target
325333
if not target:
326334
raise ArgumentError("No partition or default target specified")
327335

328336
target_device = self._get_target_device(target, manifest, console)
329337

330-
self.logger.info(f"Using target block device: {target_device}")
338+
# If a partition label is provided, construct the partition path
339+
flash_target = target_device
340+
if partition_label:
341+
flash_target = f"/dev/disk/by-partlabel/{partition_label}"
342+
self.logger.info(f"Using target partition: {partition_label} ({flash_target})")
343+
else:
344+
self.logger.info(f"Using target block device: {target_device}")
345+
331346
console.sendline(f"export dhcp_addr={self._dhcp_details.ip_address}")
332347
console.expect(manifest.spec.login.prompt, timeout=EXPECT_TIMEOUT_DEFAULT)
333348
console.sendline(f"export gw_addr={self._dhcp_details.gateway}")
@@ -372,7 +387,7 @@ def _perform_flash_operation(
372387
manifest,
373388
path,
374389
image_url,
375-
target_device,
390+
flash_target,
376391
insecure_tls,
377392
stored_cacert,
378393
header_args,
@@ -385,7 +400,7 @@ def _perform_flash_operation(
385400
manifest,
386401
path,
387402
image_url,
388-
target_device,
403+
flash_target,
389404
insecure_tls,
390405
stored_cacert,
391406
header_args,
@@ -1162,15 +1177,90 @@ def _validate_bearer_token(self, token: str | None) -> str | None:
11621177

11631178
return token
11641179

1180+
def _resolve_flash_parameters(
1181+
self, file: str | None, partitions: tuple[str, ...] | None, block_device: str | None
1182+
) -> list[tuple[str, str | None, str | None]]:
1183+
"""Resolve and validate flash parameters from CLI options.
1184+
1185+
Supports multiple modes:
1186+
1. Single file to default block device:
1187+
flash image.img
1188+
2. Single file to specific block device:
1189+
flash image.img --target usd
1190+
3. Multiple file-partition pairs:
1191+
flash -t rootfs:rootfs.img -t boot:boot.img
1192+
4. Multiple file-partition pairs to specific block device:
1193+
flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1194+
1195+
Args:
1196+
file: The image file argument (positional, optional). When provided alone,
1197+
flashes to the default block device.
1198+
partitions: The -t options in 'partition:file' format (repeatable).
1199+
Use this to specify partition and file together.
1200+
block_device: The --target option (block device name like 'usd', 'emmc').
1201+
Can be used with both file and -t options.
1202+
1203+
Returns:
1204+
list[tuple]: List of (image_file, target_partition, block_device) tuples
1205+
1206+
Raises:
1207+
click.UsageError: If parameters are invalid or conflicting
1208+
"""
1209+
flash_ops: list[tuple[str, str | None, str | None]] = []
1210+
1211+
# Mode 1 & 2: Single file with optional block device
1212+
if file:
1213+
if partitions:
1214+
raise click.UsageError(
1215+
"Cannot specify FILE argument with -t options. "
1216+
"Use either 'flash image.img' or 'flash -t partition:file'"
1217+
)
1218+
flash_ops.append((file, None, block_device))
1219+
1220+
# Mode 3 & 4: Multiple file-partition pairs with optional block device
1221+
elif partitions:
1222+
for spec in partitions:
1223+
if ':' not in spec:
1224+
raise click.UsageError(
1225+
f"Invalid flash spec format: '{spec}'. "
1226+
"Expected 'partition:filename'"
1227+
)
1228+
partition_label, filename = spec.split(':', 1)
1229+
if not partition_label or not filename:
1230+
raise click.UsageError(
1231+
f"Invalid flash spec format: '{spec}'. "
1232+
"Both partition label and filename are required"
1233+
)
1234+
flash_ops.append((filename, partition_label, block_device))
1235+
1236+
# No input provided
1237+
else:
1238+
raise click.UsageError(
1239+
"Must provide either FILE argument or -t options. "
1240+
"Use 'j storage flash --help' for usage examples"
1241+
)
1242+
1243+
return flash_ops
1244+
11651245
def cli(self):
11661246
@driver_click_group(self)
11671247
def base():
11681248
"""Software-defined flasher interface"""
11691249
pass
11701250

11711251
@base.command()
1172-
@click.argument("file")
1173-
@click.option("--target", type=str)
1252+
@click.argument("file", required=False)
1253+
@click.option(
1254+
"--target",
1255+
type=str,
1256+
help="Block device to flash to (e.g., 'usd', 'emmc'). If not provided, uses default target."
1257+
)
1258+
@click.option(
1259+
"-t",
1260+
"partitions",
1261+
multiple=True,
1262+
help="Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions.",
1263+
)
11741264
@click.option("--os-image-checksum", help="SHA256 checksum of OS image (direct value)")
11751265
@click.option(
11761266
"--os-image-checksum-file",
@@ -1219,6 +1309,7 @@ def base():
12191309
def flash(
12201310
file,
12211311
target,
1312+
partitions,
12221313
os_image_checksum,
12231314
os_image_checksum_file,
12241315
console_debug,
@@ -1233,31 +1324,66 @@ def flash(
12331324
fls_version,
12341325
fls_binary_url,
12351326
):
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}")
1327+
"""Flash image(s) to DUT
12411328
1242-
self.set_console_debug(console_debug)
1329+
Usage examples:
1330+
1331+
- Flash to default block device and target
1332+
1333+
j storage flash image.img
1334+
1335+
- Flash to specific block device (e.g., 'emmc')
12431336
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,
1337+
j storage flash image.img --target emmc
1338+
1339+
- Flash to partition(s) on default block device
1340+
1341+
j storage flash -t rootfs:rootfs.img
1342+
1343+
- Flash to partition(s) on specific block device
1344+
1345+
j storage flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1346+
"""
1347+
# Validate and resolve flash parameters
1348+
flash_operations = self._resolve_flash_parameters(
1349+
file, partitions, target
12591350
)
12601351

1352+
# Setup common options
1353+
self.set_console_debug(console_debug)
1354+
headers_dict = self._parse_headers(header) if header else None
1355+
1356+
# Load checksum from file if provided (used for all operations)
1357+
checksum = os_image_checksum
1358+
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
1359+
with open(os_image_checksum_file) as f:
1360+
checksum = f.read().strip().split()[0]
1361+
self.logger.info(f"Read checksum from file: {checksum}")
1362+
1363+
# Execute each flash operation
1364+
for idx, (image_file, target_partition) in enumerate(flash_operations):
1365+
op_num = f"{idx + 1}/{len(flash_operations)}" if len(flash_operations) > 1 else ""
1366+
op_desc = f"partition '{target_partition}'" if target_partition else "default target"
1367+
1368+
self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
1369+
1370+
# Perform the flash operation
1371+
self.flash(
1372+
image_file,
1373+
partition=target_partition,
1374+
os_image_checksum=checksum,
1375+
force_exporter_http=force_exporter_http,
1376+
force_flash_bundle=force_flash_bundle,
1377+
cacert_file=cacert,
1378+
insecure_tls=insecure_tls,
1379+
headers=headers_dict,
1380+
bearer_token=bearer,
1381+
retries=retries,
1382+
method=method,
1383+
fls_version=fls_version,
1384+
fls_binary_url=fls_binary_url,
1385+
)
1386+
12611387
@base.command()
12621388
@debug_console_option
12631389
def bootloader_shell(console_debug):

0 commit comments

Comments
 (0)