Skip to content

Commit

Permalink
WIP: Add an install command
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
cgwalters committed Dec 27, 2022
1 parent 522f8e6 commit 487d419
Show file tree
Hide file tree
Showing 12 changed files with 1,094 additions and 4 deletions.
13 changes: 13 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,24 @@ clap = { version= "3.2", features = ["derive"] }
clap_mangen = { version = "0.1", optional = true }
cap-std-ext = "1.0.1"
indicatif = "0.17.0"
once_cell = "1.9"
nix = ">= 0.24, < 0.26"
serde = { features = ["derive"], version = "1.0.125" }
serde_json = "1.0.64"
tempfile = "3.3.0"
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
tokio-util = { features = ["io-util"], version = "0.7" }
tracing = "0.1"
fn-error-context = "0.2.0"

[dependencies.uuid]
version = "1.2.2"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]


[features]
docgen = ["clap_mangen"]
Expand Down
160 changes: 160 additions & 0 deletions lib/src/blockdev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::task::Task;
use crate::utils::run_in_host_mountns;
use anyhow::{anyhow, Context, Result};
use camino::Utf8Path;
use nix::errno::Errno;
use serde::Deserialize;
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::process::Command;

#[derive(Debug, Deserialize)]
struct DevicesOutput {
blockdevices: Vec<Device>,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub(crate) struct Device {
pub(crate) name: String,
pub(crate) serial: Option<String>,
pub(crate) model: Option<String>,
pub(crate) label: Option<String>,
pub(crate) fstype: Option<String>,
pub(crate) children: Option<Vec<Device>>,
}

impl Device {
#[allow(dead_code)]
// RHEL8's lsblk doesn't have PATH, so we do it
pub(crate) fn path(&self) -> String {
format!("/dev/{}", &self.name)
}

pub(crate) fn has_children(&self) -> bool {
self.children.as_ref().map_or(false, |v| !v.is_empty())
}
}

pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
Task::new_and_run(
&format!("Wiping device {dev}"),
"wipefs",
["-a", dev.as_str()],
)
}

fn list_impl(dev: Option<&Utf8Path>) -> Result<Vec<Device>> {
let o = Command::new("lsblk")
.args(["-J", "-o", "NAME,SERIAL,MODEL,LABEL,FSTYPE"])
.args(dev)
.output()?;
if !o.status.success() {
return Err(anyhow::anyhow!("Failed to list block devices"));
}
let devs: DevicesOutput = serde_json::from_reader(&*o.stdout)?;
Ok(devs.blockdevices)
}

pub(crate) fn list_dev(dev: &Utf8Path) -> Result<Device> {
let devices = list_impl(Some(dev))?;
devices
.into_iter()
.next()
.ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
}

#[allow(dead_code)]
pub(crate) fn list() -> Result<Vec<Device>> {
list_impl(None)
}

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
// immediately, and lsblk won't pick up partition labels. Try to sleep
// our way out of this.
std::thread::sleep(std::time::Duration::from_millis(200));

let st = run_in_host_mountns("udevadm").arg("settle").status()?;
if !st.success() {
anyhow::bail!("Failed to run udevadm settle: {st:?}");
}
Ok(())
}

#[allow(unsafe_code)]
pub(crate) fn reread_partition_table(file: &mut File, retry: bool) -> Result<()> {
let fd = file.as_raw_fd();
// Reread sometimes fails inexplicably. Retry several times before
// giving up.
let max_tries = if retry { 20 } else { 1 };
for retries in (0..max_tries).rev() {
let result = unsafe { ioctl::blkrrpart(fd) };
match result {
Ok(_) => break,
Err(err) if retries == 0 && err == Errno::EINVAL => {
return Err(err)
.context("couldn't reread partition table: device may not support partitions")
}
Err(err) if retries == 0 && err == Errno::EBUSY => {
return Err(err).context("couldn't reread partition table: device is in use")
}
Err(err) if retries == 0 => return Err(err).context("couldn't reread partition table"),
Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
}
}
Ok(())
}

// create unsafe ioctl wrappers
#[allow(clippy::missing_safety_doc)]
mod ioctl {
use libc::c_int;
use nix::{ioctl_none, ioctl_read, ioctl_read_bad, libc, request_code_none};
ioctl_none!(blkrrpart, 0x12, 95);
ioctl_read_bad!(blksszget, request_code_none!(0x12, 104), c_int);
ioctl_read!(blkgetsize64, 0x12, 114, libc::size_t);
}

/// Parse a string into mibibytes
pub(crate) fn parse_size_mib(mut s: &str) -> Result<u64> {
let suffixes = [
("MiB", 1u64),
("M", 1u64),
("GiB", 1024),
("G", 1024),
("TiB", 1024 * 1024),
("T", 1024 * 1024),
];
let mut mul = 1u64;
for (suffix, imul) in suffixes {
if let Some((sv, rest)) = s.rsplit_once(suffix) {
if !rest.is_empty() {
anyhow::bail!("Trailing text after size: {rest}");
}
s = sv;
mul = imul;
}
}
let v = s.parse::<u64>()?;
Ok(v * mul)
}

#[test]
fn test_parse_size_mib() {
let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
let cases = [
("0M", 0),
("10M", 10),
("10MiB", 10),
("1G", 1024),
("9G", 9216),
("11T", 11 * 1024 * 1024),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v));
for (s, v) in ident_cases.chain(cases) {
assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
}
}
75 changes: 75 additions & 0 deletions lib/src/bootloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::os::unix::prelude::PermissionsExt;

