Skip to content

Commit 2a7e905

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 f669784 commit 2a7e905

1 file changed

Lines changed: 139 additions & 27 deletions

File tree

  • python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers

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

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

11711247
@base.command()
1172-
@click.argument("file")
1173-
@click.option("--target", type=str)
1248+
@click.argument("file", required=False)
1249+
@click.option("--target", type=str, help="Block device to flash to (e.g., 'usd', 'emmc'). If not provided, uses default target.")
1250+
@click.option(
1251+
"-t",
1252+
"partitions",
1253+
multiple=True,
1254+
help="Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions.",
1255+
)
11741256
@click.option("--os-image-checksum", help="SHA256 checksum of OS image (direct value)")
11751257
@click.option(
11761258
"--os-image-checksum-file",
@@ -1219,6 +1301,7 @@ def base():
12191301
def flash(
12201302
file,
12211303
target,
1304+
partitions,
12221305
os_image_checksum,
12231306
os_image_checksum_file,
12241307
console_debug,
@@ -1233,31 +1316,60 @@ def flash(
12331316
fls_version,
12341317
fls_binary_url,
12351318
):
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}")
1319+
"""Flash image(s) to DUT
12411320
1242-
self.set_console_debug(console_debug)
1321+
Usage examples:
1322+
# Flash to default block device and target
1323+
jumpstarter-cli flash image.img
12431324
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,
1325+
# Flash to specific block device (e.g., 'emmc')
1326+
jumpstarter-cli flash image.img --target emmc
1327+
1328+
# Flash to partition(s) on default block device
1329+
jumpstarter-cli flash -t rootfs:rootfs.img
1330+
1331+
# Flash to partition(s) on specific block device
1332+
jumpstarter-cli flash --target emmc -t rootfs:rootfs.img -t boot:boot.img
1333+
"""
1334+
# Validate and resolve flash parameters
1335+
flash_operations = self._resolve_flash_parameters(
1336+
file, partitions, target
12591337
)
12601338

1339+
# Setup common options
1340+
self.set_console_debug(console_debug)
1341+
headers_dict = self._parse_headers(header) if header else None
1342+
1343+
# Load checksum from file if provided (used for all operations)
1344+
checksum = os_image_checksum
1345+
if os_image_checksum_file and os.path.exists(os_image_checksum_file):
1346+
with open(os_image_checksum_file) as f:
1347+
checksum = f.read().strip().split()[0]
1348+
self.logger.info(f"Read checksum from file: {checksum}")
1349+
1350+
# Execute each flash operation
1351+
for idx, (image_file, target_partition, block_device) in enumerate(flash_operations):
1352+
op_num = f"{idx + 1}/{len(flash_operations)}" if len(flash_operations) > 1 else ""
1353+
op_desc = f"partition '{target_partition}'" if target_partition else "default target"
1354+
1355+
self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
1356+
1357+
# Perform the flash operation
1358+
self.flash(
1359+
image_file,
1360+
partition=target_partition,
1361+
force_exporter_http=force_exporter_http,
1362+
force_flash_bundle=force_flash_bundle,
1363+
cacert_file=cacert,
1364+
insecure_tls=insecure_tls,
1365+
headers=headers_dict,
1366+
bearer_token=bearer,
1367+
retries=retries,
1368+
method=method,
1369+
fls_version=fls_version,
1370+
fls_binary_url=fls_binary_url,
1371+
)
1372+
12611373
@base.command()
12621374
@debug_console_option
12631375
def bootloader_shell(console_debug):

0 commit comments

Comments
 (0)