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

Implementation of adoption #66

Merged
merged 1 commit into from Oct 13, 2020
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
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
57 changes: 57 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,28 @@ 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_default();
let component = component::new_from_name(name)?;
if state.installed.get(name).is_some() {
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(&update)
.context("Failed adopt and update")?;
state.installed.insert(component.name().into(), inst);
update_state(&sysroot, &state)?;
Ok(update)
}

/// 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 +256,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 +311,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 +395,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, update: &ContentMetadata) -> 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
69 changes: 68 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,73 @@ 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, updatemeta: &ContentMetadata) -> 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 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")?;
Ok(InstalledContent {
meta: updatemeta.clone(),
filetree: Some(updatef),
adopted_from: Some(meta),
})
}

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 +119,7 @@ impl Component for EFI {
Ok(InstalledContent {
meta,
filetree: Some(ft),
adopted_from: None,
})
}

Expand All @@ -72,11 +136,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