Skip to content

Commit 1ea8c45

Browse files
committed
WIP: Add an install command
Usage example from an FCOS VM with `/dev/vda` being an extra mounted disk, and pulling updated binaries for `bootupctl` from the host: ``` $ podman run --privileged --pid=host --net=none -v /usr/bin/bootc:/usr/bin/bootc -v /usr/bin/bootupctl:/usr/bin/bootupctl quay.io/fedora/fedora-coreos:testing-devel bootc install /dev/vda ```
1 parent 522f8e6 commit 1ea8c45

11 files changed

+1047
-2
lines changed

lib/Cargo.toml

+13
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,24 @@ clap = { version= "3.2", features = ["derive"] }
1616
clap_mangen = { version = "0.1", optional = true }
1717
cap-std-ext = "1.0.1"
1818
indicatif = "0.17.0"
19+
once_cell = "1.9"
20+
nix = ">= 0.24, < 0.26"
1921
serde = { features = ["derive"], version = "1.0.125" }
2022
serde_json = "1.0.64"
23+
tempfile = "3.3.0"
2124
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
2225
tokio-util = { features = ["io-util"], version = "0.7" }
2326
tracing = "0.1"
27+
fn-error-context = "0.2.0"
28+
29+
[dependencies.uuid]
30+
version = "1.2.2"
31+
features = [
32+
"v4", # Lets you generate random UUIDs
33+
"fast-rng", # Use a faster (but still sufficiently random) RNG
34+
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
35+
]
36+
2437

2538
[features]
2639
docgen = ["clap_mangen"]

