@@ -9,10 +9,14 @@ use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedSt
99use crate :: util;
1010use anyhow:: { anyhow, Context , Result } ;
1111use clap:: crate_version;
12+ use openat_ext:: OpenatDirExt ;
13+ use rustix:: fs:: link;
1214use serde:: { Deserialize , Serialize } ;
1315use std:: borrow:: Cow ;
1416use std:: collections:: BTreeMap ;
15- use std:: path:: Path ;
17+ use std:: fs:: { self , File } ;
18+ use std:: path:: { Path , PathBuf } ;
19+ use std:: thread:: current;
1620
1721pub ( crate ) enum ConfigMode {
1822 None ,
@@ -489,6 +493,148 @@ pub(crate) fn client_run_validate() -> Result<()> {
489493 Ok ( ( ) )
490494}
491495
496+ pub ( crate ) fn client_run_migrate ( ) -> Result < ( ) > {
497+ // Used to condition execution of this unit at the systemd level
498+ let stamp_file = "/boot/.bootupd-static-migration-complete" ;
499+
500+ // Did we already complete the migration?
501+ let mut ostree_cmd = std:: process:: Command :: new ( "ostree" ) ;
502+ let result = ostree_cmd
503+ . args ( [
504+ "config" ,
505+ "--repo=/sysroot/ostree/repo" ,
506+ "get" ,
507+ "sysroot.bootloader" ,
508+ ] )
509+ . output ( )
510+ . with_context ( || "failed to call ostree command. Not performing migration" ) ?;
511+ if !result. status . success ( ) {
512+ // ostree will exit with a non zero return code if the key does not exists
513+ println ! ( "ostree repo 'sysroot.bootloader' config option not set yet." ) ;
514+ } else {
515+ let bootloader = String :: from_utf8 ( result. stdout )
516+ . with_context ( || "decoding as UTF-8 output of ostree command" ) ?;
517+ if bootloader. trim_end ( ) == "none" {
518+ println ! ( "ostree repo 'sysroot.bootloader' config option already set to 'none'." ) ;
519+ println ! ( "Assuming that the migration is already complete." ) ;
520+ File :: create ( stamp_file) ?;
521+ return Ok ( ( ) ) ;
522+ } else {
523+ println ! (
524+ "ostree repo 'sysroot.bootloader' config currently set to: {}" ,
525+ bootloader. trim_end( )
526+ ) ;
527+ }
528+ }
529+
530+ // Remount /boot read write just for this unit (we are called in a slave mount namespace by systemd)
531+ ensure_writable_boot ( ) ?;
532+
533+ let grub_config_dir = PathBuf :: from ( "/boot/grub2" ) ;
534+ let Ok ( dirfd) = openat:: Dir :: open ( & grub_config_dir) . with_context ( || "a" ) else {
535+ anyhow:: bail!(
536+ "Could not open {}. Is /boot mounted? Not performing migration." ,
537+ grub_config_dir. display( )
538+ ) ;
539+ } ;
540+
541+ // Migrate /boot/grub2/grub.cfg to a static GRUB config if it is a symlink
542+ let grub_config_filename = PathBuf :: from ( "/boot/grub2/grub.cfg" ) ;
543+ match dirfd. read_link ( "grub.cfg" ) {
544+ Err ( _) => {
545+ println ! (
546+ "'{}' is not a symlink. Nothing to migrate." ,
547+ grub_config_filename. display( )
548+ ) ;
549+ }
550+ Ok ( path) => {
551+ println ! ( "Migrating to a static GRUB config..." ) ;
552+
553+ let mut current_config = grub_config_dir. clone ( ) ;
554+ current_config. push ( path) ;
555+ let backup_config = PathBuf :: from ( "/boot/grub2/grub.cfg.backup" ) ;
556+ let current_config_copy = PathBuf :: from ( "/boot/grub2/grub.cfg.current" ) ;
557+
558+ // Backup the current GRUB config which is hopefully working right now
559+ println ! (
560+ "Creating a backup of the current GRUB config '{}' in '{}'..." ,
561+ current_config. display( ) ,
562+ backup_config. display( )
563+ ) ;
564+ fs:: copy ( & current_config, & backup_config) . map_err ( |e| {
565+ anyhow ! (
566+ "Could not copy the current GRUB config: {}. Not performing migration." ,
567+ e
568+ )
569+ } ) ?;
570+
571+ // Copy it again alongside the current symlink
572+ fs:: copy ( & current_config, & current_config_copy) . map_err ( |e| {
573+ anyhow ! (
574+ "Could not copy the current GRUB config: {}. Not performing migration." ,
575+ e
576+ )
577+ } ) ?;
578+
579+ // Atomically exchange the configs
580+ dirfd. local_exchange ( "grub.cfg.current" , "grub.cfg" ) . map_err ( |e| {
581+ anyhow ! (
582+ "Could not exchange the symlink with the current GRUB config: {}. Not performing migration." ,
583+ e
584+ )
585+ } ) ?;
586+
587+ // Remove the now unused symlink (optional cleanup, ignore any failures)
588+ dirfd. remove_file ( "grub.cfg.current" ) . unwrap_or_else ( |e| {
589+ println ! (
590+ "Could not remove now unused GRUB config symlink: {}. Ignoring error." ,
591+ e
592+ )
593+ } ) ;
594+
595+ println ! ( "GRUB config symlink successfully replaced with the current config." ) ;
596+ }
597+ } ;
598+
599+ // If /etc/default/grub exists then we have to force the regeneration of the
600+ // GRUB config to remove the ostree entries that duplicates the BLS ones
601+ let grub_default = PathBuf :: from ( "/etc/default/grub" ) ;
602+ if grub_default. exists ( ) {
603+ println ! ( "Marking bootloader as BLS capable..." ) ;
604+ File :: create ( "/boot/grub2/.grub2-blscfg-supported" ) ?;
605+
606+ println ! ( "Regenerating GRUB config with only BLS configs..." ) ;
607+ // grub2-mkconfig -o /boot/grub2/grub.cfg
608+ let status = std:: process:: Command :: new ( "grub2-mkconfig" )
609+ . arg ( "-o" )
610+ . arg ( grub_config_filename)
611+ . status ( ) ?;
612+ if !status. success ( ) {
613+ anyhow:: bail!( "Failed to regenerate the GRUB config" ) ;
614+ }
615+ }
616+
617+ println ! ( "Setting up 'sysroot.bootloader' to 'none' in ostree repo config..." ) ;
618+ let status = std:: process:: Command :: new ( "ostree" )
619+ . args ( [
620+ "config" ,
621+ "--repo=/sysroot/ostree/repo" ,
622+ "set" ,
623+ "sysroot.bootloader" ,
624+ "none" ,
625+ ] )
626+ . status ( ) ?;
627+ if !status. success ( ) {
628+ anyhow:: bail!( "Failed to set 'sysroot.bootloader' to 'none' in ostree repo config" ) ;
629+ }
630+
631+ // Migration complete, let's write the stamp file
632+ File :: create ( stamp_file) ?;
633+
634+ println ! ( "Static GRUB config migration completed successfully!" ) ;
635+ Ok ( ( ) )
636+ }
637+
492638#[ cfg( test) ]
493639mod tests {
494640 use super :: * ;
0 commit comments