1use crate::admitted;
8use planarian_schema::{DeviceInfo, DriverRef, InventoryBackend, OpId, Outcome, Resources, UnitId};
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11
12pub type DeviceMap = BTreeMap<String, PathBuf>;
14
15#[derive(Clone, Debug)]
16pub struct Sysfs {
17 devices: DeviceMap,
18 txn_dir: PathBuf,
19 root: PathBuf,
21}
22
23pub fn bus_and_id(dev: &Path) -> Option<(String, String)> {
25 let canonical = std::fs::canonicalize(dev).unwrap_or_else(|_| dev.to_path_buf());
26 let id = canonical.file_name()?.to_string_lossy().into_owned();
27 let subsystem =
28 std::fs::read_link(canonical.join("subsystem")).ok().or_else(|| std::fs::read_link(dev.join("subsystem")).ok());
29 let bus = match subsystem {
30 Some(s) => s.file_name()?.to_string_lossy().into_owned(),
31 None => {
32 let parts: Vec<String> = dev.components().map(|c| c.as_os_str().to_string_lossy().into_owned()).collect();
34 let i = parts.iter().position(|p| p == "bus")?;
35 parts.get(i + 1)?.clone()
36 }
37 };
38 Some((bus, id))
39}
40
41impl Sysfs {
42 pub fn new(devices: DeviceMap, txn_dir: PathBuf) -> Sysfs {
43 Sysfs { devices, txn_dir, root: "/sys".into() }
44 }
45
46 #[cfg(test)]
47 fn at(root: &Path, devices: DeviceMap, txn_dir: PathBuf) -> Sysfs {
48 Sysfs { devices, txn_dir, root: root.into() }
49 }
50
51 fn path(&self, device: &UnitId) -> Option<&PathBuf> {
52 self.devices.get(&device.name)
53 }
54
55 fn driver_of(dev: &Path) -> Option<DriverRef> {
56 let link = std::fs::read_link(dev.join("driver")).ok()?;
57 Some(DriverRef { name: link.file_name()?.to_string_lossy().into_owned(), module: None })
58 }
59
60 fn write(path: &Path, value: &str) -> Outcome {
61 match std::fs::write(path, value) {
62 Ok(()) => Outcome::Succeeded,
63 Err(e) => Outcome::Failed(format!("{}: {e}", path.display())),
64 }
65 }
66}
67
68fn meminfo() -> (u64, u64) {
69 let text = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
70 let get = |k: &str| {
71 text.lines()
72 .find(|l| l.starts_with(k))
73 .and_then(|l| l.split_whitespace().nth(1))
74 .and_then(|v| v.parse::<u64>().ok())
75 .unwrap_or(0)
76 / 1024
77 };
78 (get("MemTotal:"), get("MemAvailable:"))
79}
80
81impl InventoryBackend for Sysfs {
82 fn devices(&self) -> Result<Vec<DeviceInfo>, Outcome> {
83 Ok(self
84 .devices
85 .iter()
86 .filter(|(_, p)| p.exists())
87 .map(|(name, p)| DeviceInfo { id: UnitId::device(name), driver: Self::driver_of(p) })
88 .collect())
89 }
90
91 fn resources(&self) -> Result<Resources, Outcome> {
92 let (total, available) = meminfo();
93 Ok(Resources { mem_total_mb: total, mem_available_mb: available, fs_free_bytes: BTreeMap::new() })
94 }
95
96 fn bound_driver(&self, device: &UnitId) -> Result<Option<DriverRef>, Outcome> {
97 let p = self.path(device).ok_or_else(|| Outcome::Failed(format!("{device} has no sysfs path")))?;
98 Ok(Self::driver_of(p))
99 }
100
101 fn bind(&self, device: &UnitId, driver: &DriverRef, op: &OpId) -> Outcome {
102 if !admitted(&self.txn_dir, op) {
103 return Outcome::fenced();
104 }
105 let Some(p) = self.path(device) else { return Outcome::Failed(format!("{device} has no sysfs path")) };
106 let Some((bus, id)) = bus_and_id(p) else {
107 return Outcome::Failed(format!("cannot tell the bus of {}", p.display()));
108 };
109 match Self::driver_of(p) {
110 Some(d) if d.name == driver.name => return Outcome::Succeeded,
111 Some(_) => {
113 if let Outcome::Failed(e) = Self::write(&p.join("driver/unbind"), &id) {
114 return Outcome::Failed(e);
115 }
116 }
117 None => {}
118 }
119 if let Some(m) = &driver.module {
120 let o = crate::cmd::run(&["modprobe", "-d", &m.0, &driver.name], std::time::Duration::from_secs(30));
121 if !o.ok() {
122 return Outcome::Failed(o.tail());
123 }
124 }
125 let ov = p.join("driver_override");
130 let overriding = ov.exists();
131 if overriding && let Outcome::Failed(e) = Self::write(&ov, &driver.name) {
132 return Outcome::Failed(e);
133 }
134 let o = Self::write(&self.root.join(format!("bus/{bus}/drivers/{}/bind", driver.name)), &id);
135 if overriding {
136 let _ = Self::write(&ov, "\n");
137 }
138 o
139 }
140
141 fn unbind(&self, device: &UnitId, op: &OpId) -> Outcome {
142 if !admitted(&self.txn_dir, op) {
143 return Outcome::fenced();
144 }
145 let Some(p) = self.path(device) else { return Outcome::Failed(format!("{device} has no sysfs path")) };
146 if Self::driver_of(p).is_none() {
147 return Outcome::Succeeded;
148 }
149 let Some((_, id)) = bus_and_id(p) else { return Outcome::Failed("cannot tell the device id".into()) };
150 Self::write(&p.join("driver/unbind"), &id)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn bus_from_path() {
160 assert_eq!(
161 bus_and_id(Path::new("/sys/bus/pci/devices/0000:00:1b.0")),
162 Some(("pci".into(), "0000:00:1b.0".into()))
163 );
164 }
165
166 #[test]
167 fn bind_overrides_the_driver_match_and_clears_it() {
168 let d = tempfile::tempdir().unwrap();
169 let dev = d.path().join("bus/pci/devices/0000:00:1b.0");
170 std::fs::create_dir_all(&dev).unwrap();
171 std::fs::write(dev.join("driver_override"), "(null)\n").unwrap();
172 let drv = d.path().join("bus/pci/drivers/vfio-pci");
173 std::fs::create_dir_all(&drv).unwrap();
174 let held = d.path().join("bus/pci/drivers/snd_hda_intel");
175 std::fs::create_dir_all(&held).unwrap();
176 std::os::unix::fs::symlink(&held, dev.join("driver")).unwrap();
177 let txn = d.path().join("txn");
178 std::fs::create_dir_all(txn.join("7")).unwrap();
179 std::fs::write(txn.join("7/lease"), "1").unwrap();
180 let s = Sysfs::at(d.path(), [("snd-hda0".to_string(), dev.clone())].into(), txn);
181 let op = OpId { txn: planarian_schema::TxnId(7), epoch: 1, seq: 1 };
182 let o = s.bind(&UnitId::device("snd-hda0"), &DriverRef { name: "vfio-pci".into(), module: None }, &op);
183 assert_eq!(o, Outcome::Succeeded);
184 assert_eq!(std::fs::read_to_string(held.join("unbind")).unwrap(), "0000:00:1b.0", "the holder lets go first");
185 assert_eq!(std::fs::read_to_string(drv.join("bind")).unwrap(), "0000:00:1b.0");
186 assert_eq!(std::fs::read_to_string(dev.join("driver_override")).unwrap(), "\n", "cleared after binding");
187 }
188
189 #[test]
190 fn driver_and_presence_from_a_fake_tree() {
191 let d = tempfile::tempdir().unwrap();
192 let dev = d.path().join("bus/pci/devices/0000:00:1b.0");
193 std::fs::create_dir_all(&dev).unwrap();
194 std::fs::create_dir_all(d.path().join("bus/pci/drivers/snd_hda_intel")).unwrap();
195 std::os::unix::fs::symlink(d.path().join("bus/pci/drivers/snd_hda_intel"), dev.join("driver")).unwrap();
196 let s = Sysfs::new(
197 [("snd-hda0".to_string(), dev.clone()), ("gone".to_string(), d.path().join("nope"))].into(),
198 d.path().into(),
199 );
200 let devs = s.devices().unwrap();
201 assert_eq!(devs.len(), 1);
202 assert_eq!(devs[0].driver.as_ref().unwrap().name, "snd_hda_intel");
203 }
204}