Skip to content

Commit fd55a04

Browse files
authored
Merge pull request #49 from cgwalters/install-to-self-prep
Prep patches for `install-to-filesystem`
2 parents cdc86ca + 1b2c4c4 commit fd55a04

File tree

2 files changed

+103
-60
lines changed

2 files changed

+103
-60
lines changed

lib/src/install.rs

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,8 @@ const PREPPN: u32 = 1;
8282
#[cfg(target_arch = "ppc64")]
8383
const RESERVEDPN: u32 = 1;
8484

85-
/// Perform an upgrade operation
86-
#[derive(Debug, Clone, clap::Parser)]
87-
pub(crate) struct InstallOpts {
88-
/// Target block device for installation. The entire device will be wiped.
89-
pub(crate) device: Utf8PathBuf,
90-
91-
/// Automatically wipe all existing data on device
92-
#[clap(long)]
93-
pub(crate) wipe: bool,
94-
95-
/// Size of the root partition (default specifier: M). Allowed specifiers: M (mebibytes), G (gibibytes), T (tebibytes).
96-
///
97-
/// By default, all remaining space on the disk will be used.
98-
#[clap(long)]
99-
pub(crate) root_size: Option<String>,
100-
85+
#[derive(clap::Args, Debug, Clone)]
86+
pub(crate) struct InstallTargetOpts {
10187
// TODO: A size specifier which allocates free space for the root in *addition* to the base container image size
10288
// pub(crate) root_additional_size: Option<String>
10389
/// The transport; e.g. oci, oci-archive. Defaults to `registry`.
@@ -115,11 +101,10 @@ pub(crate) struct InstallOpts {
115101
/// Enable verification via an ostree remote
116102
#[clap(long)]
117103
pub(crate) target_ostree_remote: Option<String>,
104+
}
118105

119-
/// Target root filesystem type.
120-
#[clap(long, value_enum, default_value_t)]
121-
pub(crate) filesystem: Filesystem,
122-
106+
#[derive(clap::Args, Debug, Clone)]
107+
pub(crate) struct InstallConfigOpts {
123108
/// Path to an Ignition config file
124109
#[clap(long, value_parser)]
125110
pub(crate) ignition_file: Option<Utf8PathBuf>,
@@ -131,13 +116,6 @@ pub(crate) struct InstallOpts {
131116
#[clap(long, value_name = "digest", value_parser)]
132117
pub(crate) ignition_hash: Option<crate::ignition::IgnitionHash>,
133118

134-
/// Target root block device setup.
135-
///
136-
/// direct: Filesystem written directly to block device
137-
/// tpm2-luks: Bind unlock of filesystem to presence of the default tpm2 device.
138-
#[clap(long, value_enum, default_value_t)]
139-
pub(crate) block_setup: BlockSetup,
140-
141119
/// Disable SELinux in the target (installed) system.
142120
///
143121
/// This is currently necessary to install *from* a system with SELinux disabled
@@ -154,9 +132,54 @@ pub(crate) struct InstallOpts {
154132
karg: Option<Vec<String>>,
155133
}
156134

135+
/// Options for installing to a block device
136+
#[derive(Debug, Clone, clap::Args)]
137+
pub(crate) struct InstallBlockDeviceOpts {
138+
/// Target block device for installation. The entire device will be wiped.
139+
pub(crate) device: Utf8PathBuf,
140+
141+
/// Automatically wipe all existing data on device
142+
#[clap(long)]
143+
pub(crate) wipe: bool,
144+
145+
/// Target root block device setup.
146+
///
147+
/// direct: Filesystem written directly to block device
148+
/// tpm2-luks: Bind unlock of filesystem to presence of the default tpm2 device.
149+
#[clap(long, value_enum, default_value_t)]
150+
pub(crate) block_setup: BlockSetup,
151+
152+
/// Target root filesystem type.
153+
#[clap(long, value_enum, default_value_t)]
154+
pub(crate) filesystem: Filesystem,
155+
156+
/// Size of the root partition (default specifier: M). Allowed specifiers: M (mebibytes), G (gibibytes), T (tebibytes).
157+
///
158+
/// By default, all remaining space on the disk will be used.
159+
#[clap(long)]
160+
pub(crate) root_size: Option<String>,
161+
}
162+
163+
/// Perform an installation to a block device.
164+
#[derive(Debug, Clone, clap::Parser)]
165+
pub(crate) struct InstallOpts {
166+
#[clap(flatten)]
167+
pub(crate) block_opts: InstallBlockDeviceOpts,
168+
169+
#[clap(flatten)]
170+
pub(crate) target_opts: InstallTargetOpts,
171+
172+
#[clap(flatten)]
173+
pub(crate) config_opts: InstallConfigOpts,
174+
}
175+
157176
// Shared read-only global state
158177
struct State {
159-
opts: InstallOpts,
178+
container_info: ContainerExecutionInfo,
179+
/// Force SELinux off in target system
180+
override_disable_selinux: bool,
181+
config_opts: InstallConfigOpts,
182+
target_opts: InstallTargetOpts,
160183
/// Path to our devtmpfs
161184
devdir: Utf8PathBuf,
162185
mntdir: Utf8PathBuf,
@@ -251,23 +274,21 @@ fn bind_mount_from_host(src: impl AsRef<Utf8Path>, dest: impl AsRef<Utf8Path>) -
251274
#[context("Creating ostree deployment")]
252275
async fn initialize_ostree_root_from_self(
253276
state: &State,
254-
containerstate: &ContainerExecutionInfo,
255277
root_setup: &RootSetup,
256-
kargs: &[&str],
257278
) -> Result<InstallAleph> {
258279
let rootfs_dir = &root_setup.rootfs_fd;
259280
let rootfs = root_setup.rootfs.as_path();
260-
let opts = &state.opts;
281+
let opts = &state.target_opts;
261282
let cancellable = gio::Cancellable::NONE;
262283

263-
if !containerstate.engine.starts_with("podman") {
284+
if !state.container_info.engine.starts_with("podman") {
264285
anyhow::bail!("Currently this command only supports being executed via podman");
265286
}
266-
if containerstate.imageid.is_empty() {
287+
if state.container_info.imageid.is_empty() {
267288
anyhow::bail!("Invalid empty imageid");
268289
}
269-
let digest = crate::podman::imageid_to_digest(&containerstate.imageid)?;
270-
let src_image = crate::utils::digested_pullspec(&containerstate.image, &digest);
290+
let digest = crate::podman::imageid_to_digest(&state.container_info.imageid)?;
291+
let src_image = crate::utils::digested_pullspec(&state.container_info.image, &digest);
271292

272293
let src_imageref = ostree_container::OstreeImageReference {
273294
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
@@ -300,7 +321,7 @@ async fn initialize_ostree_root_from_self(
300321
sigverify: target_sigverify,
301322
imgref: ostree_container::ImageReference {
302323
transport: ostree_container::Transport::Registry,
303-
name: containerstate.image.clone(),
324+
name: state.container_info.image.clone(),
304325
},
305326
}
306327
};
@@ -349,9 +370,14 @@ async fn initialize_ostree_root_from_self(
349370
r
350371
};
351372

373+
let kargs = root_setup
374+
.kargs
375+
.iter()
376+
.map(|v| v.as_str())
377+
.collect::<Vec<_>>();
352378
#[allow(clippy::needless_update)]
353379
let options = ostree_container::deploy::DeployOpts {
354-
kargs: Some(kargs),
380+
kargs: Some(kargs.as_slice()),
355381
target_imgref: Some(&target_imgref),
356382
proxy_cfg: Some(proxy_cfg),
357383
..Default::default()
@@ -461,8 +487,7 @@ struct RootSetup {
461487
}
462488

463489
#[context("Creating rootfs")]
464-
fn install_create_rootfs(state: &State) -> Result<RootSetup> {
465-
let opts = &state.opts;
490+
fn install_create_rootfs(state: &State, opts: InstallBlockDeviceOpts) -> Result<RootSetup> {
466491
// Verify that the target is empty (if not already wiped in particular, but it's
467492
// also good to verify that the wipe worked)
468493
let device = crate::blockdev::list_dev(&opts.device)?;
@@ -680,11 +705,11 @@ pub(crate) fn finalize_filesystem(fs: &Utf8Path) -> Result<()> {
680705
Ok(())
681706
}
682707

683-
/// Implementation of the `bootc install` CLI command.
684-
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
685-
// This command currently *must* be run inside a privileged container.
686-
let container_state = crate::containerenv::get_container_execution_info()?;
687-
708+
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
709+
async fn prepare_install(
710+
config_opts: InstallConfigOpts,
711+
target_opts: InstallTargetOpts,
712+
) -> Result<Arc<State>> {
688713
// We require --pid=host
689714
let pid = std::fs::read_link("/proc/1/exe").context("reading /proc/1/exe")?;
690715
let pid = pid
@@ -694,6 +719,9 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
694719
anyhow::bail!("This command must be run with --pid=host")
695720
}
696721

722+
// This command currently *must* be run inside a privileged container.
723+
let container_info = crate::containerenv::get_container_execution_info()?;
724+
697725
// Even though we require running in a container, the mounts we create should be specific
698726
// to this process, so let's enter a private mountns to avoid leaking them.
699727
if std::env::var_os("BOOTC_SKIP_UNSHARE").is_none() {
@@ -725,7 +753,7 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
725753
crate::lsm::container_setup_selinux()?;
726754
// This will re-execute the current process (once).
727755
crate::lsm::selinux_ensure_install()?;
728-
} else if opts.disable_selinux {
756+
} else if config_opts.disable_selinux {
729757
override_disable_selinux = true;
730758
println!("notice: Target has SELinux enabled, overriding to disable")
731759
} else {
@@ -755,31 +783,43 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
755783
// Overmount /var/tmp with the host's, so we can use it to share state
756784
bind_mount_from_host("/var/tmp", "/var/tmp")?;
757785
let state = Arc::new(State {
786+
override_disable_selinux,
787+
container_info,
758788
mntdir,
759789
devdir,
760-
opts,
790+
config_opts,
791+
target_opts,
761792
});
762793

794+
Ok(state)
795+
}
796+
797+
/// Implementation of the `bootc install` CLI command.
798+
pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
799+
let block_opts = opts.block_opts;
800+
let state = prepare_install(opts.config_opts, opts.target_opts).await?;
801+
763802
// This is all blocking stuff
764-
let rootfs = {
803+
let mut rootfs = {
765804
let state = state.clone();
766-
tokio::task::spawn_blocking(move || install_create_rootfs(&state)).await??
805+
tokio::task::spawn_blocking(move || install_create_rootfs(&state, block_opts)).await??
767806
};
768-
let mut kargs = rootfs.kargs.iter().map(|v| v.as_str()).collect::<Vec<_>>();
769-
if override_disable_selinux {
770-
kargs.push("selinux=0");
807+
if state.override_disable_selinux {
808+
rootfs.kargs.push("selinux=0".to_string());
771809
}
772810
// This is interpreted by our GRUB fragment
773-
if state.opts.ignition_file.is_some() {
774-
kargs.push(crate::ignition::PLATFORM_METAL_KARG);
775-
kargs.push(crate::bootloader::IGNITION_VARIABLE);
811+
if state.config_opts.ignition_file.is_some() {
812+
rootfs
813+
.kargs
814+
.push(crate::ignition::PLATFORM_METAL_KARG.to_string());
815+
rootfs
816+
.kargs
817+
.push(crate::bootloader::IGNITION_VARIABLE.to_string());
776818
}
777819

778820
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
779821
{
780-
let aleph =
781-
initialize_ostree_root_from_self(&state, &container_state, &rootfs, kargs.as_slice())
782-
.await?;
822+
let aleph = initialize_ostree_root_from_self(&state, &rootfs).await?;
783823
rootfs
784824
.rootfs_fd
785825
.atomic_replace_with(BOOTC_ALEPH_PATH, |f| {
@@ -792,11 +832,11 @@ pub(crate) async fn install(opts: InstallOpts) -> Result<()> {
792832
crate::bootloader::install_via_bootupd(&rootfs.device, &rootfs.rootfs, &rootfs.boot_uuid)?;
793833

794834
// If Ignition is specified, enable it
795-
if let Some(ignition_file) = state.opts.ignition_file.as_deref() {
835+
if let Some(ignition_file) = state.config_opts.ignition_file.as_deref() {
796836
let src = std::fs::File::open(ignition_file)
797837
.with_context(|| format!("Opening {ignition_file}"))?;
798838
let bootfs = rootfs.rootfs.join("boot");
799-
crate::ignition::write_ignition(&bootfs, &state.opts.ignition_hash, &src)?;
839+
crate::ignition::write_ignition(&bootfs, &state.config_opts.ignition_hash, &src)?;
800840
crate::ignition::enable_firstboot(&bootfs)?;
801841
println!("Installed Ignition config from {ignition_file}");
802842
}

tests/kolainst/install

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ set -xeuo pipefail
1212

1313
# See https://github.com/cgwalters/bootc-base-images
1414
IMAGE=quay.io/cgwalters/fedora-oscore:latest
15+
# TODO: better detect this, e.g. look for an empty device
16+
DEV=/dev/vda
1517

1618
# Always work out of a temporary directory
1719
cd $(mktemp -d)
1820

1921
case "${AUTOPKGTEST_REBOOT_MARK:-}" in
2022
"")
21-
podman run --rm -ti --privileged --pid=host --net=none -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install /dev/vda
23+
podman run --rm -ti --privileged --pid=host --net=none -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install --karg=foo=bar ${DEV}
2224
# In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot;
2325
# but for now let's just sanity test that the install command executes.
26+
lsblk ${DEV}
2427
echo "ok install"
2528
;;
2629
*) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;;

0 commit comments

Comments
 (0)