Skip to content

Commit 0422bf8

Browse files
committed
ridesx: support OCI flashing
by leveraging FLS, we introduce the option to do `j storage flash oci://...` for ridesx targets. Currently, this will extract the OCI archive to the exporter host and proceed to flash with fastboot CLI. This also includes the option to specify which files to use by using -t/--target in combination with oci:// For example: ``` j storage flash -t boot_a:boot_a.simg system_a:system_a.simg oci://quay.io/bzlotnik/ridesx:latest ``` Otherwise it would rely on annotations to figure out the partitions from caib annotations: "automotive.sdv.cloud.redhat.com/partition" Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent c1e4237 commit 0422bf8

2 files changed

Lines changed: 319 additions & 22 deletions

File tree

python/packages/jumpstarter-driver-ridesx/jumpstarter_driver_ridesx/client.py

Lines changed: 195 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,60 +77,236 @@ def flash_images(self, partitions: Dict[str, str], operators: Optional[Dict[str,
7777

7878
return flash_result
7979

80-
def flash(
80+
def _oci_url_from_argv(self) -> str | None:
81+
"""Return first OCI-like URL from sys.argv, or None."""
82+
import sys
83+
84+
for arg in sys.argv[1:]:
85+
if self._is_oci_path(arg):
86+
return arg
87+
return None
88+
89+
def _target_mappings_from_argv(self) -> Dict[str, str] | None:
90+
"""Return target mappings from sys.argv, if present."""
91+
import sys
92+
93+
mapping: Dict[str, str] = {}
94+
args = sys.argv[1:]
95+
i = 0
96+
while i < len(args):
97+
arg = args[i]
98+
spec = None
99+
if arg in ("-t", "--target"):
100+
if i + 1 < len(args):
101+
spec = args[i + 1]
102+
i += 1
103+
elif arg.startswith("--target="):
104+
spec = arg.split("=", 1)[1]
105+
106+
if spec and ":" in spec:
107+
name, img = spec.split(":", 1)
108+
mapping[name] = img
109+
i += 1
110+
111+
return mapping or None
112+
113+
def _is_oci_path(self, path: str) -> bool:
114+
"""Return True if path looks like an OCI image reference."""
115+
return path.startswith(("oci://", "docker://")) or (":" in path and "/" in path and not path.startswith("/"))
116+
117+
def _resolve_flash_input(
81118
self,
82119
path: str | Dict[str, str],
83-
*,
84-
target: str | None = None,
85-
operator: Operator | Dict[str, Operator] | None = None,
86-
compression=None,
87-
):
120+
target: str | None,
121+
operator: Operator | Dict[str, Operator] | None,
122+
oci_url_from_argv: str | None,
123+
) -> tuple[Dict[str, str] | None, Dict[str, Operator] | None, str | None]:
124+
"""Resolve path/target/operator into (partitions, operators, oci_url)."""
88125
if isinstance(path, dict):
89-
partitions = path
90-
operators = operator if isinstance(operator, dict) else None
126+
return path, operator if isinstance(operator, dict) else None, oci_url_from_argv
127+
128+
if self._is_oci_path(path):
129+
oci_url = path
130+
if target is None:
131+
print("Auto-detecting partitions from OCI image annotations...")
132+
return None, None, oci_url
133+
if ":" not in target:
134+
raise ValueError(f"Target must be in format 'partition:filename' for OCI flashing, got: {target}")
135+
partition_name, filename = target.split(":", 1)
136+
partitions = {partition_name: filename}
91137
else:
92138
if target is None:
93139
raise ValueError(
94140
"This driver requires a target partition.\n"
95-
"Usage: j storage flash --target <partition>:<file>\n"
96-
"Example: j storage flash -t boot_a:aboot.img -t system_a:rootfs.simg -t system_b:qm_var.simg"
141+
"Usage: j storage flash --target <partition>:<file> [path]\n"
142+
"For OCI images: j storage flash [oci://image] (auto-detects partitions)\n"
143+
"For OCI with explicit mapping: j storage flash -t boot_a:boot.simg oci://image\n"
144+
"For local files: j storage flash -t boot_a:/path/to/boot.img"
97145
)
98146
partitions = {target: path}
99-
operators = {target: operator} if isinstance(operator, Operator) else None
147+
oci_url = oci_url_from_argv
148+
149+
operators = {list(partitions.keys())[0]: operator} if partitions and isinstance(operator, Operator) else None
150+
return partitions, operators, oci_url
100151

152+
def _validate_partition_mappings(self, partitions: Dict[str, str] | None) -> None:
153+
"""Validate partition mappings; raise ValueError if any path is empty."""
154+
if partitions is None:
155+
return
101156
for partition_name, file_path in partitions.items():
102157
if not file_path or not file_path.strip():
103158
raise ValueError(
104159
f"Partition '{partition_name}' has an empty file path. "
105160
f"Please provide a valid file path (e.g., -t {partition_name}:/path/to/image)"
106161
)
107162

108-
self.logger.info("Starting RideSX flash operation")
163+
def _power_off_if_available(self) -> None:
164+
"""Power off device if power child is present."""
165+
if "power" in self.children:
166+
self.power.off()
167+
self.logger.info("device powered off")
168+
else:
169+
self.logger.info("device left running")
109170

171+
def flash(
172+
self,
173+
path: str | Dict[str, str],
174+
*,
175+
target: str | None = None,
176+
operator: Operator | Dict[str, Operator] | None = None,
177+
compression=None,
178+
fls_version: str | None = None,
179+
fls_binary_url: str | None = None,
180+
):
181+
oci_url_from_argv = self._oci_url_from_argv()
182+
if oci_url_from_argv:
183+
self.logger.info(f"Detected OCI URL from command line: {oci_url_from_argv}")
184+
185+
partitions, operators, oci_url = self._resolve_flash_input(path, target, operator, oci_url_from_argv)
186+
if oci_url and partitions is None:
187+
argv_targets = self._target_mappings_from_argv()
188+
if argv_targets:
189+
self.logger.info("Using target mappings from command line arguments")
190+
partitions = argv_targets
191+
self._validate_partition_mappings(partitions)
192+
193+
self.logger.info("Starting RideSX flash operation")
110194
self.boot_to_fastboot()
111195

112-
result = self.flash_images(partitions, operators)
196+
if oci_url:
197+
if partitions is None:
198+
self.logger.info(f"Using FLS auto-detection for OCI image: {oci_url}")
199+
else:
200+
self.logger.info(f"Using FLS OCI flash with explicit mapping for image: {oci_url}")
201+
result = self.flash_oci_auto(oci_url, partitions, fls_version=fls_version, fls_binary_url=fls_binary_url)
202+
else:
203+
self.logger.info("Using traditional file-based flashing")
204+
result = self.flash_images(partitions, operators)
113205

114206
self.logger.info("flash operation completed successfully")
207+
self._power_off_if_available()
208+
return result
115209

116-
if "power" in self.children:
117-
self.power.off()
118-
self.logger.info("device powered off")
210+
def flash_oci_auto(
211+
self,
212+
oci_url: str,
213+
partitions: Dict[str, str] | None = None,
214+
fls_version: str | None = None,
215+
fls_binary_url: str | None = None,
216+
):
217+
"""Flash OCI image using auto-detection or explicit partition mapping
218+
219+
Args:
220+
oci_url: OCI image reference (e.g., "oci://registry.com/image:latest")
221+
partitions: Optional mapping of partition -> filename inside OCI image
222+
fls_version: Optional FLS version to download from GitHub releases
223+
fls_binary_url: Optional custom URL to download FLS binary from
224+
"""
225+
# Normalize OCI URL
226+
if not oci_url.startswith(("oci://", "docker://")):
227+
if ":" in oci_url and "/" in oci_url:
228+
oci_url = f"oci://{oci_url}"
229+
else:
230+
raise ValueError(f"Invalid OCI URL format: {oci_url}")
231+
232+
if partitions:
233+
self.logger.info(f"Flashing OCI image with explicit mapping: {list(partitions.keys())}")
119234
else:
120-
self.logger.info("device left running")
235+
self.logger.info(f"Auto-detecting partitions for OCI image: {oci_url}")
121236

122-
return result
237+
self.logger.info("Checking for fastboot devices on Exporter...")
238+
detection_result = self.call("detect_fastboot_device", 5, 2.0)
239+
240+
if detection_result["status"] != "device_found":
241+
raise RuntimeError("No fastboot devices found. Make sure device is in fastboot mode.")
242+
243+
device_id = detection_result["device_id"]
244+
self.logger.info(f"Found fastboot device: {device_id}")
245+
246+
flash_result = self.call("flash_oci_image", oci_url, partitions, fls_version, fls_binary_url)
247+
248+
return flash_result
123249

124250
def cli(self):
251+
import click
252+
125253
generic_cli = FlasherClient.cli(self)
126254

127255
@driver_click_group(self)
128256
def base():
129257
"""RideSX storage operations"""
130258
pass
131259

260+
# Add all generic commands except 'flash' (we override it)
132261
for name, cmd in generic_cli.commands.items():
133-
base.add_command(cmd, name=name)
262+
if name != "flash":
263+
base.add_command(cmd, name=name)
264+
265+
@base.command()
266+
@click.argument("file")
267+
@click.option(
268+
"-t",
269+
"--target",
270+
"target_specs",
271+
multiple=True,
272+
help="Partition mapping as partition:filename (e.g., boot_a:boot.img)",
273+
)
274+
@click.option(
275+
"--fls-version",
276+
type=str,
277+
default=None,
278+
help="FLS version to download from GitHub releases (e.g., 0.1.9)",
279+
)
280+
@click.option(
281+
"--fls-binary-url",
282+
type=str,
283+
default=None,
284+
help="Custom URL to download FLS binary from (overrides --fls-version)",
285+
)
286+
def flash(file, target_specs, fls_version, fls_binary_url):
287+
"""Flash image to device
288+
289+
For OCI images with auto-detection:
290+
j storage flash oci://registry.com/image:tag
291+
292+
For OCI images with explicit partition mapping:
293+
j storage flash -t boot_a:boot.img oci://registry.com/image:tag
294+
295+
For local files:
296+
j storage flash -t boot_a:/path/to/boot.img .
297+
"""
298+
if target_specs:
299+
mapping: dict[str, str] = {}
300+
for spec in target_specs:
301+
if ":" not in spec:
302+
raise click.ClickException(f"Invalid target spec '{spec}'. Expected format: partition:filename")
303+
name, img = spec.split(":", 1)
304+
mapping[name] = img
305+
306+
self.flash(mapping, fls_version=fls_version, fls_binary_url=fls_binary_url)
307+
return
308+
309+
self.flash(file, target=None, fls_version=fls_version, fls_binary_url=fls_binary_url)
134310

135311
@base.command()
136312
def boot_to_fastboot():

0 commit comments

Comments
 (0)