diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 05f1b2b7b..348dfc1da 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -439,6 +439,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { } } else { let fetched = crate::deploy::pull(sysroot, imgref, opts.quiet).await?; + let kargs = crate::kargs::get_kargs(repo, &booted_deployment, fetched.as_ref())?; let staged_digest = staged_image.as_ref().map(|s| s.image_digest.as_str()); let fetched_digest = fetched.manifest_digest.as_str(); tracing::debug!("staged: {staged_digest:?}"); @@ -460,7 +461,10 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { println!("No update available.") } else { let osname = booted_deployment.osname(); - crate::deploy::stage(sysroot, &osname, &fetched, &spec).await?; + let mut opts = ostree::SysrootDeployTreeOpts::default(); + let kargs: Vec<&str> = kargs.iter().map(|s| s.as_str() ).collect(); + opts.override_kernel_argv = Some(kargs.as_slice()); + crate::deploy::stage(sysroot, &osname, &fetched, &spec, Some(opts)).await?; changed = true; if let Some(prev) = booted_image.as_ref() { if let Some(fetched_manifest) = fetched.get_manifest(repo)? { @@ -533,6 +537,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> { let new_spec = RequiredHostSpec::from_spec(&new_spec)?; let fetched = crate::deploy::pull(sysroot, &target, opts.quiet).await?; + let kargs = crate::kargs::get_kargs(repo, &booted_deployment, fetched.as_ref())?; if !opts.retain { // By default, we prune the previous ostree ref so it will go away after later upgrades @@ -546,7 +551,10 @@ async fn switch(opts: SwitchOpts) -> Result<()> { } let stateroot = booted_deployment.osname(); - crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?; + let mut opts = ostree::SysrootDeployTreeOpts::default(); + let kargs: Vec<&str> = kargs.iter().map(|s| s.as_str() ).collect(); + opts.override_kernel_argv = Some(kargs.as_slice()); + crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, Some(opts)).await?; Ok(()) } @@ -591,11 +599,16 @@ async fn edit(opts: EditOpts) -> Result<()> { } let fetched = crate::deploy::pull(sysroot, new_spec.image, opts.quiet).await?; + let repo = &sysroot.repo(); + let kargs = crate::kargs::get_kargs(repo, &booted_deployment, fetched.as_ref())?; // TODO gc old layers here let stateroot = booted_deployment.osname(); - crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?; + let mut opts = ostree::SysrootDeployTreeOpts::default(); + let kargs: Vec<&str> = kargs.iter().map(|s| s.as_str() ).collect(); + opts.override_kernel_argv = Some(kargs.as_slice()); + crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, Some(opts)).await?; Ok(()) } diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs index 3eb31a8a4..f6505bf13 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -221,8 +221,10 @@ async fn deploy( stateroot: &str, image: &ImageState, origin: &glib::KeyFile, + opts: Option>, ) -> Result<()> { let stateroot = Some(stateroot); + let opts = opts.unwrap_or(Default::default()); // Copy to move into thread let cancellable = gio::Cancellable::NONE; let _new_deployment = sysroot.stage_tree_with_options( @@ -230,7 +232,7 @@ async fn deploy( image.ostree_commit.as_str(), Some(origin), merge_deployment, - &Default::default(), + &opts, cancellable, )?; Ok(()) @@ -255,6 +257,7 @@ pub(crate) async fn stage( stateroot: &str, image: &ImageState, spec: &RequiredHostSpec<'_>, + opts: Option>, ) -> Result<()> { let merge_deployment = sysroot.merge_deployment(Some(stateroot)); let origin = origin_from_imageref(spec.image)?; @@ -264,6 +267,7 @@ pub(crate) async fn stage( stateroot, image, &origin, + opts, ) .await?; crate::deploy::cleanup(sysroot).await?; diff --git a/lib/src/install/config.rs b/lib/src/install/config.rs index 730d18a2f..b73ca5b19 100644 --- a/lib/src/install/config.rs +++ b/lib/src/install/config.rs @@ -47,6 +47,7 @@ pub(crate) struct InstallConfiguration { /// Kernel arguments, applied at installation time #[serde(skip_serializing_if = "Option::is_none")] pub(crate) kargs: Option>, + pub(crate) arch: Option>, } fn merge_basic(s: &mut Option, o: Option) { @@ -100,9 +101,19 @@ impl Mergeable for InstallConfiguration { merge_basic(&mut self.block, other.block); self.filesystem.merge(other.filesystem); if let Some(other_kargs) = other.kargs { - self.kargs - .get_or_insert_with(Default::default) - .extend(other_kargs) + // if arch is specified, only apply kargs if it matches the current arch + // if arch is not specified, apply kargs unconditionally + if let Some(other_arch) = other.arch { + if other_arch.contains(&std::env::consts::ARCH.to_string()) { + self.kargs + .get_or_insert_with(Default::default) + .extend(other_kargs.clone()); + } + } else { + self.kargs + .get_or_insert_with(Default::default) + .extend(other_kargs) + } } } } @@ -161,7 +172,7 @@ pub(crate) fn load_config() -> Result> { let buf = std::fs::read_to_string(&path)?; let mut unused = std::collections::HashSet::new(); let de = toml::Deserializer::new(&buf); - let c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| { + let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| { unused.insert(path.to_string()); }) .with_context(|| format!("Parsing {path:?}"))?; @@ -174,6 +185,15 @@ pub(crate) fn load_config() -> Result> { config.merge(install); } } else { + // If arch is specified and doesn't match the current arch, remove kargs + // We need to add this since the merge function isn't applied for the first config file + if let Some(ref mut install) = c.install { + if let Some(arch) = install.arch.as_ref() { + if !arch.contains(&std::env::consts::ARCH.to_string()) { + install.kargs = None; + } + } + } config = c.install; } } @@ -319,3 +339,205 @@ block = ["tpm2-luks"]"##, // And verify passing a disallowed config is an error assert!(install.get_block_setup(Some(BlockSetup::Direct)).is_err()); } + +#[test] +/// Verify that kargs are only applied to supported architectures +fn test_arch() { + // no arch specified, kargs ensure that kargs are applied unconditionally + std::env::set_var("ARCH", "x86_64"); + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +"##, + ) + .unwrap(); + let mut install = c.install.unwrap(); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); + std::env::set_var("ARCH", "aarch64"); + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +"##, + ) + .unwrap(); + let mut install = c.install.unwrap(); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); + + // one arch matches and one doesn't, ensure that kargs are only applied for the matching arch + std::env::set_var("ARCH", "x86_64"); + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +"##, + ) + .unwrap(); + let mut install = c.install.unwrap(); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=ttyS0", "foo=bar"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + arch: Some( + ["x86_64"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=ttyS0", "foo=bar"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + arch: Some( + ["aarch64"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=ttyS0", "foo=bar"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); + + // multiple arch specified, ensure that kargs are applied to both archs + std::env::set_var("ARCH", "x86_64"); + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +"##, + ) + .unwrap(); + let mut install = c.install.unwrap(); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + arch: Some( + ["x86_64", "aarch64"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + std::env::set_var("ARCH", "x86_64"); + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); + std::env::set_var("ARCH", "aarch64"); + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +"##, + ) + .unwrap(); + let mut install = c.install.unwrap(); + let other = InstallConfigurationToplevel { + install: Some(InstallConfiguration { + kargs: Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + arch: Some( + ["x86_64", "aarch64"] + .into_iter() + .map(ToOwned::to_owned) + .collect(), + ), + ..Default::default() + }), + }; + std::env::set_var("ARCH", "x86_64"); + install.merge(other.install.unwrap()); + assert_eq!( + install.kargs, + Some( + ["console=tty0", "nosmt"] + .into_iter() + .map(ToOwned::to_owned) + .collect() + ) + ); +} diff --git a/lib/src/kargs.rs b/lib/src/kargs.rs new file mode 100644 index 000000000..dfe4f900f --- /dev/null +++ b/lib/src/kargs.rs @@ -0,0 +1,158 @@ +use anyhow::Ok; +use anyhow::Result; + +use ostree::gio; +use ostree_ext::ostree; +use ostree_ext::ostree::Deployment; +use crate::deploy::ImageState; +use ostree_ext::prelude::FileExt; +use ostree_ext::prelude::Cast; +use ostree_ext::prelude::FileEnumeratorExt; + +use serde::Deserialize; + +#[derive(Deserialize)] +struct Config { + kargs: Vec, + arch: Option>, +} + +pub(crate) fn get_kargs(repo: &ostree::Repo, booted_deployment: &Deployment, fetched: &ImageState) -> Result> { + let cancellable = gio::Cancellable::NONE; + let mut kargs: Vec = vec![]; + + // Get the running kargs of the booted system + match ostree::Deployment::bootconfig(booted_deployment) { + Some(bootconfig) => { + match ostree::BootconfigParser::get(&bootconfig, "options") { + Some(options) => { + let options: Vec<&str> = options.split_whitespace().collect(); + let mut options: Vec = options.into_iter().map(|s| s.to_string()).collect(); + kargs.append(&mut options); + }, + None => () + } + }, + None => () + }; + + // Get the kargs in kargs.d of the booted system + let mut existing_kargs: Vec = vec![]; + let fragments = liboverdrop::scan(&["/usr/lib"], "bootc/kargs.d", &["toml"], true); + for (_name, path) in fragments { + let s = std::fs::read_to_string(&path)?; + let mut parsed_kargs = parse_file(s.clone())?; + existing_kargs.append(&mut parsed_kargs); + } + + // Get the kargs in kargs.d of the remote image + let mut remote_kargs: Vec = vec![]; + let (fetched_tree, _) = repo.read_commit(fetched.ostree_commit.as_str(), cancellable)?; + let fetched_tree = fetched_tree.resolve_relative_path("/usr/lib/bootc/kargs.d"); + let fetched_tree = fetched_tree.downcast::().expect("downcast"); + match fetched_tree.query_exists(cancellable) { + true => {} + false => { + return Ok(vec![]); + } + } + let queryattrs = "standard::name,standard::type"; + let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS; + let fetched_iter = fetched_tree.enumerate_children(queryattrs, queryflags, cancellable)?; + while let Some(fetched_info) = fetched_iter.next_file(cancellable)? { + let fetched_child = fetched_iter.child(&fetched_info); + let fetched_child = fetched_child.downcast::().expect("downcast"); + fetched_child.ensure_resolved()?; + let fetched_contents_checksum = fetched_child.checksum(); + let f = ostree::Repo::load_file(repo, fetched_contents_checksum.as_str(), cancellable)?; + let file_content = f.0; + let mut reader = ostree_ext::prelude::InputStreamExtManual::into_read(file_content.unwrap()); + let s = std::io::read_to_string(&mut reader)?; + let mut parsed_kargs = parse_file(s.clone())?; + remote_kargs.append(&mut parsed_kargs); + } + + // get the diff between the existing and remote kargs + let mut added_kargs: Vec = remote_kargs.clone().into_iter().filter(|item| !existing_kargs.contains(item)).collect(); + let removed_kargs: Vec = existing_kargs.clone().into_iter().filter(|item| !remote_kargs.contains(item)).collect(); + + // apply the diff to the system kargs + kargs.retain(|x| !removed_kargs.contains(x)); + kargs.append(&mut added_kargs); + + Ok(kargs) +} + +pub fn parse_file(file_content: String) -> Result> { + let mut de: Config = toml::from_str(&file_content)?; + let mut parsed_kargs: Vec = vec![]; + // if arch specified, apply kargs only if the arch matches + // if arch not specified, apply kargs unconditionally + match de.arch { + None => parsed_kargs = de.kargs, + Some(supported_arch) => { + if supported_arch.contains(&std::env::consts::ARCH.to_string()) { + parsed_kargs.append(&mut de.kargs); + } + } + } + return Ok(parsed_kargs); +} + +#[test] +/// Verify that kargs are only applied to supported architectures +fn test_arch() { + // no arch specified, kargs ensure that kargs are applied unconditionally + std::env::set_var("ARCH", "x86_64"); + let file_content = r##"kargs = ["console=tty0", "nosmt"]"##.to_string(); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + ["console=tty0", "nosmt"] + ); + std::env::set_var("ARCH", "aarch64"); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + ["console=tty0", "nosmt"] + ); + + // one arch matches and one doesn't, ensure that kargs are only applied for the matching arch + std::env::set_var("ARCH", "x86_64"); + let file_content = + r##"kargs = ["console=tty0", "nosmt"] +arch = ["x86_64"] +"##.to_string(); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + ["console=tty0", "nosmt"] + ); + let file_content = + r##"kargs = ["console=tty0", "nosmt"] +arch = ["aarch64"] +"##.to_string(); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + [] as [String; 0] + ); + + // multiple arch specified, ensure that kargs are applied to both archs + std::env::set_var("ARCH", "x86_64"); + let file_content = + r##"kargs = ["console=tty0", "nosmt"] +arch = ["x86_64", "aarch64"] +"##.to_string(); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + ["console=tty0", "nosmt"] + ); + std::env::set_var("ARCH", "aarch64"); + let parsed_kargs = parse_file(file_content.clone()).unwrap(); + assert_eq!( + parsed_kargs, + ["console=tty0", "nosmt"] + ); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b04d33642..3f90120c7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,6 +19,7 @@ pub mod cli; pub(crate) mod deploy; +pub(crate) mod kargs; pub(crate) mod generator; pub(crate) mod journal; mod lsm;