Skip to content

Enhance flash CLI for partition-based and multiple image flashing#159

Merged
bennyz merged 3 commits into
jumpstarter-dev:mainfrom
eballetbo:flash-to-partitions
Feb 12, 2026
Merged

Enhance flash CLI for partition-based and multiple image flashing#159
bennyz merged 3 commits into
jumpstarter-dev:mainfrom
eballetbo:flash-to-partitions

Conversation

@eballetbo

@eballetbo eballetbo commented Jan 27, 2026

Copy link
Copy Markdown

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.

Summary by CodeRabbit

  • New Features

    • Multi-partition flashing via -t (partition:image pairs), optional FILE argument, explicit block-device/target option, partition-label resolution to device paths, per-operation sequencing/progress logging, single-checksum load for multi-flash, HTTP auth/headers and retry control, and flasher method/version/binary selection.
  • Documentation

    • Updated CLI help and README with expanded syntax, examples (single, partition, combined), and detailed option descriptions.

@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

CLI and client now support multi-image flashing: partition:FILE mappings and optional block device are parsed into per-operation tuples; each operation resolves a flash target (partition label → /dev/disk/by-partlabel/... or block device), fetches image/checksum, and invokes the flasher per target with updated logging.

Changes

Cohort / File(s) Summary
Flasher client — multi-flash & partition handling
python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py
Adds _resolve_flash_parameters() and _resolve_and_prepare_target(); extends flash() CLI signature to accept partitions (-t) and block_device; refactors orchestration to produce a list of (file, partition, block_device) ops, load checksum once, and loop per-operation; _perform_flash_operation() updated to accept block_device and use flash_target for all flash calls; adds per-operation logging.
Documentation / CLI examples
python/packages/jumpstarter-driver-flashers/README.md
Makes FILE optional; documents multi-file/partition flashing, block-device usage; replaces --partition help with clarified --target; adds -t partition:filename mapping, HTTP-related options (--header, --bearer, --retries), and flasher options (--method, --fls-version, --fls-binary-url); updates examples.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "CLI (user)"
    participant Client as "FlasherClient"
    participant Fetch as "ImageFetcher/HTTP"
    participant OS as "OS (/dev resolution)"
    participant Flasher as "Flasher Operator"

    CLI->>Client: parse FILE, -t partitions, block_device
    Client->>Client: _resolve_flash_parameters() -> list of ops
    loop per operation
      Client->>Fetch: fetch/load image (URL or file)
      Fetch-->>Client: image + checksum
      Client->>OS: resolve partition label -> /dev/disk/by-partlabel/X (if label)
      OS-->>Client: resolved flash_target
      Client->>Flasher: invoke flash operation (flash_target, image, options)
      Flasher-->>Client: success/failure
    end
    Client-->>CLI: aggregate results / exit status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo
  • bennyz

Poem

🐇 I hop from label to device, nibbling bytes with glee,

Mapping files to partitions — a flashing jubilee.
One loop, many hops, each target finds its way,
Tiny paws press Enter, and images dance all day. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature addition: enabling partition-based and multiple image flashing in the flash CLI command.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@eballetbo eballetbo changed the title Enhance flash CLI for partition-based and multiple ima… Enhance flash CLI for partition-based and multiple image flashing Jan 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py (1)

106-112: Misplaced docstring - will not be recognized by documentation tools.

The docstring on line 112 ("""Flash image to DUT""") appears after executable code. Python docstrings must immediately follow the function definition to be properly recognized by help(), IDEs, and documentation generators.

🔧 Proposed fix
     def flash(  # noqa: C901
         self,
         path: PathBuf,
         *,
         partition: str | None = None,
         operator: Operator | None = None,
         os_image_checksum: str | None = None,
         force_exporter_http: bool = False,
         force_flash_bundle: str | None = None,
         cacert_file: str | None = None,
         insecure_tls: bool = False,
         headers: dict[str, str] | None = None,
         bearer_token: str | None = None,
         retries: int = 3,
         method: str = "fls",
         fls_version: str = "",
         fls_binary_url: str | None = None,
     ):
+        """Flash image to DUT"""
         if bearer_token:
             bearer_token = self._validate_bearer_token(bearer_token)
 
         if headers:
             headers = self._validate_header_dict(headers)
 
-        """Flash image to DUT"""
         should_download_to_httpd = True
