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 Fedora Cloud VM with `/dev/vda` being an extra mounted
disk:

```
$ 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
```

Signed-off-by: Colin Walters <walters@verbum.org>
  • Loading branch information
cgwalters committed Jan 18, 2023
1 parent 51aef4d commit 1927517
Show file tree
Hide file tree
Showing 16 changed files with 1,757 additions and 7 deletions.
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ jobs:
name: "Privileged testing"
needs: build
runs-on: ubuntu-latest
container:
image: quay.io/fedora/fedora-coreos:testing-devel
options: "--privileged --pid=host -v /run/systemd:/run/systemd -v /:/run/host"
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand All @@ -125,7 +122,7 @@ jobs:
with:
name: bootc
- name: Install
run: install bootc /usr/bin && rm -v bootc
run: sudo install bootc /usr/bin && rm -v bootc
- name: Integration tests
run: bootc internal-tests run-privileged-integration
run: sudo podman run --rm -ti --privileged -v /run/systemd:/run/systemd -v /:/run/host -v /usr/bin/bootc:/usr/bin/bootc --pid=host quay.io/fedora/fedora-coreos:testing-devel bootc internal-tests run-privileged-integration

8 changes: 8 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@ ostree-ext = "0.10.5"
clap = { version= "3.2", features = ["derive"] }
clap_mangen = { version = "0.1", optional = true }
cap-std-ext = "1.0.1"
hex = "^0.4"
fn-error-context = "0.2.0"
gvariant = "0.4.0"
indicatif = "0.17.0"
libc = "^0.2"
once_cell = "1.9"
openssl = "^0.10"
nix = ">= 0.24, < 0.26"
serde = { features = ["derive"], version = "1.0.125" }
serde_json = "1.0.64"
serde_with = ">= 1.9.4, < 2"
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
tokio-util = { features = ["io-util"], version = "0.7" }
tracing = "0.1"
tempfile = "3.3.0"
xshell = { version = "0.2", optional = true }
uuid = { version = "1.2.2", features = ["v4"] }

[features]
default = []
Expand Down
162 changes: 162 additions & 0 deletions lib/src/blockdev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use crate::task::Task;
use crate::utils::run_in_host_mountns;
use anyhow::{anyhow, Context, Result};
use camino::Utf8Path;
use fn_error_context::context;
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)
}

#[context("Listing device {dev}")]
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}");
}
}
105 changes: 105 additions & 0 deletions lib/src/bootloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::os::unix::prelude::PermissionsExt;

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

use crate::task::Task;

/// This variable is referenced by our GRUB fragment
pub(crate) const IGNITION_VARIABLE: &str = "$ignition_firstboot";
const GRUB_BOOT_UUID_FILE: &str = "bootuuid.cfg";
const STATIC_GRUB_CFG: &str = include_str!("grub.cfg");
const STATIC_GRUB_CFG_EFI: &str = include_str!("grub-efi.cfg");

fn install_grub2_efi(efidir: &Dir, uuid: &str) -> Result<()> {
let mut vendordir = None;
let efidir = efidir.open_dir("EFI").context("Opening EFI/")?;
for child in efidir.entries()? {
let child = child?;
let name = child.file_name();
let name = if let Some(name) = name.to_str() {
name
} else {
continue;
};
if name == "BOOT" {
continue;
}
if !child.file_type()?.is_dir() {
continue;
}
vendordir = Some(child.open_dir()?);
break;
}
let vendordir = vendordir.ok_or_else(|| anyhow::anyhow!("Failed to find EFI vendor dir"))?;
vendordir
.atomic_write("grub.cfg", STATIC_GRUB_CFG_EFI)
.context("Writing static EFI grub.cfg")?;
vendordir
.atomic_write(GRUB_BOOT_UUID_FILE, uuid)
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;

Ok(())
}

#[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 grub2_uuid_contents = format!("set BOOT_UUID=\"{boot_uuid}\"\n");

let bootfs = &rootfs.join("boot");

{
let efidir = Dir::open_ambient_dir(&bootfs.join("efi"), cap_std::ambient_authority())?;
install_grub2_efi(&efidir, &grub2_uuid_contents)?;
}

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())?;
// Mode 0700 to support passwords etc.
grub2.set_permissions(".", Permissions::from_mode(0o700))?;
grub2
.atomic_write_with_perms(
"grub.cfg",
STATIC_GRUB_CFG,
cap_std::fs::Permissions::from_mode(0o600),
)
.context("Writing grub.cfg")?;

grub2
.atomic_write_with_perms(
GRUB_BOOT_UUID_FILE,
grub2_uuid_contents,
Permissions::from_mode(0o644),
)
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;

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

Ok(())
}
3 changes: 3 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ pub(crate) enum Opt {
Switch(SwitchOpts),
/// Display status
Status(StatusOpts),
/// Install to the target block device
Install(crate::install::InstallOpts),
/// Internal integration testing helpers.
#[clap(hide(true), subcommand)]
#[cfg(feature = "internal-testing-api")]
Expand Down Expand Up @@ -319,6 +321,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 = "internal-testing-api")]
Opt::InternalTests(ref opts) => {
Expand Down
47 changes: 47 additions & 0 deletions lib/src/containerenv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Helpers for parsing the `/run/.containerenv` file generated by podman.

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

use anyhow::{Context, Result};
use fn_error_context::context;

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.
#[context("Parsing {PATH}")]
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)
}
18 changes: 18 additions & 0 deletions lib/src/grub-efi.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
Loading

0 comments on commit 1927517

Please sign in to comment.