use std::collections::BTreeMap;
pub mod rebuild;
pub mod steps;
pub mod role {
pub const MAIN: &str = "main";
pub const TWIN: &str = "twin";
pub const FRONT: &str = "front";
}
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Remedy {
RechargeFromTwin,
ReviewTwin,
NewTwin,
NewServer,
RestoreFront,
}
impl Remedy {
pub const ALL: [Remedy; 5] = [
Remedy::RechargeFromTwin,
Remedy::ReviewTwin,
Remedy::NewTwin,
Remedy::NewServer,
Remedy::RestoreFront,
];
pub fn as_str(self) -> &'static str {
match self {
Remedy::RechargeFromTwin => "recharge_from_twin",
Remedy::ReviewTwin => "review_twin",
Remedy::NewTwin => "new_twin",
Remedy::NewServer => "new_server",
Remedy::RestoreFront => "restore_front",
}
}
pub fn parse(s: &str) -> Option<Remedy> {
Remedy::ALL.into_iter().find(|r| r.as_str() == s)
}
pub fn words(self) -> &'static str {
match self {
Remedy::RechargeFromTwin => "Recharge from twin",
Remedy::ReviewTwin => "Review twin",
Remedy::NewTwin => "New twin",
Remedy::NewServer => "New server",
Remedy::RestoreFront => "Restore front from backup",
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct BoxView {
pub role: String,
pub heard_unix_ms: u64,
pub build: String,
pub health: BTreeMap<String, String>,
pub twin_review: String,
pub acks: Vec<AckView>,
pub laid_ipv4: String,
pub at_provider: String,
}
impl BoxView {
pub fn silent(&self, now_unix_ms: u64, silent_after_ms: u64) -> bool {
self.heard_unix_ms == 0 || now_unix_ms.saturating_sub(self.heard_unix_ms) > silent_after_ms
}
pub fn word(&self, key: &str) -> &str {
self.health.get(key).map(String::as_str).unwrap_or("")
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct AckView {
pub seq: u64,
pub kind: String,
pub ok: bool,
pub detail: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct EstateView {
pub product: String,
pub instance: String,
pub now_unix_ms: u64,
pub silent_after_ms: u64,
pub boxes: BTreeMap<String, BoxView>,
}
impl EstateView {
pub fn get(&self, role: &str) -> Option<&BoxView> {
self.boxes.get(role)
}
pub fn silent(&self, role: &str) -> bool {
self.boxes
.get(role)
.is_none_or(|b| b.silent(self.now_unix_ms, self.silent_after_ms))
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Finding {
pub code: String,
pub role: String,
pub words: String,
pub evidence: Vec<String>,
pub remedies: Vec<Remedy>,
pub auto: Option<Remedy>,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CloudStep {
Fence { role: String },
Lay { role: String },
Restore { role: String },
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Step {
Cloud(CloudStep),
Order {
role: String,
kind: String,
body: BTreeMap<String, String>,
},
Await {
role: String,
what: String,
within_ms: u64,
},
}
impl Step {
pub fn words(&self) -> String {
match self {
Step::Cloud(CloudStep::Fence { role }) => {
format!("fence {role} (server and disks given back, read back gone)")
}
Step::Cloud(CloudStep::Lay { role }) => format!("lay a new {role} (installer)"),
Step::Cloud(CloudStep::Restore { role }) => {
format!("restore {role} from its daily backup")
}
Step::Order { role, kind, .. } => format!("order {kind} → {role}"),
Step::Await { role, what, .. } => format!("await {role} {what}"),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum StepState {
Waiting(String),
Met(String),
Failed(String),
}
pub trait EstateGuardian: Send + Sync {
fn diagnose(&self, view: &EstateView) -> Vec<Finding>;
fn plan(&self, remedy: Remedy, view: &EstateView) -> Result<Vec<Step>, String>;
fn observe(&self, role: &str, what: &str, view: &EstateView) -> StepState;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_remedy_round_trips_its_wire_name() {
for r in Remedy::ALL {
assert_eq!(Remedy::parse(r.as_str()), Some(r));
}
assert_eq!(Remedy::parse("rollback"), None);
assert_eq!(serde_json_like(Remedy::NewTwin), "new_twin");
}
fn serde_json_like(r: Remedy) -> &'static str {
r.as_str()
}
#[test]
fn a_box_never_heard_is_silent_and_a_fresh_one_is_not() {
let mut v = EstateView {
now_unix_ms: 100_000,
silent_after_ms: 30_000,
..Default::default()
};
assert!(v.silent("twin"));
v.boxes.insert(
"twin".into(),
BoxView {
role: "twin".into(),
heard_unix_ms: 90_000,
..Default::default()
},
);
assert!(!v.silent("twin"));
v.now_unix_ms = 121_000;
assert!(v.silent("twin"));
}
}