Skip to content

Commit

Permalink
add /usr/lib/bootc/kargs.d support
Browse files Browse the repository at this point in the history
Fixes #255. Allows users to create files within /usr/lib/bootc/kargs.d with kernel arguments. These arguments can now be applied on a switch, upgrade, or edit.

General process:
- use ostree commit of fetched container image to return
the file tree
- navigate to /usr/lib/bootc/kargs.d
- read each file within the directory
- calculate the diff between the booted and fetched kargs in kargs.d
- apply the diff to the kargs currently on the running system
- pass the kargs to the stage() function

Signed-off-by: Luke Yang <luyang@redhat.com>
  • Loading branch information
lukewarmtemp committed Jun 10, 2024
1 parent 790e7c8 commit 0d185b4
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 4 deletions.
19 changes: 16 additions & 3 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,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:?}");
Expand All @@ -452,7 +453,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)? {
Expand Down Expand Up @@ -525,6 +529,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
Expand All @@ -538,7 +543,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(())
}
Expand Down Expand Up @@ -583,11 +591,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(())
}
Expand Down
6 changes: 5 additions & 1 deletion lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,16 +278,18 @@ async fn deploy(
stateroot: &str,
image: &ImageState,
origin: &glib::KeyFile,
opts: Option<ostree::SysrootDeployTreeOpts<'_>>,
) -> 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(
stateroot,
image.ostree_commit.as_str(),
Some(origin),
merge_deployment,
&Default::default(),
&opts,
cancellable,
)?;
Ok(())
Expand All @@ -312,6 +314,7 @@ pub(crate) async fn stage(
stateroot: &str,
image: &ImageState,
spec: &RequiredHostSpec<'_>,
opts: Option<ostree::SysrootDeployTreeOpts<'_>>,
) -> Result<()> {
let merge_deployment = sysroot.merge_deployment(Some(stateroot));
let origin = origin_from_imageref(spec.image)?;
Expand All @@ -321,6 +324,7 @@ pub(crate) async fn stage(
stateroot,
image,
&origin,
opts,
)
.await?;
crate::deploy::cleanup(sysroot).await?;
Expand Down
168 changes: 168 additions & 0 deletions lib/src/kargs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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<String>,
match_architectures: Option<Vec<String>>,
}

/// 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<Vec<String>> {
let cancellable = gio::Cancellable::NONE;
let mut kargs: Vec<String> = 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<String> = 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<String> = 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 pending image
let mut remote_kargs: Vec<String> = 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::<ostree::RepoFile>()
.expect("downcast");
if !fetched_tree.query_exists(cancellable) {
// if the kargs.d directory does not exist in the fetched image, return the existing kargs
kargs.append(&mut existing_kargs);
return Ok(kargs);
}
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::<ostree::RepoFile>()
.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<String> = remote_kargs
.clone()
.into_iter()
.filter(|item| !existing_kargs.contains(item))
.collect();
let removed_kargs: Vec<String> = 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<Vec<String>> {
let mut de: Config = toml::from_str(&file_content)?;
let mut parsed_kargs: Vec<String> = 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"]);
}
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod cli;
pub(crate) mod deploy;
pub(crate) mod generator;
pub(crate) mod journal;
pub(crate) mod kargs;
mod lints;
mod lsm;
pub(crate) mod metadata;
Expand Down

0 comments on commit 0d185b4

Please sign in to comment.