Skip to content

Commit 9344a3b

Browse files
committed
add --no-power-off to flashers
Sometimes we would not want to power off the device after flashing Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 5bfa067 commit 9344a3b

2 files changed

Lines changed: 61 additions & 21 deletions

File tree

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

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def flash( # noqa: C901
129129
fls_binary_url: str | None = None,
130130
oci_username: str | None = None,
131131
oci_password: str | None = None,
132+
power_off: bool = True,
132133
):
133134
"""Flash image to DUT"""
134135
if bearer_token:
@@ -225,6 +226,7 @@ def flash( # noqa: C901
225226
fls_binary_url,
226227
oci_username,
227228
oci_password,
229+
power_off,
228230
)
229231
self.logger.info(f"Flash operation succeeded on attempt {attempt + 1}")
230232
break
@@ -429,6 +431,7 @@ def _perform_flash_operation(
429431
fls_binary_url: str | None,
430432
oci_username: str | None,
431433
oci_password: str | None,
434+
power_off: bool = True,
432435
):
433436
"""Perform the actual flash operation with console setup.
434437
@@ -507,8 +510,11 @@ def _perform_flash_operation(
507510

508511
console.sendline("reboot")
509512
time.sleep(2)
510-
self.logger.info("Powering off target")
511-
self.power.off()
513+
if power_off:
514+
self.logger.info("Powering off target")
515+
self.power.off()
516+
else:
517+
self.logger.info("Leaving target powered on (--no-power-off)")
512518

513519
def _setup_flasher_ssl(self, console, manifest, cacert_file: str | None) -> str | None:
514520
"""Setup SSL configuration for the flasher.
@@ -1512,6 +1518,14 @@ def base():
15121518
type=str,
15131519
help="Custom URL to download FLS binary from (overrides --fls-version)",
15141520
)
1521+
@click.option(
1522+
"--no-power-off",
1523+
"power_off",
1524+
is_flag=True,
1525+
flag_value=False,
1526+
default=True,
1527+
help="Leave device powered on after flashing",
1528+
)
15151529
@debug_console_option
15161530
def flash(
15171531
file,
@@ -1530,6 +1544,7 @@ def flash(
15301544
method,
15311545
fls_version,
15321546
fls_binary_url,
1547+
power_off,
15331548
):
15341549
"""Flash image(s) to DUT
15351550
@@ -1574,6 +1589,8 @@ def flash(
15741589

15751590
self.logger.info(f"Flashing {op_num} {op_desc} with '{image_file}'".strip())
15761591

1592+
is_last = idx == len(flash_operations) - 1
1593+
15771594
# Perform the flash operation
15781595
self.flash(
15791596
image_file,
@@ -1590,6 +1607,7 @@ def flash(
15901607
method=method,
15911608
fls_version=fls_version,
15921609
fls_binary_url=fls_binary_url,
1610+
power_off=power_off if is_last else True,
15931611
)
15941612

15951613
@base.command()

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

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,18 @@ def _validate_partition_mappings(self, partitions: Dict[str, str] | None) -> Non
112112
f"Please provide a valid file path (e.g., -t {partition_name}:/path/to/image)"
113113
)
114114

115-
def _power_off_if_available(self) -> None:
116-
"""Power off device if power child is present."""
115+
def _power_off_if_available(self, power_off: bool = True) -> None:
116+
"""Power off device if power child is present and power_off is True."""
117+
if not power_off:
118+
self.logger.info("leaving device powered on")
119+
return
117120
if "power" in self.children:
118121
self.power.off()
119122
self.logger.info("device powered off")
120123
else:
121124
self.logger.info("device left running")
122125

123-
def _execute_flash_operation(self, operation_func, *args, **kwargs):
126+
def _execute_flash_operation(self, operation_func, *args, power_off: bool = True, **kwargs):
124127
"""Common wrapper for flash operations with logging and power management."""
125128
self.logger.info("Starting RideSX flash operation")
126129
self.boot_to_fastboot()
@@ -145,7 +148,7 @@ def _execute_flash_operation(self, operation_func, *args, **kwargs):
145148
)
146149

147150
try:
148-
self._power_off_if_available()
151+
self._power_off_if_available(power_off)
149152
except Exception as power_error:
150153
self.logger.exception("power-off cleanup failed after flash operation error: %s", power_error)
151154

@@ -154,7 +157,7 @@ def _execute_flash_operation(self, operation_func, *args, **kwargs):
154157
raise
155158

156159
try:
157-
self._power_off_if_available()
160+
self._power_off_if_available(power_off)
158161
except Exception as power_error:
159162
# Keep successful flashes successful, but make cleanup failures visible.
160163
self.logger.exception("power-off cleanup failed after successful flash operation: %s", power_error)
@@ -168,6 +171,7 @@ def flash(
168171
target: str | None = None,
169172
operator: Operator | Dict[str, Operator] | None = None,
170173
compression=None,
174+
power_off: bool = True,
171175
):
172176
"""Flash image to DUT - supports both OCI and traditional paths.
173177
@@ -176,23 +180,24 @@ def flash(
176180
target: Target partition (for single file mode)
177181
operator: Optional operator for file access (usually auto-detected)
178182
compression: Compression type
183+
power_off: Whether to power off the device after flashing (default: True)
179184
"""
180185
# Auto-detect flash mode based on path type
181186
if isinstance(path, dict):
182187
# Dictionary mode: {partition: file_path, ...}
183188
operators_dict = operator if isinstance(operator, dict) else None
184-
return self.flash_local(path, operators_dict)
189+
return self.flash_local(path, operators_dict, power_off=power_off)
185190

186191
elif isinstance(path, str) and (path.startswith("oci://") or self._is_oci_path(path)):
187192
# OCI mode: auto-detect partitions or use target as partition->filename mapping
188193
if target and ":" in target:
189194
# Target is "partition:filename" format for OCI explicit mapping
190195
partition_name, filename = target.split(":", 1)
191196
partitions = {partition_name: filename}
192-
return self.flash_with_targets(path, partitions)
197+
return self.flash_with_targets(path, partitions, power_off=power_off)
193198
else:
194199
# OCI auto-detection mode
195-
return self.flash_oci_auto(path, None)
200+
return self.flash_oci_auto(path, None, power_off=power_off)
196201

197202
else:
198203
# Traditional single file mode
@@ -211,18 +216,21 @@ def flash(
211216
operators = None
212217

213218
partitions = {target: path}
214-
return self.flash_local(partitions, operators)
219+
return self.flash_local(partitions, operators, power_off=power_off)
215220

216221
def flash_with_targets(
217222
self,
218223
oci_url: str,
219224
partitions: Dict[str, str],
225+
*,
226+
power_off: bool = True,
220227
):
221228
"""Flash OCI image with explicit partition mappings.
222229
223230
Args:
224231
oci_url: OCI image URL (must start with oci://)
225232
partitions: Mapping of partition name -> filename in OCI image
233+
power_off: Whether to power off the device after flashing (default: True)
226234
227235
Raises:
228236
ValueError: If partitions is empty or None
@@ -239,18 +247,21 @@ def flash_with_targets(
239247
def _flash_operation():
240248
return self._flash_oci_auto_impl(oci_url, partitions)
241249

242-
return self._execute_flash_operation(_flash_operation)
250+
return self._execute_flash_operation(_flash_operation, power_off=power_off)
243251

244252
def flash_local(
245253
self,
246254
partitions: Dict[str, str],
247255
operators: Dict[str, Operator] | None = None,
256+
*,
257+
power_off: bool = True,
248258
):
249259
"""Flash local files or URLs to partitions.
250260
251261
Args:
252262
partitions: Mapping of partition name -> file path or URL
253263
operators: Optional mapping of partition name -> operator
264+
power_off: Whether to power off the device after flashing (default: True)
254265
"""
255266
self._validate_partition_mappings(partitions)
256267

@@ -259,7 +270,7 @@ def flash_local(
259270
def _flash_operation():
260271
return self.flash_images(partitions, operators)
261272

262-
return self._execute_flash_operation(_flash_operation)
273+
return self._execute_flash_operation(_flash_operation, power_off=power_off)
263274

264275
def _read_oci_credentials(self):
265276
"""Read OCI registry credentials from environment variables.
@@ -316,12 +327,15 @@ def flash_oci_auto(
316327
self,
317328
oci_url: str,
318329
partitions: Dict[str, str] | None = None,
330+
*,
331+
power_off: bool = True,
319332
):
320333
"""Flash OCI image using auto-detection or explicit partition mapping
321334
322335
Args:
323336
oci_url: OCI image reference (e.g., "oci://registry.com/image:latest")
324337
partitions: Optional mapping of partition -> filename inside OCI image
338+
power_off: Whether to power off the device after flashing (default: True)
325339
"""
326340
# Normalize OCI URL
327341
if not oci_url.startswith("oci://"):
@@ -340,7 +354,7 @@ def flash_oci_auto(
340354
def _flash_operation():
341355
return self._flash_oci_auto_impl(oci_url, partitions)
342356

343-
return self._execute_flash_operation(_flash_operation)
357+
return self._execute_flash_operation(_flash_operation, power_off=power_off)
344358

345359
def _parse_target_specs(self, target_specs: tuple[str, ...]) -> dict[str, str]:
346360
"""Parse -t target specs into a partition->path mapping."""
@@ -373,7 +387,7 @@ def _parse_and_validate_targets(self, target_specs: tuple[str, ...]):
373387

374388
return mapping, single_target
375389

376-
def _execute_flash_command(self, path, target_specs):
390+
def _execute_flash_command(self, path, target_specs, power_off: bool = True):
377391
"""Execute flash command logic with proper argument handling."""
378392
# Parse target specifications
379393
if target_specs:
@@ -382,18 +396,18 @@ def _execute_flash_command(self, path, target_specs):
382396
if mapping:
383397
if path:
384398
# Multi-partition mode with path: extract specific files from OCI image
385-
self.flash_with_targets(path, mapping)
399+
self.flash_with_targets(path, mapping, power_off=power_off)
386400
else:
387401
# Multi-partition mode: use mapping as dict for local files
388-
self.flash(mapping)
402+
self.flash(mapping, power_off=power_off)
389403
else:
390404
# Single partition mode: use path with target
391405
if not path:
392406
raise click.ClickException("Path argument required when using single-partition target")
393-
self.flash(path, target=single_target)
407+
self.flash(path, target=single_target, power_off=power_off)
394408
elif path:
395409
# Path only - should be OCI for auto-detection
396-
self.flash(path)
410+
self.flash(path, power_off=power_off)
397411
else:
398412
raise click.ClickException("Provide a path or use -t to specify partition mappings")
399413

@@ -419,7 +433,15 @@ def base():
419433
multiple=True,
420434
help="Target spec as partition:path for multi-partition or just partition for single file",
421435
)
422-
def flash(path, target_specs):
436+
@click.option(
437+
"--no-power-off",
438+
"power_off",
439+
is_flag=True,
440+
flag_value=False,
441+
default=True,
442+
help="Leave device powered on after flashing",
443+
)
444+
def flash(path, target_specs, power_off):
423445
"""Flash image to device.
424446
425447
\b
@@ -447,7 +469,7 @@ def flash(path, target_specs):
447469
OCI_USERNAME Registry username for private OCI images
448470
OCI_PASSWORD Registry password for private OCI images
449471
"""
450-
self._execute_flash_command(path, target_specs)
472+
self._execute_flash_command(path, target_specs, power_off=power_off)
451473

452474
@base.command()
453475
def boot_to_fastboot():

0 commit comments

Comments
 (0)