Skip to main content

isb_server/servers/
store.rs

1//! What the control plane keeps about its servers and where orgs live:
2//! `<state>/servers/servers.json` and `<state>/servers/placement.json`
3//! (0600). Routing metadata only: no workload state, no secrets.
4
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use serde::de::DeserializeOwned;
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12use crate::org::OrgId;
13
14/// One server the control plane places orgs on.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ServerRecord {
17    pub name: String,
18    /// What the control plane dials (and the agent certificate's SAN).
19    pub address: String,
20    /// The agent's mTLS port.
21    pub port: u16,
22    /// `user@host` it was bootstrapped through (the key is never kept).
23    pub ssh: String,
24    pub ssh_port: u16,
25    /// Unix seconds.
26    pub added_at: u64,
27    /// SHA-256 of the agent's current certificate.
28    pub fingerprint: String,
29    /// When that certificate expires (unix seconds), if known.
30    #[serde(default)]
31    pub cert_not_after: Option<u64>,
32    /// The isb version installed at bootstrap.
33    pub isb_version: String,
34    /// Who the agent's firewall lets reach its port (empty: not managed).
35    #[serde(default)]
36    pub allow_from: Vec<String>,
37    /// Set when this control plane made the server itself: a dedicated VM
38    /// on its own host for one org.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub vm: Option<VmRecord>,
41}
42
43/// A dedicated VM this control plane runs a server in.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct VmRecord {
46    /// The org it was made for.
47    pub org: OrgId,
48    /// The incus project and instance on the control plane's host.
49    pub project: String,
50    pub instance: String,
51    pub cpus: u32,
52    pub memory: String,
53    pub disk: String,
54}
55
56fn read<T: DeserializeOwned + Default>(p: &Path) -> Result<T> {
57    match std::fs::read(p) {
58        Ok(b) => {
59            serde_json::from_slice(&b).map_err(|e| Error::invalid(format!("{}: {e}", p.display())))
60        }
61        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(T::default()),
62        Err(e) => Err(e.into()),
63    }
64}
65
66fn write<T: Serialize>(p: &Path, v: &T) -> Result<()> {
67    super::pki::write_private(p, &serde_json::to_string_pretty(v)?)
68}
69
70/// The servers and the placement, write-through.
71#[derive(Debug)]
72pub struct Store {
73    dir: PathBuf,
74    pub servers: BTreeMap<String, ServerRecord>,
75    pub placement: BTreeMap<OrgId, String>,
76}
77
78impl Store {
79    pub fn open(dir: &Path) -> Result<Store> {
80        std::fs::create_dir_all(dir)?;
81        Ok(Store {
82            dir: dir.to_path_buf(),
83            servers: read(&dir.join("servers.json"))?,
84            placement: read(&dir.join("placement.json"))?,
85        })
86    }
87
88    pub fn save(&self) -> Result<()> {
89        write(&self.dir.join("servers.json"), &self.servers)?;
90        write(&self.dir.join("placement.json"), &self.placement)
91    }
92
93    /// The orgs placed on `server`.
94    pub fn orgs_on(&self, server: &str) -> Vec<OrgId> {
95        self.placement
96            .iter()
97            .filter(|(_, s)| s.as_str() == server)
98            .map(|(o, _)| o.clone())
99            .collect()
100    }
101}
102
103/// An agent's own list of the orgs its control plane placed on it:
104/// `<state>/agent/orgs.json`. Calls for any other org are refused.
105#[derive(Debug)]
106pub struct AgentOrgs {
107    path: PathBuf,
108    orgs: std::sync::Mutex<Vec<OrgId>>,
109}
110
111impl AgentOrgs {
112    pub fn open(state_dir: &Path) -> Result<AgentOrgs> {
113        let dir = state_dir.join("agent");
114        std::fs::create_dir_all(&dir)?;
115        let path = dir.join("orgs.json");
116        let orgs: Vec<OrgId> = read(&path)?;
117        Ok(AgentOrgs {
118            path,
119            orgs: std::sync::Mutex::new(orgs),
120        })
121    }
122
123    pub fn contains(&self, o: &OrgId) -> bool {
124        self.orgs.lock().unwrap().contains(o)
125    }
126
127    pub fn list(&self) -> Vec<OrgId> {
128        self.orgs.lock().unwrap().clone()
129    }
130
131    pub fn set(&self, o: &OrgId, placed: bool) -> Result<()> {
132        let mut v = self.orgs.lock().unwrap();
133        v.retain(|x| x != o);
134        if placed {
135            v.push(o.clone());
136            v.sort();
137        }
138        write(&self.path, &*v)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn store_and_agent_orgs_persist() {
148        let d = tempfile::tempdir().unwrap();
149        let mut s = Store::open(d.path()).unwrap();
150        s.placement
151            .insert(OrgId::new("acme").unwrap(), "box".into());
152        s.save().unwrap();
153        let s = Store::open(d.path()).unwrap();
154        assert_eq!(s.orgs_on("box"), vec![OrgId::new("acme").unwrap()]);
155        assert!(s.orgs_on("other").is_empty());
156
157        let a = AgentOrgs::open(d.path()).unwrap();
158        let acme = OrgId::new("acme").unwrap();
159        a.set(&acme, true).unwrap();
160        assert!(AgentOrgs::open(d.path()).unwrap().contains(&acme));
161        a.set(&acme, false).unwrap();
162        assert!(!AgentOrgs::open(d.path()).unwrap().contains(&acme));
163    }
164}