diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 05f1b2b7b..7a4ff2fe8 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 900ca8399..9f19250ad 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -278,8 +278,10 @@ async fn deploy( stateroot: &str, image: &ImageState, origin: &glib::KeyFile, + opts: Option>, ) -> Result<()> { let stateroot = Some(stateroot); + let opts = opts.unwrap_or_default(); // Copy to move into thread let cancellable = gio::Cancellable::NONE; let _new_deployment = sysroot.stage_tree_with_options( @@ -287,7 +289,7 @@ async fn deploy( image.ostree_commit.as_str(), Some(origin), merge_deployment, - &Default::default(), + &opts, cancellable, )?; Ok(()) @@ -312,6 +314,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)?; @@ -321,6 +324,7 @@ pub(crate) async fn stage( stateroot, image, &origin, + opts, ) .await?; crate::deploy::cleanup(sysroot).await?; diff --git a/lib/src/kargs.rs b/lib/src/kargs.rs new file mode 100644 index 000000000..4ce8cdf8e --- /dev/null +++ b/lib/src/kargs.rs @@ -0,0 +1,166 @@ +use anyhow::Ok; +use anyhow::Result; + +use crate::deploy::ImageState; +use ostree::gio; +use ostree_ext::ostree; +use ostree_ext::ostree::Deployment; +use ostree_ext::prelude::Cast; +use ostree_ext::prelude::FileEnumeratorExt; +use ostree_ext::prelude::FileExt; + +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct Config { + kargs: Vec, + match_architectures: Option>, +} + +/// Compute the kernel arguments for the new deployment. This starts from the booted +/// karg, but applies the diff between the bootc karg files in /usr/lib/bootc/kargs.d +/// between the booted deployment and the new one. +pub(crate) fn get_kargs( + repo: &ostree::Repo, + booted_deployment: &Deployment, + fetched: &ImageState, +) -> Result> { + let cancellable = gio::Cancellable::NONE; + let mut kargs: Vec = vec![]; + let sys_arch = std::env::consts::ARCH.to_string(); + + // Get the running kargs of the booted system + if let Some(bootconfig) = ostree::Deployment::bootconfig(booted_deployment) { + if let Some(options) = ostree::BootconfigParser::get(&bootconfig, "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); + } + }; + + // 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(), sys_arch.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"); + if !fetched_tree.query_exists(cancellable) { + return Ok(Default::default()); + } + 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)? { + // only read and parse the file if it is a toml file + let name = fetched_info.name(); + if let Some(name) = name.to_str() { + if name.ends_with(".toml") { + 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(), sys_arch.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(); + + tracing::debug!( + "kargs: added={:?} removed={:?}", + &added_kargs, + removed_kargs + ); + + // 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, sys_arch: 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.match_architectures { + None => parsed_kargs = de.kargs, + Some(match_architectures) => { + if match_architectures.contains(&sys_arch) { + parsed_kargs.append(&mut de.kargs); + } + } + } + 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 + let sys_arch = "x86_64".to_string(); + let file_content = r##"kargs = ["console=tty0", "nosmt"]"##.to_string(); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.clone()).unwrap(); + assert_eq!(parsed_kargs, ["console=tty0", "nosmt"]); + let sys_arch = "aarch64".to_string(); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.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 + let sys_arch = "aarch64".to_string(); + let file_content = r##"kargs = ["console=tty0", "nosmt"] +match-architectures = ["x86_64"] +"## + .to_string(); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.clone()).unwrap(); + assert_eq!(parsed_kargs, [] as [String; 0]); + let file_content = r##"kargs = ["console=tty0", "nosmt"] +match-architectures = ["aarch64"] +"## + .to_string(); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.clone()).unwrap(); + assert_eq!(parsed_kargs, ["console=tty0", "nosmt"]); + + // multiple arch specified, ensure that kargs are applied to both archs + let sys_arch = "x86_64".to_string(); + let file_content = r##"kargs = ["console=tty0", "nosmt"] +match-architectures = ["x86_64", "aarch64"] +"## + .to_string(); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.clone()).unwrap(); + assert_eq!(parsed_kargs, ["console=tty0", "nosmt"]); + std::env::set_var("ARCH", "aarch64"); + let parsed_kargs = parse_file(file_content.clone(), sys_arch.clone()).unwrap(); + assert_eq!(parsed_kargs, ["console=tty0", "nosmt"]); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b04d33642..351a6db11 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -21,6 +21,7 @@ pub mod cli; pub(crate) mod deploy; pub(crate) mod generator; pub(crate) mod journal; +pub(crate) mod kargs; mod lsm; pub(crate) mod metadata; mod reboot;