🤖 Fix all issues with AI agents
In
`@python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py`:
- Around line 1344-1371: The code reads a checksum into the local variable
"checksum" (from os_image_checksum_file) but never passes it into the flash
routine; update the self.flash(...) call inside the for-loop to include the
checksum parameter (pass the read value, e.g. os_image_checksum=checksum or
checksum=checksum depending on the flash() signature) so that the flash method
(self.flash) receives and can verify the image checksum; locate the call to
self.flash in the loop that also references force_exporter_http,
force_flash_bundle, cacert_file, etc., and add the appropriate named argument.
🧹 Nitpick comments (3)
python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py (3)

1180-1239: Well-implemented parameter resolution with clear mode separation.

The method cleanly handles all four documented modes and provides helpful error messages. Using split(':', 1) correctly handles edge cases where filenames might contain colons.

Minor suggestion: The error message on line 1236 hardcodes 'jumpstarter-cli flash', which may not match the actual command invocation path. Consider using a generic reference or removing the specific command name.

♻️ Suggested improvement
         else:
             raise click.UsageError(
                 "Must provide either FILE argument or -t options. "
-                "Use 'jumpstarter-cli flash --help' for usage examples"
+                "Use '--help' for usage examples"
             )

1247-1255: Potential user confusion between --target and -t options.

Both options involve "target" conceptually but mean different things:

  • --target specifies the block device (e.g., 'emmc', 'usd')
  • -t specifies partition:filename pairs

This could lead to user confusion since -t is typically a short form of --target. Consider using a different short option for partition specs, such as -p (for partition) or --partition-file.

♻️ Suggested improvement
         `@click.option`("--target", type=str, help="Block device to flash to (e.g., 'usd', 'emmc'). If not provided, uses default target.")
         `@click.option`(
-            "-t",
+            "-p",
             "partitions",
             multiple=True,
-            help="Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions.",
+            help="Flash file to partition: 'partition:filename'. Can be repeated for multiple partitions. Example: -p rootfs:rootfs.img",
         )

1350-1371: Consider documenting multi-partition flash behavior.

When flashing multiple partitions (e.g., -t rootfs:rootfs.img -t boot:boot.img), the current implementation performs a full flash cycle (boot to busybox, flash, reboot) for each partition. This means N partitions = N device reboots.

While this is safe and leverages existing retry logic, it may result in longer flash times for multi-partition scenarios. Consider:

  1. Adding a note in the CLI help about this behavior
  2. Future optimization: batch partition writes within a single boot cycle

Comment thread python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py Outdated
@eballetbo eballetbo force-pushed the flash-to-partitions branch from 2a7e905 to eb05aa6 Compare February 5, 2026 14:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py`:
- Around line 1356-1377: The loop unpacks (image_file, target_partition,
block_device) from _resolve_flash_parameters but never forwards block_device to
self.flash(), so --target is ignored; fix by passing the unpacked block_device
into the flash call (e.g. add block_device=block_device or the appropriate named
parameter expected by flash) while keeping partition=target_partition so
single-file invocations respect --target; update the call site inside the loop
that invokes self.flash(...) to include the block_device argument and run tests
for both single-file and -t combined cases to confirm behavior.
🧹 Nitpick comments (2)
python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py (1)

1357-1360: Minor: Double space in log message for single operations.

When len(flash_operations) == 1, op_num is empty string, resulting in "Flashing partition..." (double space). The .strip() call only removes leading/trailing whitespace.

♻️ Proposed fix
-                op_num = f"{idx + 1}/{len(flash_operations)}" if len(flash_operations) > 1 else ""
                 op_desc = f"partition '{target_partition}'" if target_partition else "default target"
-
-                self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
+                if len(flash_operations) > 1:
+                    self.logger.info(f"Flashing {idx + 1}/{len(flash_operations)} {op_desc} with '{image_file}'")
+                else:
+                    self.logger.info(f"Flashing {op_desc} with '{image_file}'")
python/packages/jumpstarter-driver-flashers/README.md (1)

267-270: Consider clarifying terminology in API example.

The example flasherclient.flash("/path/to/image.raw.xz", partition="emmc") uses partition="emmc" where emmc is actually a target/block device name, not a partition label. The comment "Flash into a specific partition" is misleading - it's flashing to a specific target block device.

This reflects the dual-purpose nature of the partition parameter in the code, but could confuse API users.

📝 Suggested documentation clarification
-Flash into a specific partition
+Flash to a specific target block device
 ```python
 flasherclient.flash("/path/to/image.raw.xz", partition="emmc")

+Flash to a specific partition by label (on the default target)
+python +flasherclient.flash("/path/to/image.raw.xz", partition="rootfs") +

