Skip to content

Commit 0851130

Browse files
authored
Merge pull request #30 from cgwalters/install
Add an `install` command
2 parents cedb4bf + 9e1fbd1 commit 0851130

File tree

19 files changed

+1841
-12
lines changed

19 files changed

+1841
-12
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ jobs:
114114
name: "Privileged testing"
115115
needs: build
116116
runs-on: ubuntu-latest
117-
container:
118-
image: quay.io/fedora/fedora-coreos:testing-devel
119-
options: "--privileged --pid=host -v /run/systemd:/run/systemd -v /:/run/host"
120117
steps:
121118
- name: Checkout repository
122119
uses: actions/checkout@v3
@@ -125,7 +122,7 @@ jobs:
125122
with:
126123
name: bootc
127124
- name: Install
128-
run: install bootc /usr/bin && rm -v bootc
125+
run: sudo install bootc /usr/bin && rm -v bootc
129126
- name: Integration tests
130-
run: bootc internal-tests run-privileged-integration
127+
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
131128

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
example
2-
2+
.cosa
3+
_kola_temp
4+
bootc.tar.zst
35

46
# Added by cargo
5-
67
/target
78
Cargo.lock
8-
bootc.tar.zst

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ bin-archive: all
1010
$(MAKE) install DESTDIR=tmp-install && tar --zstd -C tmp-install -cf bootc.tar.zst . && rm tmp-install -rf
1111

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

1515
vendor:
1616
cargo xtask $@

lib/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,23 @@ ostree-ext = "0.10.5"
1515
clap = { version= "3.2", features = ["derive"] }
1616
clap_mangen = { version = "0.1", optional = true }
1717
cap-std-ext = "1.0.1"
18+
hex = "^0.4"
1819
fn-error-context = "0.2.0"
20+
gvariant = "0.4.0"
1921
indicatif = "0.17.0"
22+
libc = "^0.2"
23+
once_cell = "1.9"
24+
openssl = "^0.10"
25+
nix = ">= 0.24, < 0.26"
2026
serde = { features = ["derive"], version = "1.0.125" }
2127
serde_json = "1.0.64"
28+
serde_with = ">= 1.9.4, < 2"
2229
tokio = { features = ["io-std", "time", "process", "rt", "net"], version = ">= 1.13.0" }
2330
tokio-util = { features = ["io-util"], version = "0.7" }
2431
tracing = "0.1"
2532
tempfile = "3.3.0"
2633
xshell = { version = "0.2", optional = true }
34+
uuid = { version = "1.2.2", features = ["v4"] }
2735

2836
[features]
2937
default = []

lib/src/blockdev.rs

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

