From 889f6fc323cb6e4deaf36cc4304598ac69c5bb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Dec 2025 12:39:18 +0000 Subject: [PATCH 1/3] Link resolv.conf when running chrooted scripts --- rust/agama-files/src/runner.rs | 81 +++++++++++++++++++++++++++++++++- rust/share/bin/chroot | 3 +- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/rust/agama-files/src/runner.rs b/rust/agama-files/src/runner.rs index 3f6a18312d..5f3a3c7f09 100644 --- a/rust/agama-files/src/runner.rs +++ b/rust/agama-files/src/runner.rs @@ -19,8 +19,9 @@ // find current contact information at www.suse.com. use std::{ - fs::File, + fs::{self, File}, io::{self, Read, Seek, SeekFrom, Write}, + os::unix::fs::symlink, path::{Path, PathBuf}, process::ExitStatus, }; @@ -44,6 +45,11 @@ pub enum Error { Question(#[from] question::AskError), } +// Relative path to the resolv.conf file. +const RESOLV_CONF_PATH: &str = "etc/resolv.conf"; +// Relative path to the NetworkManager resolv.conf file. +const NM_RESOLV_CONF_PATH: &str = "run/NetworkManager/resolv.conf"; + /// Implements the logic to run a script. /// /// It takes care of running the script, reporting errors (and asking whether to retry) and write @@ -86,6 +92,11 @@ impl ScriptsRunner { pub async fn run(&self, scripts: &[&Script]) -> Result<(), Error> { self.start_progress(scripts); + let mut resolv_linked = false; + if scripts.iter().any(|s| s.chroot()) { + resolv_linked = self.link_resolv()?; + } + for script in scripts { _ = self .progress @@ -93,6 +104,10 @@ impl ScriptsRunner { self.run_script(script).await?; } + if resolv_linked { + self.unlink_resolv(); + } + _ = self .progress .cast(progress::message::Finish::new(Scope::Files)); @@ -208,6 +223,34 @@ impl ScriptsRunner { let string = String::from_utf8_lossy(&buffer); Ok(string.into_owned()) } + + /// Make sures that the resolv.conf is linked and returns true if any action was needed. + /// + /// It returns false if the resolv.conf was already linked and no action was required. + fn link_resolv(&self) -> Result { + let original = self.install_dir.join(NM_RESOLV_CONF_PATH); + let link = self.resolv_link_path(); + + if fs::exists(&link)? || !fs::exists(&original)? { + return Ok(false); + } + + // It assumes that the directory of the resolv.conf (/etc) exists. + symlink(original.as_path(), link.as_path())?; + Ok(true) + } + + /// Removes the resolv.conf file from the chroot. + fn unlink_resolv(&self) { + let link = self.resolv_link_path(); + if let Err(error) = fs::remove_file(link) { + tracing::warn!("Could not remove the resolv.conf link: {error}"); + } + } + + fn resolv_link_path(&self) -> PathBuf { + self.install_dir.join(RESOLV_CONF_PATH) + } } #[cfg(test)] @@ -239,6 +282,11 @@ mod tests { impl AsyncTestContext for Context { async fn setup() -> Context { + // Set the PATH + let old_path = std::env::var("PATH").unwrap(); + let bin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../share/bin"); + std::env::set_var("PATH", format!("{}:{}", &bin_dir.display(), &old_path)); + let tmp_dir = TempDir::with_prefix("scripts-").expect("a temporary directory"); let (events_tx, events_rx) = broadcast::channel::(16); @@ -268,6 +316,7 @@ mod tests { self.questions.clone(), ) } + pub fn setup_script(&self, content: &str, chroot: bool) -> Script { let base = BaseScript { name: "test.sh".to_string(), @@ -284,6 +333,18 @@ mod tests { .expect("Could not write the script"); script } + + // Set up a fake chroot. + pub fn setup_chroot(&self) -> std::io::Result<()> { + let nm_dir = self.install_dir.join("run/NetworkManager"); + fs::create_dir_all(&nm_dir)?; + fs::create_dir_all(self.install_dir.join("etc"))?; + + let mut file = File::create(nm_dir.join("resolv.conf"))?; + file.write_all(b"nameserver 127.0.0.1\n")?; + + Ok(()) + } } #[test_context(Context)] @@ -319,6 +380,24 @@ mod tests { Ok(()) } + #[test_context(Context)] + #[tokio::test] + async fn test_chrooted_script(ctx: &mut Context) -> Result<(), Error> { + ctx.setup_chroot()?; + + // Ideally, the script should check the existence of /etc/resolv.conf. + // However, it does not run on a real chroot (see share/bin/chroot), + // so it needs the whole path. + let file = ctx.install_dir.join("etc/resolv.conf"); + let content = format!("#!/usr/bin/bash\ntest -h {}", file.display()); + let script = ctx.setup_script(&content, true); + + let runner = ctx.runner(); + let scripts = vec![&script]; + runner.run(&scripts).await.unwrap(); + Ok(()) + } + #[test_context(Context)] #[tokio::test] async fn test_run_scripts_retry(ctx: &mut Context) -> Result<(), Error> { diff --git a/rust/share/bin/chroot b/rust/share/bin/chroot index a1c240b98f..ce10aaad1c 100755 --- a/rust/share/bin/chroot +++ b/rust/share/bin/chroot @@ -6,6 +6,7 @@ case "$2" in "install") mkdir -p "$1/$8" ;; - "chroot") + *) + sh "$2" ;; esac From edf27e00ba97b49f77419f6332c5a2a54ae008c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Dec 2025 14:10:42 +0000 Subject: [PATCH 2/3] Adapt chroot replacement to run scripts when needed --- rust/agama-files/src/runner.rs | 9 ++++++++- rust/share/bin/chroot | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/rust/agama-files/src/runner.rs b/rust/agama-files/src/runner.rs index 5f3a3c7f09..15394ce077 100644 --- a/rust/agama-files/src/runner.rs +++ b/rust/agama-files/src/runner.rs @@ -389,12 +389,19 @@ mod tests { // However, it does not run on a real chroot (see share/bin/chroot), // so it needs the whole path. let file = ctx.install_dir.join("etc/resolv.conf"); - let content = format!("#!/usr/bin/bash\ntest -h {}", file.display()); + let content = format!("#!/usr/bin/bash\ntest -h {} && echo exists", file.display()); let script = ctx.setup_script(&content, true); let runner = ctx.runner(); let scripts = vec![&script]; runner.run(&scripts).await.unwrap(); + + // It runs successfully because the resolv.conf link exists. + let path = &ctx.workdir.join("post").join("test.stdout"); + let body: Vec = std::fs::read(path).unwrap(); + let body = String::from_utf8(body).unwrap(); + assert_eq!("exists\n", body); + Ok(()) } diff --git a/rust/share/bin/chroot b/rust/share/bin/chroot index ce10aaad1c..28c7144d21 100755 --- a/rust/share/bin/chroot +++ b/rust/share/bin/chroot @@ -6,7 +6,7 @@ case "$2" in "install") mkdir -p "$1/$8" ;; - *) - sh "$2" + *.sh) + . "$2" ;; esac From 0c6e7b3c457e920ec71a3cf5d7e9cfd6debb9675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 18 Dec 2025 14:18:16 +0000 Subject: [PATCH 3/3] Reduce code repetition in scripts tests --- rust/agama-files/src/runner.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/rust/agama-files/src/runner.rs b/rust/agama-files/src/runner.rs index 15394ce077..37bb58a19d 100644 --- a/rust/agama-files/src/runner.rs +++ b/rust/agama-files/src/runner.rs @@ -345,6 +345,13 @@ mod tests { Ok(()) } + + // Return the content of a script result file. + pub fn result_content(&self, script_type: &str, name: &str) -> String { + let path = &self.workdir.join(script_type).join(name); + let body: Vec = std::fs::read(path).unwrap(); + String::from_utf8(body).unwrap() + } } #[test_context(Context)] @@ -361,20 +368,9 @@ mod tests { let runner = ctx.runner(); runner.run(&scripts).await.unwrap(); - let path = &ctx.workdir.join("post").join("test.stdout"); - let body: Vec = std::fs::read(path).unwrap(); - let body = String::from_utf8(body).unwrap(); - assert_eq!("hello\n", body); - - let path = &ctx.workdir.join("post").join("test.stderr"); - let body: Vec = std::fs::read(path).unwrap(); - let body = String::from_utf8(body).unwrap(); - assert_eq!("error\n", body); - - let path = &ctx.workdir.join("post").join("test.exit"); - let body: Vec = std::fs::read(path).unwrap(); - let body = String::from_utf8(body).unwrap(); - assert_eq!("0", body); + assert_eq!(ctx.result_content("post", "test.stdout"), "hello\n"); + assert_eq!(ctx.result_content("post", "test.stderr"), "error\n"); + assert_eq!(ctx.result_content("post", "test.exit"), "0"); assert!(std::fs::exists(file).unwrap()); Ok(()) @@ -397,10 +393,7 @@ mod tests { runner.run(&scripts).await.unwrap(); // It runs successfully because the resolv.conf link exists. - let path = &ctx.workdir.join("post").join("test.stdout"); - let body: Vec = std::fs::read(path).unwrap(); - let body = String::from_utf8(body).unwrap(); - assert_eq!("exists\n", body); + assert_eq!(ctx.result_content("post", "test.stdout"), "exists\n"); Ok(()) }