</details>

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py`:
- Around line 1180-1244: The function _resolve_flash_parameters returns 3-tuples
(image_file, target_partition, block_device) but some callers/loops unpack only
two values causing a ValueError; update any unpacking to expect three elements
(e.g., change "for a, b in flash_ops" to "for image_file, target_partition,
block_device in flash_ops") and ensure downstream uses reference the new
block_device variable (same fix applies to the similar unpacking around lines
1363-1374).

@mangelajo

Copy link
Copy Markdown
Member

I will take this for a test ride ASAP since we don't have automated testing for it yet.

@eballetbo eballetbo force-pushed the flash-to-partitions branch from edc5c8a to 2f5d556 Compare February 5, 2026 16:00
@eballetbo

Copy link
Copy Markdown
Author

A linter job is complaining about too much complexity in _perform_flash_operation

Error: python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py:304:9: C901 `_perform_flash_operation` is too complex (11 > 10)
Error: The process '/opt/hostedtoolcache/ruff/0.14.14/x86_64/ruff' failed with exit code 1

I don't think I add much more complexity to that function, probably this function needs some refactor, but could be done as a follow up patches?

@bennyz

bennyz commented Feb 9, 2026

Copy link
Copy Markdown
Member

A linter job is complaining about too much complexity in _perform_flash_operation

Error: python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py:304:9: C901 `_perform_flash_operation` is too complex (11 > 10)
Error: The process '/opt/hostedtoolcache/ruff/0.14.14/x86_64/ruff' failed with exit code 1

I don't think I add much more complexity to that function, probably this function needs some refactor, but could be done as a follow up patches?

I think by simply extracting this part to a separate method we can please the linter:

            # Check if partition is a valid target device name in the manifest
            # If not, treat it as a partition label and use the specified block_device or default target
            partition_label = None
            if partition and partition in manifest.spec.targets:
                target = partition
            else:
                # partition is a partition label, not a target device
                partition_label = partition
                # Use specified block_device if provided, otherwise use default target
                if block_device:
                    target = block_device
                else:
                    target = self.call("get_default_target") or manifest.spec.default_target

I do agree it doesn't really add much complexity, but the linter just counts branches

@eballetbo eballetbo force-pushed the flash-to-partitions branch from 2f5d556 to 3cd10f3 Compare February 9, 2026 11:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py`:
- Around line 304-356: The _resolve_and_prepare_target function treats a
provided partition string as a manifest target if it matches
manifest.spec.targets, causing collisions with partition labels and silently
discarding block_device; update the logic to detect this ambiguity and handle it
explicitly: if partition is provided and also exists in manifest.spec.targets,
log a warning (or raise/choose priority per policy) mentioning the collision
between partition_label and manifest target name, include which branch was
chosen, and preserve or surface block_device instead of discarding it; adjust
callers or documentation if you choose deterministic priority, and reference
_resolve_and_prepare_target, manifest.spec.targets, partition_label,
block_device, _get_target_device, and call("get_default_target") when
implementing the change.
🧹 Nitpick comments (3)
python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py (3)

1400-1423: Multi-partition flashing triggers a full device reboot cycle per image.

Each call to self.flash() in this loop performs the entire lifecycle: bundle setup → busybox boot → network config → image transfer → flash → reboot. For flash -t rootfs:rootfs.img -t boot:boot.img, this means two complete boot cycles, which could be very slow on real hardware.

This is fine as a first iteration, but consider adding a TODO or tracking issue for a future optimization that batches multiple partitions within a single busybox session.


1401-1405: Minor: double space in log message for single-operation case.

When len(flash_operations) == 1, op_num is "", so the f-string produces "Flashing default target with 'image.img'" (note double space). The trailing .strip() doesn't fix inner whitespace.

💅 Suggested fix
-                op_num = f"{idx + 1}/{len(flash_operations)}" if len(flash_operations) > 1 else ""
-                op_desc = f"partition '{target_partition}'" if target_partition else "default target"
-
-                self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
+                op_desc = f"partition '{target_partition}'" if target_partition else "default target"
+                if len(flash_operations) > 1:
+                    self.logger.info(f"Flashing {idx + 1}/{len(flash_operations)} {op_desc} with '{image_file}'")
+                else:
+                    self.logger.info(f"Flashing {op_desc} with '{image_file}'")

1288-1300: The --target option name collides with the -t short form.

--target has no explicit short form, while the partitions option uses -t as its flag. Click won't confuse them (one is --target, the other is -t), but for users this is easy to mix up — especially since -t typically looks like the short form of --target. Consider renaming the block device option to --block-device or giving it an explicit short form like -b to reduce confusion.

@eballetbo eballetbo force-pushed the flash-to-partitions branch from 3cd10f3 to 2282aab Compare February 9, 2026 12:05
@bennyz

bennyz commented Feb 10, 2026

Copy link
Copy Markdown
Member

