Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ def base():

@base.command()
@click.argument("file")
@click.option("--partition", type=str)
@click.option("--target", type=str)
@click.option("--os-image-checksum", help="SHA256 checksum of OS image (direct value)")
@click.option(
"--os-image-checksum-file",
Expand All @@ -488,7 +488,7 @@ def base():
@debug_console_option
def flash(
file,
partition,
target,
os_image_checksum,
os_image_checksum_file,
console_debug,
Expand All @@ -504,7 +504,7 @@ def flash(
self.set_console_debug(console_debug)
self.flash(
file,
partition=partition,
partition=target,
force_exporter_http=force_exporter_http,
force_flash_bundle=force_flash_bundle,
)
Expand Down
132 changes: 103 additions & 29 deletions packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
from typing import cast
from urllib.parse import urlparse
from uuid import UUID

Expand Down Expand Up @@ -514,10 +515,10 @@ class FlasherClientInterface(metaclass=ABCMeta):
@abstractmethod
def flash(
self,
path: PathBuf,
path: PathBuf | dict[str, PathBuf],
*,
partition: str | None = None,
operator: Operator | None = None,
target: str | None = None,
operator: Operator | dict[str, Operator] | None = None,
compression: Compression | None = None,
):
"""Flash image to DUT"""
Expand All @@ -528,7 +529,7 @@ def dump(
self,
path: PathBuf,
*,
partition: str | None = None,
target: str | None = None,
operator: Operator | None = None,
compression: Compression | None = None,
):
Expand All @@ -542,45 +543,118 @@ def base():
pass

@base.command()
@click.argument("file")
@click.option("--partition", type=str)
@click.argument("file", nargs=-1, required=False)
@click.option(
"--target",
"-t",
"target_specs",
multiple=True,
help="name:file",
)
@click.option("--compression", type=click.Choice(Compression, case_sensitive=False))
def flash(file, partition, compression):
"""Flash image to DUT from file"""
self.flash(file, partition=partition, compression=compression)
def flash(file, target_specs, compression):
if target_specs:
mapping: dict[str, str] = {}
for spec in target_specs:
if ":" not in spec:
raise click.ClickException(f"Invalid target spec '{spec}', expected name:file")
name, img = spec.split(":", 1)
mapping[name] = img
self.flash(cast(dict[str, PathBuf], mapping), compression=compression)
return

if not file:
raise click.ClickException("FILE argument is required unless --target/-t is used")

self.flash(file[0], target=None, compression=compression)

@base.command()
@click.argument("file")
@click.option("--partition", type=str)
@click.option("--target", type=str)
@click.option("--compression", type=click.Choice(Compression, case_sensitive=False))
def dump(file, partition, compression):
def dump(file, target, compression):
"""Dump image from DUT to file"""
self.dump(file, partition=partition, compression=compression)
self.dump(file, target=target, compression=compression)

return base


class FlasherClient(FlasherClientInterface, DriverClient):
def flash(
def _should_upload_file(self, storage, filename: str, src_path: PathBuf, src_operator: Operator) -> bool:
"""Check if file should be uploaded by comparing existence and hash."""
if not storage.exists(filename):
return True

try:
import hashlib

m = hashlib.sha256()
with src_operator.open(src_path, "rb") as f:
while True:
data = f.read(size=65536)
if len(data) == 0:
break
m.update(data)
src_hash = m.hexdigest()

storage_hash = storage.hash(filename)

if storage_hash == src_hash:
return False
else:
return True
except Exception:
return True

def _flash_single(
self,
path: PathBuf,
image: PathBuf,
*,
partition: str | None = None,
operator: Operator | None = None,
compression: Compression | None = None,
target: str | None,
operator: Operator | None,
compression: Compression | None,
):
"""Flash image to DUT"""
if operator is None:
path, operator, _ = operator_for_path(path)
image, operator, _ = operator_for_path(image)

with OpendalAdapter(client=self, operator=operator, path=path, mode="rb", compression=compression) as handle:
return self.call("flash", handle, partition)
with OpendalAdapter(client=self, operator=operator, path=image, mode="rb", compression=compression) as handle:
return self.call("flash", handle, target)

def flash(
self,
path: PathBuf | dict[str, PathBuf],
*,
target: str | None = None,
operator: Operator | dict[str, Operator] | None = None,
compression: Compression | None = None,
):
if isinstance(path, dict):
if target is not None:
raise ArgumentError("'target' parameter is not valid when flashing multiple images")

results: dict[str, object] = {}

oper_map = operator if isinstance(operator, dict) else {}

for part, img in path.items():
op_val = oper_map.get(part) if isinstance(operator, dict) else operator
results[part] = self._flash_single(
img, target=part, operator=cast(Operator | None, op_val), compression=compression
)

return results

if isinstance(operator, dict):
raise ArgumentError("operator mapping provided for single image flash")

return self._flash_single(path, target=target, operator=operator, compression=compression)

def dump(
self,
path: PathBuf,
*,
partition: str | None = None,
target: str | None = None,
operator: Operator | None = None,
compression: Compression | None = None,
):
Expand All @@ -589,7 +663,7 @@ def dump(
path, operator, _ = operator_for_path(path)

with OpendalAdapter(client=self, operator=operator, path=path, mode="wb", compression=compression) as handle:
return self.call("dump", handle, partition)
return self.call("dump", handle, target)


class StorageMuxClient(DriverClient):
Expand Down Expand Up @@ -648,7 +722,7 @@ def off():
"""Disconnect storage"""
self.off()

@base.command()
@base.command
@click.argument("file")
def write_local_file(file):
self.write_local_file(file)
Expand All @@ -661,13 +735,13 @@ def flash(
self,
path: PathBuf,
*,
partition: str | None = None,
target: str | None = None,
operator: Operator | None = None,
compression: Compression | None = None,
):
"""Flash image to DUT"""
if partition is not None:
raise ArgumentError(f"partition is not supported for StorageMuxFlasherClient, {partition} provided")
if target is not None:
raise ArgumentError(f"target is not supported for StorageMuxFlasherClient, {target} provided")

self.host()

Expand All @@ -684,13 +758,13 @@ def dump(
self,
path: PathBuf,
*,
partition: str | None = None,
target: str | None = None,
operator: Operator | None = None,
compression: Compression | None = None,
):
"""Dump image from DUT"""
if partition is not None:
raise ArgumentError(f"partition is not supported for StorageMuxFlasherClient, {partition} provided")
if target is not None:
raise ArgumentError(f"target is not supported for StorageMuxFlasherClient, {target} provided")

self.call("host")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def client(cls) -> str:
return "jumpstarter_driver_opendal.client.FlasherClient"

@abstractmethod
def flash(self, source, partition: str | None = None): ...
def flash(self, source, target: str | None = None): ...

@abstractmethod
def dump(self, target, partition: str | None = None): ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ def test_driver_opendal_presign(tmp_path):
)


@pytest.mark.parametrize("partition", [None, "uboot"])
def test_driver_flasher(tmp_path, partition):
@pytest.mark.parametrize("target", [None, "uboot"])
def test_driver_flasher(tmp_path, target):
with serve(MockFlasher()) as flasher:
(tmp_path / "disk.img").write_bytes(b"hello")

flasher.flash(tmp_path / "disk.img", partition=partition)
flasher.dump(tmp_path / "dump.img", partition=partition)
flasher.flash(tmp_path / "disk.img", target=target)
flasher.dump(tmp_path / "dump.img", target=target)

assert (tmp_path / "dump.img").read_bytes() == b"hello"

Expand Down
Loading
Loading