Skip to content

Commit 8078700

Browse files
committed
Add install-to-filesystem
This will address the use case of having external code set up the block devices and (empty) filesystems. For example, one could use a different privileged container, Anaconda, etc. Closes: #54 Signed-off-by: Colin Walters <[email protected]>
1 parent adf16d5 commit 8078700

File tree

10 files changed

+427
-29
lines changed

10 files changed

+427
-29
lines changed

lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ libc = "^0.2"
2323
once_cell = "1.9"
2424
openssl = "^0.10"
2525
nix = ">= 0.24, < 0.26"
26+
regex = "1.7.1"
2627
serde = { features = ["derive"], version = "1.0.125" }
2728
serde_json = "1.0.64"
2829
serde_with = ">= 1.9.4, < 2"

lib/src/blockdev.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use anyhow::{anyhow, Context, Result};
44
use camino::Utf8Path;
55
use fn_error_context::context;
66
use nix::errno::Errno;
7+
use once_cell::sync::Lazy;
8+
use regex::Regex;
79
use serde::Deserialize;
10+
use std::collections::HashMap;
811
use std::fs::File;
912
use std::os::unix::io::AsRawFd;
1013
use std::process::Command;
@@ -39,7 +42,7 @@ impl Device {
3942

4043
pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
4144
Task::new_and_run(
42-
&format!("Wiping device {dev}"),
45+
format!("Wiping device {dev}"),
4346
"wipefs",
4447
["-a", dev.as_str()],
4548
)
@@ -109,6 +112,67 @@ pub(crate) fn reread_partition_table(file: &mut File, retry: bool) -> Result<()>
109112
Ok(())
110113
}
111114