lib/src/blockdev.rs

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use crate::task::Task;
2+
use crate::utils::run_in_host_mountns;
3+
use anyhow::{anyhow, Context, Result};
4+
use camino::Utf8Path;
5+
use nix::errno::Errno;
6+
use serde::Deserialize;
7+
use std::fs::File;
8+
use std::os::unix::io::AsRawFd;
9+
use std::process::Command;
10+
11+
#[derive(Debug, Deserialize)]
12+
struct DevicesOutput {
13+
blockdevices: Vec<Device>,
14+
}
15+
16+
#[allow(dead_code)]
17+
#[derive(Debug, Deserialize)]
18+
pub(crate) struct Device {
19+
pub(crate) name: String,
20+
pub(crate) serial: Option<String>,
21+
pub(crate) model: Option<String>,
22+
pub(crate) label: Option<String>,
23+
pub(crate) fstype: Option<String>,
24+
pub(crate) children: Option<Vec<Device>>,
25+
}
26+
27+
impl Device {
28+
#[allow(dead_code)]
29+
// RHEL8's lsblk doesn't have PATH, so we do it
30+
pub(crate) fn path(&self) -> String {
31+
format!("/dev/{}", &self.name)
32+
}
33+
34+
pub(crate) fn has_children(&self) -> bool {
35+
self.children.as_ref().map_or(false, |v| !v.is_empty())
36+
}
37+
}
38+
39+
pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
40+
Task::new_and_run(
41+
&format!("Wiping device {dev}"),
42+
"wipefs",
43+
["-a", dev.as_str()],
44+
)
45+
}
46+
47+
fn list_impl(dev: Option<&Utf8Path>) -> Result<Vec<Device>> {
48+
let o = Command::new("lsblk")
49+
.args(["-J", "-o", "NAME,SERIAL,MODEL,LABEL,FSTYPE"])
50+
.args(dev)
51+
.output()?;
52+
if !o.status.success() {
53+
return Err(anyhow::anyhow!("Failed to list block devices"));
54+
}
55+
let devs: DevicesOutput = serde_json::from_reader(&*o.stdout)?;
56+
Ok(devs.blockdevices)
57+
}
58+
59+
pub(crate) fn list_dev(dev: &Utf8Path) -> Result<Device> {
60+
let devices = list_impl(Some(dev))?;
61+
devices
62+
.into_iter()
63+
.next()
64+
.ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
65+
}
66+
67+
#[allow(dead_code)]
68+
pub(crate) fn list() -> Result<Vec<Device>> {
69+
list_impl(None)
70+
}
71+
72+
pub(crate) fn udev_settle() -> Result<()> {
73+
// There's a potential window after rereading the partition table where
74+
// udevd hasn't yet received updates from the kernel, settle will return
75+
// immediately, and lsblk won't pick up partition labels. Try to sleep
76+
// our way out of this.
77+
std::thread::sleep(std::time::Duration::from_millis(200));
78+
79+
let st = run_in_host_mountns("udevadm").arg("settle").status()?;
80+
if !st.success() {
81+
anyhow::bail!("Failed to run udevadm settle: {st:?}");
82+
}
83+
Ok(())
84+
}
85+
86+
#[allow(unsafe_code)]
87+
pub(crate) fn reread_partition_table(file: &mut File, retry: bool) -> Result<()> {
88+
let fd = file.as_raw_fd();
89+
// Reread sometimes fails inexplicably. Retry several times before
90+
// giving up.
91+
let max_tries = if retry { 20 } else { 1 };
92+
for retries in (0..max_tries).rev() {
93+
let result = unsafe { ioctl::blkrrpart(fd) };
94+
match result {
95+
Ok(_) => break,
96+
Err(err) if retries == 0 && err == Errno::EINVAL => {
97+
return Err(err)
98+
.context("couldn't reread partition table: device may not support partitions")
99+
}
100+
Err(err) if retries == 0 && err == Errno::EBUSY => {
101+
return Err(err).context("couldn't reread partition table: device is in use")
102+
}
103+
Err(err) if retries == 0 => return Err(err).context("couldn't reread partition table"),
104+
Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
105+
}
106+
}
107+
Ok(())
108+
}
109+
110+
// create unsafe ioctl wrappers
111+
#[allow(clippy::missing_safety_doc)]
112+
mod ioctl {
113+
use libc::c_int;
114+
use nix::{ioctl_none, ioctl_read, ioctl_read_bad, libc, request_code_none};
115+
ioctl_none!(blkrrpart, 0x12, 95);
116+
ioctl_read_bad!(blksszget, request_code_none!(0x12, 104), c_int);
117+
ioctl_read!(blkgetsize64, 0x12, 114, libc::size_t);
118+
}
119+
120+
/// Parse a string into mibibytes
121+
pub(crate) fn parse_size_mib(mut s: &str) -> Result<u64> {
122+
let suffixes = [
123+
("MiB", 1u64),
124+
("M", 1u64),
125+
("GiB", 1024),
126+
("G", 1024),
127+
("TiB", 1024 * 1024),
128+
("T", 1024 * 1024),
129+
];
130+
let mut mul = 1u64;
131+
for (suffix, imul) in suffixes {
132+
if let Some((sv, rest)) = s.rsplit_once(suffix) {
133+
if !rest.is_empty() {
134+
anyhow::bail!("Trailing text after size: {rest}");
135+
}
136+
s = sv;
137+
mul = imul;
138+
}
139+
}
140+
let v = s.parse::<u64>()?;
141+
Ok(v * mul)
142+
}
143+
144+
#[test]
145+
fn test_parse_size_mib() {
146+
let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
147+
let cases = [
148+
("0M", 0),
149+
("10M", 10),
150+
("10MiB", 10),
151+
("1G", 1024),
152+
("9G", 9216),
153+
("11T", 11 * 1024 * 1024),
154+
]
155+
.into_iter()
156+
.map(|(k, v)| (k.to_string(), v));
157+
for (s, v) in ident_cases.chain(cases) {
158+
assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
159+
}
160+
}

