diff --git a/lib/src/blockdev.rs b/lib/src/blockdev.rs index 455aa0187..e9a76519b 100644 --- a/lib/src/blockdev.rs +++ b/lib/src/blockdev.rs @@ -1,7 +1,7 @@ use crate::install::run_in_host_mountns; use crate::task::Task; use anyhow::{anyhow, Context, Result}; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use fn_error_context::context; use nix::errno::Errno; use once_cell::sync::Lazy; @@ -10,6 +10,7 @@ use serde::Deserialize; use std::collections::HashMap; use std::fs::File; use std::os::unix::io::AsRawFd; +use std::path::Path; use std::process::Command; #[derive(Debug, Deserialize)] @@ -75,6 +76,55 @@ pub(crate) fn list() -> Result> { list_impl(None) } +pub(crate) struct LoopbackDevice { + pub(crate) dev: Option, +} + +impl LoopbackDevice { + // Create a new loopback block device targeting the provided file path. + pub(crate) fn new(path: &Path) -> Result { + let dev = Task::new("losetup", "losetup") + .args(["--show", "-P", "--find"]) + .arg(path) + .quiet() + .read()?; + let dev = Utf8PathBuf::from(dev.trim()); + Ok(Self { dev: Some(dev) }) + } + + // Access the path to the loopback block device. + pub(crate) fn path(&self) -> &Utf8Path { + // SAFETY: The option cannot be destructured until we are dropped + self.dev.as_deref().unwrap() + } + + // Shared backend for our `close` and `drop` implementations. + fn impl_close(&mut self) -> Result<()> { + // SAFETY: This is the only place we take the option + let dev = if let Some(dev) = self.dev.take() { + dev + } else { + return Ok(()); + }; + Task::new("losetup", "losetup") + .args(["-d", dev.as_str()]) + .quiet() + .run() + } + + /// Consume this device, unmounting it. + pub(crate) fn close(mut self) -> Result<()> { + self.impl_close() + } +} + +impl Drop for LoopbackDevice { + fn drop(&mut self) { + // Best effort to unmount if we're dropped without invoking `close` + let _ = self.impl_close(); + } +} + pub(crate) fn udev_settle() -> Result<()> { // There's a potential window after rereading the partition table where // udevd hasn't yet received updates from the kernel, settle will return diff --git a/lib/src/install.rs b/lib/src/install.rs index c38478c04..b21e10b67 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -144,6 +144,10 @@ pub(crate) struct InstallToDiskOpts { #[clap(flatten)] #[serde(flatten)] pub(crate) config_opts: InstallConfigOpts, + + /// Instead of targeting a block device, write to a file via loopback. + #[clap(long)] + pub(crate) via_loopback: bool, } #[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -1039,13 +1043,26 @@ fn installation_complete() { /// Implementation of the `bootc install to-disk` CLI command. pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> { - let block_opts = opts.block_opts; + let mut block_opts = opts.block_opts; let target_blockdev_meta = block_opts .device .metadata() .with_context(|| format!("Querying {}", &block_opts.device))?; - if !target_blockdev_meta.file_type().is_block_device() { - anyhow::bail!("Not a block device: {}", block_opts.device); + let mut loopback = None; + if opts.via_loopback { + if !target_blockdev_meta.file_type().is_file() { + anyhow::bail!( + "Not a regular file (to be used via loopback): {}", + block_opts.device + ); + } + let loopback_dev = crate::blockdev::LoopbackDevice::new(block_opts.device.as_std_path())?; + block_opts.device = loopback_dev.path().into(); + loopback = Some(loopback_dev); + } else { + if !target_blockdev_meta.file_type().is_block_device() { + anyhow::bail!("Not a block device: {}", block_opts.device); + } } let state = prepare_install(opts.config_opts, opts.target_opts).await?; @@ -1069,6 +1086,10 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> { Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])?; } + if let Some(loopback_dev) = loopback { + loopback_dev.close()?; + } + installation_complete(); Ok(()) diff --git a/lib/src/privtests.rs b/lib/src/privtests.rs index d1290778a..310a16ebb 100644 --- a/lib/src/privtests.rs +++ b/lib/src/privtests.rs @@ -1,45 +1,18 @@ use std::process::Command; use anyhow::Result; -use camino::{Utf8Path, Utf8PathBuf}; +use camino::Utf8Path; use fn_error_context::context; use rustix::fd::AsFd; use xshell::{cmd, Shell}; -use crate::spec::HostType; +use crate::blockdev::LoopbackDevice; use super::cli::TestingOpts; use super::spec::Host; const IMGSIZE: u64 = 20 * 1024 * 1024 * 1024; -struct LoopbackDevice { - #[allow(dead_code)] - tmpf: tempfile::NamedTempFile, - dev: Utf8PathBuf, -} - -impl LoopbackDevice { - fn new_temp(sh: &xshell::Shell) -> Result { - let mut tmpd = tempfile::NamedTempFile::new_in("/var/tmp")?; - rustix::fs::ftruncate(tmpd.as_file_mut().as_fd(), IMGSIZE)?; - let diskpath = tmpd.path(); - let path = cmd!(sh, "losetup --find --show {diskpath}").read()?; - Ok(Self { - tmpf: tmpd, - dev: path.into(), - }) - } -} - -impl Drop for LoopbackDevice { - fn drop(&mut self) { - let _ = Command::new("losetup") - .args(["-d", self.dev.as_str()]) - .status(); - } -} - fn init_ostree(sh: &Shell, rootfs: &Utf8Path) -> Result<()> { cmd!(sh, "ostree admin init-fs --modern {rootfs}").run()?; Ok(()) @@ -49,8 +22,10 @@ fn init_ostree(sh: &Shell, rootfs: &Utf8Path) -> Result<()> { fn run_bootc_status() -> Result<()> { let sh = Shell::new()?; - let loopdev = LoopbackDevice::new_temp(&sh)?; - let devpath = &loopdev.dev; + let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; + rustix::fs::ftruncate(tmpdisk.as_file_mut().as_fd(), IMGSIZE)?; + let loopdev = LoopbackDevice::new(tmpdisk.path())?; + let devpath = loopdev.path(); println!("Using {devpath:?}"); let td = tempfile::tempdir()?;