@@ -38,6 +38,7 @@ class FlashRetryableError(FlashError):
3838class FlashNonRetryableError (FlashError ):
3939 """Exception for non-retryable flash errors (configuration, file system, etc.)."""
4040
41+
4142debug_console_option = click .option ("--console-debug" , is_flag = True , help = "Enable console debug mode" )
4243
4344EXPECT_TIMEOUT_DEFAULT = 60
@@ -113,8 +114,11 @@ def flash( # noqa: C901
113114 image_url = ""
114115 original_http_url = None
115116 operator_scheme = None
116- # initrmafs cannot handle https yet, fallback to using the exporter's http server
117- if path .startswith (("http://" , "https://" )) and not force_exporter_http :
117+ if path .startswith ("oci://" ):
118+ # OCI URLs are always passed directly to fls
119+ image_url = path
120+ should_download_to_httpd = False
121+ elif path .startswith (("http://" , "https://" )) and not force_exporter_http :
118122 # the flasher image can handle the http(s) from a remote directly, unless target is isolated
119123 image_url = path
120124 should_download_to_httpd = False
@@ -172,9 +176,19 @@ def flash( # noqa: C901
172176 for attempt in range (retries + 1 ): # +1 for initial attempt
173177 try :
174178 self ._perform_flash_operation (
175- partition , path , image_url , should_download_to_httpd ,
176- storage_thread , error_queue , cacert_file , insecure_tls ,
177- headers , bearer_token , method , fls_version , fls_binary_url
179+ partition ,
180+ path ,
181+ image_url ,
182+ should_download_to_httpd ,
183+ storage_thread ,
184+ error_queue ,
185+ cacert_file ,
186+ insecure_tls ,
187+ headers ,
188+ bearer_token ,
189+ method ,
190+ fls_version ,
191+ fls_binary_url ,
178192 )
179193 self .logger .info (f"Flash operation succeeded on attempt { attempt + 1 } " )
180194 break
@@ -194,15 +208,14 @@ def flash( # noqa: C901
194208 )
195209 self .logger .info (f"Retrying flash operation (attempt { attempt + 2 } /{ retries + 1 } )" )
196210 # Wait a bit before retrying
197- time .sleep (2 ** attempt ) # Exponential backoff
211+ time .sleep (2 ** attempt ) # Exponential backoff
198212 continue
199213 else :
200214 self .logger .error (f"Flash operation failed after { retries + 1 } attempts" )
201215 raise FlashError (
202216 f"Flash operation failed after { retries + 1 } attempts. Last error: { categorized_error } "
203217 ) from e
204218
205-
206219 total_time = time .time () - start_time
207220 # total time in minutes:seconds
208221 minutes , seconds = divmod (total_time , 60 )
@@ -262,7 +275,7 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
262275 The found exception instance if found, None otherwise
263276 """
264277 # Check if this is an ExceptionGroup and look through its exceptions
265- if hasattr (exception , ' exceptions' ):
278+ if hasattr (exception , " exceptions" ):
266279 for sub_exc in exception .exceptions :
267280 result = self ._find_exception_in_chain (sub_exc , target_type )
268281 if result is not None :
@@ -273,17 +286,17 @@ def _find_exception_in_chain(self, exception: Exception, target_type: type) -> E
273286 return exception
274287
275288 # Check the cause chain
276- current = getattr (exception , ' __cause__' , None )
289+ current = getattr (exception , " __cause__" , None )
277290 while current is not None :
278291 if isinstance (current , target_type ):
279292 return current
280293 # Also check if the cause is an ExceptionGroup
281- if hasattr (current , ' exceptions' ):
294+ if hasattr (current , " exceptions" ):
282295 for sub_exc in current .exceptions :
283296 result = self ._find_exception_in_chain (sub_exc , target_type )
284297 if result is not None :
285298 return result
286- current = getattr (current , ' __cause__' , None )
299+ current = getattr (current , " __cause__" , None )
287300 return None
288301
289302 def _perform_flash_operation (
@@ -353,7 +366,6 @@ def _perform_flash_operation(
353366
354367 header_args = self ._prepare_headers (headers , bearer_token )
355368
356-
357369 if method == "fls" :
358370 self ._flash_with_fls (
359371 console ,
@@ -504,10 +516,7 @@ def _flash_with_fls(
504516 elif fls_version != "" :
505517 self .logger .info (f"Downloading FLS version { fls_version } from GitHub releases" )
506518 # Download fls binary to the target device (until it is available on the target device)
507- fls_url = (
508- f"https://github.com/jumpstarter-dev/fls/releases/download/{ fls_version } /"
509- f"fls-aarch64-linux"
510- )
519+ fls_url = f"https://github.com/jumpstarter-dev/fls/releases/download/{ fls_version } /fls-aarch64-linux"
511520 console .sendline (f"curl -L { fls_url } -o /sbin/fls" )
512521 console .expect (prompt , timeout = EXPECT_TIMEOUT_DEFAULT )
513522 console .sendline ("echo $?" )
@@ -547,7 +556,7 @@ def _monitor_fls_progress(self, console, prompt):
547556 if len (current_output ) > last_printed_length :
548557 new_output = current_output [last_printed_length :]
549558 if new_output :
550- print (new_output , end = '' , flush = True )
559+ print (new_output , end = "" , flush = True )
551560 last_printed_length = len (current_output )
552561
553562 # Check if we matched the prompt (index 0 means prompt matched)
@@ -556,7 +565,7 @@ def _monitor_fls_progress(self, console, prompt):
556565 break
557566 # If match_index is 1, it means TIMEOUT was matched, so we continue the loop
558567
559- if ' panicked at' in current_output :
568+ if " panicked at" in current_output :
560569 raise FlashRetryableError (f"FLS panicked: { current_output } " )
561570
562571 except pexpect .EOF as err :
@@ -610,8 +619,8 @@ def _flash_with_progress(
610619 flash_cmd = (
611620 f'( set -o pipefail; curl -fsSL { tls_args } { header_args } "{ image_url } " | '
612621 f"{ decompress_cmd } "
613- f' dd of={ target_path } bs=64k iflag=fullblock oflag=direct ' +
614- '&& echo "F""LASH_COMPLETE" || echo "F""LASH_FAILED" ) &'
622+ f" dd of={ target_path } bs=64k iflag=fullblock oflag=direct "
623+ + '&& echo "F""LASH_COMPLETE" || echo "F""LASH_FAILED" ) &'
615624 )
616625 console .sendline (flash_cmd )
617626 console .expect (prompt , timeout = EXPECT_TIMEOUT_DEFAULT * 2 )
@@ -629,7 +638,7 @@ def _monitor_flash_progress(self, console, prompt):
629638 console .sendline ("pidof dd" )
630639 console .expect (prompt , timeout = EXPECT_TIMEOUT_DEFAULT )
631640 pidof_output = console .before .decode (errors = "ignore" )
632- accumulated_output = pidof_output # just in case we get the FLASH_COMPLETE or FLASH_FAILED markers soon
641+ accumulated_output = pidof_output # just in case we get the FLASH_COMPLETE or FLASH_FAILED markers soon
633642
634643 # Extract the actual process ID from the output, handling potential error messages
635644 lines = pidof_output .splitlines ()
@@ -709,8 +718,8 @@ def _update_accumulated_output(self, accumulated_output, data):
709718 """Update accumulated output with new data, keeping only last 64KB."""
710719 accumulated_output += data
711720 # Keep only the last 64KB to prevent memory growth
712- if len (accumulated_output ) > 64 * 1024 :
713- accumulated_output = accumulated_output [- 64 * 1024 :]
721+ if len (accumulated_output ) > 64 * 1024 :
722+ accumulated_output = accumulated_output [- 64 * 1024 :]
714723 return accumulated_output
715724
716725 def _update_progress_stats (self , data , last_pos , last_time ):
@@ -943,7 +952,16 @@ def dump(
943952
944953 def _filename (self , path : PathBuf ) -> str :
945954 """Extract filename from url or path"""
946- if path .startswith (("http://" , "https://" )):
955+ if path .startswith ("oci://" ):
956+ oci_path = path [6 :] # Remove "oci://" prefix
957+ if ":" in oci_path :
958+ repository , tag = oci_path .rsplit (":" , 1 )
959+ repo_name = repository .split ("/" )[- 1 ] if "/" in repository else repository
960+ return f"{ repo_name } -{ tag } "
961+ else :
962+ repo_name = oci_path .split ("/" )[- 1 ] if "/" in oci_path else oci_path
963+ return repo_name
964+ elif path .startswith (("http://" , "https://" )):
947965 return urlparse (path ).path .split ("/" )[- 1 ]
948966 else :
949967 return Path (path ).name
@@ -1181,7 +1199,7 @@ def base():
11811199 @click .option (
11821200 "--fls-version" ,
11831201 type = str ,
1184- default = "0.1.9" , # TODO(majopela): set default to "" once fls is included in our images
1202+ default = "0.1.9" , # TODO(majopela): set default to "" once fls is included in our images
11851203 help = "Download an specific fls version from the github releases" ,
11861204 )
11871205 @click .option (
0 commit comments