@mangelajo I think we have some potential for a little UX confusion, in the base flasher -t is short for --target (and that's how ridesx uses it for example), but here these are different flags

@mangelajo

Copy link
Copy Markdown
Member

@mangelajo I think we have some potential for a little UX confusion, in the base flasher -t is short for --target (and that's how ridesx uses it for example), but here these are different flags

but in ridesx4 in the end those "targets" are partitions on disk. Isn't that equivalent to this?

@bennyz

bennyz commented Feb 10, 2026

Copy link
Copy Markdown
Member

@mangelajo I think we have some potential for a little UX confusion, in the base flasher -t is short for --target (and that's how ridesx uses it for example), but here these are different flags

but in ridesx4 in the end those "targets" are partitions on disk. Isn't that equivalent to this?

yes, i just mean in the base flash -t is short for --target, but here they mean different things, -t is for partitions and --target is for the block device

it's not necessarily bad, just something to consider, maybe switching back to -p/--partition would make sense

@eballetbo

eballetbo commented Feb 10, 2026

Copy link
Copy Markdown
Author

For me was also confusing but I intentionally used -t to follow what is done in ridex4 flasher.

                    "This driver requires a target partition.\n"
                    "Usage: j storage flash --target <partition>:<file>\n"
                    "Example: j storage flash -t boot_a:aboot.img -t system_a:rootfs.simg -t system_b:qm_var.simg"

But looking again, --target in the ridex4 flasher has different meaning than --target in the generic flasher driver. So I don't think it makes sense to maintain the supposed coherency. I'm fine with use -p instead. Just thumbs up and I'll change.

@bennyz

bennyz commented Feb 10, 2026

Copy link
Copy Markdown
Member

For me was also confusing but I intentionally used -t to follow what is done in ridex4 flasher.

                    "This driver requires a target partition.\n"
                    "Usage: j storage flash --target <partition>:<file>\n"
                    "Example: j storage flash -t boot_a:aboot.img -t system_a:rootfs.simg -t system_b:qm_var.simg"

But looking again, --target in the ridex4 flasher has different meaning than --target in the generic flasher driver. So I don't think it makes sense to maintain the supposed coherency. I'm fine with use -p instead. Just thumbs up and I'll change.

no no, in my opinion it's fine, just something to be aware of, but i think as usage of OCI will increase these flags will not be used as much, so it's not a big deal

@mangelajo

Copy link
Copy Markdown
Member

opps, I was about to merge but conflict, let's see if I can resolve it.

@mangelajo

Copy link
Copy Markdown
Member

Ok, I was trying to resolve by my brain is fried at this point :) may be you guys can give it a try tomorrow, it's just clashing with the oci credentials PR

@bennyz

bennyz commented Feb 11, 2026

Copy link
Copy Markdown
Member

@mangelajo i don't mind reverting my PR, it needs an additional fix anyway

@eballetbo

Copy link
Copy Markdown
Author

I was trying to rebase my patches on top of HEAD this morning, but found some errors which I'm not sure are introduced by my rebase, looks like I need to normalize to str(...) for some string/URL operations, otherwise I fail to flash with my workflow. Is that the fix are you talking about? If that's the case I'll leave to you ;-)

@bennyz

bennyz commented Feb 11, 2026

Copy link
Copy Markdown
Member

@eballetbo can you share what you test, so i can make sure it works after rebasing?

@eballetbo

eballetbo commented Feb 11, 2026

Copy link
Copy Markdown
Author

I only do basic tests:

j storage flash https://download.autosd.sig.centos.org/AutoSD-10/nightly/EBBR/auto-osbuild-ebbr-autosd10-qa-regular-aarch64-2315811537.a97690ea.raw.xz

AND

j storage flash -t ukiboot_a:../../kernel-ark/arch/arm64/boot/uki.unsigned.efi

But after rebasing and fix some conflicts, I got this running the second command:

                                 |     if path.startswith("oci://") and oci_username:                                                                                 
                                 |        ^^^^^^^^^^^^^^^                                                                                                             
                                 | AttributeError: 'PosixPath' object has no attribute 'startswith'            

Which makes sense, because PosixPath has no the 'startswith' attribute, so we should probably normalize these kinds of paths to str(...)

…ge 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>
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
@bennyz

bennyz commented Feb 11, 2026

Copy link
Copy Markdown
Member

@eballetbo updated the PR, it seems to work on my end, please let me know if it's acceptable

@eballetbo

Copy link
Copy Markdown
Author

@bennyz LGTM

@bennyz bennyz merged commit e9c4c10 into jumpstarter-dev:main Feb 12, 2026
48 checks passed
This was referenced Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants