Skip to content

install: Add reset #1389

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use ostree_ext::ostree;
use schemars::schema_for;
use serde::{Deserialize, Serialize};

use crate::deploy::RequiredHostSpec;
use crate::deploy::{MergeState, RequiredHostSpec};
use crate::lints;
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
Expand Down Expand Up @@ -218,6 +218,12 @@ pub(crate) enum InstallOpts {
/// will be wiped, but the content of the existing root will otherwise be retained, and will
/// need to be cleaned up if desired when rebooted into the new root.
ToExistingRoot(crate::install::InstallToExistingRootOpts),
/// Nondestructively create a fresh installation state inside an existing bootc system.
///
/// This is a nondestructive variant of `install to-existing-root` that works only inside
/// an existing bootc system.
#[clap(hide = true)]
Reset(crate::install::InstallResetOpts),
/// Execute this as the penultimate step of an installation using `install to-filesystem`.
///
Finalize {
Expand Down Expand Up @@ -840,8 +846,9 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
} else if booted_unchanged {
println!("No update available.")
} else {
let osname = booted_deployment.osname();
crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
let stateroot = booted_deployment.osname();
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;
changed = true;
if let Some(prev) = booted_image.as_ref() {
if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
Expand Down Expand Up @@ -926,7 +933,8 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
}

let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

Expand Down Expand Up @@ -989,7 +997,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
// TODO gc old layers here

let stateroot = booted_deployment.osname();
crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
let from = MergeState::from_stateroot(sysroot, &stateroot)?;
crate::deploy::stage(sysroot, from, &fetched, &new_spec, prog.clone()).await?;

sysroot.update_mtime()?;

Expand Down Expand Up @@ -1175,6 +1184,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
InstallOpts::ToExistingRoot(opts) => {
crate::install::install_to_existing_root(opts).await
}
InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
InstallOpts::PrintConfiguration => crate::install::print_configuration(),
InstallOpts::EnsureCompletion {} => {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
Expand Down
66 changes: 44 additions & 22 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,25 +538,26 @@ pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Optio
#[context("Writing deployment")]
async fn deploy(
sysroot: &Storage,
merge_deployment: Option<&Deployment>,
stateroot: &str,
from: MergeState,
image: &ImageState,
origin: &glib::KeyFile,
) -> Result<Deployment> {
// Compute the kernel argument overrides. In practice today this API is always expecting
// a merge deployment. The kargs code also always looks at the booted root (which
// is a distinct minor issue, but not super important as right now the install path
// doesn't use this API).
let override_kargs = if let Some(deployment) = merge_deployment {
Some(crate::kargs::get_kargs(sysroot, &deployment, image)?)
} else {
None
let (stateroot, override_kargs) = match &from {
MergeState::MergeDeployment(deployment) => {
let kargs = crate::kargs::get_kargs(sysroot, &deployment, image)?;
(deployment.stateroot().into(), kargs)
}
MergeState::Reset { stateroot, kargs } => (stateroot.clone(), kargs.clone()),
};
// Clone all the things to move to worker thread
let sysroot_clone = sysroot.sysroot.clone();
// ostree::Deployment is incorrectly !Send 😢 so convert it to an integer
let merge_deployment = from.as_merge_deployment();
let merge_deployment = merge_deployment.map(|d| d.index() as usize);
let stateroot = stateroot.to_string();
let ostree_commit = image.ostree_commit.to_string();
// GKeyFile also isn't Send! So we serialize that as a string...
let origin_data = origin.to_data();
Expand All @@ -570,11 +571,10 @@ async fn deploy(
// Because the C API expects a Vec<&str>, we need to generate a new Vec<>
// that borrows.
let override_kargs = override_kargs
.as_deref()
.map(|v| v.iter().map(|s| s.as_str()).collect::<Vec<_>>());
if let Some(kargs) = override_kargs.as_deref() {
opts.override_kernel_argv = Some(&kargs);
}
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>();
opts.override_kernel_argv = Some(&override_kargs);
let deployments = sysroot.deployments();
let merge_deployment = merge_deployment.map(|m| &deployments[m]);
let origin = glib::KeyFile::new();
Expand Down Expand Up @@ -609,11 +609,41 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
Ok(origin)
}

/// The source of data for staging a new deployment
#[derive(Debug)]
pub(crate) enum MergeState {
/// Use the provided merge deployment
MergeDeployment(Deployment),
/// Don't use a merge deployment, but only this
/// provided initial state.
Reset {
stateroot: String,
kargs: Vec<String>,
},
}
impl MergeState {
/// Initialize using the default merge deployment for the given stateroot.
pub(crate) fn from_stateroot(sysroot: &Storage, stateroot: &str) -> Result<Self> {
let merge_deployment = sysroot.merge_deployment(Some(stateroot)).ok_or_else(|| {
anyhow::anyhow!("No merge deployment found for stateroot {stateroot}")
})?;
Ok(Self::MergeDeployment(merge_deployment))
}

/// Cast this to a merge deployment case.
pub(crate) fn as_merge_deployment(&self) -> Option<&Deployment> {
match self {
Self::MergeDeployment(d) => Some(d),
Self::Reset { .. } => None,
}
}
}

/// Stage (queue deployment of) a fetched container image.
#[context("Staging")]
pub(crate) async fn stage(
sysroot: &Storage,
stateroot: &str,
from: MergeState,
image: &ImageState,
spec: &RequiredHostSpec<'_>,
prog: ProgressWriter,
Expand All @@ -639,7 +669,6 @@ pub(crate) async fn stage(
.collect(),
})
.await;
let merge_deployment = sysroot.merge_deployment(Some(stateroot));

subtask.completed = true;
subtasks.push(subtask.clone());
Expand All @@ -662,14 +691,7 @@ pub(crate) async fn stage(
})
.await;
let origin = origin_from_imageref(spec.image)?;
let deployment = crate::deploy::deploy(
sysroot,
merge_deployment.as_ref(),
stateroot,
image,
&origin,
)
.await?;
let deployment = crate::deploy::deploy(sysroot, from, image, &origin).await?;

subtask.completed = true;
subtasks.push(subtask.clone());
Expand Down
156 changes: 154 additions & 2 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use ostree_ext::oci_spec;
use ostree_ext::ostree;
use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
use ostree_ext::prelude::Cast;
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::sysroot::{allocate_new_stateroot, list_stateroots, SysrootLock};
use ostree_ext::{container as ostree_container, ostree_prepareroot};
#[cfg(feature = "install-to-disk")]
use rustix::fs::FileTypeExt;
Expand All @@ -54,7 +54,9 @@ use serde::{Deserialize, Serialize};
use self::baseline::InstallBlockDeviceOpts;
use crate::boundimage::{BoundImage, ResolvedBoundImage};
use crate::containerenv::ContainerExecutionInfo;
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
use crate::deploy::{
prepare_for_pull, pull_from_prepared, MergeState, PreparedImportMeta, PreparedPullResult,
};
use crate::lsm;
use crate::progress_jsonl::ProgressWriter;
use crate::spec::ImageReference;
Expand Down Expand Up @@ -350,6 +352,50 @@ pub(crate) struct InstallToExistingRootOpts {
pub(crate) root_path: Utf8PathBuf,
}

#[derive(Debug, clap::Parser, PartialEq, Eq)]
pub(crate) struct InstallResetOpts {
/// Acknowledge that this command is experimental.
#[clap(long)]
pub(crate) experimental: bool,

#[clap(flatten)]
pub(crate) source_opts: InstallSourceOpts,

#[clap(flatten)]
pub(crate) target_opts: InstallTargetOpts,

/// Name of the target stateroot. If not provided, one will be automatically
/// generated of the form s<year>-<serial> where <serial> starts at zero and
/// increments automatically.
#[clap(long)]
pub(crate) stateroot: Option<String>,

/// Don't display progress
#[clap(long)]
pub(crate) quiet: bool,

#[clap(flatten)]
pub(crate) progress: crate::cli::ProgressOptions,

/// Restart or reboot into the new target image.
///
/// Currently, this option always reboots. In the future this command
/// will detect the case where no kernel changes are queued, and perform
/// a userspace-only restart.
#[clap(long)]
pub(crate) apply: bool,

/// Skip inheriting any automatically discovered root file system kernel arguments.
#[clap(long)]
no_root_kargs: bool,

/// Add a kernel argument. This option can be provided multiple times.
///
/// Example: --karg=nosmt --karg=console=ttyS0,114800n8
#[clap(long)]
karg: Option<Vec<String>>,
}

/// Global state captured from the container.
#[derive(Debug, Clone)]
pub(crate) struct SourceInfo {
Expand Down Expand Up @@ -383,6 +429,24 @@ pub(crate) struct State {
pub(crate) tempdir: TempDir,
}

impl InstallTargetOpts {
pub(crate) fn imageref(&self) -> Result<Option<ostree_container::OstreeImageReference>> {
let Some(target_imgname) = self.target_imgref.as_deref() else {
return Ok(None);
};
let target_transport =
ostree_container::Transport::try_from(self.target_transport.as_str())?;
let target_imgref = ostree_container::OstreeImageReference {
sigverify: ostree_container::SignatureSource::ContainerPolicyAllowInsecure,
imgref: ostree_container::ImageReference {
transport: target_transport,
name: target_imgname.to_string(),
},
};
Ok(Some(target_imgref))
}
}

impl State {
#[context("Loading SELinux policy")]
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
Expand Down Expand Up @@ -1964,6 +2028,94 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
install_to_filesystem(opts, true, cleanup).await
}

pub(crate) async fn install_reset(opts: InstallResetOpts) -> Result<()> {
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
if !opts.experimental {
anyhow::bail!("This command requires --experimental");
}

let prog: ProgressWriter = opts.progress.try_into()?;

let sysroot = &crate::cli::get_storage().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;

let stateroots = list_stateroots(sysroot)?;
dbg!(&stateroots);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This dbg! macro call appears to be leftover from debugging. It should be removed before merging to keep the command's output clean.

let target_stateroot = if let Some(s) = opts.stateroot {
s
} else {
let now = chrono::Utc::now();
let r = allocate_new_stateroot(&sysroot, &stateroots, now)?;
r.name
};

let booted_stateroot = booted_deployment.osname();
assert!(booted_stateroot.as_str() != target_stateroot);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using assert! here is risky because target_stateroot can be user-provided. If a user specifies the same name as the booted stateroot, this will cause a panic in release builds. It's better to use anyhow::ensure! to return a graceful error message.

    anyhow::ensure!(
        booted_stateroot.as_str() != target_stateroot,
        "Target stateroot cannot be the same as the booted one"
    );

let (fetched, spec) = if let Some(target) = opts.target_opts.imageref()? {
let mut new_spec = host.spec;
new_spec.image = Some(target.into());
let fetched = crate::deploy::pull(
repo,
&new_spec.image.as_ref().unwrap(),
None,
opts.quiet,
prog.clone(),
)
.await?;
(fetched, new_spec)
} else {
let imgstate = host
.status
.booted
.map(|b| b.query_image(repo))
.transpose()?
.flatten()
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
(Box::new((*imgstate).into()), host.spec)
};
let spec = crate::deploy::RequiredHostSpec::from_spec(&spec)?;

// Compute the kernel arguments to inherit. By default, that's only those involved
// in the root filesystem.
let root_kargs = if opts.no_root_kargs {
Vec::new()
} else {
let bootcfg = booted_deployment
.bootconfig()
.ok_or_else(|| anyhow!("Missing bootcfg for booted deployment"))?;
if let Some(options) = bootcfg.get("options") {
let options = options.split_ascii_whitespace().collect::<Vec<_>>();
crate::kernel::root_args_from_cmdline(&options)
.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
} else {
Vec::new()
}
};

let kargs = crate::kargs::get_kargs_in_root(rootfs, std::env::consts::ARCH)?
.into_iter()
.chain(root_kargs.into_iter())
.chain(opts.karg.unwrap_or_default())
.collect::<Vec<_>>();

let from = MergeState::Reset {
stateroot: target_stateroot,
kargs,
};
crate::deploy::stage(sysroot, from, &fetched, &spec, prog.clone()).await?;

sysroot.update_mtime()?;

if opts.apply {
crate::reboot::reboot()?;
}
Ok(())
}

/// Implementation of `bootc install finalize`.
pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> {
crate::cli::require_root(false)?;
Expand Down
15 changes: 15 additions & 0 deletions lib/src/kernel.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::Result;
use fn_error_context::context;

/// The default root filesystem mount specification.
pub(crate) const ROOT: &str = "root=";
/// This is used by dracut.
pub(crate) const INITRD_ARG_PREFIX: &str = "rd.";
/// The kernel argument for configuring the rootfs flags.
Expand Down Expand Up @@ -37,6 +39,19 @@ pub(crate) fn find_first_cmdline_arg<'a>(
.next()
}

/// Find the subset of kernel argumetns which describe how to mount the root filesystem.
pub(crate) fn root_args_from_cmdline<'a>(cmdline: &'a [&str]) -> Vec<&'a str> {
cmdline
.iter()
.filter(|arg| {
arg.starts_with(ROOT)
|| arg.starts_with(ROOTFLAGS)
|| arg.starts_with(INITRD_ARG_PREFIX)
})
.copied()
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading