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 7, 2020
1 parent d91b6f4 commit 90332fc
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 46 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
123 changes: 99 additions & 24 deletions src/bootupd.rs
Expand Up @@ -5,11 +5,12 @@ use crate::model::{
ComponentStatus, ComponentUpdatable, ContentMetadata, OperatingSystem, SavedState, Status,
};
use crate::{component, ipc};
use anyhow::{bail, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use fs2::FileExt;
use openat_ext::OpenatDirExt;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::prelude::*;
use std::path::Path;

Expand All @@ -24,6 +25,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 @@ -70,6 +73,14 @@ pub(crate) fn get_components() -> Vec<Box<dyn Component>> {
components
}

pub(crate) fn get_components_by_name() -> HashMap<&'static str, Box<dyn Component>> {
let mut r = HashMap::new();
for v in get_components() {
r.insert(v.name(), v);
}
r
}

pub(crate) fn generate_update_metadata(sysroot_path: &str) -> Result<()> {
for component in get_components() {
let v = component.generate_update_metadata(sysroot_path)?;
Expand Down Expand Up @@ -145,6 +156,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 @@ -211,30 +248,45 @@ fn get_saved_state(sysroot_path: &str) -> Result<Option<SavedState>> {

pub(crate) fn status() -> Result<Status> {
let mut ret: Status = Default::default();
let state = if let Some(state) = get_saved_state("/")? {
state
let mut known_components = get_components_by_name();
let state = get_saved_state("/")?;
if let Some(state) = state {
for (name, ic) in state.installed.iter() {
log::trace!("Gathering status for installed component: {}", name);
let component = known_components
.remove(name.as_str())
.ok_or_else(|| anyhow!("Unknown component installed: {}", name))?;
let component = component.as_ref();
let interrupted = state
.pending
.as_ref()
.map(|p| p.get(name.as_str()))
.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 {
return Ok(ret);
};
for (name, ic) in state.installed.iter() {
let component = crate::component::new_from_name(&name)?;
let component = component.as_ref();
let interrupted = state
.pending
.as_ref()
.map(|p| p.get(name.as_str()))
.flatten();
let update = component.query_update()?;
let updatable = ComponentUpdatable::from_metadata(&ic.meta, update.as_ref());
ret.components.insert(
name.to_string(),
ComponentStatus {
installed: ic.meta.clone(),
interrupted: interrupted.cloned(),
update,
updatable,
},
);
log::trace!("No saved state");
}
log::trace!("Remaining known components: {}", known_components.len());
// Process the remaining components not installed
for (name, component) in known_components.drain() {
if let Some(adopt_ver) = component.query_adopt()? {
ret.adoptable.insert(name.to_string(), adopt_ver);
} else {
log::trace!("Not adoptable: {}", name);
}
}
if let Some(coreos_aleph) = coreos::get_aleph_version()? {
ret.os = Some(OperatingSystem::CoreOS {
Expand Down Expand Up @@ -270,6 +322,13 @@ pub(crate) fn print_status(status: &Status) {
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(ref os) = status.os {
match os {
OperatingSystem::CoreOS { aleph_imgid } => {
Expand Down Expand Up @@ -349,6 +408,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

0 comments on commit 90332fc

Please sign in to comment.