bootc_lib/install/
completion.rs1use std::io;
5use std::os::fd::AsFd;
6use std::process::Command;
7
8use anyhow::{Context, Result};
9use bootc_utils::CommandRunExt;
10use camino::Utf8Path;
11use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
12use fn_error_context::context;
13use ostree_ext::{gio, ostree};
14use rustix::fs::Mode;
15use rustix::fs::OFlags;
16
17use crate::podstorage::CStorage;
18use crate::utils::deployment_fd;
19
20use super::config;
21
22const ANACONDA_ENV_HINT: &str = "ANA_INSTALL_PATH";
25const ANACONDA_SYSROOT: &str = "mnt/sysroot";
28const OSTREE_BOOTED: &str = "run/ostree-booted";
30const RESOLVCONF: &str = "etc/resolv.conf";
32const RESOLVCONF_ORIG: &str = "etc/resolv.conf.bootc-original";
34const PROC1_ROOT: &str = "proc/1/root";
36const CGROUPFS: &str = "sys/fs/cgroup";
38const RUN_OSTREE_AUTH: &str = "run/ostree/auth.json";
40pub(crate) const RUN_BOOTC_INSTALL_RECONCILED: &str = "run/bootc-install-reconciled";
42
43fn reconcile_kargs(sysroot: &ostree::Sysroot, deployment: &ostree::Deployment) -> Result<()> {
46 let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
47 let cancellable = gio::Cancellable::NONE;
48
49 let current_kargs = deployment
50 .bootconfig()
51 .expect("bootconfig for deployment")
52 .get("options");
53 let current_kargs = current_kargs
54 .as_ref()
55 .map(|s| s.as_str())
56 .unwrap_or_default();
57 tracing::debug!("current_kargs={current_kargs}");
58 let current_kargs = ostree::KernelArgs::from_string(¤t_kargs);
59
60 let install_config = config::load_config()?;
62 let install_config_kargs = install_config
63 .as_ref()
64 .and_then(|c| c.kargs.as_ref())
65 .into_iter()
66 .flatten()
67 .map(|s| s.as_str())
68 .collect::<Vec<_>>();
69 let kargsd = crate::bootc_kargs::get_kargs_in_root(deployment_root, std::env::consts::ARCH)?;
70 let kargsd_strs = kargsd.iter_str().collect::<Vec<_>>();
71
72 current_kargs.append_argv(&install_config_kargs);
73 current_kargs.append_argv(&kargsd_strs);
74 let new_kargs = current_kargs.to_string();
75 tracing::debug!("new_kargs={new_kargs}");
76
77 sysroot.deployment_set_kargs_in_place(deployment, Some(&new_kargs), cancellable)?;
78 Ok(())
79}
80
81#[must_use]
83struct Renamer<'d> {
84 dir: &'d Dir,
85 from: &'static Utf8Path,
86 to: &'static Utf8Path,
87}
88
89impl Renamer<'_> {
90 fn _impl_drop(&mut self) -> Result<()> {
91 self.dir
92 .rename(self.from, self.dir, self.to)
93 .map_err(Into::into)
94 }
95
96 fn consume(mut self) -> Result<()> {
97 self._impl_drop()
98 }
99}
100
101impl Drop for Renamer<'_> {
102 fn drop(&mut self) {
103 let _ = self._impl_drop();
104 }
105}
106#[context("Copying host resolv.conf")]
110fn ensure_resolvconf<'d>(rootfs: &'d Dir, proc1_root: &Dir) -> Result<Option<Renamer<'d>>> {
111 let meta = rootfs
113 .symlink_metadata_optional(RESOLVCONF)
114 .context("stat")?;
115 let renamer = if meta.is_some() {
116 rootfs
117 .rename(RESOLVCONF, &rootfs, RESOLVCONF_ORIG)
118 .context("Renaming")?;
119 Some(Renamer {
120 dir: &rootfs,
121 from: RESOLVCONF_ORIG.into(),
122 to: RESOLVCONF.into(),
123 })
124 } else {
125 None
126 };
127 proc1_root
131 .copy(RESOLVCONF, rootfs, RESOLVCONF)
132 .context("Copying new resolv.conf")?;
133 Ok(renamer)
134}
135
136fn bind_from_host(
138 rootfs: &Dir,
139 src: impl AsRef<Utf8Path>,
140 target: impl AsRef<Utf8Path>,
141) -> Result<()> {
142 fn bind_from_host_impl(rootfs: &Dir, src: &Utf8Path, target: &Utf8Path) -> Result<()> {
143 rootfs.create_dir_all(target)?;
144 if rootfs.is_mountpoint(target)?.unwrap_or_default() {
145 return Ok(());
146 }
147 let target = format!("/{ANACONDA_SYSROOT}/{target}");
148 tracing::debug!("Binding {src} to {target}");
149 Command::new("nsenter")
153 .args(["-m", "-t", "1", "--", "mount", "--bind"])
154 .arg(src)
155 .arg(&target)
156 .run_capture_stderr()?;
157 Ok(())
158 }
159
160 bind_from_host_impl(rootfs, src.as_ref(), target.as_ref())
161}
162
163#[context("Ensuring cgroupfs")]
165fn ensure_cgroupfs(rootfs: &Dir) -> Result<()> {
166 bind_from_host(rootfs, CGROUPFS, CGROUPFS)
167}
168
169#[context("Propagating ostree auth")]
172fn ensure_ostree_auth(rootfs: &Dir, host_root: &Dir) -> Result<()> {
173 let Some((authpath, authfd)) =
174 ostree_ext::globals::get_global_authfile(&host_root).context("Querying authfiles")?
175 else {
176 tracing::debug!("No auth found in host");
177 return Ok(());
178 };
179 tracing::debug!("Discovered auth in host: {authpath}");
180 let mut authfd = io::BufReader::new(authfd);
181 let run_ostree_auth = Utf8Path::new(RUN_OSTREE_AUTH);
182 rootfs.create_dir_all(run_ostree_auth.parent().unwrap())?;
183 rootfs.atomic_replace_with(run_ostree_auth, |w| std::io::copy(&mut authfd, w))?;
184 Ok(())
185}
186
187#[context("Opening {PROC1_ROOT}")]
188fn open_proc1_root(rootfs: &Dir) -> Result<Dir> {
189 let proc1_root = rustix::fs::openat(
190 &rootfs.as_fd(),
191 PROC1_ROOT,
192 OFlags::CLOEXEC | OFlags::DIRECTORY,
193 Mode::empty(),
194 )?;
195 Dir::reopen_dir(&proc1_root.as_fd()).map_err(Into::into)
196}
197
198pub(crate) async fn run_from_anaconda(rootfs: &Dir) -> Result<()> {
200 crate::cli::require_root(false)?;
204 crate::cli::ensure_self_unshared_mount_namespace()?;
205
206 if std::env::var_os(ANACONDA_ENV_HINT).is_none() {
207 anyhow::bail!("Missing environment variable {ANACONDA_ENV_HINT}");
208 } else {
209 if !rootfs.try_exists(OSTREE_BOOTED)? {
212 tracing::debug!("Writing {OSTREE_BOOTED}");
213 rootfs.atomic_write(OSTREE_BOOTED, b"")?;
214 }
215 }
216
217 let proc1_root = &open_proc1_root(rootfs)?;
219
220 if proc1_root
221 .try_exists(RUN_BOOTC_INSTALL_RECONCILED)
222 .context("Querying reconciliation")?
223 {
224 println!("Reconciliation already completed.");
225 return Ok(());
226 }
227
228 ensure_cgroupfs(rootfs)?;
229 let resolvconf = ensure_resolvconf(rootfs, proc1_root)?;
231 ensure_ostree_auth(rootfs, proc1_root)?;
233
234 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path("/")));
235 sysroot
236 .load(gio::Cancellable::NONE)
237 .context("Loading sysroot")?;
238 impl_completion(rootfs, &sysroot, None).await?;
239
240 proc1_root
241 .write(RUN_BOOTC_INSTALL_RECONCILED, b"")
242 .with_context(|| format!("Writing {RUN_BOOTC_INSTALL_RECONCILED}"))?;
243 if let Some(resolvconf) = resolvconf {
244 resolvconf.consume()?;
245 }
246 Ok(())
247}
248
249pub async fn run_from_ostree(rootfs: &Dir, sysroot: &Utf8Path, stateroot: &str) -> Result<()> {
251 crate::cli::require_root(false)?;
252 let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(sysroot)));
254 sysroot.load(gio::Cancellable::NONE)?;
255
256 impl_completion(rootfs, &sysroot, Some(stateroot)).await?;
257
258 rootfs
261 .write(RUN_BOOTC_INSTALL_RECONCILED, b"")
262 .with_context(|| format!("Writing {RUN_BOOTC_INSTALL_RECONCILED}"))?;
263 Ok(())
264}
265
266pub(crate) async fn impl_completion(
275 rootfs: &Dir,
276 sysroot: &ostree::Sysroot,
277 stateroot: Option<&str>,
278) -> Result<()> {
279 const COMPLETION_JOURNAL_ID: &str = "0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4";
281 tracing::info!(
282 message_id = COMPLETION_JOURNAL_ID,
283 bootc.stateroot = stateroot.unwrap_or("default"),
284 "Starting bootc installation completion"
285 );
286
287 let deployment = &sysroot
288 .merge_deployment(stateroot)
289 .ok_or_else(|| anyhow::anyhow!("Failed to find deployment (stateroot={stateroot:?})"))?;
290 let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
291
292 let rundir = "run/bootc-install";
294 rootfs.create_dir_all(rundir)?;
295 let rundir = &rootfs.open_dir(rundir)?;
296
297 reconcile_kargs(&sysroot, deployment)?;
299
300 let bound_images = crate::boundimage::query_bound_images_for_deployment(sysroot, deployment)?;
302
303 if !bound_images.is_empty() {
304 tracing::info!(
306 message_id = COMPLETION_JOURNAL_ID,
307 bootc.bound_images_count = bound_images.len(),
308 "Found {} bound images for completion",
309 bound_images.len()
310 );
311
312 let deployment_fd = deployment_fd(sysroot, deployment)?;
314 let sepolicy = crate::lsm::new_sepolicy_at(deployment_fd)?;
315
316 let imgstorage = &CStorage::create(&sysroot_dir, &rundir, sepolicy.as_ref())?;
319 crate::boundimage::pull_images_impl(imgstorage, bound_images)
320 .await
321 .context("pulling bound images")?;
322 }
323
324 tracing::info!(
326 message_id = COMPLETION_JOURNAL_ID,
327 bootc.stateroot = stateroot.unwrap_or("default"),
328 "Successfully completed bootc installation"
329 );
330
331 Ok(())
332}