lib/src/bootloader.rs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::os::unix::prelude::PermissionsExt;
2+
3+
use anyhow::{Context, Result};
4+
use camino::Utf8Path;
5+
use cap_std::fs::Dir;
6+
use cap_std_ext::{cap_std, prelude::CapStdExtDirExt};
7+
use fn_error_context::context;
8+
9+
use crate::task::Task;
10+
11+
const GRUB_BOOT_UUID_FILE: &str = "bootuuid.cfg";
12+
const STATIC_GRUB_CFG: &str = "
13+
if [ -e (md/md-boot) ]; then
14+
# The search command might pick a RAID component rather than the RAID,
15+
# since the /boot RAID currently uses superblock 1.0. See the comment in
16+
# the main grub.cfg.
17+
set prefix=md/md-boot
18+
else
19+
if [ -f ${config_directory}/bootuuid.cfg ]; then
20+
source ${config_directory}/bootuuid.cfg
21+
fi
22+
if [ -n \"${BOOT_UUID}\" ]; then
23+
search --fs-uuid \"${BOOT_UUID}\" --set prefix --no-floppy
24+
else
25+
search --label boot --set prefix --no-floppy
26+
fi
27+
fi
28+
set prefix=($prefix)/grub2
29+
configfile $prefix/grub.cfg
30+
boot
31+
";
32+
33+
#[context("Installing bootloader")]
34+
pub(crate) fn install_via_bootupd(
35+
device: &Utf8Path,
36+
rootfs: &Utf8Path,
37+
boot_uuid: &uuid::Uuid,
38+
) -> Result<()> {
39+
Task::new_and_run(
40+
"Running bootupctl to install bootloader",
41+
"bootupctl",
42+
["backend", "install", "--src-root", "/", rootfs.as_str()],
43+
)?;
44+
45+
let bootfs = &rootfs.join("boot");
46+
let grub2 = &bootfs.join("grub2");
47+
std::fs::create_dir(grub2).context("creating boot/grub2")?;
48+
let grub2 = Dir::open_ambient_dir(grub2, cap_std::ambient_authority())?;
49+
grub2
50+
.atomic_write_with_perms(
51+
"grub.cfg",
52+
STATIC_GRUB_CFG,
53+
cap_std::fs::Permissions::from_mode(0o600),
54+
)
55+
.context("Writing grub.cfg")?;
56+
57+
let grub2_uuid_contents = format!("set BOOT_UUID=\"{boot_uuid}\"\n");
58+
grub2
59+
.atomic_write(GRUB_BOOT_UUID_FILE, grub2_uuid_contents)
60+
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;
61+
62+
let mut t = Task::new("Installing BIOS grub2", "grub2-install");
63+
t.cmd.args([
64+
"--target",
65+
"i386-pc",
66+
"--boot-directory",
67+
bootfs.as_str(),
68+
"--modules",
69+
"mdraid1x",
70+
]);
71+
t.cmd.arg(device);
72+
t.run()?;
73+
74+
Ok(())
75+
}

lib/src/cli.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ pub(crate) enum Opt {
8787
Switch(SwitchOpts),
8888
/// Display status
8989
Status(StatusOpts),
90+
/// Install to the target block device
91+
Install(crate::install::InstallOpts),
9092
#[clap(hide(true))]
9193
#[cfg(feature = "docgen")]
9294
Man(ManOpts),
@@ -96,7 +98,7 @@ pub(crate) enum Opt {
9698
/// `/sysroot` read-write
9799
/// TODO use https://github.com/ostreedev/ostree/pull/2779 once
98100
/// we can depend on a new enough ostree
99-
async fn ensure_self_unshared_mount_namespace() -> Result<()> {
101+
pub(crate) async fn ensure_self_unshared_mount_namespace() -> Result<()> {
100102
let uid = cap_std_ext::rustix::process::getuid();
101103
if !uid.is_root() {
102104
return Ok(());
@@ -302,6 +304,7 @@ where
302304
match opt {
303305
Opt::Upgrade(opts) => upgrade(opts).await,
304306
Opt::Switch(opts) => switch(opts).await,
307+
Opt::Install(opts) => crate::install::install(opts).await,
305308
Opt::Status(opts) => super::status::status(opts).await,
306309
#[cfg(feature = "docgen")]
307310
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),

lib/src/containerenv.rs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Helpers for parsing the `/run/.containerenv` file generated by podman.
2+
3+
use std::fs::File;
4+
use std::io::{BufRead, BufReader};
5+
6+
use anyhow::{Context, Result};
7+
8+
const PATH: &str = "/run/.containerenv";
9+
10+
#[derive(Debug, Default)]
11+
pub(crate) struct ContainerExecutionInfo {
12+
pub(crate) engine: String,
13+
pub(crate) name: String,
14+
pub(crate) id: String,
15+
pub(crate) image: String,
16+
pub(crate) imageid: String,
17+
}
18+
19+
/// Load and parse the `/run/.containerenv` file.
20+
pub(crate) fn get_container_execution_info() -> Result<ContainerExecutionInfo> {
21+
let f = File::open(PATH)
22+
.with_context(|| format!("Opening {PATH}"))
23+
.map(BufReader::new)?;
24+
let mut r = ContainerExecutionInfo::default();
25+
for line in f.lines() {
26+
let line = line?;
27+
let line = line.trim();
28+
let (k, v) = if let Some(v) = line.split_once('=') {
29+
v
30+
} else {
31+
continue;
32+
};
33+
// Assuming there's no quotes here
34+
let v = v.trim_start_matches('"').trim_end_matches('"');
35+
match k {
36+
"engine" => r.engine = v.to_string(),
37+
"name" => r.name = v.to_string(),
38+
"id" => r.id = v.to_string(),
39+
"image" => r.image = v.to_string(),
40+
"imageid" => r.imageid = v.to_string(),
41+
_ => {}
42+
}
43+
}
44+
Ok(r)
45+
}

0 commit comments

Comments
 (0)