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
@@ -747,6 +753,165 @@ pub(crate) async fn stage(
747753 Ok ( ( ) )
748754}
749755
756+
757+ #[ context( "Rolling back UKI" ) ]
758+ pub ( crate ) fn rollback_composefs_uki ( current : & BootEntry , rollback : & BootEntry ) -> Result < ( ) > {
759+ let user_cfg_name = "grub2/user.cfg.staged" ;
760+ let user_cfg_path = PathBuf :: from ( "/sysroot/boot" ) . join ( user_cfg_name) ;
761+
762+ let efi_uuid_source = get_efi_uuid_source ( ) ;
763+
764+ // TODO: Need to check if user.cfg.staged exists
765+ let mut usr_cfg = std:: fs:: OpenOptions :: new ( )
766+ . write ( true )
767+ . create ( true )
768+ . truncate ( true )
769+ . open ( user_cfg_path)
770+ . with_context ( || format ! ( "Opening {user_cfg_name}" ) ) ?;
771+
772+ usr_cfg. write ( efi_uuid_source. as_bytes ( ) ) ?;
773+
774+ let verity = if let Some ( composefs) = & rollback. composefs {
775+ composefs. verity . clone ( )
776+ } else {
777+ // Shouldn't really happen
778+ anyhow:: bail!( "Verity not found for rollback deployment" )
779+ } ;
780+ usr_cfg. write ( get_user_config ( & verity) . as_bytes ( ) ) ?;
781+
782+ let verity = if let Some ( composefs) = & current. composefs {
783+ composefs. verity . clone ( )
784+ } else {
785+ // Shouldn't really happen
786+ anyhow:: bail!( "Verity not found for booted deployment" )
787+ } ;
788+ usr_cfg. write ( get_user_config ( & verity) . as_bytes ( ) ) ?;
789+
790+ Ok ( ( ) )
791+ }
792+
793+ /// Filename for `loader/entries`
794+ const CURRENT_ENTRIES : & str = "entries" ;
795+ const ROLLBACK_ENTRIES : & str = "entries.staged" ;
796+
797+ #[ context( "Getting boot entries" ) ]
798+ pub ( crate ) fn get_sorted_boot_entries ( ascending : bool ) -> Result < Vec < BLSConfig > > {
799+ let mut all_configs = vec ! [ ] ;
800+
801+ for entry in std:: fs:: read_dir ( format ! ( "/sysroot/boot/loader/{CURRENT_ENTRIES}" ) ) ? {
802+ let entry = entry?;
803+
804+ let file_name = entry. file_name ( ) ;
805+
806+ let file_name = file_name
807+ . to_str ( )
808+ . ok_or ( anyhow:: anyhow!( "Found non UTF-8 characters in filename" ) ) ?;
809+
810+ if !file_name. ends_with ( ".conf" ) {
811+ continue ;
812+ }
813+
814+ let contents = std:: fs:: read_to_string ( & entry. path ( ) )
815+ . with_context ( || format ! ( "Failed to read {:?}" , entry. path( ) ) ) ?;
816+
817+ let config = parse_bls_config ( & contents) . context ( "Parsing bls config" ) ?;
818+
819+ all_configs. push ( config) ;
820+ }
821+
822+ all_configs. sort_by ( |a, b| if ascending { a. cmp ( b) } else { b. cmp ( a) } ) ;
823+
824+ return Ok ( all_configs) ;
825+ }
826+
827+ #[ context( "Rolling back BLS" ) ]
828+ pub ( crate ) fn rollback_composefs_bls ( ) -> Result < ( ) > {
829+ // Sort in descending order as that's the order they're shown on the boot screen
830+ // After this:
831+ // all_configs[0] -> booted depl
832+ // all_configs[1] -> rollback depl
833+ let mut all_configs = get_sorted_boot_entries ( false ) ?;
834+
835+ // Update the indicies so that they're swapped
836+ for ( idx, cfg) in all_configs. iter_mut ( ) . enumerate ( ) {
837+ cfg. version = idx as u32 ;
838+ }
839+
840+ assert ! ( all_configs. len( ) == 2 ) ;
841+
842+ // Write these
843+ let dir_path = PathBuf :: from ( format ! ( "/sysroot/boot/loader/{ROLLBACK_ENTRIES}" ) ) ;
844+ create_dir_all ( & dir_path) . with_context ( || format ! ( "Failed to create dir: {dir_path:?}" ) ) ?;
845+
846+ // Write the BLS configs in there
847+ for cfg in all_configs {
848+ let file_name = format ! ( "bootc-composefs-{}.conf" , cfg. version) ;
849+
850+ let mut file = std:: fs:: OpenOptions :: new ( )
851+ . create ( true )
852+ . write ( true )
853+ . open ( dir_path. join ( & file_name) )
854+ . with_context ( || format ! ( "Opening {file_name}" ) ) ?;
855+
856+ file. write_all ( cfg. to_string ( ) . as_bytes ( ) )
857+ . with_context ( || format ! ( "Writing to {file_name}" ) ) ?;
858+ }
859+
860+ // Atomically exchange "entries" <-> "entries.rollback"
861+ let dir = openat:: Dir :: open ( "/sysroot/boot/loader" ) . context ( "Opening loader dir" ) ?;
862+
863+ tracing:: debug!( "Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}" ) ;
864+ dir. local_exchange ( ROLLBACK_ENTRIES , CURRENT_ENTRIES )
865+ . context ( "local exchange" ) ?;
866+
867+ tracing:: debug!( "Removing {ROLLBACK_ENTRIES}" ) ;
868+ dir. remove_all ( ROLLBACK_ENTRIES )
869+ . context ( "Removing entries.rollback" ) ?;
870+
871+ tracing:: debug!( "Syncing to disk" ) ;
872+ dir. syncfs ( ) . context ( "syncfs" ) ?;
873+
874+ Ok ( ( ) )
875+ }
876+
877+ #[ context( "Rolling back composefs" ) ]
878+ pub ( crate ) async fn composefs_rollback ( ) -> Result < ( ) > {
879+ let host = composefs_deployment_status ( ) . await ?;
880+
881+ let new_spec = {
882+ let mut new_spec = host. spec . clone ( ) ;
883+ new_spec. boot_order = new_spec. boot_order . swap ( ) ;
884+ new_spec
885+ } ;
886+
887+ // Just to be sure
888+ host. spec . verify_transition ( & new_spec) ?;
889+
890+ let reverting = new_spec. boot_order == BootOrder :: Default ;
891+ if reverting {
892+ println ! ( "notice: Reverting queued rollback state" ) ;
893+ }
894+
895+ let rollback_status = host
896+ . status
897+ . rollback
898+ . ok_or_else ( || anyhow ! ( "No rollback available" ) ) ?;
899+
900+ // TODO: Handle staged deployment
901+ // Ostree will drop any staged deployment on rollback but will keep it if it is the first item
902+ // in the new deployment list
903+ let Some ( rollback_composefs_entry) = & rollback_status. composefs else {
904+ anyhow:: bail!( "Rollback deployment not a composefs deployment" )
905+ } ;
906+
907+ match rollback_composefs_entry. boot_type {
908+ BootType :: Bls => rollback_composefs_bls ( ) ,
909+ BootType :: Uki => rollback_composefs_uki ( & host. status . booted . unwrap ( ) , & rollback_status) ,
910+ } ?;
911+
912+ Ok ( ( ) )
913+ }
914+
750915/// Implementation of rollback functionality
751916pub ( crate ) async fn rollback ( sysroot : & Storage ) -> Result < ( ) > {
752917 const ROLLBACK_JOURNAL_ID : & str = "26f3b1eb24464d12aa5e7b544a6b5468" ;
0 commit comments