use anyhow::{Context, Result};
use camino::Utf8Path;
use cap_std::fs::Dir;
use cap_std_ext::{cap_std, prelude::CapStdExtDirExt};
use fn_error_context::context;

use crate::task::Task;

const GRUB_BOOT_UUID_FILE: &str = "bootuuid.cfg";
const STATIC_GRUB_CFG: &str = "
if [ -e (md/md-boot) ]; then
# The search command might pick a RAID component rather than the RAID,
# since the /boot RAID currently uses superblock 1.0. See the comment in
# the main grub.cfg.
set prefix=md/md-boot
else
if [ -f ${config_directory}/bootuuid.cfg ]; then
source ${config_directory}/bootuuid.cfg
fi
if [ -n \"${BOOT_UUID}\" ]; then
search --fs-uuid \"${BOOT_UUID}\" --set prefix --no-floppy
else
search --label boot --set prefix --no-floppy
fi
fi
set prefix=($prefix)/grub2
configfile $prefix/grub.cfg
boot
";

#[context("Installing bootloader")]
pub(crate) fn install_via_bootupd(
device: &Utf8Path,
rootfs: &Utf8Path,
boot_uuid: &uuid::Uuid,
) -> Result<()> {
Task::new_and_run(
"Running bootupctl to install bootloader",
"bootupctl",
["backend", "install", "--src-root", "/", rootfs.as_str()],
)?;

let bootfs = &rootfs.join("boot");
let grub2 = &bootfs.join("grub2");
std::fs::create_dir(grub2).context("creating boot/grub2")?;
let grub2 = Dir::open_ambient_dir(grub2, cap_std::ambient_authority())?;
grub2
.atomic_write_with_perms(
"grub.cfg",
STATIC_GRUB_CFG,
cap_std::fs::Permissions::from_mode(0o600),
)
.context("Writing grub.cfg")?;

let grub2_uuid_contents = format!("set BOOT_UUID=\"{boot_uuid}\"\n");
grub2
.atomic_write(GRUB_BOOT_UUID_FILE, grub2_uuid_contents)
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;

let mut t = Task::new("Installing BIOS grub2", "grub2-install");
t.cmd.args([
"--target",
"i386-pc",
"--boot-directory",
bootfs.as_str(),
"--modules",
"mdraid1x",
]);
t.cmd.arg(device);
t.run()?;

Ok(())
}
14 changes: 11 additions & 3 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub(crate) enum Opt {
Switch(SwitchOpts),
/// Display status
Status(StatusOpts),
/// Install to the target block device
Install(crate::install::InstallOpts),
#[clap(hide(true))]
#[cfg(feature = "docgen")]
Man(ManOpts),
Expand All @@ -96,20 +98,25 @@ pub(crate) enum Opt {
/// `/sysroot` read-write
/// TODO use https://github.com/ostreedev/ostree/pull/2779 once
/// we can depend on a new enough ostree
async fn ensure_self_unshared_mount_namespace() -> Result<()> {
pub(crate) async fn ensure_self_unshared_mount_namespace() -> Result<()> {
let uid = cap_std_ext::rustix::process::getuid();
if !uid.is_root() {
return Ok(());
}
let recurse_env = "_ostree_unshared";
let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
// If we already appear to be in a mount namespace, we're done
// If we already appear to be in a mount namespace, or we're already pid1, we're done
if ns_pid1 != ns_self {
return Ok(());
}
if std::env::var_os(recurse_env).is_some() {
anyhow::bail!("Failed to unshare mount namespace");
let am_pid1 = cap_std_ext::rustix::process::getpid().is_init();
if am_pid1 {
return Ok(());
} else {
anyhow::bail!("Failed to unshare mount namespace");
}
}
let self_exe = std::fs::read_link("/proc/self/exe")?;
let mut cmd = std::process::Command::new("unshare");
Expand Down Expand Up @@ -302,6 +309,7 @@ where
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::Install(opts) => crate::install::install(opts).await,
Opt::Status(opts) => super::status::status(opts).await,
#[cfg(feature = "docgen")]
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
Expand Down
45 changes: 45 additions & 0 deletions lib/src/containerenv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Helpers for parsing the `/run/.containerenv` file generated by podman.

use std::fs::File;
use std::io::{BufRead, BufReader};

use anyhow::{Context, Result};

const PATH: &str = "/run/.containerenv";

#[derive(Debug, Default)]
pub(crate) struct ContainerExecutionInfo {
pub(crate) engine: String,
pub(crate) name: String,
pub(crate) id: String,
pub(crate) image: String,
pub(crate) imageid: String,
}

/// Load and parse the `/run/.containerenv` file.
pub(crate) fn get_container_execution_info() -> Result<ContainerExecutionInfo> {
let f = File::open(PATH)
.with_context(|| format!("Opening {PATH}"))
.map(BufReader::new)?;
let mut r = ContainerExecutionInfo::default();
for line in f.lines() {
let line = line?;
let line = line.trim();
let (k, v) = if let Some(v) = line.split_once('=') {
v
} else {
continue;
};
// Assuming there's no quotes here
let v = v.trim_start_matches('"').trim_end_matches('"');
match k {
"engine" => r.engine = v.to_string(),
"name" => r.name = v.to_string(),
"id" => r.id = v.to_string(),
"image" => r.image = v.to_string(),
"imageid" => r.imageid = v.to_string(),
_ => {}
}
}
Ok(r)
}
Loading

0 comments on commit 487d419

Please sign in to comment.