Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an install command #30

Merged
merged 1 commit into from Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
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

6 changes: 3 additions & 3 deletions .gitignore
@@ -1,8 +1,8 @@
example

.cosa
_kola_temp
bootc.tar.zst

# Added by cargo

/target
Cargo.lock
bootc.tar.zst
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -10,7 +10,7 @@ bin-archive: all
$(MAKE) install DESTDIR=tmp-install && tar --zstd -C tmp-install -cf bootc.tar.zst . && rm tmp-install -rf

install-kola-tests:
install -D -t $(DESTDIR)$(prefix)/lib/coreos-assembler/tests/kola/bootc tests/kolainst/basic
install -D -t $(DESTDIR)$(prefix)/lib/coreos-assembler/tests/kola/bootc tests/kolainst/*

vendor:
cargo xtask $@
Expand Down
8 changes: 8 additions & 0 deletions lib/Cargo.toml
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
@@ -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
@@ -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(())
}
7 changes: 6 additions & 1 deletion lib/src/cli.rs
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 @@ -210,7 +212,9 @@ async fn stage(
#[context("Preparing for write")]
async fn prepare_for_write() -> Result<()> {
ensure_self_unshared_mount_namespace().await?;
ostree_ext::selinux::verify_install_domain()?;
if crate::lsm::selinux_enabled()? {
crate::lsm::selinux_ensure_install()?;
}
Ok(())
}

Expand Down Expand Up @@ -319,6 +323,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