115+
/// Runs the provided Command object, captures its stdout, and swallows its stderr except on
116+
/// failure. Returns a Result<String> describing whether the command failed, and if not, its
117+
/// standard output. Output is assumed to be UTF-8. Errors are adequately prefixed with the full
118+
/// command.
119+
pub(crate) fn cmd_output(cmd: &mut Command) -> Result<String> {
120+
let result = cmd
121+
.output()
122+
.with_context(|| format!("running {:#?}", cmd))?;
123+
if !result.status.success() {
124+
eprint!("{}", String::from_utf8_lossy(&result.stderr));
125+
anyhow::bail!("{:#?} failed with {}", cmd, result.status);
126+
}
127+
String::from_utf8(result.stdout)
128+
.with_context(|| format!("decoding as UTF-8 output of `{:#?}`", cmd))
129+
}
130+
131+
/// Parse key-value pairs from lsblk --pairs.
132+
/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't.
133+
fn split_lsblk_line(line: &str) -> HashMap<String, String> {
134+
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap());
135+
let mut fields: HashMap<String, String> = HashMap::new();
136+
for cap in REGEX.captures_iter(line) {
137+
fields.insert(cap[1].to_string(), cap[2].to_string());
138+
}
139+
fields
140+
}
141+
142+
/// This is a bit fuzzy, but... this function will return every block device in the parent
143+
/// hierarchy of `device` capable of containing other partitions. So e.g. parent devices of type
144+
/// "part" doesn't match, but "disk" and "mpath" does.
145+
pub(crate) fn find_parent_devices(device: &str) -> Result<Vec<String>> {
146+
let mut cmd = Command::new("lsblk");
147+
// Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option
148+
cmd.arg("--pairs")
149+
.arg("--paths")
150+
.arg("--inverse")
151+
.arg("--output")
152+
.arg("NAME,TYPE")
153+
.arg(device);
154+
let output = cmd_output(&mut cmd)?;
155+
let mut parents = Vec::new();
156+
// skip first line, which is the device itself
157+
for line in output.lines().skip(1) {
158+
let dev = split_lsblk_line(line);
159+
let name = dev
160+
.get("NAME")
161+
.with_context(|| format!("device in hierarchy of {device} missing NAME"))?;
162+
let kind = dev
163+
.get("TYPE")
164+
.with_context(|| format!("device in hierarchy of {device} missing TYPE"))?;
165+
if kind == "disk" {
166+
parents.push(name.clone());
167+
} else if kind == "mpath" {
168+
parents.push(name.clone());
169+
// we don't need to know what disks back the multipath
170+
break;
171+
}
172+
}
173+
Ok(parents)
174+
}
175+
112176
// create unsafe ioctl wrappers
113177
#[allow(clippy::missing_safety_doc)]
114178
mod ioctl {

lib/src/bootloader.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub(crate) const IGNITION_VARIABLE: &str = "$ignition_firstboot";
1515
const GRUB_BOOT_UUID_FILE: &str = "bootuuid.cfg";
1616
const STATIC_GRUB_CFG: &str = include_str!("grub.cfg");
1717
const STATIC_GRUB_CFG_EFI: &str = include_str!("grub-efi.cfg");
18+
/// The name of the mountpoint for efi (as a subdirectory of /boot, or at the toplevel)
19+
pub(crate) const EFI_DIR: &str = "efi";
1820

1921
fn install_grub2_efi(efidir: &Dir, uuid: &str) -> Result<()> {
2022
let mut vendordir = None;
@@ -64,7 +66,7 @@ pub(crate) fn install_via_bootupd(
6466
let bootfs = &rootfs.join("boot");
6567

6668
{
67-
let efidir = Dir::open_ambient_dir(&bootfs.join("efi"), cap_std::ambient_authority())?;
69+
let efidir = Dir::open_ambient_dir(bootfs.join("efi"), cap_std::ambient_authority())?;
6870
install_grub2_efi(&efidir, &grub2_uuid_contents)?;
6971
}
7072

lib/src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ pub(crate) enum TestingOpts {
8282
RunPrivilegedIntegration {},
8383
/// Execute integration tests that target a not-privileged ostree container
8484
RunContainerIntegration {},
85+
/// Block device setup for testing
86+
PrepTestInstallFilesystem { blockdev: Utf8PathBuf },
87+
/// e2e test of install-to-filesystem
88+
TestInstallFilesystem {
89+
image: String,
90+
blockdev: Utf8PathBuf,
91+
},
8592
}
8693

8794
/// Deploy and upgrade via bootable container images.
@@ -99,6 +106,9 @@ pub(crate) enum Opt {
99106
/// Install to the target block device
100107
#[cfg(feature = "install")]
101108
Install(crate::install::InstallOpts),
109+
/// Install to the target filesystem.
110+
#[cfg(feature = "install")]
111+
InstallToFilesystem(crate::install::InstallToFilesystemOpts),
102112
/// Internal integration testing helpers.
103113
#[clap(hide(true), subcommand)]
104114
#[cfg(feature = "internal-testing-api")]
@@ -336,6 +346,8 @@ where
336346
Opt::Switch(opts) => switch(opts).await,
337347
#[cfg(feature = "install")]
338348
Opt::Install(opts) => crate::install::install(opts).await,
349+
#[cfg(feature = "install")]
350+
Opt::InstallToFilesystem(opts) => crate::install::install_to_filesystem(opts).await,
339351
Opt::Status(opts) => super::status::status(opts).await,
340352
#[cfg(feature = "internal-testing-api")]
341353
Opt::InternalTests(opts) => crate::privtests::run(opts).await,

lib/src/ignition.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ mod tests {
313313
(false, "sha512-cdaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f")
314314
];
315315
for (valid, hash_arg) in &hash_args {
316-
let hasher = IgnitionHash::from_str(&hash_arg).unwrap();
316+
let hasher = IgnitionHash::from_str(hash_arg).unwrap();
317317
let mut rd = std::io::Cursor::new(&input);
318318
assert!(hasher.validate(&mut rd).is_ok() == *valid);
319319
}

0 commit comments

Comments
 (0)