lib/src/bootloader.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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::fs::Permissions;
7+
use cap_std_ext::cap_std;
8+
use cap_std_ext::prelude::*;
9+
use fn_error_context::context;
10+
11+
use crate::task::Task;
12+
13+
/// This variable is referenced by our GRUB fragment
14+
pub(crate) const IGNITION_VARIABLE: &str = "$ignition_firstboot";
15+
const GRUB_BOOT_UUID_FILE: &str = "bootuuid.cfg";
16+
const STATIC_GRUB_CFG: &str = include_str!("grub.cfg");
17+
const STATIC_GRUB_CFG_EFI: &str = include_str!("grub-efi.cfg");
18+
19+
fn install_grub2_efi(efidir: &Dir, uuid: &str) -> Result<()> {
20+
let mut vendordir = None;
21+
let efidir = efidir.open_dir("EFI").context("Opening EFI/")?;
22+
for child in efidir.entries()? {
23+
let child = child?;
24+
let name = child.file_name();
25+
let name = if let Some(name) = name.to_str() {
26+
name
27+
} else {
28+
continue;
29+
};
30+
if name == "BOOT" {
31+
continue;
32+
}
33+
if !child.file_type()?.is_dir() {
34+
continue;
35+
}
36+
vendordir = Some(child.open_dir()?);
37+
break;
38+
}
39+
let vendordir = vendordir.ok_or_else(|| anyhow::anyhow!("Failed to find EFI vendor dir"))?;
40+
vendordir
41+
.atomic_write("grub.cfg", STATIC_GRUB_CFG_EFI)
42+
.context("Writing static EFI grub.cfg")?;
43+
vendordir
44+
.atomic_write(GRUB_BOOT_UUID_FILE, uuid)
45+
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;
46+
47+
Ok(())
48+
}
49+
50+
#[context("Installing bootloader")]
51+
pub(crate) fn install_via_bootupd(
52+
device: &Utf8Path,
53+
rootfs: &Utf8Path,
54+
boot_uuid: &uuid::Uuid,
55+
) -> Result<()> {
56+
Task::new_and_run(
57+
"Running bootupctl to install bootloader",
58+
"bootupctl",
59+
["backend", "install", "--src-root", "/", rootfs.as_str()],
60+
)?;
61+
62+
let grub2_uuid_contents = format!("set BOOT_UUID=\"{boot_uuid}\"\n");
63+
64+
let bootfs = &rootfs.join("boot");
65+
66+
{
67+
let efidir = Dir::open_ambient_dir(&bootfs.join("efi"), cap_std::ambient_authority())?;
68+
install_grub2_efi(&efidir, &grub2_uuid_contents)?;
69+
}
70+
71+
let grub2 = &bootfs.join("grub2");
72+
std::fs::create_dir(grub2).context("creating boot/grub2")?;
73+
let grub2 = Dir::open_ambient_dir(grub2, cap_std::ambient_authority())?;
74+
// Mode 0700 to support passwords etc.
75+
grub2.set_permissions(".", Permissions::from_mode(0o700))?;
76+
grub2
77+
.atomic_write_with_perms(
78+
"grub.cfg",
79+
STATIC_GRUB_CFG,
80+
cap_std::fs::Permissions::from_mode(0o600),
81+
)
82+
.context("Writing grub.cfg")?;
83+
84+
grub2
85+
.atomic_write_with_perms(
86+
GRUB_BOOT_UUID_FILE,
87+
grub2_uuid_contents,
88+
Permissions::from_mode(0o644),
89+
)
90+
.with_context(|| format!("Writing {GRUB_BOOT_UUID_FILE}"))?;
91+
92+
Task::new("Installing BIOS grub2", "grub2-install")
93+
.args([
94+
"--target",
95+
"i386-pc",
96+
"--boot-directory",
97+
bootfs.as_str(),
98+
"--modules",
99+
"mdraid1x",
100+
device.as_str(),
101+
])
102+
.run()?;
103+
104+
Ok(())
105+
}

lib/src/cli.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ pub(crate) enum Opt {
9494
Switch(SwitchOpts),
9595
/// Display status
9696
Status(StatusOpts),
97+
/// Install to the target block device
98+
Install(crate::install::InstallOpts),
9799
/// Internal integration testing helpers.
98100
#[clap(hide(true), subcommand)]
99101
#[cfg(feature = "internal-testing-api")]
@@ -210,7 +212,9 @@ async fn stage(
210212
#[context("Preparing for write")]
211213
async fn prepare_for_write() -> Result<()> {
212214
ensure_self_unshared_mount_namespace().await?;
213-
ostree_ext::selinux::verify_install_domain()?;
215+
if crate::lsm::selinux_enabled()? {
216+
crate::lsm::selinux_ensure_install()?;
217+
}
214218
Ok(())
215219
}
216220

@@ -319,6 +323,7 @@ where
319323
match opt {
320324
Opt::Upgrade(opts) => upgrade(opts).await,
321325
Opt::Switch(opts) => switch(opts).await,
326+
Opt::Install(opts) => crate::install::install(opts).await,
322327
Opt::Status(opts) => super::status::status(opts).await,
323328
#[cfg(feature = "internal-testing-api")]
324329
Opt::InternalTests(ref opts) => {

0 commit comments

Comments
 (0)