33//! Create a merged filesystem tree with the image and mounted configmaps.
44
55use std:: collections:: HashSet ;
6+ use std:: fs:: create_dir_all;
67use std:: io:: { BufRead , Write } ;
8+ use std:: path:: PathBuf ;
79
810use anyhow:: Ok ;
911use anyhow:: { anyhow, Context , Result } ;
@@ -21,13 +23,17 @@ use ostree_ext::ostree::{self, Sysroot};
2123use ostree_ext:: sysroot:: SysrootLock ;
2224use ostree_ext:: tokio_util:: spawn_blocking_cancellable_flatten;
2325
26+ use crate :: bls_config:: { parse_bls_config, BLSConfig } ;
27+ use crate :: install:: { get_efi_uuid_source, get_user_config, BootType } ;
2428use crate :: progress_jsonl:: { Event , ProgressWriter , SubTaskBytes , SubTaskStep } ;
2529use crate :: spec:: ImageReference ;
26- use crate :: spec:: { BootOrder , HostSpec } ;
27- use crate :: status:: labels_of_config;
30+ use crate :: spec:: { BootOrder , HostSpec , BootEntry } ;
31+ use crate :: status:: { composefs_deployment_status , labels_of_config} ;
2832use crate :: store:: Storage ;
2933use crate :: utils:: async_task_with_spinner;
3034
35+ use openat_ext:: OpenatDirExt ;
36+
3137// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
3238const BASE_IMAGE_PREFIX : & str = "ostree/container/baseimage/bootc" ;
3339
@@ -740,6 +746,165 @@ pub(crate) async fn stage(
740746 Ok ( ( ) )
741747}
742748
749+
750+ #[ context( "Rolling back UKI" ) ]
751+ pub ( crate ) fn rollback_composefs_uki ( current : & BootEntry , rollback : & BootEntry ) -> Result < ( ) > {
752+ let user_cfg_name = "grub2/user.cfg.staged" ;
753+ let user_cfg_path = PathBuf :: from ( "/sysroot/boot" ) . join ( user_cfg_name) ;
754+
755+ let efi_uuid_source = get_efi_uuid_source ( ) ;
756+
757+ // TODO: Need to check if user.cfg.staged exists
758+ let mut usr_cfg = std:: fs:: OpenOptions :: new ( )
759+ . write ( true )
760+ . create ( true )
761+ . truncate ( true )
762+ . open ( user_cfg_path)
763+ . with_context ( || format ! ( "Opening {user_cfg_name}" ) ) ?;
764+
765+ usr_cfg. write ( efi_uuid_source. as_bytes ( ) ) ?;
766+
767+ let verity = if let Some ( composefs) = & rollback. composefs {
768+ composefs. verity . clone ( )
769+ } else {
770+ // Shouldn't really happen
771+ anyhow:: bail!( "Verity not found for rollback deployment" )
772+ } ;
773+ usr_cfg. write ( get_user_config ( & verity) . as_bytes ( ) ) ?;
774+
775+ let verity = if let Some ( composefs) = & current. composefs {
776+ composefs. verity . clone ( )
777+ } else {
778+ // Shouldn't really happen
779+ anyhow:: bail!( "Verity not found for booted deployment" )
780+ } ;
781+ usr_cfg. write ( get_user_config ( & verity) . as_bytes ( ) ) ?;
782+
783+ Ok ( ( ) )
784+ }
785+
786+ /// Filename for `loader/entries`
787+ const CURRENT_ENTRIES : & str = "entries" ;
788+ const ROLLBACK_ENTRIES : & str = "entries.staged" ;
789+
790+ #[ context( "Getting boot entries" ) ]
791+ pub ( crate ) fn get_sorted_boot_entries ( ascending : bool ) -> Result < Vec < BLSConfig > > {
792+ let mut all_configs = vec ! [ ] ;
793+
794+ for entry in std:: fs:: read_dir ( format ! ( "/sysroot/boot/loader/{CURRENT_ENTRIES}" ) ) ? {
795+ let entry = entry?;
796+
797+ let file_name = entry. file_name ( ) ;
798+
799+ let file_name = file_name
800+ . to_str ( )
801+ . ok_or ( anyhow:: anyhow!( "Found non UTF-8 characters in filename" ) ) ?;
802+
803+ if !file_name. ends_with ( ".conf" ) {
804+ continue ;
805+ }
806+
807+ let contents = std:: fs:: read_to_string ( & entry. path ( ) )
808+ . with_context ( || format ! ( "Failed to read {:?}" , entry. path( ) ) ) ?;
809+
810+ let config = parse_bls_config ( & contents) . context ( "Parsing bls config" ) ?;
811+
812+ all_configs. push ( config) ;
813+ }
814+
815+ all_configs. sort_by ( |a, b| if ascending { a. cmp ( b) } else { b. cmp ( a) } ) ;
816+
817+ return Ok ( all_configs) ;
818+ }
819+
820+ #[ context( "Rolling back BLS" ) ]
821+ pub ( crate ) fn rollback_composefs_bls ( ) -> Result < ( ) > {
822+ // Sort in descending order as that's the order they're shown on the boot screen
823+ // After this:
824+ // all_configs[0] -> booted depl
825+ // all_configs[1] -> rollback depl
826+ let mut all_configs = get_sorted_boot_entries ( false ) ?;
827+
828+ // Update the indicies so that they're swapped
829+ for ( idx, cfg) in all_configs. iter_mut ( ) . enumerate ( ) {
830+ cfg. version = idx as u32 ;
831+ }
832+
833+ assert ! ( all_configs. len( ) == 2 ) ;
834+
835+ // Write these
836+ let dir_path = PathBuf :: from ( format ! ( "/sysroot/boot/loader/{ROLLBACK_ENTRIES}" ) ) ;
837+ create_dir_all ( & dir_path) . with_context ( || format ! ( "Failed to create dir: {dir_path:?}" ) ) ?;
838+
839+ // Write the BLS configs in there
840+ for cfg in all_configs {
841+ let file_name = format ! ( "bootc-composefs-{}.conf" , cfg. version) ;
842+
843+ let mut file = std:: fs:: OpenOptions :: new ( )
844+ . create ( true )
845+ . write ( true )
846+ . open ( dir_path. join ( & file_name) )
847+ . with_context ( || format ! ( "Opening {file_name}" ) ) ?;
848+
849+ file. write_all ( cfg. to_string ( ) . as_bytes ( ) )
850+ . with_context ( || format ! ( "Writing to {file_name}" ) ) ?;
851+ }
852+
853+ // Atomically exchange "entries" <-> "entries.rollback"
854+ let dir = openat:: Dir :: open ( "/sysroot/boot/loader" ) . context ( "Opening loader dir" ) ?;
855+
856+ tracing:: debug!( "Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}" ) ;
857+ dir. local_exchange ( ROLLBACK_ENTRIES , CURRENT_ENTRIES )
858+ . context ( "local exchange" ) ?;
859+
860+ tracing:: debug!( "Removing {ROLLBACK_ENTRIES}" ) ;
861+ dir. remove_all ( ROLLBACK_ENTRIES )
862+ . context ( "Removing entries.rollback" ) ?;
863+
864+ tracing:: debug!( "Syncing to disk" ) ;
865+ dir. syncfs ( ) . context ( "syncfs" ) ?;
866+
867+ Ok ( ( ) )
868+ }
869+
870+ #[ context( "Rolling back composefs" ) ]
871+ pub ( crate ) async fn composefs_rollback ( ) -> Result < ( ) > {
872+ let host = composefs_deployment_status ( ) . await ?;
873+
874+ let new_spec = {
875+ let mut new_spec = host. spec . clone ( ) ;
876+ new_spec. boot_order = new_spec. boot_order . swap ( ) ;
877+ new_spec
878+ } ;
879+
880+ // Just to be sure
881+ host. spec . verify_transition ( & new_spec) ?;
882+
883+ let reverting = new_spec. boot_order == BootOrder :: Default ;
884+ if reverting {
885+ println ! ( "notice: Reverting queued rollback state" ) ;
886+ }
887+
888+ let rollback_status = host
889+ . status
890+ . rollback
891+ . ok_or_else ( || anyhow ! ( "No rollback available" ) ) ?;
892+
893+ // TODO: Handle staged deployment
894+ // Ostree will drop any staged deployment on rollback but will keep it if it is the first item
895+ // in the new deployment list
896+ let Some ( rollback_composefs_entry) = & rollback_status. composefs else {
897+ anyhow:: bail!( "Rollback deployment not a composefs deployment" )
898+ } ;
899+
900+ match rollback_composefs_entry. boot_type {
901+ BootType :: Bls => rollback_composefs_bls ( ) ,
902+ BootType :: Uki => rollback_composefs_uki ( & host. status . booted . unwrap ( ) , & rollback_status) ,
903+ } ?;
904+
905+ Ok ( ( ) )
906+ }
907+
743908/// Implementation of rollback functionality
744909pub ( crate ) async fn rollback ( sysroot : & Storage ) -> Result < ( ) > {
745910 const ROLLBACK_JOURNAL_ID : & str = "26f3b1eb24464d12aa5e7b544a6b5468" ;
0 commit comments