Skip to content

Commit

Permalink
WIP: Implementation of adoption
Browse files Browse the repository at this point in the history
This shares a lot of similarity with `update` but I'm
trying to keep them distinct because we may need
to do something different in the future, and I think
we need to be careful when operating on state we didn't create.

Closes: #38
  • Loading branch information
cgwalters committed Oct 9, 2020
1 parent b2f6f0f commit 18cae2e
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 4 deletions.
10 changes: 8 additions & 2 deletions .cci.jenkinsfile
Expand Up @@ -47,10 +47,16 @@ cosaPod(buildroot: true, runAsUser: 0, memory: "3072Mi", cpu: "4") {
mkdir -p overrides/rootfs
mv insttree/* overrides/rootfs/
rmdir insttree
coreos-assembler fetch
cosa fetch
cosa build
""")
}
// The e2e-update test does a build, so we just end at fetch above
// The e2e-adopt test will use the ostree commit we just generated above
// but a static qemu base image.
stage("e2e adopt test") {
shwrap("env COSA_DIR=${env.WORKSPACE} ./tests/e2e-adopt/e2e-adopt.sh")
}
// Now a test that upgrades using bootupd
stage("e2e upgrade test") {
shwrap("env COSA_DIR=${env.WORKSPACE} ./tests/e2e-update/e2e-update.sh")
}
Expand Down
61 changes: 61 additions & 0 deletions src/bootupd.rs
Expand Up @@ -23,6 +23,8 @@ pub(crate) const WRITE_LOCK_PATH: &str = "run/bootupd-lock";
pub(crate) enum ClientRequest {
/// Update a component
Update { component: String },
/// Update a component via adoption
AdoptAndUpdate { component: String },
/// Validate a component
Validate { component: String },
/// Print the current state
Expand Down Expand Up @@ -150,6 +152,32 @@ pub(crate) fn update(name: &str) -> Result<ComponentUpdateResult> {
})
}

/// daemon implementation of component adoption
pub(crate) fn adopt_and_update(name: &str) -> Result<ContentMetadata> {
let sysroot = openat::Dir::open("/")?;
let _lock = acquire_write_lock("/").context("Failed to acquire write lock")?;
let mut state = get_saved_state("/")?.unwrap_or_else(|| SavedState {
..Default::default()
});
let component = component::new_from_name(name)?;
if let Some(_) = state.installed.get(name) {
anyhow::bail!("Component {} is already installed", name);
};
let update = if let Some(update) = component.query_update()? {
update
} else {
anyhow::bail!("Component {} has no available update", name);
};
let inst = component
.adopt_update()
.context("Failed adopt and update")?;
state
.installed
.insert(component.name().into(), inst.clone());
update_state(&sysroot, &state)?;
Ok(update.clone())
}

/// daemon implementation of component validate
pub(crate) fn validate(name: &str) -> Result<ValidationResult> {
let state = get_saved_state("/")?.unwrap_or_else(|| SavedState {
Expand Down Expand Up @@ -232,21 +260,31 @@ pub(crate) fn status() -> Result<Status> {
.flatten();
let update = component.query_update()?;
let updatable = ComponentUpdatable::from_metadata(&ic.meta, update.as_ref());
let adopted_from = ic.adopted_from.clone();
ret.components.insert(
name.to_string(),
ComponentStatus {
installed: ic.meta.clone(),
interrupted: interrupted.cloned(),
update,
updatable,
adopted_from,
},
);
}
} else {
log::trace!("No saved state");
}

// Process the remaining components not installed
log::trace!("Remaining known components: {}", known_components.len());
for (name, component) in known_components {
if let Some(adopt_ver) = component.query_adopt()? {
ret.adoptable.insert(name.to_string(), adopt_ver);
} else {
log::trace!("Not adoptable: {}", name);
}
}

Ok(ret)
}
Expand Down Expand Up @@ -277,6 +315,13 @@ pub(crate) fn print_status(status: &Status) -> Result<()> {
println!(" Update: {}", msg);
}

if status.adoptable.is_empty() {
println!("No components are adoptable.");
}
for (name, version) in status.adoptable.iter() {
println!("Adoptable: {}: {}", name, version.version);
}

if let Some(coreos_aleph) = coreos::get_aleph_version()? {
println!("CoreOS aleph image ID: {}", coreos_aleph.aleph.imgid);
}
Expand Down Expand Up @@ -354,6 +399,22 @@ pub(crate) fn client_run_update(c: &mut ipc::ClientToDaemonConnection) -> Result
Ok(())
}

pub(crate) fn client_run_adopt_and_update(c: &mut ipc::ClientToDaemonConnection) -> Result<()> {
validate_preview_env()?;
let status: Status = c.send(&ClientRequest::Status)?;
if status.adoptable.is_empty() {
println!("No components are adoptable.");
} else {
for (name, _) in status.adoptable.iter() {
let r: ContentMetadata = c.send(&ClientRequest::AdoptAndUpdate {
component: name.to_string(),
})?;
println!("Adopted and updated: {}: {}", name, r.version);
}
}
Ok(())
}

pub(crate) fn client_run_validate(c: &mut ipc::ClientToDaemonConnection) -> Result<()> {
let status: Status = c.send(&ClientRequest::Status)?;
if status.components.is_empty() {
Expand Down
14 changes: 14 additions & 0 deletions src/cli/bootupctl.rs
Expand Up @@ -42,6 +42,8 @@ pub enum CtlVerb {
Status(StatusOpts),
#[structopt(name = "update", about = "Update all components")]
Update,
#[structopt(name = "adopt-and-update", about = "Update all adoptable components")]
AdoptAndUpdate,
#[structopt(name = "validate", about = "Validate system state")]
Validate,
}
Expand All @@ -67,6 +69,7 @@ impl CtlCommand {
match self.cmd {
CtlVerb::Status(opts) => Self::run_status(opts),
CtlVerb::Update => Self::run_update(),
CtlVerb::AdoptAndUpdate => Self::run_adopt_and_update(),
CtlVerb::Validate => Self::run_validate(),
CtlVerb::Backend(CtlBackend::Generate(opts)) => {
super::bootupd::DCommand::run_generate_meta(opts)
Expand Down Expand Up @@ -106,6 +109,17 @@ impl CtlCommand {
Ok(())
}

/// Runner for `update` verb.
fn run_adopt_and_update() -> Result<()> {
let mut client = ClientToDaemonConnection::new();
client.connect()?;

bootupd::client_run_adopt_and_update(&mut client)?;

client.shutdown()?;
Ok(())
}

/// Runner for `validate` verb.
fn run_validate() -> Result<()> {
let mut client = ClientToDaemonConnection::new();
Expand Down
8 changes: 8 additions & 0 deletions src/component.rs
Expand Up @@ -25,6 +25,14 @@ pub(crate) trait Component {
/// and should remain stable.
fn name(&self) -> &'static str;

/// In an operating system whose initially booted disk image is not
/// using bootupd, detect whether it looks like the component exists
/// and "synthesize" content metadata from it.
fn query_adopt(&self) -> Result<Option<ContentMetadata>>;

/// Given an adoptable system and an update, perform the update.
fn adopt_update(&self) -> Result<InstalledContent>;

/// Implementation of `bootupd install` for a given component. This should
/// gather data (or run binaries) from the source root, and install them
/// into the target root. It is expected that sub-partitions (e.g. the ESP)
Expand Down
1 change: 0 additions & 1 deletion src/coreos.rs
Expand Up @@ -24,7 +24,6 @@ pub(crate) struct Aleph {

pub(crate) struct AlephWithTimestamp {
pub(crate) aleph: Aleph,
#[allow(dead_code)]
pub(crate) ts: chrono::DateTime<Utc>,
}

Expand Down
6 changes: 6 additions & 0 deletions src/daemon/mod.rs
Expand Up @@ -104,6 +104,12 @@ fn process_client_requests(client: ipc::AuthenticatedClient) -> Result<()> {
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?
}
ClientRequest::AdoptAndUpdate { component } => {
bincode::serialize(&match bootupd::adopt_and_update(component.as_str()) {
Ok(v) => ipc::DaemonToClientReply::Success::<crate::model::ContentMetadata>(v),
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?
}
ClientRequest::Validate { component } => {
bincode::serialize(&match bootupd::validate(component.as_str()) {
Ok(v) => ipc::DaemonToClientReply::Success::<ValidationResult>(v),
Expand Down
71 changes: 70 additions & 1 deletion src/efi.rs
Expand Up @@ -6,10 +6,11 @@

use std::collections::{BTreeMap, BTreeSet};
use std::io::prelude::*;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{bail, Context, Result};
use openat_ext::OpenatDirExt;

use chrono::prelude::*;

Expand All @@ -26,11 +27,75 @@ pub(crate) const MOUNT_PATH: &str = "boot/efi";
#[derive(Default)]
pub(crate) struct EFI {}

impl EFI {
fn esp_path(&self) -> PathBuf {
Path::new(MOUNT_PATH).join("EFI")
}

fn open_esp_optional(&self) -> Result<Option<openat::Dir>> {
let sysroot = openat::Dir::open("/")?;
let esp = sysroot.sub_dir_optional(&self.esp_path())?;
Ok(esp)
}
fn open_esp(&self) -> Result<openat::Dir> {
let sysroot = openat::Dir::open("/")?;
let esp = sysroot.sub_dir(&self.esp_path())?;
Ok(esp)
}
}

impl Component for EFI {
fn name(&self) -> &'static str {
"EFI"
}

fn query_adopt(&self) -> Result<Option<ContentMetadata>> {
let esp = self.open_esp_optional()?;
if esp.is_none() {
log::trace!("No ESP detected");
return Ok(None);
};
// This would be extended with support for other operating systems later
let coreos_aleph = if let Some(a) = crate::coreos::get_aleph_version()? {
a
} else {
log::trace!("No CoreOS aleph detected");
return Ok(None);
};
let meta = ContentMetadata {
timestamp: coreos_aleph.ts,
version: coreos_aleph.aleph.imgid,
};
log::trace!("EFI adoptable: {:?}", &meta);
Ok(Some(meta))
}

/// Given an adoptable system and an update, perform the update.
fn adopt_update(&self) -> Result<InstalledContent> {
let meta = if let Some(meta) = self.query_adopt()? {
meta
} else {
anyhow::bail!("Failed to find adoptable system");
};

let esp = self.open_esp()?;
validate_esp(&esp)?;
let updatemeta = self.query_update()?.expect("update available");
let updated =
openat::Dir::open(&component_updatedir("/", self)).context("opening update dir")?;
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
// For adoption, we should only touch files that we know about.
let diff = updatef.relative_diff_to(&esp)?;
log::trace!("applying adoption diff: {}", &diff);
filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?;
let adopted_from = Some(meta.clone());
Ok(InstalledContent {
meta: updatemeta,
filetree: Some(updatef),
adopted_from: adopted_from,
})
}

fn install(&self, src_root: &str, dest_root: &str) -> Result<InstalledContent> {
let meta = if let Some(meta) = get_component_update(src_root, self)? {
meta
Expand All @@ -56,6 +121,7 @@ impl Component for EFI {
Ok(InstalledContent {
meta,
filetree: Some(ft),
adopted_from: None,
})
}

Expand All @@ -72,11 +138,14 @@ impl Component for EFI {
let destdir = openat::Dir::open(&Path::new("/").join(MOUNT_PATH).join("EFI"))
.context("opening EFI dir")?;
validate_esp(&destdir)?;
log::trace!("applying diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;
let adopted_from = None;
Ok(InstalledContent {
meta: updatemeta,
filetree: Some(updatef),
adopted_from: adopted_from,
})
}

Expand Down
13 changes: 13 additions & 0 deletions src/filetree.rs
Expand Up @@ -9,6 +9,7 @@ use openat_ext::OpenatDirExt;
use openssl::hash::{Hasher, MessageDigest};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Display;
use std::os::linux::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
use std::os::unix::process::CommandExt;
Expand Down Expand Up @@ -49,6 +50,18 @@ pub(crate) struct FileTreeDiff {
pub(crate) changes: HashSet<String>,
}

impl Display for FileTreeDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(
f,
"additions: {} removals: {} changes: {}",
self.additions.len(),
self.removals.len(),
self.changes.len()
)
}
}

#[cfg(test)]
impl FileTreeDiff {
pub(crate) fn count(&self) -> usize {
Expand Down
6 changes: 6 additions & 0 deletions src/model.rs
Expand Up @@ -37,6 +37,8 @@ pub(crate) struct InstalledContent {
pub(crate) meta: ContentMetadata,
/// Human readable version number, like ostree it is not ever parsed, just displayed
pub(crate) filetree: Option<crate::filetree::FileTree>,
/// The version this was originally adopted from
pub(crate) adopted_from: Option<ContentMetadata>,
}

/// Will be serialized into /boot/bootupd-state.json
Expand Down Expand Up @@ -89,6 +91,8 @@ pub(crate) struct ComponentStatus {
pub(crate) update: Option<ContentMetadata>,
/// Is true if the version in `update` is different from `installed`
pub(crate) updatable: ComponentUpdatable,
/// Originally adopted version
pub(crate) adopted_from: Option<ContentMetadata>,
}

/// Representation of bootupd's worldview at a point in time.
Expand All @@ -101,6 +105,8 @@ pub(crate) struct ComponentStatus {
pub(crate) struct Status {
/// Maps a component name to status
pub(crate) components: BTreeMap<String, ComponentStatus>,
/// Components that appear to be installed, not via bootupd
pub(crate) adoptable: BTreeMap<String, ContentMetadata>,
}

#[cfg(test)]
Expand Down

0 comments on commit 18cae2e

Please sign in to comment.