use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestSpec {
pub name: String,
pub mem_mb: u32,
pub cores: u32,
pub disk: String,
pub medium: Option<String>,
pub cdrom_first: bool,
pub rtc_skew_seconds: i64,
}
impl GuestSpec {
pub fn new(name: impl Into<String>, disk: impl Into<String>) -> GuestSpec {
GuestSpec {
name: name.into(),
mem_mb: 2048,
cores: 2,
disk: disk.into(),
medium: None,
cdrom_first: false,
rtc_skew_seconds: 7200,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GuestOutcome {
Installed { ms: u64 },
Looped { rounds: u32 },
Panicked { frame_sha256: String },
Refused { clause: &'static str, why: String },
}
impl fmt::Display for GuestOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GuestOutcome::Installed { ms } => write!(f, "installed in {ms} ms"),
GuestOutcome::Looped { rounds } => write!(f, "installer looped {rounds} times"),
GuestOutcome::Panicked { frame_sha256 } => {
write!(f, "PID 1 panicked; the only evidence is the framebuffer ({frame_sha256})")
}
GuestOutcome::Refused { clause, why } => write!(f, "refused {clause}: {why}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Machine {
pub server: String,
pub mem_mb: u32,
pub cores: u32,
pub disks: Vec<MachineDisk>,
pub medium: Option<String>,
pub cdrom_first: bool,
pub rtc_skew_seconds: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MachineDisk {
pub storage: String,
pub size_gib: u64,
}
pub trait GuestEngine: Send + Sync {
fn name(&self) -> &'static str;
fn boot(&self, spec: &GuestSpec) -> GuestOutcome;
fn power_on(&self, _machine: &Machine) -> Result<(), String> {
Ok(())
}
fn power_off(&self, _server: &str, _hard: bool) {}
fn running(&self, _server: &str) -> Option<bool> {
None
}
fn eject(&self, _server: &str) {}
fn console(&self, _server: &str) -> Option<(String, u16)> {
None
}
fn load(&self, _server: &str, _storage: &str) {}
fn resize_disk(&self, _storage: &str, _size_gib: u64) -> Result<(), String> {
Ok(())
}
fn store_medium(&self, _storage: &str, _bytes: &[u8]) -> Result<(), String> {
Ok(())
}
fn forget_server(&self, _server: &str) {}
fn forget_storage(&self, _storage: &str) {}
fn forget_all(&self) {}
fn evidence(&self, _server: &str) -> Option<serde_json::Value> {
None
}
}
pub struct VirtualGuest {
pub install_ms: u64,
}
impl Default for VirtualGuest {
fn default() -> Self {
VirtualGuest { install_ms: 10_011 }
}
}
impl GuestEngine for VirtualGuest {
fn name(&self) -> &'static str {
"virtual"
}
fn boot(&self, spec: &GuestSpec) -> GuestOutcome {
match (spec.cdrom_first, &spec.medium) {
(true, Some(_)) => GuestOutcome::Looped { rounds: 2 },
(_, _) => GuestOutcome::Installed { ms: self.install_ms },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_cd_first_with_a_medium_in_it_is_the_loop() {
let g = VirtualGuest::default();
let mut s = GuestSpec::new("appliance", "/var/lib/mock/appliance.qcow2");
s.medium = Some("/var/lib/mock/gunnar.iso".into());
s.cdrom_first = true;
assert!(matches!(g.boot(&s), GuestOutcome::Looped { .. }));
s.medium = None;
assert!(matches!(g.boot(&s), GuestOutcome::Installed { .. }));
}
#[test]
fn the_measured_install_is_ten_thousand_and_eleven_milliseconds() {
let g = VirtualGuest::default();
let s = GuestSpec::new("front", "/var/lib/mock/front.qcow2");
assert_eq!(g.boot(&s), GuestOutcome::Installed { ms: 10_011 });
}
#[test]
fn the_estate_boots_its_engine_and_follows_the_machine() {
use crate::{Clock, Estate, Faults};
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct Rec {
calls: Mutex<Vec<String>>,
alive: Mutex<Option<bool>>,
refuse: bool,
}
impl GuestEngine for Rec {
fn name(&self) -> &'static str {
"recording"
}
fn boot(&self, _: &GuestSpec) -> GuestOutcome {
GuestOutcome::Installed { ms: 0 }
}
fn power_on(&self, m: &Machine) -> Result<(), String> {
self.calls.lock().unwrap().push(format!("on {} {}G", m.server, m.disks[0].size_gib));
if self.refuse {
return Err("seabios: refused".into());
}
*self.alive.lock().unwrap() = Some(true);
Ok(())
}
fn power_off(&self, server: &str, hard: bool) {
self.calls.lock().unwrap().push(format!("off {server} hard={hard}"));
if hard {
*self.alive.lock().unwrap() = Some(false);
}
}
fn running(&self, _: &str) -> Option<bool> {
*self.alive.lock().unwrap()
}
}
let rec = Arc::new(Rec::default());
let mut e = Estate::new(Clock::virtual_only(), Faults::none(), 5).with_engine(rec.clone());
let uuid = e.create_server("a", "a", "2xCPU-4GB", "se-sto1", vec![], "boot", 8).unwrap();
e.run_to_quiet();
assert_eq!(e.server(&uuid).unwrap().state, "started");
assert_eq!(rec.calls.lock().unwrap()[0], format!("on {uuid} 8G"));
*rec.alive.lock().unwrap() = Some(false);
e.settle();
assert_eq!(e.server(&uuid).unwrap().state, "started", "not yet noticed");
e.run_to_quiet();
assert_eq!(e.server(&uuid).unwrap().state, "stopped");
e.start_server(&uuid).unwrap();
e.run_to_quiet();
assert_eq!(e.server(&uuid).unwrap().state, "started");
e.stop_server(&uuid, true).unwrap();
e.run_to_quiet();
assert_eq!(e.server(&uuid).unwrap().state, "stopped");
assert!(rec.calls.lock().unwrap().iter().any(|c| c == &format!("off {uuid} hard=true")));
let refusing = Arc::new(Rec { refuse: true, ..Rec::default() });
let mut e = Estate::new(Clock::virtual_only(), Faults::none(), 6).with_engine(refusing);
let uuid = e.create_server("b", "b", "2xCPU-4GB", "se-sto1", vec![], "boot", 8).unwrap();
e.run_to_quiet();
assert_eq!(e.server(&uuid).unwrap().state, "stopped", "a machine that never started is not `started`");
}
#[test]
fn a_guest_is_two_hours_ahead() {
assert_eq!(GuestSpec::new("x", "y").rtc_skew_seconds, 7200);
}
}